HATEOAS is a constraint of the REST application architecture which is very often ignored by developers. In many cases this is caused by the lack of support of frameworks used to build and expose RESTful services. In this post I will show how quickly we can add HATEOAS to RESTful web services using Spring Framework. Source code of the demo web application presented below are available for download, run it using mvn tomcat7:run
command. Also you can read a nice presentation about why HATEOAS is important.
The Spring HATEOAS Library (currently in Release 0.6) provides API to help creating REST representations that follow the HATEOAS principle. The core problem it tries to address is link creation and representation assembly. Let's see the main steps required for enabling the HATEOAS for a RESTful web service:
- Add support for hypermedia information to exposed resources. This is done by inheriting resource's classes from
ResourceSupport
. As result you get the support for addingLink
(s) to the resources. Below you can see the resource class representing an author:
and this is an example of adding a link to it:import org.springframework.hateoas.ResourceSupport; public class AuthorResource extends ResourceSupport { private int authorId; private String name; public AuthorResource() { } public AuthorResource(int authorId, String name) { this.authorId = authorId; this.name = name; } public int getAuthorId() { return authorId; } public void setAuthorId(int authorId) { this.authorId = authorId; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
TheAuthorResource resource = new AuthorResource(123, "Joshua Bloch"); resource.add(new Link("http://localhost:8080/hateoas-demo/authors/123"));
Link
value object follows the Atom link definition and consists of arel
and anhref
attribute. - Building links. Spring Hateoas provides a
ControllerLinkBuilder
that allows to create links by pointing to controller classes:
The builder inspects the given controller class for its root mapping and it frees developer from ugly manual string concatenation code (protocol, hostname, port, servlet base, etc.).import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; resource.add( linkTo(AuthorController.class) .slash(author.getAuthorId()) .slash("books") .withRel("books") ); // or by pointing directly to a controller method resource.add( linkTo(methodOn(AuthorController.class).getAuthorBooks(author.getAuthorId())) .withRel("books") );
- Encapsulate resource creation in a separate class. Spring Hateoas provides a
ResourceAssemblerSupport
base class that helps reducing the amount of code needed to be written for mapping from an entity to a resource type and adding respective links. The assembler can then be used to either assemble a single resource or anIterable
of them. You can see below the resource assembler for author resource:import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import org.springframework.hateoas.mvc.ResourceAssemblerSupport; import org.springframework.stereotype.Component; @Component public class AuthorResourceAssembler extends ResourceAssemblerSupport<Author, AuthorResource> { public AuthorResourceAssembler() { super(AuthorController.class, AuthorResource.class); } @Override public AuthorResource toResource(Author author) { // will add also a link with rel self pointing itself AuthorResource resource = createResourceWithId( author.getAuthorId(), author); // adding a link with rel books pointing to the author's books resource.add( linkTo(methodOn(AuthorController.class) .getAuthorBooks(author.getAuthorId())) .withRel("books")); return resource; } @Override protected AuthorResource instantiateResource(Author author) { return new AuthorResource(author.getAuthorId(), author.getName()); } }
- Exposing resources. This is achieved by writing the actual controller. Nothing special here. Just invoke the business logic services (in our case
BookRepository
) and map the business entities to their resource representations using resource assemblers:import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping("/authors") public class AuthorController extends AbstractController { @Autowired private BookRepository bookRepository; @Autowired private AuthorResourceAssembler authorResourceAssembler; @Autowired private BookResourceAssembler bookResourceAssembler; @RequestMapping(method = RequestMethod.POST) @ResponseBody public ResponseEntity<Void> createNewAuthor( @RequestBody NewAuthor newAuthor) { Author author = bookRepository.createNewAuthor(newAuthor.getName()); HttpHeaders headers = new HttpHeaders(); headers.setLocation( linkTo(methodOn(getClass()).getAuthor(author.getAuthorId())) .toUri()); return new ResponseEntity<Void>(headers, HttpStatus.CREATED); } @RequestMapping(method = RequestMethod.GET) @ResponseBody public List<AuthorResource> getAuthors() { return authorResourceAssembler.toResources( bookRepository.findAuthors()); } @RequestMapping(value = "/{authorId}", method = RequestMethod.GET) @ResponseBody public AuthorResource getAuthor(@PathVariable("authorId") int authorId) { return authorResourceAssembler.toResource( findAuthorAndValidate(authorId)); } @RequestMapping(value = "/{authorId}/books", method = RequestMethod.GET) @ResponseBody public List<BookResource> getAuthorBooks( @PathVariable("authorId") int authorId) { return bookResourceAssembler.toResources( findAuthorAndValidate(authorId).getBooks()); } private Author findAuthorAndValidate(int authorId) { Author author = bookRepository.findAuthor(authorId); if (author == null) { throw new ResourceNotFoundException( "Unable to find author with id=" + authorId); } return author; } }
- Testing. Let's eat our own dog food:
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.util.List; import org.junit.Ignore; import org.junit.Test; import org.springframework.hateoas.Link; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.client.RestTemplate; public class AuthorControllerTest { private static final String BASE_URI = "http://localhost:8080/hateoas-demo"; @Test public void createNewAuthor() { RestTemplate restTemplate = new RestTemplate(); // creating new author NewAuthor newAuthor = new NewAuthor("Brian Goetz"); ResponseEntity<Void> response = restTemplate.postForEntity( BASE_URI + "/authors", newAuthor, Void.class); assertEquals(HttpStatus.CREATED, response.getStatusCode()); // retrieve the author details using the URI received in Location header AuthorResource author = restTemplate.getForObject( response.getHeaders().getLocation(), AuthorResource.class); assertEquals(newAuthor.getName(), author.getName()); assertTrue(author.getAuthorId() > 0); // getting the author's books using the link with rel books Link authorBooksLink = author.getLink("books"); List<BookResource> authorBooks = restTemplate.getForObject( authorBooksLink.getHref(), List.class); assertTrue(authorBooks.isEmpty()); } }
- How it looks like: (output formatted using JSON Formatter Chrome extension)
- That's it, hyperlink it!
We're using MockMvc to test our services rather that using restTemplate:
ReplyDelete@Autowired
private WebApplicationContext ctx
private MockMvc mockMvc
def setup( ) {
mockMvc = MockMvcBuilders.webAppContextSetup( ctx ).build()
}
...
String result = mockMvc.perform( get( "/people/1" ).accept( PersonServiceMediaType.PERSONV1JSON ) )
.andDo( print() )
.andExpect( status().isOk() )
.andExpect( content().contentType( PersonServiceMediaType.PERSONV1JSON ) )
.andReturn().response.contentAsString
def parsedResult = JSONValue.parse( result )
assert parsedResult.forename == 'Hugh'
assert parsedResult.dateOfBirth == '1977-03-29'
Yes, you can test them using MockMvc also. We are using MockMvc in our integration tests and RestTemplate in our acceptance tests.
DeleteNice article. Very clear and concise. Look forward to trying your example.
ReplyDeletenice job! simple and to the point.
ReplyDeleteThanks!
DeleteHi,
ReplyDeleteI read your article.Its clean and precise. I have a question in connection to this.
Please see the below Response
{"enrollment": {"optionId": "AETNA"
, "actions": [{"method": "PUT", "uri": "/12345/54321/processes/111222/
, "dependentParticipation":[
{"dependentId": "1001", "enrolled":true ,"effectiveStartDate":"2010/04/04", "effectiveEndDate":"999/12/31"}
,{"dependentId": "1002", "enrolled":true ,"effectiveStartDate":"2010/04/04", "effectiveEndDate":"999/12/31"}
]}
}
I am able to get the above output without the actions. Can i get that actions inserted in the output without creating an actions object.
i geeting this compile time error
ReplyDeleteType mismatch: cannot convert from ControllerLinkBuilder to Link
Learn when and how to replace coil springs in your car or truck. Replacing coil springs explained in detail. View our simple step by step guide on how to replace coil springs in your own garage. Guesthouse in springs
ReplyDeleteEverything is a very open and very clear clarification of the issues. It contains true facts. Your website is very valuable. Thanks for sharing.
ReplyDeleteelectric masturbator cup
pg slot ให้บริการเกมสล็อตออนไลน์บนโทรศัพท์ที่มีเกมให้เลือก เป็นเกมรูปแบบใหม่ที่ทำเงินให้ผู้เล่นได้เงินจริงการเล่นเกมง่าย มีแนวทางสอนการเล่นเกมสล็อตออนไลน์สำหรับมือใหม่
ReplyDelete