Guide rapide de MapStruct

Guide rapide de MapStruct

1. Vue d'ensemble

Dans cet article, nous allons explorer l'utilisation deMapStruct qui est, en termes simples, un mappeur Java Bean.

Cette API contient des fonctions qui mappent automatiquement entre deux Java Beans. Avec MapStruct, il suffit de créer l'interface, et la bibliothèque créera automatiquement une implémentation concrète lors de la compilation.

2. MapStruct et modèle d'objet de transfert

Dans la plupart des applications, vous remarquerez beaucoup de code standard qui convertit les POJO en d’autres POJO.

Par exemple, un type de conversion courant se produit entre des entités basées sur la persistance et des DTO allant du côté client.

C'est donc le problème que MapStruct résout. La création manuelle de mappeurs de haricots prend du temps. La bibliothèquecan generate bean mapper classes automatically.

3. Maven

Ajoutons la dépendance ci-dessous dans nos Mavenpom.xml:


    org.mapstruct
    mapstruct-jdk8
    1.3.0.Beta2

La dernière version stable deMapstruct et de sesprocessor sont toutes deux disponibles à partir du référentiel central Maven.

Ajoutons également la sectionannotationProcessorPaths à la partie configuration du pluginmaven-compiler-plugin.

Lemapstruct-processor est utilisé pour générer l'implémentation du mappeur lors de la construction:


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

4. Cartographie de base

4.1. Créer un POJO

Commençons par créer un simple POJO Java:

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. L'interface Mapper

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

Notez que nous n'avons pas créé de classe d'implémentation pour nosSimpleSourceDestinationMapper - car MapStruct la crée pour nous.

4.3. Le nouveau mappeur

Nous pouvons déclencher le traitement MapStruct en exécutant unmvn clean install.

Cela générera la classe d'implémentation sous/target/generated-sources/annotations/.

Voici la classe que MapStruct crée automatiquement pour nous:

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. Un cas de test

Enfin, avec tout ce qui est généré, écrivons un scénario de test qui montrera que les valeurs enSimpleSource correspondent aux valeurs enSimpleDestination.

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. Cartographie avec injection de dépendances

Ensuite, obtenons une instance d'un mappeur dans MapStruct en appelant simplementMappers.getMapper(YourClass.class).

Bien sûr, c’est un moyen très manuel d’obtenir l’instance - une bien meilleure alternative serait d’injecter directement le mappeur là où nous en avons besoin (si notre projet utilise une solution d’injection de dépendance).

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

Pour utiliser Spring IoC dans notre mappeur, nous devons ajouter l'attributcomponentModelà@Mapper avec la valeurspring et pour CDI seraitcdi.

5.1. Modifier le mappeur

Ajoutez le code suivant àSimpleSourceDestinationMapper:

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

6. Mappage de champs avec différents noms de champ

Dans notre exemple précédent, MapStruct était en mesure de mapper nos beans automatiquement car ils portent les mêmes noms de champs. Alors que se passe-t-il si un haricot que nous sommes sur le point de mapper a un nom de champ différent?

Pour notre exemple, nous allons créer un nouveau bean appeléEmployee etEmployeeDTO.

6.1. Nouveaux POJO

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. L'interface Mapper

Lors du mappage de différents noms de champ, nous devrons configurer son champ source sur son champ cible et pour ce faire, nous devrons ajouter l'annotation@Mappings. Cette annotation accepte un tableau d'annotation@Mapping que nous utiliserons pour ajouter l'attribut cible et source.

Dans MapStruct, nous pouvons également utiliser la notation pointée pour définir un membre d'un 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. Le cas de test

Encore une fois, nous devons vérifier que les valeurs des objets source et cible correspondent:

@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());
}

D'autres cas de test peuvent être trouvés dans lesGithub project.

7. Cartographie des haricots avec des haricots enfants

Ensuite, nous montrerons comment mapper un bean avec des références à d’autres beans.

7.1. Modifier le POJO

Ajoutons une nouvelle référence de bean à l'objetEmployee:

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. Modifier le mappeur

Ici, nous devons ajouter une méthode pour convertir lesDivision enDivisionDTO et vice versa; si MapStruct détecte que le type d'objet doit être converti et que la méthode à convertir existe dans la même classe, il l'utilisera automatiquement.

Ajoutons ceci au mappeur:

DivisionDTO divisionToDivisionDTO(Division entity);

Division divisionDTOtoDivision(DivisionDTO dto);

7.3. Modifier le scénario de test

Modifions et ajoutons quelques cas de test à celui existant:

@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. Cartographie avec conversion de type

MapStruct propose également quelques conversions de types implicites prêtes à l'emploi, et pour notre exemple, nous allons essayer de convertir une date String en un objetDate réel.

Pour plus de détails sur la conversion de type implicite, vous pouvez lire lesMapStruct reference guide.

