Este tutorial guia você na criação de uma aplicação backend do zero utilizando Spring Boot, Java, Maven e o banco de dados Oracle, além de boas práticas como DTOs, validações, tratamento global de exceções e organização modular.
Acesse: https://start.spring.io
- Project: Maven
- Language: Java
- Spring Boot: Versão LTS (ex: 3.2.x)
- Group:
com.seuprojeto - Artifact:
nomedoprojeto - Name:
nomedoprojeto - Description: Uma breve descrição da aplicação
- Package name:
com.seuprojeto.nomedoprojeto - Packaging: Jar
- Java Version: 17 ou superior
- Spring Web
- Spring Data JPA
- Validation
- Lombok
- Flyway Migration
- Oracle Driver
- Spring Security
Clique em Generate para baixar o projeto.
@SpringBootApplication: adicionada automaticamente na classe principal, combina@Configuration,@EnableAutoConfiguratione@ComponentScan.
- Extraia o
.zipbaixado. - Abra o IntelliJ IDEA.
- Vá em File > Open e selecione a pasta extraída.
- O IntelliJ reconhecerá o projeto como Maven automaticamente.
Dentro de src/main/java/com/seuprojeto/nomedoprojeto, crie:
controller— Controladores REST (entrada da API)modeloudomain— Entidades JPArepository— Interfaces que herdam de JpaRepositoryservice— Regras de negóciodto— Objetos de transferência de dados (entrada/saída)exception— Classes de exceção customizadasadvice— Manipuladores globais de erro
@Entity
@Table(name = "usuarios")
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "usuario_seq")
@SequenceGenerator(name = "usuario_seq", sequenceName = "usuario_seq", allocationSize = 1)
private Long id;
@Column(nullable = false, length = 100)
private String nome;
@Column(nullable = false, unique = true)
private String email;
public Usuario() {}
public Usuario(Long id, String nome, String email) {
this.id = id;
this.nome = nome;
this.email = email;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNome() { return nome; }
public void setNome(String nome) { this.nome = nome; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Usuario)) return false;
Usuario usuario = (Usuario) o;
return Objects.equals(id, usuario.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}@Entity: indica que a classe é uma entidade JPA.@Table: define o nome da tabela no banco.@Id: define o campo como chave primária.@GeneratedValue: gera o valor da chave automaticamente.@SequenceGenerator: usa sequência no banco para gerar o ID.@Column: configura propriedades da coluna (nullable, length, unique).
public interface UsuarioRepository extends JpaRepository<Usuario, Long> {
List<Usuario> findByNome(String nome);
Optional<Usuario> findByEmail(String email);
@Query("SELECT u FROM Usuario u WHERE u.email = :email")
Optional<Usuario> buscarPorEmailJPQL(@Param("email") String email);
}JpaRepository: interface do Spring Data que oferece métodos prontos comosave,findAll,deleteByIdetc.@Query: permite escrever consultas personalizadas em JPQL.@Param: vincula o nome do parâmetro na query ao parâmetro do método.
findByNome(String nome)findByEmail(String email)findByNomeContaining(String termo)— busca parcialfindByEmailAndNome(String email, String nome)— múltiplas condiçõesexistsByEmail(String email)— verifica existênciacountByNome(String nome)— retorna a contagem
@Service
public class UsuarioService {
@Autowired
private UsuarioRepository usuarioRepository;
public Usuario salvar(Usuario usuario) {
return usuarioRepository.save(usuario);
}
public List<UsuarioDTO> listarTodos() {
return usuarioRepository.findAll()
.stream()
.map(usuario -> new UsuarioDTO(usuario.getNome(), usuario.getEmail()))
.toList();
}
public Page<UsuarioDTO> listarPaginado(Pageable pageable) {
return usuarioRepository.findAll(pageable)
.map(usuario -> new UsuarioDTO(usuario.getNome(), usuario.getEmail()));
}
public Usuario buscarPorEmail(String email) {
return usuarioRepository.findByEmail(email)
.orElseThrow(() -> new RecursoNaoEncontradoException("Usuário com e-mail " + email + " não encontrado"));
}
public void deletarPorId(Long id) {
usuarioRepository.deleteById(id);
}
}@Service: define a classe como componente de serviço (regra de negócio).@Autowired: injeta automaticamente dependências gerenciadas pelo Spring.
- O retorno
Page<UsuarioDTO>permite paginação automática pela interface do Spring Data. - A resposta inclui:
content,totalElements,totalPages,number(página atual) etc. - Exemplos de uso na URL:
?page=0&size=10— primeira página com 10 elementos?sort=nome,asc— ordena por nome de forma crescente
@RestController
@RequestMapping("/usuarios")
public class UsuarioController {
@Autowired
private UsuarioService usuarioService;
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public Usuario criar(@RequestBody @Valid UsuarioDTO dto) {
Usuario usuario = new Usuario(null, dto.getNome(), dto.getEmail());
return usuarioService.salvar(usuario);
}
@GetMapping
@ResponseStatus(HttpStatus.OK)
public List<Usuario> listar() {
return usuarioService.listarTodos();
}
@DeleteMapping("/{id}")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void deletar(@PathVariable Long id) {
usuarioService.deletarPorId(id);
}
}@RestController: define a classe como controller REST.@RequestMapping: define o path base do controller.@PostMapping,@GetMapping,@DeleteMapping: mapeiam requisições HTTP.@RequestBody: indica que o corpo da requisição deve ser mapeado para o parâmetro.@Valid: ativa validações no DTO.@ResponseStatus: define o código de status da resposta.@PathVariable: extrai valores da URL.
#Crie uma sessão explicando a diferença entre Mappings com @PathVariable e @PathParam e seus casos de uso (seja breve)#
Crie a pasta src/main/resources/db/migration
V1__criar_tabela_usuarios.sql
CREATE SEQUENCE usuario_seq START WITH 1 INCREMENT BY 1;
CREATE TABLE usuarios (
id NUMBER PRIMARY KEY,
nome VARCHAR2(100) NOT NULL,
email VARCHAR2(150) NOT NULL UNIQUE
);🔁 Siga o padrão
V{versao}__descricao.sql, comoV2__adicionar_coluna_status.sql
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:XE
spring.datasource.username=usuario
spring.datasource.password=senha
spring.datasource.driver-class-name=oracle.jdbc.OracleDriver
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
# Exibir erros no response
server.error.include-message=always
server.error.include-binding-errors=always
server.error.include-stacktrace=alwayspublic class UsuarioDTO {
@NotBlank(message = "Nome é obrigatório")
private String nome;
@NotBlank(message = "Email é obrigatório")
@Email(message = "Email inválido")
private String email;
// Getters e setters
}@NotBlank: valida que o campo não é nulo nem vazio.@Email: valida que o campo tem formato de email válido.@Size: valida a quantidade de caracteres com o uso dos parametros min e max.
@ResponseStatus(HttpStatus.NOT_FOUND)
public class RecursoNaoEncontradoException extends RuntimeException {
public RecursoNaoEncontradoException(String message) {
super(message);
}
}@RestControllerAdvice
public class ApplicationExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<List<String>> handleValidacoes(MethodArgumentNotValidException ex) {
List<String> erros = ex.getBindingResult().getFieldErrors()
.stream()
.map(FieldError::getDefaultMessage)
.toList();
return ResponseEntity.badRequest().body(erros);
}
@ExceptionHandler(RecursoNaoEncontradoException.class)
public ResponseEntity<String> handleNotFound(RecursoNaoEncontradoException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body("Erro inesperado: " + ex.getMessage());
}
}@RestControllerAdvice: intercepta e trata exceções globalmente para todos os controllers.@ExceptionHandler: define o tratamento de uma exceção específica.@ResponseStatus: define o código de status HTTP a ser retornado.
GET /usuarios— Lista todos os usuáriosPOST /usuarios— Cria um novo usuárioDELETE /usuarios/{id}— Deleta um usuário por ID
{
"nome": "João da Silva",
"email": "joao@email.com"
}-- 13. Autenticação e Autorização Ao instalar as dependencis do Spring Security, ao inicializar a aplicação, agora precisaremos nos autenticar para acessar os endpoints. Por padrão no Spring Security usamos o "user" como usuário e a senha é uma combinação hexadecimal gerada ao iniciar a aplicação e pode ser obtida no console. (Importante: esses usuários e senhas são somente para produção!) Se for necessário para o ambiente de desenvolvimento, no arquivo application.properties é possível definir o usuário e senha para facilitar o uso, através das váriáveis: spring.security.user.name=jonathan spring.security.user.password=123456
A autenticação pode ser via Basic Auth (Stateful) (que é o padrão do Spring Boot) que usa "user" e "password" ou pode ser via JWT (JSON Web Token) (Stateless) que é através de um token criptografado composto por três partes: Header, Payload e Verify Signatura. Importante: precisa adicionar a dependencia do JWT, pode ser obtida no site da JWT. Explicar o uso de cada parte do JWT
Criar a entidade de usuário no diretório Model(ou Domain) com nome, email, senha, role, entre outras propriedades, bem como seus getters, setters, equals e hashCode. Muito importate: implements UserDetails na entidade usuario e implemente os métodos da interface. Exemplo:
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(this.role == UsuarioRole.ADMIN){
return List.of(
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("ROLE_USER")
);
} else if (this.role == UsuarioRole.USER){
return List.of(
new SimpleGrantedAuthority("ROLE_USER")
);
}
return List.of();
}
@Override
public String getPassword() {
return senha;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}Criar o Enum para UserRole (os papeis do usuário para autorização, por exemplo: ADMIN(para acesso geral) e USER(para acesso básico), entre outros). Criar os DTOs de cadastro e exibição do usuário (no exibição por exemplo não retornar a senha) Criar a Sequence para o ID do usuário e a tabela tbl_usuarios na migração do FlyWay. Configurar a model do usuário para implementar a UserDetails (do package: "org.springframework.security.core.userdetails.UserDetails"). Importante: Não esquecer de implementar os métodos da UserDetails e configura-los. Exemplo de implementação do método public Collection<? extends GrantedAuthority> getAuthorities():
if(this.role == UsuarioRole.ADMIN){
return List.of(
new SimpleGrantedAuthority("ROLE_ADMIN"),
new SimpleGrantedAuthority("ROLE_USER")
);
} else if (this.role == UsuarioRole.USER){
return List.of(
new SimpleGrantedAuthority("ROLE_USER")
);
}
return List.of();Criar o repository do usuário que extends a JpaRepository<Usuario, Long> Criar o método public findByEmail(String email). Criar o service do usuario com os métodos de gravarUsuario (não esquecendo de encriptografar a senha), buscarUsuarioPorId, etc. Criar o service de authorization que implementa o UserDetailsService (do package: org.springframework.security.core.userdetails.UserDetailsService)
Criar os diretórios "config.security" e criar o "SecurityConfig" com as seguintes implementações:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filtrarCadeiaDeSeguranca(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize -> authorize
.requestMatchers(HttpMethod.GET, "/api/contatos").permitAll()
.requestMatchers(HttpMethod.PUT, "/api/contatos").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST,"/api/contatos").hasRole("ADMIN")
.anyRequest().authenticated())
.build();
}
}E criar também o gerenciador de autenticação dentro da classe SecurityConfig que será usado nos controladores de autenticação (AuthController)
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}E não esquecer de criar o PasswordEncoder:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}Caso na hora de cadastrar um novo usuario, exista um conflito de integridade de dados do banco de dados como por exemplo tentar cadastrar um email já utilizado, será necessario lidar com esse erro dentro da classe ApplicationExceptionHandler:
@ResponseStatus(HttpStatus.CONFLICT)
@ExceptionHandler(DataIntegrityViolationException.class)
public Map<String, String> manusearIntegridadeDosDados() {
Map<String, String> mapaDeErro = new HashMap<>();
mapaDeErro.put("erro", "Email já está cadastrado.");
return mapaDeErro;
}Criar os endpoints de Autenticação com através de um controller (AuthController) no diretório de controles e cadastrar o endpoints, por exemplo:
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UsuarioService usuarioService;
@PostMapping("/login")
public ResponseEntity login(@RequestBody @Valid UsuarioLoginDto usuarioLoginDto) {
UsernamePasswordAuthenticationToken usernamePassword = new UsernamePasswordAuthenticationToken(usuarioLoginDto.email(), usuarioLoginDto.senha());
Authentication auth = authenticationManager.authenticate(usernamePassword);
return ResponseEntity.ok().build();
}
@PostMapping("/register")
@ResponseStatus(HttpStatus.CREATED)
public UsuarioExibicaoDto register(@RequestBody @Valid UsuarioCadastroDto usuarioCadastroDto) {
return usuarioService.gravarUsuario(usuarioCadastroDto);
}
}Criar em "config.security" a classe TokenService para será responsável por gerar e validar os tokens e criar a palavraSecreta (que será usado para criação do token) em variável de ambiente ou no application.properties (exemplo: minha.chave.secreta=${JWT.SECRET:fiap})
@Service
public class TokenService {
@Value("${minha.chave.secreta}")
private String palavraSecreta;
public String gerarToken(Usuario usuario) {
try {
Algorithm algorithm = Algorithm.HMAC256(palavraSecreta);
String token = JWT
.create()
.withIssuer("contatos")
.withSubject(usuario.getEmail())
.withExpiresAt(gerarDataDeExpiracao())
.sign(algorithm);
return token;
} catch (JWTCreationException exception) {
throw new RuntimeException("Não foi possível gerar o token.");
}
}
public String validarToken(String token) {
try{
Algorithm algorithm = Algorithm.HMAC256(palavraSecreta);
return JWT.require(algorithm)
.withIssuer("contatos")
.build()
.verify(token)
.getSubject();
} catch (JWTVerificationException exception) {
return "";
}
}
private Instant gerarDataDeExpiracao() {
return LocalDateTime.now().plusHours(2).toInstant(ZoneOffset.of("-03:00"));
}
}Criar a classe VerificarToken:
@Component
public class VerificarToken extends OncePerRequestFilter {
@Autowired
private TokenService tokenService;
@Autowired
private UsuarioRepository usuarioRepository;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String authorizationHeader = request.getHeader("Authorization");
String token = "";
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
token = authorizationHeader.substring(7);
String login = tokenService.validarToken(token);
UserDetails usuario = usuarioRepository.findByEmail(login);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(usuario, null, usuario.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} else {
token = null;
}
filterChain.doFilter(request, response);
}
}Explicar cada anotação e cada método usado no return
- Utilize DTOs para manter as entidades desacopladas da entrada/saída da API.
- Sempre valide entradas com
@Valid. - Crie exceções específicas e trate-as globalmente.
- Mantenha o projeto modular e bem estruturado.
- Use migrations com Flyway para versionar seu banco de dados.
- Implemente
equals()ehashCode()corretamente com base no ID da entidade.
Ficou com dúvidas ou quer expandir este projeto com autenticação JWT, testes, Swagger, entre outros? Me avise! 🚀