Visão Arquitetural: Construindo um Web Service com Camadas Lógicas
O objetivo é desenvolver um web service RESTful robusto, seguindo as melhores práticas de design de software. Para isso, adotaremos uma arquitetura em camadas, que é fundamental para a organização, manutenibilidade e testabilidade do código.
- Resource Layer (Camada de Recursos): É a porta de entrada da nossa API. Composta por Controladores REST (
REST Controllers), esta camada é responsável por expor os endpoints HTTP (ex:/users,/products/{id}) e por traduzir as requisições HTTP em chamadas para a camada de serviço. - Service Layer (Camada de Serviço): Contém a lógica de negócio da aplicação. Ela orquestra as operações, aplica regras e validações. Os serviços são agnósticos em relação à tecnologia de apresentação (não sabem que estão sendo chamados por um controlador REST) e delegam as operações de persistência à camada de acesso a dados.
- Data Access Layer (Camada de Acesso a Dados): Também conhecida como Repository Layer. É a única camada que se comunica com o banco de dados. Utilizando o Spring Data JPA, definimos interfaces que, por mágica do framework, nos fornecem implementações completas para operações CRUD (Create, Retrieve, Update, Delete), abstraindo toda a complexidade do JDBC e do Hibernate.
As Entidades são os objetos do nosso modelo de domínio, que transitam entre essas camadas.
Estrutura do Projeto: Organização para Escalabilidade
A organização dos pacotes deve refletir a nossa arquitetura em camadas. Esta estrutura torna o projeto intuitivo e fácil de navegar.
com.yourproject
├── config/ // Classes de configuração (ex: popular o banco de teste)
├── entities/ // Classes do modelo de domínio (User, Order, Product, etc.)
│ └── enums/ // Enums específicos do domínio (OrderStatus)
├── repositories/ // Interfaces do Spring Data JPA (UserRepository, etc.)
├── services/ // Classes da camada de serviço (UserService, etc.)
│ └── exceptions/ // Exceções customizadas da camada de serviço
└── resources/ // Controladores REST (UserResource, etc.)
└── exceptions/ // Manipulador de exceções global para a camada de recursos
Fase 1: Configuração do Projeto e Ambiente de Teste
Vamos criar a base do projeto e configurar um ambiente de desenvolvimento rápido com o banco de dados em memória H2.
-
Criação do Projeto: Utilize o Spring Initializr ou a funcionalidade integrada da sua IDE (
File -> New -> Spring Starter Project).- Project: Maven
- Language: Java (versão 11 ou superior)
- Packaging: JAR
- Dependencies:
Spring Web,Spring Data JPA,H2 Database.
-
Configuração de Perfis (Profiles): O Spring permite criar perfis de configuração para diferentes ambientes. Vamos criar um perfil
testpara usar o H2.src/main/resources/application.properties(arquivo principal que ativa o perfil de teste)spring.profiles.active=testsrc/main/resources/application-test.properties(configurações específicas para o H2)# Configuração da conexão com o banco H2 em memória spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.username=sa spring.datasource.password= # Habilita o console web do H2 para debug (acessível em http://localhost:8080/h2-console) spring.h2.console.enabled=true spring.h2.console.path=/h2-console # Configurações do JPA/Hibernate para mostrar e formatar o SQL gerado spring.jpa.show-sql=true spring.jpa.properties.hibernate.format_sql=true
Fase 2: Implementando a Camada de Dados e o Modelo de Domínio
Com o projeto configurado, vamos criar nossa primeira entidade e seu repositório.
-
Entidade
User.java: Esta é uma classe POJO (Plain Old Java Object) anotada com JPA para mapeamento objeto-relacional.// package com.yourproject.entities; import javax.persistence.*; import java.io.Serializable; import java.util.Objects; @Entity @Table(name = "tb_user") // Define o nome da tabela no banco de dados public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; private String phone; private String password; // Construtores, Getters, Setters, hashCode e equals // ... } -
Repositório
UserRepository.java: Ao estenderJpaRepository, o Spring Data JPA gera automaticamente todos os métodos CRUD básicos em tempo de execução.// package com.yourproject.repositories; import com.yourproject.entities.User; import org.springframework.data.jpa.repository.JpaRepository; // JpaRepository<TipoDaEntidade, TipoDaChavePrimaria> public interface UserRepository extends JpaRepository<User, Long> { // Métodos CRUD como save(), findById(), findAll(), deleteById() já estão disponíveis. } -
Povoando o Banco de Testes (Database Seeding): Vamos criar uma classe de configuração para inserir dados no banco H2 sempre que a aplicação iniciar no perfil
test.// package com.yourproject.config; import com.yourproject.entities.User; import com.yourproject.repositories.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import java.util.Arrays; @Configuration @Profile("test") // Indica que esta configuração só será ativa no perfil 'test' public class TestConfig implements CommandLineRunner { @Autowired // O Spring injeta a dependência automaticamente private UserRepository userRepository; @Override public void run(String... args) throws Exception { User u1 = new User(null, "Maria Brown", "maria@gmail.com", "988888888", "123456"); User u2 = new User(null, "Alex Green", "alex@gmail.com", "977777777", "123456"); userRepository.saveAll(Arrays.asList(u1, u2)); } }
Fase 3: Implementando as Camadas de Serviço e Recursos (CRUD)
Agora, vamos expor as operações CRUD através de uma API REST.
-
UserService.java: Contém a lógica de negócio. Ela depende doUserRepositorypara acessar os dados.// package com.yourproject.services; import com.yourproject.entities.User; import com.yourproject.repositories.UserRepository; import com.yourproject.services.exceptions.ResourceNotFoundException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; @Service // Registra a classe como um componente de serviço do Spring public class UserService { @Autowired private UserRepository repository; public List<User> findAll() { return repository.findAll(); } public User findById(Long id) { Optional<User> obj = repository.findById(id); return obj.orElseThrow(() -> new ResourceNotFoundException(id)); } public User insert(User obj) { return repository.save(obj); } } -
UserResource.java: O Controlador REST que expõe os endpoints HTTP.// package com.yourproject.resources; import com.yourproject.entities.User; import com.yourproject.services.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.support.ServletUriComponentsBuilder; import java.net.URI; import java.util.List; @RestController @RequestMapping(value = "/users") public class UserResource { @Autowired private UserService service; @GetMapping public ResponseEntity<List<User>> findAll() { List<User> list = service.findAll(); return ResponseEntity.ok().body(list); } @GetMapping(value = "/{id}") public ResponseEntity<User> findById(@PathVariable Long id) { User obj = service.findById(id); return ResponseEntity.ok().body(obj); } @PostMapping public ResponseEntity<User> insert(@RequestBody User obj) { obj = service.insert(obj); // Retorna o status HTTP 201 Created com a URL do novo recurso no header URI uri = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}") .buildAndExpand(obj.getId()).toUri(); return ResponseEntity.created(uri).body(obj); } }
Fase 4: Tratamento de Exceções Centralizado
Para evitar blocos try-catch em cada método do controlador e padronizar as respostas de erro, criamos um manipulador de exceções global.
-
Exceção Customizada:
ResourceNotFoundException.java -
ResourceExceptionHandler.java: Intercepta exceções lançadas pelos controladores e retorna uma resposta HTTP padronizada.// package com.yourproject.resources.exceptions; import com.yourproject.services.exceptions.ResourceNotFoundException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import javax.servlet.http.HttpServletRequest; import java.time.Instant; @ControllerAdvice // Intercepta exceções para serem tratadas globalmente public class ResourceExceptionHandler { @ExceptionHandler(ResourceNotFoundException.class) // Trata exceções do tipo especificado public ResponseEntity<StandardError> resourceNotFound(ResourceNotFoundException e, HttpServletRequest request) { String error = "Resource not found"; HttpStatus status = HttpStatus.NOT_FOUND; StandardError err = new StandardError(Instant.now(), status.value(), error, e.getMessage(), request.getRequestURI()); return ResponseEntity.status(status).body(err); } }
Fase 5: Implantação em Produção com PostgreSQL e Heroku
Para a produção, trocamos o H2 por um banco de dados robusto como o PostgreSQL e fazemos o deploy na nuvem.
- Adicionar Dependência do PostgreSQL no
pom.xml. - Criar Perfis
deveprod:application-dev.properties: Para conectar a um PostgreSQL local.application-prod.properties: Para conectar ao banco do Heroku, usando variáveis de ambiente (${...}).# URL do banco de dados fornecida pelo Heroku spring.datasource.url=${DATABASE_URL} # Desativa a criação automática de tabelas em produção spring.jpa.hibernate.ddl-auto=none
- Configurar o Heroku:
- Crie uma conta e um novo aplicativo no Heroku.
- Provisione o add-on “Heroku Postgres”.
- No painel de configurações do seu app Heroku (
Settings -> Config Vars), o Heroku já cria a variávelDATABASE_URL. Adicione outras variáveis que sua aplicação precise (como segredos JWT, etc.).
- Criar
system.properties: Na raiz do projeto, crie este arquivo para especificar a versão do Java para o Heroku.java.runtime.version=11 - Fazer o Deploy: Após configurar o Git para o seu repositório Heroku, o deploy é feito com um simples comando:
git push heroku master
Seguindo este guia, você terá construído um web service completo, desde a concepção e desenvolvimento local até a implantação em um ambiente de produção na nuvem, utilizando um conjunto de tecnologias moderno e poderoso do ecossistema Java.