In kaart brengen met Orika

1. Overzicht

Orika is een Java Bean-mappingraamwerk dat kopieert recursief gegevens van het ene object naar het andere. Het kan erg handig zijn bij het ontwikkelen van meerlagige applicaties.

Bij het heen en weer verplaatsen van gegevensobjecten tussen deze lagen komt het vaak voor dat we objecten van de ene instantie naar de andere moeten converteren om verschillende API's mogelijk te maken.

Enkele manieren om dit te bereiken zijn: hard coderen van de kopieerlogica of om bean mappers zoals Dozer te implementeren. Het kan echter worden gebruikt om het proces van mapping tussen de ene objectlaag en de andere te vereenvoudigen.

Orika gebruikt bytecode-generatie om snelle mappers te maken met minimale overhead, waardoor het veel sneller is dan andere op reflectie gebaseerde mappers zoals Dozer.

2. Eenvoudig voorbeeld

De fundamentele hoeksteen van het mapping framework is het MapperFactory klasse. Dit is de klasse die we zullen gebruiken om toewijzingen te configureren en de MapperFacade instantie die het eigenlijke mappingwerk uitvoert.

We creëren een MapperFactory object zoals zo:

MapperFactory mapperFactory = nieuwe DefaultMapperFactory.Builder (). Build ();

Stel dat we een brongegevensobject hebben, Bron.java, met twee velden:

