Construindo uma API RESTful com Java e Spring Framework — Parte 2

Mariana Azevedo
13 min readApr 6, 2020

No primeiro artigo da série, o foco foi um tutorial para desenvolvedores que estão iniciando os estudos em Java ou Spring Framework e na construção de uma API RESTful. Nesse artigo, daremos continuidade à série comentando as otimizações que poderíamos fazer e boas práticas que podem ser utilizadas no projeto. Esse código refatorado está nesse repositório no Github:

Devemos usar o Lombok?

O Lombok (Projeto Lombok — Project Lombok) é uma biblioteca Java que se conecta automaticamente à sua IDE para auxiliar o desenvolvedor a escrever menos código. O framework possui annotations que geram esses métodos, por exemplo, @Getter (métodos getter), @Setter (métodos setter), @ToString (método toString), @AllArgsConstructor, @NoArgsConstructor e outros.

Tenho acompanhado recentemente em artigos e discussões no Github, de especialistas opiniões divididas sobre o uso do Lombok em projetos. Por exemplo, o projeto spring-cloud-azure da Microsoft, que é a integração do Spring Cloud para serviços da Azure, removeu o framework do projeto. De forma bem resumida, a decisão foi tomada como medida de redução de dependências externas (a discussão inteira pode ser lida no pull request da alteração).

Se por um lado há ganho grande de produtividade e legibilidade na sua API, por outro cria-se uma dependência entre o seu projeto e uma biblioteca de um terceiro, que em um contexto muito simples pode trazer, no futuro, problemas com manutenibilidade. Dessa forma, vale ponderar quando e como usar a biblioteca. Uma boa prática para usuários do Lombok é de anotar somente os atributos que você de fato vai acessar ou modificar ao invés da anotação no nível da classe (o @Data, por exemplo). Isso vale tanto para as annotations @Getter e @Setter.

Exemplo da classe Travel.java refatorada.

Em relação à outras annotations, o @ToString, que geralmente é utilizado nos logs, só manter nas classes em que é necessário a informação. Quanto ao @AllArgsConstructor, devemos utilizá-lo em cenários de classes Model/DTO com poucos atributos. Por organização, é preferível a criação de um Builder (ou o uso da annotation @Builder).

Implementando Spring Data (JPA)

Na versão inicial da API, fizemos uma implementação mais simples, que manipulava os dados apenas em memória, sem uma camada de persistência de dados. Iremos ajustar o projeto para que ele tenha uma camada de persistência e use o Spring Data JPA para isso.

O Spring Data JPA é um framework fornece suporte para criação de DAOs (ou repositórios) para quem usa o Java Persistence API (JPA). Na verdade, o Spring Data JPA faz parte do projeto Spring Data, que possui outros projetos da mesma natureza, mas para outros meios de persistência como o Spring Data MongoDB, Spring Data Redis, etc. O principal conceito do framework é o do Repository, que é a interface central na abstração de persistência do Spring Data.

O que vamos fazer no projeto é criar os pacotes da camada de dados, com os métodos básicos de CRUD, ou seja, criar, atualizar, buscar e deletar uma viagem ou uma estatística. Essas classes, que chamaremos conforme sugerido no Spring Bean Naming Conventions de TravelRepository e StatisticRepository estenderão JpaRepository.

Exemplo da classe TravelRepository.java
Exemplo da classe StatisticRepository.java

Para dar suporte arquitetural a camada de persistência, iremos remover o pacote factory criado para simular as manipulações dos objetos em memória e ajustaremos os Services para implementarem os comportamentos dos JpaRepository.

Exemplo da classe TravelService.java refatorada
Exemplo da classe TravelServiceImpl.java refatorada
Exemplo da classe StatisticService.java refatorada
Exemplo da classe StatisticServiceImpl.java refatorada

Dessa forma, a arquitetura da nossa API ficará conforme o desenho abaixo.

Parte da arquitetura da solução

