Sua primeira API com Spring Boot e Docker

Sua primeira API com Spring Boot e Docker

Primeiramente, por que usar Spring boot?

Spring Boot é uma Java framework que é amplamente usado para desenvolvimento de aplicações WEB, em especial REST APIs. Um dos recursos mais importantes encontrados nesta ferramenta é a sua configuração automática bem ao estilo plug and play.

Algumas de suas funcionalidades:

  • Gerenciamento de dependências,
  • Tomcat, Jetty or Undertow embutidos e disponíveis de forma nativa,
  • Disponibilização de funcionalidades para serem usadas em produção como métricas, health checks, logs, etc
  • Não há necessidade de escrever código XML para configurações.

Essa última é uma das minhas preferidas. Caso você esteja criando uma aplicação Java WEB sem o uso de algum framework, uma série de coisas tem que ser gerenciadas. Dentre elas: properties e configurações em XML. O Spring Boot faz uma abstração dessa complexidade permitindo que as configurações sejam feitas por meio de anotações no código.

É importante ressaltar também a facilidade adquirida em colocar as aplicações em produção. Conforme mencionado anteriormente: métricas, health checks, logs dentre outras funcionalidades estão prontas para uso. Em adição também temos as bibliotecas que funcionam apenas com a adição da dependência no arquivo pom.xml: construção de APIs, bibliotecas para testes, segurança, ORM estão disponíveis facilmente.

Mãos à obra!

Vou supor aqui que você já entende o básico de Java, alguns conceitos REST e banco de dados relacionais. Agora que já foram detalhados os motivos para usar Spring Boot vamos falar um pouco da aplicação que vamos construir com a ferramenta. O escopo aqui será simples:

Nosso cliente gostaria de implementar uma wishlist para os produtos de seu e-commerce. Com essa requisito em mãos e conversando com o cliente escrevemos a seguinte lista requisitos:

O sistema precisa as possuir entidades:

  • Customer,
  • Product,
  • Wishlist

Precisaremos persistir as informações em um banco de dados MySQL.

As funcionalidades para o consumidor serão:

  • Cadastrar no sistema
  • Adicionar um produto à sua lista no sistema.

Teremos um CRUD praticamente completo para todas as nossas entidades Conteinerização em Docker. O cliente necessita disso para conseguir fazer deploy da aplicação com mais facilidade.

Usaremos a versão de testes da IntelliJ IDEA Ultimate para codarmos o projeto. Você pode fazer o download da aplicação aqui.

Vamos gerar a estrutura do projeto com o Spring Initializr. Essa ferramenta cria todo o esqueleto de um projeto Spring Boot conforme as dependências desejadas.

Ao abrir a página da aplicação vamos preencher as informações do lado esquerdo:

Vamos a uma breve descrição de todos os campos que vamos preencher/selecionar:

Project

Define o tipo de projeto, refere-se ao dependencies manager a ser usado. Neste tutorial vamos trabalhar com o Maven project.

Language

O Spring Boot permite o uso das três linguagens de programação acima. Java é a padrão e vamos trabalhar com ela.

Spring Boot

Versão do Spring Boot. Vamos trabalhar com a última LTS disponível: 2.7.1.

Group

Domínio base package da aplicação.

Artifact

Nome da aplicação.

Description

Descrição da aplicação.

Package name

Java package principal da aplicação.

Java

Versão do Java a ser utilizada.

Essa é a primeira etapa, agora vamos selecionar as dependências:

Vá em add dependencies e pesquise cada uma das dependências:

Spring Data JPA

Reduz o código boilerplate necessário pelo JPA que por sua vez lida com a complexidade de acesso e mapeamento de entidades relacionais. Essa dependência vai permitir que não escrevamos código MySQL na mão. O Hibernate que vem com ela irá gerenciar as operações no nosso banco de dados em um nível mais alto.

Spring WEB

Usa o padrão de padrão de projeto MVC, REST e Tomcat como web server padrão.

MySQL driver

Vai permitir a conexão com o banco de dados MySQL.

Spring security

Usaremos para gerenciar o acesso a certas rotas da API.

Lombok

Permite a redução de código boilerplate repetitivo como getters, setters, construtores, etc.

Agora só precisamos gerar o projeto clicando em generate e o zip será baixado em seu navegador. Vamos exportar o projeto para o IntelliJ e abri-lo na IDE:

