Guia rápido do MapStruct

Guia rápido do MapStruct

1. Visão geral

Neste artigo, exploraremos o uso deMapStruct, que é, simplesmente, um mapeador Java Bean.

Essa API contém funções que são mapeadas automaticamente entre dois Java Beans. Com o MapStruct, precisamos criar apenas a interface, e a biblioteca criará automaticamente uma implementação concreta durante o tempo de compilação.

2. MapStruct e Transfer Object Pattern

Para a maioria dos aplicativos, você notará muitos códigos padronizados convertendo POJOs para outros POJOs.

Por exemplo, um tipo comum de conversão ocorre entre entidades suportadas por persistência e DTOs que saem para o lado do cliente.

Portanto, esse é o problema que MapStruct resolve criar manualmente mapeadores de bean consome muito tempo. A bibliotecacan generate bean mapper classes automatically.

3. Maven

Vamos adicionar a dependência abaixo em nosso Mavenpom.xml:


    org.mapstruct
    mapstruct-jdk8
    1.3.0.Beta2

A última versão estável deMapstruct e seuprocessor estão disponíveis no Repositório Central Maven.

Vamos também adicionar a seçãoannotationProcessorPaths à parte de configuração do pluginmaven-compiler-plugin.

Omapstruct-processor é usado para gerar a implementação do mapeador durante a construção:


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
        
    

4. Mapeamento Básico

4.1. Criando um POJO

Vamos primeiro criar um Java POJO simples:

public class SimpleSource {
    private String name;
    private String description;
    // getters and setters
}

public class SimpleDestination {
    private String name;
    private String description;
    // getters and setters
}

4.2. A interface do mapeador

@Mapper
public interface SimpleSourceDestinationMapper {
    SimpleDestination sourceToDestination(SimpleSource source);
    SimpleSource destinationToSource(SimpleDestination destination);
}

Observe que não criamos uma classe de implementação para nossoSimpleSourceDestinationMapper - porque MapStruct a cria para nós.

4.3. O Novo Mapeador

Podemos acionar o processamento MapStruct executando ummvn clean install.

Isso irá gerar a classe de implementação em/target/generated-sources/annotations/.

Aqui está a classe que o MapStruct cria automaticamente para nós:

public class SimpleSourceDestinationMapperImpl
  implements SimpleSourceDestinationMapper {
    @Override
    public SimpleDestination sourceToDestination(SimpleSource source) {
        if ( source == null ) {
            return null;
        }
        SimpleDestination simpleDestination = new SimpleDestination();
        simpleDestination.setName( source.getName() );
        simpleDestination.setDescription( source.getDescription() );
        return simpleDestination;
    }
    @Override
    public SimpleSource destinationToSource(SimpleDestination destination){
        if ( destination == null ) {
            return null;
        }
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName( destination.getName() );
        simpleSource.setDescription( destination.getDescription() );
        return simpleSource;
    }
}

4.4. Um caso de teste

Finalmente, com tudo gerado, vamos escrever um caso de teste que mostrará que os valores emSimpleSource correspondem aos valores emSimpleDestination.

public class SimpleSourceDestinationMapperIntegrationTest {
    private SimpleSourceDestinationMapper mapper
      = Mappers.getMapper(SimpleSourceDestinationMapper.class);
    @Test
    public void givenSourceToDestination_whenMaps_thenCorrect() {
        SimpleSource simpleSource = new SimpleSource();
        simpleSource.setName("SourceName");
        simpleSource.setDescription("SourceDescription");
        SimpleDestination destination = mapper.sourceToDestination(simpleSource);

        assertEquals(simpleSource.getName(), destination.getName());
        assertEquals(simpleSource.getDescription(),
          destination.getDescription());
    }
    @Test
    public void givenDestinationToSource_whenMaps_thenCorrect() {
        SimpleDestination destination = new SimpleDestination();
        destination.setName("DestinationName");
        destination.setDescription("DestinationDescription");
        SimpleSource source = mapper.destinationToSource(destination);
        assertEquals(destination.getName(), source.getName());
        assertEquals(destination.getDescription(),
          source.getDescription());
    }
}

5. Mapeamento com injeção de dependência

A seguir, vamos obter uma instância de um mapeador em MapStruct simplesmente chamandoMappers.getMapper(YourClass.class).

Obviamente, essa é uma maneira muito manual de obter a instância - uma alternativa muito melhor seria injetar o mapeador diretamente onde precisamos (se nosso projeto usar qualquer solução de injeção de dependência).

