1. Overview

Dozer is a Java Bean to Java Bean mapper that recursively copies data from one object to another, attribute by attribute.

The library not only supports mapping between attribute names of Java Beans, but also automatically converts between types – if they’re different.

Most conversion scenarios are supported out of the box, but Dozer also allows you to specify custom conversions via XML.

2. Simple Example

For our first example, let’s assume that the source and destination data objects all share the same common attribute names.

This is the most basic mapping one can do with 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
}

Then our destination file, 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
}

We need to make sure to include the default or zero argument constructors, since Dozer uses reflection under the hood.

And, for performance purposes, let’s make our mapper global and create a single object we’ll use throughout our tests:

DozerBeanMapper mapper;

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

Now, let’s run our first test to confirm that when we create a Source object, we can map it directly onto a Dest object:

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

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

As we can see, after the Dozer mapping, the result will be a new instance of the Dest object that contains values for all fields that have the same field name as the Source object.

Alternatively, instead of passing mapper the Dest class, we could just have created the Dest object and passed mapper its reference:

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

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

3. Maven Setup

Now that we have a basic understanding of how Dozer works, let’s add the following dependency to the pom.xml:

<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>

The latest version is available here.

4. Data Conversion Example

As we already know, Dozer can map an existing object to another as long as it finds attributes of the same name in both classes.

However, that’s not always the case; and so, if any of the mapped attributes are of different data types, the Dozer mapping engine will automatically perform a data type conversion.

Let’s see this new concept in 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
}

And the destination class:

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
}

Notice that the attribute names are the same but their data types are different.

In the source class, id is a String and points is a double, whereas in the destination class, id and points are both integers.

Let’s now see how Dozer correctly handles the 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);
}

We passed “320” and 15.2, a String and a double into the source object and the result had 320 and 15, both integers in the destination object.

5. Basic Custom Mappings via XML

In all the previous examples we have seen, both the source and destination data objects have the same field names, which allows for easy mapping on our side.

However, in real world applications, there will be countless times where the two data objects we’re mapping won’t have fields that share a common property name.

To solve this, Dozer gives us an option to create a custom mapping configuration in XML.

In this XML file, we can define class mapping entries which the Dozer mapping engine will use to decide what source attribute to map to what destination attribute.

Let’s have a look at an example, and let’s try unmarshalling data objects from an application built by a French programmer, to an English style of naming our objects.

We have a Person object with name, nickname and age fields:

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
}

The object we are unmarshalling is named Personne and has fields nom, surnom and age:

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
}

These objects really achieve the same purpose but we have a language barrier. In order to help with that barrier, we can use Dozer to map the French Personne object to our Person object.

We only have to create a custom mapping file to help Dozer do this, we will call it dozer_mapping.xml:

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://dozer.sourceforge.net
      http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <mapping>
        <class-a>com.baeldung.dozer.Personne</class-a>
        <class-b>com.baeldung.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

This is the simplest example of a custom XML mapping file we can have.

For now, it’s enough to notice that we have as our root element, which has a child , we can have as many of these children inside as there are incidences of class pairs that need custom mapping.

Notice also how we specify the source and destination classes inside the tags. This is followed by a for each source and destination field pair that need custom mapping.

Finally, notice that we have not included the field age in our custom mapping file. The French word for age is still age, which brings us to another important feature of Dozer.

Properties that are of the same name do not need to be specified in the mapping XML file. Dozer automatically maps all fields with the same property name from the source object into the destination object.

We will then place our custom XML file on the classpath directly under the src folder. However, wherever we place it on the classpath, Dozer will search the entire classpath looking for the specified file.

Let us create a helper method to add mapping files to our mapper:

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

Let’s now test the 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());
}

As shown in the test, DozerBeanMapper accepts a list of custom XML mapping files and decides when to use each at runtime.

Assuming we now start unmarshalling these data objects back and forth between our English app and the French app. We don’t need to create another mapping in the XML file, 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());
}

And so this example test uses this another feature of Dozer – the fact that the Dozer mapping engine is bi-directional, so if we want to map the destination object to the source object, we do not need to add another class mapping to the XML file.

We can also load a custom mapping file from outside the classpath, if we need to, use the “*file:*” prefix in the resource name.

On a Windows environment (such as the test below), we’ll of course use the Windows specific file syntax.

On a Linux box, we may store the file under /home and then:

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

