© Adam L. Davis 2020
A. L. DavisSpring Quick Reference Guidehttps://doi.org/10.1007/978-1-4842-6144-6_11

11. Spring REST

Adam L. Davis1 
(1)
Oviedo, FL, USA
 

REST (Representational State Transfer) outlines a way of designing web services around resources and metadata using HTTP methods like GET, POST, PUT, and PATCH to map to well-defined actions. It was first defined by Roy Fielding in his 2000 PhD dissertation “Architectural Styles and the Design of Network-based Software Architectures” at UC Irvine.1 A web service that adheres to these principles is called RESTful .

This chapter is primarily about two Spring projects, Spring REST Docs and Spring HATEOAS.2 It builds on the content from Chapter 7, so be sure to read it first before reading this chapter. Although using these projects is not required to build a RESTful web service, using them together with Spring MVC allows you to build a fully featured web API all using Spring.

Spring REST Docs

Spring REST Docs3 generates documentation based on tests combined with text documents using the Asciidoctor syntax, although you may use Markdown instead. This approach is meant to generate API documents, similar to Swagger, but with more flexibility.

Spring REST Docs uses snippets produced by tests written with Spring MVC’s MockMvc, Spring WebFlux’s WebTestClient, or REST Assured 3.4 This test-driven approach helps to guarantee the accuracy of your web service’s documentation. If a snippet is incorrect, the test that produces it fails.

Getting Started

To get started, first add the Spring REST Docs dependency to your project. If using Maven, add the following dependency:
<dependency>
  <groupId>org.springframework.restdocs</groupId>
  <artifactId>spring-restdocs-mockmvc</artifactId>
  <version>2.0.4.RELEASE</version>
  <scope>test</scope>
</dependency>
Also, add the following Maven plugin which will process the asciidoctor text during the prepare-package phase:
<build>
  <plugins>
   <plugin>
      <groupId>org.asciidoctor</groupId>
      <artifactId>asciidoctor-maven-plugin</artifactId>
      <version>1.5.8</version>
      <executions>
        <execution>
         <id>generate-docs</id>
         <phase>prepare-package</phase>
         <goals>
           <goal>process-asciidoc</goal>
         </goals>
         <configuration>
           <backend>html</backend>
           <doctype>book</doctype>
         </configuration>
       </execution>
     </executions>
     <dependencies>
       <dependency>
         <groupId>org.springframework.restdocs</groupId>
         <artifactId>spring-restdocs-asciidoctor</artifactId>
         <version>2.0.4.RELEASE</version>
       </dependency>
      </dependencies>
    </plugin>
  </plugins>
</build>
If using a Gradle build, use the following build file:
plugins {
    id "org.asciidoctor.convert" version "2.4.0"
    id "java"
}
ext {
    snippetsDir = file('build/generated-snippets')
    ver = '2.0.4.RELEASE'
}
dependencies {
asciidoctor "org.springframework.restdocs:spring-restdocs-asciidoctor:$ver"
testCompile "org.springframework.restdocs:spring-restdocs-mockmvc:$ver"
}
test {
    outputs.dir snippetsDir
}
asciidoctor {
    inputs.dir snippetsDir
    dependsOn test
}

REST Docs Generation

To generate REST Docs from an existing Spring MVC–based project, you need to write unit or integration tests for each request/response you want to document and include the JUnitRestDocumentation rule on your test.

For example, define a test using @SpringBootTest, or otherwise set up your application context in the setUp method of your test, and using @Rule, define an instance of JUnitRestDocumentation:
@RunWith(SpringRunner.class)
@SpringBootTest
public class GettingStartedDocumentation {
  @Rule
  public final JUnitRestDocumentation restDocumentation =
                new JUnitRestDocumentation();
Then set up the MockMvc instance
private MockMvc mockMvc;
@Before
public void setUp() {
  this.mockMvc =
  MockMvcBuilders.webAppContextSetup(this.context)
  .apply(documentationConfiguration(this.restDocumentation))
  .alwaysDo(document("{method-name}/{step}/",
        preprocessRequest(prettyPrint()),
                preprocessResponse(prettyPrint())))
        .build();
}
using the following static imports
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
import static
org.springframework.restdocs.mockmvc.MockMvcRestDocumentation
.documentationConfiguration;
For each test method within the JUnit test which uses mockMvc, Spring REST Docs will now create (during the build) a directory named by converting the test’s name from CamelCase to dash-separated names (e.g., creatingACourse becomes creating-a-course) and a number-indexed directory for each HTTP request. For example, if there are four requests in a test you will have directories 1/ 2/ 3/ and 4/. Each HTTP request in turn gets the following snippets generated:
  • curl-request.adoc

  • httpie-request.adoc

  • http-request.adoc

  • http-response.adoc

  • request-body.adoc