No profile de desenvolvimento (e produção), usaremos como banco de dados o PostgreSQL (caso queiram usar OracleDB ou MySQL é só importar as dependências Maven no pom.xml). No profile de teste usaremos H2. Adicionaremos no pacote src/main/resources, o arquivo de propriedades application.properties, com as configurações de desenvolvimento e o arquivo application-test.properties para as configurações de teste.

Exemplo das configurações de banco de dados no profile de desenvolvimento
Exemplo das configurações de banco de dados no profile de teste

Após a implementação da camada de persistência, precisamos ajustar o pacote de testes src/test/java para validar as modificações. Para facilitar a manutenção de testes de cada um dos componentes (service, controller e repository), vamos remover a classe TravelsJavaApiUnitTests do projeto e recriar os testes seguindo a arquitetura proposta acima, utilizando o MockMvc. O resultado final pode ser visto aqui.

Versionamento de APIs (API versioning)

Gestão transparente das alterações da sua API

Depois que sua API já está em produção, a tarefa mais difícil é fazer a sua manutenção. Imagine que você utiliza na sua solução de software os endpoints de uma API externa e alguém autoriza uma atualização que simplesmente faz a sua aplicação deixar de funcionar. Desagradável, não?

Para facilitar a manutenibilidade da API, é uma boa prática fazer o versionamento dela. O versionamento é importante para caracterizar a sua evolução, mas também é útil para os clientes que integram sua API em soluções próprias conhecer o que estão consumindo e caso exista uma nova versão, se esta, quando disponibilizada, é compatível com sua solução.

Existem várias formas de versionar sua API. As mais comuns são:

  • Versionamento pelos Headers: inclusão de uma chave no cabeçalho da mensagem. Por exemplo, você criar um novo header na requisição para representar a versão (version ou api-version) ou acrescentar no content-type, no header Accept.

Exemplo de versionamento pelo content-type:

HTTP GET
https://travels-java-api.herokuapp.com/travels
Accept: application/vnd.travels.v1+json

Exemplo de versionamento customizado:

HTTP GET
https://travels-java-api.herokuapp.com/travels
version: 1
  • Versionamento no modelo path/URI: embutir como parte da URI a versão. Por exemplo, antes da rota adicionar o /v1.
https://travels-java-api.herokuapp.com/v1/travels
  • Versionamento por Query String: adicionar na URI a informação da versão como um parâmetro da requisição. Exemplo:
https://travels-java-api.herokuapp.com/travels?version=1.0

Na nossa implementação, vamos usar uma combinação das duas primeiras abordagens: vamos ter uma major version, representada pelo /v1 nas rotas da API e uma minor version, com uma customização nos headers da requisição para sempre enviar como parâmetro o travels-api-version: data da última alteração da v1 em produção. Dessa forma, as requests ficarão conforme o exemplo abaixo:

HTTP GET
https://travels-java-api.herokuapp.com/v1/travels
travels-api-version: 2020-04-05

Essa abordagem é similar a utilizada pela Stripe no versionamento de API deles e vem sendo bem recebida pelos especialistas.

Implementando Spring Boot HATEOAS

HATEOAS, acrônimo para Hypermedia As The Engine Of Application State (em tradução livre, Hipermídia como o Mecanismo de Estado da Aplicação) é uma restrição arquitetural que faz parte de aplicações REST. Uma API que utiliza o conceito, dispõe informações sobre seus recursos em endpoints no objeto de resposta, ou seja, a cada requisição feita na API, na resposta incluímos uma URL descritiva com o endereço de onde a informação está localizada. Com isso, conseguimos ter um mecanismo para guiar os clientes na aplicação quanto ao estado atual dos recursos e quanto as transições de estado possíveis no momento.

Cada objeto da lista de links possui dois atributos:

rel: de relacionamento. Podem incluir relacionamentos da classe do objeto com outras classes do sistema ou, caso não exista, o link se autorreferencia.
href: é uma URL completa que define um único recurso.

Por exemplo, no response abaixo, ao criar uma Travel, temos uma lista de links com apenas um objeto, cujos atributos são referências ao próprio objeto (pois a classe não possui relacionamento com outras classes do projeto) e o href é o link do recurso criado.

