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

Chegamos à quarta e última parte da série de construção de uma API RESTful com Java + Spring Framework. Nesse artigo, continuaremos falando sobre performance, mas sobre alguns tópicos mais específicos às rotas da nossa API: paginação, ordenação, filtros de consultas e resposta parcial. Além disso, finalizaremos adicionando uma camada de segurança em nossa API.

Se você começou a leitura da série por esse artigo, sugiro voltar nos três primeiros, Parte 1, Parte 2 e Parte 3, respectivamente, para acompanhar a evolução do nossa aplicação e todos os assuntos abordados até então. Para os leitores que já acompanham a série, o código tudo que desenvolvemos nessa API, inclusive da Parte 4, pode ser encontrado nesse repositório no Github:

Filtros em consultas

Você não precisa trazer todos os dados em uma request.

Na primeira parte da série, uma das rotas que especificamos foi um GET ao endpoint /travels. Inicialmente, essa consulta retorna TODAS as viagens criadas na API desde o início da nossa etapa de estudo. TODAS! Repetindo: a consulta traz tudo que estiver na reta, sem filtro algum!

Mas, então, para que serve filtros em uma consulta? Filtros são parâmetros utilizados para criar consultas personalidas de acordo com a necessidade do seu produto para atender um cenário específico. Por exemplo, em cenários como o nosso, em que a API seria usada por uma agência de viagens online, em uma época de alta temporada, o número de viagens agendadas por usuários pode crescer substancialmente. Dessa forma, podemos criar filtros para manipular o que é retornado, principalmente, se a quantidade de dados é grande.

Dessa forma, para melhorar a performance do nosso findAll (que retorna o mundo em viagens hehe), adicionaremos na consulta parâmetros que representarão o startDate, mas com o conceito de ter uma data de início e uma data de fim. Em resumo: a consulta agora deve retornar todas as viagens que foram iniciadas em um período determinado pelo cliente.

Para refletir esse novo comportamento, também alteraremos o nome da consulta para /travels/findBetweenDates no TravelController. As variáveis que iremos acrescentar como @RequestParam para mapear o período serão: startDate e endDate, ambos do tipo LocalDate introduzido na Date Time API do Java 8.

Exemplo de consulta com filtros no TravelController

Adequaremos também as classes de serviço TravelServiceImpl e de persistência TravelRepository para usar o filtro de datas.

Exemplo de consulta customizada no TravelRepository
Exemplo de consulta customizada no TravelServiceImpl

Para ficar mais claro o contexto de aplicação, vamos imaginar que o cliente da nossa API gostaria de visualizar apenas as viagens efetuadas do mês de Agosto de 2020. Dessa forma, ao chamar a rota, precisaríamos adicionar uma data de início na consulta, que seria 2020–08–01; e, uma data de fim na consulta, que seria 2020–08–31 como demonstrado no exemplo abaixo:

Exemplo do funcionamento dos filtros de data

Com esse novo comportamento na rota de busca de todas as viagens na API, podemos trabalhar com dados mais enxutos. Entretanto, os filtros ainda não resolvem um problema crítico que podemos ter: um grande volume de viagens em um dia. É sobre a solução para esse problema que falaremos na próxima seção.

Paginação

Dividir os resultados para conquistar performance!

Imagine que nossa aplicação cresceu em número de usuários e obviamente no volume de viagens agendadas. Considerando o dia-a-dia de uma agência online real, esse número de viagens chega na cada dos bilhões!

Agora, vamos pensar no pior cenário que pode ocorrer com a nossa API: inúmeras viagens no banco de dados sendo agendadas em um curto espaço de tempo, por exemplo, em dias, horas, minutos ou segundos. Mesmo com a refatoração feita na rota GET /travels para ter o filtro por data, imagine a consulta retornando um conjunto de milhares, milhões ou bilhões de viagens: é impossível a API retornar um resultado em tempo hábil e sem sobrecarregá-la! E como poderíamos entregar os resultados rapidamente e sem sacrifícios na API? Paginando a resposta!

Paginação é uma estratégia para controlar a quantidade de informações que serão retornadas na resposta (pode ser um JSON, XML, plain/text, etc…). Basicamente é permitir que apenas um número padronizado de objetos seja demonstrado ao invés do conjunto inteiro dos dados.

Existem várias estratégias de paginação que podem ser utilizadas para esse propósito. Nós usaremos a mais simples, baseada em page e size. Como o próprio nome já diz, teremos que passar para a consulta o número da página a ser navegada e o seu respectivo tamanho (quantidade de registros). Os detalhes da implementação falarei nos próximos parágrafos.

