Publié par
Il y a 4 années · 7 minutes · DevOps

Selma le mapping Java à la compilation

Au bout de quelques années à œuvrer dans la communauté Java de mission en mission, j’ai décidé de rentrer en guerre contre les frameworks de mapping au Runtime, Dozer en tête. Pourquoi me direz vous ? Eh bien, ils ne sont pas fortement typés, ils ne supportent pas bien le refactoring, ils vous laissent découvrir les erreurs au Runtime et pour finir, ils sont rarement performants. Je préfère donc toujours écrire mon mapping de bean à bean, à la main plutôt que d’utiliser un framework, avec lequel je vais découvrir les bugs au dernier moment (en production ?). L’idéal est sûrement d’éviter tout bonnement le mapping de bean à bean, mais cette vision reste idyllique et ne concerne que peu de cas. Cette philosophie a fini par me coûter cher, le jour où j’ai dû attaquer un mapping sur une grande grappe d’objets, le code était répétitif et n’avait que peu de valeur ajoutée. Après 300 lignes de if set, …, on en viendrait presque à regretter le bon vieux Mapping en réflexion. L’idée est née ici : tirer le meilleur des deux mondes : automatiser le mapping et le typer fortement en remontant les erreurs à la compilation.

Dans cet article, je vous présenterai Selma, la bibliothèque développée par Xebia pour en finir avec les anciens frameworks de mapping.

Le principe

Comment automatiser le mapping de bean Java tout en remontant les erreurs à la compilation ? La JSR 269 introduite dans Java 6, plus connue sous le nom d’API AnnotationProcessor, permet de traiter les annotations à la compilation en générant du code Java. C’est la solution utilisée par Selma pour répondre au besoin.

Selma fournit un AnnotationProcessor nécessaire uniquement à la compilation, qui va générer l’implémentation des interfaces de mapping annotées avec @Mapper.

Au runtime, vous pouvez passer par la classe Selma pour récupérer votre instance de Mapper via son interface.

Mise en place

Il faut tout d’abord ajouter Selma dans les dépendances du POM :

<!-- scope provided car le processor est uniquement requis pour la compilation -->
<dependency>
 <groupId>fr.xebia.extras</groupId>
 <artifactId>selma-processor</artifactId>
 <version>0.4</version>
 <scope>provided</scope>
</dependency>

<!-- Seul dependance requise au runtime -->
<dependency>
 <groupId>fr.xebia.extras</groupId>
 <artifactId>selma</artifactId>
 <version>0.4</version>
</dependency>

Vous pouvez ensuite déclarer une interface décrivant le contrat de votre mapping.
Par exemple pour mapper un bean Person vers le bean PersonDto:

@Mapper
public interface PersonMapper {

 PersonDto asPersonDto(Person in);

}

Voilà, vous avez défini votre premier mapper Selma ; son implémentation sera générée à la compilation. Vous n’avez plus qu’à l’utiliser là où vous en avez besoin :

// Récupérer l'instance du mapper
PersonMapper mapper = Selma.mapper(PersonMapper.class);
// Executer le mapping pour convertir notre Person en PersonDto
PersonDto res = mapper.asPersonDto(myPerson);

Ce petit exemple de code vous explique comment récupérer une instance du PersonMapper et l’utiliser au runtime. Comme vous pouvez le constater, le code est léger et facilement testable.

Stratégie de Mapping

À la compilation, Selma a généré une classe implémentant l’interface PersonMapper qui se charge du mapping. La stratégie de mapping par défaut consiste à faire correspondre les champs via les noms des getter et setter. Il faut que Person et PersonDto aient exactement les même champs, et qu’il existe un constructeur vide pour que cela fonctionne immédiatement.

Admettons maintenant que PersonDto ne contienne pas tous les champs de Person, ou qu’il contienne des champs supplémentaires ; ces champs doivent être ignorés pour permettre la compilation.

@Mapper
public interface PersonMapper {

 @IgnoreFields({"password", "technicalId"})
 PersonDto asPersonDto(Person in);

}

Dans cette nouvelle version de PersonMapper, les champs password et technicalId seront ignorés par le code de mapping généré.

Faire correspondre des champs différents

Il arrive que les beans à mapper ne soient pas exactement nommés de la même façon, par exemple "dob" peut devenir "birthDate", ou "nom" devenir "name". Dans ces deux cas, Selma ne sait pas réaliser le mapping tout seul pour ces champs.
La compilation finira en erreur car la philosophie de Selma est de remonter l’erreur au plus tôt.
Ici, la solution est de déclarer un mapping personnalisé des champs.

Par exemple:

@Mapper
public interface PersonMapper {

 @Fields({
  @Field({"dob", "birthdate"}), @Field({"nom", "name"})
 })
 PersonDto asPersonDto(Person in);

}

J’attire ici votre attention sur le fait que le mapping ignore la casse. Avec cette nouvelle configuration, le mapping des champs "dob" et "nom" sera assuré comme souhaité et la compilation finie sans erreur.

Personnaliser le mapping d’une énumération

Dans le cas des énumérations, Selma, utilise par défaut une stratégie considérant que toutes les valeurs de l’énumération source existent dans l’énumération destination. Là encore, si vous n’avez pas les mêmes valeurs dans les deux énumération, la compilation échouera. Pour résoudre ce problème, vous pouvez fournir une valeur par défaut qui sera utilisée pour les valeurs sources n’ayant pas d’équivalent en destination.

@Mapper
public interface PersonMapper {

 @EnumMapper(from=MemberLevel.class, to=MemberLevelDto.class, defaultValue="NOOB")
 PersonDto asPersonDto(Person in);

}

