Spring Boot Tutorial
Spring Boot Tutorial
Spring Framework.
1. Spring Boot
Spring Boot is an opinionated framework built on top of the Spring
Framework. You can find out more about the Spring framework and its
modules in our Spring tutorial.
Spring typical requires a lot of configuration. Spring Boot simplifies this setup
by providing defaults for many features. You can still adjust the defaults
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. This JAR contains an embedded web server that can be
started with java -jar. Spring Boot provides selected groups of auto
configured features and dependencies, which makes it faster to get started.
Once you have started the Spring Tool Suite click on File New Spring
Starter Project to open the project creation wizard.
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!";
}
Start the class Application as a Spring Boot App. The embedded server
starts listening on port 8080. When you point your browser
to http://localhost:8080 you should see the welcome message:
3. Exercise - Configuring Spring
Boot for web based applications
In the following exercises your create a web-based issue reporting tool. With
this tool users can submit issues they found on a website.
3.1. Configure
This example application needs more JAR libraries. For this, open
the build.gradle file in the root folder of the project. Add the following to the
section 'dependencies'.
implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('org.springframework.boot:spring-boot-devtools')
runtime('com.h2database:h2')
Thymeleaf
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.
3.2. Validate
Your build.gradle should now look like this
plugins {
id 'org.springframework.boot' version '2.1.7.RELEASE'
id 'io.spring.dependency-management' version '1.0.8.RELEASE'
id 'java'
}
group = 'com.vogella'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
implementation('org.springframework.boot:spring-boot-starter-data-jpa')
runtime('org.springframework.boot:spring-boot-devtools')
runtime('com.h2database:h2')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
Start your application with Right Click on your Project > Run As >
Spring Boot App.
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";
}
}
This class contains the methods responsible for handling incoming web
requests.
The class is annotated with the @Controller annotation to tell the Spring framework that
it is a controller.
The @GetMapping annotation above the method signals the Spring Core that this method
should only handle GET requests.
The getReport() 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 @PostMapping annotation signals that this method should only handle POST requests
and thus only gets called when a POST request is received.
The submitReport() method is 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 getIssues() method will handle the HTML template for a list view in which all the
requests can be viewed. 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.1. Validate
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.
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;
import javax.persistence.Table;
@Entity// #1
@Table(name = "issues")// #2
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() {}
}
The @Entity annotation tells our JPA provider Hibernate that this class should be mapped
to the database.
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.
To let Spring instantiate 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 all fields and select
the Generate button .
5.2. 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
@Table(name = "issues")
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() {}
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/issuereport_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.
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;
@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.
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()); \\ #1
return "issues/issuereport_form";
}
@PostMapping(value="/issuereport")
public String submitReport(IssueReport issueReport, Model model) { \\ #2
model.addAttribute("issuereport", new IssueReport()); \\ #3
model.addAttribute("submitted", true); \\ #4
return "issues/issuereport_form";
}
@GetMapping("/issues")
public String getIssues(Model model) {
return "issues/issuereport_list";
}
}
Spring provides a Model object which can be passed into the controller. You
can configure this model object via the addAttribute() method. The first
parameter in this method is the key under which the second parameter can
be accessed. You will use this name to refer to this object in the template.
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>
<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.
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.
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.
<!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>
<!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>
*{
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.
The route /issues will show an empty list. This is because we have nothing
added there yet.
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.
package com.vogella.example.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import com.vogella.example.entity.IssueReport;
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;
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.
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;
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;
@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";
}
}
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;
@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.
@PostMapping(value="/issuereport")
public String submitReport(IssueReport issueReport, RedirectAttributes ra)
{
this.issueRepository.save(issueReport);
ra.addAttribute("submitted", true);
return "redirect:/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;
@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;
@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";
}
}
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;
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.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.
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.example.entity.IssueReport;
@RestController
@RequestMapping("/api/issues")
public class IssueRestController {
@GetMapping
public List<IssueReport> getIssues() {
return null;
}
@GetMapping("/{id}")
public IssueReport getIssue(@PathVariable("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}). Then you tell
Spring to inject it into your method by adding a parameter with
the @PathVariable annotation. You might notice
the @RequestMapping annotation we put above our class definition. Using this
annotation at the class level allows us to extract the part of the path that is
shared by all endpoints defined in the class.
package com.vogella.example.controller;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.example.entity.IssueReport;
import com.vogella.example.repositories.IssueRepository;
@RestController
@RequestMapping("/api/issues")
public class IssueRestController {
private IssueRepository issueRepository;
@GetMapping("/{id}")
public ResponseEntity<IssueReport> getIssue(@PathVariable("id")
Optional<IssueReport> issueReportOptional) {
if (!issueReportOptional.isPresent() ) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
Again the IssueRepository is automatically injected into the class. You can
still use the custom query method findAllButPrivate().
The getIssue method is a little more interesting. You might notice that we let
Spring inject an Optional<IssueReport> into the method. This is done with
the help of DomainClassConverter$ToEntityConverter which takes the id we
specified with @PathVariable and tries to retrieve the respective entity from
the database. If Spring couldn’t find an entity with the given id we return an
empty response with status code 404 in the guard clause. Otherwise the
entity gets returned as JSON.
If you need to write integration tests you need Spring support to load a Spring
ApplicationContext for your test.
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:
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:
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();
}
First select Spring Boot Version 2.1.0 and the following dependencies:
Lombok
MongoDB
Reactive MongoDB
Embedded MongoDB
Actuator
Reactive Web
Then press Finish so that the project will be generated.
@SpringBootApplication
public class Application {
springApplication.setWebApplicationType(WebApplicationType.REACTIV
E);
springApplication.run(args);
}
}
16. Exercise: @Component and
@Service annotations
Create a com.vogella.spring.playground.di package inside
the com.vogella.spring.playground project. This package should contain an
interface called Beer.
package com.vogella.spring.playground.di;
package com.vogella.spring.playground.di;
import org.springframework.stereotype.Component;
@Component
public class Flensburger implements Beer {
@Override
public String getName() {
return "Flensburger";
}
Now a beer instance can be injected into another class, which deserves a
beer.
package com.vogella.spring.playground.di;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class BarKeeperService {
Now go into the Application class and inject the BarKeeperService via
method injection by using the @Autowired annotation.
package com.vogella.spring.playground;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.vogella.spring.playground.di.BarKeeperService;
@SpringBootApplication
public class Application {
@Autowired
public void setBeerService(BarKeeperService beerService) {
beerService.logBeerName();
}
}
Now you can run the application and see Barkeeper serves Flensburger in the
logs.
package com.vogella.spring.playground.di;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeerConfig {
@Bean
public Beer getBecks() {
return new Beer() {
@Override
public String getName() {
return "Becks";
}
};
}
}
Now try to start the application and figure out what went wrong.
Then add the @Primary annotation to whatever beer you like most and rerun
the application, which should now print your primary beer.
18. Optional
Exercise: @Qualifier annotation
Different components or beans can also be qualified by using
the @Qualifier annotation. This approach is used to handle ambiguity of
components of the same type, in case the @Primary approach is not
sufficient.
@Component
@Qualifier("Flensburger")
public class Flensburger implements Beer {
@Override
public String getName() {
return "Flensburger";
}
@Configuration
public class BeerConfig {
@Bean
@Qualifier("Becks")
public Beer getBecks() {
return new Beer() {
@Override
public String getName() {
return "Becks";
}
};
}
}
After the different beers have been qualified, a certain bean can be
demanded by using the @Qualifier annotation as well.
@Service
public class BarKeeperService {
You can simply create a list and spring automatically gathers all beer beans
and components and passes them to the barkeeper.
package com.vogella.spring.playground.di;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class BarKeeperService {
beer.name=Carlsberg
package com.vogella.spring.playground.di;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class BeerConfig {
@Bean
public Beer getBeerNameFromProperty(@Value("${beer.name}") String
beerName) {
return new Beer() {
@Override
public String getName() {
return beerName;
}
};
}
}
beer.names=Bitburger,Krombacher,Berliner Kindl
Now with a dedicated properties file the @Configuration class has to point to
this, because other property files are not automatically parsed like
the application.properties file. The @PropertySource annotation can be used
to archieve this.
package com.vogella.spring.playground.di;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:/beers.properties")
public class BeerConfig {
@Bean
public List<Beer> getBeerNamesFromProperty(@Value("${beer.names}")
List<String> beerNames) {
return beerNames.stream().map(bN -> new Beer() {
@Override
public String getName() {
return bN;
}
}).collect(Collectors.toList());
}
}
package com.vogella.spring.playground.di;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class BeerImpl implements Beer {
@Configuration
@PropertySource("classpath:/beers.properties")
public class BeerConfig {
@Bean
public List<Beer> getBeerNamesFromProperty(@Value("${beer.names}")
List<String> beerNames) {
return
beerNames.stream().map(BeerImpl::new).collect(Collectors.toList());
}
}
First select Spring Boot Version 2.1.0 and the following dependencies:
Lombok
Reactive MongoDB
Embedded MongoDB
Actuator
Reactive Web
DevTools
package com.vogella.spring.user.domain;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
@Builder.Default
private String name = "";
@Builder.Default
private String email = "";
@Builder.Default
private String password = "";
@Builder.Default
private List<String> roles = new ArrayList<>();
@Builder.Default
private Instant lastLogin = Instant.now();
private boolean enabled;
The User 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.
If a certain constructor is implemented and a constructor with no arguments should still be
available the @NoArgsConstructor annotation can be used.
This is a convenience annotation to provide a constructor with all available field
automatically
This annotation automatically generates a Builder for a class. Usually used for classes with
many field, that may have default values.
The User 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.
The @Builder.Default annotation tells Lombok to apply these default values, if nothing
else will be set during the creation of a User object
package com.vogella.spring.user.controller;
import java.time.Instant;
import java.util.Collections;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/user")
class UserRestController {
private Flux<User> users;
public UserRestController() {
users = createUserModel();
}
@GetMapping
public Flux<User> getUsers() {
return users;
}
}
The @RestController annotation tells Spring that this class is a rest controller, which
will be instantiated by the Spring framework
The @RequestMapping annotation is used to point to a default prefix for the rest endpoints
defined in this rest controller
Rest controllers should be package private since they should only be created by the Spring
framework and not by anyone else by accident
Flux<T> is a type of the Reactor Framework, which implements the reactive stream api like
RxJava does.
The @GetMapping annotation tells Spring that the endpoint http://{yourdomain}/user should
invoke the getUsers() method.
Now start the application by right clicking the project and clicking on Run
as Spring Boot App.
package com.vogella.spring.user.controller;
import java.time.Instant;
import java.util.Collections;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/user")
class UserRestController {
@GetMapping
public Flux<User> getUsers(@RequestParam(name = "limit", required = false,
defaultValue = "-1") long limit) {
if(-1 == limit) {
return users;
}
return users.take(limit);
}
@GetMapping("/{id}")
public Mono<User> getUserById(@PathVariable("id") long id) {
return Mono.from(users.filter(user -> id == user.getId()));
}
}
@RequestParam can be used to request parameters and also apply default values, if the
parameter is not required
Spring will automatically map the {id} from a request to be a method parameter when
the @PathVariable annotation is used
import java.time.Instant;
import java.util.Collections;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/user")
class UserRestController {
@PostMapping
public Mono<User> newUser(@RequestBody User user) {
Mono<User> userMono = Mono.just(user);
users = users.mergeWith(userMono);
return userMono;
}
}
Curl or any rest client you like, e.g., RESTer for Firefox, can be used to post
data to the rest endpoint.
This will return the "New custom User" and show it on the command line.
import java.time.Instant;
import java.util.Collections;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/user")
class UserRestController {
@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
@PathVariable specifies the path, which will be used for the {id} path variable
User no. 3 can be deleted, since we learned how to create new users now.
After using this curl command the remaining Users are returned without User
no. 3.
The tests reside in the src/test/java test folder. Create a new package
called com.vogella.spring.user.controller inside the test folder. The
advantage of using the same package names as in the src/main/java folder
is that you can access protected and package private methods in your tests.
@Autowired
private ApplicationContext context;
private WebTestClient webTestClient;
@Before
public void setUp() {
webTestClient =
WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/")
.build();
}
@WebFluxTest starts a Spring application with only this Controller loaded, shortening the
test startup time
@Autowired since no other class should instanciate a test class we can use field injection
WebTestClient allows us to programatically make reactive REST calls in our tests
UserRestControllerTest.java
@Test
public void getUserById_userIdFromInitialDataModel_returnsUser() throws
Exception {
ResponseSpec rs = webTestClient.get().uri("/user/1").exchange();
rs.expectStatus().isOk()
.expectBody(User.class)
.consumeWith(result -> {
User user = result.getResponseBody();
assertThat(user).isNotNull();
assertThat(user.getName()).isEqualTo("Fabian Pfaff");
});
}
Great, our code works like a charm. But our previous test only tests the
happy path. Now we write a tests that tests what happens if the server
receives an unknown id.
UserRestControllerTest.java
@Test
public void getUserById_invalidId_error() throws Exception {
ResponseSpec rs = webTestClient.get().uri("/user/-1").exchange();
rs.expectStatus().isNotFound();
}
When you run this test you’ll notice that it fails. Our controller doesn’t return
the right http status in case he can’t find the entity.
UserRestController.java
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable("id") long id)
{
Mono<User> foundUser = Mono.from(users.filter(user -> id ==
user.getId()));
return foundUser
.map(user -> ResponseEntity.ok(user))
.switchIfEmpty(Mono.error(new
ResponseStatusException(HttpStatus.NOT_FOUND)));
}
@Test
public void createUser_validUserInput_userCreated() throws Exception {
ResponseSpec rs = webTestClient.post().uri("/user")
.body(BodyInserters.fromObject(
User.builder().name("Jonas
Hungershausen").email("[email protected]").build()))
.exchange();
rs.expectStatus().isCreated().expectHeader()
.valueMatches("LOCATION", "^/user/\\d+");
}
@PostMapping
public Mono<ResponseEntity<Object>> newUser(@RequestBody Mono<User>
userMono, ServerHttpRequest req) {
userMono = userMono.map(user -> {
user.setId(6);
return user;
});
users = users.mergeWith(userMono);
return userMono.map(u ->
ResponseEntity.created(URI.create(req.getPath() + "/" + u.getId())).build());
}
For example:
POST /user/search should return status 400 if the given json can’t be
mapped to user
DELETE /user/{id} should return status 404 if the id is not valid
DELETE /user/{id} should return status 204 if a user was successfully
deleted == Exercise: Creating a service for the business logic
package com.vogella.spring.user.service;
import java.time.Instant;
import java.util.Collections;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class UserService {
public UserService() {
users = createUserModel();
}
The @Service annotation specifies this UserService as spring service, which will be
created when it is demanded by other classes like the refactored UserRestController.
Basically we just moved everything into another class, but left out the rest
controller specific annotations.
Now the UserRestController looks clearly arranged and just delegates the
rest requests to the UserService.
package com.vogella.spring.user.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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.spring.user.domain.User;
import com.vogella.spring.user.service.UserService;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
@RequestMapping("/user")
class UserRestController {
@GetMapping
public Flux<User> getUsers(@RequestParam(name = "limit", required = false,
defaultValue = "-1") long limit) {
return userService.getUsers(limit);
}
@GetMapping("/{id}")
public Mono<ResponseEntity<User>> findUserById(@PathVariable("id") long
id) {
return userService.findUserById(id)
.map(user -> ResponseEntity.ok(user))
.switchIfEmpty(Mono.error(new
ResponseStatusException(HttpStatus.NOT_FOUND)));
}
@PostMapping
public Mono<ResponseEntity<Object>> newUser(@RequestBody User user,
ServerHttpRequest req) {
return userService.newUser(user)
.map(u -> ResponseEntity.created(URI.create(req.getPath()
+ "/" + u.getId())).build());
}
@DeleteMapping("/{id}")
public Mono<Void> deleteUser(@PathVariable("id") int id) {
return userService.deleteUser(id);
}
}
One easy way to get around this it to transform the test into an intergration
test that loads the full Spring application. This is slower but ensures that the
UserService is available for injection.
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRestControllerIntegrationTest {
// test code copied from UserRestController..
}
Then adjust the tests. In the setup phase we define the desired mock
behavior and then trigger the test call as before:
UserRestControllerTest.java
@Test
public void getUserById_userIdFromInitialDataModel_returnsUser() throws
Exception {
int id = 1;
String name = "Fabian Pfaff";
when(userService.findUserById(id)).thenReturn(Mono.just(User.builder().name(na
me).build()));
rs.expectStatus().isOk().expectBody(User.class).consumeWith(result ->
{
User user = result.getResponseBody();
assertThat(user).isNotNull();
assertThat(user.getName()).isEqualTo(name);
});
}
UserRestControllerTest.java
@Test
public void getUserById_invalidId_404() throws Exception {
long invalidId = -1;
when(userService.findUserById(invalidId)).thenReturn(Mono.empty());
ResponseSpec rs = webTestClient.get().uri("/user/" +
invalidId).exchange();
rs.expectStatus().isNotFound();
}
The user creation only reads the id from the created user, so this is enough to
make the test pass:
UserRestControllerTest.java
@Test
public void createUser_validUserInput_userCreated() throws Exception {
long id = 42;
when(userService.newUser(ArgumentMatchers.any()))
.thenReturn(Mono.just(User.builder().id(id).build()));
ResponseSpec rs = webTestClient.post().uri("/user")
.body(BodyInserters.fromObject(
User.builder().name("Jonas
Hungershausen").email("[email protected]").build()))
.exchange();
rs.expectStatus().isCreated().expectHeader().valueEquals("LOCATION",
"/user/" + id);
}
Write mocks for all test methods you’ve implemented to make them pass
again.
@RunWith(SpringRunner.class)
@DataMongoTest
public class UserMongoIntegrationTest {
@Autowired
private UserRepository userRepository;
@Test
public void save_validUserInput_canBeFoundWithFindAll() throws Exception {
userRepository.save(User.builder().id(1).name("Lars Vogel").build())
.mergeWith(userRepository.save(User.builder().id(2).name("Simo
n Scholz").build()))
.blockLast();
StepVerifier.create(users)
.recordWith(ArrayList::new)
.expectNextCount(2)
.consumeRecordedWith(userList -> {
assertThat(userList).withFailMessage("Should contain user
with name <%s>", "Simon Scholz")
.anyMatch(user -> user.getName().equals("Simon
Scholz"));
}).expectComplete()
.verify();
}
}
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.
package com.vogella.spring.user.data;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import com.vogella.spring.user.domain.User;
package com.vogella.spring.user.domain;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import org.springframework.data.annotation.Id;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
@Id
private long id;
@Builder.Default
private String name = "";
@Builder.Default
private String email = "";
@Builder.Default
private String password = "";
@Builder.Default
private List<String> roles = new ArrayList<>();
@Builder.Default
private Instant lastLogin = Instant.now();
@Id is used to specify the id of the object, which is supposed to be stored in the database
package com.vogella.spring.user.service;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.stereotype.Service;
import com.vogella.spring.user.data.UserRepository;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class UserService {
Even tough the UserRepository interface is not annotated with @Service, @Bean,
@Component or something similar it is automatically injected. The Spring Framework
creates an instance of the UserRepository at runtime once it is requested by
the UserService, because the UserRepository is derived
from ReactiveCrudRepository.
For the initial model the 3 Users from former chapters are now stored in the MongoDB.
But wait, is it really efficient to get all Users 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 UserRepository 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 Users from the database on ourselves it can be
done like this:
package com.vogella.spring.user.data;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
The schema possibilities for writing such methods are huge, but out of scope
in this exercise.
package com.vogella.spring.user.data;
import
org.springframework.data.repository.query.ReactiveQueryByExampleExecutor;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
Flux<User>
findByEmailContainingAndRolesContainingAllIgnoreCaseAndEnabledIsTrue(String
email, String role);
}
So instead of using
a findByEmailContainingAndRolesContainingAllIgnoreCaseAndEnabledIsTr
ue method an Example can be used to express the same:
@PostMapping("/search")
public Mono<User> getUserByExample(@RequestBody User user) {
return userService.findUserByExample(user);
}
package com.vogella.spring.user.initialize;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;
import com.vogella.spring.user.data.UserRepository;
import com.vogella.spring.user.domain.User;
@Profile("!production")
@Component
public class UserDataInitializer implements SmartInitializingSingleton {
@Override
public void afterSingletonsInstantiated() {
User user = new User(1, "Fabian Pfaff", "[email protected]",
"sdguidsdsghuds",
Collections.singletonList("ADMIN"), Instant.now(), true);
User user2 = new User(2, "Simon Scholz", "[email protected]",
"sdguidsdsghuds",
Collections.singletonList("ADMIN"), Instant.now(), false);
User user3 = new User(3, "Lars Vogel", "[email protected]",
"sdguidsdsghuds",
Collections.singletonList("USER"), Instant.now(), true);
@Profile("!production") this stops Spring from loading this bean when the
"production" profile is activated
Profiles can be activated by specifying them in the application.properties file
inside the src/main/resources/ folder.
spring.profiles.active=production
Please startup the server without the production profile and with the
production profile being activated. You can see the difference by navigating
to http://localhost:8080/user for both scenarios.
The user object must only be created when a valid email and password have
been provided:
package com.vogella.spring.user.domain;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
import org.springframework.data.annotation.Id;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
@Id
private long id;
@Builder.Default
private String name = "";
@NotEmpty
@Email
@Builder.Default
private String email = "";
@Builder.Default
private List<String> roles = new ArrayList<>();
@Builder.Default
private Instant lastLogin = Instant.now();
To tell Spring that it should run the validations in the controller we have to
add a @Valid annotation to the incoming data:
@PostMapping
public Mono<ResponseEntity<Object>> newUser(@RequestBody @Valid Mono<User>
userMono, ServerHttpRequest req) {
return userMono.flatMap(user -> {
return userService.newUser(user)
.map(u -> ResponseEntity.created(URI.create(req.getPath()
+ "/" + u.getId())).build());
});
}
Now try to create a user like we’ve done in a former exercise and see what
happens:
This time the response contains a 400 error code and complains about the
invalid email and password field.
Therefore the request has to be updated to include a valid email adress and
password.
curl -d '{"id":100, "name":"Spiderman", "email":"[email protected]",
"password":"WithGreatPowerComesGreatResponsibility"}' -H "Content-Type:
application/json" -X POST http://localhost:8080/user
This time the Spiderman user is successfully added to the list of users, which
can be verified by navigating to http://localhost:8080/user.
User.java
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { ValidRolesValidator.class })
@Documented
public @interface ValidRoles {
User.java
import java.util.Collection;
import java.util.List;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import com.google.common.collect.Lists;
@Override
public boolean isValid(Collection<String> collection,
ConstraintValidatorContext context) {
return collection.stream().allMatch(validRoles::contains);
}
Finally we can add the annotation to the field in the User class and Spring will
do the rest:
User.java
@ValidRoles
private List<String> roles = new ArrayList<>();
Micro services have become more public and Spring Cloud helps to manage
this architecture.
First of all the ports should be changed so that the gateway uses port 8080
and the port of the user project should have been changed to 8081. This can
be archived by changing the server.port property in
the application.properties file in both projects.
In case port 8080 is already blocked on your machine you can choose a
different port, e.g. 8090, and target this instead.
package com.vogella.spring.gateway;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class RouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("users", r ->
r.path("/user/**")
.uri("http://localhost:8081"))
.build();
}
}
buildscript {
ext {
springBootVersion = '2.1.0.M3'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-
gradle-plugin:${springBootVersion}")
}
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
}
ext {
springCloudVersion = 'Greenwich.M1'
}
dependencies {
implementation('org.springframework.boot:spring-boot-
starter-actuator')
implementation('org.springframework.boot:spring-boot-
starter-webflux')
implementation('org.springframework.cloud:spring-cloud-
starter')
implementation('org.springframework.cloud:spring-cloud-
starter-gateway')
testImplementation('org.springframework.boot:spring-
boot-starter-test')
testImplementation('io.projectreactor:reactor-test')
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-
dependencies:${springCloudVersion}"
}
}
Once this has been done the gateway server and the user server should be
started.
For service discovery a generic name for services has to be applied. This can
be done by using the spring.application.name property.
For the user project the application.properties file should look like this:
server.port=8081
spring.application.name=user
We want to use Netflix Eureka for service discovery and therefore have to
add a dependency to org.springframework.cloud:spring-cloud-starter-
netflix-eureka-client.
buildscript {
ext {
springBootVersion = '2.1.0.RELEASE'
}
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:$
{springBootVersion}")
}
}
group = 'com.vogella'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8
repositories {
mavenCentral()
maven { url "https://repo.spring.io/milestone" }
}
ext {
springCloudVersion = 'Greenwich.M1'
}
dependencies {
implementation('org.springframework.boot:spring-boot-starter-actuator')
implementation('org.springframework.boot:spring-boot-starter-data-mongodb-
reactive')
implementation('org.springframework.boot:spring-boot-starter-webflux')
implementation('org.springframework.cloud:spring-cloud-starter-netflix-
eureka-client')
compileOnly('org.projectlombok:lombok')
testImplementation('org.springframework.boot:spring-boot-starter-test')
testImplementation('de.flapdoodle.embed:de.flapdoodle.embed.mongo')
testImplementation('io.projectreactor:reactor-test')
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:$
{springCloudVersion}"
}
}
The milestone repositories have to be added, because the Spring Cloud Greenwich version is
published as milestone for now.
Store the Spring Cloud version in a ext project property
Add the org.springframework.cloud:spring-cloud-starter-netflix-eureka-client dependency
dependencyManagement for Spring Cloud dependencies has to be applied
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
And these would be the default application.yml for the Eureka server.
Now you can navigate to http://localhost:8761 with a browser and see the
Eureka dashboard and the services, which have been discovered.
You should now (re-)start the user server and then see the user service in the
Eureka dashboard:
In order to start it you can run spring cloud eureka in the command line.
Once the eureka server has been started it prints the following to the
command line:
Starting the eureka server with Spring Cloud CLI may take
awail. Just be patient or try to create a new Spring Boot
project, which is described in the next NOTE of this
exercise.
Now you can navigate to http://localhost:8761 with a browser and see the
Eureka dashboard and the services, which have been discovered.
You should now (re-)start the user server and then see the user service in the
Eureka dashboard:
In case you do not want to use the Spring Cloud CLI you
can easily create a Eureka server on your own by creating
a new Spring Boot project. Add the Netflix Eureka Server
depenedency ('org.springframework.cloud:spring-cloud-
starter-eureka-server') and add
the @EnableEurekaServer annotation to the application
config.
@SpringBootApplication
@EnableEurekaServer
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
server:
port: 8761
eureka:
client:
registerWithEureka: false
fetchRegistry: false
46. Exercise: Let the gateway find
services load balanced
Now that we are able to add services to the Eureka server, we no longer want
to address services via its physical address, but by service name.
package com.vogella.spring.gateway;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableDiscoveryClient
public class RouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("users", r -> r.path("/user/**")
.uri("lb://user"))
.build();
}
}
The gateway itself has also to be registered to eureka to make it work properly
lb stands for load balanced
Load balanced means that Eureka can now decide to which user service
instance it passes the request, in case several user service instances are
running.
server.port=8080
spring.application.name=gateway
spring.cloud.gateway.discovery.locator.enabled=true
spring.cloud.gateway.discovery.locator.lower-case-service-id=true
The implementation('org.springframework.cloud:spring-cloud-starter-
netflix-eureka-client') dependency has to be added to the gateway as
well in the build.gradle as well.
Now three server should be started: Eureka server, Gateway server and the
User server.
The big benefit is that it is now possible to start several user server on
different ports and eureka will load balance the requests.
Imagine all user services are down and you still want to return a fallback from
the gateway when the /user end point is requested.
After the gradle dependencies have been refreshed, we can either add
the @EnableCircuitBreaker annotation to a @Configuration class.
@Configuration
@EnableDiscoveryClient
@EnableCircuitBreaker
public class RouteConfig {
@SpringCloudApplication
public class Application {
@SpringCloudApplication applies
both @EnableDiscoveryClient and @EnableCircuitBreaker
Once the Hystrix circuit breaker has been enabled a filter for providing a
hystrix fallback can be applied to the RouteLocator.
@Configuration
@EnableDiscoveryClient
@EnableCircuitBreaker
public class RouteConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user",
r -> r.path("/user/**")
.filters(f -> f.hystrix(c ->
c.setName("fallback")
.setFallbackUri("forward:/fallback")))
.uri("lb://user"))
.build();
}
}
package com.vogella.spring.gateway;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
@RestController
class HystrixFallbackContoller {
@GetMapping("/fallback")
public Mono<ResponseEntity<String>> userFallback() {
return
Mono.just(ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).build());
}
}
@SpringBootApplication
@EnableConfigServer
public class Application {
application.properties
server.port=8888
spring.cloud.config.server.git.uri=https://github.com/vogellacompany/
codeexamples-javaweb/
spring.cloud.config.server.git.searchPaths=config
bootstrap.properties
spring.application.name=user
spring.cloud.config.uri=http://localhost:8888
management.security.enabled=false
Besides Eureka, also the Cloud Config Server makes use of this property to find the right
config file in the git repo
Tells this Config Server client where to find the Config Server
Disable security right now for convenience, but it will be added again later ;-)
Now start the Eureka Server, the Config Server and finally the user server.
Look into the console log of the user application and validate that the remote
properties in the git repo have been used.
mkdir config
git init
git add .
Now the Cloud Config Server can point to the local repository by changing
the application.properties of the com.vogella.spring.config project.
server.port=8888
spring.cloud.config.server.git.uri=file://${path-to-repo}
${path-to-repo} has to be replaced by the actual path to the previously created repo
Now again start the Eureka Server, the Config Server and finally the user
server.
Look into the console log of the user application and validate that the remote
properties in the git repo have been used.
dependencies {
compile('org.springframework.boot:spring-boot-starter-security')
testImplementation('org.springframework.security:spring-security-test')
}
User.java
public User(User user) {
this.id = user.id;
this.name = user.name;
this.email = user.email;
this.password = user.password;
this.roles = user.roles;
this.lastLogin = user.lastLogin;
this.enabled = user.enabled;
}
Basically this is a copy constructor to create new User instances by copying the field values
from an existing user object.
package com.vogella.spring.user.security;
import java.util.Collection;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import
org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import com.vogella.spring.user.domain.User;
import com.vogella.spring.user.service.UserService;
import reactor.core.publisher.Mono;
@Component
public class ServiceReactiveUserDetailsService implements
ReactiveUserDetailsService {
@Override
public Mono<UserDetails> findByUsername(String username) {
return userService
.findUserByEmail(username)
.map(CustomUserDetails::new);
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return getRoles()
.stream()
.map(authority -> new SimpleGrantedAuthority(authority))
.collect(Collectors
.toList());
}
@Override
public String getUsername() {
return getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
}
The email adress is supposed to be used for a login and therefore the email is queried
Make use of an adapter for the UserDetails interface so that CustomUserDetails can
be returned in by the overridden findByUsername method.
Map the roles to be authorities
Use the Users email as unique user name
For the other values we simply return true for now. The UserDetails class
also has also an isEnabled and getPassword, which is already being
implemented by the User class.
We also want to use a proper password encoder for our users, which are
created in the UserDataInitializer class.
package com.vogella.spring.user.security;
import org.springframework.context.annotation.Bean;
import
org.springframework.security.config.annotation.method.configuration.EnableReac
tiveMethodSecurity;
import
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecur
ity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories
.createDelegatingPasswordEncoder();
}
}
package com.vogella.spring.user.initialize;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.context.annotation.Profile;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import com.vogella.spring.user.data.UserRepository;
import com.vogella.spring.user.domain.User;
@Profile("!production")
@Component
public class UserDataInitializer implements SmartInitializingSingleton {
@Override
public void afterSingletonsInstantiated() {
User user = new User(1, "Fabian Pfaff", "[email protected]",
passwordEncoder
.encode("fap"),
Collections
.singletonList("ROLE_ADMIN"),
Instant
.now(),
true);
User user2 = new User(2, "Simon Scholz", "[email protected]",
passwordEncoder
.encode("simon"),
Collections
.singletonList("ROLE_ADMIN"),
Instant
.now(),
false);
User user3 = new User(3, "Lars Vogel", "[email protected]",
passwordEncoder
.encode("vogella"),
Collections
.singletonList("ROLE_USER"),
Instant
.now(),
true);
To verify that now the users from the database are used try to navigate
to http://localhost:8080/user and you’ll be redirected to a login form, where
you can type in [email protected] as user and simon as password.
(See UserDataInitializer for other names and passwords)
And now you should be able to see the user json again. == Exercise: Using
JWT token for authentication
Using a form login or http basic authentication has some drawbacks, because
with http basic authentication the user credentials have to be sent together
with each and every request. A form login is also not always suitable in case
you’re not using a browser to access the data.
JWT tokens offer the possibility to exchange bearer tokens for each request,
where authentication is necessary.
package com.vogella.spring.user.security;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import com.vogella.spring.user.domain.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@Component
public class JWTUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private String expirationTime;
To learn more about JWT tokens you can get further information
on https://jwt.io/
The JWTUtil class makes use of the jwt.secret and jwt.expiration properties,
which we’ll add to the bootstrap.properties file for now.
The next thing to do is to create a rest end point to obtain a JWT token, which
can be used for authorization.
package com.vogella.spring.user.controller;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class AuthRequest {
private String email;
private String password;
}
package com.vogella.spring.user.controller;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class AuthResponse {
private String token;
}
package com.vogella.spring.user.controller;
import java.security.Principal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import
org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.vogella.spring.user.domain.User;
import com.vogella.spring.user.security.JWTUtil;
import reactor.core.publisher.Mono;
@RestController
public class AuthRestController {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private ReactiveUserDetailsService userDetailsService;
@PostMapping("/auth")
public Mono<ResponseEntity<AuthResponse>> auth(@RequestBody AuthRequest
ar) {
return userDetailsService
.findByUsername(ar
.getEmail())
.map((userDetails) -> {
if (passwordEncoder
.matches(ar
.getPassword(),
userDetails
.getPassword())) {
return ResponseEntity
.ok(new AuthResponse(jwtUtil
.generateToken((User) userDetails)));
} else {
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.build();
}
});
}
}
With this /auth end point a JWT token can be obtained in case the
sent AuthRequest has correct credentials.
Try to obtain a JWT token by using curl or your favorite rest client:
curl -d '{"email":"[email protected]", "password":"simon"}' -H
"Content-Type: application/json" -X POST http://localhost:8080/auth
{
"token":
"eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJsYXJzLnZvZ2VsQHZvZ2VsbGEuY29tIiwicm9sZSI6WyJS
T0xFX1VTRVIiXSwiZW5hYmxlIjp0cnVlLCJleHAiOjE1NDIzNTQ0MTIsImlhdCI6MTU0MjMyNTYxMn
0.zBVx_-
Npp3y6_6EqIpEVWy4EtQoCo01Ii8lSsI1w3X2imIUkrylTOgabgbNo8HgSunMwCujz1d5uIZ6JuGyc
Qw"
}
Now that you got a valid JWT token the server side has to validate this token
and secure the application in case the token is not valid.
package com.vogella.spring.user.security;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.beans.factory.annotation.Autowired;
import
org.springframework.security.authentication.ReactiveAuthenticationManager;
import
org.springframework.security.authentication.UsernamePasswordAuthenticationToke
n;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import reactor.core.publisher.Mono;
@Component
public class AuthenticationManager implements ReactiveAuthenticationManager {
@Autowired
private JWTUtil jwtUtil;
@Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> roles = claims.get("role", List.class);
UsernamePasswordAuthenticationToken auth = new
UsernamePasswordAuthenticationToken(username, null, roles
.stream().map(authority -> new
SimpleGrantedAuthority(authority)).collect(Collectors.toList()));
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
package com.vogella.spring.user.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import
org.springframework.security.authentication.ReactiveAuthenticationManager;
import
org.springframework.security.authentication.UsernamePasswordAuthenticationToke
n;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextImpl;
import
org.springframework.security.web.server.context.ServerSecurityContextRepositor
y;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
public class SecurityContextRepository implements
ServerSecurityContextRepository{
@Autowired
private ReactiveAuthenticationManager authenticationManager;
@Override
public Mono<Void> save(ServerWebExchange swe, SecurityContext sc) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public Mono<SecurityContext> load(ServerWebExchange swe) {
ServerHttpRequest request = swe.getRequest();
String authHeader =
request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
public class CORSFilter implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOrigins("*").allowedMethods("*").allowedHead
ers("*");
}
}
package com.vogella.spring.user.security;
import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpMethod;
import
org.springframework.security.authentication.ReactiveAuthenticationManager;
import
org.springframework.security.config.annotation.method.configuration.EnableReac
tiveMethodSecurity;
import
org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecur
ity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import
org.springframework.security.web.server.context.ServerSecurityContextRepositor
y;
@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories
.createDelegatingPasswordEncoder();
}
@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity
http,
ReactiveAuthenticationManager authenticationManager,
ServerSecurityContextRepository securityContextRepository) {
return http
.csrf()
.disable()
.formLogin()
.disable()
.httpBasic()
.disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS)
.permitAll()
.pathMatchers("/auth")
.permitAll()
.anyExchange()
.authenticated()
.and()
.build();
}
}
This is the new method, the rest of the class except of the imports stays the same
With this SecurityWebFilterChain you now need to pass the JWT token as
authentication header to the server.
<your-token> must be replaced by your actual token, which you obtained from the /auth rest
end point.
We can say that any delete request can only the done by ADMIN users:
SecurityConfig.java
@Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http,
ReactiveAuthenticationManager authenticationManager,
ServerSecurityContextRepository securityContextRepository) {
return http
.csrf()
.disable()
.formLogin()
.disable()
.httpBasic()
.disable()
.authenticationManager(authenticationManager)
.securityContextRepository(securityContextRepository)
.authorizeExchange()
.pathMatchers(HttpMethod.OPTIONS)
.permitAll()
.pathMatchers("/auth")
.permitAll()
.pathMatchers(HttpMethod.DELETE)
.hasAuthority("ADMIN")
.anyExchange()
.authenticated()
.and()
.build();
The pathMatchers method is also overloaded and you can be more precise
about this, but you can also make use of the @PreAuthorize annotation.
This @PreAuthorize annotation can for example be added to
the deleteUser method in the UserService class.
package com.vogella.spring.user.service;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.ExampleMatcher;
import org.springframework.data.domain.ExampleMatcher.GenericPropertyMatcher;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import com.vogella.spring.user.data.UserRepository;
import com.vogella.spring.user.domain.User;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
public class UserService {
@PreAuthorize("hasAuthority('ADMIN')")
public Mono<Void> deleteUser(long id) {
return userRepository.deleteById(id);
}
Before this method will be invoked the @PreAuthorize checks whether to logged in user
has the ADMIN authority.
You can try this by logging in with different user, which have different
roles/authorities.
54. Exercise: Generating RestDocs
from Tests
In this exercise we’ll generate documentation for our REST api from test
definitions.
build.gradle
buildscript {
ext {
springBootVersion = '2.1.0.RELEASE'
}
repositories {
mavenCentral()
jcenter()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:$
{springBootVersion}")
classpath('org.asciidoctor:asciidoctor-gradle-plugin:1.5.9.2')
}
}
ext {
// ...
snippetsDir = file('build/generated-snippets')
}
dependencies {
// ... other dependencies
asciidoctor('org.springframework.restdocs:spring-restdocs-asciidoctor')
testCompile('org.springframework.restdocs:spring-restdocs-webtestclient')
}
test {
outputs.dir snippetsDir
}
asciidoctor {
inputs.dir snippetsDir
dependsOn test
}
bootJar {
dependsOn asciidoctor
from ("${asciidoctor.outputDir}/html5") {
into 'static/docs'
}
}
package com.vogella.spring.user.controller;
import static
org.springframework.restdocs.request.RequestDocumentation.parameterWithName;
import static
org.springframework.restdocs.request.RequestDocumentation.pathParameters;
import static
org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.docu
ment;
import static
org.springframework.restdocs.webtestclient.WebTestClientRestDocumentation.docu
mentationConfiguration;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.restdocs.JUnitRestDocumentation;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.reactive.server.WebTestClient;
import
org.springframework.test.web.reactive.server.WebTestClient.ResponseSpec;
import com.vogella.spring.user.domain.User;
@RunWith(SpringRunner.class)
@SpringBootTest
public class UserRestDocsControllerTest {
@Rule
public JUnitRestDocumentation restDocumentation = new
JUnitRestDocumentation();
@Autowired
private ApplicationContext context;
@Before
public void setUp() {
this.webTestClient =
WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("/")
.filter(documentationConfiguration(restDocumentation)).build()
;
}
@Test
@WithMockUser
public void shouldReturnUser() throws Exception {
ResponseSpec rs = webTestClient.get().uri("/user/{id}", 1).exchange();
rs.expectStatus().isOk().expectBody(User.class)
.consumeWith(document("sample",
Application demonstrating how to use Spring REST Docs with Spring Framework's
WebTestClient.
cURL request:
include::{snippets}/sample/curl-request.adoc[]
HTTPie request:
include::{snippets}/sample/httpie-request.adoc[]
HTTP request:
include::{snippets}/sample/http-request.adoc[]
Request body:
IMPORTANT: The following snippet is empty because it does not have any request
body.
include::{snippets}/sample/request-body.adoc[]
HTTP response:
include::{snippets}/sample/http-response.adoc[]
Response body:
include::{snippets}/sample/response-body.adoc[]
Path Parameters:
include::{snippets}/sample/path-parameters.adoc[]
There are probably failing tests in your project. Deactivate all other test
classes with @Ignore before proceeding.
./gradlew bootJar
Online Training
fitness_center
Onsite Training
group
Consulting
Appendix A: Copyright, License
and Source code
Copyright © 2012-2019 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.
Licence
Source code