A rota que iremos modificar será a findBetweenDates, que já ajustamos na seção anterior. Para adicionar o comportamento da paginação nessa rota, faremos duas modificações no método de mesmo nome na nossa classe TravelController:

  • Trocaremos o tipo do retorno do método: ao invés de retornarmos a interface List passaremos a retornar a interface Page como tipo do ResponseEntity. Por definição, uma página é uma sublista de uma lista de objetos. Com isso, nosso JSON de resposta passará a ser quebrado em páginas com número restrito de elementos.
  • Adicionaremos um Pageable como parâmetro: Pageable é uma interface abstrata para informações de paginação do Spring Core Data. Na annotation, podemos determinar as configurações default para a sua consulta como a página inicial (por default o Spring considera 0 como página inicial — page) e o tamanho do retorno da consulta (por default o Spring considera 10 como número de elementos retornados — size). Nossa implementação adotará como default o 1 como página inicial (page) e 10 para número de elementos retornados (size).
Exemplo da implementação da paginação no TravelController

Em seguida, criaremos uma classe de configuração, a PageableConfiguration, para forçar essa paginação sempre a começar do número 1.

Exemplo de configuração do comportamento do Pageable no SpringBoot

Além disso, ajustaremos a consulta de transações por período nas classes TravelServiceImpl e TravelRepository, para adicionar o atributo Pageable no corpo do método findBetweenDates.

Exemplo da implementação da paginação no TravelService
Exemplo da implementação da paginação no TravelRepository

Dessa forma, o resultado da consulta será similar ao demonstrado abaixo:

Exemplo do funcionamento da paginação na API

Ao final do JSON de resposta original da consulta, temos um objeto Pageable e outras informações sobre o retorno da consulta são adicionados ao response junto às informações do request feito. Um exemplo desse objeto pode ser visto abaixo:

"pageable": {
"offset": 0,
"pageSize": 5,
"pageNumber": 0,
"paged": true,
"unpaged": false
},
"last": false,
"totalPages": 2,
"totalElements": 7,
"number": 0,
"size": 5,
"numberOfElements": 5,
"first": true,
"empty": false

Agora que nossa consulta está paginada, outro questionamento muito comum surge em quem lida com a funcionadade: como é que faríamos para trazer os elementos em ordem diferente, por exemplo, na ordem inversa? A resposta para essa pergunta está na próxima seção.

Ordenação

Colocando ordem na casa!

Na seção anterior, vimos que com o Pageable e as configurações default conseguimos implementar o comportamento de paginação em nossa API. Agora, vamos imaginar que um cliente deseja demonstrar as transações em um relatório em tela, tal qual um extrato bancário.

Com a nossa versão atual, se criássemos um front-end em que o cliente possa interagir com páginas existentes por um componente semelhante ao abaixo, a nossa API entrega adequadamente o resultado esperado na integração.

Mas, e se o cliente quiser implementar um componente que traz os resultados da tela de forma ordenada com base em um ou mais campos? Por exemplo, permitir que se ordene as transações por id (do menor para o maior) ou data (da mais antiga para a mais recente)?

Da mesma forma como configuramos os parâmetros de page e size no Pageable, podemos adicionar também o sort. O sort indica qual dos campos da entidade vão ser utilizados como referência na ordenação. No nosso código de exemplo, nós usaremos o id como campo de referência na ordenação.

Exemplo da implementação da paginação e ordenação no TravelController

Por fim, complementaremos a classe PageableConfiguration com um método que configura o uso do atributo sort no Pageablede como parâmetro das rotas da API que implementam a funcionalidade.

Exemplo de configuração do comportamento do Pageable com ordenação no SpringBoot

Por default, a direção da ordenação é sempre ascendente (valor igual a ASC) e é passado logo depois do campo a ser ordenado, separado por vírgulas. Caso o usuário da API deseje o resultado em ordem decrescente, basta passar como parâmetro da String o valor DESC. Assim, o resultado da consulta seria similar ao demonstrado abaixo:

Exemplo do funcionamento da ordenação dos registros na API

Resposta Parcial

Nesse JSON só entra o que eu quero!

Nas duas últimas seções, vimos técnicas para melhorar a performance da API reduzindo o número de elementos carregados na resposta. E se fosse possível reduzir o conteúdo da resposta, ou seja, retornar no JSON apenas os campos necessários para um determinado contexto?

Por exemplo, um determinado cliente opta por exibir o relatório de extrato das transações de forma resumida, quando o usuário está vendo por um app mobile. Porém quando o usuário vê as informações via browser, exibe-se a versão completa dos dados. Outro cenário seria de customização de relatórios sobre as transações e suas estatísticas, apenas manipulando o JSON e sem criar novas estruturas.

