Desenvolvimento Back-End
com Java e Spring Framework
Criação de
API RESTful
Para quem é este material?
Este material foi escrito para todos que desejam iniciar sua jornada no
desenvolvimento back-end utilizando o framework Spring com a linguagem Java. Ele
também serve como uma excelente fonte de aprendizado para desenvolvedores web
que estão começando a trabalhar com APIs RESTful e arquitetura de microsserviços,
oferecendo uma base sólida para avançar nessa área. Embora seja recomendado ter
uma boa base na linguagem Java, não se preocupe se o seu conhecimento for básico
— afinal, todos começam de algum lugar. Buscarei apresentar os conteúdos de forma
simples e didática, mas com a profundidade técnica necessária para aproximar você
das práticas do mercado de trabalho. Este é um convite para explorar, aprender e
crescer juntos. Seja muito bem-vindoao Desenvolvimento Back-End com Java!
Sumário
1. Como chegamos até aqui ..........................................................................
2. Aplicação Back-End ....................................................................................
3. Conhecendo a plataforma do Spring Framework ...................................
2.1 Por que usar o Spring Framework ..................................................................
2.2 Inversão de Controle e Injeção de Dependência ............................................
2.3 Como a Inversão de Controle Funciona no Spring? ........................................
2.4 Core Container ............................................................................................
2.5 Ciclo de Vida de um Bean no Spring ..............................................................
2.6 Injeção por Construtor .................................................................................
2.7 Integração e escalabilidade ..........................................................................
2.8 Injeção por Construtor .................................................................................
2.9 Injeção por Setter .........................................................................................
4. Spring Boot ...................................................................................................
3.1 Vantagens do Spring Boot ...........................................................................
3.2 Pontos positivos do Spring Boot ..................................................................
3.3 Estrutura de um projeto Spring Boot ............................................................
3.4 Segurança na aplicação .............................................................................
3.5 Desenvolvimento de APIs RESTful ...............................................................
3.6 Suporte a Microsserviços ............................................................................
3.7 Integração e Escalabilidade ........................................................................
5. O que é JPA (Java Persistence API)? .........................................................
6. O que é o Spring Data JPA? ........................................................................
7. O que é o Hibernate ? .................................................................................
8. Iniciando primeiro projeto com Spring Boot ...........................................
9. Arquitetura REST e APIs RESTful ...............................................................
8.1 REST e RESTful ...........................................................................................
8.2 Tipos de Representações em APIs RESTful ..................................................
10. Modelo de Maturidade de Richardson .....................................................
10.1 Os 4 níveis do modelo de maturidade de Leonardo Richardson ....................
11. Criação de API RESTful de usuários ....................................................................
11.1 Funcionalidades da API RESTful de usuários ................................................
11.2 Descrição do Projeto ...................................................................................
11.3 Dependências principais .............................................................................
12. Criação do projeto e configurações iniciais ........................................................
12.1 Dependências que usaremos ....................................................................
12.2 Configuração da organização do projeto em módulos ................................
12.3 Criação da base de dados no PostgreSQL ..................................................
12.4 Configuração de conexão com o SGB em arquivo .ymal ..............................
12.5. Outras configurações importantes no arquivo .ymal ..................................
12.6 Mapeamento da entidade usuário .............................................................
12.7 Criação dos ENUMS ..................................................................................
13. Realizando CRUD .......................................................................................
13.1 Verbos HTTP ..............................................................................................
13.2 Criação do Repository ................................................................................
13.3 Controllers da aplicação ...........................................................................
14. Criando UsuarioController ........................................................................
14.1 Injeção de Dependência via Construtor ......................................................
14.2 Endpoint getAllUsuario ..............................................................................
14.3 Melhorando o método getAllUsuario ...........................................................
14.1 Criando a Interface do Serviço: findAll ........................................................
14.2 Implementando a interface do service ........................................................
14. 3 Endpoint para o método getOnUsuario ......................................................
14.4 Interface do service: findById .....................................................................
14.5 Implementação da interface: findById ........................................................
14.6 Endpoint para o método deleteUsuario ......................................................
14.7 Interface do service: deleteUsuarioId .........................................................
14.8 Implementação da interface: deleteUsuarioId ............................................
14.9 Endpoint para o método updateUsuario .....................................................
14.10 Interface do service: updateUsuario .........................................................
14.11 Implementaçã da interface: updateUsuario ..............................................
14.12 Endpoint para o método upadateImagem .................................................
14.13 Interface do service: updateImagem .........................................................
14.14 Implementaçã da interface: updateUsuario ..............................................
15. Criação AutenticacaoUsuarioController ................................................
15.1 Interface do service: saveUsuario ............................................................
15.2 Implementação do service: saveUsuario .................................................
15.3 Uso de DTOs na aplicação ......................................................................
15.4 Usando Records .....................................................................................
15.5 Criação do DTO com anotação @JsonView ..............................................
16. Implementação de visualização por campo .........................................
16.1 Vantagens da Visualização por Campo ....................................................
16.2 Implementação do método getAllUsuario ................................................
16.3 Implementação do método updateUsuario ..............................................
16.4 Implementação do método updateSenha ................................................
16.5 Implementação do método updateImagem .............................................
16.6 Implementação do método registroUsuario .............................................
17. Tratamento de exceções .........................................................................
17.1 ErroRecordResponse ..............................................................................
17.2 NotFoundException ................................................................................
17. HandleNotFoundException .......................................................................
18. Validações customizadas para uso de senhas ....................................
18.1 Anotação SenhaConstraint .....................................................................
18.2 SenhaConstraintImpl .............................................................................
19. Paginação ................................................................................................
19. Formas de implementar paginação ..........................................................
20. CORS na aplicação .................................................................................
20.1 Implementação a nivel de classe ...........................................................
20.2 Implementação a nivel global ................................................................
21. Hipermídias com Spring Hateoas .........................................................
21.1 O que é HATEOAS? ...............................................................................
21.2 Implementar Heteoas no controller .......................................................
22. Configuração de serialização de paginação a nivel global ...............
23. Padrão de data global com ISO 8601 UTC .........................................
24. Filtros avançados e dinâmicos ............................................................
24.1 Mapeamento dos filtros dinâmicos .......................................................
24.2 O que é o JpaSpecificationExecutor? .....................................................
24.3 Adicionar configuração para filtros específicos ......................................
24.4 Interface do service ..............................................................................
24.5 Implementação do service ....................................................................
24.6 Implementando o Specification no controller .........................................
24.7 Como testar? .......................................................................................
25. Documentando a API .............................................................................
25.1 Vantagens do SpringDoc .......................................................................
25.2 Configuração do arquivo .yaml ..............................................................
25.3 Configuração do SpringDoc ..................................................................
26. Logs na aplicação com log4J2 ..............................................................
26.1 Níveis de logs .......................................................................................
26.2 Implementando Logs ............................................................................
26.3 Configuração dos logs no arquivo .ymal .................................................
26.4 Aplicando logs no controller ..................................................................
26.5 Configuração de Logs a Nível Global ......................................................
27. Considerações finais ..............................................................................
Como chegamos até aqui?
No início da era da computação, o desenvolvimento de software era simples e linear.
As aplicações eram construídas como sistemas monolíticos, onde todas as
funcionalidades estavam agrupadas em uma única base de código. Esses sistemas
eram projetados para rodar em hardware específico e, geralmente, eram
desenvolvidos para atender apenas uma única organização ou objetivo.
Com o advento da internet e o aumento do número de usuários e demandas, os
sistemas monolíticos começaram a mostrar suas limitações. O crescimento
exponencial da web exigiu soluções que permitissem escalabilidade, flexibilidade e
ciclos de desenvolvimento mais rápidos.
Empresas de grande porte, como Amazon, Netflix e Google, enfrentaram desafios
significativos com sistemas monolíticos devido à sua arquitetura centralizada e
inflexível. Essas organizações lideraram a busca por uma nova abordagem. Este
material não tem como objetivo principal explorar a arquitetura de microsserviços em
profundidade. Contudo, ele foi desenvolvido para guiá-lo na criação de APIs RESTful
bem estruturadas, que podem ser escaladas e evoluídas para uma arquitetura
baseada em microsserviços no futuro, caso necessário. É importante destacar que,
para criar microsserviços robustos e escaláveis, é essencial ter uma base sólida na
criação de APIs RESTful, pois elas são o alicerce dessa abordagem.
Antes de iniciarmos o conteúdo propriamente dito, é importante entender como
chegamos até aqui. Se vamos desenvolver uma API RESTful para um serviço que
funcionará em um servidor conectado à grande rede mundial de computadores, é
essencial compreendermos como isso tudo funciona, afinal não somos apenas
usuários comuns, mas desenvolvedores, criativos e engenhosos.
A aplicação back-end que desenvolveremos funcionará na internet, conhecida como a
"grande rede mundial de computadores". Segundo Andrew S. Tanenbaum, uma rede de
computadores é definida como um conjunto de sistemas interconectados que podem
compartilhar recursos e se comunicar entre si.
A origem da internet remonta a 1969, em meio a projetos militares conduzidos pela
ARPA (Advanced Research Projects Agency) nos Estados Unidos. Esse período foi
marcado pela Guerra Fria, um conflito entre os Estados Unidos e a União Soviética.
Após o lançamento do primeiro satélite artificial, o Sputnik, pela União Soviética, os
Estados Unidos intensificaram seus esforços tecnológicos, criando a DARPA (Defense
Advanced Research Projects Agency), com o objetivo de desenvolver tecnologias
estratégicas para a defesa e guerra.
Nesse contexto, foi desenvolvida uma rede de comunicação resiliente, capaz de operar
mesmo em situações adversas, como cenários de guerra, e em 1969, surgiu a
ARPANET, considerada o precursor da internet moderna. Essa rede foi criada pela
DARPA com o propósito de conectar sistemas de maneira segura e eficiente,
estabelecendo as bases para o que viria a ser a grande revolução da comunicação
global.
Na década de 1970, Vinton Cerf e Robert Kahn criaram o protocolo TCP/IP, que se
tornou a base para a comunicação entre redes. Já nos anos 1980, a internet deixou de
ser apenas uma rede militar e se transformou em uma ferramenta global. com o
crescimento da rede em 1983, o protocolo TCP/IP foi adotado oficialmente, marcando
um ponto crucial na padronização da comunicação em rede. Com a popularização da
internet nos anos 1990, Tim Berners-Lee, cientista do CERN, desenvolveu a World Wide
Web, conhecida popular mente como WWW. Dois anos depois, em 1993, foi lançado o
primeiro navegador de internet, o Mosaic, que facilitou o acesso à web e contribuiu
para sua disseminação global.
Em 2010, surgiram tecnologias revolucionárias, como a computação em nuvem (cloud
computing), a Internet das Coisas (IoT) e os avanços significativos na inteligência
artificial (IA). Essas inovações ampliaram ainda mais o potencial da internet, que
passou a desempenhar um papel essencial em praticamente todos os setores.
Hoje, as aplicações para o uso da internet são praticamente ilimitadas. Seja no
comércio, na indústria, na educação, na saúde ou nas interações sociais, a internet
continua sendo uma ferramenta indispensável, transformando profundamente a forma
como nos comunicamos, trabalhamos e vivemos. Ela conecta pessoas, empresas e
dispositivos em uma escala global, desempenhando um papel fundamental nas
relações comerciais e sociais.
Nos anos 2010, a arquitetura de microserviços começou a ganhar força como uma
solução para os problemas apresentados pelos sistemas monolíticos. Essa nova
abordagem propunha dividir a aplicação em pequenos serviços independentes, cada
um responsável por uma funcionalidade específica. Esses serviços poderiam ser
desenvolvidos, testados, implantados e escalados de forma independente.
Aplicação Back-End
Antes de iniciarmos os estudos do desenvolvimento back-end, vamos entender o que é
uma aplicação Back-End, de forma sucinta uma aplicação back-end é a parte
"invisível" de um sistema ou aplicação que gerencia toda a lógica de negócios,
manipula dados e se comunica com bancos de dados, APIs externas e outras
integrações. É o responsável por processar informações e garantir que o front-end (a
interface visível para o usuário) funcione corretamente.
Com base nesse contexto histórico e tecnológico, desenvolveremos uma aplicação
back-end para gerenciamento de usuários utilizando Java e o poderoso framework
Spring. Essa escolha combina a robustez e escalabilidade do Java com a eficiência e a
flexibilidade do Spring, permitindo a criação de soluções modernas, seguras e de alto
desempenho.
Nossa aplicação será projetada para atender às demandas atuais, aproveitando as
melhores práticas de desenvolvimento back-end, incluindo APIs RESTful, integração
com banco de dados, e uma arquitetura bem estruturada para facilitar a manutenção e
a escalabilidade do sistema.
Antes de colocarmos a mão na massa, exploraremos os principais conceitos
fundamentais, sem negligenciar a teoria. Essa base teórica será essencial para o
desenvolvimento do projeto nas etapas posteriores, garantindo uma compreensão
sólida e estruturada do que será implementado, iremos começar entendendo o que é o
Spring framework e como ele funciona.
Conhecendo a plataforma do Spring Framework
O Spring Framework é uma plataforma completa e abrangente para o
desenvolvimento de aplicativos em Java. Ele foi criado com o objetivo de simplificar o
processo de desenvolvimento em Java EE, oferecendo uma abordagem mais leve,
modular e eficiente. Com uma ampla variedade de módulos, o Spring auxilia na
construção de sistemas de forma integrada, reduzindo significativamente o tempo e a
complexidade do desenvolvimento.
Sua flexibilidade e capacidade de adaptação fazem dele uma escolha ideal para
projetos de diferentes escalas, desde pequenas aplicações até sistemas corporativos
e arquiteturas baseadas em microsserviços. Para conhecer todos os projetos desse
ecossistema, visite o site https://spring.io/projects.
É importante diferenciar o Spring Framework do Spring Boot , pois, embora
relacionado, eles têm funções diferentes no desenvolvimento de aplicações.
O Spring Framework é um ecossistema completo para a criação de aplicações
modernas, incluindo microsserviços, APIs RESTful e muito mais. Ele oferece um
conjunto abrangente de funcionalidades que atendem praticamente todas as
necessidades de desenvolvimento back-end, como injeção de dependências,
gerenciamento de transações, segurança e integração com bancos de dados.
Por outro lado, o Spring Boot é um módulo dentro deste ecossistema. Ele foi projetado
para simplificar e acelerar o desenvolvimento, diminuindo a quantidade de
configuração manual necessária. O Spring Boot automatiza boa parte das
configurações do Spring Framework, proporcionando um ambiente pronto para rodar
aplicações com o mínimo de esforço inicial.
Por que usar o Spring Framework?
Usar o Spring Framework oferece uma série de vantagens que o tornam uma das
escolhas mais populares para o desenvolvimento de aplicações Java, especialmente
no contexto de sistemas empresariais e APIs web. Spring Boot permite o
desenvolvimento rápido de aplicações Java, oferecendo um modelo de configuração
mínima. Ele simplifica a configuração e a inicialização do projeto, reduzindo a
quantidade de código repetitivo e a complexidade de configuração, através do uso de
"conventions over configuration" (convenções sobre configurações).
O Spring Framework oferece uma enorme quantidade de funcionalidades, mas permite
que você utilize apenas o que for necessário para o seu projeto. Além disso, você pode
estender o Spring com diversas bibliotecas de terceiros para lidar com aspectos como
persistência (Spring Data JPA), segurança (Spring Security), testes (Spring Test), etc.
A modularidade do Spring possibilita adicionar apenas os módulos que você precisa,
evitando sobrecarga de funcionalidades.
O Spring é altamente escalável e pode ser usado para desenvolver desde pequenas
aplicações até sistemas corporativos de grande porte. A arquitetura do Spring,
especialmente com a injeção de dependências, permite que você crie aplicativos bem
estruturados e de fácil manutenção.
O Spring Framework oferece uma plataforma robusta, flexível e escalável para o
desenvolvimento de aplicações Java. Suas características como injeção de
dependência, simplicidade no gerenciamento de configuração, suporte a segurança e
integração com bancos de dados fazem dele uma escolha sólida para uma grande
variedade de cenários de desenvolvimento.
Se você está buscando eficiência, flexibilidade e uma grande comunidade de suporte,
o Spring é uma excelente opção para o desenvolvimento de aplicações Java modernas.
Agora, vamos explorar os principais conceitos que fazem o Spring Framework ser tão
relevante e amplamente utilizado no mercado.
Inversão de Controle e Injeção de Dependência
No Spring Framework, a Inversão de Controle (IoC) é um dos princípios
fundamentais que permite a criação de aplicações mais desacopladas e modulares.
No Spring, a IoC é implementada principalmente através do conceito de Injeção de
Dependência (DI), o qual permite que as dependências de uma classe sejam
fornecidas de fora, em vez de a classe ser responsável por criar suas próprias
instâncias.
O Spring utiliza um contêiner de IoC que gerencia a criação e o ciclo de vida dos
objetos (beans). Isso facilita o desacoplamento de componentes, permitindo uma
arquitetura mais flexível e fácil de testar. Com o DI, o Spring injeta automaticamente
dependências em classes, eliminando a necessidade de instanciar objetos
manualmente. Isso reduz o código padrão e promove um design voltado para
interfaces.
Como a Inversão de Controle Funciona no Spring?
No Spring, o contêiner de IoC é responsável por gerenciar a criação e o ciclo de vida
dos objetos (também conhecidos como beans) dentro da aplicação. Quando uma
classe precisa de uma dependência, em vez de instanciá-la diretamente, o Spring
injetará a dependência de forma automática. Isso é feito com base nas configurações
do contêiner de IoC, que pode ser configurado por XML, anotações ou Java Config
Core Container
O Spring Framework utiliza a Injeção de Dependência (DI) para aplicar o princípio de
Inversão de Controle (IoC), centralizando toda a implementação no Core Container.
Esse container é a base de configuração do Spring e é responsável por gerenciar o ciclo
de vida e as dependências dos objetos dentro da aplicação. Quando a aplicação é
iniciada, o Core Container é ativado, carrega as configurações pré-definidas (seja por
meio de classes Java ou arquivos XML) e define as dependências necessárias, criando-
as de forma automática através da IoC. Essas dependências são chamadas de beans
dentro do contexto do Spring.
Os beans são objetos cujo ciclo de vida é gerenciado pelo container de IoC do Spring,
desde a criação até a destruição, quando não são mais necessários. Isso permite que
o Spring se ocupe da instância e do gerenciamento das dependências, facilitando a
modularidade e o desacoplamento entre os componentes da aplicação. Este processo
define o ciclo de vida do Container, que abrange a criação, configuração e destruição
dos beans, garantindo que a aplicação seja eficiente e escalável. O contêiner gerencia
a injeção das dependências conforme o design da aplicação, proporcionando
flexibilidade para que a aplicação evolua sem acoplamento rígido entre os seus
componentes.
Esse gerenciamento de ciclo de vida e a injeção automática de dependências são o
que tornam o Spring uma ferramenta poderosa para o desenvolvimento de aplicativos
modulares e de fácil manutenção.
Ciclo de Vida de um Bean no Spring
Criação do Bean: O contêiner do Spring é responsável pela criação do bean, que pode
ocorrer de várias formas, dependendo da configuração. O contêiner usa as definições
de configuração (seja por anotações, XML ou Java Config) para determinar como e
quando instanciar o bean.
Injeção de Dependências: Após a criação, o Spring injeta as dependências
necessárias no bean. Isso pode incluir outros beans ou recursos que o bean precisa
para operar corretamente. A injeção pode ocorrer por meio de construtores, setters ou
diretamente nos campos, conforme a configuração do contêiner.
Inicialização: O método de inicialização do bean é chamado, se houver algum
configurado. Esse método pode ser definido por meio de anotações como
@PostConstruct, ou no próprio XML de configuração. Aqui, é possível realizar
configurações adicionais ou lógica que precisa ser executada após a injeção das
dependências.
Uso do Bean: Após a injeção das dependências e a inicialização, o bean é colocado à
disposição para uso. A classe cliente, ou qualquer outro componente da aplicação que
precise da instância do bean, pode acessar e interagir com ele durante a execução da
aplicação.
Destruição: Quando o bean não for mais necessário, o contêiner do Spring gerencia
sua destruição. Isso ocorre automaticamente no caso de beans de escopo singleton
(quando a aplicação é encerrada) ou de escopo prototype (quando o bean deixa de ser
utilizado). O método de destruição pode ser configurado com a anotação
@PreDestroy ou em arquivos de configuração XML, permitindo liberar recursos ou
realizar tarefas de limpeza antes que o bean seja descartado.
O ciclo de vida de um bean no Spring é gerido inteiramente pelo contêiner, desde sua
criação, passando pela injeção de dependências, até sua inicialização, uso e
destruição. Esse processo de gerenciamento proporciona ao desenvolvedor mais
flexibilidade e modularidade, já que as dependências são tratadas automaticamente
pelo contêiner, permitindo que a aplicação seja construída de maneira mais eficiente e
desacoplada. Na configuração por anotações, é possível utilizar os estereótipos do
Spring para determinar de forma mais objetiva e específica qual o tipo de bean será
cada classe. Há quatro principais tipos: • @Component; • @Service; • @Controller; •
@Repository.
Exemplo 1: Bean do tipo Component
@Component
public class Produto {
private String nome;
private BigDecimal value;
//... getters e setters
}
Exemplo 2: Bean do tipo Service
@Service
public class ProdutoService {
// Implementação
}
Exemplo 3: Bean do tipo Controller
@Controller
public class ProductController {
// ... GET, POST, DELETE, UPDATE methods
}
Exemplo 4: Bean do tipo Repository
@Repository
public class ProductRepository {
// database transaction methods
}
Exemplo 5: Bean do tipo Component e injeção de dependência
@Component
public class UserService {
private final UserRepository userRepository;
@Autowired // A dependência é injetada pelo construtor
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void addUser(User user) {
userRepository.save(user);
}
}
No Spring, existem dois principais tipos de pontos de injeção de dependência:
injeção por construtor e injeção por setter. Ambos permitem que o contêiner do
Spring forneça as dependências necessárias a um bean, mas de formas diferentes.
Que será mostrado mais à frente.
Injeção por Construtor
Na injeção por construtor, as dependências são fornecidas ao bean por meio de seu
construtor. Essa abordagem é considerada a mais segura e recomendada, pois
garante que o bean esteja completamente configurado no momento da criação,
tornando-o imutável. Além disso, as dependências obrigatórias são garantidas, já que
o construtor precisa ser chamado com todos os parâmetros necessários.
A injeção por construtor é o método preferido para injeção de dependências no
Spring, pois garante que todas as dependências necessárias para a criação de um
objeto sejam fornecidas no momento da sua construção. Esse tipo de injeção promove
um design mais robusto e seguro, já que o objeto é sempre instanciado com todas as
suas dependências satisfeitas, evitando problemas de inicialização ou estado
inconsistente.
Como funciona:
Na injeção por construtor, o Spring resolve as dependências ao passar os objetos
necessários diretamente no construtor da classe. Isso torna a dependência
obrigatória, ou seja, a classe não pode ser criada sem as dependências necessárias.
@Component
public class ExampleService {
private final SomeDependency dependency;
// O Spring vai injetar a dependência automaticamente no
construtor
@Autowired
public ExampleService(SomeDependency dependency) {
this.dependency = dependency;
// Métodos que utilizam a dependência
}
Injeção por Setter
Na injeção por setter, as dependências são fornecidas ao bean por meio de um
método setter. Isso permite que a dependência seja configurada após a instância do
bean ser criada. Embora seja flexível, essa abordagem pode ser mais propensa a erros,
já que as dependências não são obrigatórias e podem ser configuradas a qualquer
momento durante o ciclo de vida do bean.
Como funciona:
Na injeção por setter, as dependências são injetadas por meio de métodos públicos,
geralmente no formato de setters, que são chamados pelo contêiner do Spring após a
criação do bean. Essa abordagem pode ser útil quando as dependências são opcionais
ou podem ser alteradas dinamicamente durante a vida útil do objeto.
@Component
public class ExemploService {
private SomeDependency dependency;
// O Spring vai injetar a dependência automaticamente através do
setter
@Autowired
public void setDependency(SomeDependency dependency) {
this.dependency = dependency;
// Métodos que utilizam a dependência
Spring Boot
O Spring Boot é um framework que facilita o desenvolvimento de aplicações Java,
especialmente APIs e microserviços, com base no ecossistema Spring. Ele é um
projeto do Spring Framework, criado com o objetivo de simplificar a configuração e o
setup de novos projetos Java, além de agilizar o desenvolvimento de aplicações de
forma robusta e eficiente.
Com Spring Boot, você pode criar aplicações Java prontas para produção de forma
muito mais rápida, sem precisar configurar um servidor de aplicação ou componentes
adicionais, como o Tomcat ou o Jetty, já que esses servidores estão embutidos. Ele
também permite a automação da configuração, para que os desenvolvedores possam
se concentrar no que realmente importa: o desenvolvimento da lógica da aplicação.
O Spring Boot permite a criação rápida de APIs por meio de seu modelo de "convenção
sobre configuração". Em vez de ter que configurar detalhes como portas de servidor ou
beans de infraestrutura, o Spring Boot fornece valores padrão e configurações
automáticas. Ele também oferece o recurso de "Spring Initializr" (disponível em
https://start.spring.io/), que gera a estrutura do projeto com as dependências
necessárias, permitindo começar rapidamente.
Vantagens do Spring Boot
O Spring Boot revolucionou a forma como aplicações baseadas no Spring Framework
são iniciadas e configuradas, proporcionando aos desenvolvedores um ambiente mais
ágil, eficiente e produtivo. Ele elimina grande parte da complexidade inicial, permitindo
que os desenvolvedores foquem na implementação das regras de negócio, sem se
preocupar com a configuração detalhada de infraestrutura.
O Spring Boot é a soma do Spring Framework com um servidor embutido menos as
configurações XML e classes de configurações.
Pontos positivos do Spring Boot
• Configuração Simplificada: Dispensa configurações extensivas em arquivos
XML ou classes de configuração.
• Servidor Embutido: Inclui servidores como Tomcat ou Jetty, permitindo
executar a aplicação diretamente, sem necessidade de deploy em um servidor
externo.
• Autoconfiguração: O Spring Boot fornece configurações automáticas
inteligentes para muitos casos de uso comuns, reduzindo ainda mais o esforço
de configuração manual.
• Dependências Prontas: Com uma única dependência no arquivo pom.xml (ou
build.gradle), como spring-boot-starter, ele já traz todas as
dependências essenciais do Spring Framework.
Estrutura de um projeto Spring Boot
A estrutura básica de um projeto Spring Boot segue uma organização simplificada
que facilita o desenvolvimento de aplicações Java, permitindo que os desenvolvedores
se concentrem mais na lógica de negócios e menos na configuração do sistema. Spring
Boot foi projetado para ser o mais simples possível, eliminando configurações
complexas e oferecendo configurações automáticas.
src/main/java
Contém o código fonte da aplicação e classe principal, com a anotação
@SpringBootApplication. Essa classe é responsável por inicializar a aplicação Spring
Boot e iniciar o contexto da aplicação.
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
src/main/resources
Contém recursos como arquivos de configuração (application.properties ou
application.yml), templates, estáticos, entre outros. O arquivo application.properties
ou application.yml é onde você pode configurar a conexão com o banco de dados,
definir portas, ou ajustar outras configurações da aplicação. No Spring Boot, muitas
configurações padrão são definidas automaticamente, e você pode sobrescrevê-las
quando necessário.
src/test/java
Contém os testes unitários e de integração da aplicação. O Spring Boot integra-se bem
com frameworks de teste, como JUnit e TestNG, facilitando a criação de testes
automatizados para a aplicação.
target
Gerada automaticamente após a compilação do projeto, contém os artefatos
compilados, como o .jar ou .war, o que é definido na criação do projeto.
Segurança na aplicação
Ele tem integração com o Spring Security, um dos frameworks de segurança mais
usados para proteger APIs e aplicações. Isso facilita a implementação de autenticação
e autorização, com suporte a tokens JWT, OAuth2, LDAP, etc. A segurança de
endpoints críticos da API pode ser configurada com facilidade, com suporte para
autenticação baseada em roles (perfis de usuário) e criptografia.
Desenvolvimento de APIs RESTful
Com o Spring MVC e o Spring Boot, é fácil criar APIs RESTful robustas e bem
estruturadas, utilizando recursos como o suporte ao padrão JSON e ferramentas de
documentação, como o SpringDoc. O Controlador para Mapear Endpoints Utilize
uma anotação @RestController para definir classes que atendem às requisições
HTTP. As requisições são mapeadas para métodos específicos por meio de anotações
como:
@GetMapping
@PostMapping
@PutMapping
@DeleteMapping
Contém suporte para parâmetros de requisição, cabeçalhos e corpos de requisição,
para melhor tratamento das requisições, podem ser usadas as seguintes anotações:
@RequestParam para parâmetros de consulta.
@PathVariable para capturar variáveis na URL.
@RequestBody para capturar o corpo da requisição.
O Spring Boot facilita a implementação de boas práticas de tratamento de erros em
APIs RESTful, oferecendo ferramentas como o @ControllerAdvice e o
@ExceptionHandler. Essas anotações permitem a centralização e personalização do
tratamento de exceções, garantindo que erros sejam tratados de forma consistente em
toda a aplicação. Com o @ControllerAdvice, você pode criar uma classe global de
manipulação de exceções, enquanto o @ExceptionHandler permite tratar exceções
específicas de maneira customizada, enviando respostas adequadas ao cliente, de
acordo com o tipo e contexto do erro.
Além disso, o Spring Boot oferece suporte integrado para validação de dados com o
Hibernate Validator, o que torna o processo de validação de entradas simples e
eficiente. Anotações como @NotNull, @Size, @Email, e @Valid permitem validar
objetos e parâmetros em tempo de execução, garantindo que os dados enviados para
a API estejam no formato correto e atendam às restrições de negócios. Por exemplo, o
uso de @Valid em parâmetros de métodos pode validar automaticamente os objetos
passados como argumento, enquanto @NotNull impede a entrada de valores nulos.
Suporte a Microsserviços
O Spring Boot, combinado com ferramentas como Spring Cloud, oferece uma solução
poderosa para o desenvolvimento e a orquestração de microsserviços. Com o Spring
Cloud, é possível implementar facilmente padrões essenciais de microsserviços,
como descoberta de serviços, balanceamento de carga e circuit breakers
(disjuntores), que garantem a resiliência e escalabilidade da aplicação.
Descoberta de serviços: O Spring Cloud facilita a comunicação entre microsserviços,
permitindo que cada serviço se registre automaticamente em um servidor de
descoberta, como o Eureka, tornando a localização de serviços dinâmicos e
descentralizados.
Balanceamento de carga: Usando o Ribbon ou outros mecanismos de
balanceamento, o Spring Cloud distribui as requisições entre instâncias de
microsserviços, melhorando a performance e evitando sobrecarga em um único ponto.
Circuit Breakers (Disjuntores): O Hystrix, uma das ferramentas do Spring Cloud,
oferece suporte para implementar circuit breakers, que protegem a aplicação de falhas
em cascata, isolando falhas em partes da aplicação e permitindo que o sistema
continue funcionando enquanto tenta se recuperar.
Integração e Escalabilidade
Integração com tecnologias como Kafka, RabbitMQ e Redis para lidar com sistemas
distribuídos e aplicativos em larga escala.
Iniciar uma aplicação do zero utilizando o Spring Framework pode ser uma tarefa
bastante trabalhosa. Isso porque é necessário realizar diversas configurações
manuais, como definir parâmetros em arquivos XML ou classes de configuração,
configurar o DispatcherServlet, gerar um arquivo WAR, e implantar a aplicação em um
Servlet Container (como o Tomcat), para finalmente executar a aplicação e começar
a implementar as regras de negócio.
O Spring Boot surgiu como uma extensão do Spring Framework, projetada para
simplificar esse processo. Ele reduz significativamente a complexidade das
configurações iniciais, além de acelerar o tempo necessário para iniciar uma aplicação
e deixá-la pronta para o desenvolvimento das regras de negócio. Entre suas principais
vantagens está a inclusão de um servidor de aplicação embutido (como Tomcat, Jetty
ou Undertow), que elimina a necessidade de configurar e gerenciar um servidor externo
para rodar a aplicação.
Dependências Starter
O Spring Boot fornece "starters", que são pacotes convenientes de dependências pré-
configuradas que facilitam a inclusão de funcionalidades na aplicação. Cada starter
inclui todas as dependências necessárias para um determinado recurso.
Exemplos de starters incluem:
• spring-boot-starter-web para aplicações web com Spring MVC.
• spring-boot-starter-data-jpa para trabalhar com banco de dados e
JPA.
• spring-boot-starter-security para integrar autenticação e
autorização.
O que é JPA (Java Persistence API)?
JPA é uma especificação do Java que define um conjunto de regras e convenções para
mapeamento objeto-relacional (ORM) e manipulação de dados em bancos de dados
relacionais. Ela é uma interface que define os comportamentos de persistência que os
frameworks ORM (como o Hibernate) devem implementar.
JPA não é um framework, mas uma especificação. Ou seja, ela define como as
operações de persistência devem ser feitas, mas não fornece uma implementação
concreta.
O que é o Spring Data JPA?
O Spring Data é um projeto do Spring para facilitar a criação da camada de persistência
de dados. Esse projeto tem abstrações para diferentes modelos de dados, como banco
de dados relacionais e não relacionais como o MongoDB e o Redis. Relacionado a
banco de dados relacionais, o Spring Data possibilita o acesso aos dados utilizando
interfaces e definindo apenas o nome de um método. O framework, com isso,
implementa todo o acesso ao banco de dados automaticamente. Por exemplo, a
classe na listagem a seguir define três métodos que serão traduzidos para consultas
em um banco de dados relacional:
List findByName(String name);
List findByAge(int age);
Pessoa findByCpf(String cpf);
O Spring Data JPA é uma abstração sobre JPA (Java Persistence API) e frameworks
como Hibernate, projetado para simplificar ainda mais o acesso aos dados. Ele elimina
boa parte do código boilerplate necessário no Hibernate, oferecendo repositórios
prontos para uso. O Spring Data JPA facilita a interação com bancos de dados
relacionais, como MySQL, PostgreSQL e outros. Ele fornece abstrações de repositório
que simplificam a criação de queries e acesso a dados, além de suportar o uso de JPQL
(Java Persistence Query Language) e critérios dinâmicos.
Configuração Simplificada:
spring.datasource.url=jdbc:mysql://localhost:3306/meu_banco
spring.datasource.username=root spring.datasource.password=senha
spring.jpa.hibernate.ddl-auto=update
Definição de Repositórios:
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
List<Usuario> findByNome(String nome);
}
Consulta Customizada:
Para criar consultas customizadas com Spring Data JPA utilizando a anotação @Query,
você pode escrever consultas JPQL (Java Persistence Query Language) ou SQL nativo
diretamente no repositório. No JPQL, você pode escrever consultas baseadas nas
entidades JPA, ao invés de diretamente nas tabelas do banco de dados. JPQL é
independente de banco de dados e trabalha com os nomes das entidades e seus
atributos.
@Query("SELECT u FROM Usuario u WHERE u.nome LIKE %:nome%") List<Usuario>
buscarPorNomeParcial(@Param("nome") String nome);
Às vezes, você pode precisar usar SQL nativo para aproveitar os recursos específicos
do banco de dados ou para otimizar a consulta. No Spring Data JPA, isso é possível
com o atributo nativeQuery = truena anotação @Query.
Integração com service:
@Service
public class UsuarioService {
@Autowired
private UsuarioRepository usuarioRepository;
public List<Usuario> listarUsuarios() {
return usuarioRepository.findAll();
}
}
O que é o Hibernate
O Hibernate é um framework ORM (Object-Relational Mapping) que automatiza a
conversão de objetos Java para tabelas de banco de dados e vice-versa. Ele reduz a
necessidade de escrever SQL manualmente e oferece controle total sobre o
mapeamento e operações com dados.
O Spring Data JPA é uma abstração sobre JPA (Java Persistence API) e frameworks
como Hibernate, projetado para simplificar ainda mais o acesso aos dados. Ele elimina
boa parte do código boilerplate necessário no Hibernate, oferecendo repositórios
prontos para uso.
Iniciando primeiro projeto com Spring Boot
Para iniciar um projeto Spring Boot leva-se apenas poucos minutos, utilizando o Spring
Initializr através do site https://start.spring.io/. Basta definir algumas configurações
iniciais como o gerenciador de dependências a ser utilizado, a linguagem e versão da
linguagem, a versão do Spring Boot, dados básicos do projeto como Group e Artifact e
por fim, selecionar as dependências iniciais que irão compor o projeto. Assim, ao clicar
no botão Generate um projeto compactado é disponibilizado.
Então, é preciso descompactar esse projeto em um determinado diretório na máquina
e importá-lo para a IDE escolhida para o desenvolvimento. Dentro do diretório raiz
com.usuario.usuario fica localizada a classe principal da aplicação,
UsuarioApplication, gerada automaticamente pelo Spring Boot sendo a responsável e
para iniciar a aplicação basta clicar com o botão direito do mouse em Run e a
aplicação será iniciada.
Arquitetura REST e APIs RESTful
Uma API REST (Representational State Transfer) é uma aplicação cliente-servidor que
permite o envio e recebimento de dados por meio do protocolo HTTP. Ela utiliza
padrões de comunicação como XML e JSON e pode ser implementada em qualquer
linguagem de programação. A principal finalidade de uma API é facilitar a
interoperabilidade entre aplicações, tornando possível que diferentes sistemas
compartilhem informações de maneira eficiente e consistente.
Por exemplo, considere dois sistemas: um desktop e outro mobile. Ambos podem
consumir a mesma API para montar suas interfaces e realizar operações, mostrando
como uma API pode ser reutilizada em diferentes plataformas, reduzindo redundâncias
e promovendo integração.
REST e RESTful
REST (Representational State Transfer): É um modelo arquitetural abstrato que
define um conjunto de princípios para projetar sistemas distribuídos, como a
comunicação entre cliente e servidor. Ele se baseia em conceitos como: Recursos
representados por URLs. Operações padrão do protocolo HTTP, como GET, POST, PUT,
DELETE. Comunicação sem estado (stateless), ou seja, cada requisição do cliente ao
servidor deve conter todas as informações necessárias para ser processada.
RESTful: Refere-se à implementação prática e concreta do modelo REST em uma API.
Para que uma API seja considerada RESTful, ela deve aplicar corretamente os
princípios da arquitetura REST.
Uma API REST é um conceito amplo que define como sistemas podem se comunicar
via HTTP, enquanto uma API RESTful é a implementação desse conceito, aderindo
rigorosamente aos princípios da arquitetura REST. Para criar uma API RESTful, é
essencial entender os fundamentos do REST e aplicá-los corretamente, garantindo
uma comunicação eficiente, escalável e interoperável entre sistemas distintos.
Tipos de Representações em APIs RESTful
Uma API RESTful pode utilizar dois formatos principais de comunicação: XML e JSON.
O XML utiliza tags para estruturar os dados, o que resulta em um formato mais
verboso e pesado em comparação com o JSON, que é mais compacto e eficiente.
Enquanto o XML é adequado para casos que exigem uma estrutura mais rígida e
validação, o JSON é amplamente preferido em APIs RESTful devido à sua leveza e
simplicidade, o que facilita o processamento e a transmissão de dados.
1. JSON (JavaScript Object Notation)
O JSON (JavaScript Object Notation) é um formato leve de troca de dados, fácil de ler e
escrever para humanos, e fácil de analisar e gerar para máquinas. Ele é amplamente
utilizado em APIs RESTful devido à sua simplicidade e eficiência.
Características do JSON:
• Chave-valor: Os dados são estruturados em pares chave-valor, onde a
chave é uma string e o valor pode ser qualquer tipo de dado (string, número,
booleano, objeto, array).
• Simplicidade: A sintaxe do JSON é simples e intuitiva, o que facilita sua
leitura e manipulação.
• Leveza: Comparado ao XML, o JSON é mais leve, o que torna a troca de
dados mais eficiente, especialmente em redes de baixa largura de banda.
Vantagens do JSON em APIs RESTful:
1. Leveza: Menor sobrecarga de dados em comparação com o XML.
2. Facilidade de Integração: A maioria das linguagens de programação
modernas oferece suporte nativo ao JSON.
3. Desempenho: JSON é mais rápido de processar do que XML.
4. Popularidade: É o formato de dados preferido em APIs RESTful devido à sua
simplicidade e suporte amplo.
2. XML (eXtensible Markup Language)
O XML para APIs segue o mesmo conceito básico de estruturação de dados, utilizando
tags para organizar e descrever a informação. A principal vantagem do XML é sua
extensibilidade, permitindo criar tags personalizadas para representar dados de
maneira hierárquica e estruturada. Em APIs, o XML é frequentemente utilizado em APIs
baseadas em SOAP (Simple Object Access Protocol), mas também pode ser usado em
APIs RESTful, dependendo dos requisitos do sistema.
Vantagens do XML em APIs:
• Estrutura Complexa: O XML é ideal para representar dados complexos e
hierárquicos, tornando-o adequado para APIs que exigem essa complexidade
(como em integrações empresariais ou sistemas bancários).
• Validação de Dados: A capacidade de validar documentos XML contra um XML
Schema torna o formato robusto para garantir que os dados enviados ou
recebidos estejam corretos e bem estruturados.
• Extensibilidade: O XML permite a definição de tags personalizadas, oferecendo
flexibilidade para adaptar os dados às necessidades específicas da aplicação.
Desvantagens do XML em APIs:
• Verboso: O XML é mais pesado em termos de tamanho, pois exige mais tags
para encapsular a mesma informação que o JSON. Isso pode resultar em
maiores tempos de resposta e maior consumo de banda.
• Desempenho: O XML é mais lento para processar em comparação com o
JSON, devido à necessidade de analisar as tags e, em muitos casos, realizar a
validação do conteúdo.
• Menos Compacto: O formato do XML é mais expandido, o que pode aumentar
o tempo de transmissão de dados, especialmente em redes com largura de
banda limitada.
Modelo de Maturidade de Richardson
O Modelo de Maturidade de Richardson é uma abordagem que organiza a evolução
de APIs em quatro níveis, oferecendo uma métrica para medir quão aderente uma API é
aos princípios REST. Ele foi proposto por Leonard Richardson como uma forma de
avaliar e classificar o nível de maturidade de uma API com base em sua utilização das
boas práticas da arquitetura REST.
Esse modelo é especialmente útil porque reconhece que, em certos cenários, seguir
todas as seis constraints definidas por Roy Fielding pode ser inviável, especialmente
para casos mais simples. Portanto, ele oferece uma maneira progressiva de alcançar
uma API RESTful, com níveis que agregam valor gradualmente.
Os 4 níveis do modelo de maturidade de Leonardo
Richardson
O Modelo de Maturidade de Leonardo Richardson descreve quatro níveis de
maturidade para a criação de APIs RESTful, com foco na forma como as APIs são
projetadas e na adesão aos princípios do estilo arquitetural REST. Esses níveis são
baseados em como as APIs lidam com recursos, métodos HTTP, e representação de
dados, entre outros aspectos.
Nível 0: POX (Plain Old XML over HTTP)
A API utiliza o protocolo HTTP apenas como um mecanismo de transporte, sem
aproveitar os recursos semânticos dos verbos ou status HTTP.
Características:
1- Geralmente, todas as operações são executadas no mesmo endpoint.
2- Dados são enviados e recebidos sem uma estrutura clara ou
semântica.
Nível 1: Recursos
A API começa a estruturar seus dados com base em recursos, cada um com uma URL
única que o identifica.
Características:
1- Utilização de substantivos para representar recursos (como users,
orders).
2- URLs são bem definidas e representam recursos específicos.
Nível 2: Verbos HTTP
A API utiliza os métodos HTTP de forma semântica, como GET, POST, PUT e DELETE,
para operações específicas nos recursos.
Características:
1- Cada método HTTP tem um propósito claro: GET para leitura, POST
para criação, PUT para atualização, e DELETE para exclusão.
2- O protocolo HTTP é usado em sua totalidade, incluindo cabeçalhos e
códigos de status para cada resposta.
Exemplo:
▪ GET /usuario/1 (retorna o usuário com ID 1).
▪ POST /usuario (cria um novo usuário).
▪ DELETE /usuario /1 (exclui o usuário com ID 1).
▪ UPDAT /usuario/ 1 (Atualização)
Nível 3: HATEOAS (Hypermedia as the Engine of Application State)
No nível mais avançado, a API implementa hipermídia, permitindo que os clientes
naveguem entre os recursos e descubram relacionamentos por meio de links
dinâmicos fornecidos nas respostas.
Características:
1- As respostas incluem links que mostram o estado atual do recurso e
as possíveis ações futuras.
2- O cliente pode navegar pela API sem precisar de conhecimento prévio
sobre a estrutura dos endpoints.
O Modelo de Maturidade de Richardson avalia o quão bem uma API aplica os
princípios REST e propõe um progresso gradual em quatro níveis. APIs que atingem o
Nível 3 (HATEOAS) podem ser consideradas totalmente RESTful, pois implementam
todos os aspectos da arquitetura REST. Essa abordagem ajuda desenvolvedores a criar
APIs de forma evolutiva, priorizando as necessidades do projeto e facilitando a adoção
de práticas mais robustas conforme o sistema cresce em complexidade.
Criação da API RESTful de Usuários
Com base em toda a teoria que trabalhamos até aqui vamos praticar um pouco
desenvolvendo uma a API RESTful de Usuários que será responsável por gerenciar o
cadastro, consulta, atualização e exclusão de informações relacionadas aos usuários
do sistema. Ela será desenvolvida com base em práticas modernas de
desenvolvimento e organizada em camadas para garantir escalabilidade,
manutenibilidade e facilidade de uso.
Funcionalidades da API RESTful de usuários
1. Cadastro de Usuário:
• Permite o registro de novos usuários com dados como nome, email,
senha e perfil de acesso (ADMIN ou USER).
• Validações serão aplicadas para garantir que os dados inseridos
estejam corretos e únicos (como e-mails).
2. Consulta de Usuários:
• Consulta geral: Retorna a lista de todos os usuários cadastrados.
• Consulta por ID: Retorna os dados detalhados de um usuário
específico.
3. Atualização de Usuário:
• Permite a modificação de dados de usuários, exceto campos
sensíveis como senha (que terá um endpoint específico para
alteração).
4. Exclusão de Usuário:
• Remove o usuário do banco de dados ou marca-o como inativo para
preservação de histórico.
5. Alteração de Senha:
• Endpoint exclusivo para que o usuário possa alterar sua senha,
seguindo boas práticas de segurança.
6. Alteração da foto do usuário:
• Endpoint para atualizar foto de perfiu do usuario.
Descrição do projeto
A aplicação será iniciada a partir do site oficial do Spring io https://start.spring.io/,
com JDK 17 ou superior e IDE IntelliJ. O gerenciamento de dependências será
realizado com MAVE , garantindo uma estrutura limpa e bem organizada, essencial
para uma manutenção eficiente do código. Utilizaremos PostgreSQL como sistema de
gerenciamento de banco de dados (SGBD), oferecendo robustez, escalabilidade e
confiabilidade no armazenamento dos dados. Para a validação e testes dos endpoints
da API, a ferramenta escolhida será o Postman, garantindo a qualidade da
comunicação da API com o cliente.
O projeto será modularizado, permitindo que cada módulo tenha responsabilidades
bem definidas e independentes, facilitando a evolução e manutenção contínua da
aplicação. A API será estruturada segundo o modelo de maturidade de Leonardo
Richardson, adotando uma abordagem incremental, com foco nas boas práticas do
estilo arquitetural RESTful. O JSON será utilizado como formato de representação de
dados, por sua eficiência e ampla adoção, garantindo uma comunicação ágil e leve
entre cliente e servidor.
A aplicação será construída com Spring Boot, proporcionando uma solução
escalável, de fácil manutenção e com alto desempenho. A versão do Java escolhida
será a 21, aproveitando os recursos mais recentes para otimizar a performance,
segurança e qualidade do código. A versão do Spring será a 3.3.5, oferecendo
estabilidade e segurança, garantindo conformidade com as melhores práticas de
desenvolvimento. O Spring MVC será aplicado para estruturar e organizar o controle
de requisições e respostas, promovendo a separação de responsabilidades e
implementando uma arquitetura limpa e eficiente.
A comunicação com o banco de dados será gerenciada pelo Spring Data JPA,
proporcionando uma forma eficiente e simplificada de manipulação de dados, com a
vantagem de aproveitar a abstração poderosa do JPA para operações de CRUD.
Também implementaremos validações customizadas e filtros dinâmicos para
garantir a flexibilidade e consistência na aplicação.
Além disso, a API contará com recursos de paginação, melhorando a performance em
consultas que retornam grandes volumes de dados. O tratamento global de exceções
será implementado para garantir que erros sejam gerenciados de forma centralizada,
com respostas adequadas e amigáveis para o cliente. O uso de Log4j será incorporado
para monitoramento eficiente, permitindo um controle de logs robusto e de fácil
análise. Para facilitar o uso e integração da API, a documentação será gerada
automaticamente, utilizando Swagger ou outras ferramentas, garantindo que a API
seja fácil de consumir e bem documentada.
Com essa combinação de tecnologias e práticas de desenvolvimento de alto nível, a
API será robusta, escalável e de fácil manutenção, pronta para atender as
necessidades de negócios de forma eficiente, segura e com alto desempenho.
Dependências principais
As dependências são muito importantes no Spring Framwork, elas são bibliotecas
ou módulos externos que sua aplicação precisa para funcionar corretamente. Elas
adicionam funcionalidades específicas ao projeto, como conexão com bancos de
dados, segurança, manipulação de dados, criação de APIs REST, entre outras.
• spring-boot-starter-web: Para criar e gerenciar endpoints RESTful e configurar
o servidor embutido.
• spring-boot-starter-data-jpa: Para implementar o acesso e a manipulação de
dados usando o JPA (Java Persistence API).
• PostgreSQL Driver: Para conectar a aplicação ao banco de dados PostgreSQL.
• spring-boot-starter-validation: Para adicionar validações nos DTOs e entradas
de dados usando a especificação Bean Validation.
• Lombok: Para reduzir o código boilerplate, como getters, setters, equals,
hashCode, toString, e construtores.
• spring-boot-starter-test: Incluída por padrão, fornece ferramentas para testes
unitários e de integração, incluindo o JUnit e o Mockito.
Essas dependências cobrem o essencial para desenvolver, validar e testar uma API
RESTful robusta e eficiente, claro que ao longo do desenvolvimento do projeto
poderemos e vamos acrescentar mais dependências, mas até o dado momento essas
são o suficientes.
Criação do projeto e configurações iniciais
O primeiro passo para criar uma aplicação Spring Boot é configurar o projeto com suas
dependências. Utilizaremos o Maven para gerenciá-las. Duas configurações são
importantes nesta fase do projeto, como visto anteriomente acessaremos o site
https://start.spring.io/ e criaremos o projeto na linguagem Java, com o gerenciador de
dependência MAVE, nesse material utilizaremos a verção 3.3.5 que atualmente é a
verção mais estavel definiremos o Group como com.sistema_vendas e o Artifact como
usuario o Packaging será Jar e a versão do Java que usaremos será a 21.
Dependências que usaremos:
• Spring-boot-starter-web
• JPA
• PostgreSQL
• Validation
• Lombok
• spring-boot-starter-test (já vem implementada).
Após configurar esses detalhes, gere o projeto clicando no botão “Gerar”. O arquivo
compactado (ZIP) contendo a estrutura inicial será baixado, pronto para ser importado em
seu
Configuração da organização do projeto em módulos
É essencial entender claramente o papel de cada módulo do seu projeto, dividindo
bem as responsabilidades. Recomenda-se organizar o projeto de forma estruturada, o
que pode variar conforme o estilo de desenvolvimento. Particularmente, gosto de criar
toda a organização inicial do projeto e, a partir daí, trabalhar no seu desenvolvimento.
Porém, essa não é uma regra fixa. O mais importante é que cada parte do projeto tenha
uma função bem definida e que você saiba como essas partes irão interagir entre si.
Dedicar tempo para planejar a arquitetura e o fluxo de trabalho facilita a manutenção,
a escalabilidade e a colaboração com outros desenvolvedores.
Modelo de organização que seguirei:
Não se preocupe se tudo isso parecer novo para você. Ao longo deste material,
abordaremos todos os módulos dessa arquitetura de forma detalhada, garantindo que
você compreenda cada etapa do processo.
Como o projeto está sendo criado com o MAVE (que é a ferramenta de gerenciamento
de dependências e automação de builds) é importante dizer que na organização do
projeto teremos também um arquivo chamado pom.xml, nele será tratada todas as
dependências da aplicação.
Criação da base de dados no PostgreSQL
A criação da base de dados no PostgreSQL envolve alguns passos simples, que podem
ser realizados tanto pela linha de comando quanto por uma interface gráfica, como o
pgAdmin. Aqui está o processo básico utilizando a linha de comando, que pode ser
adaptado conforme a necessidade:
CREATE DATABASE nome_do_banco;
Utilizando o pgAdmin (Interface Gráfica):
Se preferir uma interface gráfica para criar o banco de dados:
1. Abra o pgAdmin e conecte-se ao servidor PostgreSQL.
2. No painel esquerdo, clique com o botão direito em Databases e selecione
Create -> Database.
3. No formulário de criação, insira o nome do banco de dados e clique em Save.
Configuração de conexão com o SGB em arquivo .ymal
Para configurar e conectar uma API Spring Boot com um banco de dados relacional
usando o Spring Data JPA devemos primeiro certificarmos que temos a dependência do
PostgreSQL e do JPA instaladas, isso foi realizado na criação da aplicação
anteriomente.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
</dependency>
Por padrão, o arquivo de configuração de uma aplicação Spring Boot vem com a
extensão .properties. No entanto, optaremos por usar a extensão .yml (YAML) para
configurar a aplicação. O formato YAML é mais legível e organizado, especialmente
para configurações hierárquicas e valores agrupados, tornando-o ideal para projetos
que exigem maior clareza e estrutura nos arquivos de configuração. Além disso, o
Spring Boot oferece suporte nativo para ambos os formatos, permitindo a escolha com
base nas preferências do time ou nas necessidades do projeto.
server:
port: 8087 # Define a porta em que o servidor será iniciado.
servlet:
context-path: '/sistema-vendas-usuario/'# Define o contexto base para os endpoints.
# Conexão da base de dados PostgreSQL.
spring:
datasource:
url: jdbc:postgresql://localhost:5433/sistema_vendas_usuario
username: postgres
password: postgres
driver-class-name: org.postgresql.Driver
hikari:
maximum-pool-size: 10
jpa: # Configurações do JPA
open-in-view: false
hibernate: # Configurações do hibernate
ddl-auto: update
dialect: org.hibernate.dialect.PostgreSQLDialect
jdbc:
lob.non-contextual-creation: true # Não carregar logs de erro do JDBC
properties: # Propriedades do JPA
hibernate:
show_sql: true # Exibe o SQL no console
format_sql: true # Formata os comandos SQL para facilitar a leitura
output:
ansi:
enabled: Always # Exibir logs coloridos
SQL: debug # Exibe o SQL gerado pelo Hibernate
type:
descriptor:
sql:
BasicBinder: trace # Exibe os parâmetros que são inseridos no SQL
Outras configurações importantes no arquivo .ymal
Além da configuração da conexão com a base de dados, também realizamos ajustes
importantes nas propriedades da aplicação para garantir um ambiente de
desenvolvimento eficiente e organizado. A seguir, detalho as principais configurações
realizadas:
A porta do servidor está configurada para ser 8087 . Isso significa que o serviço estará
disponível em http://localhost:8087, permitindo que os usuários acessem uma
API na porta especificada. O contexto base para todos os endpoints da aplicação é
configurado como /sistema-vendas-usuario. Isso implica que todos os caminhos
de URL para os endpoints da API serão precedidos por /api. Por exemplo, um endpoint
de cadastro de usuário poderia ser acessado via http://localhost:8087/sistema-
vendas-usuario /usuarios.
Configurações do JPA (Java Persistence API): A configuração de
spring.jpa.hibernate.ddl-auto foi ajustada para definir o comportamento do
Hibernate na criação ou atualização das tabelas. O valor comum é update para
ambientes de desenvolvimento, o que permite ao Hibernate atualizar o esquema do
banco conforme as alterações no código.A opção spring.jpa.show-sql foi
habilitada para exibir os comandos SQL gerados pela JPA diretamente no terminal, o
que facilita a depuração e o acompanhamento das operações realizadas na base de
dados.
Configurações do Hibernate: As configurações do Hibernate foram otimizadas,
incluindo a definição do dialeto específico do PostgreSQL
(spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.Postg
reSQLDialect), garantindo que o Hibernate use as particularidades do banco para
gerar as consultas de forma eficiente. O nível de log de SQL foi ajustado para garantir
que todas as consultas e atualizações feitas pelo Hibernate sejam visíveis no terminal,
o que é útil durante o desenvolvimento para acompanhar o fluxo das transações.
Logs SQL no Terminal: Foi configurado para que os logs SQL sejam exibidos
diretamente no terminal (console) com o uso do spring.jpa.show-sql=true. Isso
facilita a visualização de todas as consultas SQL executadas pela aplicação,
permitindo um diagnóstico rápido de problemas no banco de dados.
Logs Coloridos: A configuração de logs coloridos foi aplicada para melhorar a
legibilidade no terminal, permitindo identificar facilmente os diferentes tipos de logs
gerados pela aplicação (informações, erros, avisos, etc.), e facilitando a análise dos
logs durante o desenvolvimento.
Mapeamento da entidade de usuário
O mapeamento de uma entidade de usuário em uma aplicação Java utilizando JPA
(Java Persistence API) e Hibernate envolve a definição de uma classe Java que
representa uma tabela no banco de dados. Essa classe contém anotações para
descrever como os campos da tabela são mapeados para os atributos da classe.
Esse mapeamento da entidade de usuário segue boas práticas de organização e
validação de dados, garantindo que o modelo seja robusto, seguro e facilmente
integrável a APIs RESTful.
O mapeamento das entidades será realizado dentro dos modelos de módulo , que
foram previamente criados.
@JsonInclude(JsonInclude.Include.NON_NULL) // Iclua apenas valores que não sejam
nulos.
@Entity // Indica que essa classe é uma entidade.
@Table(name = "TB_USUARIO") // Nome da tabela no BD
@Data // Anotação do lombok para incluir GETs, SETs e Construtores.
public class UsuarioModel implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO) //
private UUID usuarioId;
@Column(nullable = false, unique = true, length = 150)
private String nome;
@Column(nullable = false, unique = true, length = 150)
@Email(message = "O e-mail deve ser válido.")
private String email;
@JsonIgnore // No processo de formalização, ignore a senha.
@Column(nullable = false, unique = true, length = 255)
private String senha;
@Column(nullable = false, length = 255)
private String nomeCompleto;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private StatusUsuario statusUsuario;
@Column(nullable = false)
@Enumerated(EnumType.STRING)
private TipoUsuario tipoUsuario;
@Column(length = 20)
private String telefoneCelular;
@Column(nullable = true, length = 255)
private String imagemUrl;
@Column(nullable = false, updatable = false)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyy HH:mm:ss")
private LocalDateTime dataCriacao;
@Column(nullable = false)
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyy HH:mm:ss")
private LocalDateTime dataAtualizacao;
}
Criação dos ENUMS
No módulo enums, serão criados dois enums para padronizar e organizar as informações
relacionadas ao tipo de usuário e ao status do usuário. Isso garante que esses valores sejam
consistentes em toda a aplicação, reduzindo o risco de erros e facilitando a manutenção do
código.
public enum TipoUsuario {
PROPRIETARIO,
VENDEDOR,
GERENTE,
CAIXA,
CONTADORA,
ESTOQUISTA
}
public enum StatusUsuario {
ATIVO,
BLOQUEADO,
}
Algumas anotações comuns no mapeamento
• @Entity: Marca a classe como uma entidade JPA, representando uma tabela no
banco de dados.
• @Table: Define o nome da tabela no banco de dados, caso seja diferente do
nome da classe.
• @Id: Indica que o campo é a chave primária da tabela.
• @GeneratedValue: Especifica a estratégia de geração do valor da chave
primária (ex: AUTO, IDENTITY, SEQUENCE).
• @Column: Especifica o nome da coluna e outras propriedades de mapeamento
de um campo.
Como realizo um CRUD
Ordem que sigo para realizar uma operação de CRUD:
1. Defininir o mapeamento e realcionamento das Entidades (modelo de dados)
2. Criar os Repositórios (para interação com o banco de dados)
3. Criar os Controllers (endpoints para expor as funcionalidades via API)
4. Criar DTOs (Objetos de Transferência de Dados)
5. Criar interfaces do service(Contratos a serem implementados)
6. Implementar o Service (lógica de negócio)
Essa ordem não é uma regra. Você pode optar por outra sequência de desenvolvimento
que se adapte melhor ao seu fluxo de trabalho. O mais importante é estabelecer um
padrão de desenvolvimento claro, definindo onde o processo começa e onde termina.
Isso garante organização, consistência e facilita a colaboração em equipe durante o
desenvolvimento do projeto.
Verbos HTTP
Os métodos HTTP correspondem diretamente às operações CRUD (Create, Read,
Update, Delete), que são os pilares das operações em uma API RESTful. Aqui está
como cada método HTTP é associado a uma operação CRUD e como usá-los
corretamente:
POST: O método POST é utilizado para criar novos recursos no servidor. Ele envia
dados ao servidor, que processa esses dados e cria um novo recurso.
GET: O método GET é utilizado para recuperar informações de um recurso ou uma
coleção de recursos. Este método não altera o estado dos dados.
PUT: O método PUT é utilizado para atualizar um recurso existente. Ele envia dados ao
servidor, que substitui o recurso atual com os novos dados.
PATCH (Update): O método PATCH é semelhante ao PUT, mas é usado para
atualizações parciais de um recurso. Ou seja, apenas os campos que precisam ser
atualizados são enviados no corpo da
DELETE: O método DELETE é utilizado para excluir um recurso do servidor. Mesmo que
a operação seja uma exclusão logica, ou seja, não apague o registro da base de dados
o método HTTP correspondente deve ser o DELETE.
Status HTTP: É importante retornar os códigos de status HTTP apropriados para indicar
o sucesso ou falha da operação:
• 200 OK – Requisição bem-sucedida.
• 201 Created – Recurso criado com sucesso (geralmente para POST).
• 204 No Content – Requisição bem-sucedida, mas sem corpo de resposta
(geralmente para DELETE).
• 400 Bad Request – A requisição está malformada ou contém dados inválidos.
• 404 Not Found – Recurso não encontrado.
• 500 Internal Server Error – Erro interno no servidor.
Os endpoints de uma API RESTful devem representar recursos (substantivos) e não
ações (verbos). O HTTP já define os verbos que representam ações (como GET, POST,
PUT, DELETE), portanto, você não precisa incluí-los na URL.
Criação do Repository
No módulo criado para os repositórios, vamos adicionar uma nova interface chamada
UsuarioRepository. Essa interface será responsável pela realização de operações
de persistência relacionadas à entidade UsuarioModel. Para isso, ela irá herdar a
interface JpaRepository<UsuarioModel, UUID>, fornecendo acesso a métodos
prontos para manipulação da entidade, como salvar, buscar, atualizar e excluir
registros no banco de dados. Na definição de JpaRepository, será especificada a
classe da entidade ( UsuarioModel) e o tipo de chave primária utilizada pela entidade (
UUID).
Essa abordagem segue as boas práticas do Spring Data JPA, permitindo que as
operações de CRUD sejam realizadas de forma eficiente.
public interface UsuarioRepository extends
JpaRepository<UsuarioModel,UUID>{
Controllers da aplicação
A classe UsuarioController será responsável por gerenciar as operações relacionadas
à entidade Usuário na API. Ela contém 6 métodos principais, cada um com sua
funcionalidade específica, seguindo as melhores práticas RESTful para garantir
clareza, escalabilidade e segurança.
• getAllUsuarios: Responsável por listar todos os usuários cadastrados na
base de dados. Esse método retornará uma lista com os dados de todos os
usuários existentes.
• getUsuarioById: Responsável por buscar e retornar os dados de um usuário
específico a partir de seu ID. Esse método será utilizado quando for necessário
acessar informações detalhadas de um único usuário.
• updateUsuario:
• Este método pode ser utilizado para alterar informações como nome, email, ou
outros dados associados ao usuário, mantendo o restante das informações
intactas.
• deleteUsuario: Realiza a exclusão física dos dados de um usuário na tabela.
Após a execução, o registro correspondente ao usuário será permanentemente
removido do banco de dados.
• updateSenha: Permite a atualização da senha de um usuário previamente
cadastrado. Esse método garantirá que a senha seja alterada de forma segura,
atendendo às regras de validação estabelecidas.
• updateImagem: Atualiza a foto de perfil do usuário. Esse método possibilita a
modificação ou inclusão da imagem associada a um usuário existente no
sistema.
Você deve estar se perguntado, mas ceder o método que salva o cadastro de usuários
na base de dados, calma, esse método será implementado em uma classe distinta
chamada de AutenticacaoUsuarioController, para melhor organização e estruturação
do código criaremos esse método em uma classe separada, em grandes projetos de
arquitetura de microsserviços essa prática é muito bem vista e adotada, vamos
entender porquer.
Ao projetar uma API bem estruturada e escalável, é importante adotar práticas que
ajudem na manutenção do código, facilitem a colaboração da equipe e sigam as
melhores convenções de arquitetura. No caso da nossa aplicação, o método de
cadastro de usuários será implementado em uma classe distinta chamada
AutenticacaoUsuarioController, seguindo um princípio comum em sistemas de
grande escala e microserviços. Em projetos de arquitetura de microsserviços e
sistemas maiores, é comum adotar o princípio da separação de responsabilidades.
Isso significa que cada controlador (ou componente) é responsável por um conjunto
específico de funcionalidades, evitando que um único controlador se torne
sobrecarregado e difícil de manter.
A separação do cadastro de usuários da lógica de manipulação de dados (como
atualização, exclusão ou listagem) em um controlador distinto traz benefícios claros,
como:
1. Clareza e Organização: Ao dividir responsabilidades, o código se torna mais
legível e modular. O UsuarioController será responsável por operações como
listagem, atualização e exclusão de usuários, enquanto o
AutenticacaoUsuarioController será encarregado das operações relacionadas
ao registro de novos usuários.
2. Escalabilidade e Manutenibilidade: Em sistemas grandes, cada parte da
aplicação pode evoluir independentemente. O AutenticacaoUsuarioController
pode ser modificado ou escalado sem impactar diretamente os outros
controladores que gerenciam a lógica de negócio dos usuários.
3. Segurança: O cadastro de usuários frequentemente envolve a validação e a
criptografia de senhas, algo que merece uma atenção especial. Isolando a
lógica de cadastro em um controlador dedicado, podemos garantir que a
segurança do processo de criação de novos usuários seja tratada de maneira
específica, sem misturar com outras responsabilidades.
4. Futuras Extensões: Em um cenário de microsserviços, esse controlador de
autenticação pode, no futuro, evoluir para um serviço autônomo de
autenticação e registro de usuários, possivelmente integrado com serviços
externos de identidade, como OAuth, OpenID Connect ou LDAP.
AutenticacaoUsuarioController
O AutenticacaoUsuarioController será responsável exclusivamente pela criação de
novos usuários na base de dados. Este controlador será um ponto central para o
processo de registro de usuários, implementando a lógica de validação de dados e,
principalmente, a criptografia de senhas. Abaixo estão as principais responsabilidades
e a estrutura desse controlador:
1. Cadastro de Usuário: O método de cadastro será responsável por receber as
informações do usuário (como nome, email, e senha), validar esses dados e,
então, criar um novo usuário na base de dados.
2. Validação de Dados: O controlador vai garantir que todos os dados obrigatórios
sejam fornecidos e válidos (ex: email no formato correto, senha com requisitos
mínimos de complexidade).
3. Respostas HTTP: Ao finalizar o cadastro, o controlador deve retornar uma
resposta que confirme a criação do usuário ou, em caso de erro, forneça
detalhes sobre o que deu errado (ex: email já cadastrado, senha fraca, etc.).
Importância do AutenticacaoUsuarioController
A separação da lógica de cadastro de usuários em um controlador distinto, como o
AutenticacaoUsuarioController, é uma prática recomendada, especialmente em
sistemas que seguem uma arquitetura modular ou baseada em microsserviços. Ela
promove clareza, facilita a manutenção do código e prepara o sistema para futuras
expansões e melhorias. Além disso, essa abordagem segue os princípios de
separação de responsabilidades e encapsulamento, que são fundamentais para
garantir que o código seja robusto, escalável e seguro.
Criando UsuarioController
@RestController
@RequestMapping("/usuario")
public class UsuarioController {
final UsuarioService usuarioService;
public UsuarioController(UsuarioService usuarioService) {
this.usuarioService = usuarioService;
}
@GetMapping
public ResponseEntity<List<UsuarioModel>> getAllUsuario(){
// uso de listas (Tipo do retorno List)
List<UsuarioModel> usuarioModels = usuarioService.findAll();
return ResponseEntity.status(HttpStatus.OK).body(usuarioModels);
}
}
utilizamos o @RestController para definir a classe como um Bean do tipo
controlador, indicando que ela será responsável por lidar com requisições HTTP e
retornar respostas no formato JSON (ou outro, dependendo da configuração). A
anotação @RestController combina @Controller e @ResponseBody, permitindo
que os métodos dentro da classe retornem dados diretamente como resposta, sem a
necessidade de configurar o corpo da resposta manualmente.
A anotação @RequestMapping("/usuario") define o prefixo de URL para todas as
rotas dentro deste controlador. Nesse caso, todas as requisições para o caminho
/usuario serão tratadas por este controlador. Portanto, o endpoint completo para
listar os usuários será algo como /usuario quando você fizer uma requisição GET.
Injeção de Dependência via Construtor
A injeção de dependência é feita através do construtor da classe. O Spring,
automaticamente, gerencia a criação e injeção do objeto UsuarioService dentro do
controlador. O uso de final na declaração da variável usuarioService garante que
essa dependência não será reatribuída após a inicialização da classe, promovendo
boas práticas de imutabilidade e clareza no código.
Endpoint getAllUsuario
O método getAllUsuario() é responsável por tratar as requisições GET na rota
/usuario. Esse método invoca a lógica de negócios contida no UsuarioService para
recuperar todos os usuários da base de dados. Ele retorna uma lista de usuários, que é
então convertida para um formato JSON e enviada como resposta.
A resposta é encapsulada em um ResponseEntity, o que permite adicionar detalhes
extras, como o status da resposta (HTTP 200 OK) e o corpo da resposta (a lista de
usuários).
Melhorando o método getAllUsuario
Este código pode ser melhorado significativamente ao longo do desenvolvimento do
projeto, especialmente com a implementação de paginações e especificações:
• Paginação: Em vez de retornar todos os usuários de uma vez, podemos
implementar um sistema de paginação para limitar a quantidade de usuários
retornados por requisição, melhorando a performance e a escalabilidade da
aplicação.
• Especificações Dinâmicas: No futuro, poderemos usar especificações ou
filtros dinâmicos para permitir buscas avançadas, como filtrar usuários por
nome, email, ou outros critérios, sem precisar criar endpoints específicos para
cada caso.
Após a criação do controlador UsuarioController, o próximo passo é implementar a
lógica de negócios relacionada aos usuários, que deve ser organizada em uma camada
de serviço. Para isso, seguem os passos que devemos seguir:
Criar a Interface do Serviço: findAll
Primeiro, criamos uma interface UsuarioService. Essa interface define os contratos
dos métodos que a camada de serviço deve fornecer. No caso, temos o método
findAll(), que será responsável por buscar todos os usuários na base de dados:
List<UsuarioModel> findAll();
Implementando a interface do service
A próxima etapa é criar uma classe que implementa a interface UsuarioService. Essa
classe será a responsável por implementar a lógica de negócios, realizando operações
específicas no banco de dados, entre outras funções.
@Service
public class UsuarioServiceImpl implements UsuarioService {
@Override
public List<UsuarioModel> findAll() {
return usuarioRepository.findAll();
}
}
Endpoint para o método getOnUsuario
@GetMapping("/{usuarioId}")
public ResponseEntity<Object> getOnUsuario(@PathVariable(value = "usuarioId")UUID
usuarioId){
Return
ResponseEntity.status(HttpStatus.OK).body(usuarioService.findById(usuarioId));
}
Conforme discutido anteriormente, o próximo passo após a criação do endpoint
getOnUsuario é estruturar a camada de serviço, implementando uma interface de
serviço e sua classe concreta.
Criar uma interface de serviço e sua implementação segue o princípio de design
modular e desacoplado, que é essencial para o desenvolvimento de sistemas
robustos, escaláveis e fáceis de manter. Essa abordagem garante maior organização
e promove a separação de responsabilidades entre as camadas da aplicação. P Para
evitar repetitividade, não detalharei novamente a organização modular nas próximas
implementações. Apenas desenvolveremos o código das interfaces e suas respectivas
implementações, mantendo a estrutura e boas práticas previamente discutidas.
Interface do service: findById
Optional<UsuarioModel> findById(UUID usuarioId);
Implementação da interface: findById
@Override
public Optional<UsuarioModel> findById(UUID usuarioId) {
Optional<UsuarioModel> usuarioModelOptional = usuarioRepository.findById(usuarioId);
return usuarioModelOptional;
}
Endpoint para o método deleteUsuario
@DeleteMapping("/{usuarioId}")
public ResponseEntity<Object> deleteUsuario(@PathVariable(value =
"usuarioId")UUID usuarioId){
usuarioService.deleteUsuarioId(usuarioService.findById(usuarioId).get());
return ResponseEntity.status(HttpStatus.OK).body("Usuario deletado com suceso!");
}
Interface do service: deleteUsuarioId
UsuarioModel deleteUsuarioId(UsuarioModel usuarioModel);
Implementação da interface: deleteUsuarioId
@Override
public UsuarioModel deleteUsuarioId(UsuarioModel usuarioModel) {
usuarioRepository.delete(usuarioModel);
return usuarioModel;
}
Endpoint para o método updateUsuario
@PutMapping("/{usuarioId}/usuario")
public ResponseEntity<Object> updateUsuario(@PathVariable(value =
"usuarioId")UUID usuarioId, @RequestBody @Valid UsuarioRecordDto
usuarioRecordDto){
Return
ResponseEntity.status(HttpStatus.OK).body(usuarioService.updateUsuario(usuarioSer
vice.findById(usuarioId).get(), usuarioRecordDto));
}
Interface do service: updateUsuario
UsuarioModel updateUsuario(UsuarioModel usuarioModel, UsuarioRecordDto
recordDtoUsuario);
Implementaçã da interface: updateUsuario
@Override
public UsuarioModel updateUsuario(UsuarioModel usuarioModel, UsuarioRecordDto
recordDtoUsuario) {
// Populando status, tipo de usuario data de criaçao e data de atualização
usuarioModel.setStatusUsuario(StatusUsuario.ATIVO);
usuarioModel.setTipoUsuario(TipoUsuario.USUARIO);
usuarioModel.setDataAtualizacao(LocalDateTime.now(ZoneId.of("America/Recife")));
usuarioModel.setNomeCompleto(recordDtoUsuario.nomeCompleto());
usuarioModel.setTelefoneCelular(recordDtoUsuario.telefoneCelular());
return usuarioRepository.save(usuarioModel);
}
Além do ID do usuário, que será fornecido na URL da requisição, o corpo da requisição
deverá conter os campos nomeCompleto e telefone. Durante o processamento, a
data de atualização do usuário será automaticamente ajustada para refletir o
momento da modificação.
Arquivo JSON
{
"nomeCompleto":"Maria",
"telefoneCelular":"8906296"
}
Endpoint para o método upadateImagem
@PutMapping("/{usuarioId}/imagem")
public ResponseEntity<Object> updateImagem(@PathVariable(value = "usuarioId")UUID
usuarioId, @RequestBody @Valid UsuarioRecordDto usuarioRecordDto){
return
ResponseEntity.status(HttpStatus.OK).body(usuarioService.updateImagem(usuarioService.fin
dById(usuarioId).get(), usuarioRecordDto));
}
Interface do service: updateImagem
UsuarioModel updateImagem(UsuarioModel usuarioModel, UsuarioRecordDto
usuarioRecordDto);
Implementação do service: updateImagem
@Override
public UsuarioModel updateImagem(UsuarioModel usuarioModel,
UsuarioRecordDto usuarioRecordDto) {
usuarioModel.setImagemUrl(usuarioRecordDto.imagemUrl());
usuarioModel.setDataAtualizacao(LocalDateTime.now(ZoneId.of("America/Recif
e")));
return usuarioRepository.save(usuarioModel);
}
Arquivo JSON
{
"imagemUrl":"aqui vai a URL"
Após o processamento da requisição, o sistema realizará a atualização dos dados do
usuário, atualizando a URL da imagem antiga pela nova e registrando os dados da
atualização no banco de dados.
Criação AutenticacaoUsuarioController
Vamos iniciar a implementação do endpoint de AutenticacaoUsuario, garantindo uma
abordagem bem estruturada e com boas práticas.
@RestController @RequestMapping("/autenticacao") public class
AutenticacaoUsuarioController {
final UsuarioService usuarioService;
/**
* Construtor da classe AutenticacaoUsuarioController.
*
* @param usuarioService - Serviço utilizado para gerenciar
operações relacionadas ao usuário.
*/
public AutenticacaoUsuarioController(UsuarioService usuarioService)
{
this.usuarioService = usuarioService;
}
/**
* Endpoint para registro de um novo usuário.
*
* Esse endpoint realiza a criação de um novo usuário, após a
verificação de
* nome, e-mail e senha. Se o nome, e-mail ou senha já existirem na
base de
* dados, será retornado um erro com código de status HTTP 409
(CONFLICT). Caso
* contrário, o usuário será registrado com sucesso e um código HTTP
201 (CREATED)
* será retornado.
*
* @param usuarioRecordDto - DTO que contém as informações do
usuário a ser registrado.
*
* @return ResponseEntity - Retorna a resposta da requisição com o
status apropriado:
* 409 (CONFLICT) caso já exista um usuário com o mesmo
nome, e-mail ou senha,
* ou 201 (CREATED) caso o usuário seja registrado com
sucesso.
*/
@PostMapping("/registro")
public ResponseEntity<Object> registroUsuario(@RequestBody
UsuarioRecordDto usuarioRecordDto) {
// Verifica se já existe um usuário com o mesmo nome
if (usuarioService.existByNome(usuarioRecordDto.nome())) {
return
ResponseEntity.status(HttpStatus.CONFLICT).body("ERRO, USUARIO JÁ
EXISTENTE!");
}
// Verifica se já existe um usuário com o mesmo e-mail
if (usuarioService.existByEmail(usuarioRecordDto.email())) {
return
ResponseEntity.status(HttpStatus.CONFLICT).body("ERRO, ESSE E-MAIL
JÁ CADASTRADO!");
}
// Verifica se já existe um usuário com a mesma senha
if (usuarioService.findBySenha(usuarioRecordDto.senha())) {
return
ResponseEntity.status(HttpStatus.CONFLICT).body("ERRO, ESTÁ SENHA JÁ
ESTÁ EM USO!");
}
// Caso todas as verificações sejam bem-sucedidas, registra o
novo usuário
return
ResponseEntity.status(HttpStatus.CREATED).body(usuarioService.saveUs
uario(usuarioRecordDto));
}
A interface UsuarioRepository será ampliada para incluir métodos de verificação
dos atributos nome, email e senha. O Spring Data JPA oferece um padrão de
nomenclatura para a criação de métodos de consulta com base nos atributos das
entidades mapeadas. Ao utilizarmos palavras-chave como existsBy combinadas com
o nome do atributo, podemos realizar verificações diretas na base de dados. Esses
métodos retornam um valor booleano: true se o registro for encontrado ou false caso
contrário.
public interface UsuarioRepository extends JpaRepository<UsuarioModel,
UUID> {
boolean existsByNome(String nome);
boolean existsByEmail(String email);
boolean existsBySenha(String senha);
Interface do service: saveUsuario
UsuarioModel saveUsuario(UsuarioRecordDto recordDtoUsuario);
boolean existByNome(String nome);
boolean existByEmail(String email);
boolean findBySenha(String senha);
Implementação do service: saveUsuario
@Override
public UsuarioModel saveUsuario(UsuarioRecordDto recordDtoUsuario) {
var usuarioModel = new UsuarioModel(); //Instanciando o UsuarioModel
BeanUtils.copyProperties(recordDtoUsuario, usuarioModel); // Convertendo de DTO
para Model
// Populando status, tipo de usuario data de criaçao e data de atualização
usuarioModel.setStatusUsuario(StatusUsuario.ATIVO);
usuarioModel.setTipoUsuario(TipoUsuario.USUARIO);
usuarioModel.setDataCriacao(LocalDateTime.now(ZoneId.of("America/Recife")));
usuarioModel.setDataAtualizacao(LocalDateTime.now(ZoneId.of("America/Recife")));
// Retornando os dados salvos
return usuarioRepository.save(usuarioModel);
}
O método saveUsuario realiza o processo de persistência de um novo usuário no banco de
dados de forma eficiente e estruturada. O fluxo de execução do método pode ser descrito em
detalhes da seguinte forma:
Instanciação do Modelo: Inicialmente, o método cria uma nova instância do
UsuarioModel, que é a representação da entidade que será persistida no banco de
dados. Este modelo contém os dados necessários para a criação de um usuário na
aplicação.
Conversão de DTO para Model: Utilizando a classe BeanUtils, o método faz a cópia
das propriedades do objeto UsuarioRecordDto (um Data Transfer Object - DTO) para
o UsuarioModel. Isso é feito através do método copyProperties, que mapeia
automaticamente as propriedades com o mesmo nome entre o DTO e o Model. Essa
conversão é fundamental para transferir os dados fornecidos pelo cliente (ou pela
camada de apresentação) para o modelo que será persistido.
População de Campos Adicionais: Após a conversão, o método preenche alguns
campos adicionais diretamente no UsuarioModel:
setStatusUsuario(StatusUsuario.ATIVO): O campo statusUsuario é atribuído
com o valor ATIVO, representando o status do usuário como ativo desde a criação.
setTipoUsuario(TipoUsuario.USUARIO): O campo tipoUsuario é preenchido
com o valor USUARIO, designando o tipo de usuário padrão para a nova instância.
setDataCriacao(LocalDateTime.now(ZoneId.of("America/Recife"))): A data
e hora atuais (no fuso horário "America/Recife") são atribuídas ao campo de data de
criação, garantindo que o timestamp correto seja registrado.
setDataAtualizacao(LocalDateTime.now(ZoneId.of("America/Recife"))):
Similar à data de criação, a data de atualização também é configurada com o
timestamp atual, assegurando que, ao criar o usuário, essa informação seja registrada.
Persistência no Banco de Dados: Com todos os campos preenchidos, o método
chama usuarioRepository.save(usuarioModel), que persiste a entidade
usuarioModel no banco de dados. Esse método é responsável por inserir ou atualizar
o registro no banco, dependendo se o usuário já existe ou não (geralmente identificado
por uma chave primária).
Retorno da Entidade Persistida: O método retorna o objeto usuarioModel que foi
salvo, já com os dados persistidos, incluindo quaisquer campos preenchidos
automaticamente pelo banco de dados (como o ID gerado, caso seja um campo
autoincrementado). Isso pode ser útil para enviar uma resposta ao cliente com os
dados atualizados da entidade.
// Implementação dos métodos de verificação de existencia na base de dados
@Override
public boolean existByNome(String nome) {
return usuarioRepository.existsByNome(nome);
}
@Override
public boolean existByEmail(String email) {
return usuarioRepository.existsByEmail(email);
}
@Override
public boolean findBySenha(String senha) {
return usuarioRepository.existsBySenha(senha);
}
Uso de DTOs na aplicação
Observe que alguns métodos no controller utilizam DTOs. Mas, o que é um DTO?
DTO, ou Data Transfer Object (Objeto de Transferência de Dados), é um padrão de design
utilizado para transferir dados de maneira eficiente e organizada entre diferentes partes de um
sistema ou até mesmo entre sistemas distintos.
O principal objetivo do uso de DTOs é encapsular e transportar apenas os dados necessários
para uma operação específica, evitando a exposição direta das entidades do domínio. Isso
oferece várias vantagens, como:
• Segurança: Protege informações sensíveis ao garantir que apenas os dados essenciais
sejam expostos.
• Performance: Reduz a quantidade de informações trafegadas, o que é especialmente
relevante em APIs e operações de rede.
• Customização: Permite adaptar os dados ao formato esperado pelo cliente ou pela
camada consumidora, sem impactar a estrutura interna das entidades.
Em resumo, os DTOs são essenciais para melhorar a organização e eficiência na troca de
informações dentro de sistemas ou APIs, promovendo boas práticas e alinhamento com os
princípios de design de software.
Usando Records
Os records são ideais para DTOs, pois são sintéticos, imutáveis e automaticamente geram os
métodos necessários (getters, equals, hashCode, e toString).
No Java 21, implementar DTOs é ainda mais eficiente devido a novos recursos da linguagem,
como records. Os records são perfeitos para DTOs, pois permitem criar classes imutáveis de
forma concisa e segura, reduzindo o esforço de implementação. Vou explicar como você pode
implementar DTOs no Java 21 usando diferentes abordagens, incluindo as mais modernas.
Criação do DTO com anotação @JsonView
Vamos criar um DTO utilizando records no Java e aplicar a anotação @JsonView. O objetivo é
trabalhar com diferentes visualizações para controlar quais campos do DTO serão serializados
e expostos em cada contexto da API.
A utilização de records simplifica a implementação de DTOs, fornecendo uma abordagem
imutável e concisa para manipular objetos de transferência de dados. A anotação @JsonView,
por sua vez, ajuda a gerenciar diferentes representações de um mesmo objeto, garantindo que
apenas os campos necessários sejam retornados de acordo com o cenário ou endpoint
específico.
public record UsuarioRecordDto(
@NotBlank(groups = UsuarioView.RegistroUsuarioPost.class, message = "O campo
nome é obrigatorio!")
@Size(groups = UsuarioView.RegistroUsuarioPost.class, min = 5, max = 50,
message = "O número máximo de caracteres é 50, e o mínimo é 5.")
@JsonView(UsuarioView.RegistroUsuarioPost.class)
String nome,
@NotBlank(groups = UsuarioView.RegistroUsuarioPost.class, message = "Campo
E-mail é obrigatorio")
@Email(groups = UsuarioView.RegistroUsuarioPost.class, message = "Formato de
E-mail invalido. Verifique o campo!")
@JsonView(UsuarioView.RegistroUsuarioPost.class)
String email,
@NotBlank(groups = {UsuarioView.RegistroUsuarioPost.class,
UsuarioView.SenhaPut.class}, message = "O campo senha é obrigatorio")
@Size(groups = {UsuarioView.RegistroUsuarioPost.class,
UsuarioView.SenhaPut.class}, min = 5, max = 20, message = "Informe a senha com no
minimo 5 caracteres e no maximo 20")
@JsonView({UsuarioView.RegistroUsuarioPost.class,
UsuarioView.SenhaPut.class})
String senha,
@NotBlank(groups = UsuarioView.SenhaPut.class, message = "Informe a senha
antiga")
@Size(groups = UsuarioView.SenhaPut.class, min = 5, max = 20, message =
"Informe a senha com no minimo 5 caracteres e no maximo 20")
@JsonView(UsuarioView.SenhaPut.class)
String senhaAntiga,
@NotBlank(groups = {UsuarioView.RegistroUsuarioPost.class,
UsuarioView.UsuarioPut.class}, message = "informe o nome completo")
@JsonView({UsuarioView.RegistroUsuarioPost.class,
UsuarioView.UsuarioPut.class})
String nomeCompleto,
@JsonView({UsuarioView.RegistroUsuarioPost.class,
UsuarioView.UsuarioPut.class})
String telefoneCelular,
@NotBlank(groups = UsuarioView.ImagemPut.class, message = "Informe a URL da
imagem")
@JsonView(UsuarioView.ImagemPut.class)
String imagemUrl){
// Interface usada para criar diferentes "visões" na aplicação.
public interface UsuarioView{
interface RegistroUsuarioPost{}
interface UsuarioPut {}
interface SenhaPut{}
interface ImagemPut{}
}
}
Depois que o DTO com suas visualizações for criado devemos implementar nos controllers a
anotação @JsonView() e @Validated() passando nos parametros o nível de visualização, que
será usado e validado.
Implementação de visualização por campo
A implementação de visualização por campo em uma API Spring Boot pode ser feita
utilizando a anotação @JsonView do Jackson. Com ela, podemos definir diferentes
visualizações para controlar quais campos de um objeto serão serializados (ou deserializados)
em diferentes cenários. Essa técnica é especialmente útil para APIs que precisam fornecer
respostas customizadas de acordo com o contexto ou nível de acesso.
Vantagens da Visualização por Campo
1. Customização de Respostas: Permite adaptar as respostas JSON conforme a
necessidade do cliente ou endpoint.
2. Segurança e Desempenho: Evita a exposição de dados sensíveis e reduz o volume de
dados transferidos em operações básicas.
3. Reutilização de DTOs: O mesmo DTO pode ser reutilizado em diferentes endpoints
com representações distintas.
Implementação no método getAllUsuario
@GetMapping
public ResponseEntity<Page<UsuarioModel>>
getAllUsuario(SpecificationsTemplate.UsuarioSpec spec, Pageable pageable){
Page<UsuarioModel> usuarioModelPage = usuarioService.findAll(spec, pageable);
if(!usuarioModelPage.isEmpty()){
for (UsuarioModel usuario: usuarioModelPage.toList()){
usuario.add(linkTo(methodOn(UsuarioController.class).getOnUsuario(usuario.g
etUsuarioId())).withSelfRel());
}
}
return ResponseEntity.status(HttpStatus.OK).body(usuarioModelPage);
}
Implementação do método updateUsuario
@PutMapping("/{usuarioId}/usuario")
public ResponseEntity<Object> updateUsuario(@PathVariable(value = "usuarioId")UUID
usuarioId, @RequestBody @Validated(UsuarioRecordDto.UsuarioView.UsuarioPut.class)
@JsonView(UsuarioRecordDto.UsuarioView.UsuarioPut.class) UsuarioRecordDto
usuarioRecordDto){
return
ResponseEntity.status(HttpStatus.OK).body(usuarioService.updateUsuario(usuarioService.fin
dById(usuarioId).get(), usuarioRecordDto));
}
Implementação do método updateSenha
@PutMapping("/{usuarioId}/senha")
public ResponseEntity<Object> updateSenha(@PathVariable(value = "usuarioId")UUID
usuarioId, @RequestBody
@Validated(UsuarioRecordDto.UsuarioView.SenhaPut.class)
@JsonView(UsuarioRecordDto.UsuarioView.SenhaPut.class) UsuarioRecordDto
usuarioRecordDto){
Optional<UsuarioModel> optionalUsuarioModel =
usuarioService.findById(usuarioId);
usuarioService.updateSenha(usuarioRecordDto, optionalUsuarioModel.get());
return ResponseEntity.status(HttpStatus.OK).body("Senha atualizada com
sucesso!");
Implementação do método updateImagem
@PutMapping("/{usuarioId}/imagem")
public ResponseEntity<Object> updateImagem(@PathVariable(value =
"usuarioId")UUID usuarioId, @RequestBody
@Validated(UsuarioRecordDto.UsuarioView.ImagemPut.class)
@JsonView(UsuarioRecordDto.UsuarioView.ImagemPut.class) UsuarioRecordDto
usuarioRecordDto){
Return ResponseEntity.status(HttpStatus.OK) .body(usuarioService
.updateImagem(usuarioService.findById(usuarioId).get(), usuarioRecordDto));
public AutenticacaoUsuarioController(UsuarioService usuarioService)
{
this.usuarioService = usuarioService;
}
Implementação do método registroUsuario
@PostMapping("/registro")
public ResponseEntity<Object> registroUsuario(@RequestBody
@Validated(UsuarioRecordDto.UsuarioView.RegistroUsuarioPost.class)
@JsonView(UsuarioRecordDto.UsuarioView.RegistroUsuarioPost.class)
UsuarioRecordDto usuarioRecordDto) {
/* O nome, o E-mail e a senha receberam a restrição UNIQUE no
mapeamento da ENTITY UsuarioModel,
isso significa que antes de salvar o registro na base de dados será
realizado uma verificação de
nome,E-mail e a senha
*/
if (usuarioService.existByNome(usuarioRecordDto.nome())) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("ERRO,
USUARIO JÁ EXISTENTE!");
}
if (usuarioService.existByEmail(usuarioRecordDto.email())) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("ERRO, ESSE
E-MAIL JÁ CADASTRADO!");
}
if (usuarioService.findBySenha(usuarioRecordDto.senha())) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("ERRO, ESTÁ
SENHA JÁ ESTÁ EM USO!");
}
return
ResponseEntity.status(HttpStatus.OK).body(usuarioService.saveUsuario(usuar
ioRecordDto));
}
Tratamento de exceções
Já estruturamos o projeto seguindo as melhores práticas. Criamos o projeto base,
adicionamos as dependências necessárias, configuramos o arquivo .yml, mapeamos a
entidade de usuários, implementamos o CRUD, e utilizamos DTOs com visualização
por campo para personalizar as respostas da API. Agora, é o momento de tratar as
exceções da aplicação de forma centralizada e eficiente.
O tratamento de exceções é uma parte fundamental para tornar a API mais robusta,
garantindo respostas consistentes, informativas e alinhadas com o padrão HTTP. Isso
melhora a experiência do cliente ao interagir com os endpoints e facilita a depuração e
manutenção do sistema.
Objetivo do Tratamento de Exceções
1. Uniformidade: Garantir que erros sejam tratados de forma consistente em toda
a aplicação.
2. Segurança: Evitar o vazamento de informações sensíveis.
3. Clareza: Fornecer mensagens claras e apropriadas aos consumidores da API.
4. Padrões de Resposta: Utilizar códigos de status HTTP adequados (e.g., 400,
404, 500).
ErroRecordResponse
No módulo exceptions, será criada uma classe chamada ErroRecordResponse. Essa
classe será um record no Java para representar de forma imutável e eficiente as
informações de erro que serão retornadas pela API em caso de exceções. Utilizando
records, conseguimos reduzir a quantidade de código boilerplate, tornando o design
mais limpo e objetivo.
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ErroRecordResponse(int codigoErro,
String mensagemErro,
Map<String, String> detalhesErro
){}
NotFoundException
Criaremos tabém a classe NotFoundException que é uma exceção personalizada
que estende a classe RuntimeException. Ela é usada para representar cenários onde
um recurso solicitado na aplicação não foi encontrado, como quando um ID de usuário
ou produto não existe no banco de dados.
public class NotFoundException extends RuntimeException{
// Construtor da class NotFoundException que retorna uma message
public NotFoundException(String message) {
super(message);
}
}
handleNotFoundException
O método handleNotFoundException no contexto do GlobalExceptionHandler serve para
tratar, de forma centralizada e padronizada, as exceções do tipo NotFoundException
lançadas na aplicação.
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* Trata exceções do tipo NotFoundException.
*
* @param exception Exceção personalizada indicando que um recurso não foi encontrado.
* @return ResponseEntity com o status HTTP 404 (Not Found) e detalhes do erro.
*/
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ErroRecordResponse> handleNotFoundException(NotFoundException
exception) {
// Criação do objeto de resposta para erro
var erroRecordResponse = new ErroRecordResponse(
HttpStatus.NOT_FOUND.value(),
exception.getMessage(),
null // Descrição adicional opcional
);
// Retorno da resposta com status HTTP 404
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(erroRecordResponse);
}
/**
* Trata exceções de validação geradas pelo Spring quando um DTO falha na validação.
*
* @param exception Exceção do tipo MethodArgumentNotValidException gerada por falhas na
validação.
* @return ResponseEntity com o status HTTP 400 (Bad Request) e detalhes do erro.
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErroRecordResponse>
handleValidationException(MethodArgumentNotValidException exception) {
Map<String, String> erros = new HashMap<>();
// Percorre todos os erros de validação e os adiciona ao mapa de erros
exception.getBindingResult().getAllErrors().forEach(error -> {
String nomeCampo = ((FieldError) error).getField(); // campo erro
String mensagemErro = error.getDefaultMessage(); // Mensagem de erro
erros.put(nomeCampo, mensagemErro); // Adiciona no mapa
});
// Criação do objeto de resposta para erro de validação
var erroRecordResponse = new ErroRecordResponse(
HttpStatus.BAD_REQUEST.value(),
"Erro de Validação",
erros // Lista de erros detalhados
);
// Retorno da resposta com status HTTP 400
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(erroRecordResponse);
}
}
Validações customizadas para uso de senhas
Para realizar validações mais específicas relacionadas ao uso de senhas, iremos
implementar uma anotação personalizada. Essa anotação será aplicada diretamente
no DTO, permitindo que as regras de validação sejam centralizadas e reutilizáveis. Essa
abordagem melhora a consistência e organiza a lógica de validação, garantindo que as
senhas atendam aos critérios necessários.
Será adicionada a anotação personalizada @SenhaConstraint nos campos senha e
senhaAntiga do DTO. Essa anotação permitirá validar os dois campos de forma
específica, garantindo que as regras de negócio aplicadas às senhas sejam
respeitadas, como a força, formato ou qualquer outro requisito definido. Essa
abordagem centraliza e simplifica as validações, deixando o código mais organizado e
fácil de manter.
@SenhaConstraint(groups = {UsuarioView.RegistroUsuarioPost.class,
UsuarioView.SenhaPut.class})
String senha,
@SenhaConstraint(groups = UsuarioView.SenhaPut.class)
String senhaAntiga,
O módulo validates será criado para centralizar e organizar todas as validações
personalizadas do projeto. Este módulo terá as seguintes responsabilidades:
1. Definição de anotações customizadas: Criar anotações específicas para
validações personalizadas, alinhadas às regras de negócio e necessidades do
domínio da aplicação.
2. Implementação das validações: Fornecer classes que contenham a lógica
necessária para processar as regras associadas às anotações, garantindo que
as validações sejam aplicadas de forma consistente e eficiente.
Essa abordagem promove maior organização, reutilização de código e facilita a
manutenção e evolução do sistema.
Anotação SenhaConstraint
@Documented // anotação do java doc
@Constraint(validatedBy = SenhaConstraintImpl.class) // Indica qual é a classe que
implementara as regras
@Target({ElementType.METHOD, ElementType.FIELD}) // indica que a anotação vai
servir a nivel de método e de campo.
@Retention(RetentionPolicy.RUNTIME) // A validação vai ocorre em tempo de
execução.
public @interface SenhaConstraint {
// Mensagem padrão de erro.
String message() default """
A senha deve conter entre 5 e 20 caracteres,
a senha deve incluir pelo menos uma letra maiúscula (A-Z),
a senha deve incluir pelo menos uma letra minúscula (a-z),
a senha deve conter pelo menos um número (0-9),
a senha deve incluir pelo menos um caractere especial dentre os seguintes: @, $,
!, %, *, ?, &.
""";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
SenhaConstraintImpl
A classe SenhaConstraintImpl é a implementação da lógica de validação para a anotação
personalizada @SenhaConstraint. Essa classe contém o comportamento necessário para
verificar se o valor de um campo anotado atende aos critérios definidos para senhas na
aplicação.
Principais características:
1. Interface Validator: A classe implementa a interface ConstraintValidator,
fornecida pelo Bean Validation, que permite associar a lógica de validação a uma
anotação específica.
2. Método isValid: Esse método é onde a lógica de validação é definida. Ele verifica se a
senha atende aos requisitos especificados, como comprimento mínimo, presença de
caracteres especiais, letras maiúsculas, números, entre outros.
3. Injeção de dependências (opcional): Caso a validação dependa de serviços ou
configurações externas, esses recursos podem ser injetados para enriquecer a lógica.
4. Mensagem de erro personalizada: Caso o valor não seja válido, uma mensagem de
erro definida na anotação ou no arquivo de mensagens será exibida, informando ao
usuário qual regra não foi atendida.
public class SenhaConstraintImpl implements ConstraintValidator<SenhaConstraint, String> {
// constraint: padrão de verificação de senha
private static final String SENHA_PATTER = "^(?=.[0-9])(?=.[a-z])(?=.[A-Z])(?=.[!@#&()–
[{}]:;',?/*~$^+=<>]).{5,20}$";
// Inicialização dos métodos defaul (initialize, e isValid)
@Override
public void initialize(SenhaConstraint constraintAnnotation) {
ConstraintValidator.super.initialize(constraintAnnotation);
}
@Override
public boolean isValid(String senha, ConstraintValidatorContext context) {
// Uso do padrão PATTERN
Pattern pattern = Pattern.compile(SENHA_PATTER);
// Verificação se a senha é: nula, ou vazia ou com espassos
if (senha == null || senha.trim().isEmpty() || senha.contains(" ")){
return false;
} else if (!pattern.matcher(senha).matches()){
return false;
}
return true;
}
}
Essa implementação permite aplicar a validação de senhas de forma reutilizável e
centralizada, reduzindo a duplicação de código e facilitando ajustes futuros nos critérios de
validação.
Paginação
Implementaremos a configuração de paginação em nossa aplicação Spring Boot para
gerenciar grandes volumes de dados retornados pela API, dividindo-os em partes menores e
mais fáceis de consumir. Isso não só otimiza a performance da aplicação, ao reduzir o
processamento e o uso de memória, mas também proporciona uma melhor experiência para o
usuário, garantindo que ele acesse os dados de maneira mais rápida e eficiente, sem
sobrecarregar o sistema ou a interface.
Será criado um módulo de configuração no projeto, no qual as definições de paginação serão
centralizadas e configuradas a nível global. Isso garantirá que as configurações de paginação,
como tamanho padrão de página, ordenação e outras preferências, sejam aplicadas de
maneira consistente em toda a aplicação. Com essa abordagem, o código se torna mais
modular e fácil de manter, além de permitir ajustes rápidos nas configurações de paginação
sem a necessidade de alterações em cada controlador ou serviço individualmente.
Formas de implementar paginação
1. Paginação a nível de método:
• A paginação é aplicada diretamente nos métodos do controlador. Em cada endpoint
que lida com a listagem de dados, você especifica os parâmetros de paginação (como
page e size) na assinatura do método.
• Essa abordagem oferece flexibilidade, pois você pode definir a paginação de forma
específica para cada operação ou tipo de dado.
2. Paginação a nível de classe:
• A configuração de paginação é definida de maneira centralizada na classe, geralmente
em um nível mais global (como no caso de uma configuração do Spring).
• Essa abordagem é útil quando você deseja aplicar uma configuração de paginação
consistente para todos os endpoints que envolvem listagens, garantindo que a lógica
de paginação seja reutilizada de forma uniforme em toda a aplicação.
ATENÇÃO: Ao decidir entre essas abordagens, é importante considerar o equilíbrio entre
flexibilidade e simplicidade. A paginação a nível de classe pode ser mais eficiente para APIs
grandes, enquanto a paginação a nível de método pode ser mais indicada quando há requisitos
específicos para cada endpoint.
@Configuration
public class ResolverConfig implements WebMvcConfigurer{
// Configuração de paginação a nivel de classe.
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver>
argumentResolvers){
var pageableResolver = new
PageableHandlerMethodArgumentResolver();
// Inicia na página 0, exibe até 10 elementos por página e ordena
de forma crescente
pageableResolver.setFallbackPageable(PageRequest.of(0, 10));
argumentResolvers.add(pageableResolver);
}
CORS na aplicação
CORS (Cross-Origin Resource Sharing) é um mecanismo de segurança que permite ou
restringe recursos da web solicitados por páginas de um domínio diferente daquele
que hospeda o recurso. Quando você está criando uma API em Java, especialmente
com Spring Boot, pode ser necessário configurar o CORS para garantir que sua API
possa ser acessada de diferentes origens (domínios, protocolos ou portas),
especialmente quando a API é consumida por um frontend hospedado em um domínio
diferente.
Por padrão, o CORS é restritivo e não permite requisições de origens diferentes. O
Spring Boot permite configurar facilmente o CORS para permitir o acesso a partir de
origens específicas.
Configuração CORS por controlador
Se você deseja configurar CORS de maneira mais granular, pode aplicar as
configurações diretamente nos controladores (ou métodos específicos).
Implementação a nivel de classe
@RestController
@CrossOrigin(origins = "http://localhost:3000", allowedHeaders = "Authorization")
public class MeuControlador {
@GetMapping("/dados")
public String getDados() {
return "Dados acessados com CORS";
}
}
Implementação a nivel global
Você pode definir uma configuração global para CORS em toda a aplicação, criando
uma classe de configuração e utilizando o WebMvcConfigurer para personalizar as
políticas:
@Override
public void addCorsMappings(CorsRegistry corsRegistry){
corsRegistry.addMapping("/**").maxAge(3600) // Todos os controllers teram acesso
independente da origem
}
Hipermídias com Spring Hateoas
O Spring HATEOAS (Hypermedia as the Engine of Application State) é uma
especificação que faz parte do conceito de RESTful APIs, onde a navegação da
aplicação é feita por meio de links fornecidos pela própria resposta da API. HATEOAS
permite que as APIs sejam mais dinâmicas e autoexplicativas, fornecendo aos
consumidores da API links que indicam quais recursos ou operações estão disponíveis
a partir do estado atual da aplicação.
O que são HATEOAS?
HATEOAS é uma parte da arquitetura REST que permite que os recursos da API
ofereçam links que podem ser seguidos pelo cliente para realizar operações
adicionais. Em vez de fornecer apenas dados, um servidor RESTful também fornece
informações sobre como navegar entre os recursos, melhorando a flexibilidade da API.
O Spring HATEOAS é uma biblioteca que facilita a implementação de HATEOAS em
uma aplicação Spring. Ela fornece suporte para a construção de respostas que
incluem links, além de permitir a navegação entre os recursos usando esses links.
Para trabalharmos com hipermídias (hyperlinks) e recursos no estilo HATEOAS em uma
aplicação Spring Boot, devemos incluir a dependência spring-boot-starter-hateoas no arquivo
pom.xml. Essa dependência fornece os componentes necessários para adicionar links a
respostas da API, permitindo a navegação entre os recursos de forma dinâmica.
Além disso, após adicionar a dependência, é importante atualizar as dependências para
garantir que tudo seja baixado corretamente e esteja disponível no projeto.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
Implemetação do uso de Hateoas na entidade:
@JsonInclude(JsonInclude.Include.NON_NULL)
@Entity
@Table(name = "TB_USUARIO")
public class UsuarioModel extends RepresentationModel<UsuarioModel> implements
Serializable {
@Serial
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID usuarioId;
@Column(nullable = false, unique = true, length = 150)
private String nome;
}
Implementar Heteoas no controller
Para implementar o HATEOAS (Hypermedia as the Engine of Application State) no controller, é
uma boa prática verificar se a paginação está presente e, caso positivo, iterar sobre os
registros para adicionar links a cada recurso. O método getOneUsuario será usado para criar o
link que aponta para o recurso específico. Aqui está uma abordagem melhorada e organizada.
@GetMapping
public ResponseEntity <Page<UsuarioModel>>
getAllUsuarios(SpecificationsTemplate.UsuarioSpec spec, Pageable pageable){
Page<UsuarioModel> usuarioModelPageSpec = usuarioService.findAll(spec,
pageable);
// Se a paginação não for vazia, será exibido para cada registro um link.
if (!usuarioModelPageSpec.isEmpty()){
for (UsuarioModel usuario: usuarioModelPageSpec.toList()){
usuario.add(linkTo(methodOn(UsuarioController.class).getOnUsuario(usuario.getUsu
arioId())).withSelfRel());
}
}
return ResponseEntity.status(HttpStatus.OK).body(usuarioModelPageSpec);
}
Configuração de serialização de paginação a nivel
global
Para configurar a serialização de paginação a nível global em uma aplicação Spring
Boot, você pode personalizar a forma como a resposta de paginação é estruturada e
serializada. Isso é útil para garantir que todas as respostas paginadas sejam
consistentes em termos de formato e conteúdo, sem precisar configurar cada
controlador individualmente.
A configuração de serialização de paginação a nível global, será realizada na classe de
configuração da aplicação (ResolverConfigs), e será acrescida a anotação:
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
@Configuration
@EnableSpringDataWebSupport(pageSerializationMode = VIA_DTO)
// Configuração de serialização de paginação a nivel global
public class ResolverConfigs implements WebMvcConfigurer{
}
A configuração de serialização de paginação a nivel global é uma sugestão do propio
Spring Boot.
Padrão de data global com ISO 8601 UTC
Para implementar um padrão de data global no seu projeto Spring Boot, você pode criar
uma classe chamada DateConfig no módulo de configurações. Esta classe será
responsável por configurar a maneira como as datas são manipuladas e serializadas
em todo o sistema. Isso é útil para garantir que todas as datas sigam o mesmo formato
e padrão, evitando inconsistências e facilitando a manutenção.
@Configuration
public class DateConfig {
// Configuração de data padrão ISO 8601 UTC, configuração global.
public static final String DATETIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
@Bean
public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
return builder -> {
builder.serializers(new
LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATETIME_FORMAT)));
};
}
Após a configuração, todas as datas nas entidades serão automaticamente
serializadas no formato configurado quando forem enviadas na resposta JSON. Por
exemplo, se você tiver uma entidade com um campo Date, ele será automaticamente
serializado de acordo com o formato de data configurado.
O padrão de data também pode ser configurado a nível de atributo, o que significa que
você pode aplicar a formatação de data diretamente nas propriedades das suas
entidades ou DTOs (Data Transfer Objects) sem precisar configurar globalmente para
toda a aplicação.
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyy HH:mm:ss")
private LocalDateTime dataCriacao;
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyy HH:mm:ss")
private LocalDateTime dataAtualizacao;
Filtros avançados e dinâmicos
Filtros avançados e dinâmicos em uma API são mecanismos que permitem pesquisar
e filtrar dados de forma flexível e personalizada, com base nas interações fornecidas
pelo cliente na requisição. Esse recurso é essencial para oferecer uma experiência
mais rica ao consumir APIs, permitindo consultas otimizadas e específicas sem a
necessidade de várias implementações de endpoints para cada cenário.
Para configurar filtros avançados e dinâmicos em uma API Spring Boot, você pode
implementar um mecanismo que interpreta os parâmetros de requisição HTTP e os
converte em critérios de busca no banco de dados. Isso pode ser feito utilizando as
funcionalidades do Spring Data JPA combinadas com classes utilitárias para construir
consultas dinâmicas.
Para implementar filtros sonoros usando Especificações no Spring Data JPA, o
primeiro passo é configurar o projeto e adicionar a dependência necessária para
habilitar a funcionalidade de Especificações . Isso permite construir consultas
dinâmicas baseadas em critérios definidos pelo cliente.
<dependency>
<groupId>net.kaczmarzyk</groupId>
<artifactId>specification-arg-resolver</artifactId>
<version>${specification.version}</version>
</dependency>
Em properties não se esqueça de configurar a verção do specification:
<properties>
<java.version>21</java.version>
<specification.version>3.1.0</specification.version>
</properties>
Se você estiver usando Maven , certifique-se de incluir uma dependência para o Spring
Data JPA no arquivo pom.xml. O Spring já oferece suporte nativo às Especificações
através do módulo JPA.
Com as dependências definidas no projeto, o próximo passo é criar o mapeamento dos
filtros, definindo os campos que serão usados para realizar as consultas dinâmicas.
Vamos utilizar uma classe UsuarioModel previamente mapeada como entidade no
projeto.
Mapeamento dos filtros dinâmicos
Para implementar filtros dinâmicos de forma bem estruturada e organizada, é uma boa
prática centralizar a lógica relacionada às Specifications em um módulo dedicado.
Isso facilita a manutenção, a reutilização do código e segue o Princípio de
Responsabilidade Única (SRP). Abaixo, apresento uma abordagem refinada:
Vamos criar uma interface para centralizar as especificações dinâmicas com a ajuda
de anotações parece utilizar o framework Spring Data JPA Specification com Spring
Data REST Helpers como o Spec e And, que fazem parte de bibliotecas externas como
o spring-data-jpa-datatables ou specification-arg-resolver.
Uso do JpaSpecificationExecutor<> no repository
public class SpecificationsTemplate {
@And({
@Spec(path = "tipoUsuario", spec = Equal.class),
@Spec(path = "statusUsuario", spec = Equal.class),
@Spec(path = "email", spec = Like.class),
@Spec(path = "nome", spec = Like.class),
@Spec(path = "nomeCompleto", spec = LikeIgnoreCase.class)
})
public interface UsuarioSpec extends Specification<UsuarioModel>{}
}
Com o mapeamento dos filtros configurado, precisamos informar ao repositório que
utilizaremos Specifications. Para isso, o repositório deve herdar a interface
JpaSpecificationExecutor<>, além da interface JpaRepository<>.
Aqui surge uma questão interessante: no Java, não é permitido herança múltipla em
classes, mas no caso de interfaces, isso é perfeitamente possível. Isso acontece
porque o Java possui uma particularidade: enquanto classes podem herdar de apenas
uma outra classe (para evitar conflitos de implementação), interfaces podem ser
combinadas livremente, permitindo que uma interface herde várias outras interfaces.
Herança em Classes vs Interfaces:
• Classes: Permitem herança única porque herdar de múltiplas classes poderia
causar conflitos na implementação dos métodos (conhecido como o
"problema do diamante").
• Interfaces: São essencialmente contratos, sem implementação obrigatória (até
o Java 8). Mesmo com os métodos default introduzidos no Java 8, o compilador
consegue lidar com conflitos de forma clara e explícita.
Embora o Java não permita herança múltipla em classes, ele suporta herança múltipla
em interfaces, permitindo combinar diversas funcionalidades em uma única interface.
No caso do Spring Data JPA, isso nos permite criar repositórios que aproveitam o
melhor de ambas as interfaces — o suporte completo a operações de CRUD e a
flexibilidade de consultas dinâmicas baseadas em Specifications. Essa característica
torna os repositórios no Spring Data JPA extremamente poderosos e fáceis de usar.
O que é o JpaSpecificationExecutor?
A interface JpaSpecificationExecutoroferece métodos para consultas dinâmicas
utilizando Especificações , que são opções flexíveis e personalizáveis para criar
consultas no banco de dados de acordo com os parâmetros fornecidos em tempo de
execução. Quando o repositório estende essa interface, ele herda métodos como o
findAll(Specification<T> spec, Pageable pageable), que permite combinar
filtros dinâmicos e paginação em uma única consulta.
public interface UsuarioRepository extends JpaRepository<UsuarioModel,UUID>,
JpaSpecificationExecutor<UsuarioModel> {
boolean existsByNome(String nome);
boolean existsByEmail(String email);
boolean existsBySenha(String senha);
}
Adicionar configuração para filtros específicos
Agora que adicionamos a dependência da Especificação no pom.xml, mapeamos os filtros e
implementamos a Especificação no repositório, o próximo passo é configurar a classe
ResolverConfig para habilitar o uso das Especificações de maneira dinâmica. Isso permitirá
que as consultas utilizem os filtros de forma automática, tornando-os acessíveis para todas as
consultas que requerem lógica de Especificação no Spring.
A configuração envolve o uso de um SpecificationArgumentResolver, que interceptará as
configurações de consulta e as transformará em Especificações. Isso automatiza o processo
de construção da consulta dinâmica com base nos parâmetros passados na requisição.
@Configuration
public class ResolverConfig implements WebMvcConfigurer{
// Configuração de paginação a nivel de classe.
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver>
argumentResolvers){
// Uso da dependência do Specification.
argumentResolvers.add(new SpecificationArgumentResolver());
var pageableResolver = new PageableHandlerMethodArgumentResolver();
// Inicia na página 0, exiba até 10 elementos por página e ordena de forma
crescente.
pageableResolver.setFallbackPageable(PageRequest.of(0, 10,
Sort.by(Sort.Direction.ASC,"usuarioId")));
argumentResolvers.add(pageableResolver);
}
@Override
public Page<UsuarioModel> findAll(Specification<UsuarioModel> spec, Pageable
pageable){
return usuarioRepository.findAll(spec, pageable);
}
Interface do service
Agora que configuramos a paginação e os filtros dinâmicos usando Especificações , o
próximo passo é integrar esses dois aspectos de forma eficiente. Além de garantir que a
paginação seja aplicada às consultas, também precisamos passar a Especificação para o
serviço de forma que ele utilizará na construção da consulta no repositório.
Page<UsuarioModel> findAll(Specification<UsuarioModel> spec, Pageable
pageable);
Implementação do service
Ao estender a interface JpaSpecificationExecutor no repositório, podemos utilizar o
método findAll(Specification<T>, Pageable) para realizar buscas dinâmicas com
paginação de forma simples e eficiente.
@Override
public Page<UsuarioModel> findAll(Specification<UsuarioModel> spec, Pageable
pageable){
return usuarioRepository.findAll(spec, pageable);
}
Ao estender a interface JpaSpecificationExecutor no repositório, podemos utilizar o
método findAll(Specification<T>, Pageable) para realizar buscas dinâmicas com
paginação de forma simples e eficiente.
@Override
public Page<UsuarioModel> findAll(Specification<UsuarioModel> spec, Pageable
pageable){
return usuarioRepository.findAll(spec, pageable);
}
O método findAll(Specification<T> spec, Pageable pageable)oferece uma maneira
prática de realizar a busca utilizando a Especificação , que define uma lógica de filtragem, e o
Pageable , que gerencia a paginação. Essa combinação permite consultas altamente flexíveis
e eficientes, especialmente em cenários onde a quantidade de dados é grande.
Implementando o Specification no controller
Por fim, para completar o fluxo de consulta dinâmica com filtros e paginação, precisamos
implementar uma lógica no Controller. Ao implementar a Especificação no Controlador,
conseguimos construir consultas dinâmicas de forma flexível e eficiente, permitindo que o
cliente envie filtros para refinar a busca. O Controlador se carrega de montar a Especificação e
passar as informações de paginação e filtros para o Serviço , que realiza a consulta no
repositório.
Essa abordagem facilita o gerenciamento de consultas complexas e melhora o desempenho
da aplicação, já que podemos realizar buscas paginadas e filtradas sem sobrecarregar a
memória ou a rede.
@GetMapping
public ResponseEntity<Page<UsuarioModel>>
getAllUsuario(SpecificationsTemplate.UsuarioSpec spec, Pageable pageable){
Page<UsuarioModel> usuarioModelPage = usuarioService.findAll(spec, pageable);
if(!usuarioModelPage.isEmpty()){
for (UsuarioModel usuario: usuarioModelPage.toList()){
usuario.add(linkTo(methodOn(UsuarioController.class).getOnUsuario(usuario.getUsu
arioId())).withSelfRel());
}
}
return ResponseEntity.status(HttpStatus.OK).body(usuarioModelPage);
}
Como testar?
Para testar a melhoria de filtros dinâmicos e paginação com Especificações no Postman,
você pode realizar as seguintes etapas. O Postman é uma ferramenta poderosa para testar
APIs RESTful e permite que você envie requisições HTTP e verifique os resultados.
Certifique-se de que a aplicação está em execução: Antes de interagir com a API usando o
Postman, é essencial garantir que a aplicação Spring Boot esteja em execução. Para isso, se
você estiver usando a IDE IntelliJ ou o comando spring-boot:run, verifique se o servidor da
aplicação foi iniciado corretamente. No IntelliJ, você deve ver uma mensagem no console
indicando que o servidor está rodando na porta configurada (geralmente 8080 por padrão).
Definindo o Endpoint da API no Postman: No Postman, configure a requisição HTTP GET para
o endpoint da sua API que lida com a busca de usuários. Este endpoint deve ser responsável
por buscar usuários no banco de dados, aplicando filtros e possivelmente lidando com
paginação.
URL: Certifique-se de inserir a URL correta. Parâmetros de consulta (query parameters):
Para filtrar os resultados ou aplicar paginação, você pode passar parâmetros como
?page=1&size=10 (para paginação), ou filtros como ?nome=João&idade=30 para refinar a
busca.
Cabeçalhos: Se a sua API exigir autenticação, como um token JWT, adicione um cabeçalho
com o token de autenticação.
Testando a Requisição no Postman: Após configurar o Postman, clique em "Send" para enviar
a requisição. O Postman exibirá a resposta da API. Se a aplicação estiver funcionando
corretamente, você verá os dados retornados conforme configurado no seu controlador e
serviço, com os filtros e paginação aplicados conforme esperado.
Documentando a API
O SpringDoc é uma biblioteca que integra o Swagger com aplicações Spring Boot, facilitando a
geração de documentação interativa da API de maneira automática. Ele usa as anotações do
Spring, como @RestController, @RequestMapping, @GetMapping, entre outras, para criar
uma documentação detalhada e interativa da API REST.
Documentação automática: O SpringDoc gera automaticamente a documentação da sua API
com base nas anotações presentes no seu código, sem necessidade de configuração manual
detalhada. Ele usa o OpenAPI Specification (OAS) para definir os detalhes da API, como
endpoints, parâmetros, tipos de resposta, etc.
Customização: Embora a configuração seja automática, o SpringDoc permite personalizar a
documentação usando anotações específicas, como @OpenAPIDefinition e @Info, para
definir o título, descrição, versão da API e outros detalhes.
Após adicionar o SpringDoc à sua aplicação Spring Boot, ele examina automaticamente os
controladores REST (@RestController e @RequestMapping, por exemplo) para gerar a
documentação. A interface Swagger UI gerada permite visualizar todos os endpoints e realizar
testes interativos diretamente pela web.
Vantagens do SpringDoc
• Facilidade de uso: Não é necessário criar ou manter documentação manualmente, o
que reduz a carga de trabalho e melhora a consistência.
• Acessibilidade: A interface Swagger UI torna a documentação acessível e interativa,
permitindo que desenvolvedores e integradores testem os endpoints diretamente no
navegador.
• Suporte ao OpenAPI: O SpringDoc segue o padrão OpenAPI 3.0, que é amplamente
utilizado na indústria e suporta uma documentação mais detalhada e precisa da API.
Para utilizar o SpringDoc em sua aplicação, é necessário adicionar a dependência no seu
projeto:
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
Configuração do arquivo .yaml
springdoc:
api-docs:
path: /v3/api-docs # Especifica o caminho onde a documentação em formato JSON
será exposta
swagger-ui:
path: /swagger-ui/index.html # Define o caminho para acessar a interface do
Swagger UI
Configuração do SpringDoc
Para um projeto simples ou monolítico, colocar a configuração do SpringDoc dentro do
módulo conf é uma boa escolha, pois mantém a estrutura mais simples e organizada. Para
projetos mais complexos ou quando você está lidando com microserviços ou precisa de
modularização, é recomendável colocar o SpringDoc em um módulo separado dedicado à
documentação. Isso facilita a reutilização e manutenção em larga escala.
Optaremos por manter a documentação da API em um módulo separado para garantir uma
melhor organização e modularização do projeto. Isso facilita a manutenção, permite uma
separação clara de responsabilidades e mantém o código da aplicação principal mais limpo e
organizado.
Vantagens de separar a documentação em um módulo:
1. Modularidade: A documentação ficará isolada em seu próprio módulo, permitindo que
alterações ou melhorias sejam feitas sem impacto direto na lógica principal da
aplicação.
2. Manutenção simplificada: Facilita a manutenção da documentação e torna mais claro
onde configurar aspectos relacionados ao Swagger/SpringDoc.
3. Desativação em diferentes ambientes: Com um módulo separado, é mais fácil
desativar a documentação em ambientes como produção, onde ela pode não ser
necessária.
4. Escalabilidade: Essa abordagem é especialmente útil em projetos maiores ou com
múltiplos módulos, onde a documentação precisa ser organizada separadamente para
cada módulo da API.
@Configuration
public class SpringDoc {
@Bean
public OpenAPI customOpenAPI() {
// http://localhost:8087/sistema-vendas-usuario/swagger-ui/swagger-ui/index.html
return new OpenAPI()
.info(new Info()
.title("Siatema de vendas Online, área do usuário.") // Titulo da API
.description("Microserviço: API RESTful para o sistema de vendas online de
Usuários\n" +
"Esta aplicação é dedicada ao gerenciamento de controle de acessos e autenticação para a
API da sistema de vendas online. Seu objetivo é centralizar e organizar as operações
relacionadas à autenticação, autorização e gerenciamento de usuários, garantindo segurança
e eficiência no acesso aos recursos da plataforma.")
.contact(new Contact()
.name("Time Backend")
.email("backend@sistema_vendas.com"))
.license(new License()
.name("Apache 2.0")
.url("http://sistema_vendas.com/api/licenca")));
}
}
Logs na aplicação com log4J2
Implementar logs em aplicações com Spring Boot é uma prática indispensável para
assegurar a manutenção, monitoramento e depuração proativa do sistema. Um
sistema de logs bem estruturado permite identificar problemas com agilidade,
monitorar o comportamento dos serviços em diferentes ambientes e obter insights
valiosos para otimizar a aplicação.
O objetivo deste tópico não é desenvolver uma aplicação complexa com
funcionalidades avançadas de logging, mas destacar a importância dos logs em uma
aplicação e demonstrar como implementá-los de forma eficiente. A implementação de
logs é um passo fundamental para aumentar a confiabilidade do sistema e fornecer
suporte adequado para o diagnóstico de erros e análises operacionais.
Níveis de logs
Os níveis de logs são utilizados para categorizar as mensagens registradas, ajudando
a diferenciar a importância e o propósito de cada registro. Eles são fundamentais para
controlar a quantidade de informações geradas pelos logs e para identificar
rapidamente problemas em um sistema. Ao utilizar os níveis de logs corretamente, é
possível manter os registros organizados, reduzindo a quantidade de informações
irrelevantes e tornando os logs mais úteis para diagnóstico e análise.
TRACE: Informações detalhadas para depuração, geralmente não utilizadas em
produção.
DEBUG: Detalhes úteis para desenvolvedores durante o desenvolvimento.
INFO: Informações gerais sobre o fluxo normal do sistema.
WARN: Avisos sobre situações inesperadas que não interrompem o funcionamento.
ERROR: Mensagens relacionadas a falhas ou erros críticos.
Implementando Logs
Para implementar logs na aplicação, podemos substituir o Logback (configuração
padrão do Spring Boot) pelo Log4j2, uma poderosa biblioteca de logging que oferece
alta performance, suporte avançado a configurações e recursos como logs
assíncronos. O Spring Boot utiliza o Logback como a implementação de logging
padrão. Para substituí-lo, é necessário excluí-lo explicitamente do projeto.
<!--starter-web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--Exclusão do logback-spring-->
<exclusion>
<groupId>org.springframework.boot</groupId
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--log4j2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
É importante posicionar a dependência do spring-boot-starter-web como a primeira no arquivo
pom.xml. Isso ajuda a evitar conflitos de dependências, especialmente em projetos que
utilizam outras bibliotecas ou starters com implementações específicas de logging. A ordem
das dependências no pom.xml pode influenciar a resolução e o comportamento das mesmas,
garantindo que as configurações funcionem conforme esperado.
Configuração dos logs no arquivo .ymal
A configuração de logs no arquivo .yml (ou .yaml) para uma aplicação Spring Boot pode ser
realizada definindo parâmetros no application.yml. Isso permite ajustar o nível de log, formatar
as saídas e configurar os pacotes que devem ser monitorados. O arquivo application.yml
permite personalizar os logs de forma simples e centralizada. Essas configurações são úteis
tanto para desenvolvimento quanto para produção, oferecendo controle sobre o
comportamento e o armazenamento de logs da aplicação.
# Configuração de logs
logging:
level:
plataforma_ead: TRACE
root: INFO
org.springframework.web: DEBUG
org.hibernate: INFO
Aplicando logs no controller
Se estiver utilizando a dependência do Lombok: utilize a anotação @Log4j2
diretamente na classe para habilitar o logger, evitando a necessidade de criar uma
instância manualmente. Caso não esteja utilizando a anotação @Log4j2 do Lombok:
instancie o logger manualmente.
@RestController
@RequestMapping("/autenticacao")
public class AutenticacaoUsuarioController {
// Instaciando logs na aplicação
Logger log = LogManager.getLogger(AutenticacaoUsuarioController.class);
// Exemplo de como usar
log.error("PUT updateImagem mensagem: imagem atualizada");
}
É importante destacar que as importações corretas devem ser realizadas para que o
Log4j2 funcione adequadamente. As importações necessárias são:
• import org.apache.logging.log4j.Logger; import
• org.apache.logging.log4j.LogManager;
Essas classes são responsáveis por criar e gerenciar o logger na aplicação. O Logger é
utilizado para registrar as mensagens de log, enquanto o LogManager é usado para
obter a instância do logger associada à classe em questão.
Configuração de Logs a Nível Global
Para centralizar e padronizar o uso de logs na aplicação, podemos criar uma classe de
configuração no pacote de configurações. Essa abordagem permite configurar o
comportamento dos logs a nível global, tornando-os mais consistentes e fáceis de
gerenciar. Essa prática é particularmente útil em sistemas maiores, como aplicações
de microserviços, onde consistência e padronização nos logs são fundamentais para
monitoramento e depuração.
@Configuration
public class RaquestLoggingFiltroConfig {
// Configuração de logs
@Bean
public CommonsRequestLoggingFilter loggingFilter(){
var filter = new CommonsRequestLoggingFilter();
filter.setIncludeQueryString(true); // Incluindo a query String
filter.setIncludePayload(true); // Payload da mensagem
filter.setMaxPayloadLength(10000); // limite do payload
filter.setAfterMessagePrefix("SOLICITAÇÃO DE DADOS: "); // Mensagem fixa
filter.setHeaderPredicate(header -> !header.equalsIgnoreCase("authorization")); //
Ingnore a tag authorization
return filter;
}
}
No módulo de configuração, será criada uma classe chamada
RequestLoggingFiltroConfig, que conterá o método loggingFilter. Esse método, do tipo
CommonsRequestLoggingFilter, retornará um filtro com as configurações iniciais de
logs para registrar as requisições HTTP da aplicação. Documentação do log4j2:
https://logging.apache.org/log4j/2.x/index.html
Considerações finais
Este material consolida os fundamentos e as práticas avançadas para o
desenvolvimento de aplicações back-end modernas utilizando o ecossistema Java
com Spring Boot. Ele aborda a forma didática desde os conceitos básicos até as
implementações mais sofisticadas, tornando-se um guia importante para
desenvolvedores que desejam criar soluções robustas, escaláveis e compatíveis às
exigênci
Fundamentos do Spring Framework
No início da jornada, destacamos a relevância do Spring Framework, uma das
ferramentas mais poderosas para o desenvolvimento em Java. Conceitos como
Inversão de Controle (IoC) e Injeção de Dependência foram explorados a fundo,
revelando como esses paradigmas adaptados para a modularidade, a testabilidade e a
flexibilidade das aplicações. Além disso, uma explicação detalhada sobre o ciclo de
vida dos beans e os diferentes métodos de injeção de dependências (via construtor ou
setter) mostrou como o contêiner gerencia os objetos de forma eficiente, promovendo
um design limpo e desacoplado.
A Revolução do Spring Boot
O Spring Boot foi apresentado como uma evolução natural para simplificar a
configuração e o desenvolvimento de aplicações Spring. Este capítulo destacou suas
vantagens, como inicialização rápida, configuração mínima, integração nativa com
APIs RESTful e suporte completo a microsserviços. Ao abordar segurança,
escalabilidade e modularidade, enfatizamos como o Spring Boot permite aos
desenvolvedores focar na lógica de negócios, enquanto automatiza tarefas repetitivas
e simplifica processos
Persistência de Dados e JPA
A introdução à Java Persistence API (JPA) e à sua implementação pelo Hibernate
declarada como o Spring Data JPA transforma o gerenciamento de dados relacionados
em uma tarefa intuitiva e eficiente. O mapeamento de entidades, a criação de enums e
a execução de operações de CRUD foram explorados em profundidade, ilustrando
como o JPA abstrai a complexidade do acesso a dados, ao mesmo tempo em que
garante desempenho e escalabilidade.
Desenvolvimento de APIs RESTful
O guia enfatizou o desenvolvimento de APIs RESTful como base para arquiteturas
modernas. Desde os conceitos fundamentais de REST e os níveis do Modelo de
Maturidade de Richardson, até a implementação de endpoints com segurança e
validação, cada detalhe foi cuidadosamente explicado. Funcionalidades como
paginação, filtros dinâmicos e suporte a hipermídias (HATEOAS) foram abordadas para
garantir que as APIs criadas não apenas atendam aos requisitos funcionais, mas
também apresentam uma experiência de uso consistente
Melhoria Contínua e Boas Práticas
O tratamento de erros, as validações customizadas, a configuração de CORS e a
integração de logs com Log4j2 revelaram a importância de boas práticas para garantir a
qualidade e a manutenibilidade das aplicações. Além disso, o uso de padrões globais,
como serialização em ISO 8601 e validação de senhas personalizadas, reforça o
compromisso com a consistência e a
Documentação e Testabilidade
A documentação foi abordada com destaque para o SpringDoc, permitindo que as APIs
sejam informadas e utilizadas de maneira eficiente por desenvolvedores e clientes.
Além disso, o guia incentiva a implementação de testes, garantindo que os sistemas
sejam robustos e confiáveis mesmo diante da mudança
Com este material, percorremos uma jornada que conecta teoria e prática de forma
integrada. Do primeiro contato com o Spring até a construção de APIs RESTful
sofisticadas, o leitor é orientado por um caminho que equilibra conceitos
fundamentais e técnicas avançadas. Ao finalizar este estudo, espero que o
desenvolvedor esteja não apenas apto a implementar aplicações back-end de alta
qualidade, mas também preparado para explorar novos desafios, criar soluções
inovadoras e contribuir para projetos que façam
Este material não é apenas uma referência técnica; é um convite para dominar o
mundo do desenvolvimento com Spring e se destacar como um profissional
capacitado em um dos ecossistas.