Spring Boot REST HATEOAS
Problem:
How to create simple Spring Boot REST service with HATEOAS support?
It’s very simple with spring-boot-hateoas project.
Solution:
Project configuration for Gradle (build.gradle):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
apply plugin: 'java'
apply plugin: 'groovy'
apply plugin: 'idea'
apply plugin: 'spring-boot'
sourceCompatibility = 1.8
targetCompatibility = 1.8
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.2.3.RELEASE")
}
}
jar {
baseName = 'spring-boot-rest-hateoas-sample'
version = '0.1.0'
}
repositories {
mavenCentral()
}
dependencies {
// Dependencies for Spring Boot with REST HATEOAS:
compile "com.fasterxml.jackson.core:jackson-databind"
compile "com.jayway.jsonpath:json-path:0.9.1"
compile "org.springframework.boot:spring-boot-starter-hateoas"
// Dependencies for Spock tests:
compile "org.codehaus.groovy:groovy-all:2.4.1"
testCompile "org.spockframework:spock-core:1.0-groovy-2.4"
testCompile "org.spockframework:spock-spring:1.0-groovy-2.4"
testCompile "org.springframework.boot:spring-boot-starter-test"
}
In TDD we start with a test, so create src/test/groovy/com/farenda/java/spring/EchoServiceTest.groovy with the following content:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
package com.farenda.java.spring
import com.fasterxml.jackson.databind.ObjectMapper
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.boot.test.TestRestTemplate
import org.springframework.boot.test.WebIntegrationTest
import org.springframework.hateoas.MediaTypes
import org.springframework.hateoas.ResourceSupport
import org.springframework.hateoas.hal.Jackson2HalModule
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter
import org.springframework.http.HttpStatus
import org.springframework.http.converter.HttpMessageConverter
import org.springframework.test.context.ContextConfiguration
import spock.lang.Shared
import spock.lang.Specification
// Need to specify "loader" here to workaround Spock issue
// with meta annotations:
// https://code.google.com/p/spock/issues/detail?id=349
@ContextConfiguration(classes = [HateoasExample],
loader = SpringApplicationContextLoader)
// Tell Spring to start Web Server on random port, because
// usual 8080 may be busy:
@WebIntegrationTest(randomPort = true)
class EchoServiceTest extends Specification {
// Inject random port of test Web Server:
@Value('${local.server.port}')
def int serverPort
@Shared
def rest = getRestTemplateWithHalMessageConverter()
// Registered converters are processed in order, so it's
// not enough to add the new one - we need to add it at
// the beginning, so it can process before generic JSON
// converter, else HAL links won't be interpreted!
def TestRestTemplate getRestTemplateWithHalMessageConverter() {
def restTemplate = new TestRestTemplate()
restTemplate.getMessageConverters().add(0, halMessageConverter())
return restTemplate
}
// Needed to interpret HAL "_links". Without this
// response will be with empty links!
private static HttpMessageConverter halMessageConverter() {
def objectMapper = new ObjectMapper().registerModule(new Jackson2HalModule())
def halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport)
halConverter.setSupportedMediaTypes([MediaTypes.HAL_JSON])
halConverter.setObjectMapper(objectMapper)
return halConverter
}
def 'should receive sent message with self link'() {
given:
// toString() is only to convert from GString to a String:
def url = "http://localhost:${serverPort}/echo?message=".toString()
def query = url + '{message}'
def myMessage = 'HelloWorld!'
when:
// Post GET to the service and receive response as ResponseEntity.
// We want ResponseEntity to verify HttpStatus code and links.
// The last parameter can also be a map: [message:myMessage]
def response = rest.getForEntity(query, EchoResource, myMessage)
then:
response.statusCode == HttpStatus.OK
response.body.message == myMessage
response.body.getLinks().size() == 1
response.body.getLink('self').href == url+myMessage
}
}
Now, we’ve got a failing test. So here’s the service:
Spring Boot config for the app:
1
2
3
4
5
6
7
8
9
10
11
package com.farenda.java.spring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HateoasExample {
public static void main(String[] args) throws Exception {
SpringApplication.run(HateoasExample.class, args);
}
}
@SpringBootApplication gives @Component, @Configuration, and @EnableAutoConfiguration for free.
REST HATEOAS resource:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.farenda.java.spring;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.springframework.hateoas.ResourceSupport;
// Created separate class for "only" message to have
// also HATEOAS "links" from ResourceSupport - see
// EchoService for usage.
public class EchoResource extends ResourceSupport {
private final String message;
@JsonCreator
public EchoResource(@JsonProperty("message") String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
And, finally, Spring REST HATEOAS service:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.farenda.java.spring;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;
// It's like @Controller, but automatically assumes @ResponseBody.
@RestController
public class EchoService {
@RequestMapping("/echo")
public HttpEntity<EchoResource> echo(
@RequestParam("message") String message) {
EchoResource resource = new EchoResource(message);
// Nice DSL for creating links. Here we don't have
// other actions, so the only link is "self":
resource.add(linkTo(methodOn(EchoService.class).echo(message))
.withSelfRel());
return new ResponseEntity<>(resource, HttpStatus.OK);
}
}
Result:
You can run Spock tests as usual:
1
$> gradle test
Also you can start the app and verify it:
1
2
3
4
5
6
7
8
9
10
$> gradle bootRun
$> curl http://localhost:8080/echo?message=Drakaris!
{
"message": "Drakaris!",
"_links": {
"self": {
"href": "http://localhost:8080/echo?message=Drakaris!"
}
}
}
This post is licensed under CC BY 4.0 by the author.