Guide de cartographie avec bulldozer

Guide de cartographie avec bulldozer

1. Vue d'ensemble

Dozer est unJava Bean to Java Bean mapper qui copie récursivement des données d'un objet à un autre, attribut par attribut.

La bibliothèque prend en charge non seulement le mappage entre les noms d’attributs des Java Beans, mais aussiautomatically converts between types - s’ils sont différents.

La plupart des scénarios de conversion sont pris en charge par défaut, mais Dozer vous permet également despecify custom conversions via XML.

2. Exemple simple

Pour notre premier exemple, supposons que les objets de données source et de destination partagent tous les mêmes noms d'attributs communs.

Voici la cartographie la plus simple que l'on puisse faire avec Dozer:

public class Source {
    private String name;
    private int age;

    public Source() {}

    public Source(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // standard getters and setters
}

Puis notre fichier de destination,Dest.java:

public class Dest {
    private String name;
    private int age;

    public Dest() {}

    public Dest(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // standard getters and setters
}

Nous devons nous assurer deinclude the default or zero argument constructors, car Dozer utilise la réflexion sous le capot.

Et, à des fins de performances, rendons notre mappeur global et créons un seul objet que nous utiliserons tout au long de nos tests:

DozerBeanMapper mapper;

@Before
public void before() throws Exception {
    mapper = new DozerBeanMapper();
}

Maintenant, exécutons notre premier test pour confirmer que lorsque nous créons un objetSource, nous pouvons le mapper directement sur un objetDest:

@Test
public void givenSourceObjectAndDestClass_whenMapsSameNameFieldsCorrectly_
  thenCorrect() {
    Source source = new Source("example", 10);
    Dest dest = mapper.map(source, Dest.class);

    assertEquals(dest.getName(), "example");
    assertEquals(dest.getAge(), 10);
}

Comme nous pouvons le voir, après le mappage Dozer, le résultat sera une nouvelle instance de l'objetDest qui contient des valeurs pour tous les champs qui ont le même nom de champ que l'objetSource.

Alternativement, au lieu de passermapper la classeDest, nous aurions pu simplement créer l'objetDest et passer sa référence àmapper:

@Test
public void givenSourceObjectAndDestObject_whenMapsSameNameFieldsCorrectly_
  thenCorrect() {
    Source source = new Source("example", 10);
    Dest dest = new Dest();
    mapper.map(source, dest);

    assertEquals(dest.getName(), "example");
    assertEquals(dest.getAge(), 10);
}

3. Maven Setup

Maintenant que nous avons une compréhension de base du fonctionnement de Dozer, ajoutons la dépendance suivante auxpom.xml:


    net.sf.dozer
    dozer
    5.5.1

La dernière version est disponiblehere.

4. Exemple de conversion de données

Comme nous le savons déjà, Dozer peut mapper un objet existant sur un autre tant qu'il trouve des attributs du même nom dans les deux classes.

Cependant, ce n’est pas toujours le cas; et ainsi, si l'un des attributs mappés est de types de données différents, le moteur de mappage Dozer seraautomatically perform a data type conversion.

Voyons ce nouveau concept en action:

public class Source2 {
    private String id;
    private double points;

    public Source2() {}

    public Source2(String id, double points) {
        this.id = id;
        this.points = points;
    }

    // standard getters and setters
}

Et la classe de destination:

public class Dest2 {
    private int id;
    private int points;

    public Dest2() {}

    public Dest2(int id, int points) {
        super();
        this.id = id;
        this.points = points;
    }