Cette nouvelle configuration permet d’indiquer à Selma, d’utiliser MemberLevelDto.NOOB comme valeur par défaut.
Notez tout de même que l’EnumMapper peut-être ajouté en paramètre de @Mapper pour être disponible au niveau de la classe.
Et dernier raffinement, vous pouvez utiliser l’EnumMapper sur une méthode spécialisée pour le mapping de l’enum.

@Mapper
public interface PersonMapper {

 PersonDto asPersonDto(Person in);

 @EnumMapper(defaultValue="NOOB")
 MemberLevelDto asMemberLevelDto(MemberLevel in);

}

Cette technique est moins verbeuse car les énumérations source et destination sont données par la méthode de mapping. Le code généré à la compilation utilisera la méthode asMemberLevelDto pour assurer le mapping requis.

Aller plus loin dans le mapping

Ok, le mapping automatique c’est bien gentil, mais ça ne fait pas tout. Parfois, il faut retourner à ses premières amours, le code. Dans le cas de Selma, par exemple, les conversions ne sont pas gérées au-delà de ce que fournit la JVM sans perte. La bibliothèqe supporte la conversion de type natif à type objet, les tableaux et les collections. Si vous voulez convertir une String en Date, ou un Long en Double, bref faire une conversion et non un mapping, vous devrez utiliser votre propre code, via un Custom Mapper.

Prenons l’exemple d’une date à convertir en String pour le PersonMapper

@Mapper(withMapper=CustomMapper.class)
public interface PersonMapper {

 PersonDto asPersonDto(Person in);

}

public class CustomMapper{

 String dateToGMTString(Date in){

  return in.toGMTString();
 }

}

Ce mapper personnalisé est assez basique, mais il a l’avantage de démontrer simplement son utilisation. Lorsque Selma rencontrera un champs à mapper d’un type date vers un type String, il utilisera une instance de notre CustomMapper. Cela fonctionne avec n’importe quelle association de type d’entrée vers type de sortie.

Et encore plus

Dans cet article, j’ai tenté de vous donner un aperçu des fonctionnalités de Selma, pour plus d’information vous pouvez consulter le site du projet : http://selma-java.org/.

Parmi les fonctionnalités supplémentaires, vous trouverez le support pour les JDK 6 à 8, les tableaux à N-dimensions, les collections et les maps, et quelques raffinements comme le mapping post-processor pour encore plus de contrôle. 

Pour conclure

La librairie Selma est encore jeune (0.4 à l’écriture de l’article), mais elle est déjà utilisée en production sans problème. Si comme moi, vous avez des problèmes avec les bibliothèques de mapping au runtime, et que vous souhaitez explorer une nouvelle voie, testez Selma. La migration est simple, le mapping est plus performant et il garantit l’immuabilité de l’objet source.

Notez enfin qu’il existe d’autres alternatives à Dozer, la meilleure de mon point de vue étant Orika ; bien que fonctionnant au runtime, il utilise l’API JavaCompiler pour générer le code du mapping. N’hésitez pas à nous remonter vos éventuels bugs, ainsi que les évolutions à inscrire dans la roadmap.

9 thoughts on “Selma le mapping Java à la compilation”

  1. Publié par chris, Il y a 4 années

    PersonMapper mapper = Selma.mapper(SelmaMapper.class);

    c’est pas plutot :
    PersonMapper mapper = Selma.mapper(PersonMapper.class);

    A part ca, c’est une excellente idée. Je ne sais pas si Lombok fonctionne pareil mais ce genre de bibliothèques peut être super pratique !

    Une petite question : ca marche avec des collections ?

    List asListPersonDto(List persons)

  2. Publié par chris, Il y a 4 années

    Ah les chevrons sont toujours aussi mal supportés :

    List?PersonDto? asListPersonDto(List?Person? persons)

  3. Publié par Séven Le Mesle, Il y a 4 années

    @chris l’erreur est corrigée merci pour la relecture.
    Pour votre question sur les listes/collections la réponse est oui bien-sûr, cela fonctionne aussi avec les Map.

  4. Publié par antcha, Il y a 4 années

    Est-ce possible de créer un mapper depuis un bean vers un java.sql.ResultSet ?

  5. Publié par Same, Il y a 2 années

    Bonjour,

    En lisant la présentation, l’API s’avère très facile à utiliser.
    De plus, en lisant ce comparatif réalisé par Antoine [http://javaetmoi.com/2015/09/benchmark-frameworks-javas-mapping-objet], les performances sont meilleures.
    J’ai vu également que Selma s’intègre bien avec spring framework, ce qui la rends encore très pratique.
    J’aimerais savoir si dans la feuille de route du projet, l’intégration avec Google Guice est prévu.
    Je vous remercie par avance.

  6. Publié par Séven Le Mesle, Il y a 2 années

    Bonjour et merci pour votre message, je n’ai pas de feuille de route concernant l’intégration de Guice. Cela étant dit, je viens de valider un pull request implémentant CDI via @Inject. Je ne vois aucun problème à intégrer un support pour Guice, j’ai d’ailleurs étudié l’intégration Dagger.
    Je vous invite à tester la version snapshot actuelle pour voir si la dernière pull request répond au besoin.
    Dans le cas contraire n’hésitez pas à créer une issue sur notre github voir à proposer votre pull request.
    J’ai assez peu de temps de mon côté à consacrer à Selma ces derniers temps.

  7. Publié par Daringa, Il y a 2 années

    Bonjour,
    Je n’arrive pas à utiliser Selma quand on mappe un attribut de type double vers un attribut de type BigDecimal même en utilisant un custommapper.
    Existe-t-il une solution ?
    Merci

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *