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.mapstructmapstruct-jdk81.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:
public class SimpleSource {
private String name;
private String description;
// getters and setters
}
public class SimpleDestination {
private String name;
private String description;
// getters and setters
}
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:
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.
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:
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:
À 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:
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.