Spring Boot Rest Tutorial
I'm sure you're looking for a complete Spring Rest Tutorial which covers the most important topics related to Spring Boot. You're in the right place!
You want to build a web application or a REST API using Spring Boot (and other popular technologies like Thymeleaf), but you don't know where to start... Let me help you get things done. This tutorial explains how to create a simple Rest Api exposing data as Json.
Don't worry, Spring isn't that difficult! In under 5 minutes, you will build your first web app using Spring Boot.
NOTE: Updated with Spring Boot 2 and Spring 5!
Full source code is available at Spring Boot 2 Demo on Github.
Spring Initializr¶
To get started quickly, please generate a Sample Web project using Spring Initializr. Some of the sections below review part of the code being generated by Spring Initializr.
Even if you could not use Spring Initialzr, you should be able to follow this tutorial.
Maven Module Pom¶
First, we need to create a Maven Project with the following pom.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.octoperf</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Let's understand what's being configured here:
- Parent:
spring-boot-starter-parent
is a convenient Maven Parent POM configured with everything needed to run a Spring Boot application. By defining it as a parent, most dependencies likelombok
orjackson
are already managed (no need to specify a version), -
Dependencies:
- lombok: provides annotations to generate most of the boiler plate code (like constructors, equals and hashcode methods etc.),
jackson-core
: Jackson is a popular Java Json serialization framework,- spring-boot-starter-web is Spring dependencies which imports everything needed to build a web application using Spring Boot.
By adding Jackson as a dependency, Spring Boot automatically configures The Rest endpoints with Jackson serializer.
Please make sure to enable Annotation Processing within the IDE. It can be done in Build > Compiler > Annotation Processors in Intellij (Enable Annotation Processing
, and Obtain processors from project classpath
).
Now, we need to create a main application which will bootstrap Spring Boot.
Spring-Boot Bootstrap¶
If you're already so far, you have a working maven project in your favorite IDE (like Intellij). It's time to create the main application:
package com.octoperf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
I've created the class DemoApplication
in the com.octoperf.demo
package. When the Java application will be run, the execution will be directly delegated to the SpringApplication
class.
SpringApplication
bootstraps and launches a Spring application from a Java main
method. By default it will perform the following steps to bootstrap your
application:
- Create an appropriate ApplicationContext instance (depending on your classpath),
- Register a CommandLinePropertySource to expose command line arguments as Spring properties,
- Refresh the application context, loading all singleton beans,
- And Trigger any CommandLineRunner beans.
What is an ApplicationContext
?
Central interface to provide configuration for an application. This is read-only while the application is running, but may be reloaded if the implementation supports this.
What is a CommandLinePropertySource
?
Abstract base class for {@link PropertySource} implementations backed by command line arguments. The parameterized type T represents the underlying source of command line options.
What is a CommandLineRunner
?
Interface used to indicate that a bean should run when it is contained within a SpringApplication
.
If we take a look at what the @SpringBootApplication
does:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
It's an annotation which enables various features like Spring Boot Auto-Configuration and component scanning.
Spring will automatically discover any Spring annotated class (like @Component
or @Service
) under the com.octoperf.demo
and instantiate them on application startup.
So yeah, there is a lot of magic behind the scene, but no worries! You have plenty of time to master Spring internals once you understand the basics.
Person Bean¶
Let's now create a Person
bean within the same package. This bean represents a Person:
package com.octoperf;
import lombok.Data;
@Data
public class Person {
String firstname, lastname;
}
Thanks to lombok's @Value
annotation, the bean is pretty simple! Lombok takes care of generating the constructor, equals/hashcode methods, getters and define all fields as private final
.
We're going to send this bean over the wire by serializing it into Json.
Spring MVC Controller¶
Now let's expose the Person
bean through a Spring MVC Controller:
package com.octoperf;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/person")
class PersonController {
@GetMapping("/hello")
public Person hello() {
final Person person = new Person();
person.setFirstname("John");
person.setLastname("Smith");
return person;
}
}
This endpoint exposes the path /person/hello
, which returns a Person
instance with John as firstname, and Smith as lastname.
Let's edit the application.properties to configure the server.port
property which controls the port on which the Spring Boot server will run:
server.port=8081
Spring Boot embeds an Apache Tomcat application server by default.
Starting the Demo App¶
It's time to run the application! Open the DemoApplication
class and right-click on it. Then select, Run DemoApplication.main()
. It should start the web application:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.RELEASE)
2018-04-03 16:28:16.378 INFO 186339 --- [ main] com.octoperf.DemoApplication : Starting DemoApplication on desktop with PID 186339 (/home/ubuntu/git/demo/target/classes started by ubuntu in /home/ubuntu/git/demo)
2018-04-03 16:28:16.381 INFO 186339 --- [ main] com.octoperf.DemoApplication : No active profile set, falling back to default profiles: default
2018-04-03 16:28:16.427 INFO 186339 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3c130745: startup date [Tue Apr 03 16:28:16 CEST 2018]; root of context hierarchy
2018-04-03 16:28:17.427 INFO 186339 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2018-04-03 16:28:17.452 INFO 186339 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-04-03 16:28:17.453 INFO 186339 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.28
2018-04-03 16:28:17.467 INFO 186339 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib]
2018-04-03 16:28:17.550 INFO 186339 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-04-03 16:28:17.550 INFO 186339 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1126 ms
2018-04-03 16:28:17.638 INFO 186339 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2018-04-03 16:28:17.641 INFO 186339 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-04-03 16:28:17.642 INFO 186339 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-04-03 16:28:17.642 INFO 186339 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-04-03 16:28:17.642 INFO 186339 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-04-03 16:28:17.918 INFO 186339 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@3c130745: startup date [Tue Apr 03 16:28:16 CEST 2018]; root of context hierarchy
2018-04-03 16:28:17.997 INFO 186339 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/person/hello],methods=[GET]}" onto public com.octoperf.Person com.octoperf.PersonController.hello()
2018-04-03 16:28:18.001 INFO 186339 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-04-03 16:28:18.002 INFO 186339 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-04-03 16:28:18.034 INFO 186339 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-04-03 16:28:18.035 INFO 186339 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-04-03 16:28:18.076 INFO 186339 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-04-03 16:28:18.243 INFO 186339 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-04-03 16:28:18.281 INFO 186339 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2018-04-03 16:28:18.284 INFO 186339 --- [ main] com.octoperf.DemoApplication : Started DemoApplication in 2.267 seconds (JVM running for 2.65)
The output above shows the application command-line output. It states the application is running on port 8081
. The application has started in 2.181 seconds
on my computer.
2018-01-16 14:49:59.373 INFO 35582 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/person/hello],methods=[GET]}" onto public com.octoperf.demo.Person com.octoperf.demo.PersonController.hello()
The line above means the PersonController
has been successfully detected and mapped.
Now let's run a curl
command-line to check if the endpoint is working properly: curl http://localhost:8081/person/hello
.
The output should be:
{"firstname":"John","lastname":"Smith"}
Congratulations, you've just built your first Rest Api using Spring Boot!
Post Rest Endpoint¶
Let's go further by adding a new endpoint to our existing PersonController
:
@PostMapping("/hello")
public String postHello(@RequestBody final Person person) {
return "Hello " + person.getFirstname() + " " + person.getLastname() + "!";
}
The server should respond:
$ curl -XPOST -H 'Content-type: application/json' -d '{"firstname": "John","lastname":"Smith"}' http://localhost:8081/person/hello
Hello John Smith!
Nice! We were able to send an object in Json format and Spring converted it back to the corresponding Java Object using Jackson.
Securing the Endpoints¶
Now, let's secure our endpoints to prevent unauthorized access. We're going to enable Basic Authentication. How does it work?
The client has to send an Authorization
HTTP Header within the request like the following:
Authorization: Basic QWxhZGRpbjpPcGVuU2VzYW1l
The header value starts with the Basic
keyword followed by the username:password
encoded in Base64.
First, we need to add the following Maven dependency to the pom.xml
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
First, let's add the following config to our application.yml
:
spring:
security:
user:
name: admin
password: passw0rd
roles: USER
Then, we must create a Security @Configuration
annotated class:
package com.octoperf;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.httpBasic()
.and()
.authorizeRequests()
.anyRequest()
.authenticated();
}
}
{noop}
prefix tells Spring Security to ignore password encoding in this case.
For more information, see Spring Security 5 Password Encoder for more information. This is new to Spring 5.
Now it's time to restart the web application:
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.0.0.RELEASE)
2018-04-03 16:32:59.059 INFO 188101 --- [ main] com.octoperf.DemoApplication : Starting DemoApplication on desktop with PID 188101 (/home/ubuntu/git/demo/target/classes started by ubuntu in /home/ubuntu/git/demo)
2018-04-03 16:32:59.062 INFO 188101 --- [ main] com.octoperf.DemoApplication : No active profile set, falling back to default profiles: default
2018-04-03 16:32:59.111 INFO 188101 --- [ main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@35ef1869: startup date [Tue Apr 03 16:32:59 CEST 2018]; root of context hierarchy
2018-04-03 16:33:00.185 INFO 188101 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8081 (http)
2018-04-03 16:33:00.210 INFO 188101 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2018-04-03 16:33:00.210 INFO 188101 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet Engine: Apache Tomcat/8.5.28
2018-04-03 16:33:00.220 INFO 188101 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib]
2018-04-03 16:33:00.302 INFO 188101 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2018-04-03 16:33:00.302 INFO 188101 --- [ost-startStop-1] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 1195 ms
2018-04-03 16:33:00.437 INFO 188101 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'characterEncodingFilter' to: [/*]
2018-04-03 16:33:00.438 INFO 188101 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2018-04-03 16:33:00.438 INFO 188101 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2018-04-03 16:33:00.438 INFO 188101 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean : Mapping filter: 'requestContextFilter' to: [/*]
2018-04-03 16:33:00.438 INFO 188101 --- [ost-startStop-1] .s.DelegatingFilterProxyRegistrationBean : Mapping filter: 'springSecurityFilterChain' to: [/*]
2018-04-03 16:33:00.439 INFO 188101 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean : Servlet dispatcherServlet mapped to [/]
2018-04-03 16:33:00.712 INFO 188101 --- [ main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@35ef1869: startup date [Tue Apr 03 16:32:59 CEST 2018]; root of context hierarchy
2018-04-03 16:33:00.790 INFO 188101 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/person/hello],methods=[GET]}" onto public com.octoperf.Person com.octoperf.PersonController.hello()
2018-04-03 16:33:00.791 INFO 188101 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/person/hello],methods=[POST]}" onto public java.lang.String com.octoperf.PersonController.postHello(com.octoperf.Person)
2018-04-03 16:33:00.795 INFO 188101 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2018-04-03 16:33:00.796 INFO 188101 --- [ main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2018-04-03 16:33:00.829 INFO 188101 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-04-03 16:33:00.829 INFO 188101 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-04-03 16:33:00.867 INFO 188101 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2018-04-03 16:33:01.349 INFO 188101 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@7f02251, org.springframework.security.web.context.SecurityContextPersistenceFilter@73877e19, org.springframework.security.web.header.HeaderWriterFilter@30404dba, org.springframework.security.web.csrf.CsrfFilter@53093491, org.springframework.security.web.authentication.logout.LogoutFilter@75b3673, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@7dd00705, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@2b0b4d53, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@6d4a65c6, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5bfc257, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@4443ef6f, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@dffa30b, org.springframework.security.web.session.SessionManagementFilter@4c0884e8, org.springframework.security.web.access.ExceptionTranslationFilter@23ee75c5, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@267517e4]
2018-04-03 16:33:01.436 INFO 188101 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2018-04-03 16:33:01.475 INFO 188101 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path ''
2018-04-03 16:33:01.479 INFO 188101 --- [ main] com.octoperf.DemoApplication : Started DemoApplication in 2.774 seconds (JVM running for 3.16)
2018-04-03 16:33:15.467 INFO 188101 --- [nio-8081-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-04-03 16:33:15.467 INFO 188101 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2018-04-03 16:33:15.490 INFO 188101 --- [nio-8081-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 23 ms
As you see, new log lines related to org.springframework.security.web
have appeared, meaning Spring Security has been enabled. For those who want to understand how the auto-configuration works, see SpringBootWebSecurityConfiguration javadoc.
Now, let's run the curl command again:
curl -XPOST -H 'Content-type: application/json' -d '{"firstname": "John","lastname":"Smith"}' http://localhost:8081/person/hello
{"timestamp":1516112340374,"status":401,"error":"Unauthorized","message":"Full authentication is required to access this resource","path":"/person/hello"}
The server responds by stating the endpoint requires an authentication. Let's try again by specifying the username and password:
curl -XPOST -H 'Content-type: application/json' -d '{"firstname": "John","lastname":"Smith"}' http://admin:passw0rd@localhost:8081/person/hello
Hello John Smith!
Great! The endpoint is now secured by a general Basic Authentication. Of course, when building a real-world web application, you probably want to secure your web-application with a specific access per user. We'll cover this point in a future article.
Business Logic moved to a service¶
The thing is, it's pretty ugly to have the application logic written right inside the controller. Sure, this demo application is simple enough and it does not really matter here. But, when building a fully-fledged web-application using Spring, you have high-level services which do the job for you.
Let's create a simple PersonService
which does the actual job in a sub-package called com.octoperf.demo.service
:
package com.octoperf;
import com.octoperf.demo.Person;
public interface PersonService {
Person johnSmith();
String hello(Person person);
}
The implementation is the following:
package com.octoperf.demo.service;
import com.octoperf.demo.Person;
import org.springframework.stereotype.Service;
@Service
final class DemoPersonService implements PersonService {
@Override
public Person johnSmith() {
final Person person = new Person();
person.setFirstname("John");
person.setLastname("Smith");
return person;
}
@Override
public String hello(final Person person) {
return "Hello " + person.getFirstname() + " " + person.getLastname() + "!";
}
}
In the code above:
DemoPersonService
implementsPersonService
,DemoPersonService
is annotated Spring's@Service
annotation: tells Spring this is a service that needs to be instantiated. A Service is a long running instance which lives as long as the web application is running,- The service is
package protected
: the class is not accessible outside the package, - Only the
PersonService
interface is public.
Now, modify the PersonController
to delegate the work to the service defined above:
package com.octoperf.demo;
import com.octoperf.demo.service.PersonService;
import lombok.AllArgsConstructor;
import lombok.NonNull;
import lombok.experimental.FieldDefaults;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import static lombok.AccessLevel.PACKAGE;
import static lombok.AccessLevel.PRIVATE;
@RestController
@RequestMapping("/person")
@AllArgsConstructor(access = PACKAGE)
@FieldDefaults(level = PRIVATE, makeFinal = true)
class PersonController {
@NonNull
PersonService persons;
@GetMapping("/johnsmith")
public Person hello() {
return persons.johnSmith();
}
@PostMapping("/hello")
public String postHello(@RequestBody final Person person) {
return persons.hello(person);
}
}
A few things have changed:
PersonController
is annotated with@AllArgsConstructor
and@FieldDefaults
lombok annotations: tells lombok to create a constructor for required params, and mark all fields asprivate final
(making them all required). The controller is immutable,@NonNull PersonService persons;
: the service is defined as a field ofPersonController
, and should not be null (nullcheck code written by lombok).
The generated code looks like:
@RestController
@RequestMapping({"/person"})
class PersonController {
@NonNull
private final PersonService persons;
@GetMapping({"/johnsmith"})
public Person hello() {
return this.persons.johnSmith();
}
@PostMapping({"/hello"})
public String postHello(@RequestBody Person person) {
return this.persons.hello(person);
}
@ConstructorProperties({"persons"})
PersonController(@NonNull PersonService persons) {
if (persons == null) {
throw new NullPointerException("persons");
} else {
this.persons = persons;
}
}
}
You see how powerful Lombok
is! Your application logic is inside the PersonService
implementation. Spring automatically:
- Instantiated the
DemoPersonService
, - Instantiated the
PersonController
by providing an instance ofPersonService
to its constructor.
Once you understand all those simple concepts, designing even really big web applications does not differ that much from the model above. It's all about services being exposed through Rest or Web Endpoints. And those services themselves can delegate to sub-services.
RestAssured Unit Test¶
Now, it's time to write a unit test to check our Rest Endpoint. Testing is a critical point to avoid regressions when the code evolves.
First, add the RestAssured test dependency:
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<version>3.0.7</version>
<scope>test</scope>
</dependency>
Then, let's write a JUnit which performs a Rest call using RestAssured:
package com.octoperf;
import io.restassured.RestAssured;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import static io.restassured.RestAssured.get;
import static io.restassured.RestAssured.preemptive;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = { DemoApplication.class }, webEnvironment = RANDOM_PORT)
public class PersonControllerTest {
@Value("${local.server.port}")
private int port;
@Before
public void setUp() {
RestAssured.authentication = preemptive().basic("admin", "passw0rd");
}
@Test
public void shouldSayHello() {
get("http://localhost:" + port + "/person/johnsmith")
.then()
.assertThat()
.statusCode(200)
.body("firstname", Matchers.equalTo("John"))
.and()
.body("lastname", Matchers.equalTo("Smith"));
}
}
The Junit above does several things:
- Runs with
SpringRunner
: the unit test should be embedded into a Spring application, @SpringBootTest
: specifies the bootstrap application to use, and setup a web context on a randomly available port,@Value("${local.server.port}")
: autowires the random web application port, so we can reuse it when performing the Rest call through RestAssured,RestAssured.authentication = preemptive().basic("admin", "passw0rd");
: configures RestAssured to use Basic Authentication as our endpoints have been secured previously.
Now you have a unit-test which automatically spins up an embedded web server with your controller and your services inside. The unit test then performs a real HTTP request to the controller endpoint /person/johnsmith
, and checks the content of the response.
I suggest you deep dive into the RestAssured documentation to further explore testing Rest Endpoints.
Retrofit Unit Test¶
Alternatively to RestAssured, you can use Retrofit. Retrofit is a type-safe HTTP client for Java. In fact, you could use any Java rest-client that suits your needs.
First, add the test dependency:
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>retrofit</artifactId>
<version>2.3.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.squareup.retrofit2</groupId>
<artifactId>converter-jackson</artifactId>
<version>2.3.0</version>
<scope>test</scope>
</dependency>
Please make sure to use latest version at the time you are reading this. The code may vary depending on the future evolutions of the library.
Let's now write the API Interface using Retrofit:
package com.octoperf.demo;
import retrofit2.Call;
import retrofit2.http.GET;
public interface PersonApi {
@GET("/person/johnsmith")
Call<Person> johnSmith();
}
Then, we need a Basic Authentication request interceptor:
package com.octoperf.demo;
import okhttp3.Credentials;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
final class BasicAuthInterceptor implements Interceptor {
private final String credentials;
BasicAuthInterceptor(final String user, final String password) {
this.credentials = Credentials.basic(user, password);
}
@Override
public Response intercept(Chain chain) throws IOException {
final Request request = chain.request();
final Request authenticatedRequest = request.newBuilder()
.header("Authorization", credentials).build();
return chain.proceed(authenticatedRequest);
}
}
And finally, use this interface within the unit test:
package com.octoperf.demo;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.Credentials;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import retrofit2.Retrofit;
import retrofit2.converter.jackson.JacksonConverterFactory;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = { DemoApplication.class }, webEnvironment = RANDOM_PORT)
public class PersonControllerRetrofitTest {
@Value("${local.server.port}")
private int port;
private Retrofit retrofit;
@Before
public void setUp() {
final OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new BasicAuthInterceptor("admin", "passw0rd"))
.build();
retrofit = new Retrofit.Builder()
.baseUrl("http://localhost:"+port)
.client(client)
.addConverterFactory(JacksonConverterFactory.create(new ObjectMapper()))
.build();
}
@Test
public void shouldSayHello() throws IOException {
final PersonApi api = retrofit.create(PersonApi.class);
final Person person = api.johnSmith().execute().body();
assertEquals("John", person.getFirstname());
assertEquals("Smith", person.getLastname());
}
}
It's up to you to choose the Rest client you're the more comfortable with. There are many other clients available (Feign, Resteasy, Spring RestTemplate, UniRest and more).
Exception Handler¶
Spring offers a simple centralized error handling mechanism using @ControllerAdvice
annotation. Here is an example.
First, let's create a DemoException
in package com.octoperf.demo.exception
:
package com.octoperf.demo.exception;
public class DemoException extends Exception {
}
Now, we're going to create the DemoExceptionHandler
:
package com.octoperf.demo.exception;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
class DemoExceptionHandler extends ResponseEntityExceptionHandler {
@ExceptionHandler({ DemoException.class })
protected ResponseEntity<Object> handleNotFound(
Exception ex, WebRequest request) {
return handleExceptionInternal(ex, "Demo Exception Encountered",
new HttpHeaders(), HttpStatus.NOT_FOUND, request);
}
}
The handler basically sends an Http 404 not found when the DemoException
is thrown by any Spring MVC Controller.
Finally, enrich our PersonController
by adding a endpoint to simulate this exception:
@GetMapping("/exception")
public void exception() throws DemoException {
throw new DemoException();
}
Here is the output when executing an HTTP request to this endpoint with curl
:
ubuntu@desktop:~$ curl -v http://admin:passw0rd@localhost:8081/person/exception
* Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 8081 (#0)
* Server auth using Basic with user 'admin'
> GET /person/exception HTTP/1.1
> Host: localhost:8081
> Authorization: Basic YWRtaW46cGFzc3cwcmQ=
> User-Agent: curl/7.47.0
> Accept: */*
>
< HTTP/1.1 404
< Set-Cookie: JSESSIONID=B1AE72512170F07C4D440BF87167C014; Path=/; HttpOnly
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 26
< Date: Tue, 03 Apr 2018 15:12:14 GMT
<
* Connection #0 to host localhost left intact
Demo Exception Encountered
The server properly catches the exception and returns the HTTP 404 as stated in the DemoExceptionHandler
. This way, you can control how the application behaves for each exception being thrown by any Rest Controller.
Even multiple @ControllerAdvice
annotated classes can be defined. Each can be responsible for handling exceptions thrown by a particular part of your web application.
Final Words¶
We have only scratched the surface of what's possible to do with Spring Boot in this tutorial. Spring is a powerful aggregate of dozen of libraries which make developing web applications as easy as it can be. Make sure to explore them each time you have a need, maybe there is a library to do it for you!
Full source code is available at Spring Boot 2 Demo on Github.
Feel free to share your own code examples if you feel like something is missing in this article!