Dernière modification : 15/04/2023

Mapping d'objets - MapStruct - Java

 

MapStruct est un générateur de code permettant la simplification de la conversion d'un objet à un autre. Il utilise des annotations et des méthodes de mapping afin de générer automatiquement les implémentations des classes de conversions. MapStruct est particulièrement utile lors de la conversion entre les DTO et les DAO.

1. Déclaration des dépendances

L'utilisation de la librairie MapStruct nécessite des configurations dans la gestion de dépendance (ici Maven).

Premièrement ajouter la dépendance suivante dans le fichier pom.xml :


    org.mapstruct
    mapstruct
    1.5.3.Final

 

1.1 Ajout du plugin : Cas générique

Ajouter le plugin suivant dans le fichier pom.xml :


  
    
      org.mapstruct
      mapstruct-maven-plugin
      1.5.3.Final
      
        
          generate-mappers
          
            generate-mappers
          
        
      
      
        annotated
        target/generated-sources/mapstruct
      
    
  

 

1.2 Ajout du plugin : Cas de l'utilisation de Lombok

Si Lombok est utilisé dans le projet, alors il est nécessaire d'utiliser le plugin suivant dans le fichier pom.xml :


    org.apache.maven.plugins
    maven-compiler-plugin
    
        
            
                org.projectlombok
                lombok
                ${lombok.version}
            
            
                org.projectlombok
                lombok-mapstruct-binding
                ${lombok-mapstruct-binding.version}
            
            
                org.mapstruct
                mapstruct-processor
                ${mapstruct.version}
            
        
    

 

2. Génération des sources

La génération des sources (implémentation) dans le reste de l'article est réalisée à l'aide de la commande Maven suivante :

mvn clean install

 

3. Mapping simple

Premièrement définissons les deux classes suivantes :

public class UserDTO {
    private long id;
    private String usename;

    // Getter / Setter
}

public class UserDBO {
    private long id;
    private String usename;

    // Getter / Setter
}

Note : Les getter/setter peuvent être générés à l'aide de Lombok via les annotations @Getter et @Setter sur les deux classes.

 

La définition de l'interface de mapping est réalisée comme suit :

@Mapper
public interface UserMapper {
    UserDTO mapToUserDTO(UserDBO user);
}

Ici, nous cherchons à mapper la classe UserDBO en UserDTO. L'annotation @Mapper indique à MapStruct de générer une implémentation de cette interface de mapper.

 

La génération du mapping est réalisée via la commande Maven suivante (à utiliser avant l'exécution de l'application afin d'éviter toute erreur) :

mvn clean install

 

Dans le dossier target/generated-sources/mapstruct sont générées les implémentations des mappers. Ici, nous retrouvons l'implémentation du UserMapper :

import javax.annotation.processing.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-01-18T21:56:41+0100",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 17.0.3.1 (Oracle Corporation)"
)
public class UserMapperImpl implements UserMapper {

    @Override
    public UserDTO mapToUserDTO(UserDBO user) {
        if ( user == null ) {
            return null;
        }

        UserDTO userDTO = new UserDTO();

        userDTO.setId( user.getId() );
        userDTO.setUsename( user.getUsename() );

        return userDTO;
    }
}

 

Pour instancier le UserMapper, utiliser la commande Mappers.getMapper(TheMapperInterface.class) :

UserMapper mapper = Mappers.getMapper(UserMapper.class);

 

4. Injection de dépendance avec Spring

Il est possible d'instancier le UserMapper à l'aide de l'injection des dépendances Spring. Pour ce faire, utiliser l'attribut "componentModel" de l'annotation @Mapper comme suit :

@Mapper(componentModel = "spring")
public interface UserMapper {
    UserDTO mapToUserDTO(UserDBO user);
}

 

5. Mapping de champs avec des noms différents

Le mapping des classes contenant des noms différents est réalisé comme l'exemple suivant :

Définition des modèles à mapper :

public class UserDTO {
    private long id;
    private String name;
    private String surname;