Luckily MapStruct has solid support for both Spring and CDI (Contexts and Dependency Injection).

Para usar Spring IoC em nosso mapeador, precisamos adicionarcomponentModelattribute a@Mapper com o valorspringe CDI seriacdi.

5.1. Modifique o Mapeador

Adicione o seguinte código aSimpleSourceDestinationMapper:

@Mapper(componentModel = "spring")
public interface SimpleSourceDestinationMapper

6. Mapeando campos com diferentes nomes de campo

Do nosso exemplo anterior, o MapStruct conseguiu mapear nossos beans automaticamente porque eles têm o mesmo nome de campo. E se um bean que estamos prestes a mapear tiver um nome de campo diferente?

Para nosso exemplo, estaremos criando um novo bean chamadoEmployeeeEmployeeDTO.

6.1. Novos POJOs

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    // getters and setters
}
public class Employee {
    private int id;
    private String name;
    // getters and setters
}

6.2. A interface do mapeador

Ao mapear diferentes nomes de campo, precisaremos configurar seu campo de origem para seu campo de destino e para fazer isso precisaremos adicionar a anotação@Mappings. Esta anotação aceita um array de anotações@Mapping que usaremos para adicionar o atributo target e source.

No MapStruct, também podemos usar a notação de ponto para definir um membro de um bean:

@Mapper
public interface EmployeeMapper {
    @Mappings({
      @Mapping(target="employeeId", source="entity.id"),
      @Mapping(target="employeeName", source="entity.name")
    })
    EmployeeDTO employeeToEmployeeDTO(Employee entity);
    @Mappings({
      @Mapping(target="id", source="dto.employeeId"),
      @Mapping(target="name", source="dto.employeeName")
    })
    Employee employeeDTOtoEmployee(EmployeeDTO dto);
}

6.3. O Caso de Teste

Novamente, precisamos testar se os valores dos objetos de origem e de destino correspondem:

@Test
public void givenEmployeeDTOwithDiffNametoEmployee_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeId(1);
    dto.setEmployeeName("John");

    Employee entity = mapper.employeeDTOtoEmployee(dto);

    assertEquals(dto.getEmployeeId(), entity.getId());
    assertEquals(dto.getEmployeeName(), entity.getName());
}

Mais casos de teste podem ser encontrados emGithub project.

7. Mapeando Feijões com Feijões Filhos

A seguir, mostraremos como mapear um bean com referências a outros beans.

7.1. Modifique o POJO

Vamos adicionar uma nova referência de bean ao objetoEmployee:

public class EmployeeDTO {
    private int employeeId;
    private String employeeName;
    private DivisionDTO division;
    // getters and setters omitted
}
public class Employee {
    private int id;
    private String name;
    private Division division;
    // getters and setters omitted
}
public class Division {
    private int id;
    private String name;
    // default constructor, getters and setters omitted
}

7.2. Modifique o Mapeador

Aqui, precisamos adicionar um método para converterDivision emDivisionDTO e vice-versa; se MapStruct detectar que o tipo de objeto precisa ser convertido e o método para converter existir na mesma classe, ele o usará automaticamente.

Vamos adicionar isso ao mapeador:

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

7.3. Modifique o caso de teste

Vamos modificar e adicionar alguns casos de teste ao existente:

@Test
public void givenEmpDTONestedMappingToEmp_whenMaps_thenCorrect() {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setDivision(new DivisionDTO(1, "Division1"));
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    assertEquals(dto.getDivision().getId(),
      entity.getDivision().getId());
    assertEquals(dto.getDivision().getName(),
      entity.getDivision().getName());
}

8. Mapeamento com conversão de tipo

MapStruct também oferece algumas conversões de tipo implícitas prontas e, para nosso exemplo, tentaremos converter uma data String em um objetoDate real.

Para obter mais detalhes sobre a conversão implícita de tipo, você pode ler oMapStruct reference guide.

8.1. Modifique os Feijões

Adicione uma data de início para nosso funcionário:

public class Employee {
    // other fields
    private Date startDt;
    // getters and setters
}
public class EmployeeDTO {
    // other fields
    private String employeeStartDt;
    // getters and setters
}

8.2. Modifique o Mapeador

Modifique o mapeador e forneçadateFormat para nossa data de início:

@Mappings({
  @Mapping(target="employeeId", source = "entity.id"),
  @Mapping(target="employeeName", source = "entity.name"),
  @Mapping(target="employeeStartDt", source = "entity.startDt",
           dateFormat = "dd-MM-yyyy HH:mm:ss")})
