Java e Spring Boot - Aplicando conhecimentos
O Boar Task nasceu como uma iniciativa para aplicar e expandir meus conhecimentos, servindo tanto como ferramenta prática do dia a dia quanto como ponto de referência para o meu desenvolvimento profissional. Inspirado no Trello, este projeto tem como objetivo replicar funcionalidades essenciais de um sistema de gerenciamento de tarefas, permitindo a criação e movimentação de cards entre colunas pré-definidas.
1. Configuração Inicial
Para estruturar o projeto, optei por utilizar o Spring Boot, que possibilita a criação de APIs REST robustas e escaláveis, essenciais para o backend do sistema. Com o PostgreSQL como banco de dados, os cards — compostos por título, descrição, data de criação e um status — são armazenados e gerenciados de forma eficiente. Ao serem criados, os cards iniciam automaticamente no status “A_FAZER”, sendo posteriormente movidos para as colunas “FAZENDO”, “PAUSADO” ou “CONCLUÍDO”, conforme o fluxo de trabalho.
Além disso, a utilização do Lombok foi decisiva na configuração inicial do projeto. Este recurso elimina a necessidade de escrever manualmente métodos repetitivos, como getters, setters, construtores e métodos auxiliares, promovendo um código mais limpo e de fácil manutenção. Dessa forma, posso me concentrar na implementação da lógica de negócio e na evolução do sistema, enquanto o Lombok cuida do boilerplate, agilizando o desenvolvimento e reduzindo a possibilidade de erros.
Com essa base sólida, o Boar Task não só demonstra os conceitos fundamentais do desenvolvimento backend com Spring Boot e integração com bancos de dados, como também reforça meu método de estudo, que combina a leitura de livros e o desenvolvimento prático de projetos para consolidar o conhecimento adquirido.
Dependências (pom.xml)
<dependencies>
<! - Spring Boot Web →
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<! - Spring Data JPA →
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<! - PostgreSQL Driver →
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<! - Validação →
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<! - Lombok →
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
2. Estrutura do Projeto
src/main/java/
└── com.demanxier.cards/
├── controller/ # Endpoints da API
├── dto/ # Objetos de transferência de dados
├── exception/ # Tratamento de exceções
├── model/ # Entidades JPA
├── repository/ # Interfaces de acesso ao banco
├── service/ # Lógica de negócio
3. Camada Model (Entidades)
Card.java
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Card {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String titulo;
@Column(columnDefinition = "TEXT")
private String descricao;
private LocalDateTime dataCriacao = LocalDateTime.now();
private LocalDateTime dataConclusao;
@Enumerated(EnumType.STRING)
private StatusColuna statusColuna;
}
StatusColuna.java (Enum)
public enum StatusColuna {
A_FAZER,
FAZENDO,
PAUSADO,
CONCLUIDO
}
Anotações Explicadas:
- @Entity: Define que a classe é uma entidade JPA
- @Id: Marca o campo como chave primária
- @GeneratedValue: Estratégia de geração de IDs
- @Enumerated: Mapeia enums para strings no banco
4. Camada Repository
CardRepository.java
@Repository
public interface CardRepository extends JpaRepository<Card, Long> {
List<Card> findByStatusColuna(StatusColuna status);
@Query("SELECT c FROM Card c WHERE c.dataConclusao BETWEEN :inicio AND :fim")
List<Card> findCompleteBetweenDatas(@Param("inicio")LocalDateTime inicio,
@Param("fim") LocalDateTime fim);
@Query("SELECT c FROM Card c WHERE c.statusColuna = 'CONCLUIDO' AND c.dataConclusao >= :cutoff")
List<Card> findRecentCompletado(@Param("cutoff") LocalDateTime cutoff);
}
Conceitos Importantes:
- Herda métodos CRUD básicos do JpaRepository
- Consultas customizadas com `@Query`
- Parâmetros nomeados com `@Param`
5. Camada DTO (Data Transfer Objects)
CardRequestDTO.java
public record CardRequestDTO (
@NotBlank String titulo,
String descricao
){}
CardResponseDTO.java
public record CardResponseDTO(
Long id,
String titulo,
String descricao,
StatusColuna statusColuna,
LocalDateTime dataConclusao
) {
public CardResponseDTO(Card card){
this(
card.getId(),
card.getTitulo(),
card.getDescricao(),
card.getStatusColuna(),
card.getDataConclusao()
);
}
}
Por que usar DTOs?
- Separar a camada de persistência da camada de API
- Controlar quais dados são expostos
- Validar entradas de forma centralizada
6. Camada de serviços(Service).
CardService.java
@Service
public class CardService {
private final CardRepository cardRepository;
public CardService(CardRepository cardRepository){
this.cardRepository = cardRepository;
}
public Card criarCard(CardRequestDTO cardDTO){
Card novoCard = new Card();
novoCard.setTitulo(cardDTO.titulo());
novoCard.setDescricao(cardDTO.descricao());
novoCard.setStatusColuna(StatusColuna.A_FAZER);
return cardRepository.save(novoCard);
}
public Optional<Card> atualizarCard(Long id, String novaDescricao){
return cardRepository.findById(id)
.map(card -> {
card.setDescricao(novaDescricao);
return cardRepository.save(card);
});
}
public Card moverCard(Long id, StatusColuna novoStatus){
return cardRepository.findById(id)
.map(card -> {
validarTransicao(card.getStatusColuna(), novoStatus);
if (novoStatus == StatusColuna.CONCLUIDO){
card.setDataConclusao(LocalDateTime.now());
} else if (card.getStatusColuna() == StatusColuna.CONCLUIDO) {
card.setDataConclusao(null);
}
card.setStatusColuna(novoStatus);
return cardRepository.save(card);
})
.orElseThrow(()-> new RuntimeException("Card não encontrado."));
}
private void validarTransicao(StatusColuna atual, StatusColuna novo){
if(novo == StatusColuna.A_FAZER){
throw new IllegalArgumentException("Não pode voltar o Card para o Status inicial. Pause ele.");
}
if(atual == StatusColuna.CONCLUIDO && novo != StatusColuna.CONCLUIDO) {
throw new IllegalArgumentException("Card concluído não pode ser alterado");
}
}
public List<Card> buscarConcluidos(LocalDateTime inicio, LocalDateTime fim){
if(inicio != null && fim != null){
return cardRepository.findCompleteBetweenDatas(inicio, fim);
}
return cardRepository.findRecentCompletado(LocalDateTime.now().minusDays(15));
}
public List<Card> buscarCardsStatus(StatusColuna status){
return cardRepository.findByStatusColuna(status);
}
}
Responsabilidades do Service:
- Lógica de negócio
- Validações complexas
- Transformação de DTOs em entidades
- Comunicação com o Repository
7. Camada Controller
CardController.java
@RestController
@RequestMapping("/api/v1/cards")
@CrossOrigin(origins = "*")
public class CardController {
private final CardService cardService;
public CardController(CardService cardService){
this.cardService = cardService;
}
@PostMapping
public ResponseEntity<CardResponseDTO> criarCard(@Valid @RequestBody CardRequestDTO cardDTO){
Card card = cardService.criarCard(cardDTO);
return ResponseEntity.status(HttpStatus.CREATED).body(new CardResponseDTO(card));
}
@PutMapping("/{id}/move")
public ResponseEntity<CardResponseDTO> moverCard(@PathVariable Long id, @RequestParam StatusColuna newStatus){
Card card = cardService.moverCard(id, newStatus);
return ResponseEntity.ok(new CardResponseDTO(card));
}
@GetMapping("/relatorio/concluido")
public ResponseEntity<List<CardResponseDTO>> getCompletedReport(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime inicio,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime fim
) {
List<Card> cards = cardService.buscarConcluidos(inicio, fim);
return ResponseEntity.ok(cards.stream().map(CardResponseDTO::new).toList());
}
@GetMapping
public ResponseEntity<List<CardResponseColunaDTO>> getCardsStatus(@RequestParam(required = false) StatusColuna status){
List<Card> cards = cardService.buscarCardsStatus(status);
return ResponseEntity.ok(cards.stream().map(CardResponseColunaDTO::new).toList());
}
@PutMapping("/atualizar/{id}")
public ResponseEntity<CardResponseDTO> atuaizarCard(@PathVariable Long id, @RequestBody CardAtualizarDTO atualizarDTO){
return cardService.atualizarCard(id, atualizarDTO.descricao())
.map(card -> ResponseEntity.ok(new CardResponseDTO(card)))
.orElse(ResponseEntity.notFound().build());
}
}
Anotações Principais:
- @RestController: Combina `@Controller` e `@ResponseBody`
- @RequestMapping: Define o caminho base do endpoint
- @Valid: Ativa a validação do DTO
- @PathVariable: Captura variáveis da URL
- @CrossOrigin: Controla quem poderá enviar requisições.
8. Tratamento de Erros Global
GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleBadRequest(Exception ex) {
return ResponseEntity.badRequest().body(ex.getMessage());
}
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<String> handleNotFound(Exception ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
}
}
Funcionalidades:
- Captura exceções em todo o aplicativo
- Padroniza respostas de erro
- Mantém logs consistentes
WebConfig.Java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry){
registry.addMapping("/**")// Aplica a todos os endpoints
.allowedOrigins("*") // Permite todas as origens
.allowedMethods("*") // Permite todos os métodos (GET, POST, PUT, etc.)
.allowedHeaders("*") // Permite todos os cabeçalhos
.allowCredentials(false) // Não permite credenciais (cookies, autenticação)
.maxAge(3600); // Cache de opções CORS por 1 hora
}
}
Funcionalidades:
- Gerencia o controle do CrossOrigin, para permitir ou não requisições.
- Existem outras maneiras, mas no meu caso estou permitindo qualquer origem.
9. Endpoints da API
10. Melhorias Futuras
o Autenticação: Adicionar Spring Security
o Upload de Imagens: Implementar armazenamento em nuvem
o Histórico de Alterações: Audit trail das mudanças
o Paginação: Para listagens grandes
o Cache: Melhorar performance com Redis
Conclusão
Este projeto demonstrou os principais conceitos do desenvolvimento backend com Spring Boot, explorando uma arquitetura em camadas que promove a separação de responsabilidades, ideal tanto para iniciantes quanto para projetos de pequena e média escala. A utilização do JPA/Hibernate permitiu uma abordagem orientada a objetos para a persistência dos dados, eliminando a necessidade de consultas SQL complexas e facilitando as operações de leitura e escrita. Além disso, a validação rigorosa de dados e o tratamento estruturado de erros contribuíram para a criação de um sistema robusto e tolerante a falhas, enquanto as boas práticas de desenvolvimento de APIs REST asseguraram a consistência e a clareza na comunicação dos serviços.
No front-end, a implementação simples com HTML, CSS e JavaScript, para testes práticos(por exemplo, o movimento de cards entre colunas), demonstrou a integração entre diferentes camadas da aplicação e possibilitou simulações reais de uso.
Perspectivas e Compromisso com o Aprendizado.
Meu foco é aprofundar meus conhecimentos em Java e seus frameworks, e este projeto é apenas o ponto de partida de uma jornada contínua de aprendizado. Adoto um método que combina a leitura de livros e a execução prática por meio do desenvolvimento de projetos reais, além de vídeo aulas no Youtube e aqui na Dio, o que me permite colocar em prática os conceitos estudados e assimilar melhor os ensinamentos. Com essa abordagem, planejo explorar temas avançados, como Lambdas, Streams, Optional, Concorrência, Gerenciamento de Memória, Coleções e Generics, fortalecendo minha base e ampliando minha expertise no ecossistema Java.
Bibliografia:
Livro: Java Como Programar. 10ª Edição
Livro: Arquitetura Limpa.
Repositório: