MapStruct est une bibliothèque Java open source qui génère automatiquement le code de conversion entre deux objets (entre une entité JPA et un DTO). Elle est statique et compilée donc pas de réflexion à l’exécution, ce qui la rend très performante.
Séparer les entités persistantes de la couche de présentation grâce à un mapper automatique (ici, MapStruct)
Voici un extrait du corrigé de l’application Waouf Waouf (TP UML) :
package com.ffc.wouaf.web.mapper; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import com.ffc.wouaf.model.Chien; import com.ffc.wouaf.web.dto.ChienDto; @Mapper(componentModel = "spring") public interface ChienMapper { @Mapping(target="etat", expression="java(chien.getEtat().name())") @Mapping(target="raceId", source="race.id") @Mapping(target="raceCode", source="race.code") @Mapping(target="raceNom", source="race.nom") @Mapping(target="proprietaireId", source="proprietaire.id") @Mapping(target="proprietaireNom", source="proprietaire.nom") ChienDto toDto(Chien chien); }
Explications :
Quand vous utilisez JPA/Hibernate, vous manipulez des entités (@Entity) qui sont liées directement à la base de données.
Ces entités :
lazy-loaded
En générale, exposer directement ces entités dans une API REST (via un votre @RestController) entraîne :
@RestController
La bonne pratique consiste à transformer les entités (dans notre exemple : Chien, Race, Proprietaire) en objets simples de transfert de données : les fameux DTO (Data Transfer Objects). C’est là que MapStruct intervient !
DTO (Data Transfer Objects)
MapStruct
Mais à quoi sert-il ?
MapStruct est une librairie qui génère automatiquement le code de conversion entre deux objets Java, ici entre notre entité JPA (Chien) et le DTO (ChienDto).
L’objectif étant d’automatiser le “copier-coller intelligent” de champs sans écrire des centaines de code de la forme dto.setXXX(entity.getXXX()).
dto.setXXX(entity.getXXX())
@Mapper(componentModel = "spring") public interface ChienMapper {
Il indique à MapStruct de générer une classe d’implémentation automatiquement.
L’option componentModel = "spring" fait en sorte que cette classe soit un bean Spring injectable via @Autowired ou par constructeur (recommandée).
componentModel = "spring"
bean Spring
@Autowired
par constructeur
On pourra écrire :
private final ChienMapper chienMapper; public ChienService(ChienMapper chienMapper) { this.chienMapper = chienMapper; }
@Mapping(target="etat", expression="java(chien.getEtat().name())") @Mapping(target="raceId", source="race.id") @Mapping(target="raceCode", source="race.code") @Mapping(target="raceNom", source="race.nom") @Mapping(target="proprietaireId", source="proprietaire.id") @Mapping(target="proprietaireNom", source="proprietaire.nom") ChienDto toDto(Chien chien);
La méthode toDto(Chien chien) demande à MapStruct de générer une méthode qui :
toDto(Chien chien)
@Mapping(target="etat", expression="java(chien.getEtat().name())")
getEtat()
chien
String
etat
"etat": "EN_BONNE_SANTE"
@Mapping(target="raceId", source="race.id")
chien.getRace().getId()
ChienDto.raceId
"raceId": 5
@Mapping(target="raceCode", source="race.code")
"raceCode": "LABRADOR"
@Mapping(target="raceNom", source="race.nom")
"raceNom": "Labrador Retriever"
@Mapping(target="proprietaireId", source="proprietaire.id")
"proprietaireId": 12
@Mapping(target="proprietaireNom", source="proprietaire.nom")
"proprietaireNom": "Dupont"
@Entity public class Chien { @Id private Long id; private EtatChien etat; @ManyToOne private Race race; @ManyToOne private Proprietaire proprietaire; }
public class ChienDto { private Long id; private String etat; private Long raceId; private String raceCode; private String raceNom; private Long proprietaireId; private String proprietaireNom; }
ChienDto chienDto = chienMapper.toDto(chien);
Grâce à notre Mapper prédéfini :
ChienDto dto = new ChienDto(); dto.setEtat(chien.getEtat().name()); dto.setRaceId(chien.getRace().getId()); dto.setRaceCode(chien.getRace().getCode()); dto.setRaceNom(chien.getRace().getNom()); dto.setProprietaireId(chien.getProprietaire().getId()); dto.setProprietaireNom(chien.getProprietaire().getNom());
Du coup, MapStruct le compile à la volée au moment du build, pas besoin d’écrire plus de code !
@Service @RequiredArgsConstructor public class ChienService { private final ChienRepository chienRepository; private final ChienMapper chienMapper; public List<ChienDto> getAllChiens() { return chienRepository.findAll().stream() .map(ChienMapper::toDto) .toList(); } }
L’API renverra des ChienDto bien formatés, sans jamais exposer les entités JPA !
dto.setXxx()
Par conséquent :
@Mapper(componentModel = "spring")
@Mapping(target, source)
expression="java(...)"
Donc, on transforme les entités JPA en DTOs sans trop d’effort !
Voilà, maintenant, vous savez tout sur la notion de DTO et MapStruct
DTO et MapStruct
Propriétes de @Mapping (attention, de MapStruct uniquement)
source
@Mapping(source = "nom", target = "nomClient")
target
@Mapping(target = "raceNom", source = "race.nom")
expression
@Mapping(target = "etat", expression = "java(chien.getEtat().name())")
constant
@Mapping(target = "type", constant = "ANIMAL")
defaultValue
null
@Mapping(source = "pays", target = "pays", defaultValue = "France")
defaultExpression
@Mapping(target = "dateCreation", defaultExpression = "java(LocalDate.now())")
qualifiedByName
@Mapping(source = "dateNaissance", target = "age", qualifiedByName = "calculerAge")
@Named("calculerAge")
qualifiedBy
@Mapping(source = "dateNaissance", target = "age", qualifiedBy = AgeQualifier.class)
dateFormat
@Mapping(source = "dateNaissance", target = "dateNaissance", dateFormat = "dd/MM/yyyy")
String ↔ Date
numberFormat
@Mapping(source = "salaire", target = "salaire", numberFormat = "#,###.00")
ignore
@Mapping(target = "password", ignore = true)
nullValuePropertyMappingStrategy
@Mapping(target = "adresse", source = "adresse", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
nullValueCheckStrategy
@Mapping(target = "nom", source = "nom", nullValueCheckStrategy = ALWAYS)
nullValueMappingStrategy
@Mapping(target = "dto", source = "entity", nullValueMappingStrategy = RETURN_DEFAULT)
resultType
@Mapping(target = "animal", source = "animalEntity", resultType = ChienDto.class)
dependsOn
@Mapping(target = "fullName", dependsOn = {"prenom", "nom"})
Le lien vers la documentation est en fin de page de ce cours pratique.
@Mapping
Dto → Entity
@AfterMapping
update
Exemple avec l’utilisation de @InheritInverseConfiguration :
@InheritInverseConfiguration
@InheritInverseConfiguration Chien toEntity(ChienDto chienDto);
Pour info, **MapStruct génère automatiquement la classe ChienMapperImpl.java pendant la compilation. Cette classe contient tout le code de conversion, optimisé et typé.
Illustration avec @Aftermappinget @Mappingtarget :
@Aftermapping
@Mappingtarget
@Mapper(componentModel = "spring") public interface AdherentMapper { AdherentDto toDto(Adherent entity); @AfterMapping default void enrichirDto(Adherent entity, @MappingTarget AdherentDto dto) { dto.setNomComplet(entity.getPrenom() + " " + entity.getNom()); } }
Unmapped target property
unmappedTargetPolicy = ReportingPolicy.IGNORE
Can't map property X to Y
Date
Ambiguous mapping methods found
NullPointerException
nullValuePropertyMappingStrategy = IGNORE
Vous pouvez ajouter ce code pour une vérification stricte :
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
Les propriétés source et target sont les plus fréquentes : elles définissent le lien entre les champs.
Les propriétés comme expression, constant, defaultValue et ignore sont très pratiques pour des cas spécifiques.
MapStruct génère du code à la compilation (pas en runtime) : Donc pas de perte de performance.
On peut combiner plusieurs @Mapping() comme ce que l’on a fait dans les exemples de code.
mapstruct.org
Elle correspond à l’implémentation de notre Interface ChienMapper définie plus haut.
```java package com.ffc.wouaf.web.mapper;
import com.ffc.wouaf.model.Adherent; import com.ffc.wouaf.model.Chien; import com.ffc.wouaf.model.Race; import com.ffc.wouaf.web.dto.ChienDto; import javax.annotation.processing.Generated; import org.springframework.stereotype.Component;
@Generated( value = “org.mapstruct.ap.MappingProcessor”, date = “2025-10-16T09:37:26+0200”, comments = “version: 1.5.5.Final, compiler: Eclipse JDT (IDE) 3.43.0.v20250819-1513, environment: Java 21.0.8 (Eclipse Adoptium)” ) @Component public class ChienMapperImpl implements ChienMapper {
@Override public ChienDto toDto(Chien chien) { if ( chien == null ) { return null; } Long raceId = null; String raceCode = null; String raceNom = null; Long proprietaireId = null; String proprietaireNom = null; Long id = null; String numeroTatouage = null; String nom = null; raceId = chienRaceId( chien ); raceCode = chienRaceCode( chien ); raceNom = chienRaceNom( chien ); proprietaireId = chienProprietaireId( chien ); proprietaireNom = chienProprietaireNom( chien ); id = chien.getId(); numeroTatouage = chien.getNumeroTatouage(); nom = chien.getNom(); String etat = chien.getEtat().name(); ChienDto chienDto = new ChienDto( id, numeroTatouage, nom, etat, raceId, raceCode, raceNom, proprietaireId, proprietaireNom ); return chienDto; } private Long chienRaceId(Chien chien) { if ( chien == null ) { return null; } Race race = chien.getRace(); if ( race == null ) { return null; } Long id = race.getId(); if ( id == null ) { return null; } return id; } private String chienRaceCode(Chien chien) { if ( chien == null ) { return null; } Race race = chien.getRace(); if ( race == null ) { return null; } String code = race.getCode(); if ( code == null ) { return null; } return code; } private String chienRaceNom(Chien chien) { if ( chien == null ) { return null; } Race race = chien.getRace(); if ( race == null ) { return null; } String nom = race.getNom(); if ( nom == null ) { return null; } return nom; } private Long chienProprietaireId(Chien chien) { if ( chien == null ) { return null; } Adherent proprietaire = chien.getProprietaire(); if ( proprietaire == null ) { return null; } Long id = proprietaire.getId(); if ( id == null ) { return null; } return id; } private String chienProprietaireNom(Chien chien) { if ( chien == null ) { return null; } Adherent proprietaire = chien.getProprietaire(); if ( proprietaire == null ) { return null; } String nom = proprietaire.getNom(); if ( nom == null ) { return null; } return nom; } }```