8.1. Modifier les haricots

Ajoutez une date de début pour notre employé:

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

8.2. Modifier le mappeur

Modifiez le mappeur et fournissez lesdateFormat pour notre date de début:

@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. Modifier le scénario de test

Ajoutons quelques cas de test supplémentaires pour vérifier que la conversion est correcte:

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. Cartographie avec une classe abstraite

Parfois, nous pouvons vouloir personnaliser notre mappeur d’une manière qui dépasse les capacités de @Mapping.

Par exemple, en plus de la conversion de type, nous souhaitons peut-être transformer les valeurs d’une manière ou d’une autre, comme dans notre exemple ci-dessous.

Dans ce cas, nous pouvons créer une classe abstraite et implémenter les méthodes que nous voulons personnaliser et laisser abstraites celles qui doivent être générées par MapStruct.

9.1. Modèle de base

Dans cet exemple, nous utiliserons la classe suivante:

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

    //standard getters
}

et un DTO correspondant:

public class TransactionDTO {

    private String uuid;
    private Long totalInCents;

    // standard getters and setters
}

La partie la plus délicate ici est de convertir leBigDecimaltotalamount de dollars en unLong totalInCents.

9.2. Définition d'un mappeur

Nous pouvons y parvenir en créant notreMapper  comme une classe abstraite:

@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);
}

Ici, nous avons mis en œuvre notre méthode de mappage entièrement personnalisée pour une conversion d'objet unique.

D'un autre côté, nous avons laissé la méthode qui est censée mapperCollectionur un sabstractList, doncMapStruct l'implémente pour nous.

9.3. Résultat généré

Comme nous avons déjà implémenté la méthode pour mapper unTransaction en unTransactionDTO, nous nous attendons à ce queMapstructl'utilise dans la deuxième méthode. Les éléments suivants seront générés:

@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;
    }
}

Comme nous pouvons le voir à la ligne 12,MapStruct reprend notre implémentation dans la méthode qu'elle a générée.

10. Annotations avant et après cartographie

Voici une autre façon de personnaliser les capacités de@Mapping en utilisant les annotations@BeforeMapping et@AfterMapping. The annotations are used to mark methods that are invoked right before and after the mapping logic.

Ils sont très utiles dans les scénarios où nous pourrions vouloir cebehavior to be applied to all mapped super-types.

Prenons un exemple qui mappe les sous-types deCar;ElectricCar, etBioDieselCar àCarDTO.

Lors du mappage, nous aimerions mapper la notion de types au champ d'énumérationFuelType dans le DTO, et une fois le mappage terminé, nous aimerions changer le nom du DTO en majuscules.

10.1. Modèle de base

Dans cet exemple, nous allons utiliser les classes suivantes:

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

Sous-types deCar:

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

LesCarDTO avec un champ d'énumération de typeFuelType:

public class CarDTO {
    private int id;
    private String name;
    private FuelType fuelType;
}
public enum FuelType {
    ELECTRIC, BIO_DIESEL
}
10.2. Définir le mappeur

Continuons maintenant et écrivons notre classe de mappeur abstrait, qui mappeCar àCarDTO:

@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 est une annotation de paramètre quepopulates the target mapping DTO right before the mapping logic is executedin case of @BeforeMapping et la méthode annotéeright after in case of @AfterMapping.

10.3. Résultat

LesCarsMapper 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;
    }
}

Remarquez commentthe annotated methods invocations surround the mapping logic dans l'implémentation.

11. Assistance pour Lombok

Dans la version récente de MapStruct, le support de Lombok a été annoncé. So we can easily map a source entity and a destination using Lombok. 

Pour activer la prise en charge de Lombok, nous devons ajouterthe dependency dans le chemin du processeur d'annotations. Nous avons donc maintenant le processeur mapstruct ainsi que Lombok dans le plug-in du compilateur 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
            
        
    

Définissons l'entité source à l'aide des annotations Lombok:

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

Et l'objet de transfert de données de destination:

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

L'interface du mappeur pour cela reste similaire à notre exemple précédent:

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

12. Prise en charge dedefaultExpression

À partir de la version 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. Ceci s'ajoute à la fonctionnalité d'attributdefaultValue existante.

L'entité source:

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

L'objet de transfert de données de destination:

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

Si le champid de l'entité source estnull,, nous voulons générer unid aléatoire et l'assigner à la destination en conservant les autres valeurs de propriété telles quelles:

@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);
}

Ajoutons un scénario de test pour vérifier l'exécution de l'expression:

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

Cet article a fourni une introduction à MapStruct. Nous avons présenté la plupart des bases de la bibliothèque Mapping et comment l'utiliser dans nos applications.

L'implémentation de ces exemples et tests se trouve dans le projetGithub. Ceci est un projet Maven, il devrait donc être facile à importer et à exécuter tel quel.