{
"data": {
"id": 1,
"orderNumber": "220788",
"amount": "30.06",
"startDate": "2020-04-01T09:59:51.312Z",
"type": "ONE-WAY",
"links": [
{
"rel": "self",
"href": "https://travels-java-api.herokuapp.com/v1/travels/1"
}
]
}
}

O Spring Framework chamado Spring Boot HATEOAS, para facilitar a criação dessas representações REST exemplificadas acima, que segue o princípio HATEOAS quando estamos trabalhando com APIs Java + Spring Boot. O principal problema que o framework tenta resolver é a criação e a montagem de representação do objeto de links sem criar classes para isso.

Iremos importar o Spring Boot HATEOAS no pom.xml projeto conforme o trecho abaixo:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>

Em seguida, iremos eliminar a classe Link.java do projeto original e transformar as classes de DTO em uma extensão de um Representation Model, para que ela tenha em seu comportamento uma lista de links não explícita.

Exemplo da classe TravelDTO
Exemplo da classe StatisticDTO

Também criaremos duas classes de apoio, Response e ResponseError para representar as informações da resposta da requisição e dos detalhes de erro (caso existam). As classes ficarão assim:

Exemplo da classe Response.java
Exemplo da classe ResponseError.java

Para que de fato os links apareçam no response, nos métodos de manipulação das requests nas controllers acrescentaremos os links, conforme o exemplo abaixo, no método de criação de uma transaction na API:

Exemplo da implementação do Spring Hateoas nas controllers

Afinal, o uso do HATEOAS para API RESTful é opcional? Essa é uma velha discussão entre os desenvolvedores. Segundo as documentações oficiais e o próprio criador do conceito REST, Roy Fielding, se não o implementamos, não podemos considerar uma API como RESTful. Mas para os pragmáticos, em um cenário em que a API possui um único cliente (que seria sua própria aplicação, nenhum outro consumidor externo) e também que a manutenção e o número de requisições são mais controlados, a utilização da restrição pode ser repensada.

Implementando Spring Exception Handling

Para tudo na vida, há uma exceção…

Em outro texto, já discutimos a importância da programação defensiva como boa prática de desenvolvimento de software. O que ainda não estava em pauta no texto é o tratamento de exceções dentro do Spring Framework.

Nas versões do Spring Framework anteriores à 3.2, existiam duas formas de lidar com exceções de forma sistemática: usando uma espécie de dispacher, o HandlerExceptionResolver, para tratar de forma global as exceções ou usando a anotação@ExceptionHandler em um método dentro de cada Controller com a lógica de mitigação de erros. Ambas opções não nos permitiam flexibilizar customizações adequadas às HTTP responses da API e às possíveis múltiplas exceções em um mesmo método.

A partir do Spring 3.2, temos duas opções de implementação que atendem muito bem as customizações de tratamento de exceção em nível global ou no escopo dos Controllers. Para a primeira opção, de tratamento de exceção global, foi criada a anotação @ControllerAdvice que é feita em nível de classe e permite criar métodos para tratar exceções específicas, anotadas com @ExceptionHandler. O @ControllerAdvice funciona como um interceptador de exceções geradas por métodos anotados com @RequestMapping. Abaixo, podemos ver o exemplo de como esse conceito foi adotado na API:

Exemplo da classe TravelsJavaAPIExceptionHandler.java

Temos várias vantagens em usar essa abordagem:

  • Consolidação múltiplos @ExceptionHandler, antes dispersos, em um único componente global de tratamento de erros;
  • Controle total do código de status da resposta da requisição;
  • Mapeamento de várias exceções em um mesmo método, para serem tratadas em conjunto;
  • Possibilidade de utilizar o ResponseEntitynos métodos para tratar corretamente o body da resposta da requisição.

A segunda opção é utilizar o ResponseStatusException, que é uma classe base para exceções usadas para aplicar um código de status a uma resposta HTTP. Como é uma RuntimeException, não precisa ser explicitamente adicionada a uma assinatura de método. Basta cercar o código do método com um try-catch e lançá-la como exceção.

Exemplo de uma controller utilizando ResponseStatusException

