Aplicando HATEOAS em uma API JAX-RS

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.

Java - Fundamentos de JAX-WS e JAX-RS
Curso de Java - Fundamentos de JAX-WS e JAX-RS
CONHEÇA O CURSO

HATEOAS 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:

Enpoint Login com Hateoas retornado no header

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:

Endpoint Pessoa com Hateoas com um link mostrando todas as propriedades da classe Link

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:

Enpoint Pessoa com HATEOAS de um link 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:

Enpoint Pessoa com HATEOAS de um link amigável

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:

Endpoint de Pessoa com HATEOAS com três links

Java - Fundamentos para Web
Curso de Java - Fundamentos para Web
CONHEÇA O CURSO

Conclusã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 🙂

Deixe seu comentário

Instrutor, nerd, cinéfilo e desenvolvedor nas horas vagas. Graduado em Ciências da Computação pela Universidade Metodista de São Paulo.

© 2004 - 2019 TreinaWeb Tecnologia LTDA - CNPJ: 06.156.637/0001-58 Av. Paulista, 1765, Conj 71 e 72 - Bela Vista - São Paulo - SP - 01311-200