Executando verificação de segurança...
1

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)));
    }
}
Carregando publicação patrocinada...