    // standard getters and setters
}

Notez que les noms d'attributs sont les mêmes maistheir data types are different.

Dans la classe source,id est unString etpoints est undouble, alors que dans la classe destination,id etpoints sont tous les deux integers.

Voyons maintenant comment Dozer gère correctement la conversion:

@Test
public void givenSourceAndDestWithDifferentFieldTypes_
  whenMapsAndAutoConverts_thenCorrect() {
    Source2 source = new Source2("320", 15.2);
    Dest2 dest = mapper.map(source, Dest2.class);

    assertEquals(dest.getId(), 320);
    assertEquals(dest.getPoints(), 15);
}

Nous avons passé“320” et15.2, aString et adouble dans l'objet source et le résultat avait320 et15, tous deuxinteger) ss dans l'objet de destination.

5. Mappages personnalisés de base via XML

Dans tous les exemples précédents que nous avons vus, les objets de données source et de destination ont les mêmes noms de champ, ce qui facilite la cartographie de notre côté.

Cependant, dans les applications du monde réel, il y aura un nombre incalculable de fois où les deux objets de données que nous mappons n'auront pas de champs partageant un nom de propriété commun.

Pour résoudre ce problème, Dozer nous offre la possibilité de créer uncustom mapping configuration in XML.

Dans ce fichier XML, nous pouvons définir les entrées de mappage de classe que le moteur de mappage Dozer utilisera pour décider quel attribut source mapper vers quel attribut de destination.

Examinons un exemple et essayons de démarshalling des objets de données d'une application créée par un programmeur français, à un style anglais de dénomination de nos objets.

Nous avons un objetPerson avec les champsname,nickname etage:

public class Person {
    private String name;
    private String nickname;
    private int age;

    public Person() {}

    public Person(String name, String nickname, int age) {
        super();
        this.name = name;
        this.nickname = nickname;
        this.age = age;
    }

    // standard getters and setters
}

L'objet que nous supprimons est nomméPersonne et a les champsnom,surnom etage:

public class Personne {
    private String nom;
    private String surnom;
    private int age;

    public Personne() {}

    public Personne(String nom, String surnom, int age) {
        super();
        this.nom = nom;
        this.surnom = surnom;
        this.age = age;
    }

    // standard getters and setters
}

Ces objets atteignent vraiment le même objectif mais nous avons une barrière linguistique. Afin d'aider avec cette barrière, nous pouvons utiliser Dozer pour mapper l'objet françaisPersonne à notre objetPerson.

Nous n'avons qu'à créer un fichier de mappage personnalisé pour aider Dozer à faire cela, nous l'appelleronsdozer_mapping.xml:



    
        com.example.dozer.Personne
        com.example.dozer.Person
        
            nom
            name
        
        
            surnom
            nickname
        
    

C'est l'exemple le plus simple d'un fichier de mappage XML personnalisé que nous pouvons avoir.

Pour l'instant, il suffit de remarquer que nous avons<mappings> comme élément racine, qui a un enfant<mapping>, nous pouvons avoir autant de ces enfants à l'intérieur de<mappings> qu'il y a d'incidences de classe paires nécessitant un mappage personnalisé.

Notez également comment nous spécifions les classes source et destination à l'intérieur des balises<mapping></mapping>. Ceci est suivi d'un<field></field> pour chaque paire de champs source et destination nécessitant un mappage personnalisé.

Enfin, notez que nous n'avons pas inclus le champage dans notre fichier de mappage personnalisé. Le mot français pour l'âge est encore l'âge, ce qui nous amène à une autre caractéristique importante de Dozer.

Properties that are of the same name do not need to be specified in the mapping XML file. Dozer mappe automatiquement tous les champs portant le même nom de propriété de l'objet source dans l'objet de destination.

Nous placerons ensuite notre fichier XML personnalisé sur le chemin de classe directement sous le dossiersrc. Cependant, partout où nous le placerons sur le chemin de classe, Dozer recherchera le chemin de classe entier à la recherche du fichier spécifié.

Créons une méthode d'assistance pour ajouter des fichiers de mappage à nosmapper:

public void configureMapper(String... mappingFileUrls) {
    mapper.setMappingFiles(Arrays.asList(mappingFileUrls));
}

Testons maintenant le code:

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper_
  whenMaps_thenCorrect() {
    configureMapper("dozer_mapping.xml");
    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

Comme indiqué dans le test,DozerBeanMapper accepte une liste de fichiers de mappage XML personnalisés et décide quand utiliser chacun d'eux au moment de l'exécution.

En supposant que nous commencions maintenant à mapper ces objets de données entre notre application anglaise et l'application française. Nous n'avons pas besoin de créer un autre mappage dans le fichier XML,Dozer is smart enough to map the objects both ways with only one mapping configuration:

@Test
public void givenSrcAndDestWithDifferentFieldNamesWithCustomMapper_
  whenMapsBidirectionally_thenCorrect() {
    configureMapper("dozer_mapping.xml");
    Person englishAppPerson = new Person("Dwayne Johnson", "The Rock", 44);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

Et donc cet exemple de test utilise cette autre fonctionnalité de Dozer - le fait quethe Dozer mapping engine is bi-directional, donc si nous voulons mapper l'objet de destination à l'objet source, nous n'avons pas besoin d'ajouter un autre mappage de classe au fichier XML.

Nous pouvons également charger un fichier de mappage personnalisé depuis l'extérieur du chemin de classe, si nécessaire, utilisez le préfixe «file:» dans le nom de la ressource.

Dans un environnement Windows (comme le test ci-dessous), nous utiliserons bien sûr la syntaxe de fichier spécifique à Windows.

Sur une box Linux, on peut stocker le fichier sous/home puis:

configureMapper("file:/home/dozer_mapping.xml");

Et sur Mac OS:

configureMapper("file:/Users/me/dozer_mapping.xml");

Si vous exécutez les tests unitaires à partir desgithub project (ce que vous devriez), vous pouvez copier le fichier de mappage à l'emplacement approprié et modifier l'entrée de la méthodeconfigureMapper.

Le fichier de mappage est disponible dans le dossier test / resources du projet GitHub:

@Test
public void givenMappingFileOutsideClasspath_whenMaps_thenCorrect() {
    configureMapper("file:E:\\dozer_mapping.xml");
    Person englishAppPerson = new Person("Marshall Bruce Mathers III","Eminem", 43);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

6. Caractères génériques et personnalisation XML supplémentaire

Créons un deuxième fichier de mappage personnalisé appelédozer_mapping2.xml:



    
        com.example.dozer.Personne
        com.example.dozer.Person
        
            nom
            name
        
        
            surnom
            nickname
        
    

Notez que nous avons ajouté un attributwildcard à l'élément<mapping></mapping> qui n'existait pas auparavant.

Par défaut,wildcard esttrue. Il indique au moteur Dozer que nous voulons que tous les champs de l'objet source soient mappés sur les champs de destination appropriés.

Lorsque nous le définissons surfalse,, nous demandons à Dozer de ne mapper que les champs que nous avons explicitement spécifiés dans le XML.

Donc, dans la configuration ci-dessus, nous ne voulons que deux champs mappés, en laissant de côtéage:

@Test
public void givenSrcAndDest_whenMapsOnlySpecifiedFields_thenCorrect() {
    configureMapper("dozer_mapping2.xml");
    Person englishAppPerson = new Person("Shawn Corey Carter","Jay Z", 46);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(),englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

Comme nous pouvons le voir dans la dernière assertion, le champ de destinationage est resté0.

7. Mappage personnalisé via des annotations

Pour les cas de mappage simples et les cas où nous avons également un accès en écriture aux objets de données que nous aimerions mapper, il peut ne pas être nécessaire d'utiliser le mappage XML.

Mapper des champs de noms différents via des annotations est très simple et nous devons écrire beaucoup moins de code que dans le mappage XML, mais nous ne pouvons nous aider que dans des cas simples.

Répliquons nos objets de données enPerson2.java etPersonne2.java sans changer les champs du tout.

Pour implémenter cela, il suffit d'ajouter l'annotation @mapper(“destinationFieldName”) sur les méthodesgetter de l'objet source. Ainsi:

@Mapping("name")
public String getNom() {
    return nom;
}

@Mapping("nickname")
public String getSurnom() {
    return surnom;
}

Cette fois, nous traitonsPersonne2 comme la source, mais cela n'a pas d'importance en raison desbi-directional nature du Dozer Engine.

Maintenant, avec tout le code XML supprimé, notre code de test est plus court:

@Test
public void givenAnnotatedSrcFields_whenMapsToRightDestField_thenCorrect() {
    Person2 englishAppPerson = new Person2("Jean-Claude Van Damme", "JCVD", 55);
    Personne2 frenchAppPerson = mapper.map(englishAppPerson, Personne2.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

Nous pouvons également tester la bidirectionnalité:

@Test
public void givenAnnotatedSrcFields_whenMapsToRightDestFieldBidirectionally_
  thenCorrect() {
    Personne2 frenchAppPerson = new Personne2("Jason Statham", "transporter", 49);
    Person2 englishAppPerson = mapper.map(frenchAppPerson, Person2.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

8. Mappage d'API personnalisé

Dans nos exemples précédents où nous démontons des objets de données d'une application française, nous avons utilisé XML et des annotations pour personnaliser notre mappage.

Une autre alternative disponible dans Dozer, similaire au mappage d'annotation est le mappage API. Ils sont similaires car nous éliminons la configuration XML et utilisons strictement le code Java.

Dans ce cas, nous utilisons la classeBeanMappingBuilder, définie dans notre cas le plus simple comme ceci:

BeanMappingBuilder builder = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom");
    }
};

Comme nous pouvons le voir, nous avons une méthode abstraite,configure(), que nous devons surcharger pour définir nos configurations. Ensuite, tout comme nos balises<mapping></mapping> en XML, nous définissons autant deTypeMappingBuilders que nécessaire.

Ces générateurs indiquent à Dozer les champs source à destination que nous mappons. Nous passons ensuite lesBeanMappingBuilder àDozerBeanMapper comme nous le ferions, le fichier de mappage XML, uniquement avec une API différente:

@Test
public void givenApiMapper_whenMaps_thenCorrect() {
    mapper.addMapping(builder);

    Personne frenchAppPerson = new Personne("Sylvester Stallone", "Rambo", 70);
    Person englishAppPerson = mapper.map(frenchAppPerson, Person.class);

    assertEquals(englishAppPerson.getName(), frenchAppPerson.getNom());
    assertEquals(englishAppPerson.getNickname(), frenchAppPerson.getSurnom());
    assertEquals(englishAppPerson.getAge(), frenchAppPerson.getAge());
}

L'API de cartographie est également bidirectionnelle:

@Test
public void givenApiMapper_whenMapsBidirectionally_thenCorrect() {
    mapper.addMapping(builder);

    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), englishAppPerson.getAge());
}

Ou nous pouvons choisir de mapper uniquement les champs explicitement spécifiés avec cette configuration de générateur:

BeanMappingBuilder builderMinusAge = new BeanMappingBuilder() {
    @Override
    protected void configure() {
        mapping(Person.class, Personne.class)
          .fields("name", "nom")
            .fields("nickname", "surnom")
              .exclude("age");
    }
};

et notre testage==0 est de retour:

@Test
public void givenApiMapper_whenMapsOnlySpecifiedFields_thenCorrect() {
    mapper.addMapping(builderMinusAge);
    Person englishAppPerson = new Person("Sylvester Stallone", "Rambo", 70);
    Personne frenchAppPerson = mapper.map(englishAppPerson, Personne.class);

    assertEquals(frenchAppPerson.getNom(), englishAppPerson.getName());
    assertEquals(frenchAppPerson.getSurnom(), englishAppPerson.getNickname());
    assertEquals(frenchAppPerson.getAge(), 0);
}

9. Convertisseurs personnalisés

Un autre scénario auquel nous pouvons faire face dans le mappage est celui où nous voudrionsperform custom mapping between two objects.

Nous avons examiné des scénarios où les noms des champs source et destination sont différents comme dans l'objet françaisPersonne. Cette section résout un problème différent.

Que faire si un objet de données que nous supprimons représente un champ de date et d'heure tel qu'unlong ou une heure Unix comme ceci:

1182882159000

Mais notre propre objet de données équivalent représente le même champ de date et d'heure et la même valeur dans ce format ISO tel qu'unString:

2007-06-26T21:22:39Z

Le convertisseur par défaut mapperait simplement la valeur longue à unString comme ceci:

"1182882159000"

Ce serait certainement un bug de notre application. Alors, comment pouvons-nous résoudre ce problème? Nous le résolvons paradding a configuration block dans le fichier XML de mappage etspecifying our own converter.

Tout d'abord, répliquons le DTOPerson de l'application distante avec unname, puis la date et l'heure de naissance, champdtob:

public class Personne3 {
    private String name;
    private long dtob;

    public Personne3(String name, long dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

    // standard getters and setters
}

et voici notre propre:

public class Person3 {
    private String name;
    private String dtob;

    public Person3(String name, String dtob) {
        super();
        this.name = name;
        this.dtob = dtob;
    }

    // standard getters and setters
}

Notez la différence de type dedtob dans les DTO source et destination.

Créons également nos propresCustomConverter à transmettre à Dozer dans le mappage XML:

public class MyCustomConvertor implements CustomConverter {
    @Override
    public Object convert(Object dest, Object source, Class arg2, Class arg3) {
        if (source == null)
            return null;

        if (source instanceof Personne3) {
            Personne3 person = (Personne3) source;
            Date date = new Date(person.getDtob());
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            String isoDate = format.format(date);
            return new Person3(person.getName(), isoDate);

        } else if (source instanceof Person3) {
            Person3 person = (Person3) source;
            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
            Date date = format.parse(person.getDtob());
            long timestamp = date.getTime();
            return new Personne3(person.getName(), timestamp);
        }
    }
}

Nous n'avons qu'à redéfinir la méthodeconvert() puis retourner ce que nous voulons y retourner. Nous sommes disponibles avec les objets source et destination et leurs types de classe.

Remarquez comment nous nous sommes occupés de la bidirectionnalité en supposant que la source peut être l’une ou l’autre des deux classes que nous mappons.

Nous allons créer un nouveau fichier de mappage pour plus de clarté,dozer_custom_convertor.xml:



    
        
            
                com.example.dozer.Personne3
                com.example.dozer.Person3
            
        
    

C'est le fichier de mappage normal que nous avons vu dans les sections précédentes, nous avons seulement ajouté un bloc<configuration></configuration> dans lequel nous pouvons définir autant de convertisseurs personnalisés que nécessaire avec leurs classes de données source et de destination respectives.

Testons notre nouveau codeCustomConverter:

@Test
public void givenSrcAndDestWithDifferentFieldTypes_whenAbleToCustomConvert_
  thenCorrect() {

    configureMapper("dozer_custom_convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Person3 person = new Person3("Rich", dateTime);
    Personne3 person0 = mapper.map(person, Personne3.class);

    assertEquals(timestamp, person0.getDtob());
}

Nous pouvons également tester pour nous assurer qu'il s'agit d'un système bidirectionnel:

@Test
public void givenSrcAndDestWithDifferentFieldTypes_
  whenAbleToCustomConvertBidirectionally_thenCorrect() {
    configureMapper("dozer_custom_convertor.xml");
    String dateTime = "2007-06-26T21:22:39Z";
    long timestamp = new Long("1182882159000");
    Personne3 person = new Personne3("Rich", timestamp);
    Person3 person0 = mapper.map(person, Person3.class);

    assertEquals(dateTime, person0.getDtob());
}

10. Conclusion

Dans ce tutoriel, nous avonsintroduced most of the basics of the Dozer Mapping library et comment l'utiliser dans nos applications.

L'implémentation complète de tous ces exemples et extraits de code peut être trouvée dans le Dozergithub project.