Esse recurso em APIs é conhecido como Resposta Parcial (ou Partial Response). Conceitualmente, é a possibilidade de retornar diferentes campos de uma entidade de acordo com a necessidade, na resposta da requisição. Para habilitarmos essa funcionalidade no nosso projeto travels-java-api, usaremos a biblioteca Partialize que pode ser encontrada nesse repositório aqui.

Importaremos a biblioteca no pom.xml e, juntamente com a dependência, devemos adicionar o repositório do jitpack, pois é onde o código-fonte/jar da biblioteca está disponibilizada, de forma similar ao exemplo abaixo:

Exemplo parcial do pom.xml do projeto

O próximo passo será implementar um método genérico para tratar a parcialização da entidade Travel, bem como converters de String para tipos específicos do Java como LocalDateTime e BigDecimal.

Exemplo da implementação de um LocalDateTimeConverter com a biblioteca Partialize
Exemplo da implementação de um BigDecimalConverter com a biblioteca Partialize

Agora, iremos alterar a rota escolhida para ter esse comportamento na API: a findById da TravelController. Adicionaremos nela, o parâmetro fields, que representará os campos selecionados pelo cliente da API, que serão retornados no JSON de resposta.

A lógica da parcialização ficará na TravelServiceImpl, em que criamos o método que, de fato, fazemos o tratamento do JSON final com os campos que foram solicitados no parâmetro fields.

Exemplo do método no TravelServiceImpl que monta o JSON final

Dessa forma, por exemplo, se quisessemos que retornar somente os campos id, a data de início e o valor da viagem, o formato da requisição será o seguinte:

http://localhost:8080/api-travels/v1/travels/1?fields=id,orderNumber,amount

O JSON da resposta deverá ser similar ao exemplo abaixo:

Exemplo do funcionamento da resposta parcial na API

A grande vantagem do uso da resposta parcial é otimização dos recursos e melhoria de performance. Basicamente, passamos a evitar a transferência e armazenamento de campos desnecessários à aplicação na sua rede, além de poupar CPU e memória.

Outro ponto interessante é que, dessa forma, deixamos a responsabilidade do controle da granularidade da informação para os clientes da API e oferecemos à eles opções de customização (mesmo que controlada) nas rotas.

Adicionando camada de segurança com Spring Security

Para fechar com chave de ouro, precisamos cercar nossa API para as rotas principais só possam ser acessadas se o usuário tiver permissão. O Spring Framework tem um projeto que trata exatamente da camada de segurança de uma aplicação: Spring Security. E é ele que vamos utilizar e acrescentar as dependências no pom.xml, juntamente com JWT Token.

Adicionando dependências do Spring Security e JWT

Iremos acrescentar no modelo, duas entidades novas para que essa camada possa ser implementada: Usere Account. A entidade User representa o usuário e a entidade Account representa a conta do cliente no site da agência de viagens. Dessa forma, uma Travel deve estar relacionada à uma Account. Nossa arquitetura final será similar a abaixo:

Desenho final da API

Outra alteração que faremos é criar uma rota de autenticação /api-travels/v1/auth, que ficará na controller AuthenticationController. Com isso, sempre que o usuário for acessar as rotas da /travels e da /statistics, ele precisará passar um token de acesso para validar suas permissões.

Exemplo de controller de autenticação

Além da rota de autenticação, precisaremos criar um serviço que implementa o UserDetailsService, para nos apoiar na gestão (criação e verificação) dos tokens de acesso: JwtUserDetailsServiceImpl.

Exemplo do serviço que usa UserDetailsService do Spring Security

Toda a implementação dessa camada de serviços está no pacote security da API. Já os filtros JWT, que apoiam a implementação do Spring Security, estão no pacote filters. Por fim, precisamos criar uma classe de configuração para tratar quais rotas vão precisar de token para serem acessadas e quais não vão.

Exemplo de classe de configuração do Spring Security

O fluxo final da API ficará semelhante ao abaixo:

Fluxo final da API

Com esse artigo encerramos a série com dicas que são amplamente utilizadas no dia-a-dia que desenvolvedores lidam com APIs. Sobre os exemplos utilizados nesse artigo e nos anteriores, vocês podem encontrá-los no meu repositório travels-java-api no Github.

Cabe ressaltar que o objetivo do artigo é apresentar recursos técnicos fundamentais para sua API RESTful, porém, é crucial que você leitor complemente o seu conhecimento em outras fontes, por exemplo, com a documentação oficial do Spring.

E não menos importante: pratique! Use esse projeto de referência (entenda como funciona o recurso fork de projetos do Github) para seus estudos. Esse projeto está constantemente sendo refatorado a cada artigo e pode servir de base para você experimentar todo esse conteúdo apresentado (por exemplo, testar outros tipos de paginação no Spring Data JPA) .

Espero que tenham gostado da série. Abraços!

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

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