Clique em Open e selecione a pasta zip (que acabamos de baixar) já descomprimida.

O IntelliJ irá abrir e baixar todas as dependências que selecionamos para o projeto.

Vamos dar uma olhada em algumas pastas e arquivos chaves gerados anteriormente e agora abertos na IDE:

  • O nosso código irá ser criado no seguinte diretório: src/main/java/com/community/wishlist/
  • No mesmo diretório temos o java file src/main/java/com/community/wishlist/WishlistApplication.java ele é o arquivo main do nosso projeto. É ele que será executado quando rodarmos nossa aplicação. Note a anotação @SpringBootApplication.
  • Nossos testes serão escritos no diretório: src/test/java/com/community/wishlist

O arquivo pom.xml na raiz do projeto contém informações sobre o projeto (preenchemos várias delas na seção project metadata no Spring Initilizr) assim como as configurações para buildar a aplicação. Atualmente nosso arquivo está da seguinte forma:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="<http://maven.apache.org/POM/4.0.0>" xmlns:xsi="<http://www.w3.org/2001/XMLSchema-instance>"
	xsi:schemaLocation="<http://maven.apache.org/POM/4.0.0> <https://maven.apache.org/xsd/maven-4.0.0.xsd>">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.1</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.community</groupId>
	<artifactId>wishlist</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>wishlist</name>
	<description>A ecommerce wishlist</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-security</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.24</version>
			<scope>provided</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

No futuro ele será alterado para adicionarmos as dependências necessárias.

Começaremos a criação de todos os nossos modelos/entidades conforme planejamos anteriormente. Crie um novo pacote chamado model dentro de com.community.wishlist

Agora vamos criar nossas classes Java para os modelos:

  • Costumer
  • Product
  • Wishlist

Ótimo! Agora temos os seguintes arquivos:

Os arquivos estão criados, porém estão todos iguais, sem seus atributos e comportamentos, continuaremos definindo seus atributos. Teremos:

com/community/wishlist/model/Customer.java

public class Customer {

    private Long id;
    private String name;
    private String email;
    private String password;

}

Para o customer, que é o usuário que irá adicionar um produto à sua wishlist teremos os seguintes atributos principais:

  • ID
  • Name
  • Email
  • Password
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password;
}

Essas palavras chaves são as anotações do Spring Boot que comentamos logo no início do texto.

@Id - Indica que esse atributo é o identificador único do modelo

@GeneratedValue -  Indica que esse atributo será gerado

Vamos fazer o uso do Lombok que selecionamos como uma de nossas dependências:

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Entity
public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password;

}

Essa série de anotações gera em tempo de compilação todo esse código:

public class Customer {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String email;
    private String password;
    public Customer(){

    }

    public Customer(Longid, String name, String email, String password) {
        this.id =id;
        this.name =name;
        this.email =email;
        this.password =password;
    }

@Override
public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((id == null) ? 0 : id.hashCode());
        return result;
    }

@Override
public boolean equals(Objectobj) {
        if (this ==obj)
            return true;
        else if (obj== null)
            return false;
        else if (getClass() !=obj.getClass())
            return false;
        Customer other = (Customer)obj;
        if (id == null)
            return other.id == null;
        else return id.equals(other.id);
    }

    public Long getId() {
        return id;
    }

    public void setId(Longid) {
        this.id =id;
    }

    public String getName() {
        return name;
    }

    public void setName(Stringname) {
        this.name =name;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(Stringemail) {
        this.email =email;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(Stringpassword) {
        this.password =password;
    }

}

Bem mais simples e limpo usando o lombok, não?

Para a classe Product teremos os seguintes atributos:

  • Id
  • Price
  • Brand
  • Title

Vamos cria-los no arquivo Product e já definir as anotações do Lombok:

com/community/wishlist/model/Product.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Entity
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private float price;
    private String brand;
    private String title;
}

Para a classe Wishlist teremos os seguintes atributos:

  • Id
  • CostumerId
  • Products

Vamos cria-los no arquivo Wishlist já definir as anotações do Lombok:

com/community/wishlist/model/Wishlist.java

@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@EqualsAndHashCode
@Entity
public class Wishlist {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @OneToOne
    @JoinColumn(name = "customerId")
    private Customer customer;
    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
    @JoinColumn(name = "productId")
    Set<Product> products = new LinkedHashSet<>();
}

