Home Tutorials Training Consulting Products Books Company Donate Contact us









Online Training

Quick links

Share

Spring Boot is a rapid application development platform built on top of the popular Spring Framework.

1. Spring Boot

Spring Boot is an opinionated framework built on top the Spring Framework. You can find out more about the Spring framework and its modules in our Spring tutorial.

While Spring is very powerful and gives you many choices, it can also require a lot of manual configuration. Spring Boot aims to be highly productive by providing default configuration for most features while still giving you the ability to easily change the behavior according to your needs.

Spring Boot is mostly used to create web applications but can also be used for command line applications. A Spring Boot web application can be built to a stand-alone jar with an embedded web server that can be started with java -jar. Spring Boot makes it easy to integrate the various Spring modules into your application through starter POMs that contain all necessary dependencies, which get auto configured.

2. Example Spring Boot Application

We will create our first Spring Boot application with the Spring Tool Suite, a powerful editor based on the Eclipse IDE. You can download it from its Spring project website.

Once you have started the Spring Tool Suite click on File ▸ New ▸ Spring Starter Project to open the project creation wizard. For the purpose of this example we’ll choose a Gradle based project with the web starter dependency.

Spring Boot Project wizard page 1
Spring Boot Project wizard page 2

Alternatively you can create your project directly with the online wizard and import it into your favorite IDE.

Spring Boot web wizard