Quais são as vantagens dessa abordagem? De forma mais sucinta, redução do acoplamento (tratamento de exceção não depende de um ExceptionHandler) e a possibilidade de tratar um mesmo tipo de exceção com HttpResponse diferentes, pois a definição dos status e da mensagem de erro fica sob responsabilidade do próprio método na controller.

Por convenção, é sempre bom lidar com um tipo específico de exceção. Um trade-off importante do uso do ResponseStatusException é a duplicação de código nas controllers. Em um contexto de múltiplos cenários de exceção específicos talvez se justifique o uso dessa abordagem. Em um contexto em que as exceções são genéricas e globais (ocorrem em todos os serviços da API), o ControllerAdvice é uma forma mais prática de obter maior controle do mecanismo de tratamento de erros na aplicação. Justamente por esse motivo, utilizaremos a segunda opção.

Adequando o projeto aos conceitos de Clean architecture

Clean architecture para todos nós!

A arquitetura das aplicações Spring é outro assunto de discussão entre especialistas em clean architecture. Em seu livro Clean Architecture, Uncle Bob (Robert C. Martin) levanta quatro abordagens mais comuns ao se organizar arquiteturalmente um projeto:

  • Package by Layer: talvez a abordagem mais simples, em que definimos as camadas na forma tradicionalmente horizontal, onde separamos nosso código com base no que ele faz em uma perspectiva técnica. Por exemplo, camada de interface (webcontrollers e views no Spring MVC), camada lógica de negócio (service models, domains e services no Spring MVC) e camada de persistência (data repository).
  • Package by Feature: é baseada em recursos relacionados ao conceito de domínio (design orientada a domínio). Geralmente, nas implementações que adotam esse padrão, todos os tipos de um determinado domínio são colocados em um único pacote Java, nomeado para refletir o conceito que está sendo agrupado. Usando nossa API como exemplo, seria como separássemos em dois domínios: transactions e statistics, com todos os recursos (controllers, services, repository) de cada um agrupados nesse contexto.
  • Ports and adapters: abordagem que sugere que o negócio/domínio seja independente e separado dos detalhes técnicos da implementação, como banco de dados. Cria o conceito de composição por “dentro” (domínio) e por “fora” (infraestrutura). Dessa forma, a camada de configuração de conexão com o banco ficaria em um pacote, todo o contexto de negócio em outra (services, repository e model) e a camada web em outra.
  • Package by Component: é uma abordagem híbrida, com o objetivo de agrupar todas as responsabilidades relacionadas a um componente de em um único pacote Java. Trata-se de ter uma visão centrada em serviços, que também vemos nas arquiteturas de microsserviços. Dessa forma, um componente seria “um agrupamento de funcionalidades relacionadas por trás de uma interface limpa”, ou seja, uma composição das interfaces de service + repository e suas implementações. Assim como o padrão Ports and adapters, essa abordagem mantém a interface do usuário separada desses componentes com a integração backend.

Há muitos prós e os contras de cada abordagem, mas focaremos aqui apenas nas implicações para a nossa API. Usar somente padrão Package by Layer força toda a estrutura de métodos e variáveis serem públicas, o que não é ideal, pois criamos alto acoplamento. A abordagem Package by Feature nos ajudaria com um mecanismo simples de organização, mas muito poderoso, a dissociar as estruturas da aplicação nos domínios (entre Model, Service, ServiceImpl e Repository) tornando acoplamento mais baixo.

Dessa forma, vamos adotar um híbrido das duas primeiras abordagens, com uma ideia próxima a proposta do Package by Component. Cada uma das camadas são um componente separado por features. Assim protegemos nossos pacotes, fazendo com que nada mais na base de código e nas camadas, fora dos pacotes, podem acessar informações relacionadas à travel ou à statistic, ao menos que eles passem por seus respectivos controladores. Na figura abaixo, temos um desenho da lógica desse modelo híbrido:

Desenho da segunda camada arquitetural do projeto

Nesse modelo híbrido, por exemplo, para a camada de service, teremos os pacotes io.github.mariazevedo88.service.travel para a funcionalidade Travel e io.github.mariazevedo88.service.statistic para a funcionalidade Statistic. As outras camadas seguem a mesma lógica.