Ótimo! Agora temos todos os nossos modelos criados na aplicação. Mais tarde faremos com que o hibernate crie todas as tabelas automaticamente.

Criação do Repository

Agora vamos criar as classes de repository. Estas classes que irão implementar a interface Repository ficam responsáveis por abstrair o código necessário para as camadas de acessos aos dados. Desta forma o desenvolvedor não precisa escrever classes concretas com as implementações de seus métodos e nem mesmo criar uma classe @Bean do spring. Ao implementar essa interface já temos todos as funções comuns a vários bancos de dados como: CREATE, READ, UPDATE e DELETE.

Precisaremos de um repository para cada um de nossos modelos, afinal poderemos fazer operações com todos eles.

Usaremos o diretório src/main/java/com/community/wishlist/repository

CustomerRepository.java

public interface CustomerRepository extends JpaRepository<Customer, Long> {
    Optional<Customer> findByEmail(String email);
}

WishlistRepository.java

public interface WishlistRepository extends JpaRepository<Wishlist, Long> {
    Optional<Wishlist> findByCustomerId(Long customerId);
}

ProductRepository.java

public interface ProductRepository extends JpaRepository<Product, Long> {
}

Tratamento de exceções

Antes de definirmos uma de nossas novas camadas precisamos falar um pouco sobre o tratamento de exceções. No Spring Boot temos a opção de atrelar uma exceção específica que definimos a um HTTP status. Por exemplo, você pode criar uma classe de exceção chamada EntityAlreadyExistsException que é atrelada ao HTTP status code 422 (unprocessable entity). Vamos usar essa exceção no caso de um usuário que já tem e-mail cadastrado tentar se cadastrar novamente.

Crie o package com.community.wishlist.exception e dentro dele defina:

EntityAlreadyExistsException.java

@ResponseStatus(value = HttpStatus.UNPROCESSABLE_ENTITY)
public class EntityAlreadyExistsException extends Exception {

    public EntityAlreadyExistsException(Stringmessage) {
        super(message);
    }
}

Uma outra exceção que iremos ter é para o caso de um recurso não ser encontrado. Esse caso será engatilhado caso o usuário tente por exemplo atualizar ou ler uma entidade que não exista.

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends Exception {
    public ResourceNotFoundException(Stringmessage) {
        super(message);
    }
}

Ainda precisamos de outros dois arquivos, um para representar nosso objeto modelo de resposta quando uma dessas exceções forem acionadas e um outro para fazer o wiring de nosso erro com a resposta que o Spring Boot irá retornar caso ele ocorra.

Nosso modelo de resposta será o seguinte:

ErrorResponse.java

@Getter
@Setter
@AllArgsConstructor
public class ErrorResponse {
    private Date timestamp;
    private String status;
    private String message;
    private String details;
}

E nosso ControllerAdvice:

GlobalExceptionHandler.java

@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<?> resourceNotFoundException(
            ResourceNotFoundExceptionex, WebRequestrequest) {
        ErrorResponse errorDetails =
                new ErrorResponse(new Date(), HttpStatus.NOT_FOUND.toString(),ex.getMessage(),request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
    }

@ExceptionHandler(EntityAlreadyExistsException.class)
    public ResponseEntity<?> entityAlreadyExistsException(
            ResourceNotFoundExceptionex, WebRequestrequest) {
        ErrorResponse errorDetails =
                new ErrorResponse(new Date(), HttpStatus.UNPROCESSABLE_ENTITY.toString(),ex.getMessage(),request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.UNPROCESSABLE_ENTITY);
    }

@ExceptionHandler(Exception.class)
    public ResponseEntity<?> globalExceptionHandler(Exceptionex, WebRequestrequest) {
        ErrorResponse errorDetails =
                new ErrorResponse(new Date(), HttpStatus.INTERNAL_SERVER_ERROR.toString() ,ex.getMessage(),request.getDescription(false));
        return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
    }
}

No arquivo acima definimos o retorno caso nossas duas exceções anteriormente criadas aconteçam, em adição na hipótese de uma exceção genérica ser acionada

Agora temos nossas exceções prontas para serem usadas em nossa próxima camada!

A camada intermediária entre o endpoint o e banco de dados

No momento criamos nossos modelos e também nossos repositórios, mas iremos acessar os repositories que acabamos de construir? Iremos usar a camada de services que será chamada pela chamada de controller. A primeira por sua vez chamará os repositories que criamos. Algo assim:

Controller → Service → Repository

Vamos começar declarando o serviço do customer em src/main/java/com/community/wishlist/repository/CustomerService.java:

@Service
public class CustomerService {
    private final CustomerRepository customerRepository;

    public CustomerService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public Customer create(Customer customer) throws EntityAlreadyExistsException {
        this.findByEmail(customer.getEmail()).orElseThrow(() -> new EntityAlreadyExistsException(""));
        return this.customerRepository.save(customer);
    }

    public Optional<Customer> findById(Long id) {
        return this.customerRepository.findById(id);
    }

    public List<Customer> getAll() {
        return new ArrayList<>(customerRepository.findAll());

    }

    public Customer update(Customer newCustomer, Long id) throws ResourceNotFoundException {
        Optional<Customer> optionalCustomer = this.findById(id);
        Customer customer = optionalCustomer.orElseThrow(() -> new ResourceNotFoundException(""));
        customer.setEmail(newCustomer.getEmail());
        customer.setName(newCustomer.getName());
        return customer;
    }
    
    public void delete(Long id) throws ResourceNotFoundException {
        Optional<Customer> optionalCustomer = this.findById(id);
        Customer customer = optionalCustomer.orElseThrow(() -> new ResourceNotFoundException(""));
        this.customerRepository.delete(customer);
    }

    public Optional<Customer> findByEmail(String email) {
        return this.customerRepository.findByEmail(email);
    }
}

Note que estamos levantando as nossas exceções criadas caso o erro específico aconteça (new ResourceNotFoundException("") e new ResourceNotFoundException("")). Definimos no arquivo acima o CRUD da aplicação em nosso serviço. Em cada um dos métodos os serviço chama o repository para realizar as ações no banco de dados.

Vamos as definições das anotações que usamos:

  • @Service - Essa notação indica que a classe faz operações baseadas na regra de negócio da aplicação. O spring automaticamente irá detectá-la quando fizer o classpath scanning.
  • @Transactional - Indica que todo o método deve ser executado de forma atômica.

Em resumo o que fizemos:

1- Declaramos o customerRepository que será usado para as operações do banco de dados.

2- Declaramos o construtor da classe com o customerRepository como argumento. Isso é importante para Spring Boot injetar essa dependência na classe.

3- Declaramos as funções:

  • create: cria um customer. É o famoso cadastre-se nos sites e plataformas.
  • readById: irá procurar o customer pelo seu id e retorná-lo.
  • update: irá atualizar o email e nome do customer. O newCustomer como argumento é o objeto que foi enviado pelo controller.
  • delete: deleta o customer do banco de dados.

Cada uma delas chama o repository e realiza as operações no banco de dados.

Vamos declarar também o ProductService:

@Service
public class ProductService {
    private final ProductRepository productRepository;

    public ProductService(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    public Product create(Product product) {
        return this.productRepository.save(product);
    }

    public Optional<Product> findById(Long id) {
        return this.productRepository.findById(id);
    }

    public List<Product> getAll() {
        return new ArrayList<>(productRepository.findAll());

    }

    public Product update(Product newProduct, Long id) throws ResourceNotFoundException {
        Optional<Product> optionalProduct = this.findById(id);
        Product product = optionalProduct.orElseThrow(() -> new ResourceNotFoundException(""));
        product.setTitle(newProduct.getTitle());
        product.setPrice(newProduct.getPrice());
        return product;
    }

    @Transactional
    public void delete(Long id) throws ResourceNotFoundException {
        Optional<Product> optionalProduct = this.findById(id);
        Product product = optionalProduct.orElseThrow(() -> new ResourceNotFoundException(""));
        this.productRepository.delete(product);
    }

}

E agora o WishlistService:

@Service
public class WishlistService {
    private final WishlistRepository wishlistRepository;

    public WishlistService(WishlistRepository wishlistRepository) {
        this.wishlistRepository = wishlistRepository;
    }
    public Wishlist create(Wishlist wishlist) {
        return this.wishlistRepository.save(wishlist);
    }

    public Optional<Wishlist> findByCustomerId(Long customerId) {
        return this.wishlistRepository.findByCustomerId(customerId);
    }