Three folders were automatically created src/main/java, src/main/resources and src/main/test. `src/main/java will be used to save all java source files, src/main/resources will be used for templates and any other files and src/main/test will be used for tests.

Open your project and create a new controller class named HelloWorldController.java in the com.vogella.example package of the src/main/java folder:

package com.vogella.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class HelloWorldController {

    @RequestMapping("/")
    @ResponseBody
    String index() {
        return "Hello, World!";
    }

}

Now start the class DemoApplication as a Spring Boot App. The embedded Tomcat server starts listening on port 8080. When you point your browser to http://localhost:8080 you should see the welcome message:

hello_world_message.png

3. Exercise - Configuring Spring Boot for web based applications

3.1. Target

In the following exercises an issue reporting tool is created. With this tool users can submit issues they found on a website.

3.2. Configure

First, there are a few dependencies that need to be added to the example project. Such as:

Thymeleaf

Thymeleaf is a powerful template processing engine for the Spring framework.

Spring Boot Devtools

The Spring Boot Devtools provides an ensemble of very useful tools that enhance the development experience a lot. Such as Automatic recompiling upon saving and much more.

Spring Data JPA

Spring Data JPA makes it easy to implement JPA based repositories and build Spring-powered applications that use data access technologies.

H2

H2 is a Java SQL database. It’s a lightweight database that can be run in-memory.

To add these dependencies to your project, just open the build.gradle file in the root folder of the project and add the following to the section 'dependencies'. Then Right Click > Gradle > Refresh Gradle Project.

    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    runtime('org.springframework.boot:spring-boot-devtools')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    runtime('com.h2database:h2')

3.3. Validate

Your build.gradle should now look like this

buildscript {
    ext {
        springBootVersion = '1.5.8.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

group = 'com.vogella.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}


dependencies {
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    compile('org.springframework.boot:spring-boot-starter-web')
    runtime('org.springframework.boot:spring-boot-devtools')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    runtime('com.h2database:h2')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

Start your application with Right Click on your Project > Run As > Spring Boot App. Since we have the dev-tools dependency added to the project we can use it’s live-reload functionality. Without any further configuration it reloads the application every time you save a file in the project.

4. Exercise - Creating a web @Controller

In src/main/java create a new package with the name com.vogella.example.controller. In there create a new java Class named IssueController. This class will contain the methods responsible for handling incoming web requests.

To tell Spring that this is a controller the class needs to have the @Controller annotation. Place it right on top of the class declaration and add import org.springframework.stereotype.Controller;

package com.vogella.example.controller;

import org.springframework.stereotype.Controller;

@Controller
public class IssueController {

}

4.1. Creating controller methods

In the IssueController class, create the following Methods:

    @GetMapping("/issuereport")
    @ResponseBody
    public String getReport() {
        return "issues/issuereport_form";
    }

This method later will return the base form template in which the user can submit the issue they found. Right now it only returns a string, the functionality will be added later.

The @GetMapping annotation above the method signals the Spring Core that this method should only handle GET requests.

Since this method is only be responsible for sending the required HTML files to the user, there should be another method:

    @PostMapping("/issuereport")
    @ResponseBody
    public String submitReport() {
        return "issues/issuereport_form";
    }

This method will be responsible for handling the user input after submitting the form. When the data is received and handled (e.g. added to the database), this method returns the same issuereport template from the first controller method.

The @PostMapping annotation signals that this method should only handle POST requests and thus only gets called when a POST request is received.

The next method will handle the HTML template for a list view in which all the requests can be viewed.

    @GetMapping("/issues")
    @ResponseBody
    public String getIssues() {
        return "issues/issuereport_list";
    }

This method will return a template with a list of all reports that were submitted.

The @ResponseBody annotation will be removed in a later step. For now we need to output just the text to the HTML page. If we would remove it now the framework would search for a template with the given name and since there is none would throw an error.

4.2. Validate

The IssueController should now look like this:

package com.vogella.example.controller;

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.ResponseBody;

@Controller
public class IssueController {
    @GetMapping("/issuereport")
    @ResponseBody
    public String getReport() {
        return "issues/issuereport_form";
    }
    @PostMapping("/issuereport")
    @ResponseBody
    public String submitReport() {
        return "issues/issuereport_form";
    }
    @GetMapping("/issues")
    @ResponseBody
    public String getIssues() {
        return "issues/issuereport_list";
    }
}

Since we use the dependency dev-tools of the SpringBoot framework the server already recompiled the code for us. We only need to refresh the page. If you navigate to localhost:8080/issuereport you should see the text issuereport_form.

5. Exercise - Creating an entity data class

5.1. Target

In this exercise we will create a data class that contains all the relevant information, essentially representing an issue report from a user.

5.2. Setup

Create a new class in the com.vogella.example package and name it IssueReport.

Add the @Entity annotation. This tells our JPA provider Hibernate that this class should be mapped to the database. Also set the database table name with the @Table(name = "issues") annotation. By explicitly setting the table name you avoid the possibility of accidently breaking the database mapping by renaming the class later on.

5.3. Adding fields to the entity data class

Add the following fields and a default constructor to the class:

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String email;
    private String url;
    private String description;
    private boolean markedAsPrivate;
    private boolean updates;
    private boolean done;
    private Date created;
    private Date updated;

    public IssueReport() {}

The values of the fields email, url, description, markedAsPrivate and updates will be coming from the form the user submitted. The others will be generated on creation or when the report is updated.

To let Spring instanciate the Issue object from the submitted html form we have to implement getters and setters, as Spring expects a valid Java Bean and won’t use reflection to set the fields. To automatically generate them Right Click in the source code window of the IssueReport class. Then select the Source sub-menu; from that menu selecting Generate Getters and Setters will cause a wizard window to appear. Select the fields (in this case all of them) that you want getters and setters to be generated for. Confirm with ok.

5.4. Validation

Your IssueReport class should look like this:

package com.vogella.example.entity;

import java.sql.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class IssueReport {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String email;
    private String url;
    private String description;
    private boolean markedAsPrivate;
    private boolean updates;
    private boolean done;
    private Date created;
    private Date updated;

    public IssueReport() {}

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isMarkedAsPrivate() {
        return markedAsPrivate;
    }

    public void setMarkedAsPrivate(boolean markedAsPrivate) {
        this.markedAsPrivate = markedAsPrivate;
    }

    public boolean isUpdates() {
        return updates;
    }

    public void setUpdates(boolean updates) {
        this.updates = updates;
    }

    public boolean isDone() {
        return done;
    }

    public void setDone(boolean done) {
        this.done = done;
    }

    public Date getCreated() {
        return created;
    }

    public void setCreated(Date created) {
        this.created = created;
    }

    public Date getUpdated() {
        return updated;
    }

    public void setUpdated(Date updated) {
        this.updated = updated;
    }
}

6. Exercise - Creating Templates using Thymeleaf

Thymeleaf is a powerful template engine that can be used with the Spring framework. It lets you write plain HTML code while also using Java objects for data binding. You’ve already added the Library to your project when you configured it in Exercise - Configuring Spring Boot for web based applications.

6.1. HTML Templates

In the root folder of your project there is another folder called src/main/resources. This contains all resources you need in your SpringBoot application. This is where we create our templates. Create and open the folder src/main/resources/templates/issues and create a new file called issuereport_form.html in it. This is the file that will be served on the route /issuereport. Create another file with the name issuereport_list.html in the same folder. This file will be served on the /issues route. But to achieve this, we need to configure our controller to do so.

6.2. Serving HTML Templates

Currently all our controller methods feature the @ResponseBody annotation. With this annotation in place the String returned by our controller methods gets sent to the browser as plain text. If we remove it, the Thymeleaf library will look for an HTML Template with the name returned.

Each route will then return the name of the template it should serve.

getReport()

issues/issuereport_form

submitReport()

issues/issuereport_form

getIssues()

issues/issurereport_list

You specify the folder structure inside your templates folder separated by forward slashes. But it’s important that the String doesn’t start with a /. So this won’t work: /issues/issuerepor_form.

Since we want to pass data into the template we also need to add a Model to the method parameters. Add Model model to the controller methods parameters. These will be automatically injected when the endpoint is called. Since this is fully done by the Spring framework we don’t have to worry about this. In the next step we’ll add attributes to the Model object to make them available in the template.

The IssueController class should look like this:

package com.vogella.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class IssueController {
    @GetMapping("/issuereport")
    public String getReport(Model model) {
        return "issues/issuereport_form";
    }
    @PostMapping("/issuereport")
    public String submitReport(Model model) {
        return "issues/issuereport_form";
    }
    @GetMapping("/issues")
    public String getIssues(Model model) {
        return "issues/issuereport_list";
    }
}

Now the Framework will look for the templates with the given name and serve them to the browser.

6.3. Binding objects to templates

Now we want to pass some data to the template. This is done by adding an object and optionally a name to the Model object passed in by the Spring framework. Use the addAttribute() method to achieve this. The first parameter is the name, the second parameter is an object. You will use this name to refer to this object in the template.

For the initial form route pass an empty object of the data class you created in the previous exercise. Add model.addAttribute("issuereport", new IssueReport()); to the method getReport().

Repeat the same for the method submitReport().

In the submitReport() method we also want to handle the data submitted via the form. To do this we also need to add IssueReport issueReport to the method parameters.

This will also be automatically constructed from the form data and injected by the Spring framework.

Since we want the template to show some kind of feedback upon receiving the form data, we should also add another attribute containing a boolean. If it’s set to true the template will show some kind of modal or confirming message.

Just add another attribute with the name submitted and the value true.

Since this boolean is only passed to the template if the route hit from the user was via POST HTTP method (and thus only upon form submission) the confirmation message is ONLY shown after the form was submitted.

6.3.1. Validate

The methods should now look like this:

package com.vogella.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import com.vogella.example.entity.IssueReport;

@Controller
public class IssueController {
    @GetMapping("/issuereport")
    public String getReport(Model model) {
        model.addAttribute("issuereport", new IssueReport());
        return "issues/issuereport_form";
    }
    @PostMapping(value="/issuereport")
    public String submitReport(IssueReport issueReport, Model model) {
        model.addAttribute("issuereport", new IssueReport());
        model.addAttribute("submitted", true);
       return "issues/issuereport_form";
    }
    @GetMapping("/issues")
    public String getIssues(Model model) {
        return "issues/issuereport_list";
    }
}

6.4. Creating a template

To use the objects passed in, we need to use specific Thymeleaf HTML syntax in the templates. All properties and attributes in an HTML file that are being used by Thymeleaf and are not standard HTML. They will begin with the prefix th:.

We will start with the following basic HTML document with a form in it. Add the following coding to the issuereport_form.html file:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Vogella Issuereport</title>
    <link rel="stylesheet" href="./style.css" />
    <meta charset="UTF-8" />
</head>
<body>
    <div class="container">
        <form method="post" action="#">
            <h3>Vogella Issuereport</h3>
            <input type="text" placeholder="Email" id="email"/>
            <input type="text" placeholder="Url where the issue was found on" id="url"/>
            <textarea placeholder="Description of the issue" rows="5" id="description"></textarea>

            <label for="private_id">
               Private?
               <input type="checkbox" name="private" id="private_id"/>
            </label>
            <label for="updates_id">
                Keep me posted
                <input type="checkbox" id="updates_id" name="updates"/>
            </label>

            <input type="submit" value="Submit"/>
        </form>

        <div class="result_message">
            <h3>Your report has been submitted.</h3>
            <p>Find all issues <a href="/issues">here</a></p>
        </div>
    </div>
</body>
</html>

This does not have any logic or data-binding in it, yet.

Without the attribute xmlns:th="http://www.thymeleaf.org" in the <html> tag, your editor might show warnings because he doesn’t know the attributes prefixed with th:.

Now the file will be served on the route /issuereport. If you have the application still running you can navigate to the route or click the link.

6.5. Data-binding

Now we want to tell Spring that this form should populate the fields of the IssueReport object we passed earlier. This is done by adding th:object="${issuereport}" to the <form> tag in issuereport_form.html: <form method="post" th:action="@{/issuereport}" th:object="${issuereport}">

th:action is the syntax for adding the action that should happen upon submission of the form.
Remember that we set the name of the IssueReport object to issuereport? We refer to it now by using that name. The same can be done with any name and object.

This alone will not tell Spring to auto-populate the fields in the object. We need to specify in the <input> elements what field this should represent. This is done by adding the attribute th:field="*{}".

${} is the way to refer to objects that were passed to the template, using SpEL. *{} is the syntax to refer to fields of the object bound to the form.

Add the following attributes to the <input> and <textarea> elements respectively.

<input type="text" placeholder="Email" id="email" th:field="*{email}"/>

<input type="text" placeholder="Url where the issue was found on" id="url" th:field="*{url}"/>

<textarea placeholder="Description of the issue" rows="5" id="description" th:field="*{description}"></textarea>

<input type="checkbox" name="private" id="private_id" th:field="*{markedAsPrivate}"/>

<input type="checkbox" id="updates_id" name="updates" th:field="*{updates}"/>

We also wanted to show some kind of confirmation modal upon submission. A modal for this already exists in the template: <div class="result_message">. But this should obviously be hidden until the user submits an issue. This is done via a conditional expression. Namely th:if="".

Remember that we passed a boolean with the name submitted in the submitReport() method? We could now use this to determine if we should show the confirmation modal.

Add th:if="${submitted}" to the <div class="result_message">. The result should look like this: <div class="result_message" th:if="${submitted}">

Now the class result_message will only be displayed if submitted is true.

The reason for this is that we hardcoded the submitted boolean ONLY to the POST request mapping. Thus it will only be added to the template if the HTTP method was POST. So only if the form was submitted.

The issuereport_form.html should now look like this:

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Vogella Issuereport</title>
<link rel="stylesheet" href="./style.css" />
    <meta charset="UTF-8" />
</head>
<body>
    <div class="container">
        <form method="post" action="#" th:object="${issuereport}" th:action="@{/issuereport}">
            <h3>Vogella Issue Report</h3>
            <input type="text" placeholder="Email" id="email" th:field="*{email}"/>
            <input type="text" placeholder="Url where the issue was found on" id="url" th:field="*{url}" />
            <textarea placeholder="Description of the issue" rows="5" id="description" th:field="*{description}" ></textarea>

            <label for="private_id">
                Private?
                <input type="checkbox" name="private" id="private_id" th:field="*{markedAsPrivate}" />
            </label>

            <label for="updates_id">
                Keep me posted
                <input type="checkbox" id="updates_id" name="updates" th:field="*{updates}" />
            </label>

            <input type="submit" value="Submit"/>
        </form>


        <div class="result_message" th:if="${submitted}">
            <h3>Your report has been submitted.</h3>
            <p>Find all issues <a href="/issues">here</a></p>
        </div>
    </div>
</body>
</html>

6.6. List view

Now we will create the HTML page for the issue report list. Add the following coding to issuereport_list.html.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Vogella Issuereport</title>
    <link rel="stylesheet" href="./style.css" />
    <meta charset="UTF-8" />
</head>
<body>
    <div class="container issue_list">
        <h2>Issues</h2>
        <br />
        <table>
            <tr>
                <th>Url</th>
                <th class="desc">Description</th>
                <th>Done</th>
                <th>Created</th>
            </tr>
            <th:block th:each="issue : ${issues}">
                <tr>
                    <td ><a th:href="@{${issue.url}}" th:text="${issue.url}"></a></td>
                    <td th:text="${issue.description}">...</td>
                    <td><span class="status" th:classappend="${issue.done} ? done : pending"></span></td>
                    <td th:text="${issue.created}">...</td>
                </tr>
            </th:block>
        </table>
    </div>
</body>
</html>
th:classappend conditionally adds classes to an element if the expression passed to it is true or false.
th:each="issue : ${issues} will loop over the issues list.

6.7. Optional: Stylesheets

If you want to have some styling for the page, this snippet styles it a bit. This is optional and does not change the behavior of the application in any way. It is already linked to both HTML pages via the <link rel="stylesheet" href="./style.css" /> element in the <head> section. Create a new file in the static folder in src/main/resources. Name it style.css and copy the following snippet into it.

*{
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}
body{
    font-family: sans-serif;
}
.container {
    width: 100vw;
    height: 100vh;
    padding: 100px 0;
    text-align: center;
}
.container form{
    width: 100%;
    height: 100%;
    margin: 0 auto;
    max-width: 350px;
}
.container form input[type="text"], .container form textarea{
    width: 100%;
    padding: 10px;
    border-radius: 3px;
    border: 1px solid #b8b8b8;
    font-family: inherit;
    margin-bottom: 20px;
}
.container h3{
    margin-bottom: 20px;
}
.container form input[type="submit"]{
    max-width: 250px;
    margin: auto;
    display: block;
    width: 55%;
    padding: 10px;
    background: darkorange;
    border: 1px solid #b8b8b8;
    border-radius: 3px;
    margin-top: 20px;
    cursor: pointer;
}
.issue_list table{
    text-align: left;
    border-collapse: collapse;
    border: 1px #b8b8b8 solid;
    margin: auto;
}
.issue_list .desc{
    min-width: 500px;
}
.issue_list td, .issue_list th{
    border-bottom: 1px #b8b8b8 solid;
    border-top: 1px #b8b8b8 solid;
    padding: 5px;
}
.issue_list tr{
    height: 35px;
    transition: background .25s;

}
.issue_list tr:hover{
    background: #eee;
}
.issue_list .status.done:after{
    content: '';
}

6.8. Validate

Reload the page on the http://localhost:8080/issuereport. The styling should have been applied. Enter some values in the fields and press submit. Now the result_message <div> will also be shown.

Spring Boot Project Submission Modal

The route /issues will show an empty list. This is because we have nothing added there yet.

7. Exercise - Embedding a database

In this exercise you will learn how to use a database to store the users issues and query them to show them on the list view.

7.1. Setup

We will use the h2 database for this. You already added this to your project in Exercise - Configuring Spring Boot for web based applications. Spring Boot automatically picks up and configures h2 when it’s on the classpath.

Now we only need to write a repository to interface with the db.

Create a new package called com.vogella.example.repositories. In here create a new interface with the name IssueRepository. This interface should extend the interface 'JpaRepository<>' from the package org.springframework.data.jpa.repository. Pass in IssueReport as the first parameter and Long as the second. This represents the object you are storing and the id it has inside the database.

Your interface should now look like this:

package com.vogella.example.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.vogella.example.entity.IssueReport;

public interface IssueRepository extends JpaRepository<IssueReport, Long>{
}

This alone would already be enough to fetch all the entries from the database, add new entries and do all basic CRUD operations.

But we want to fetch all entries which are not marked private and show them on the public list view. This is done by adding a custom query string to a method. Add this method to your interface

package com.vogella.example.repository;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.JpaRepository;

import com.vogella.example.entity.IssueReport;

public interface IssueRepository extends JpaRepository<IssueReport, Long> {
    @Query(value = "SELECT i FROM IssueReport i WHERE markedAsPrivate = false")
    List<IssueReport> findAllButPrivate();
The annotation @Query lets us add custom JPQL queries that are executed upon calling the method.

We also want to get all IssueReport reported by the same email-address. This is also done with a custom method. But for this we don’t need a custom @Query. It’s enough to create a method named findAllByXXX. XXX is a placeholder for the column you want to select by from the database. The value for this is passed in as a method parameter.

Add the following to your interface as well:

package com.vogella.example.repository;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.JpaRepository;

import com.vogella.example.entity.IssueReport;

public interface IssueRepository extends JpaRepository<IssueReport, Long> {
    @Query(value = "SELECT i FROM IssueReport i WHERE markedAsPrivate = false")
    List<IssueReport> findAllButPrivate();

    List<IssueReport> findAllByEmail(String email);
}

7.2. Using the repository

Go back to your controller class IssueController.java and add a new field of the repository interface to the class. Since the @Controller is managed by Spring the IssueRepository will automatically be injected into the constructor.

package com.vogella.example.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.vogella.example.entity.IssueReport;
import com.vogella.example.repository.IssueRepository;

@Controller
public class IssueController {
    IssueRepository issueRepository;

    public IssueController(IssueRepository issueRespository) {
        this.issueRepository = issueRepository;
    }

    @GetMapping("/issuereport")
    public String getReport(Model model) {
        model.addAttribute("issuereport", new IssueReport());
        return "issues/issuereport_form";
    }

    @PostMapping(value="/issuereport")
    public String submitReport(IssueReport issueReport, Model model) {
        model.addAttribute("submitted", true);
        model.addAttribute("issuereport", new IssueReport());
        return "issues/issuereport_form";
    }

    @GetMapping("/issues")
    public String getIssueReport(Model model) {
        return "issues/issuereport_list";
    }
}

7.2.1. Saving records to the database

To save a record to the database simply use the method save() from the IssueRepository interface and pass the object you want to store. In this case this is the received data on the path /issuereport.

package com.vogella.example.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.vogella.example.entity.IssueReport;
import com.vogella.example.repository.IssueRepository;

@Controller
public class IssueController {
    IssueRepository issueRepository;

    public IssueController(IssueRepository issueRespository) {
        this.issueRepository = issueRepository;
    }

    @GetMapping("/issuereport")
    public String getReport(Model model) {
        model.addAttribute("issuereport", new IssueReport());
        return "issues/issuereport_form";
    }

    @PostMapping(value="/issuereport")
    public String submitReport(IssueReport issueReport, Model model) {
        IssueReport result = this.issueRepository.save(issueReport);
        model.addAttribute("submitted", true);
        model.addAttribute("issuereport", result);
        return "issues/issuereport_form";
    }

    @GetMapping("/issues")
    public String getIssueReport(Model model) {
        return "issues/issuereport_list";
    }
}

This saves the given object to the database and then returns the freshly saved object. You should always continue with the entity returned by the repository, because it contains the id set by the database and might have changed in other ways too.

7.3. Redirecting after POST

If you post a IssueReport to the server and then refresh the page (F5) you’ll notice that the browser wants the send the posted information again. This could make users accidentally post an issue multiple times. For this ŕeason we’ll redirect them in our controller method.

    @PostMapping(value="/issuereport")
    public String submitReport(IssueReport issueReport, RedirectAttributes ra) {
        this.issueRepository.save(issueReport);
        ra.addAttribute("submitted", true);
        return "redirect:/issuereport";
    }

7.3.1. Fetching all records from the database

Normally this would be done using findAll(). But in this case we don’t want to include records that are marked as private and for this we created the method findAllButPrivate().

package com.vogella.example.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import com.vogella.example.entity.IssueReport;
import com.vogella.example.repository.IssueRepository;

@Controller
public class IssueController {
    IssueRepository issueRepository;

    public IssueController(IssueRepository issueRespository) {
        this.issueRepository = issueRepository;
    }

    @GetMapping("/issuereport")
    public String getReport(Model model, @RequestParam(name = "submitted", required = false) boolean submitted) {
        model.addAttribute("submitted", submitted);
        model.addAttribute("issuereport", new IssueReport());
        return "issues/issuereport_form";
    }

    @PostMapping(value="/issuereport")
    public String submitReport(IssueReport issueReport, RedirectAttributes ra) {
        this.issueRepository.save(issueReport);
        ra.addAttribute("submitted", true);
        return "redirect:/issuereport";
    }

    @GetMapping("/issues")
    public String getIssueReport(Model model) {
        model.addAttribute("issues", this.issueRepository.findAllButPrivate());
       return "issues/issuereport_list";
    }
}

7.4. Validate

Your IssueController should now look like this:

package com.vogella.example.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
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.servlet.mvc.support.RedirectAttributes;

import com.vogella.example.entity.IssueReport;
import com.vogella.example.repositories.IssueRepository;

@Controller
public class IssueController {
    IssueRepository issueRepository;

    public IssueController(IssueRepository issueRepository) {
        this.issueRepository = issueRepository;
    }

    @GetMapping("/issuereport")
    public String getReport(Model model, @RequestParam(name = "submitted", required = false) boolean submitted) {
        model.addAttribute("submitted", submitted);
        model.addAttribute("issuereport", new IssueReport());
        return "issues/issuereport_form";
    }

    @PostMapping(value="/issuereport")
    public String submitReport(IssueReport issueReport, RedirectAttributes ra) {
        this.issueRepository.save(issueReport);
        ra.addAttribute("submitted", true);
        return "redirect:/issuereport";
    }

    @GetMapping("/issues")
    public String getIssues(Model model) {
        model.addAttribute("issues", this.issueRepository.findAllButPrivate());
       return "issues/issuereport_list";
    }
}

The IssueRepository should look like this:

package com.vogella.example.repositories;

import java.util.List;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.jpa.repository.JpaRepository;

import com.vogella.example.entity.IssueReport;

public interface IssueRepository extends JpaRepository<IssueReport, Long> {
    @Query(value = "SELECT i FROM IssueReport i WHERE markedAsPrivate = false")
    List<IssueReport> findAllButPrivate();

    List<IssueReport> findAllByEmail(String email);
}

Go ahead and reload the form and enter some data. Now click submit and go to the route /issues. You should see the previously entered data.

8. Exercise - Making the information available via REST

If you want to access your data from another application or make it available for the public your best and most secure way is a REST api. Luckily the SpringBoot framework has useful methods for this.

8.1. Setup

Create a new class named IssueRestController. You may create a new package for this or use the existing com.vogella.example.controller package. To tell Spring that this is a RestController and that the methods inside this controller should return JSON data, add the @RestController annotation to the class.

The setup for the routes is similar to normal routes. Use the @GetMapping for GET requests. And @PostMapping for POST requests. The difference is that this time you don’t want templates to be rendered. So the return type for the methods should be whatever you want to return. E.g. IssueReport or even List<IssueReport>.

package com.vogella.example.controller;

import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.vogella.issuereport.IssueReport;

@RestController
public class IssueRestController {
    @GetMapping(value="/api/issues")
    public List<IssueReport> getIssues() {
        return null;
    }

    @GetMapping(value="/api/issues/{id}")
    public IssueReport getIssue(@PathVariable(value="id") long id) {
        return null;
    }
}

If you want to access a variable in the URL (in this case id) you do this by first declaring it a variable in the @GetMapping arguments ({id}). Than you require it as a method parameter with the @PathVariable annotation.

8.2. Making the data available

Accessing the data is pretty easy too. Just (re-)use the previously created IssueRepository and return the values from the methods in there.

package com.vogella.example.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

import com.vogella.issuereport.IssueReport;
import com.vogella.issuereport.repositories.IssueRepository;

@RestController
public class IssueRestController {
    IssueRepository issueRepository;

    public IssueRestController(IssueRespository issueRespositoy) {
        this.issueRepository = issueRespository;
    }

    @GetMapping(value="/api/issues")
    public List<IssueReport> getIssues() {
        return this.issueRepository.findAllButPrivate();
    }

    @GetMapping(value="/api/issues/{id}")
    public IssueReport getIssue(@PathVariable(value="id") long id) {
        return this.issueRepository.findOne(id);
    }
}

Again the IssueRepository is automatically injected into the class. You can still use the custom query method findAllButPrivate().

The method findOne(long id) queries only the object with the given Id. In this case we get it from the URL parameters and then we return the object queried or (in case nothing is found) nothing.

9. Testing Spring Boot Applications

As long as you use constructor or setter injection you can write unit tests without any dependency on Spring. Either create the dependencies of the class under test with the new keyword or mock them.

If you need to write integration tests you need Spring support to load a Spring ApplicationContext for your test.

To add testing support to your Spring Boot application you can require spring-boot-starter-test in your build configuration. Besides testing support for Spring boot spring-boot-starter-test adds a handful of other useful libraries for your tests like JUnit, Mockito and AssertJ.

10. Rolling back database changes after tests

If you want Spring to roll back your database changes after a test finishes you can add @Transactional to your test class. If you run a @SpringBootTest with either RANDOM_PORT or DEFINED_PORT your test will get executed in a different thread than the server. This means that every transaction initiated on the server won’t be rolled back. Using @Transactional on a test class can hide errors because changes don’t get actually flushed to the database. Another option is to manually reset/reload the database state after every test. This approach works but makes your tests hard to parallelize.

11. Test annotations

Spring Boot provides several test annotations that allow you to decide which parts of the application should get loaded. To keep test startup times minimal you should only load what your test actually needs. For these annotations to work you have to add the @RunWith(SpringRunner.class) annotation to your test class. You can find an overview of all the auto-configurations that get loaded by a particular annotation in the Spring Boot manual.

11.1. @SpringBootTest

The @SpringBootTest annotation searches upwards from the test package until it finds a @SpringBootApplication or @SpringBootConfiguration. The Spring team advises to place the Application class into the root package, which should ensure that your main configuration is found by your test. This means that your test will start with all Spring managed classes loaded. You can set the webEnvironment attribute if you want to change which ApplicationContext is created:

  • MOCK (default): loads a WebApplicationContext but mocks the servlet environment

  • RANDOM_PORT: loads an EmbeddedWebApplicationContext with servlet containers in their own thread, listening on a random port

  • DEFINED_PORT: loads an EmbeddedWebApplicationContext with servlet containers in their own thread, listening on their configured port

  • NONE: loads an ApplicationContext with no servlet environment

To make calls to the server started by your test you can let Spring inject a TestRestTemplate:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SpringIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;
    // ...
}

11.2. @WebMvcTest

WebMvcTests are used to test controller behavior without the overhead of starting a web server. In conjunction with mocks it is possible to test that routes are configured correctly without the overhead of executing the operations associated with the endpoints. A WebMvcTest configures a MockMvc instance that can be used to simulate network calls.

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserLoginIntegrationTest {
    @Autowired
    private MockMvc mvc;
    @MockBean
    private UserService userService;

    @Test
    public void loginTest() throws Exception {
        mvc.perform(get("/login"))
            .andExpect(status().isOk())
            .andExpect(view().name("user/login"));
    }
}

If you want Spring to load additional classes you can specify an include filter:

@WebMvcTest(value = UserController.class, includeFilters = { @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = UserService.class) })

11.3. @DataJpaTest

DataJpaTests load @Entity and @Repository but not regular @Component classes. This makes it possible to test your JPA integration with minimal overhead. You can inject a TestEntityManager into your test, which is an EntityManager specifically designed for tests. If you want to have your JPA repositories configured in other tests you can use the @AutoConfigureDataJpa annotation. To use a different database connection than the one specified in your configuration you can use @AutoConfigureTestDatabase.

@RunWith(SpringRunner.class)
@DataJpaTest
public class JpaDataIntegrationTest {

    @Autowired
    private UserRepository userRepository;

    // ...
}

12. Mocking

Spring Boot provides the @MockBean annotation that automatically creates a mock object. When this annotation is placed on a field this mock object is automatically injected into any class managed by Spring that requires it.

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserTest {

    @MockBean
    private UserService userService;
    @Autowired
    private MockMvc mvc;

    @Test
    public void loginTest() throws Exception {
        when(userService.login(anyObject())).thenReturn(true);
        mvc.perform(get("/login"))
            .andExpect(status().isOk())
            .andExpect(view().name("user/login"));
    }
}

13. MockMvc

MockMvc is a powerful tool that allows you to test controllers without starting an actual web server. In an @WebMvcTest MockMvc gets auto configured and can be injected into the test class with @Autowired. To auto configure MockMvc in a different test you can use the @AutoConfigureMockMvc annotation. Alternatively you can create it yourself:

@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mvc;

@Before
public void setUp() throws Exception {
    mvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}

14. Spring Boot 2.0

The second major release of Spring Boot is based on new features coming with Version 5 of the Spring Framework. Since reactive functional programming has proven to be a great concept for asynchronous processing of code this is one of the main new features coming with Spring Boot 2.0

15. Exercise: Create a reactive Spring Boot project

In the Spring Tool Suite just right click in the Package Explorer and go to New ▸ Spring Starter Project

create spring starter project

Use the following settings in the project creation dialog:

spring starter project wizard

When pressing Next the desired dependencies can be specified.

First select Spring Boot Version 2.0.0 and the following dependencies:

  • Lombok

  • MongoDB

  • Reactive MongoDB

  • Embedded MongoDB

  • Actuator

  • Reactive Web

spring starter dependencies

Then press Finish so that the project will be generated.

16. Exercise: Create a Todo domain model

Create another package called com.vogella.springboot2.domain and create a Todo class inside it.

package com.vogella.springboot2.domain;

import java.util.Date;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data (1)
@NoArgsConstructor (2)
@JsonIgnoreProperties(ignoreUnknown = true) (3)
public class Todo {

    private long id;
    private String summary = "";
    private String description = "";
    private boolean done = false;
    private Date dueDate = new Date();

    public Todo(long id) {
        this.id = id;
    }
}
1 The Todo class is a simple data class and the @Data annotation of the Lombok library automatically generates getters and setters for the properties and hashCode(), equals() and toString() methods.
2 If a certain constructor is implemented and a constructor with no arguments should still be available the @NoArgsConstructor annotation can be used.
3 The Todo is also supposed to be serialized and deserialized with JSON, Spring uses the Jackson library for this by default. @JsonIgnoreProperties(ignoreUnknown = true) specifies that properties, which are available in the JSON String, but not specified as class members will be ignored instead of raising an Exception.

17. Exercise: Creating a reactive rest controller

Create a package private TodoRestController class inside a com.vogella.springboot2.controller package.

package com.vogella.springboot2.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.vogella.springboot2.domain.Todo;

import reactor.core.publisher.Flux;

@RestController (1)
class TodoRestController {

    private Flux<Todo> todos; (2)

    public TodoRestController() {
        todos = createTodoModel();
    }

    private Flux<Todo> createTodoModel() {
        Todo todo = new Todo(1);
        todo.setSummary("Learn Spring Boot 2.0");
        todo.setDescription("Easily create modern reactive webapps with Spring Boot 2.0");

        Todo todo2 = new Todo(2);
        todo2.setSummary("Learn Reactor Framework");
        todo2.setDescription("Use the power of the reactive io api with the Reactor Framework");

        Todo todo3 = new Todo(3);
        todo3.setSummary("Learn @RestController");
        todo3.setDescription("Learn how to create @RestController and use rest endpoints");

        return Flux.just(todo, todo2, todo3);
    }

    @GetMapping("/getTodos") (3)
    public Flux<Todo> getTodos() {
        return todos;
    }

}
1 The @RestController annotation tells Spring that this class is a rest controller, which will be instantiated by the Spring framework
2 Flux is a type of the Reactor Framework, which implements the reactive stream api like RxJava does.
3 The @GetMapping annotation tells Spring that the endpoint http://{yourdomain}/getTodos should invoke the getTodos() method.

Now start the application by right clicking the project and clicking on Run as ▸ Spring Boot App.

spring start console

Along with many more logging statements the previously created rest endpoint /getTodos is mentioned.

Now let’s test the result when navigating to http://localhost:8080/getTodos.

restcontroller json

What we can see here is that the result is shown as JSON. By default the @RestController annotation handles this and if no specific mime type for the response is requested the result will be the object serialized as JSON. By default Spring uses the Jackson library the serialize and deserialize Java objects from and to JSON.

18. Optional: Change the default port of the web app

By default Spring Boot uses port 8080, but this can be changed by using the server.port property in the application.properties file.

server port app props

Since this is an optional exercise, we should leave the port to be 8080.

server.port=8080

19. Exercise: Passing parameters to the rest api

Since the amount of Todos can potentially increase really fast it would be nice to have search capabilities so that clients do not have to receive the whole list of Todos all the time and do the search on the client side.

@RestController
class TodoRestController {

    // ... more code

    @GetMapping("/getTodos")
    public Flux<Todo> getTodos(
    @RequestParam(name = "limit", required = false, defaultValue = "-1") long limit) { (1)
        if(-1 == limit) {
            return todos;
        }
        return todos.take(limit);
    }

    @GetMapping("/getTodoById")
    public Mono<Todo> getTodoById(long id) {  (2)
        return Mono.from(todos.filter(t -> id == t.getId()));
    }

}
1 @RequestParam can be used to request parameters and also apply default values, if the parameter is not required.
2 If the parameter is required a the name of the request parameter should be equal to method parameter’s name, the @RequestParam annotation can be omitted.

20. Exercise: Posting data to the rest controller

It is nice to receive data, but we also want to create new Todos.

@PostMapping("/newTodo") (1)
public Mono<Todo> newTodo(@RequestBody Todo todo) {
    Mono<Todo> todoMono = Mono.just(todo);
    todos = todos.mergeWith(todoMono); (2)
    return todoMono;
}
1 @PostMapping is used to specify that data has to be posted
2 The mergeWith method merges the existing Flux with the new Mono<Todo> containing the posted Todo object

Curl or any rest client you like, e.g., RESTer for Firefox, can be used to post data to the rest endpoint.

curl -d '{"id":4, "summary":"New custom Todo"}' -H "Content-Type: application/json" -X POST http://localhost:8080/newTodo

This will return the "New custom Todo" and show it on the command line.

21. Exercise: Sending a delete request

Last but not least Todos should also be deleted by using the rest API.

@DeleteMapping("/deleteTodo/{id}") (1)
public Mono<Void> deleteTodo(@PathVariable("id") int id) { (2)
    todos = todos.filter(todo -> todo.getId() != id);
    return todos.then();
}
1 @DeleteMapping can be used for delete rest operations and curly braces + name like {id} can be used as alternative of using query parameters like ?id=3
2 @PathVariable specifies the path, which will be used for the {id} path variable

Todo no. 3 can be deleted, since we learned how to create rest controllers now.

curl -X DELETE http://localhost:8080/deleteTodo/3

After using this curl command the remaining Todos are returned without Todo no. 3.

Call the http://localhost:8080/getTodos method again to check whether the deletion was successful.

22. Exercise: Creating a service for the business logic

Creating an initial model should not be part of the TodoRestController itself. A rest controller should simply specify the rest API and then delegate to a service, which handles the business logic in behind.

Therefore a TodoService class should be created in the com.vogella.springboot2.service package.

package com.vogella.springboot2.service;

import org.springframework.stereotype.Service;

import com.vogella.springboot2.domain.Todo;

import reactor.core.publisher.Flux;

@Service (1)
public class TodoService {

    private Flux<Todo> todos;

    public TodoService() {
        todos = createTodoModel();
    }

    private Flux<Todo> createTodoModel() {
        Todo todo = new Todo(1);
        todo.setSummary("Learn Spring Boot 2.0");
        todo.setDescription("Easily create modern reactive webapps with Spring Boot 2.0");

        Todo todo2 = new Todo(2);
        todo2.setSummary("Learn Reactor Framework");
        todo2.setDescription("Use the power of the reactive io api with the Reactor Framework");

        Todo todo3 = new Todo(3);
        todo3.setSummary("Learn @RestController");
        todo3.setDescription("Learn how to create @RestController and use rest endpoints");

        return Flux.just(todo, todo2, todo3);
    }

    public Flux<Todo> getTodos(long limit) {
        if (-1 == limit) {
            return todos;
        }
        return todos.take(limit);
    }

    public Mono<Todo> getTodoById(long id) {
        return Mono.from(todos.filter(t -> id == t.getId()));
    }

    public Mono<Todo> newTodo(Todo todo) {
        Mono<Todo> todoMono = Mono.just(todo);
        todos = todos.mergeWith(todoMono);
        return todoMono;
    }

    public Mono<Void> deleteTodo(int id) {
        todos = todos.filter(todo -> todo.getId() != id);
        return todos.then();
    }
}
1 The @Service annotation specifies this TodoService as spring service, which will be created when it is demanded by other classes like the refactored TodoRestController.

Basically we just moved everything into another class, but left out the rest controller specific annotations.

Now the TodoRestController looks clearly arranged and just delegates the rest requests to the TodoService.

package com.vogella.springboot2.controller;

import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.vogella.springboot2.domain.Todo;
import com.vogella.springboot2.service.TodoService;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@RestController
class TodoRestController {

    private TodoService todoService;

    public TodoRestController(TodoService todoService) { (1)
        this.todoService = todoService;
    }

    @GetMapping("/getTodos")
    public Flux<Todo> getTodos(@RequestParam(name = "limit", required = false, defaultValue = "-1") long limit) {
        return todoService.getTodos(limit);
    }

    @GetMapping("/getTodoById")
    public Mono<Todo> getTodoById(long id) {
        return todoService.getTodoById(id);
    }

    @PostMapping("/newTodo")
    public Mono<Todo> newTodo(@RequestBody Todo todo) {
        return todoService.newTodo(todo);
    }

    @DeleteMapping("/deleteTodo/{id}")
    public Mono<Void> deleteTodo(@PathVariable("id") int id) {
        return todoService.deleteTodo(id);
    }
}
1 Since the Spring framework instantiates the TodoRestController it is able to find the TodoService, which is demanded in the TodoRestControllers constructor. This works, because the TodoService class has the @Service annotation being applied.

The TodoService will do more stuff in the upcoming chapters.

23. Exercise: Reactive database access with Spring Data

Using reactive Reactor types like Flux<T> is really nice and powerful, but keeping the Todo data in-memory is not.

In former chapters, where the project has been created MongoDB dependencies have already been selected.

MongoDB has the benefit that it comes with a reactive database driver, which other databases like JDBC cannot offer. Hopefully in the future other databases catch up and provide reactive asynchronous database drivers as well.

But don’t be scared, if you haven’t used MongoDB yet, because there is a compile('org.springframework.boot:spring-boot-starter-data-mongodb-reactive') dependency in your build.gradle file, which provides an abstraction layer around the database.

In order to find, save and delete Todo objects in the mongo db a TodoRepository in the com.vogella.springboot2.data package should be created.

package com.vogella.springboot2.data;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;

import com.vogella.springboot2.domain.Todo;

public interface TodoRepository extends ReactiveCrudRepository<Todo, Long> {

}

Now Spring can create a ReactiveCrudRepository for Todo objects at runtime automatically, which provides ©reate, ®ead, (U)pdate and (D)elete capabilties.

The ReactiveCrudRepository class is similar to Spring Datas' CrudRepository class, but is able to return reactive asynchronous types rather than synchronous types.

Now we have to enable MongoDB to work with Todo objects:

package com.vogella.springboot2.domain;

import java.util.Date;

import org.springframework.data.annotation.Id;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class Todo {

    @Id (1)
    private long id;
    private String summary = "";
    private String description = "";
    private boolean done = false;
    private Date dueDate = new Date();

    public Todo(long id) {
        this.id = id;
    }
}
1 @Id is used to specify the id of the object, which is supposed to be stored in the database

Now the TodoService should be updated to store the data by using the TodoRepository.

package com.vogella.springboot2.service;

import java.util.Arrays;

import org.springframework.stereotype.Service;

import com.vogella.springboot2.data.TodoRepository;
import com.vogella.springboot2.domain.Todo;

import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Service
public class TodoService {

    private TodoRepository todoRepository;

    public TodoService(TodoRepository todoRepository) { (1)
        this.todoRepository = todoRepository;
        createTodoModel();
    }

    private void createTodoModel() {
        Todo todo = new Todo(1);
        todo.setSummary("Learn Spring Boot 2.0");
        todo.setDescription("Easily create modern reactive webapps with Spring Boot 2.0");

        Todo todo2 = new Todo(2);
        todo2.setSummary("Learn Reactor Framework");
        todo2.setDescription("Use the power of the reactive io api with the Reactor Framework");

        Todo todo3 = new Todo(3);
        todo3.setSummary("Learn @RestController");
        todo3.setDescription("Learn how to create @RestController and use rest endpoints");

        todoRepository.saveAll(Arrays.asList(todo, todo2, todo3)); (2)
    }

    public Flux<Todo> getTodos(long limit) {
        if (-1 == limit) {
            return todoRepository.findAll();
        }
        return todoRepository.findAll().take(limit);
    }

    public Mono<Todo> getTodoById(long id) {
        return todoRepository.findById(id);
    }

    public Mono<Todo> newTodo(Todo todo) {
        return todoRepository.save(todo);
    }

    public Mono<Void> deleteTodo(long id) {
        return todoRepository.deleteById(id);
    }
}
1 Even tough the TodoRepository interface is not annotated with @Service, @Bean, @Component or something similar it is automatically injected. The Spring Framework creates an instance of the TodoRepository at runtime once it is requested by the TodoService, because the TodoRepository is derived from ReactiveCrudRepository.
2 For the initial model the 3 Todos from former chapters are now stored in the MongoDB.

For all other operations the ReactiveCrudRepository default methods, which return Reactor types, are used (findAll, findById, save, deleteById).

24. Exercise: Implement custom query methods

Basically everything can be done by using CRUD operations. In case a Todo should be found by looking for text in the summary the findAll() method can be used and the service can iterate over the Flux<Todo> in order to find appropriate Todo objects.

public Flux<Todo> getBySummary(String textInSummary) {
        Flux<Todo> findAll = todoRepository.findAll();
        Flux<Todo> filteredFlux = findAll.filter(todo -> todo.getSummary().toLowerCase().contains(textInSummary.toLowerCase()));
        return filteredFlux;
}

But wait, is it really efficient to get all Todos and then filter them?

Modern databases can do this way more efficient by for example using the SQL LIKE statement. In general it is way better to delegate the query of certain elements to the database to gain more performance.

Spring data provides way more possibilities than just using the CRUD operations, which are derived from the ReactiveCrudRepository interface.

Inside the almost empty TodoRepository class custom method with a certain naming schema can be specified and Spring will take care of creating appropriate query out of them.

So rather than filtering the Todos from the database on ourselves it can be done like this:

package com.vogella.springboot2.data;

import org.springframework.data.repository.reactive.ReactiveCrudRepository;

import com.vogella.springboot2.domain.Todo;

import reactor.core.publisher.Flux;

public interface TodoRepository extends ReactiveCrudRepository<Todo, Long> {

    Flux<Todo> findBySummaryContainingIgnoreCase(String summary);
}

We leave it up to the reader to make use of the findBySummaryContainingIgnoreCase in the TodoService and then make use of it in the TodoRestController by providing a http://localhost:8080/getBySummary rest endpoint.

The schema possibilities for writing such methods are huge, but out of scope in this exercise.

You can also write real queries by using the @Query annotation.

// Just pretend that a Todo has a category to see the @Query syntax

@Query("from Todos a join a.category c where c.name=:categoryName")
Flux<Todo> findByCategory(@Param("categoryName") String categoryName);

25. Exercise: Using Example objects for queries

With the query method schema lots of properties of the Todo class can be combined for a query, but sometimes this can also be very verbose:

// could be even worse...
Flux<Todo> findBySummaryContainingAndDescriptionContainingAllIgnoreCaseAndDoneIsTrue(String summary, String description);

It would be nicer to create an instance of a Todo and then pass it to a find method.

Todo todo = new Todo(1);
Todo theTodoWithIdEquals1 = todoRepository.find(todo);

Unfortunately the ReactiveCrudRepository does not provide such a method.

But this capability is proivded by the ReactiveQueryByExampleExecutor<T> class.

ReactiveQueryByExampleExecutor
package com.vogella.springboot2.data;

import org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;

import com.vogella.springboot2.domain.Todo;

import reactor.core.publisher.Flux;

public interface TodoRepository extends ReactiveCrudRepository<Todo, Long>, ReactiveQueryByExampleExecutor<Todo> { (1)

    Flux<Todo> findBySummaryContainingIgnoreCase(String textInSummary);

    Flux<Todo> findBySummaryContainingAndDescriptionContainingAllIgnoreCaseAndDoneIsTrue(String summary, String description);

}
1 By implementing the ReactiveQueryByExampleExecutor<Todo> interface the methods above an be used to query by using Example objects.

So instead of using a findBySummaryContainingAndDescriptionContainingAllIgnoreCaseAndDoneIsTrue method an Example can be used to express the same:

public Mono<Todo> findTodoByExample(Todo todo) {
    ExampleMatcher matcher = ExampleMatcher.matching().withIgnoreCase()
            .withMatcher("summary", GenericPropertyMatcher::contains)
            .withMatcher("description", GenericPropertyMatcher::contains)
            .withMatcher("done", GenericPropertyMatcher::exact);
    Example<Todo> example = Example.of(todo, matcher);
    return todoRepository.findOne(example);
}

When looking for exact matches no ExampleMatcher has to be configured.

public Mono<Todo> findTodoByExampleSimple(Todo todo) {
    Example<Todo> example = Example.of(todo);
    return todoRepository.findOne(example);
}

26. About this website

27. Spring Boot resources

27.1. vogella GmbH training and consulting support

TRAINING SERVICE & SUPPORT

The vogella company provides comprehensive training and education services from experts in the areas of Eclipse RCP, Android, Git, Java, Gradle and Spring. We offer both public and inhouse training. Whichever course you decide to take, you are guaranteed to experience what many before you refer to as “The best IT class I have ever attended”.

The vogella company offers expert consulting services, development support and coaching. Our customers range from Fortune 100 corporations to individual developers.

Copyright © 2012-2018 vogella GmbH. Free use of the software examples is granted under the terms of the Eclipse Public License 2.0. This tutorial is published under the Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Germany license.

See Licence.