The course stands on three important pillars:
- Programmer skills: What does it take to write a working program?
- HTTP: How do you develop backend logic for web based systems?
- JDBC/JPA: How do a Java program use a database?
This summary shows all the essential knowledge you should master at the end of the course.
If you are not comfortable with any of the following words, please ask in class or during the exercise working lessons:
- variable
- method
- parameter
- argument
- parse
- protocol
- inheritance, superclass, subclass
- override
- overload
These are the programming skills you should have mastered at the end of the semester:
- Test-driven development
- See the test fail before implementing it
- Make it pass with as little code as possible
- Refactor the code before adding the next test
- Pair programming and teamwork
- Ping-pong programming
- Remove pair programming
- Continuous integration with Github Actions
- Debugging
- Starting IntelliJ in debugger
- Pausing debugger when stuck
- Adding breakpoints
- Examine program state
- Programming discipline
- Making something small work and expending it
- What happens when you program tired
- Giving and receiving feedback
// It's good practice for all your Java classes to live in a `package`.
// This means that the "full name" of this class is "no.kristiania.HelloThere"
package no.kristiania;
// Import: Makes `Scanner` as an alias for `java.util.Scanner`
import java.util.Scanner;
public class HelloThere {
// A method with the name `main` that is "public static void" is a program entry-point
public static void main(String[] args) {
// A local variable named Scanner
// This will be used to read input from the user
Scanner scanner = new Scanner(System.in);
// Write output to the user
System.out.println("What is your name?");
// Read a response from the user
String name = scanner.nextLine();
System.out.println("What is your age?");
// Read a number from the user
int age = scanner.nextInt();
// Format and print out a response to the user
System.out.printf("Hello %s, you are %d years old\n", name, age);
}
}
// It's good practice for all your Java classes to live in a `package`.
// This means that the "full name" of this class is "no.kristiania.http.QueryString"
package no.kristiania.http;
// Import: Creates `Map` as an alias for `java.util.Map`
import java.util.Map;
public class QueryString {
// A method with the name `main` that is "public static void" is a program entry-point
// This can be started with `java <classname> <arg1> <arg2>`.
// The `String[] args` parameter is assigned to <arg1>, <arg2> etc
public static void main(String[] args) {
// An `if` check. The statements within `{}` is
if (args.length == 0) {
System.out.println("Usage: 'java no.kristiania.http.QueryString <queryString>'." +
"For example 'java no.kristiania.http.QueryString status=302&location=http://example.com'");
System.exit(1); // Exits the program
}
QueryString queryString = new QueryString(args[0] /* constructor argument*/);
System.out.println(queryString.getParameter("status"));
}
// Instance field (or "instance variable")
// A map is an association from keys to values,
// e.g. we could have a map where status => 302 and loction => example.com
private Map<String, String> parameters =
new java.util.HashMap<>(); // Fully qualified class name
// Constructor
public QueryString(String queryString /* constructor parameter */) {
parse(queryString); // Calls an "instance method" named `parse`
}
// Instance method
private void parse(String queryString /* method parameter */) {
// Local variable - can only be used with the method
String[] parameters = queryString.split("&");
// "foreach" loop: `parameter` is assigned to each value in `parameters` in turn
// and the code within the {}-block is executed for each value
for (String parameter : parameters) {
// Block local variable. Can only by used within the `for {}` block
int equalsPos = parameter.indexOf('='); // Find the location (index)
// of `=` within parameter.
// E.g. "status=302".indexOf('=') => 6
// Extract the substring from first to second argument.
// E.g. "status=302".substring(0,6) => "status"
String parameterName = parameter.substring(0, equalsPos);
// Extract the substring from argument to end.
// E.g. "status=302".substring(7) => "302"
String parameterValue = parameter.substring(equalsPos+1);
// Use "this" to avoid confusion with the local variable with the same name
this.parameters.put(parameterName, parameterValue);
}
}
// A public method can be from any other Java class
public String getParameter(String name) {
return parameters.get(name);
}
}
package no.kristiania.http;
// Use the JUnit 5 test framework
import org.junit.jupiter.api.Test;
// use the static method `assertequals` in the code
import static org.junit.jupiter.api.assertions.assertequals;
// Test class - groups test methods together
class HttpClientTest {
// Test method - must be annotated with "@Test"
@Test
void shouldReturnStatusCode() {
// Executes some production code
HttpClient httpClient = new HttpClient("httpbin.org", 80, "/html");
HttpResponse httpResponse = httpClient.getResponse();
// "assertions" verify the result of the production code
assertEquals(200, httpResponse.getStatusCode());
}
}
"Ping pong programming" is a technique for practicing co-working in pair programming and test-driven development. it's provides a useful rhythm of changing who is in control, helping both programmers stay engaged and focused. Here is how it works:
- The first programmer writes a test, but only enough of the test to run with a failure. Make sure the test fails and then pass the keyboard to her partner
- The second programmer makes the test pass with as little thought as possible, then refactors the solution to express intent better (but without adding functionality). After making the test pass, she writes another test and passes the keyboard back to the first programmer
- The first programmer makes the second pass, refactors the code and writes a new failing test
- This pattern repeats: When a programmer receives the keyboard the last test is failing. The programmer makes the test pass, refactors the code and writes another failing test before passing the keyboard back
- Continue until the problem is solved
As remote working has become a more frequent constraint, it's important to be able to practice pair programming and co-working in a remote setting. We are developing the following procedure to attempt to improve the remote pair programming experience. This approach gives each programmer the possibility to work on their own computer while sharing the screen and synchronizes the code through git without cluttering the normal commit history.
- The programmers join a voice channel on Discord
- Programmer #1 creates a new Java project in IntelliJ and shares the IntelliJ window on Discord. Import the project into Github (VCS > Import into Version Control > Share Project on Github)
- Programmer #2 imports the github project in IntelliJ and shares the IntelliJ window on Discord (import either from the initial IntelliJ window or from File > New > New from Version Control). Create a new branch (under alt-9) named dev- and create a test class with a failing test method. Run the test to make sure it fails (alt-shift-f10 / alt-shift-r) and commit (ctrl-k) and push the project (ctrl-shift-k)
- Programmer #1 shares pulls (ctrl-t / cmd-t) and runs the tests to make sure it runs and fails (alt-shift-f10 / alt-shift-r). Make the test pass and run it again (shift-f10 / ctrl-r). Refactor the code (ctrl-alt-shift-t / ctrl-t). Write a new failing test and run it to make sure it fails as expected (shift-f10 / ctrl-r). Commit (ctrl-k) and push (ctrl-alt-k)
- Programmer #2 does the same: pull (ctrl-t), run to see fail (shift-f10), make pass, refactor (ctrl-alt-shift-t), write a new test, verify that it fails (shift-f10), commit (ctrl-k) and push (ctrl-alt-k)
- Repeat until the task is complete: pull, fix, refactor, new test, commit, push
- When complete, go to the project on github.com and merge the dev branch into the main branch by creating a "Pull request" and "Merge and Squash". This replaces the many small commits in the dev-branch with a single "squashed" commit on the main branch
Other remote pair programming approaches may also be valid, such as remote control of one programmers computer. However, most people find the keyboard lag too disturbing in these situations.
package no.kristiania.http;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
public class FileDemoProgram {
private String filename;
public FileDemoProgram(String filename) {
this.filename = filename;
}
public static void main(String[] args) throws IOException {
FileDemoProgram program = new FileDemoProgram("testfile.txt");
program.read();
program.readFirstLine();
}
private void read() throws IOException {
// `try (variable)` automatically calls `variable.close()` at the end of {}
// If a program fails to close files and other resources it uses, this means
// that other programs may be prevented from using these files and that the
// operating system may eventually stop the program for opening more files
try (InputStream inputStream = new FileInputStream(filename)) {
int c; // Each byte (0-255) from the file should be read in turn
while ((c = inputStream.read()) != -1) {
// -1 is the only value outside 0-255 that may be returned from `read()`.
// It means "end of file"
// Print out each byte, interpreted as a character ("ASCII" value).
System.out.print((char) c);
}
}
}
private void readFirstLine() throws IOException {
try (InputStream inputStream = new FileInputStream(filename)) {
// StringBuilder is used to gradually construct a String
StringBuilder line = new StringBuilder();
int c;
while ((c = inputStream.read()) != -1) {
// On Mac, Linux, \n (newline) is used for end of line,
// on Windows \r\n (carriage return, newline)
if (c == '\r' || c == '\n') {
// breaks out of the `while` loop, execution continues after while loop
break;
}
line.append((char)c); // Add each byte, interpreted as a char ("ASCII" value).
}
System.out.println(line);
}
}
}
package no.kristiania.http;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class HttpServer {
private final ServerSocket serverSocket;
public HttpServer(int port) throws IOException {
// new ServerSocket throws IOException if for example the port is already in use
// new ServerSocket opens a "port" for incoming connections on the network
// In this case, putting http://localhost:9080 in the web browser address bar
// will attach the browser to this server
this.serverSocket = new ServerSocket(port);
}
public static void main(String[] args) throws IOException {
new HttpServer(9080).start();
}
private void start() throws IOException {
// waits and blocks the current thread until a client tries to connect
// then returns a socket to the client
Socket socket = serverSocket.accept();
// What the server writes to socket.getOutputStream,
// the client can read from socket.getInputStream
socket.getOutputStream().write(("HTTP/1.1 200 OK\r\n" +
// "HTTP/1.1 .... Hello world" is a single string broken up over several lines
"Content-length: 12\r\n" +
// The HTTP protocol (RFC7230) requires each line to end with
// carriage return (\r) and linefeed (\n)
"Content-type: text/plain\r\n" +
// An HTTP header. Header name and header value are separated by ":"
"\r\n" + // A newline separates the headers from the content body
"Hello World!").getBytes());
// body should be the same number of bytes as the content-length header
}
}
package no.kristiania.http;
import java.io.IOException;
import java.net.Socket;
public class HttpClient {
private final String host;
private final int port;
private final String requestTarget;
public static void main(String[] args) throws IOException {
new HttpClient("urlecho.appspot.com", 80, "/echo").executeRequest();
}
public HttpClient(String host, int port, String requestTarget) {
this.host = host;
this.port = port;
this.requestTarget = requestTarget;
}
private void executeRequest() throws IOException {
// new Socket opens a connection to the server
Socket socket = new Socket(host, port);
// The outputstream of the client is connected to the input stream of the server
socket.getOutputStream().write(("GET " + requestTarget + " HTTP/1.1\r\n" +
"Host: " + host + "\r\n" +
"\r\n").getBytes());
// HTTP messages are specified in [RFC 7230](https://tools.ietf.org/html/rfc7230)
/*
HTTP-message = start-line
*( header-field CRLF )
CRLF
[ message-body ]
start-line = request-line / status-line
request-line = method SP request-target SP HTTP-version CRLF
status-line = HTTP-version SP status-code SP reason-phrase CRLF
header-field = field-name ":" OWS field-value OWS
*/
// As FileDemoProgram
int c;
while ((c = socket.getInputStream().read()) != -1) {
System.out.print((char)c);
}
}
}
- Installing PostgreSQL from https://www.postgresql.org
- Adding dependencies to
pom.xml
: h2, postgres, flyway
package no.kristiania.jdbc;
import org.flywaydb.core.Flyway;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.random;
import static org.assertj.core.api.Assertions.assertThat;
class ProductDaoTest {
private ProductDao productDao;
// This method is executed before each test method
@BeforeEach
void setUp() {
// A DataSource specifies HOW TO connect to a db: hostname, port, database name, password
JdbcDataSource dataSource = new JdbcDataSource();
dataSource.setURL("jdbc:h2:mem:product-test;DB_CLOSE_DELAY=-1");
// The meaning of the database URL:
// jdbc - this is always the first part
// h2 - connect to a H2 database (https://h2database.com)
// mem - the database is "in-memory". When the Java program terminates,
// everything will be forgotten
// product-test - database name
// ;.... - extra properties given to the connection
// DB_CLOSE_DELAY - wait this much time from last connection.close until database is
// discarded (-1 means wait until Java program terminates). Important because
// Flyway closes the connection after update
// Flyway makes sure that the database is always consistent with what we want
// It reads files from `src/main/resource` folder `db/migrate` with names
// like V001__create_table_orders.sql (notice two "_" after V001)
// and compares with what's already done in the database.
// Flyway will find files under db/migrate that haven't yet been applied yet
// and apply them
Flyway.configure().dataSource(dataSource).load().migrate();
productDao = new ProductDao(dataSource);
}
@Test
void shouldRetrieveSavedProduct() {
// This test uses random data. This ensures that we don't accidentally tie the test
// logic to specific values which can be hard to track down. It can occasionally
// also help us find corner case bugs
Product product = sampleProduct();
// Before we use this product, make sampleProduct worked correctly and set all fields
assertThat(product).hasNoNullFieldsOrPropertiesExcept("id");
// Save and retrieve the product
long id = productDao.insert(product);
assertThat(productDao.retrieve(id))
// Make sure everything matches on the returned product
.isEqualToComparingFieldByField(product);
}
@Test
void shouldListAllProduct() {
Product product1 = sampleProduct();
productDao.insert(product1);
Product product2 = sampleProduct();
productDao.insert(product2);
// List back all the objects in the database
assertThat(productDao.listAll())
// to make it easy, just consider the name property of the products
.extracting(Product::getName)
// ensure that they are both there
.contains(product1.getName(), product1.getName());
}
private Product sampleProduct() {
Product product = new Product();
product.setName(pickOne(new String[] { "apples", "bananas", "pears", "grapes" }));
return product;
}
private Random random = new Random();
private String pickOne(String[] alternatives) {
return alternatives[random.nextInt(alternatives.length)];
}
}
In IntelliJ Ultimate, there is a built-in database tool under View > Tool Windows > Database. After installing PostgreSQL from the PostgreSQL website, you can click +
> Data Source
> PostgreSQL
and define a datasource with Username postgres
and password you created when installing PostgreSQL. You can execute database commands in IntelliJ by selecting the text and pressing ctrl-enter
(cmd-enter
on Mac) to create a database for the application:
create user demo with password 'GENERATE A RANDOM PASSWORD HERE';
create database demo_app with owner demo;
Use for example https://www.random.org/passwords/ to generate a password
In IntelliJ, create a new data source with +
> Data Source
> PostgreSQL
with the new database user, username and database and create our test table:
create table PRODUCTS (
id serial primary key, -- SERIAL is an auto-incremented column in PostgreSQL
name varchar(200) not null,
price numeric
);
Run the command with ctrl-enter
. You should now be able to see the database table in IntelliJ's Database explorer view.
package no.kristiania.jdbc;
import org.postgresql.ds.PGSimpleDataSource;
import javax.sql.DataSource;
import java.io.FileReader;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
// DAO = Data Access Object: A class that can exchange data from Java object and the database
public class ProductDao {
// A DataSource specifies HOW TO connect to a db: hostname, port, database name, password
private DataSource dataSource;
public ProductDao(DataSource dataSource) {
this.dataSource = dataSource;
}
public static void main(String[] args)
// If we can't read the configuration or connect to the database,
// it's okay that main throws the exception and the program thus terminates
throws IOException, SQLException
{
PGSimpleDataSource dataSource = new PGSimpleDataSource();
// database URL specifies the location of the database, exact format varies with db
// jdbc - this is always the first part
// postgres - connect to a PostgreSQL database
// localhost - the database is on this machine
// 5432 - port number (this is standard for PostgreSQL)
// demo - database name
dataSource.setUrl("jdbc:postgres://localhost:5432/demo");
// Database connections must specify a database user
dataSource.setUser("demo_user");
Properties properties = new Properties();
properties.load(new FileReader("demo.properties"));
// Never store database passwords in the source code or in git! They are the master
// key to your most valauble asset: You data
dataSource.setPassword(properties.getProperty("dataSource.password"));
ProductDao productDao = new ProductDao(dataSource);
// Create a "domain object" or an "entity": An object of the type our users care about
Product product = new Product();
product.setName("apples");
long id = productDao.insertProduct(product);
System.out.println("New row id: " + id);
}
private long insertProduct(Product entity) throws SQLException {
// Use the dataSource to open a connection (network socket, actually) to the database
// A database will limit the number of concurrent connections, so they should be closed
// try () - calls connection.close() when block is existed
try (Connection connection = dataSource.getConnection()) {
String sql = "insert into products (name) values (?)";
// (Some dbs may keep connections open after close() is there are unclosed statements)
try ( PreparedStatement stmt = connection.prepareStatement(
sql,
PreparedStatement.RETURN_GENERATED_KEYS
)) {
stmt.setString(1, entity.getName());
stmt.executeUpdate();
// This is available when RETURN_GENERATED_KEYS was used above
ResultSet generatedKeys = stmt.getGeneratedKeys();
// Some operations may generate more than one row. Then we would need
// while (generatedKeys.next()) to retrieve them all
generatedKeys.next();
// Returns the value of the "id" column in the new row
return generatedKeys.getLong("id");
}
}
}
}
public class orderdao {
private DataSource dataSource;
public OrderDao(DataSource dataSource) {
this.dataSource = dataSource;
}
private List<Order> listAll(LocalDate orderDate) throws SQLException {
// Connect to the database specified the datasource
// try() calls connection.close() when block exits (even because of Exception)
try (Connection connection = dataSource.getConnection()) {
// start working with an sql statement; stmt.close() when block exits
try (PreparedStatement stmt = connection.prepareStatement(
"select * from orders where order_date = ?"
)) {
// Specify value of sql statement '?'
stmt.setDate(1, Date.valueOf(orderDate));
// Query for the result
try (ResultSet rs = stmt.executeQuery()) {
List<Order> orders = new ArrayList<>();
// Process each row until there's no next() row (so we're at the end)
while (rs.next()) {
// Create an entity for the row
Order order = new Order();
// read values from the current row into the entity
order.setId(rs.getLong("id"));
order.setCustomerEmail(rs.getString("customer_email"));
orders.add(order);
}
// Return all the collected entities
return orders;
}
}
}
}
}
These are some of the most versatile keyboard shortcuts in IntelliJ. There are many more, but learning these 12 will really speed up your code
Shortcut (Windows) | Shortcut (Mac) | Command |
---|---|---|
alt-enter | opt-enter | Show content action (quick fix) |
ctrl-alt-shift-t | ctrl-t | Refactor this (show refactor menu) |
alt-insert | cmd-n | New... (add some content) |
ctrl-w | opt-up | Expand selection |
shift-alt-f10 | ctrl-alt-r | Run.... |
shift-alt-f9 | ctrl-alt-d | Debug.... |
shift-f10 | ctrl-d | Rerun last.... |
ctrl-b | cmd-b | Navigate to symbol |
alt-j | ctrl-g | Add next match to selection (multi-cursor) |
shift-ctrl-backspace | shift-cmd-backspace | Goto last edit location |
shift, shift | shift, shift | Search anywhere |
Make yourself familiar with Refactor this
(ctrl-alt-shift-t / ctrl-t) and use it to learn the shortcut keys for your favorite refactorings like Extract method, Rename and Inline.
Less used than the shortcuts, these shorthand ways of writing common bits of Java code can save a bit of time. Write the name of the code template in the right spot and press Tab to have IntelliJ expand it
Template | Result |
---|---|
fori |
for (int i=0; i<...; i++) {} |
main |
public static void main(String[] args) { |
sout |
System.out.println(); |
Command | Description | IntelliJ shortcut |
---|---|---|
git init |
Creates a new local git repo in .git/ |
VCS > Import into version control |
git add |
Stage files to include in next commit | (not needed) |
git commit |
Store your local changes in git history | ctrl-k / cmd-k |
git push |
Upload changes to remote repo (github) | ctrl-sh-k / cmd-sh-k |
git clone |
Create a local copy from remote (github) | File > New > Project from version control |
git pull |
Update local copy with others' changes | ctrl-t / cmd-t |
git log |
View change history | View > Tool Windows > Version control |
<!-- Always the same, just needed to make Maven happy -->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- What are the "coordinates" of this project: -->
<!-- To publish an open source project, you must control the domain referred in groupId -->
<groupId>no.kristiania.demo</groupId>
<!-- The name of the project goes in artifactId -->
<artifactId>demo</artifactId>
<!-- Version numbers -->
<version>1.0-SNAPSHOT</version>
<!-- Dependencies are libraries made by others that you use -->
<dependencies>
<dependency>
<!-- org.junit.jupiter:junit-jupiter is our test framework -->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.2</version>
<!-- make available to code under `src/test/java`, but not `src/main/java` -->
<scope>test</scope>
</dependency>
<dependency>
<!-- org.postgresql:postgresql lets Java connect to PostgreSQL -->
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.2.8</version>
<!-- make available both to code under `src/test/java` and `src/main/java` -->
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<!-- plugins control what maven does when you run e.g. `mvn test` -->
<!-- The code in a plugin is NOT available to your code -->
<plugins>
<plugin>
<!-- Used to compile Java code under src/main/java and src/test/java -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
</configuration>
</plugin>
<plugin>
<!-- Used to run tests -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.22.2</version>
</plugin>
</plugins>
</build>
</project>
clean
: Deletes everything under /targetcompile
: Compilessrc/main/java
intotarget/classes
test-compile
: Compilessrc/test/java
intotarget/test-classes
(runscompile
)test
: Runs tests fromtarget/test-classes
(runstest-compile
first)package
: Packagestarget/classes
intotarget/<projectname>.jar
(runstest
first)
org.junit.jupiter:junit
(scope: test): the test frameworkorg.postgresql:postgresql
(scope: compile): needed to connect to PostgreSQLorg.flywaydb:flyway-core
(scope: compile): reads database migrations fromsrc/main/resources/db/migration
and applies those that have not yet been run to the databaseorg.assertj:assertj-core
(scope: test): makes tests more readable and powerful
maven-compiler-plugin
: Used to compile Java code. Specify<configuration><release>11</release></configuration>
to work with Java 11maven-surefire-plugin
: Used to run tests. (Needed for JUnit 5, but not JUnit 4)maven-shade-plugin
: Used to make the jar-file executable withjava -jar <jarfile>
Learn more about Github Actions: Github Actions Tutorial on YouTube
Example of file checked in as .github/workflows/maven.yml
name: Java CI with Maven
# When the workflow should be run. In this case, whenever someone
# pushes code on the main-branch or creates a pull request to the
# main branch
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
# standard - specifies what operating system should run the Action
runs-on: ubuntu-latest
# IMPORTANT: Timeout if your build hangs. If you fail to add this
# you can impact the rest of the class, as we share "build minutes"
# on Github
timeout-minutes: 5
# How should we build?
steps:
# First - check out the code
- uses: actions/checkout@v2
# Then, make sure Java is available
- name: Set up JDK 11
uses: actions/setup-java@v1
with:
java-version: 11
# Then, build with maven (but continue if the build fails)
- name: Build with Maven
run: mvn test --batch-mode -Dmaven.test.failure.ignore=true
# The publish the report from the test
- name: Publish Test Report
uses: scacap/action-surefire-report@v1
with:
# This authenticates the build step (scacap/action-surefire-report) with your GitHub account
# So it can publish the test report
github_token: ${{ secrets.GITHUB_TOKEN }}
To create a runnable web server, include jetty-webapp as a Maven dependency and from you main class, start the Jetty Server class with a WebAppContext
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-webapp</artifactId>
<version>11.0.12</version>
</dependency>
public class AzureServer {
private final Server server;
public AzureServer(int port) {
server = new Server(port);
server.setHandler(createWebAppContext());
}
private WebAppContext createWebAppContext() {
var context = new WebAppContext();
context.setContextPath("/");
context.setBaseResource(Resource.newClassPathResource("/
-demo-webapp"));
return context;
}
public void start() throws Exception {
server.start();
}
public URL getURL() throws MalformedURLException {
return server.getURI().toURL();
}
public static void main(String[] args) throws Exception {
var port = Optional.ofNullable(System.getenv("HTTP_PLATFORM_PORT"))
.map(Integer::parseInt)
.orElse(8080);
new AzureServer(port).start();
}
}
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<archive>
<manifest>
<mainClass>no.kristiania.AzureServer</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.4.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
<!-- TODO: This must be extended later -->
</plugin>
To start working with Azure, you have to log in at https://portal.azure.com and sign up for student benefits.
To deploy to Azure, run mvn com.microsoft.azure:azure-webapp-maven-plugin:2.7.0:config
to create an initial configuration in pom.xml
. You should modify this somewhat:
<plugin>
<groupId>com.microsoft.azure</groupId>
<artifactId>azure-webapp-maven-plugin</artifactId>
<version>2.7.0</version>
<configuration>
<schemaVersion>v2</schemaVersion>
<resourceGroup>${project.groupId}</resourceGroup>
<appName>${project.artifactId}-${user.name}</appName>
<pricingTier>F1</pricingTier>
<region>westeurope</region>
<runtime>
<os>Windows</os>
<javaVersion>Java 17</javaVersion>
<webContainer>Java SE</webContainer>
</runtime>
<deployment>
<resources>
<resource>
<directory>${project.basedir}/target</directory>
<includes>
<include>${project.build.finalName}.jar</include>
</includes>
</resource>
<resource>
<type>static</type>
<directory>${project.basedir}/src/main/azure</directory>
<includes>
<include>*.properties</include>
</includes>
</resource>
</resources>
</deployment>
</configuration>
</plugin>
- Create a SQL database in the Azure Portal. Make sure to choose settings that brings the cost down to a minimal level
- Make sure that the database accepts connections from your IP-address and "Allow Azure services and resources to access this server"
- Make sure the database server uses username and password for authentication (not only Active Directory)
- Write down the username and password in a file named
application.properties
Add the following dependencies to your pom.xml
:
com.microsoft.sqlserver:mssql-jdbc:11.2.1.jre17
(Microsoft SQL Server JDBC driver)jakarta.persistence:jakarta.persistence-api:3.1.0
(JPA interface)org.eclipse.persistence:eclipselink:3.0.3
(Eclipselink JPA implementation)org.flywaydb:flyway-core:9.7.0
(Flyway: database migrations)org.flywaydb:flyway-sqlserver:9.7.0
(Flyway: database migrations - SQL Server variant)com.zaxxer:HikariCP:5.0.1
(Hikari Connection Pool - a flexible and robust DataSource implementation)org.eclipse.jetty:jetty-plus:11.0.12
(Jetty Plus - implementation of the JNDI API)
The following file defines how JPA should connect to the database and which classes JPA should recognize:
<persistence xmlns="https://jakarta.ee/xml/ns/persistence" version="3.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="https://jakarta.ee/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">
<persistence-unit name="library">
<non-jta-data-source>jdbc/dataSource</non-jta-data-source>
<class>no.kristiania.Book</class>
</persistence-unit>
</persistence>
Notice the reference to jdbc/dataSource - this needs to be defined by a JNDI (Java Naming and Directory Interface) resource, for which we have chosen Jetty as an implementation.
The following is used by Flyway to setup the database. As the application evolves, we can add additional Vxxx__xxx.sql files with new CREATE TABLE
, ALTER TABLE
or even DROP TABLE
statements.
CREATE TABLE books
(
id int identity primary key,
title varchar(100) not null,
author_name varchar(100) not null
);
@Entity
@Table(name = "books")
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Column(name = "author_name")
private String authorName;
// getters and setters omitted for brevity
}
public class Demo {
public static void main(String[] args) throws SQLException {
// Register the jdbc/dataSource name references as non-jta-data-source in persistence.xml
new org.eclipse.jetty.plus.jndi.Resource("jdbc/dataSource", createDataSource());
// Reads src/main/resources/META-INF/persistence.xml and returns the `library` resource
var entityManagerFactory = Persistence.createEntityManagerFactory("library");
var entityManager = entityManagerFactory.createEntityManager();
var books = entityManager.createQuery("select b from Book b").getResultList();
System.out.println(books);
}
public static DataSource createDataSource() throws IOException {
var props = new Properties();
// Read the database url, username and password from a properties file
try (var fileInputStream = new FileInputStream("application.properties")) {
props.load(fileInputStream);
}
var dataSource = new HikariDataSource();
dataSource.setJdbcUrl(props.getProperty("dataSource.url"));
dataSource.setUsername(props.getProperty("dataSource.username"));
dataSource.setPassword(props.getProperty("dataSource.password"));
dataSource.setConnectionTimeout(2000);
// Apply new files from src/main/resources/db/migration to the database
var flyway = Flyway.configure().dataSource(dataSource).load();
flyway.migrate();
return dataSource;
}
}
TODO:
- Create a React application
- Set output to src/main/resources
- Add frontend-maven-plugin