And on Mac OS:

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

If you are running the unit tests from the github project (which you should), you can copy the mapping file to the appropriate location and change the input for configureMapper method.

The mapping file is available under test/resources folder of the GitHub project:

@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. Wildcards and Further XML Customization

Let’s create a second custom mapping file called dozer_mapping2.xml:

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://dozer.sourceforge.net 
      http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <mapping wildcard="false">
        <class-a>com.baeldung.dozer.Personne</class-a>
        <class-b>com.baeldung.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

Notice that we have added an attribute wildcard to the element which was not there before.

By default, wildcard is true. It tells the Dozer engine that we want all fields in the source object to be mapped to their appropriate destination fields.

When we set it to false, we are telling Dozer to only map fields we have explicitly specified in the XML.

So in the above configuration, we only want two fields mapped, leaving out 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);
}

As we can see in the last assertion, the destination age field remained 0.

7. Custom Mapping via Annotations

For simple mapping cases and cases where we also have write access to the data objects we would like to map, we may not need to use XML mapping.

Mapping differently named fields via annotations is very simple and we have to write much less code than in XML mapping but can only help us in simple cases.

Let’s replicate our data objects into Person2.java and Personne2.java without changing the fields at all.

To implement this, we only need to add @mapper(“destinationFieldName”) annotation on the getter methods in the source object. Like so:

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

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

This time we are treating Personne2 as the source, but it does not matter due to the bi-directional nature of the Dozer Engine.

Now with all the XML related code stripped out, our test code is shorter:

@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());
}

We can also test for bi-directionality:

@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. Custom API Mapping

In our previous examples where we are unmarshalling data objects from a french application, we used XML and annotations to customize our mapping.

Another alternative available in Dozer, similar to annotation mapping is API mapping. They are similar because we eliminate XML configuration and strictly use Java code.

In this case, we use BeanMappingBuilder class, defined in our simplest case like so:

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

As we can see, we have an abstract method, configure(), which we must override to define our configurations. Then, just like our tags in XML, we define as many TypeMappingBuilders as we require.

These builders tell Dozer which source to destination fields we are mapping. We then pass the BeanMappingBuilder to DozerBeanMapper as we would, the XML mapping file, only with a different API:

@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());
}

The mapping API is also bi-directional:

@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());
}

Or we can choose to only map explicitly specified fields with this builder configuration:

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

and our age==0 test is back:

@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. Custom Converters

Another scenario we may face in mapping is where we would like to perform custom mapping between two objects.

We have looked at scenarios where source and destination field names are different like in the French Personne object. This section solves a different problem.

What if a data object we are unmarshalling represents a date and time field such as a long or Unix time like so:

1182882159000

But our own equivalent data object represents the same date and time field and value in this ISO format such as a String:

2007-06-26T21:22:39Z

The default converter would simply map the long value to a String like so:

"1182882159000"

This would definitely bug our app. So how do we solve this? We solve it by adding a configuration block in the mapping XML file and specifying our own converter.

First, let’s replicate the remote application’s Person DTO with a name, then date and time of birth, dtob field:

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
}

and here is our own:

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
}

Notice the type difference of dtob in the source and destination DTOs.

Let’s also create our own CustomConverter to pass to Dozer in the mapping 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);
        }
    }
}

We only have to override convert() method then return whatever we want to return to it. We are availed with the source and destination objects and their class types.

Notice how we have taken care of bi-directionality by assuming the source can be either of the two classes we are mapping.

We will create a new mapping file for clarity, dozer_custom_convertor.xml:

<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net" 
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://dozer.sourceforge.net
      http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <configuration>
        <custom-converters>
            <converter type="com.baeldung.dozer.MyCustomConvertor">
                <class-a>com.baeldung.dozer.Personne3</class-a>
                <class-b>com.baeldung.dozer.Person3</class-b>
            </converter>
        </custom-converters>
    </configuration>
</mappings>

This is the normal mapping file we have seen in preceding sections, we have only added a block within which we can define as many custom converters as we require with their respective source and destination data classes.

Let’s test our new CustomConverter code:

@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());
}

We can also test to ensure it is bi-directional:

@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

In this tutorial, we have introduced most of the basics of the Dozer Mapping library and how to use it in our applications.

The full implementation of all these examples and code snippets can be found in the Dozer github project.


« 上一篇: Immutables简介