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
- 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.