Java and Spring Boot
Learning objectives
- Sees how a simple web application could be implemented with Java and Spring Boot.
For working with the following example on your computer, you need to have the Java Development Kit (JDK) installed (Version 8 or newer). Java Development Kit can be downloaded from jdk.java.net. In addition, if you wish to work with web applications in Java more than just trying out the example, we strongly recommend retrieving an IDE such as Apache NetBeans. Support for building Java applications is available also for VSCode (see e.g. https://code.visualstudio.com/docs/java/extensions).
Spring -- or Spring Framework -- is one of the most popular frameworks for building Web Applications in Java. Spring projects are typically created using Spring Boot, which provides starter files for a range of web development objectives (e.g. basic web functionality, authentication, building APIs, ...) with working dependency versions.
Project templates for starting new Spring projects are created at spring initializr, which is a site that allows selecting e.g. the programming language, used Java version, used build tool, and the dependencies used for the project. For the application that we are building here, we create a Gradle project with Java as the language and Java 8 as the Java version. The following dependencies are selected:
- Spring Boot DevTools (application restarts on code change)
- Lombok (easy generation of constructors, getters, and setters)
- Spring Web (support for building web applications)
- Spring Data JPA (object-relational mapping functionality, database abstraction)
- PostgreSQL Driver (well, a driver for PostgreSQL)
Once the dependencies and the settings are entered on the spring initializr site, clicking the Generate button on the page downloads a zip file with a starter project. When extracted, the project template looks follows -- in the example below, we have created the project using the default package set as wsd.example
:
tree --dirsfirst
.
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src
│ ├── main
│ │ ├── java
│ │ │ └── wsd
│ │ │ └── example
│ │ │ └── WsdApplication.java
│ │ └── resources
│ │ └── application.properties
│ └── test
│ └── java
│ └── wsd
│ └── example
│ └── WsdApplicationTests.java
├── build.gradle
├── gradlew
├── gradlew.bat
├── HELP.md
└── settings.gradle
12 directories, 10 files
We wont go into the specifics of the folder structure -- it is the pattern that most Java applications created using Gradle or Maven follow.
The application is run using the gradlew
command (or gradlew.bat
in older Windows) with bootRun
as a parameter.
$ ./gradlew bootRun
On the first run, the command downloads Gradle (the build tool), the project dependencies, and then starts the application. In our case, starting the application means running the file WsdApplication.java
, which contains the "basic stuff" needed to run the application.
When we run the application, it crashes. When selecting the dependencies for the project, we also chose a database. At this point, as we have not configured the database, the application does not know which database to connect to.
***************************
APPLICATION FAILED TO START
***************************
Description:
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured.
Reason: Failed to determine a suitable driver class
Action:
Consider the following:
If you want an embedded database (H2, HSQL or Derby), please put it on the classpath.
If you have database settings to be loaded from a particular profile you may need to activate it (no profiles are currently active).
Project and database settings
Project settings are given in the application.properties
file, which resides in the folder src/main/resources
. In order to use PostgreSQL, we need to specify the correct driver name and the correct database credentials. Enter the following details to the application.properties
file, matching them with your database setup.
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://my-database-address.elephantsql.com:5432/
spring.datasource.username=my-username
spring.datasource.database=my-database
spring.datasource.password=my-password
Once the properties are saved, we can launch the application again.
./gradlew bootRun
// ...
--- [ restartedMain] o.s.b.web.embedded.netty.NettyWebServer : Netty started on port(s): 8080
--- [ restartedMain] DeferredRepositoryInitializationListener : Triggering deferred initialization of Spring Data repositories…
--- [ restartedMain] DeferredRepositoryInitializationListener : Spring Data repositories initialized!
--- [ restartedMain] wsd.example.WsdApplication : Started WsdApplication in 5.408 seconds (JVM running for 6.108)
// ...
Now, given that the database credentials and the name of the database driver class were correctly entered, the application starts. By default, Spring applications are launched on port 8080. Let's curl the address to see what's going on.
curl http://localhost:8080
{"timestamp":"(current timestamp)","path":"/","status":404,"error":"Not Found", // long message
The server at the address responds with a long message. Effectively, we see that there is no functionality yet. Let us create a controller that responds to requests.
Controller and request parameters
Requests made to the server are processed by the Spring Framework, which then forwards the requests to controllers. While in oak applications written in Deno, we used router middleware for explicitly mapping paths to functions, in Spring, much of configuration is done using annotations. Annotations are used for, e.g., specifying classes that respond to requests, specifying which methods handle requests made to specific paths, and specifying how to handle the return values of those methods.
Class-level annotation @Controller
defines a class as a controller. When the web application starts, it inspects classes within the project, and uses the annotations to add specific behavior to the classes. In the case of the @Controller
annotation, the class is considered as a class to which requests can be routed to.
Create a file called HelloController.java
to the path src/main/java/wsd/example
and add the following content to the file.
package wsd.example;
import org.springframework.stereotype.Controller;
@Controller
public class HelloController {
}
Methods used for handling requests are annotated with a mapping annotation, which specifies the method and the path of the request that a particular method should handle. For example, if a method has an annotation @GetMapping("/")
, then all GET requests to the root path of the application are handled by that method.
In the below example, we have added a method called hello
to the class HelloController
. All GET requests to the root path of the application are directed to the method hello
. At the current state, the method returns a string.
package wsd.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HelloController {
@GetMapping("/")
public String hello() {
return "Hello world!";
}
}
We need to define what to do with the return value. By default, Spring would look for a view template that has the name that the method returns -- we won't go into the details of language- or framework-specific template libraries here, but wish to just return the returned value in the response body.
Let's add another annotation to specify that the returned value is the actual response to the request, and should be put into the response body. This is done using the @ResponseBody
annotation.
package wsd.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
@GetMapping("/")
@ResponseBody
public String hello() {
return "Hello world!";
}
}
Now, when we start the application and make a request to the root path of the server, we see the message Hello world!
as a response.
curl http://localhost:8080
Hello world!%
Perhaps unsurprisingly, annotations are also used to described what to do with e.g. the request body. In the example below, the root path of the application responds to both GET and POST requests. When handling a POST request, the request body is read and a value for a variable message
is sought for. If such a variable exists in the request, then the value of that variable is set as the value of the parameter message
of the setMessage
method.
The method setMessage
sets the value of the class attribute message
, and then responds with a redirect (i.e. using the POST/Redirect/GET pattern).
package wsd.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class HelloController {
private String message = "world";
@GetMapping("/")
@ResponseBody
public String hello() {
return "Hello " + message + "!";
}
@PostMapping("/")
public String setMessage(@RequestParam String message) {
this.message = message;
return "redirect:/";
}
}
Now, when we launch the application, we see that the server responds with a redirect request to POST requests made to the server. In addition, the data that we send to the server when making a POST request is used to change the message that we receive as a response to GET requests to the server.
curl --verbose --request POST --data "message=Spring" http://localhost:8080
// ...
< HTTP/1.1 302
< Location: http://localhost:8080/
// ...
curl http://localhost:8080
Hello Spring!%
Currently, the structure of the project is as follows (a build directory created by gradle has been removed in the output).
tree --dirsfirst
.
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src
│ ├── main
│ │ ├── java
│ │ │ └── wsd
│ │ │ └── example
│ │ │ ├── HelloController.java
│ │ │ └── WsdApplication.java
│ │ └── resources
│ │ └── application.properties
│ └── test
│ └── java
│ └── wsd
│ └── example
│ └── WsdApplicationTests.java
├── build.gradle
├── gradlew
├── gradlew.bat
├── HELP.md
└── settings.gradle
12 directories, 11 files
A simple API
Let's next create a simple API that can be used to add and read songs and their ratings using a database. For the database schema, we use the following one.
CREATE TABLE songs (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
rating INTEGER NOT NULL
);
When working with Spring and relational databases, it is quite common to use Object-relational Mapping (ORM). This refers to a technique where data in one form is transformed to data in another form -- in the case of working with Java, this mapping is done from the database query results to Java objects, and from Java objects to queries. Spring Data JPA, which is one of the dependencies that we added for the project, imports a handful of libraries used for working with databases using ORM -- in the case of Java, this often includes Hibernate.
Defining a class that is used to represent a database table is, perhaps unsuprisingly, done using annotations. A direct mapping to the above database table would be a class similar to the following one. The class name would almost correspond to the table name, and the attributes correspond to the table columns.
package wsd.example;
public class Song {
private Long id;
private String name;
private Integer rating;
}
To use a class as a representation of a database table, we use a class-level annotation @Entity
. In addition, we specify which table the class maps to using an annotation @Table
, which takes the table name as a parameter. For convenience, we also use a few annotations from Project Lombok to remove the need to create constructors and getters and setters.
Finally, we specify the id attribute as an id (in practice, a primary key), and specify how it should be generated. We wont go into the specifics here.
With the annotations in place, the class Song
looks as follows. Note that normally, when programming using Java and Spring Boot, the IDE effectively does much of the work for you, creating the imports, suggesting meaningful autocompletions, and so on.
package wsd.example;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
@Entity
@Table(name = "songs")
@Data @NoArgsConstructor @AllArgsConstructor
public class Song {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer rating;
}
When using an ORM, it is also possible to abstract away database queries. To do this in Spring Boot, we use an interface called JpaRepository
. The interface takes two type parameters -- the entity class and the type of the id. The interface JpaRepository
exposes plenty of methods for database queries, see the JpaRepository documentation for more detail.
In the example below, we have created a SongRepository
interface that extends the JpaRepository
interface. Our interface can then be used to query the database.
package wsd.example;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SongRepository extends JpaRepository<Song, Long> {
}
Starting to use an interface is done using injection functionality provided by the Spring framework. In practice, when we want to take an interface (or a class) that is handled by the Spring framework into use in another class, we ask that Spring would inject (or autowire) an instance of that class to our class. Then, when Spring injects an instance of that class, it also injects a lot of functionality, which -- i.e. the source code -- the developer practically never sees.
In the example below, we create an API controller that exposes an endpoint /songs
. GET requests to the endpoint are sent a list of songs as a response -- Spring transforms the list of objects to a JSON document automatically. A SongRepository instance is autowired to the project. We also changed the class-level annotation -- API controllers are typically created using an annotation @RestController
. This annotation effectively adds both the @Controller
annotation to the class as well as required @ResponseBody
(and @RequestBody
in the case of handling e.g. JSON in request body) to the methods.
package wsd.example;
import java.util.List;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
public class SongController {
@Autowired
private SongRepository songRepository;
@GetMapping("/songs")
public List<Song> getSongs() {
return songRepository.findAll();
}
}
Now, when we make a request to the path /songs
, we receive a list of songs as a response. In the example below, the database contains only one song.
curl http://localhost:8080/songs
[{"id":1,"name":"Songiti song song","rating":5}]%
Let's add a method for adding a song to the database. In the example below, if a POST request is made to the path /songs
, we ask Spring to map the body of the request to a new Song
object using the @RequestBody
annotation. Then, in the method, we save the newly created object to the database using the SongRepository
interface. The method responsible for handling the POST request is a void method as it does not return anything. In practice, Spring will then return a status code for the request.
package wsd.example;
import java.util.List;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
public class SongController {
@Autowired
private SongRepository songRepository;
@GetMapping("/songs")
public List<Song> getSongs() {
return songRepository.findAll();
}
@PostMapping("/songs")
public void addSong(@RequestBody Song song) {
songRepository.save(song);
}
}
Now, when we make a request to the server, sending a new song in JSON format, the song is added to the database.
curl --verbose --header "Content-Type: application/json" --request POST --data "{\"name\":\"Happy song\",\"rating\":5}" http://localhost:8080/songs
// ...
< HTTP/1.1 200
// ...
curl http://localhost:8080/songs
[{"id":1,"name":"Songiti song song","rating":5},{"id":2,"name":"Happy song","rating":5}]%
When the three files, Song.java
, SongRepository.java
, and SongController.java
have been added to the project, the folder structure is as follows.
tree --dirsfirst
.
├── gradle
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── src
│ ├── main
│ │ ├── java
│ │ │ └── wsd
│ │ │ └── example
│ │ │ ├── HelloController.java
│ │ │ ├── SongController.java
│ │ │ ├── Song.java
│ │ │ ├── SongRepository.java
│ │ │ └── WsdApplication.java
│ │ └── resources
│ │ └── application.properties
│ └── test
│ └── java
│ └── wsd
│ └── example
│ └── WsdApplicationTests.java
├── build.gradle
├── gradlew
├── gradlew.bat
├── HELP.md
└── settings.gradle
12 directories, 14 files
Brief summary
When starting to work with Spring Boot, we selected the programming language, Java version, the dependency and project management tool, and a set of dependencies using spring initializr. Then, when we started the project that we created, we added database configuration to the project. From here, we continued to first create a controller that responds to requests with a String, and then created an API that provided data from the database. For database access, we used an ORM, which also provided the means to abstract away much of the actual database functionality.
While we demonstrated the creation of an API, we intentionally chose a bit more verbose approach to show how content from the request is mapped to objects. If we would have chosen to add a dependency called Spring HATEOAS, at an extreme, we would have only needed the Song class as the dependency would have created an API based on it.
Here, and in the upcoming examples, we did not go into working with templates. For Spring, one quite commonly used template engine is Thymeleaf.
We briefly mentioned that developers would work in an IDE when working with Spring (or with Java). The cold restart time of a Spring application can be relatively long, and thus, when creating web applications with Spring, most developers rely on hot-reload functionality that can swap compiled classes in the running application. In addition, while the language is relatively verbose, IDEs provide good support for autocompletion and so on, which makes working with the web applications very fast, once one becomes accustomed to it.
As Spring runs on the Java Virtual Machine (JVM), it leverages the multithreading functionality provided by the JVM. One can use Spring either using the thread per request model, where each request is assigned its own thread, or with a non-blocking thread model similar to the one that Deno also uses. The non-blocking model can be taken into use by replacing the Spring Web
dependency with Spring Reactive Web
.
Deno and ORMs
Note that there are also ORMs for Deno such as DenoDB. For the purposes of the present course (and some other learning objectives at Aalto University), we have however chosen to use vanilla SQL.