1. Overview

Orika is a Java Bean mapping framework that recursively copies data from one object to another. It can be very useful when developing multi-layered applications.

While moving data objects back and forth between these layers it is common to find that we need to convert objects from one instance into another to accommodate different APIs.

Some ways to achieve this are: hard coding the copying logic or to implement bean mappers like Dozer. However, it can be used to simplify the process of mapping between one object layer and another.

Orika uses byte code generation to create fast mappers with minimal overhead, making it much faster than other reflection based mappers like Dozer.

2. Simple Example

The basic cornerstone of the mapping framework is the MapperFactory class. This is the class we will use to configure mappings and obtain the MapperFacade instance which performs the actual mapping work.

We create a MapperFactory object like so:

MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();

Then assuming we have a source data object, Source.java, with two fields:

public class Source {
    private String name;
    private int age;
    
    public Source(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // standard getters and setters
}

And a similar destination data object, Dest.java:

public class Dest {
    private String name;
    private int age;
    
    public Dest(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    // standard getters and setters
}

This is the most basic of bean mapping using Orika:

@Test
public void givenSrcAndDest_whenMaps_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class);
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source("Baeldung", 10);
    Dest dest = mapper.map(src, Dest.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

As we can observe, we have created a Dest object with identical fields as Source, simply by mapping. Bidirectional or reverse mapping is also possible by default:

@Test
public void givenSrcAndDest_whenMapsReverse_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest("Baeldung", 10);
    Source dest = mapper.map(src, Source.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

3. Maven Setup

To use Orika mapper in our maven projects, we need to have orika-core dependency in pom.xml:

<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.4.6</version>
</dependency>

The latest version can always be found here.

3. Working With MapperFactory

The general pattern of mapping with Orika involves creating a MapperFactory object, configuring it incase we need to tweak the default mapping behaviour, obtaining a MapperFacade object from it and finally, actual mapping.

We shall be observing this pattern in all our examples. But our very first example showed the default behaviour of the mapper without any tweak from our side.

3.1. The BoundMapperFacade vs MapperFacade

One thing to note is that we could choose to use BoundMapperFacade over the default MapperFacade which is quite slow. These are cases where we have a specific pair of types to map.

Our initial test would thus become:

@Test
public void givenSrcAndDest_whenMapsUsingBoundMapper_thenCorrect() {
    BoundMapperFacade<Source, Dest> 
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Source src = new Source("baeldung", 10);
    Dest dest = boundMapper.map(src);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

However, for BoundMapperFacade to map bi-directionally, we have to explicitely call the mapReverse method rather than the map method we have looked at for the case of the default MapperFacade:

@Test
public void givenSrcAndDest_whenMapsUsingBoundMapperInReverse_thenCorrect() {
    BoundMapperFacade<Source, Dest> 
      boundMapper = mapperFactory.getMapperFacade(Source.class, Dest.class);
    Dest src = new Dest("baeldung", 10);
    Source dest = boundMapper.mapReverse(src);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

The test will fail otherwise.

3.2. Configure Field Mappings

The examples we have looked at so far involve source and destination classes with identical field names. This subsection tackles the case where there is a difference between the two.

Consider a source object, Person ,with three fields namely name, nickname and age:

public class Person {
    private String name;
    private String nickname;
    private int age;
    
    public Person(String name, String nickname, int age) {
        this.name = name;
        this.nickname = nickname;
        this.age = age;
    }
    
    // standard getters and setters
}

Then another layer of the application has a similar object, but written by a french programmer. Let's say that's called Personne, with fields nom, surnom and age, all corresponding to the above three:

public class Personne {
    private String nom;
    private String surnom;
    private int age;
    
    public Personne(String nom, String surnom, int age) {
        this.nom = nom;
        this.surnom = surnom;
        this.age = age;
    }
    
    // standard getters and setters
}

Orika cannot automatically resolve these differences. But we can use the ClassMapBuilder API to register these unique mappings.

We have already used it before, but we have not tapped into any of its powerful features yet. The first line of each of our preceding tests using the default MapperFacade was using the ClassMapBuilder API to register the two classes we wanted to map:

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

We could also map all fields using the default configuration, to make it clearer:

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

By adding the byDefault() method call, we are already configuring the behaviour of the mapper using the ClassMapBuilder API.

Now we want to be able to map Personne to Person, so we also configure field mappings onto the mapper using ClassMapBuilder API:

@Test
public void givenSrcAndDestWithDifferentFieldNames_whenMaps_thenCorrect() {
    mapperFactory.classMap(Personne.class, Person.class)
      .field("nom", "name").field("surnom", "nickname")
      .field("age", "age").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Personne frenchPerson = new Personne("Claire", "cla", 25);
    Person englishPerson = mapper.map(frenchPerson, Person.class);

    assertEquals(englishPerson.getName(), frenchPerson.getNom());
    assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
    assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}

Don't forget to call the register() API method in order to register the configuration with the MapperFactory.

Even if only one field differs, going down this route means we must explicitly register all field mappings, including age which is the same in both objects, otherwise the unregistered field will not be mapped and the test would fail.

This will soon become tedious, what if we only want to map one field out of 20, do we need to configure all of their mappings?

No, not when we tell the mapper to use it's default mapping configuration in cases where we have not explicitly defined a mapping:

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

Here, we have not defined a mapping for the age field, but nevertheless the test will pass.

3.3. Exclude a Field

Assuming we would like to exclude the nom field of Personne from the mapping – so that the Person object only receives new values for fields that are not excluded:

@Test
public void givenSrcAndDest_whenCanExcludeField_thenCorrect() {
    mapperFactory.classMap(Personne.class, Person.class).exclude("nom")
      .field("surnom", "nickname").field("age", "age").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Personne frenchPerson = new Personne("Claire", "cla", 25);
    Person englishPerson = mapper.map(frenchPerson, Person.class);

    assertEquals(null, englishPerson.getName());
    assertEquals(englishPerson.getNickname(), frenchPerson.getSurnom());
    assertEquals(englishPerson.getAge(), frenchPerson.getAge());
}

Notice how we exclude it in the configuration of the MapperFactory and then notice also the first assertion where we expect the value of name in the Person object to remain null, as a result of it being excluded in mapping.

4. Collections Mapping

Sometimes the destination object may have unique attributes while the source object just maintains every property in a collection.

4.1. Lists and Arrays

Consider a source data object that only has one field, a list of a person's names:

public class PersonNameList {
    private List<String> nameList;
    
    public PersonNameList(List<String> nameList) {
        this.nameList = nameList;
    }
}

Now consider our destination data object which separates firstName and lastName into separate fields:

public class PersonNameParts {
    private String firstName;
    private String lastName;

    public PersonNameParts(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Let's assume we are very sure that at index 0 there will always be the firstName of the person and at index 1 there will always be their lastName.

Orika allows us to use the bracket notation to access members of a collection:

@Test
public void givenSrcWithListAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonNameList.class, PersonNameParts.class)
      .field("nameList[0]", "firstName")
      .field("nameList[1]", "lastName").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    List<String> nameList = Arrays.asList(new String[] { "Sylvester", "Stallone" });
    PersonNameList src = new PersonNameList(nameList);
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Sylvester");
    assertEquals(dest.getLastName(), "Stallone");
}

Even if instead of PersonNameList, we had PersonNameArray, the same test would pass for an array of names.

4.2. Maps

Assuming our source object has a map of values. We know there is a key in that map, first, whose value represents a person's firstName in our destination object.

Likewise we know that there is another key, last, in the same map whose value represents a person's lastName in the destination object.

public class PersonNameMap {
    private Map<String, String> nameMap;

    public PersonNameMap(Map<String, String> nameMap) {
        this.nameMap = nameMap;
    }
}

Similar to the case in the preceding section, we use bracket notation, but instead of passing in an index, we pass in the key whose value we want to map to the given destination field.

Orika accepts two ways of retrieving the key, both are represented in the following test:

@Test
public void givenSrcWithMapAndDestWithPrimitiveAttributes_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonNameMap.class, PersonNameParts.class)
      .field("nameMap['first']", "firstName")
      .field("nameMap[\"last\"]", "lastName")
      .register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Map<String, String> nameMap = new HashMap<>();
    nameMap.put("first", "Leornado");
    nameMap.put("last", "DiCaprio");
    PersonNameMap src = new PersonNameMap(nameMap);
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Leornado");
    assertEquals(dest.getLastName(), "DiCaprio");
}

We can use either single quotes or double quotes but we must escape the latter.

5. Map Nested Fields

Following on from the preceding collections examples, assume that inside our source data object, there is another Data Transfer Object (DTO) that holds the values we want to map.

public class PersonContainer {
    private Name name;
    
    public PersonContainer(Name name) {
        this.name = name;
    }
}
public class Name {
    private String firstName;
    private String lastName;
    
    public Name(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

To be able to access the properties of the nested DTO and map them onto our destination object, we use dot notation, like so:

@Test
public void givenSrcWithNestedFields_whenMaps_thenCorrect() {
    mapperFactory.classMap(PersonContainer.class, PersonNameParts.class)
      .field("name.firstName", "firstName")
      .field("name.lastName", "lastName").register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    PersonContainer src = new PersonContainer(new Name("Nick", "Canon"));
    PersonNameParts dest = mapper.map(src, PersonNameParts.class);

    assertEquals(dest.getFirstName(), "Nick");
    assertEquals(dest.getLastName(), "Canon");
}

6. Mapping Null Values

In some cases, you may wish to control whether nulls are mapped or ignored when they are encountered. By default, Orika will map null values when encountered:

@Test
public void givenSrcWithNullField_whenMapsThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = mapper.map(src, Dest.class);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

This behavior can be customized at different levels depending on how specific we would like to be.

6.1. Global Configuration

We can configure our mapper to map nulls or ignore them at the global level before creating the global MapperFactory. Remember how we created this object in our very first example? This time we add an extra call during the build process:

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

We can run a test to confirm that indeed, nulls are not getting mapped:

@Test
public void givenSrcWithNullAndGlobalConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class);
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

What happens is that, by default, nulls are mapped. This means that even if a field value in the source object is null and the corresponding field's value in the destination object has a meaningful value, it will be overwritten.

In our case, the destination field is not overwritten if its corresponding source field has a null value.

6.2. Local Configuration

Mapping of null values can be controlled on a ClassMapBuilder by using the mapNulls(true|false) or mapNullsInReverse(true|false) for controlling mapping of nulls in the reverse direction.

By setting this value on a ClassMapBuilder instance, all field mappings created on the same ClassMapBuilder, after the value is set, will take on that same value.

Let's illustrate this with an example test:

@Test
public void givenSrcWithNullAndLocalConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .mapNulls(false).field("name", "name").byDefault().register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

Notice how we call mapNulls just before registering name field, this will cause all fields following the mapNulls call to be ignored when they have null value.

Bi-directional mapping also accepts mapped null values:

@Test
public void givenDestWithNullReverseMappedToSource_whenMapsByDefault_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).byDefault();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest(null, 10);
    Source dest = new Source("Vin", 44);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), src.getName());
}