    // Getter / Setter
}

public class UserDBO {
    private long id;
    private String firstName;
    private String lastName;

    // Getter / Setter
}

 

Définition du mapper :

@Mapper
public interface UserMapper {
    @Mapping(target = "name", source = "firstName")
    @Mapping(target = "surname", source = "lastName")
    UserDTO mapToUserDTO(UserDBO user);
}

Ici, la méthode UserMapper.mapToUserDTO permet d'identifier les correspondances entre la classe UserDBO en UserDTO suivantes :

  • UserDBO.firstName vers UserDTO.name,
  • UserDBO.lastName vers UserDTO.surname.

 

6. Mapping des objets enfants

Si la classe UserDBO contenait des classes non génériques (exemple, des classes DBO), alors il serait nécessaire de créer une seconde méthode dans notre interface de mapping afin qu'elle soit également convertie. Exemple :

@Mapper
public interface UserMapper {
    @Mapping(target = "name", source = "firstName")
    @Mapping(target = "surname", source = "lastName")
    UserDTO mapToUserDTO(UserDBO user);

    @Mapping(target = "id", source = "identifier")
    RightDTO mapToUserDTO(RightDBO right);
}

 

7. Mapping avec conversion de type

Le mapping des classes contenant des types différents est réalisé comme l'exemple suivant :

Définition des modèles à mapper :

public class UserDTO {
    private long id;
    private String name;
    private String surname;
    private String birthdate;

    // Getter / Setter
}

public class UserDBO {
    private long id;
    private String firstName;
    private String lastName;
    private Date birthdate;

    // Getter / Setter
}

 

Définition du mapper :

@Mapper
public interface UserMapper {
    @Mapping(target = "name", source = "firstName")
    @Mapping(target = "surname", source = "lastName")
    UserDTO mapToUserDTO(UserDBO user);

    default String setBirthdate(Date birthdate) {
        return birthdate.toString();
    }
}

Ici, la méthode UserMapper.setBirthdate permet d'identifier les correspondances entre le type Date vers String. Cependant, cela aura pour conséquence de mapper toutes les dates en chaîne de caractères en suivant cette méthode.

 

Si plusieurs champs du même type doivent suivre des procédures différente pour leurs mapping, alors il est nécessaire d'utiliser l'attribut qualifiedByName de l'annotation @Mapping comme l'exemple suivant :

@Mapper
public interface UserMapper {

    @Mapping(target = "name", source = "firstName")
    @Mapping(target = "surname", source = "lastName")
    @Mapping(target = "birthdate", source = "birthdate", qualifiedByName = "birthdateToString")
    UserDTO mapToUserDTO(UserDBO User);

    @Named("birthdateToString") 
    public static String birthdateToString(Date birthdate) {
        return birthdate.toString();
    }
}

Lors de la conversion du champ birthdate du type Date en String, MapStruct utilisera la méthode UserMapper.birthdateToString(Date) pour réaliser la conversion. A noter que cette nouvelle méthode est statique.

 

Il est également possible d'utiliser l'attribut dateFormat de l'annotation @Mapper pour convertir plus facilement une date. Exemple :

@Mapper
public interface UserMapper {

    @Mapping(target = "name", source = "firstName")
    @Mapping(target = "surname", source = "lastName")
    @Mapping(target = "birthdate", source = "birthdate", dateFormat = "yyyy/MM/dd HH:mm:ss")
    UserDTO mapToUserDTO(UserDBO user);
}

 

8. Mapping avec modification des données

Le mapping contenant des modifications de données peut être réalisé en suivant un des deux premiers exemples du point précédent.

 

9. Conclusion

En conclusion, MapStruct est un outil utile pour simplifier la conversion entre objets. Il permet de gagner du temps et de rendre le code d'avantage lisible et plus facile à maintenir via la génération automatique du code de conversion entre différents objets. Cependant, il est important de de connaître ses limitations pour anticiper les mapping manuels obligatoires.