openbare klasse Bron {naam van privé-tekenreeks; privé int leeftijd; openbare bron (tekenreeksnaam, int leeftijd) {this.name = naam; this.age = leeftijd; } // standaard getters en setters}

En een soortgelijk bestemmingsgegevensobject, Dest.java:

public class Dest {private String naam; privé int leeftijd; openbare bestemming (tekenreeksnaam, int leeftijd) {this.name = naam; this.age = leeftijd; } // standaard getters en setters}

Dit is de meest elementaire manier van bean-mapping met Orika:

@Test openbare ongeldige gegevenSrcAndDest_whenMaps_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade (); Source src = nieuwe bron ("Baeldung", 10); Dest dest = mapper.map (src, Dest.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Zoals we kunnen zien, hebben we een Best object met identieke velden als Bron, simpelweg door in kaart te brengen. Bidirectionele of omgekeerde mapping is ook standaard mogelijk:

@Test openbare ongeldige gegevenSrcAndDest_whenMapsReverse_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = nieuwe Dest ("Baeldung", 10); Bron dest = mapper.map (src, Source.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

3. Maven-instellingen

Om Orika-mapper te gebruiken in onze maven-projecten, hebben we orika-kern afhankelijkheid in pom.xml:

 ma.glasnost.orika orika-core 1.4.6 

De laatste versie is hier altijd te vinden.

3. Werken met MapperFactory

Het algemene patroon van mapping met Orika omvat het maken van een MapperFactory object, het configureren voor het geval we het standaard mappinggedrag moeten aanpassen en een MapperFacade object ervan en tot slot, daadwerkelijke mapping.

We zullen dit patroon in al onze voorbeelden observeren. Maar ons allereerste voorbeeld toonde het standaardgedrag van de mapper zonder enige aanpassing van onze kant.

3.1. De BoundMapperFacade vs MapperFacade

Een ding om op te merken is dat we ervoor kunnen kiezen om BoundMapperFacade boven de standaard MapperFacade wat vrij traag is. Dit zijn gevallen waarin we een specifiek paar typen hebben om in kaart te brengen.

Onze eerste test zou dus worden:

@Test openbare leegte gegevenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect () {BoundMapperFacade boundMapper = mapperFactory.getMapperFacade (Source.class, Dest.class); Source src = nieuwe bron ("baeldung", 10); Dest dest = boundMapper.map (src); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Echter, voor BoundMapperFacade om bidirectioneel in kaart te brengen, moeten we expliciet de mapReverse methode in plaats van de kaartmethode die we hebben bekeken voor het geval van de standaard MapperFacade:

@Test openbare ongeldige gegevenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect () {BoundMapperFacade boundMapper = mapperFactory.getMapperFacade (Source.class, Dest.class); Dest src = nieuwe Dest ("baeldung", 10); Bron dest = boundMapper.mapReverse (src); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Anders mislukt de test.

3.2. Configureer veldtoewijzingen

De voorbeelden die we tot nu toe hebben bekeken, hebben betrekking op bron- en bestemmingsklassen met identieke veldnamen. Deze onderafdeling behandelt het geval waarin er een verschil is tussen de twee.

Beschouw een bronobject, Persoon , met namelijk drie velden naam, bijnaam en leeftijd:

openbare klasse Persoon {privé Stringnaam; private String-bijnaam; privé int leeftijd; publieke persoon (String naam, String nickname, int leeftijd) {this.name = name; this.nickname = bijnaam; this.age = leeftijd; } // standaard getters en setters}

Dan heeft een andere laag van de applicatie een soortgelijk object, maar geschreven door een Franse programmeur. Laten we zeggen dat dat heet Personne, met velden nom, som en leeftijd, die allemaal overeenkomen met de bovenstaande drie:

openbare klasse Personne {privé String nom; privé String surnom; privé int leeftijd; public Personne (String nom, String surnom, int age) {this.nom = nom; this.surnom = surnom; this.age = leeftijd; } // standaard getters en setters}

Orika kan deze verschillen niet automatisch oplossen. Maar we kunnen de ClassMapBuilder API om deze unieke toewijzingen te registreren.

We hebben het al eerder gebruikt, maar we hebben nog geen gebruik gemaakt van een van de krachtige functies. De eerste regel van elk van onze voorgaande tests met de default MapperFacade gebruikte de ClassMapBuilder API om de twee klassen te registreren die we in kaart wilden brengen:

mapperFactory.classMap (Source.class, Dest.class);

We kunnen ook alle velden in kaart brengen met behulp van de standaardconfiguratie, om het duidelijker te maken:

mapperFactory.classMap (Source.class, Dest.class) .byDefault ()

Door de standaard() method call, we zijn het gedrag van de mapper al aan het configureren met behulp van de ClassMapBuilder API.

Nu willen we in kaart kunnen brengen Personne naar Persoon, dus we configureren ook veldtoewijzingen op de mapper met ClassMapBuilder API:

@Test openbare ongeldige gegevenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect () {mapperFactory.classMap (Personne.class, Person.class) .field ("nom", "name"). Field ("surnom", "nickname") .field ("age", " leeftijd "). register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Personne frenchPerson = nieuwe persoon ("Claire", "cla", 25); Persoon englishPerson = mapper.map (frenchPerson, Person.class); assertEquals (englishPerson.getName (), frenchPerson.getNom ()); assertEquals (englishPerson.getNickname (), frenchPerson.getSurnom ()); assertEquals (englishPerson.getAge (), frenchPerson.getAge ()); }

Vergeet niet het registreren() API-methode om de configuratie te registreren met de MapperFactory.

Zelfs als er maar één veld verschilt, betekent het volgen van deze route dat we ons expliciet moeten registreren alle veldtoewijzingen, inclusief leeftijd wat hetzelfde is in beide objecten, anders wordt het niet-geregistreerde veld niet in kaart gebracht en zou de test mislukken.

Dit wordt snel vervelend, wat als we slechts één veld van de 20 in kaart willen brengen?, moeten we al hun toewijzingen configureren?

Nee, niet wanneer we de mapper vertellen om zijn standaard mapping-configuratie te gebruiken in gevallen waarin we niet expliciet een mapping hebben gedefinieerd:

mapperFactory.classMap (Personne.class, Person.class) .field ("nom", "name"). field ("surnom", "nickname"). byDefault (). register ();

Hier hebben we geen mapping gedefinieerd voor de leeftijd veld, maar toch zal de test slagen.

3.3. Sluit een veld uit

Ervan uitgaande dat we de nom gebied van Personne van de mapping - zodat de Persoon object ontvangt alleen nieuwe waarden voor velden die niet zijn uitgesloten:

@Test openbare ongeldige gegevenSrcAndDest_whenCanExcludeField_thenCorrect () {mapperFactory.classMap (Personne.class, Person.class) .exclude ("nom") .field ("surnom", "nickname"). Field ("age", "age"). registreren(); MapperFacade mapper = mapperFactory.getMapperFacade (); Personne frenchPerson = nieuwe persoon ("Claire", "cla", 25); Persoon englishPerson = mapper.map (frenchPerson, Person.class); assertEquals (null, englishPerson.getName ()); assertEquals (englishPerson.getNickname (), frenchPerson.getSurnom ()); assertEquals (englishPerson.getAge (), frenchPerson.getAge ()); }

Merk op hoe we het uitsluiten in de configuratie van het MapperFactory en let dan ook op de eerste bewering waar we de waarde van verwachten naam in de Persoon bezwaar maken om te blijven nul, omdat het wordt uitgesloten bij het in kaart brengen.

4. Verzamelingen in kaart brengen

Soms heeft het doelobject unieke attributen, terwijl het bronobject gewoon elke eigenschap in een verzameling behoudt.

4.1. Lijsten en arrays

Beschouw een brongegevensobject dat slechts één veld heeft, een lijst met de namen van een persoon:

openbare klasse PersonNameList {naamlijst privélijst; openbare PersonNameList (List nameList) {this.nameList = nameList; }}

Beschouw nu ons bestemmingsgegevensobject dat scheidt Voornaam en achternaam in afzonderlijke velden:

openbare klasse PersonNameParts {private String firstName; private String achternaam; openbare PersonNameParts (String firstName, String lastName) {this.firstName = firstName; this.lastName = achternaam; }}

Laten we aannemen dat we er heel zeker van zijn dat er bij index 0 altijd de Voornaam van de persoon en bij index 1 zullen er altijd hun achternaam.

Met Orika kunnen we de haakjesnotatie gebruiken om toegang te krijgen tot leden van een collectie:

@Test openbare ongeldige gegevenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect () {mapperFactory.classMap (PersonNameList.class, PersonNameParts.class) .field ("nameList [0]", "firstName") .Name ("nameList [1") ". (); MapperFacade mapper = mapperFactory.getMapperFacade (); Lijst nameList = Arrays.asList (nieuwe String [] {"Sylvester", "Stallone"}); PersonNameList src = nieuwe PersonNameList (nameList); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Sylvester"); assertEquals (dest.getLastName (), "Stallone"); }

Zelfs als in plaats van PersonNameList, wij hadden PersonNameArray, dezelfde test zou slagen voor een reeks namen.

4.2. Kaarten

Ervan uitgaande dat ons bronobject een kaart met waarden heeft. We weten dat er een sleutel in die kaart zit, eerste, waarvan de waarde die van een persoon vertegenwoordigt Voornaam in ons bestemmingsobject.

Evenzo weten we dat er nog een sleutel is, laatste, op dezelfde kaart waarvan de waarde die van een persoon vertegenwoordigt achternaam in het bestemmingsobject.

openbare klasse PersonNameMap {private Map nameMap; openbare PersonNameMap (Map nameMap) {this.nameMap = nameMap; }}

Vergelijkbaar met het geval in de voorgaande sectie, gebruiken we de notatie tussen haakjes, maar in plaats van een index door te geven, geven we de sleutel door waarvan we de waarde willen toewijzen aan het opgegeven bestemmingsveld.

Orika accepteert twee manieren om de sleutel op te halen, beide worden weergegeven in de volgende test:

@Test openbare ongeldige gegevenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect () {mapperFactory.classMap (PersonNameMap.class, PersonNameParts.class) .field ("nameMap ['first']", "firstName") .field ("name \"] "\" "achternaam") .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Map nameMap = nieuwe HashMap (); nameMap.put ("eerste", "Leornado"); nameMap.put ("last", "DiCaprio"); PersonNameMap src = nieuwe PersonNameMap (nameMap); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Leornado"); assertEquals (dest.getLastName (), "DiCaprio"); }

We kunnen enkele aanhalingstekens of dubbele aanhalingstekens gebruiken, maar we moeten aan het laatste ontsnappen.

5. Wijs geneste velden toe

Ga er in navolging van de voorgaande verzamelingsvoorbeelden van uit dat er in ons brongegevensobject nog een Data Transfer Object (DTO) is met de waarden die we willen toewijzen.

openbare klasse PersonContainer {private naam naam; openbare PersonContainer (naam naam) {this.name = naam; }}
naam openbare klasse {private String firstName; private String achternaam; openbare naam (String firstName, String lastName) {this.firstName = firstName; this.lastName = achternaam; }}

Om toegang te krijgen tot de eigenschappen van de geneste DTO en deze toe te wijzen aan ons bestemmingsobject, gebruiken we puntnotatie, zoals zo:

@Test openbare ongeldige gegevenSrcWithNestedFields_whenMaps_thenCorrect () {mapperFactory.classMap (PersonContainer.class, PersonNameParts.class) .field ("name.firstName", "firstName") .field ("name.lastName", "lastName"). Register () ; MapperFacade mapper = mapperFactory.getMapperFacade (); PersonContainer src = nieuwe PersonContainer (nieuwe naam ("Nick", "Canon")); PersonNameParts dest = mapper.map (src, PersonNameParts.class); assertEquals (dest.getFirstName (), "Nick"); assertEquals (dest.getLastName (), "Canon"); }

6. Null-waarden in kaart brengen

In sommige gevallen wilt u misschien bepalen of null-waarden worden toegewezen of genegeerd wanneer ze worden aangetroffen. Standaard zal Orika null-waarden toewijzen wanneer ze worden aangetroffen:

@Test openbare leegte gegevenSrcWithNullField_whenMapsThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Source src = nieuwe bron (null, 10); Dest dest = mapper.map (src, Dest.class); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Dit gedrag kan op verschillende niveaus worden aangepast, afhankelijk van hoe specifiek we zouden willen zijn.

6.1. Globale configuratie

We kunnen onze mapper configureren om null-waarden toe te wijzen of ze op globaal niveau te negeren voordat we de global MapperFactory. Weet je nog hoe we dit object in ons allereerste voorbeeld hebben gemaakt? Deze keer voegen we een extra oproep toe tijdens het bouwproces:

MapperFactory mapperFactory = nieuwe DefaultMapperFactory.Builder () .mapNulls (false) .build ();

We kunnen een test uitvoeren om te bevestigen dat de null-waarden inderdaad niet in kaart worden gebracht:

@Test openbare leegte gegevenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class); MapperFacade mapper = mapperFactory.getMapperFacade (); Source src = nieuwe bron (null, 10); Dest dest = nieuwe Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

Wat er gebeurt, is dat null-waarden standaard worden toegewezen. Dit betekent dat zelfs als een veldwaarde in het bronobject nul en de waarde van het overeenkomstige veld in het bestemmingsobject een betekenisvolle waarde heeft, wordt deze overschreven.

In ons geval wordt het bestemmingsveld niet overschreven als het corresponderende bronveld een nul waarde.

6.2. Lokale configuratie

In kaart brengen van nul waarden kunnen worden gecontroleerd op een ClassMapBuilder door de mapNulls (true | false) of mapNullsInReverse (true | false) voor het besturen van het in kaart brengen van nullen in de omgekeerde richting.

Door deze waarde in te stellen op a ClassMapBuilder bijvoorbeeld alle veldtoewijzingen die op hetzelfde ClassMapBuilder, nadat de waarde is ingesteld, dezelfde waarde aannemen.

Laten we dit illustreren met een voorbeeldtest:

@Test openbare ongeldige gegevenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .mapNulls (false) .field ("name", "name"). ByDefault ("name", "name"). ByDefault ( ).registreren(); MapperFacade mapper = mapperFactory.getMapperFacade (); Source src = nieuwe bron (null, 10); Dest dest = nieuwe Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

Let op hoe we bellen mapNulls net voordat u zich registreert naam veld, hierdoor worden alle velden volgend op het mapNulls oproep om te worden genegeerd wanneer ze hebben nul waarde.

Bidirectionele mapping accepteert ook toegewezen null-waarden:

@Test openbare ongeldige gegevenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .byDefault (); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = nieuwe Dest (null, 10); Bronbestemming = nieuwe bron ("Vin", 44); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), src.getName ()); }

Ook kunnen we dit voorkomen door te bellen mapNullsInReverse en passeren false:

@Test openbare ongeldige gegevenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("age", "age") .mapNullsInReverse (name byD) .field (naam byDefault "). ). registreren (); MapperFacade mapper = mapperFactory.getMapperFacade (); Dest src = nieuwe Dest (null, 10); Bronbestemming = nieuwe bron ("Vin", 44); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Vin"); }

6.3. Configuratie op veldniveau

We kunnen dit op veldniveau configureren met fieldMap, zoals zo:

mapperFactory.classMap (Source.class, Dest.class) .field ("leeftijd", "leeftijd") .fieldMap ("naam", "naam"). mapNulls (false) .add (). byDefault (). register ( );

In dit geval heeft de configuratie alleen invloed op het naam veld zoals we het op veldniveau hebben genoemd:

@Test openbare leegte gegevenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect () {mapperFactory.classMap (Source.class, Dest.class) .field ("leeftijd", "leeftijd") .fieldMap ("naam", "naam"). MapNullads (false). ) .byDefault (). register (); MapperFacade mapper = mapperFactory.getMapperFacade (); Source src = nieuwe bron (null, 10); Dest dest = nieuwe Dest ("Clinton", 55); mapper.map (src, dest); assertEquals (dest.getAge (), src.getAge ()); assertEquals (dest.getName (), "Clinton"); }

7. Orika Custom Mapping

Tot nu toe hebben we gekeken naar eenvoudige aangepaste toewijzingsvoorbeelden met behulp van de ClassMapBuilder API. We zullen nog steeds dezelfde API gebruiken, maar onze mapping aanpassen met Orika's CustomMapper klasse.

Ervan uitgaande dat we twee data-objecten hebben, elk met een bepaald veld genaamd dtob, die de datum en tijd van de geboorte van een persoon vertegenwoordigt.

Een data-object vertegenwoordigt deze waarde als een datetime String in het volgende ISO-formaat:

2007-06-26T21: 22: 39Z

en de andere staat voor hetzelfde als een lang typ het volgende Unix-tijdstempelformaat in:

1182882159000

Het is duidelijk dat geen van de aanpassingen die we tot nu toe hebben behandeld voldoende is om tijdens het mappingproces tussen de twee formaten te converteren, zelfs de ingebouwde converter van Orika kan de klus niet aan. Dit is waar we een moeten schrijven CustomMapper om de vereiste conversie uit te voeren tijdens het in kaart brengen.

Laten we ons eerste data-object maken:

openbare klasse Person3 {private String-naam; privé String dtob; public Person3 (String naam, String dtob) {this.name = name; this.dtob = dtob; }}

dan ons tweede data-object:

openbare klasse Personne3 {private String-naam; privé lange dtob; public Personne3 (String naam, lange dtob) {this.name = naam; this.dtob = dtob; }}

We zullen de bron en de bestemming op dit moment niet labelen als het CustomMapper stelt ons in staat om bi-directionele mapping te verzorgen.

Hier is onze concrete implementatie van de CustomMapper abstracte klasse:

klasse PersonCustomMapper breidt CustomMapper uit {@Override public void mapAtoB (Personne3 a, Person3 b, MappingContext context) {Date date = new Date (a.getDtob ()); DateFormat format = nieuwe SimpleDateFormat ("jjjj-MM-dd'T'HH: mm: ss'Z '"); String isoDate = format.format (datum); b.setDtob (isoDate); } @Override public void mapBtoA (Person3 b, Personne3 a, MappingContext context) {DateFormat format = new SimpleDateFormat ("jjjj-MM-dd'T'HH: mm: ss'Z '"); Datum datum = format.parse (b.getDtob ()); lange tijdstempel = date.getTime (); a.setDtob (tijdstempel); }};

Merk op dat we methoden hebben geïmplementeerd mapAtoB en mapBtoA. Door beide te implementeren, is onze mapping-functie bidirectioneel.

Elke methode legt de data-objecten bloot die we in kaart brengen en we zorgen ervoor dat de veldwaarden van de ene naar de andere worden gekopieerd.

Daarin schrijven we de aangepaste code om de brongegevens volgens onze vereisten te manipuleren voordat we deze naar het bestemmingsobject schrijven.

Laten we een test uitvoeren om te bevestigen dat onze aangepaste mapper werkt:

@Test openbare ongeldige gegevenSrcAndDest_whenCustomMapperWorks_thenCorrect () {mapperFactory.classMap (Personne3.class, Person3.class) .customize (customMapper) .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); String dateTime = "2007-06-26T21: 22: 39Z"; long timestamp = new Long ("1182882159000"); Personne3 personne3 = nieuwe Personne3 ("Leornardo", tijdstempel); Person3 person3 = mapper.map (personne3, Person3.class); assertEquals (person3.getDtob (), dateTime); }

Merk op dat we de aangepaste mapper nog steeds doorgeven aan de mapper van Orika via ClassMapBuilder API, net als alle andere eenvoudige aanpassingen.

We kunnen ook bevestigen dat bi-directionele mapping werkt:

@Test openbare leegte gegevenSrcAndDest_whenCustomMapperWorksBidirectionally_thenCorrect () {mapperFactory.classMap (Personne3.class, Person3.class) .customize (customMapper) .register (); MapperFacade mapper = mapperFactory.getMapperFacade (); String dateTime = "2007-06-26T21: 22: 39Z"; long timestamp = new Long ("1182882159000"); Person3 person3 = nieuwe Person3 ("Leornardo", dateTime); Personne3 personne3 = mapper.map (person3, Personne3.class); assertEquals (person3.getDtob (), tijdstempel); }

8. Conclusie

In dit artikel hebben we verkende de belangrijkste kenmerken van het Orika-mappingraamwerk.

Er zijn zeker meer geavanceerde functies die ons veel meer controle geven, maar in de meeste gevallen zijn de functies die hier worden behandeld meer dan voldoende.

De volledige projectcode en alle voorbeelden zijn te vinden in mijn github-project. Vergeet niet om ook onze tutorial over het Dozer mapping framework te bekijken, aangezien ze allebei min of meer hetzelfde probleem oplossen.


$config[zx-auto] not found$config[zx-overlay] not found