    public Wishlist addProduct(Set<Product> newProducts, Long id) throws ResourceNotFoundException {
        Wishlist wishlist = this.wishlistRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException(""));
        wishlist.getProducts().addAll(newProducts);
        return wishlist;
    }

    public void delete(Long id) {
        Wishlist wishlist = this.wishlistRepository.getOne(id);
        this.wishlistRepository.delete(wishlist);
    }
}

Controllers: a exposição das rotas da aplicação

Como ao executar um PUT para localhost:8080/wishlist/1 como seriam feitas as adições dos produtos à wishlist? É neste momento que entram os controllers da aplicação. Vamos criar um controller para cada entidade, assim mantemos a separação das urls respeitando o REST framework.

Os próximos arquivos serão criadas em: src/main/java/com/community/wishlist/controller/

Vamos criar o seguinte arquivo e explicarei sobre sua estrutura e anotações em seguida:

public class CustomerController {
    private final CustomerService customerService;

    public CustomerController(CustomerService customerService) {
        this.customerService = customerService;
    }

    @GetMapping
    public List<Customer> getAll() {
        return customerService.getAll();
    }

    @PostMapping
    @Transactional
    public ResponseEntity<Customer> create(@RequestBody @Valid Customer newCustomer, UriComponentsBuilder uriBuilder) throws EntityAlreadyExistsException {
        Customer customer = customerService.create(newCustomer);
        URI uri = uriBuilder.path("/customer/{id}").buildAndExpand(newCustomer.getId()).toUri();
        return ResponseEntity.created(uri).body(customer);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Customer> read(@PathVariable Long id) {
        Optional<Customer> customer = customerService.findById(id);
        return customer.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    @Transactional
    public ResponseEntity<Customer> update(@PathVariable Long id, @RequestBody @Valid Customer newCustomer) throws ResourceNotFoundException {
        Customer customer = customerService.update(newCustomer, id);
        return ResponseEntity.ok(customer);
    }

    @DeleteMapping("/{id}")
    @Transactional
    public ResponseEntity<?> delete(@PathVariable Long id) throws ResourceNotFoundException {
        customerService.delete(id);
        return ResponseEntity.noContent().build();
    }
  • @RestController - Indica que estamos trabalhando com um Controller. Combina as anotações @Controller e @ResponseBody. Indica que esta é a camada que controla as requisições. É direcionado a quem deve respondê-las.
  • @RequestMapping - Infere o mapeamento HTTP para a dada URL. Neste caso esse controller responderá a url: nossoenderecoweb.com.br/customer/
  • @PostMapping, @GetMapping, @PutMapping, @DeleteMapping Indica a requisição usando o dado verbo (get, post, put, delete). Ou seja, ao enviar uma requisição GET para a url nossoenderecoweb.com.br/customer/ o código do método getAll será executado.

Como pode-se perceber no arquivo acima definimos o CRUD completo para a entidade Customer. A anotação faz a ligação da operação HTTP com a função que fará a tratativa dessa requisição e retorna o resultado conforme o esperado.

É importante notar que usamos a camada de service para acessar as informações de nosso modelo. O nosso controller não acessa o banco de dados ou executa regras de negócio. Ele apenas recebe a requisição, chama a camada responsável e retorna o que é necessário.

Agora vamos definir nossos outros controllers:

ProductController.java

public class ProductController {
    private final ProductService productService;
    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<Product> getAll() {
        return productService.getAll();
    }

    @PostMapping
    @Transactional
    public ResponseEntity<Product> create(@RequestBody @Valid Product newProduct, UriComponentsBuilder uriBuilder) {
        Product product = productService.create(newProduct);
        URI uri = uriBuilder.path("/product/{id}").buildAndExpand(newProduct.getId()).toUri();
        return ResponseEntity.created(uri).body(product);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> read(@PathVariable Long id) {
        Optional<Product> product = productService.findById(id);
        return product.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    @Transactional
    public ResponseEntity<Product> update(@PathVariable Long id, @RequestBody @Valid Product newProduct) throws ResourceNotFoundException {
        Product product = productService.update(newProduct, id);
        return ResponseEntity.ok(product);
    }

    @DeleteMapping("/{id}")
    @Transactional
    public ResponseEntity<?> delete(@PathVariable Long id) throws ResourceNotFoundException {
        productService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

WishlistController.java

@RestController
@RequestMapping("/wishlist")
public class WishlistController {
    private final WishlistService wishlistService;

    public WishlistController(WishlistService wishlistService) {
        this.wishlistService = wishlistService;
    }

    @PostMapping
    @Transactional
    public ResponseEntity<Wishlist> create(@RequestBody @Valid Wishlist newWishlist, UriComponentsBuilder uriBuilder) {
        Wishlist wishlist = wishlistService.create(newWishlist);
        URI uri = uriBuilder.path("/wishlist/{id}").buildAndExpand(newWishlist.getId()).toUri();
        return ResponseEntity.created(uri).body(wishlist);
    }

    @GetMapping("/{customerId}")
    public ResponseEntity<Wishlist> read(@PathVariable Long customerId) {
        Optional<Wishlist> wishlist = wishlistService.findByCustomerId(customerId);
        return wishlist.map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PutMapping("/{id}")
    @Transactional
    public ResponseEntity<Wishlist> addProduct(@PathVariable Long id, @RequestBody @Valid Set<Product> newProducts) throws ResourceNotFoundException {
        Wishlist wishlist = wishlistService.addProduct(newProducts, id);
        return ResponseEntity.ok(wishlist);
    }

    @DeleteMapping("/{id}")
    @Transactional
    public ResponseEntity<?> delete(@PathVariable Long id){
        wishlistService.delete(id);
        return ResponseEntity.noContent().build();
    }
}

Algumas configurações adicionais

Precisamos definir algumas configurações para que nossa aplicação rode conforme desejamos. Podemos fazê-las no arquivo src/main/resources/application.properties. Teremos o seguinte:

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url =jdbc:mysql://mysql-service:3306/wishlist?useSSL=false
spring.datasource.username=root
spring.datasource.password=ourpassword

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto=update

spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
logging.level.org.hibernate.SQL= DEBUG
logging.level.org.hibernate.type=TRACE
server.port = 8080

spring.mvc.pathmatch.matching-strategy=ant_path_matcher

Temos acima nossas configurações do banco de dados. Url, username e senha. Como também as configurações do hibernate. Gostaria de destacar, em especial a spring.jpa.hibernate.ddl-auto=update. A palavra chave update fará com que o hibernate compare nossas entidades definidas no package com.community.wishlist.model com o que existe em nosso banco de dados. Ou seja, será essa keyword que habilitará a criação e modificações em nosso banco de dados quando iniciarmos a aplicação

Swagger

Nossa aplicação está pronta para ser executada. Porém caso não definimos nenhuma resposta a ser mostrada em nossa URL raiz ela mostrará a seguinte página:

Vamos adicionar em nosso projeto uma página de documentação que permitirá que vejamos todas as requisições, respostas e entidades da nossa aplicação.

No arquivo pom.xml adicione:

<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger2</artifactId>
   <version>2.9.2</version>
</dependency>
<dependency>
   <groupId>io.springfox</groupId>
   <artifactId>springfox-swagger-ui</artifactId>
   <version>2.9.2</version>
   <scope>compile</scope>
</dependency>

Crie um pacote com.community.wishlist.config.swagger e nele o arquivo SpringFoxConfig.java:

@Configuration
@EnableSwagger2
public class SpringFoxConfig {
@Bean
public Docket productApi() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.community.wishlist"))
                .build()
                .apiInfo(metaData());
    }

    private ApiInfo metaData() {
        return new ApiInfoBuilder()
                .title("Spring Boot REST API")
                .description("\\"Spring Boot REST API\\"")
                .version("1.0.0")
                .license("Apache License Version 2.0")
                .licenseUrl("<https://www.apache.org/licenses/LICENSE-2.0\\>"")
                .build();
    }
}

Esse arquivo irá fazer todo o wiring com o spring para mostrar nossa documentação que será gerada automaticamente. Agora quando acessarmos: http://localhost:8080/swagger-ui.html com o server up veremos a seguinte página:

Mas afinal, o que são containers!?

Pode-se pensar em container como um lugar isolado que sua aplicação roda. Quase como um sistema operacional que serve apenas para executar sua aplicação. É importante notar que esse “sistema operacional” contém apenas os binários e dependências necessárias para executar a sua aplicação, o que o torna extremamente leve. Os containers compartilham o mesmo kernel que os outros e podem ser iniciadas em questão de segundos.

Por esse ambiente ser isolado facilita que seu software seja enviado a sua infra com todas as dependências necessárias que irão ser exatamente as mesmas (em versões e libs por exemplo). Isso diminui a fricção dos ambientes como dev, QA e prod, aquele velho problema de: na minha máquina funciona! Com o container todas as máquinas terão exatamente o mesmo ambiente, com as mesmas dependências e binários com as mesmas versões. É como se agora sempre fosse funcionar porque de forma simplificada é uma simulação da mesma máquina que pode ser executada em qualquer lugar: no seu computador, do seu colega de trabalho ou no cloud da empresa.

Dockerizando nossa aplicação

Certo, agora vamos inserir nossos arquivos para que a aplicação fique conteinerizada. Serão dois:

Dockerfile: contém todas as instruções necessárias para montar a nossa imagem. Quando a build for realizada todos os comandos deste arquivo serão executados automaticamente.

# Definição de build para a imagem do Spring boot
FROM openjdk:8-jdk-alpine as build

WORKDIR /app

COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .

RUN chmod +x ./mvnw
# Faça o download das dependencias do pom.xml
RUN ./mvnw dependency:go-offline -B

COPY src src

RUN ./mvnw package -DskipTests
RUN mkdir -p target/dependency && (cd target/dependency; jar -xf ../*.jar)

# Definição de produção para a imagem do Spring boot
FROM openjdk:8-jre-alpine as production
ARG DEPENDENCY=/app/target/dependency

# Copiar as dependencias para o build artifact
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app

# Rodar a aplicação Spring boot
ENTRYPOINT ["java", "-cp", "app:app/lib/*","com.community.wishlist.WishlistApplication"]

docker-compose.yml nosso arquivo de inicialização do docker-compose. É importante notar que esse simples arquivo é nosso orquestrador de containers.

# spring-boot-docker/docker-compose.yml
version: "3.7"
services:
  mysql-service:
    image: mysql:5.7
    networks:
      - spring-boot-mysql-network
    restart: always
    environment:
      - MYSQL_ROOT_PASSWORD=ourpassword
      - MYSQL_DATABASE=wishlist
  web-service:
    build:
      context: ./
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    networks:
      - spring-boot-mysql-network
    depends_on:
      - mysql-service
networks:
  spring-boot-mysql-network:
    driver: bridge

Note que em nosso arquivo acima definimos dois serviços

  • mysql-service: nosso banco de dados mySQL que o hibernate usará. Em environment usamos as mesmas credenciais do arquivo application.properties
  • web-service: nossa API rest Spring boot Ela depende que o serviço do mysql esteja rodando antes dela.

Também temos a declaração e uso nos serviços de nossa network que será a rede interna do compose. Note que na url do bando de dados em application.properties é formada por mysql-service. Essa configuração é feita pela network acima.

Ambos serão criados na pasta raiz da aplicação.

Agora execute em seu terminal o comando docker-compose up e você verá toda nossa aplicação e banco de dados subindo:

Agora temos nossa aplicação funcional! Você pode ir a página do Swagger e tentar executar algumas operações.

Esse é nossa file tree final:

E isso é tudo!

Você acabou de construir uma API em Java Spring boot do zero! Temos todas as nossas rotas funcionando para as entidades e o cliente vai ficar feliz que conseguimos cumprir todos os requisitos. O sistema de wishlist vai permitir que os clientes guardem seus produtos desejados e possam voltar depois para comprá-los!

Claro que do lado técnico poderíamos ter feito bem mais coisas, mas esse tutorial ficaria ainda mais longo. Uma lista de possíveis coisas para você tentar implementar:

  • Testes unitários,
  • Troca de senha por e-mail para o customer usando o spring-boot-starter-mail,
  • Verificação de senha segura no cadastro do customer,
  • Uso de DTO e form classes para customizar os campos recebidos e enviados como resposta às requisições,
  • Rotas que necessitam de autenticação. No momento todas as nossas rotas são públicas,
  • Imagine que poderíamos receber algumas informações de uma API já existente do nosso cliente, como os produtos. E aí, como fazer para criar um serviço que irá acessar essa outra API para usarmos em nossa?
  • Múltiplas wishlists. No momento o usuário pode ter apenas uma.

Espero que você se divirta tentando as sugestões acima. Caso queira ver o código completo assim ou apenas baixar e executar ele com um docker-compose up.