Spring Boot Rest Tutorial

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 like lombok or jackson 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();
  }
}

This configuration tells Spring Boot 2 to enable basic authentication. Note the {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 implements PersonService,
  • 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 as private final (making them all required). The controller is immutable,
  • @NonNull PersonService persons;: the service is defined as a field of PersonController, 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 of PersonService 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!

By - CTO.
Tags: Java Spring Rest Api Http

Comments

B. K. Oxley (binkley)  

This is a lovely post, thank you.

I especially appreciate the clarity of the steps (using numbers is helpful), and giving both Rest-Assured and Retrofit solutions.

Reply

Hector  

Very well explained article, thanks! Could you tell us which specific dependencies did you use in the initializer?
Reply

Jerome
In reply to Hector
 

Hi Hector, I don’t remember anymore but you can download the full source code on Github.

zief  

why it always using generated password rather than password we defined in configuration?
Reply

Jerome
In reply to zief
 

Hi, you can use a generated password if you don’t specify any through spring configuration. See Spring Documentation for more information.
 

Thank you

Your comment has been submitted and will be published once it has been approved.

OK

OOPS!

Your post has failed. Please return to the page and try again. Thank You!

OK