Also we can prevent this by calling mapNullsInReverse and passing in false:

@Test
public void 
  givenDestWithNullReverseMappedToSourceAndLocalConfigForNoNull_whenFailsToMap_thenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .mapNullsInReverse(false).field("name", "name").byDefault()
      .register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Dest src = new Dest(null, 10);
    Source dest = new Source("Vin", 44);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Vin");
}

6.3. Field Level Configuration

We can configure this at the field level using fieldMap, like so:

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

In this case, the configuration will only affect the name field as we have called it at field level:

@Test
public void givenSrcWithNullAndFieldLevelConfigForNoNull_whenFailsToMap_ThenCorrect() {
    mapperFactory.classMap(Source.class, Dest.class).field("age", "age")
      .fieldMap("name", "name").mapNulls(false).add().byDefault().register();
    MapperFacade mapper = mapperFactory.getMapperFacade();
    Source src = new Source(null, 10);
    Dest dest = new Dest("Clinton", 55);
    mapper.map(src, dest);

    assertEquals(dest.getAge(), src.getAge());
    assertEquals(dest.getName(), "Clinton");
}

7. Orika Custom Mapping

So far, we have looked at simple custom mapping examples using the ClassMapBuilder API. We shall still use the same API but customize our mapping using Orika's CustomMapper class.

