O HATEOAS, é uma das principais constraints arquiteturais do REST e possibilita a navegação entre recursos, que são representações dos modelos de negócio da aplicação. Esse post é bem mão na massa e se quiser aprofundar um pouco mais em conceitos teóricos de HATEOAS confira o nosso post Entendendo HATEOAS. Vamos tomar como base a aplicação desenvolvida no nosso outro post Documentando aplicações REST com SpringBoot e Swagger. Para começar você pode baixar o código deste post aqui e descompactar o arquivo zip e importar na sua IDE preferida ou clonar usando Git:
git clone https://github.com/leandrocgsi/simple-rest-example-swagger.git
Primeiro altere o pom.xml adicionando os trechos destacados por comentários abaixo.
<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>br.com.erudio</groupid> <artifactid>simple-rest-example-hateoas</artifactid> <version>0.0.1-SNAPSHOT</version> <parent> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-parent</artifactid> <version>1.3.3.RELEASE</version> </parent> <dependencies> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-actuator</artifactid> </dependency> <!-- Adicione a dependencia de HATEOAS--> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-hateoas</artifactid> </dependency> <dependency> <groupid>com.mangofactory</groupid> <artifactid>swagger-springmvc</artifactid> <version>1.0.0</version> </dependency> <dependency> <groupid>org.ajar</groupid> <artifactid>swagger-spring-mvc-ui</artifactid> <version>0.4</version> </dependency> <dependency> <groupid>org.apache.tomcat.embed</groupid> <artifactid>tomcat-embed-jasper</artifactid> <scope>provided</scope> </dependency> <!-- Adicione as dependencias de teste --> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-test</artifactid> <scope>test</scope> </dependency> <dependency> <groupid>com.jayway.jsonpath</groupid> <artifactid>json-path</artifactid> <scope>test</scope> </dependency> </dependencies> <properties> <java.version>1.8</java.version> </properties> <build> <plugins> <plugin> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-maven-plugin</artifactid> </plugin> </plugins> </build> <repositories> <repository> <id>spring-releases</id> <url>https://repo.spring.io/libs-release</url> </repository> <repository> <id>jcenter-release</id> <name>jcenter</name> <url>http://oss.jfrog.org/artifactory/oss-release-local/</url> </repository> </repositories> <pluginrepositories> <pluginrepository> <id>spring-releases</id> <url>https://repo.spring.io/libs-release</url> </pluginrepository> </pluginrepositories> </project>
Primeiro vamos alterar a classe Greeting que agora ira extender ResourceSupport.
package br.com.erudio.models; import org.springframework.hateoas.ResourceSupport; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; public class Greeting extends ResourceSupport { private final long idGreeting; private final String content; @JsonCreator public Greeting(@JsonProperty("id") long id, @JsonProperty("content") String content) { this.idGreeting = id; this.content = content; } public long getIdGreeting() { return idGreeting; } public String getContent() { return content; } }
Agora adicione os trechos comentados a classe GreetingController.
package br.com.erudio.web.controllers; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import java.util.concurrent.atomic.AtomicLong; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import br.com.erudio.models.Greeting; @Api(value = "greeting") @RestController @RequestMapping("/greeting") public class GreetingController { private static final String template = "Hello, %s!"; private final AtomicLong counter = new AtomicLong(); @ApiOperation(value = "Show Greeting Message" ) @ResponseStatus(HttpStatus.OK) @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public HttpEntity<greeting> greeting(@RequestParam(value="name", defaultValue="World") String name) { Greeting greeting = new Greeting(counter.incrementAndGet(), String.format(template, name)); // Na prática essa linha adiciona uma auto referência ao próprio endpoint // e apenas esse pequeno trecho de código já é o suficiente para que o endpoint // greeting seja HATEOAS greeting.add(linkTo(methodOn(GreetingController.class).greeting(name)).withSelfRel()); return new ResponseEntity<greeting>(greeting, HttpStatus.OK); } }
Se você iniciar a aplicação e acessar o endereço localhost:8080/greeting verá algo similar a imagem abaixo.
package br.com.erudio.models; import java.io.Serializable; import org.springframework.hateoas.ResourceSupport; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; // Adicione a anotação @JsonIgnoreProperties @JsonIgnoreProperties(ignoreUnknown = true) //Extenda ResourceSupport public class Person extends ResourceSupport implements Serializable{ private static final long serialVersionUID = 1L; // Por padrão implementação HATEOAS do Spring tem um atributo id // como default por isso o ID de nossa entidade deve ser alterado private Long idPerson; private String firstName; private String lastName; private String address; public Person() {} public Long getIdPerson() { return idPerson; } public void setIdPerson(Long id) { this.idPerson = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } }
Agora vamos alterar a classe PersonController:
package br.com.erudio.web.controllers; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import br.com.erudio.models.Person; import br.com.erudio.services.PersonService; @Api(value = "person") @RestController @RequestMapping("/person/") public class PersonController { @Autowired private PersonService personService; @ApiOperation(value = "Find person by ID" ) @ResponseStatus(HttpStatus.OK) @RequestMapping(value = "/{personId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public Person get(@PathVariable(value = "personId") String personId){ Person person = personService.findById(personId); //Adicione uma auto referencia ao método get do controller passando o ID como parâmetro person.add(linkTo(methodOn(PersonController.class).get(personId)).withSelfRel()); return person; } . . . }
Note que ao acessar esse recurso com uma ferramenta como o POSTman teremos uma resultado similar a imagem abaixo.
Agora vamos alterar o findAll:
package br.com.erudio.web.controllers; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import br.com.erudio.models.Person; import br.com.erudio.services.PersonService; @Api(value = "person") @RestController @RequestMapping("/person/") public class PersonController { @Autowired private PersonService personService; . . . @ApiOperation(value = "Find all persons" ) @ResponseStatus(HttpStatus.OK) @RequestMapping(value = "/findAll", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) public List<person> findAll(){ List<person> persons = personService.findAll(); ArrayList<person> personsReturn = new ArrayList<person>(); for (Person person : persons) { String idPerson = person.getIdPerson().toString(); // Adicione uma referencia ao método get do controller passando o ID como parâmetro // isso é feito para todos os elementos da lista person.add(linkTo(methodOn(PersonController.class).get(idPerson)).withSelfRel()); personsReturn.add(person); } return personsReturn; } . . . }
A imagem abaixo nos mostra o resultado dessa mudança. A nossa lista nos tras referencias únicas para cada um dos recursos.
Agora vamos adicionar o suporte a HATEOAS ao verbo POST da nossa aplicação.
package br.com.erudio.web.controllers; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import br.com.erudio.models.Person; import br.com.erudio.services.PersonService; @Api(value = "person") @RestController @RequestMapping("/person/") public class PersonController { @Autowired private PersonService personService; . . . @ApiOperation(value = "Create a new person" ) @ResponseStatus(HttpStatus.OK) @RequestMapping(method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) public Person create(@RequestBody Person person){ Person createdPerson = personService.create(person); String idPerson = createdPerson.getIdPerson().toString(); // Após criarmos um novo recurso do tipo Person nós recuperamos seu ID e adicionamos // uma referencia ao método get do controller passando o ID como parâmetro createdPerson.add(linkTo(methodOn(PersonController.class).get(idPerson)).withSelfRel()); return createdPerson; } . . . }
Como se pode ver na imagem após salvar uma nova pessoa a aplicação retorna um link para que as informações da mesma possam ser acessadas.
Agora vamos modificar o verbo PUT.
package br.com.erudio.web.controllers; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo; import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; 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.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import br.com.erudio.models.Person; import br.com.erudio.services.PersonService; @Api(value = "person") @RestController @RequestMapping("/person/") public class PersonController { @Autowired private PersonService personService; . . . @ApiOperation(value = "Update an existing person") @ResponseStatus(HttpStatus.OK) @RequestMapping(method = RequestMethod.PUT,consumes = MediaType.APPLICATION_JSON_VALUE) public Person update(@RequestBody Person person){ Person updatedPerson = personService.update(person); String idPerson = updatedPerson.getIdPerson().toString(); // Após atualizarmos um recurso nós recuperamos seu ID e adicionamos // uma referencia ao método get do controller passando o ID como parâmetro updatedPerson.add(linkTo(methodOn(PersonController.class).get(idPerson)).withSelfRel()); return updatedPerson; } . . . }
Da mesma forma que com o verbo POST após atualizar uma pessoa a aplicação retorna um link para que as informações da mesma possam ser acessadas.
Como o verbo DELETE exclui um recurso não há necessidade de adicionar suporte a HATEOAS nele. Sendo assim fechamos a nossa implementação e podemos dizer que a nossa API é finalmente RESTful. Assim como nos posts anteriores você pode baixar o código deste post aqui e descompactar o arquivo zip e importar na sua IDE preferida ou clonar usando Git:
git clone https://github.com/leandrocgsi/simple-rest-example-hateoas.git
É isso aí bons estudos e continuem ligados no blog para mais novidades 😉
Treinamentos relacionados com este post
Excelente! Gostando muito dos artigos sobre HATEOAS.