EmployeeDTO employeeToEmployeeDTO(Employee entity);
@Mappings({
  @Mapping(target="id", source="dto.employeeId"),
  @Mapping(target="name", source="dto.employeeName"),
  @Mapping(target="startDt", source="dto.employeeStartDt",
           dateFormat="dd-MM-yyyy HH:mm:ss")})
Employee employeeDTOtoEmployee(EmployeeDTO dto);

8.3. Modifique o caso de teste

Vamos adicionar mais alguns casos de teste para verificar se a conversão está correta:

private static final String DATE_FORMAT = "dd-MM-yyyy HH:mm:ss";
@Test
public void givenEmpStartDtMappingToEmpDTO_whenMaps_thenCorrect() throws ParseException {
    Employee entity = new Employee();
    entity.setStartDt(new Date());
    EmployeeDTO dto = mapper.employeeToEmployeeDTO(entity);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}
@Test
public void givenEmpDTOStartDtMappingToEmp_whenMaps_thenCorrect() throws ParseException {
    EmployeeDTO dto = new EmployeeDTO();
    dto.setEmployeeStartDt("01-04-2016 01:00:00");
    Employee entity = mapper.employeeDTOtoEmployee(dto);
    SimpleDateFormat format = new SimpleDateFormat(DATE_FORMAT);

    assertEquals(format.parse(dto.getEmployeeStartDt()).toString(),
      entity.getStartDt().toString());
}

9. Mapeamento com uma classe abstrata

Às vezes, podemos querer personalizar nosso mapeador de uma maneira que exceda os recursos do @Mapping.

Por exemplo, além da conversão de tipo, podemos transformar os valores de alguma forma, como no nosso exemplo abaixo.

Nesse caso, podemos criar uma classe abstrata e implementar métodos que queremos personalizar e deixar abstratos aqueles que devem ser gerados pelo MapStruct.

9.1. Modelo básico

Neste exemplo, usaremos a seguinte classe:

public class Transaction {
    private Long id;
    private String uuid = UUID.randomUUID().toString();
    private BigDecimal total;

    //standard getters
}

e um DTO correspondente:

public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    // standard getters and setters
}

A parte complicada aqui é converter a amostraBigDecimaltotalde dólares emLong totalInCents.

9.2. Definindo um Mapeador

Podemos conseguir isso criando nossoMapper  como uma classe abstrata:

@Mapper
abstract class TransactionMapper {

    public TransactionDTO toTransactionDTO(Transaction transaction) {
        TransactionDTO transactionDTO = new TransactionDTO();
        transactionDTO.setUuid(transaction.getUuid());
        transactionDTO.setTotalInCents(transaction.getTotal()
          .multiply(new BigDecimal("100")).longValue());
        return transactionDTO;
    }

    public abstract List toTransactionDTO(
      Collection transactions);
}

Aqui, implementamos nosso método de mapeamento totalmente personalizado para uma única conversão de objeto.

Por outro lado, deixamos o método que pretende mapearCollectionto aListabstract, entãoMapStruct irá implementá-lo para nós.

9.3. Resultado Gerado

Como já implementamos o método para mapearTransactioninto aTransactionDTO único, espera-se queMapstructuse no segundo método. O seguinte será gerado:

@Generated
class TransactionMapperImpl extends TransactionMapper {

    @Override
    public List toTransactionDTO(Collection transactions) {
        if ( transactions == null ) {
            return null;
        }

        List list = new ArrayList<>();
        for ( Transaction transaction : transactions ) {
            list.add( toTransactionDTO( transaction ) );
        }

        return list;
    }
}

Como podemos ver na linha 12,MapStruct ustenta nossa implementação no método que gerou.

10. Anotações de antes e depois do mapeamento

Esta é outra maneira de personalizar os recursos de@Mapping usando as anotações@BeforeMappinge@AfterMapping. The annotations are used to mark methods that are invoked right before and after the mapping logic.

Eles são bastante úteis em cenários onde podemos querer estebehavior to be applied to all mapped super-types.

Vamos dar uma olhada em um exemplo que mapeia os subtipos deCar;ElectricCar,eBioDieselCar, paraCarDTO.

Durante o mapeamento, gostaríamos de mapear a noção de tipos para o campo enumFuelType no DTO, e depois que o mapeamento for feito, gostaríamos de alterar o nome do DTO para maiúsculas.

10.1. Modelo básico

Neste exemplo, usaremos as seguintes classes:

public class Car {
    private int id;
    private String name;
}

Subtipos deCar:

public class BioDieselCar extends Car {
}
public class ElectricCar extends Car {
}

