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.
Adequaremos também as classes de serviço TravelServiceImpl
e de persistência TravelRepository
para usar o filtro de datas.
TravelRepository
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:
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 interfacePage
como tipo doResponseEntity
. 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).
Em seguida, criaremos uma classe de configuração, a PageableConfiguration
, para forçar essa paginação sempre a começar do número 1.
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
.
Dessa forma, o resultado da consulta será similar ao demonstrado abaixo:
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.
Por fim, complementaremos a classe PageableConfiguration
com um método que configura o uso do atributo sort
no Pageable
de como parâmetro das rotas da API que implementam a funcionalidade.
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:
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:
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
.
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
.
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:
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.
Iremos acrescentar no modelo, duas entidades novas para que essa camada possa ser implementada: User
e 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:
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.
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
.
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.
O fluxo final da API ficará semelhante ao abaixo:
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!
Referências
- Spring Data Core — https://docs.spring.io/spring-data/rest/docs/2.3.x/reference/html/
- REST: Dealing with Pagination: https://www.mscharhag.com/api-design/rest-pagination
- Pageable — https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/domain/Pageable.html
- Baeldung — https://www.baeldung.com/
- Resposta Parcial — Dicas de desempenho — https://developers.google.com/tag-manager/api/v2/performance?hl=pt-br
- Spring Security — https://spring.io/projects/spring-security