  • response-body.adoc

Then, you can write the Asciidoctor documentation under the src/docs/asciidoc/ directory and include the generated snippets into your output, for example:
include::{snippets}/creating-a-course/1/curl-request.adoc[]
This text is included in output.
include::{snippets}/creating-a-course/1/http-response.adoc[]

This would include each of the preceding snippets within your documentation output (typically HTML5 output).

Serving the Documentation in Spring Boot

To serve the HTML5 generated documentation in a Spring Boot–based project, add the following to your Gradle build file:
bootJar {
  dependsOn asciidoctor
  from ("${asciidoctor.outputDir}/html5") {
    into 'static/docs'
  }
}

Spring HATEOAS

Closely related to REST is the concept of Hypermedia as the engine of application state (HATEOAS),5 which outlines how each response from a web service should provide information, or links, that describe other endpoints, much like how websites work. Spring HATEOAS6 helps enable these types of RESTful web services.

Getting Started

To get started, first add the Spring HATEOAS dependency to your project. If using Spring Boot and Maven, add the following dependency:
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
If using Spring Boot with Gradle, use the following dependency:
implementation 'org.springframework.boot:spring-boot-starter-hateoas'

Creating Links

The key part of HATEOAS is the link, which can contain a URI or URI template and allows the client to easily navigate the REST API and provides for future compatibility – the client can use the link allowing the server to change where that link points.

Spring HATEOAS supplies methods for easily creating Links, such as the LinkBuilder and WebMvcLinkBuilder. It also supplies models for representing Links in the response, such as EntityModel, PagedModel, CollectionModel, and RepresentationModel. Which model to use depends on what type of data you are returning one entity (EntityModel), pages of data (PagedModel), or others.

Let’s take one example using the WebMvcLinkBuilder and the EntityModel:
package com.apress.spring_quick.rest;
import org.springframework.hateoas.EntityModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
@RestController
public class GettingStartedController {
    @GetMapping("/")
    public EntityModel<Customer> getCustomer() {
        return EntityModel.of(new Customer("John", "Doe"))
.add(linkTo(GettingStartedController.class).withSelfRel())
    .add(linkTo(GettingStartedController.class)
    .slash("next").withRel("next"));
    }
}
At runtime, this endpoint would return the following as JSON (when running locally):
{
  "firstname":"John",
  "lastname":"Doe",
  "_links":{
    "self":{"href":"http://localhost:8080"},
    "next":{"href":"http://localhost:8080/next"}
  }
}
Hypertext Application Language
Hypertext Application Language (HAL)7 is a draft standard for defining hypermedia such as links to external resources within JSON or XML code. The standard was initially proposed on June 2012 specifically for use with JSON and has since become available in two variations, JSON and XML. The two associated MIME types are media type: application/hal+xml and media type: application/hal+json. HAL consists of resources and links. It can have embedded resources which also have links. For example, if a Course has many tests, you might see HAL JSON like the following:
{
    "_links": {
        "self": { "href": "http://localhost:8080/courses" },
        "next": { "href": "http://localhost:8080/courses?page=2" },
        "my:find": {
            "href": "http://localhost:8080/courses/{?name}",
            "templated": true
        }
    },
    "total": 14,
    "_embedded": {}
}

Testing

Testing the HATEOAS output can be achieved similarly to testing any web service that produces XML or JSON.

In the common case where your service produces JSON, it would be helpful to use a library to navigate the JSON in a similar way to how XPath navigates XML documents, JsonPath. One library that implements JsonPath in Java is Jayway JsonPath.8 Although you can use it directly, Spring wraps the usage of JsonPath with the static MockMvcResultMatchers.jsonPath method for ease of use with Hamcrest matchers.

To use JsonPath, we simply need to include a dependency in the Maven pom:
<dependency>
    <groupId>com.jayway.jsonpath</groupId>
    <artifactId>json-path</artifactId>
    <version>2.4.0</version>
    <scope>test</scope>
</dependency>
Or if using Gradle, include
testCompile 'com.jayway.jsonpath:json-path:2.4.0'
For example, see the following JUnit test class which uses JsonPath to validate that _links.self and _links.next are not null:
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.hateoas.MediaTypes;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.hamcrest.Matchers.*;
@ExtendWith(SpringExtension.class) // JUnit 5
@SpringBootTest
public class GettingStartedDocumentation {
  @Autowired
  private WebApplicationContext context;
  private MockMvc mockMvc;
  @BeforeEach
  public void setUp() {
    this.mockMvc = MockMvcBuilders.webAppContextSetup(this.context)
        .build();
  }
  @Test
  public void index() throws Exception {
    this.mockMvc.perform(get("/").accept(MediaTypes.HAL_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("_links.self", is(notNullValue())))
        .andExpect(jsonPath("_links.next", is(notNullValue())));
  }
}
Listing 11-1

GettingStartedDocumentation.java