Utilizando MapStruct
Demonstrar o uso da biblioteca MapStruct
Repositórios:
https://bitbucket.org/jorge_rabello/anyapi/src/stable/
https://bitbucket.org/jorge_rabello/mapstructdemo/src
Caso de Uso 1:
Transformar um DTO em entidade
Transformar uma entidade em dto
Transformar um DTO em entidade
Temos nossa aplicação e nela podemos ver que já existe: - Um controller - Um service - Um repositório - A entidade Customer - Um exception handler - Migrations com FlyWay
Porém no controller estamos retornando a entidade Customer e isso é extremamente prejudicial para a aplicação:
- Exposição de informações sensíveis
- Acoplamento excessivo
- Overfetching e underfetching de dados
- Versionamento e evolução
Mudando para DTO's
O que queremos é no lugar de retornar a entidade Customer
retornar CustomerDTO
porém pra isso vamos
precisar de algo que transforme o DTO em entidade para salvarmos no banco de dados e depois transforme
a entidade em DTO para retornarmos.
Pra isso vamos utilizar a biblioteca MapStruct
A primeira coisa a se fazer é a declaração da lib no build.gradle
da seguinte forma:
dependencies {
. . .
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}
Quando você adiciona uma dependência como annotationProcessor
, está dizendo ao Gradle que essa dependência contém processadores de anotações.
Processadores de anotações são ferramentas que examinam o código-fonte em busca de anotações específicas e podem gerar código adicional com base nessas anotações.
A lib Lombok
se utiliza dessa mesma técnica.
O próximo passo será criarmos o dto CustomerDTO, pra isso vamos utilizar um record
do Java 17.
classDiagram
class CustomerDTO {
+Long id
+String name
+String legalPersonDocument
+String naturalPersonDocument
}
package br.com.sharebook.mapstructdemo.model.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
@Builder(toBuilder = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public record CustomerDTO(
Long id,
String name,
String legalPersonDocument,
String naturalPersonDocument
) {
}
A seguir vamos criar o nosso mapper do MapStruct que será uma interface
package br.com.sharebook.mapstructdemo.model.mapper;
import br.com.sharebook.mapstructdemo.model.dto.CustomerDTO;
import br.com.sharebook.mapstructdemo.model.entity.Customer;
import org.mapstruct.Mapper;
import org.mapstruct.ReportingPolicy;
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface CustomerMapper {
Customer toEntity(CustomerDTO dto);
CustomerDTO toDTO(Customer entity);
}
Por fim vamos alterar o controller para utilizar o CustomerDTO
package br.com.sharebook.mapstructdemo.controller;
import br.com.sharebook.mapstructdemo.model.dto.CustomerDTO;
import br.com.sharebook.mapstructdemo.model.entity.Customer;
import br.com.sharebook.mapstructdemo.service.CustomerService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("customers")
public class CustomersController {
private final CustomerService service;
@PostMapping
public ResponseEntity<CustomerDTO> save(@RequestBody CustomerDTO dto) {
return new ResponseEntity<>(service.save(dto), HttpStatus.CREATED);
}
}
E alterar o service, aqui vamos injetar o mapper
private final CustomerMapper mapper;
Transformar o dto em entity
Customer customer = mapper.toEntity(dto);
Salvar os dados no banco de dados
Customer savedCustomer = repository.save(customer);
Transformar a entidade em dto
CustomerDTO customerDTO = mapper.toDTO(savedCustomer);
E retornar o DTO
return customerDTO;
O código do método save()
ficaria mais ou menos assim:
public CustomerDTO save(CustomerDTO dto) {
Customer customer = mapper.toEntity(dto);
Customer savedCustomer = repository.save(customer);
CustomerDTO customerDTO = mapper.toDTO(savedCustomer);
return customerDTO;
}
Porém podemos passar as chamadas como argumentos dos métodos reduzindo a verbosidade deixando assim
public CustomerDTO save(CustomerDTO dto) {
return mapper.toDTO(repository.save(mapper.toEntity(dto)));
}
Caso de Uso 2
Fazer a integração com uma API de terceiros
Para o segundo caso de uso imagina que temos de integrar com uma api de terceiros para validar o cliente.
Porém o nosso contrato (DTO) tem o seguinte formato:
Para pessoa física:
{
"name": "Yusuke Urameshi",
"naturalPersonDocument": "86869763011"
}
Para pessoa jurídica:
{
"name": "Haja Saco Ltda-ME",
"legalPersonDocument": "36288336000149"
}
E a api que vamos integrar tem o seguinte contrato:
{
"name": "Barry Allen",
"federalDocument": "3115551261060"
}
Tanto para PF quanto pra PJ, sendo que federalDocument
pode receber um CPF ou CNPJ para pessoa física ou jurídica respectivamente.
Assim sendo vamos começar:
O primeiro passo será criar as classes que vamos utilizar como modelos de request e response para a api:
classDiagram
class PersonAPIRequest {
+String name
+String federalDocument
}
package br.com.sharebook.mapstructdemo.model.request;
import lombok.Builder;
@Builder(toBuilder = true)
public record PersonAPIRequest(
String name,
String federalDocument) {
}
E
classDiagram
class PersonAPIResponse {
+Long id
+String name
+String federalDocument
}
package br.com.sharebook.mapstructdemo.model.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Builder;
@Builder(toBuilder = true)
@JsonIgnoreProperties(ignoreUnknown = true)
public record PersonAPIResponse(
Long id,
String name,
String federalDocument
) {
}
Agora vamos criar a request para a API utilizando a classe RestClient
nesse classe vamos fazer o seguinte:
Configurar a url para acessar a api
CUIDADO:
Aqui ao importar @Value
tome cuidado para não importar a anotação do pacote lombok.Value
o pacote correto deve ser import org.springframework.beans.factory.annotation.Value;
@Value("${spring.application.parameters.person-api.create-person.url}")
private String url;
Injetar o CustomerMapper
e o ObjectMapper
private final CustomerMapper mapper;
private final ObjectMapper objectMapper;
Criar o método que faz a request
public PersonAPIResponse create(CustomerDTO dto) {
return RestClient.create(url)
.post()
.headers(h -> h.setContentType(MediaType.APPLICATION_JSON))
.body(toJSON(mapper.toPersonAPIRequest(dto)))
.exchange((req, res) -> {
if (res.getStatusCode().isError()) {
log.error("Got an error while creating person with status code {}", res.getStatusCode());
throw new CreatePersonException();
}
log.info("Received a successfull create person response with status code {}", res.getStatusCode());
return res.bodyTo(PersonAPIResponse.class);
});
}
Criar um método auxiliar para converter o body para JSON
private <T> String toJSON(T t) {
try {
return objectMapper.writeValueAsString(t);
} catch (JsonProcessingException e) {
log.error("Failed to serialize {}", t, e);
throw new RuntimeException(e);
}
}
O código inteiro ficará assim:
package br.com.sharebook.mapstructdemo.request;
import br.com.sharebook.mapstructdemo.model.dto.CustomerDTO;
import br.com.sharebook.mapstructdemo.model.mapper.CustomerMapper;
import br.com.sharebook.mapstructdemo.model.response.PersonAPIResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestClient;
@Slf4j
@Component
@RequiredArgsConstructor
public class PersonRequest {
@Value("${spring.application.parameters.person-api.create-person.url}")
private String url;
private final CustomerMapper mapper;
private final ObjectMapper objectMapper;
public PersonAPIResponse create(CustomerDTO dto) {
return RestClient.create(url)
.post()
.headers(h -> h.setContentType(MediaType.APPLICATION_JSON))
.body(toJSON(mapper.toPersonAPIRequest(dto)))
.exchange((req, res) -> {
if (res.getStatusCode().isError()) {
log.error("Got an error while creating person with status code {}", res.getStatusCode());
throw new CreatePersonException();
}
log.info("Received a successfull create person response with status code {}", res.getStatusCode());
return res.bodyTo(PersonAPIResponse.class);
});
}
private <T> String toJSON(T t) {
try {
return objectMapper.writeValueAsString(t);
} catch (JsonProcessingException e) {
log.error("Failed to serialize {}", t, e);
throw new RuntimeException(e);
}
}
}
Vamos criar a classe de exception
package br.com.sharebook.mapstructdemo.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class CreatePersonException extends RuntimeException {
}
Também vamos criar a url para acessar a api que vamos substituir por esse placeholder
@Value("${spring.application.parameters.person-api.create-person.url}")
private String url;
spring:
application:
name: mapstructdemo
parameters:
person-api:
create-person:
url: "${PERSON_URL:http://localhost:8081/person}"
Assim nosso application.yaml
ficará da seguinte forma:
spring:
application:
name: mapstructdemo
parameters:
person-api:
create-person:
url: "${PERSON_URL:http://localhost:8081/person}"
main:
allow-bean-definition-overriding: true
flyway:
locations: "classpath:db/migration"
validate-on-migrate: false
datasource:
driver-class-name: "com.mysql.cj.jdbc.Driver"
url: "${MYSQL_URL:jdbc:mysql://localhost:3306/db?createDatabaseIfNotExist=true}"
username: "${MYSQL_USERNAME:admin}"
password: "${MYSQL_PASSWORD:root}"
jpa:
hibernate:
ddl-auto: none
show-sql: false
database-platform: "org.hibernate.dialect.MySQL8Dialect"
springdoc:
swagger-ui:
path: /swagger-ui
api-docs:
path: /api-docs
server:
error:
include-message: always
management:
endpoint:
health:
probes:
enabled: true
Por fim vamos dizer ao mapstruct como fazer o mapeamento dos campos criando no mapper o método toPersonAPIRequest()
@Mapping(target = "federalDocument", source = ".", qualifiedByName = "toFederalDocument")
PersonAPIRequest toPersonAPIRequest(CustomerDTO dto);
Também vamos precisar criar um método para que use o campo naturalPersonDocument
ou legalPersonDocument
dependendo de qual está presente.
@Named("toFederalDocument")
default String toFederalDocument(CustomerDTO dto) {
return ofNullable(dto.naturalPersonDocument()).orElseGet(dto::legalPersonDocument);
}
Por fim agora a api deve responder com o seguinte contrato
{
"id": 1,
"name": "John Rambo",
"federalDocument": 36421147844
}
Agora precisamos mapear esse response body para o nosso CustomerDTO
Então vamos criar no mapper mais um método pra fazer isso:
@Mapping(target = "legalPersonDocument", source = "federalDocument", qualifiedByName = "toLegalPersonDocument")
@Mapping(target = "naturalPersonDocument", source = "federalDocument", qualifiedByName = "toNaturalPersonDocument")
CustomerDTO toCustomerDTO(PersonAPIResponse response);
E vamos criar os métodos auxiliares
@Named("toLegalPersonDocument")
default String toLegalPersonDocument(String federalDocument) {
if (isCNPJ(federalDocument)) {
return federalDocument;
}
return null;
}
@Named("toNaturalPersonDocument")
default String toNaturalPersonDocument(String federalDocument) {
if (isCPF(federalDocument)) {
return federalDocument;
}
return null;
}
default boolean isCPF(String federalDocument) {
return federalDocument != null && federalDocument.length() == 11;
}
default boolean isCNPJ(String federalDocument) {
return federalDocument != null && federalDocument.length() == 14;
}
Por fim vamos utilizar o mapper na classe de service:
@Slf4j
@Service
@RequiredArgsConstructor
public class CustomerService {
private final CustomerRepository repository;
private final CustomerMapper mapper;
private final PersonRequest personRequest;
public CustomerDTO save(CustomerDTO dto) {
PersonAPIResponse response = personRequest.create(dto);
CustomerDTO customerDTO = mapper.toCustomerDTO(response);
return mapper.toDTO(repository.save(mapper.toEntity(customerDTO)));
}
}