HATEOAS (Hypermedia as the Engine of Application State) é um requisito da arquitetura REST, onde os recursos de uma API deve retornar nas suas respostas links (URI) que indicam como acessar outros recursos e/ou aplicar outras ações nos recursos acessados.
Desta forma, o client necessita conhecer apenas o endpoint principal da API para poder navegar nela. Ao implementar HATEOAS, este endpoint e todos os demais irão retornar links que permitirá ao client explorar todos os recursos fornecidos.
Existem algumas formas de implementar este recurso em API JAX-RS, que veremos a seguir.
Curso Java - Fundamentos de JAX-WS e JAX-RS
Conhecer o cursoHATEOAS no JAX-RS com UriBuilder e Link
Para a implementação do HATEOAS, o JAX-RS fornece duas classes: UriBuilder
e Link
. Como o nome sugere, a UriBuilder
facilita a criação de uma URI, enquanto Link
é uma representação de um recurso relacionado que atende a especificação RFC 5988.
Para compreendê-los, vamos ver um exemplo prático.
Neste artigo, utilizarei de exemplo a API RESTful implementada em JAX-RS API já apresentada anteriormente. Como ela implementa autenticação JWT, o seu ponto de entrada será o endpoint de login, que no momento retorna apenas o token de acesso:
@POST
@Produces(MediaType.TEXT_PLAIN)
@Consumes(MediaType.APPLICATION_JSON)
public Response post(Usuario usuario)
{
try{
if(usuario.getUsuario().equals("teste@treinaweb.com.br")
&& usuario.getSenha().equals("1234"))
{
String jwtToken = Jwts.builder()
.setSubject(usuario.getUsuario())
.setIssuer("localhost:8080")
.setIssuedAt(new Date())
.setExpiration(
Date.from(
LocalDateTime.now()
.plusMinutes(15L)
.atZone(ZoneId.systemDefault())
.toInstant()
)
)
.signWith(CHAVE, SignatureAlgorithm.HS512)
.compact();
return Response.status(Response.Status.OK).entity(jwtToken).build();
}
else
return Response
.status(Response.Status.UNAUTHORIZED)
.entity("Usuário e/ou senha inválidos")
.build();
}
catch(Exception ex)
{
return Response
.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(ex.getMessage())
.build();
}
}
A partir dele, ao ser autenticado, o client poderá acessar as pessoas cadastradas. Para indicar isso, podemos utilizar a classe UriBuilder
para criar a URI deste endpoint:
UriBuilder.fromUri("http://localhost:8080/")
.path("pessoa")
.build();
Este endpoint não define parâmetros, mas caso houve isso poderia ser indicado:
UriBuilder.fromUri("http://localhost:8080/")
.path("pessoa/{id}")
.build(2);
Desta forma, a URI criada seria:
http://localhost:8080/pessoa/2
Da mesma forma, também poderia ser adicionadas querystrings:
UriBuilder.fromUri("http://localhost:8080/")
.path("pessoa/{id}")
.queryParam("q", "{nome}")
.build(2, "Carlos");
Neste caso, a URI seria:
http://localhost:8080/pessoa/2?q=Carlos
Para definir a URI do nosso endpoint, além da classe UriBuilder
, faremos uso da classe Link
:
Link link = Link.fromUriBuilder(
UriBuilder.fromUri("http://localhost:8080/")
.path("pessoa")
)
.rel("lista_pessoas")
.type("GET")
.build();
Note que esta classe, além do UriBuilder
, define a relação da URI com o endpoint atual (de login) e o tipo da solicitação.
Para que este dado seja retornado na resposta da solicitação, ele deve ser informado no método link
da classe Response
:
return Response.status(Response.Status.OK).entity(jwtToken).links(link).build();
Ao fazer isso, esta informação estará no header da resposta:
Este é o formato que a especificação RFC 5899 define. Porém este não é o usual, o client normalmente espera esta informação no corpo da resposta. Veremos como fazer isso conhecendo o recursos para HATEOAS do Jersey.
Problemas do HATEOAS no corpo da resposta
Para que o link do HATEOAS também seja mostrado no corpo de uma resposta, é necessário que ele seja definido como um campo do recurso:
public class Pessoa {
private int id;
private String nome;
private int idade;
private Link link;
public Link getLink() {
return link;
}
public void setLink(Link link) {
this.link = link;
}
//..Código omitido
}
E ao retornar este recurso, este campo deve ser preenchido:
@Authorize
@GET
@Path("{id}")
@Produces(MediaType.APPLICATION_JSON)
public Pessoa getById(@PathParam("id") int id) {
Pessoa pessoa = _repositorio.Get(id);
pessoa.setLink(
Link.fromUriBuilder(
UriBuilder.fromUri("http://localhost:8080/")
.path("pessoa/{id}")
)
.rel("self")
.type("GET")
.build(id)
);
return pessoa;
}
Com isso, no corpo da resposta desta solicitação, o link será informado:
Entretanto, um problema disso é que também são retornadas algumas informações indesejadas. Para que isso seja resolvido, é necessário alterar json provider para o Jackson:
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
</dependency>
Registrá-lo:
public static HttpServer startServer() {
final ResourceConfig rc = new ResourceConfig().packages("br.com.treinaweb");
rc.register(JacksonFeature.class);
return GrizzlyHttpServerFactory.createHttpServer(URI.create(BASE_URI), rc);
}
E por fim, criar um Adapter:
public class LinkAdapter extends XmlAdapter<LinkJaxb, Link> {
public LinkAdapter() {}
public Link unmarshal(LinkJaxb p1) {
Link.Builder builder = Link
.fromUri(p1.getUri())
.rel(p1.getRel())
.type(p1.getType());
return builder.build();
}
public LinkJaxb marshal(Link p1) {
return new LinkJaxb(
p1.getUri(),
p1.getRel(),
p1.getType()
);
}
}
class LinkJaxb {
private URI uri;
private String rel;
private String type;
public LinkJaxb() {
this(null, null, null);
}
public LinkJaxb(URI uri,
String rel,
String type)
{
this.uri = uri;
this.rel = rel;
this.type = type;
}
@XmlAttribute(name = "href")
public URI getUri() {
return uri;
}
public void setUri(URI uri) {
this.uri = uri;
}
@XmlAttribute(name = "rel")
public String getRel(){
return rel;
}
public void setRel(String rel) {
this.rel = rel;
}
@XmlAttribute(name = "type")
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
Que deve ser informado no campo do recurso:
@XmlJavaTypeAdapter(LinkAdapter.class)
private Link link;
Agora, o link retornado será mais amigável:
Entretanto, até agora foi necessário definir “manualmente” o link do recurso. Imagine quando for uma lista, este procedimento será bem “chato”. Felizmente o Jersey tem a solução para este problema.
HATEOAS no Jersey com @InjectLink
Para resolver o trabalho de criar um link individualmente para cada recurso, o Jersey fornece módulo Declarative Linking, que define anotação @InjectLink
, que pode ser aplicada diretamente no campo Link
:
@InjectLink(
resource = PessoaResource.class,
style = Style.ABSOLUTE,
rel = "self",
bindings = @Binding(name = "id", value = "${instance.id}"),
method = "GET"
)
@XmlJavaTypeAdapter(LinkAdapter.class)
private Link link;
Este módulo requer a dependência abaixo:
<dependency>
<groupId>org.glassfish.jersey.ext</groupId>
<artifactId>jersey-declarative-linking</artifactId>
</dependency>
Com isso não é necessário criá-lo manualmente:
@Authorize
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public Response post(Pessoa pessoa)
{
try{
_repositorio.Add(pessoa);
return Response.status(Response.Status.CREATED).entity(pessoa).build();
}
catch(Exception ex)
{
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(ex.getMessage()).build();
}
}
Pois será adicionado automaticamente pelo Jersey:
Caso o recurso possua vários links, eles podem ser agrupados em uma lista:
List<Link> links;
E esta lista pode receber a anotação @InjectLinks
:
@InjectLinks({
@InjectLink(
resource = PessoaResource.class,
style = Style.ABSOLUTE,
rel = "self",
bindings = @Binding(name = "id", value = "${instance.id}"),
method = "GET"
),
@InjectLink(
resource = PessoaResource.class,
style = Style.ABSOLUTE,
rel = "update",
bindings = @Binding(name = "id", value = "${instance.id}"),
method = "PUT"
),
@InjectLink(
resource = PessoaResource.class,
style = Style.ABSOLUTE,
rel = "delete",
bindings = @Binding(name = "id", value = "${instance.id}"),
method = "DELETE"
)
})
@XmlJavaTypeAdapter(LinkAdapter.class)
List<Link> links;
Ao acessar o recurso, ele retornará todos esses links:
Curso Java - Fundamentos
Conhecer o cursoConclusão
O HATEOAS possui pontos negativos e positivos (que não foram abordados aqui pois este não é o objetivo do artigo), entretanto é um novo padrão de projeto que facilita a compreensão de uma API RESTful. Como a sua implementação não é muito complexa, é algo que deve ser avaliado e sempre que possível implementado nas aplicações.
Por hoje é só, até a próxima :)