image

Acesse bootcamps ilimitados e +650 cursos pra sempre

60
%OFF
Article image
Demanxier Rodrigues
Demanxier Rodrigues23/03/2025 18:20
Compartilhe
Nexa - Análise Avançada de Imagens e Texto com IA na AWSRecomendados para vocêNexa - Análise Avançada de Imagens e Texto com IA na AWS

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

    image

    10. Melhorias Futuras

    Autenticação: Adicionar Spring Security

    Upload de Imagens: Implementar armazenamento em nuvem

    Histórico de Alterações: Audit trail das mudanças

    Paginação: Para listagens grandes

    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:

    https://github.com/Demanxier/BoarTaks

    Compartilhe
    Recomendados para você
    Microsoft AI for Tech - Azure Databricks
    Microsoft Certification Challenge #3 DP-100
    Decola Tech 2025
    Comentários (1)
    DIO Community
    DIO Community - 24/03/2025 15:29

    Muito legal o seu artigo, Demanxier! O seu projeto "Boar Task" é uma excelente aplicação dos conceitos que estamos aprendendo com Spring Boot, JPA, e PostgreSQL, e fico muito feliz de ver como você usou a arquitetura em camadas para criar uma estrutura de código limpa e organizada.

    A escolha de usar o Spring Boot para criar a API REST e a integração com o PostgreSQL é perfeita para quem busca praticidade e escalabilidade. Além disso, a utilização do Lombok para reduzir o boilerplate é uma ótima prática que facilita o desenvolvimento e melhora a legibilidade do código.

    Você fez um ótimo trabalho ao dividir o projeto em camadas bem definidas, como o controlador, serviço e repositório. Isso facilita a manutenção e expansão do projeto, além de garantir que cada camada tenha uma responsabilidade bem delimitada. O uso de DTOs (Data Transfer Objects) também é uma excelente maneira de separar a camada de persistência da API, tornando a aplicação mais flexível.

    Agora que você compartilhou esse conhecimento, minha dúvida é: qual seria o próximo passo para melhorar ainda mais a escalabilidade desse sistema? Talvez uma implementação de cache com Redis ou o uso de Spring Security para autenticação? Como você planeja expandir o projeto no futuro?

    Recomendados para você