HATEOAS

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 de JAX-WS e JAX-RS
Curso de Java - Fundamentos de JAX-WS e JAX-RS
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 🙂

O que é HATEOAS?

HATEOAS é uma restrição que faz parte da arquitetura de aplicações REST, cujo objetivo é ajudar os clientes a consumirem o serviço sem a necessidade de conhecimento prévio profundo da API.

O acrônimo HATEOAS vem de Hypermedia As the Engine Of Application State e o termo “hypermedia” no seu nome já dá uma ideia de como este componente funciona em uma aplicação RESTful. Ao ser implementado, a API passa a fornecer links que indicarão aos clientes como navegar através dos seus recursos.

Com isso, o cliente não precisa ter um conhecimento profundo da API, basta conhecer a URL de inicial e partir dos links fornecidos poderá acessar todos os recursos de forma circular, se guiando através das requisições realizadas.

Um exemplo clássico para explicar o HATEOAS, é o Hypertext, do HTML. Quando se necessita obter uma informação de um site, como um curso aqui da Treinaweb, o usuário pode acessar a página inicial (treinaweb.com.br), nela clicar em Cursos e depois no curso desejado. No fim, o usuário terá acessado uma URL como treinaweb.com.br/curso/java-fundamentos-de-jax-ws-e-jax-rs, onde conseguirá visualizar o curso. Não foi necessário que o usuário soubesse de antemão a URL desejada, pois é importante que o site, através de links, guie-o e auxilie-o na busca do recurso desejado. Com o HATEOAS uma API REST segue o mesmo padrão.

Um exemplo simples do HATEOAS

Por exemplo, ao acessar uma API de cursos, na URL api.treinaweb.com.br/cursos, o cliente terá o seguinte resultado:

{
    "cursos": [
        {
            "id": 1,
            "nome": "C# (C Sharp)",
            "aulas": [
                {
                    "id": 1,
                    "titulo": "Título da aula 3"
                },
                {
                    "id": 2,
                    "titulo": "Título da aula 3"
                },
                {
                    "id": 3,
                    "titulo": "Título da aula 3"
                },
            ]
        },
        {
            "id": 2,
            "nome": "PHP",
            "aulas": [
                {
                    "id": 1,
                    "titulo": "Título da aula 3"
                },
                {
                    "id": 2,
                    "titulo": "Título da aula 3"
                },
                {
                    "id": 3,
                    "titulo": "Título da aula 3"
                },
            ]
        },
        {
            "id": 3,
            "nome": "Java",
            "aulas": [
                {
                    "id": 1,
                    "titulo": "Título da aula 3"
                },
                {
                    "id": 2,
                    "titulo": "Título da aula 3"
                },
                {
                    "id": 3,
                    "titulo": "Título da aula 3"
                },
            ]
        },
    ]
}

Se esta API implementasse o padrão HATEOAS, a resposta obtida poderia ser outra. No lugar de listar as aulas, cada curso teria um recurso próprio que retornaria suas aulas. Evitando assim que elas fiquem expostas do corpo da requisição. Algo como o exemplo abaixo:

{
    "cursos": [
        {
            "id": 1,
            "nome": "C# (C Sharp)",
            "aulas": "api.treinaweb.com.br/cursos/1/aulas"
        },
        {
            "id": 2,
            "nome": "PHP",
            "aulas": "api.treinaweb.com.br/cursos/2/aulas"
        },
        {
            "id": 3,
            "nome": "Java",
            "aulas": "api.treinaweb.com.br/cursos/3/aulas"
        },
    ]
}

Desta forma, se o cliente desejasse obter as aulas do curso de PHP, em seguida acessaria a URL api.treinaweb.com.br/cursos/2/aulas. Note que nem é necessário montar a URL, a API já fornece o caminho correto completo.

Java - Criação de aplicações web com Spring Boot
Curso de Java - Criação de aplicações web com Spring Boot
CONHEÇA O CURSO

Especificação do HATEOAS

No exemplo anterior, o HATEOAS foi exemplificado como uma URL na resposta da API, entretanto, na prática, recomenda-se que a implementação da HATEOAS siga um dos padrões abaixo:

RFC 5988

A especificação RFC 5988 da IETF define como links devem ser implementados. De acordo com ela, cada link deve ter as informações:

  • URI: Cada link deve conter uma URI, representada no atributo href;
  • Tipo de relação: Descreve como a URI se relaciona com o recurso atual, representado pelo atributo rel, devidado de relationship;
  • Atributos para URI: Para descrever melhor a URI podem ser adicionados atributos como: hreflang, media, title e type.

JSON Hypermedia API Language (HAL)

JSON HAL é uma especificação, proposta por Mike Kelly, ainda não aprovada, mas já comumente utilizada, que define dois MIME Types:

application/hal+xml
application/hal+json

Que ao serem enviados na solicitação, a API REST deve retornar uma propriedade links, contendo as informações:

  • URI: A URI do recurso, representada pelo atributo href;
  • Tipo de relação: Descreve como a URI se relaciona com o recurso atual, representado pelo atributo rel;
  • Tipo: Descreve o tipo de conteúdo obtido ou do tipo de verbo que deve ser utilizado para acessar a URI. Representado pelo atributo type.

Desta forma, na prática, uma API REST que implemente HATEOAS retornar uma resposta como a abaixo:

{
    "cursos": [
        {
            "id": 1,
            "nome": "C# (C Sharp)",
            "links": [
                {
                    "type": "GET",
                    "rel": "self",
                    "uri": "api.treinaweb.com.br/cursos/1"
                },
                {
                    "type": "GET",
                    "rel": "curso_aulas",
                    "uri": "api.treinaweb.com.br/cursos/1/aulas"
                },
                {
                    "type": "PUT",
                    "rel": "curso_atualizacao",
                    "uri": "api.treinaweb.com.br/cursos/1"
                },
                {
                    "type": "DELETE",
                    "rel": "curso_exclusao",
                    "uri": "api.treinaweb.com.br/cursos/1"
                }
            ]
        },
        {
            "id": 2,
            "nome": "PHP",
            "links": [
                {
                    "type": "GET",
                    "rel": "self",
                    "uri": "api.treinaweb.com.br/cursos/2"
                },
                {
                    "type": "GET",
                    "rel": "curso_aulas",
                    "uri": "api.treinaweb.com.br/cursos/2/aulas"
                },
                {
                    "type": "PUT",
                    "rel": "curso_atualizacao",
                    "uri": "api.treinaweb.com.br/cursos/2"
                },
                {
                    "type": "DELETE",
                    "rel": "curso_exclusao",
                    "uri": "api.treinaweb.com.br/cursos/2"
                }
            ]
        },
        {
            "id": 3,
            "nome": "Java",
            "links": [
                {
                    "type": "GET",
                    "rel": "self",
                    "uri": "api.treinaweb.com.br/cursos/3"
                },
                {
                    "type": "GET",
                    "rel": "curso_aulas",
                    "uri": "api.treinaweb.com.br/cursos/3/aulas"
                },
                {
                    "type": "PUT",
                    "rel": "curso_atualizacao",
                    "uri": "api.treinaweb.com.br/cursos/3"
                },
                {
                    "type": "DELETE",
                    "rel": "curso_exclusao",
                    "uri": "api.treinaweb.com.br/cursos/3"
                }
            ]
        }
    ]
}

Vantagens do HATEOAS

Uma das principais vantagens do HATEOAS é permitir representar o estado de um recurso ou as limitações de um usuário sem a necessidade da implementação de outras lógicas no cliente.

Por exemplo, imagine uma API de um banco. Ao consultar o saldo do usuário, caso este tenha saldo em conta, pode ser retornado os seguintes dados:

{
    "conta": {
        "numero": 1234,
        "saldo": 5120.09,
        "links": 
        [
            {
                "type": "GET",
                "rel": "self",
                "uri": "api.banco.com.br/usuario/1/conta/1234"
            },
            {
                "type": "PUT",
                "rel": "conta_saque",
                "uri": "api.banco.com.br/usuario/1/conta/1234/saque"
            },
            {
                "type": "PUT",
                "rel": "conta_deposito",
                "uri": "api.banco.com.br/usuario/1/conta/1234/deposito"
            }
        ]
    }
}

Caso não tenha saldo, o resultado pode mudar para:

{
    "conta": {
        "numero": 1234,
        "saldo": -20.14,
        "links": 
        [
            {
                "type": "GET",
                "rel": "self",
                "uri": "api.banco.com.br/usuario/1/conta/1234"
            },
            {
                "type": "PUT",
                "rel": "conta_deposito",
                "uri": "api.banco.com.br/usuario/1/conta/1234/deposito"
            }
        ]
    }
}

Desta forma, o cliente não precisa se preocupar com o estado do usuário. Ele saberá que os ações disponíveis serão as retornadas no atributo links.

Outra vantagem é a prevenção do chamado “hardcoding”, quando informações são inseridas diretamente no código da aplicação. Em todos os nossos exemplos vimos que o cliente precisa conhecer apenas a URL base da API, pois esta já irá retornar as demais URLs disponíveis.

Por este comportamento que o HATEOAS geralmente é aplicado em aplicações de proposito mais geral, que são criadas para sobreviverem por um longo período. Onde alterações na API não irão impactar muito nos clientes.

C# (C Sharp) - APIs REST com ASP.NET Web API
Curso de C# (C Sharp) - APIs REST com ASP.NET Web API
CONHEÇA O CURSO

Conclusão

De acordo com o modelo de maturidade de Richardson, o HATEOAS é considerado o último nível de uma API RESTful. Desta forma, caso esteja procurando definir uma API que siga o padrão RESTful, o HATEOAS deve ser implementado nela.

Mas mesmo que não esteja seguindo o padrão RESTful a risca, é fato que o componente HATEOAS facilita e muito a manutenção de uma API e a sua integração com outras aplicações. Então, sempre que possível procure implementá-la nas APIs que desenvolver.

© 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