Assuming we have two data objects each with a certain field called dtob, representing the date and time of the birth of a person.

One data object represents this value as a datetime String in the following ISO format:

2007-06-26T21:22:39Z

and the other represents the same as a long type in the following unix timestamp format:

1182882159000

Clearly, non of the customizations we have covered so far suffices to convert between the two formats during the mapping process, not even Orika's built in converter can handle the job. This is where we have to write a CustomMapper to do the required conversion during mapping.

Let us create our first data object:

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

then our second data object:

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

We will not label which is source and which is destination right now as the CustomMapper enables us to cater for bi-directional mapping.

Here is our concrete implementation of the CustomMapper abstract class:

class PersonCustomMapper extends CustomMapper<Personne3, Person3> {

    @Override
    public void mapAtoB(Personne3 a, Person3 b, MappingContext context) {
        Date date = new Date(a.getDtob());
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        String isoDate = format.format(date);
        b.setDtob(isoDate);
    }

    @Override
    public void mapBtoA(Person3 b, Personne3 a, MappingContext context) {
        DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
        Date date = format.parse(b.getDtob());
        long timestamp = date.getTime();
        a.setDtob(timestamp);
    }
};

Notice that we have implemented methods mapAtoB and mapBtoA. Implementing both makes our mapping function bi-directional.

Each method exposes the data objects we are mapping and we take care of copying the field values from one to the other.

There in is where we write the custom code to manipulate the source data according to our requirements before writing it to the destination object.

Let's run a test to confirm that our custom mapper works:

@Test
public void givenSrcAndDest_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 = new Personne3("Leornardo", timestamp);
    Person3 person3 = mapper.map(personne3, Person3.class);

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

Notice that we still pass the custom mapper to Orika's mapper via ClassMapBuilder API, just like all other simple customizations.

We can confirm too that bi-directional mapping works:

@Test
public void givenSrcAndDest_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 = new Person3("Leornardo", dateTime);
    Personne3 personne3 = mapper.map(person3, Personne3.class);

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

8. Conclusion

In this article, we have explored the most important features of the Orika mapping framework.

There are definitely more advanced features that give us much more control but in most use cases, the ones covered here will be more than enough.

The full project code and all examples can be found in my github project. Don't forget to check out our tutorial on the Dozer mapping framework as well, since they both solve more or less the same problem.


« 上一篇: JaCoCo 介绍