Cabe ressaltar que, embora exista desenvolvedores que apoiam o Package by Feature no contexto de microsserviços, as duas últimas abordagens (a última em especial) são mais recomendadas para esse cenário.

Documentação automatizada

Como habilitar Swagger na minha API Spring Boot

Uma das habilidades mais importantes que um bom profissional de software deve ter é da escrita de documentações de alta qualidade. Para muitos, essa é uma tarefa difícil e traumática. Por exemplo, se uma API não for bem documentada, provavelmente, seus usuários encontrarão dificuldades para entender o seu funcionamento. Isso certamente influenciará na utilização dos serviços oferecidos em sua API.

Nos últimos anos, foram criados mecanismos para facilitar a criação de documentações: a documentação automatizada e interativa de recursos, da definição das rotas e parâmetros. Das ferramentas mais conhecidas, o Swagger se destaca, pois implementa a especificação da OpenAPI.

A premissa do framework é que o desenvolvedor, por meio de annotations nas Controllers, pode fazer o mapeamento das rotas da API e as expor publicamente (ou limitando por profile) um endpoint específico em uma interface bem intuitiva e interativa.

Para habilitar o Swagger na nossa API, precisamos criar uma classe chamada SwaggerConfiguration, como no exemplo abaixo:

Dessa forma, ao executar a aplicação no profile dev, o Swagger passa a disponibilizar uma interface para consulta e manipulação da API. Podemos acessar a nossa pelo link: http://localhost:8080/swagger-ui/index.html.

Exemplo da API demonstrada na Swagger UI

Em seguida, mapearemos todos os serviços da API com a annotation @ApiOperation. Nessa annotation, adicionaremos uma descrição intuitiva do que a rota faz, como no exemplo da rota de criação de viagens.

O resultado dessa atualização pode ser visto ao clicar nos detalhes das controllers na UI do Swagger.

Rotas com a descrição da sua funcionalidade

Ao clicar nos detalhes da rota em específico, você verá que é possível interagir com a API. Por exemplo, criar, atualizar, buscar, deletar recursos pelo Swagger.

Exemplo da criação de um recurso pelo Swagger

A documentação automatizada, além de ajudar na gestão do conhecimento da API para o seu time e clientes, pode servir também como facilitador na criação de ambientes sandbox. Basta configurar um profile específico para esse fim na aplicação e estruturar um ambiente para a execução.

Com os tópicos discutidos nesse artigo, deixamos nossa API mais robusta. Caso queiram analisar o código final e também sugerir melhorias, é só dar um fork no projeto travels-java-api no Github, codar ou adicionar sugestões via issues. Ainda temos em mãos outros recursos para fazer nossa API ainda mais madura, mas isso fica para outro(s) artigo(s).

Espero que tenham gostado do post. Abraços!

Referências

  1. Spring Data JPA: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/
  2. Versioning a REST API: https://www.baeldung.com/rest-versioning
  3. O versionamento de APIs Web: https://www.infoq.com/br/news/2016/07/web-api-versioning/
  4. Melhores práticas para uma API RESTful pragmática (parte 1): https://desenvolvimentoparaweb.com/miscelanea/api-restful-melhores-praticas-parte-1/
  5. Spring HATEOAS — Reference Documentation: https://docs.spring.io/spring-hateoas/docs/current/reference/html/
  6. Nivelando sua REST API: https://www.infoq.com/br/articles/nivelando-sua-rest-api/
  7. Spring Response Status Exception: https://www.baeldung.com/spring-response-status-exception
  8. Exception Handling for REST with Spring: https://www.baeldung.com/exception-handling-for-rest-with-spring
  9. Package by Layer for Spring Projects is Obsolete: https://dzone.com/articles/package-by-layer-for-spring-projects-is-obsolete
  10. Clean Architecture: A Craftsman’s Guide to Software Structure and Design — Robert Martin Series (2017)

--

--

Mariana Azevedo

Senior Software Developer/Tech Lead, master in Computer Science/Software Engineering, Java, open source, and software quality enthusiast.