OCarDTO com um tipo de campo enumFuelType:

public class CarDTO {
    private int id;
    private String name;
    private FuelType fuelType;
}
public enum FuelType {
    ELECTRIC, BIO_DIESEL
}
10.2. Definindo o Mapeador

Agora vamos prosseguir e escrever nossa classe de mapeador abstrato, que mapeiaCar paraCarDTO:

@Mapper
public abstract class CarsMapper {
    @BeforeMapping
    protected void enrichDTOWithFuelType(Car car, @MappingTarget CarDTO carDto) {
        if (car instanceof ElectricCar) {
            carDto.setFuelType(FuelType.ELECTRIC);
        }
        if (car instanceof BioDieselCar) {
            carDto.setFuelType(FuelType.BIO_DIESEL);
        }
    }

    @AfterMapping
    protected void convertNameToUpperCase(@MappingTarget CarDTO carDto) {
        carDto.setName(carDto.getName().toUpperCase());
    }

    public abstract CarDTO toCarDto(Car car);
}

@MappingTarget é uma anotação de parâmetro quepopulates the target mapping DTO right before the mapping logic is executedin case of @BeforeMappingeright after in case of @AfterMapping método anotado.

10.3. Resultado

OCarsMapper defined above generatestheimplementation:

@Generated
public class CarsMapperImpl extends CarsMapper {

    @Override
    public CarDTO toCarDto(Car car) {
        if (car == null) {
            return null;
        }

        CarDTO carDTO = new CarDTO();

        enrichDTOWithFuelType(car, carDTO);

        carDTO.setId(car.getId());
        carDTO.setName(car.getName());

        convertNameToUpperCase(carDTO);

        return carDTO;
    }
}

Observe comothe annotated methods invocations surround the mapping logic na implementação.

11. Suporte para Lombok

Na versão recente do MapStruct, o suporte ao Lombok foi anunciado. So we can easily map a source entity and a destination using Lombok. 

Para ativar o suporte ao Lombok, precisamos adicionarthe dependency no caminho do processador de anotação. Então agora temos o processador mapstruct e o Lombok no plug-in do compilador Maven:


    org.apache.maven.plugins
    maven-compiler-plugin
    3.5.1
    
        1.8
        1.8
        
            
                org.mapstruct
                mapstruct-processor
                1.3.0.Beta2
            
            
                org.projectlombok
                lombok
            1.18.4
            
        
    

Vamos definir a entidade de origem usando anotações do Lombok:

@Getter
@Setter
public class Car {
    private int id;
    private String name;
}

E o objeto de transferência de dados de destino:

@Getter
@Setter
public class CarDTO {
    private int id;
    private String name;
}

A interface do mapeador para isso permanece semelhante ao nosso exemplo anterior:

@Mapper
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
    CarDTO carToCarDTO(Car car);
}

12. Suporte paradefaultExpression

A partir da versão 1.3.0,we can use the defaultExpression attribute of the @Mapping annotation to specify an expression that determines the value of the destination field if the source field is null. Isso é um acréscimo à funcionalidade de atributodefaultValue existente.

A entidade de origem:

public class Person {
    private int id;
    private String name;
}

O objeto de transferência de dados de destino:

public class PersonDTO {
    private int id;
    private String name;
}

Se o campoid da entidade de origem fornull,, queremos gerar umid aleatório e atribuí-lo ao destino mantendo outros valores de propriedade como estão:

@Mapper
public interface PersonMapper {
    PersonMapper INSTANCE = Mappers.getMapper(PersonMapper.class);

    @Mapping(target = "id", source = "person.id",
      defaultExpression = "java(java.util.UUID.randomUUID().toString())")
    PersonDTO personToPersonDTO(Person person);
}

Vamos adicionar um caso de teste para verificar a execução da expressão:

@Test
public void givenPersonEntitytoPersonWithExpression_whenMaps_thenCorrect()
    Person entity  = new Person();
    entity.setName("Micheal");
    PersonDTO personDto = PersonMapper.INSTANCE.personToPersonDTO(entity);
    assertNull(entity.getId());
    assertNotNull(personDto.getId());
    assertEquals(personDto.getName(), entity.getName());
}

13. Conclusão

Este artigo forneceu uma introdução ao MapStruct. Apresentamos a maioria dos fundamentos da biblioteca de mapeamento e como usá-la em nossos aplicativos.

A implementação desses exemplos e testes pode ser encontrada no projetoGithub. Como é um projeto do Maven, deve ser fácil importar e executar como está.