Dozer指南

2025/03/26

1. 概述

Dozer是一个Java Bean到Java Bean的映射器,它以递归方式将数据从一个对象逐个属性地复制到另一个对象。

该库不仅支持Java Bean的属性名称之间的映射,而且还支持类型之间的自动转换(如果它们不同)。

大多数转换场景都是开箱即用的,但Dozer还允许你通过XML指定自定义转换。

2. 简单示例

对于我们的第一个例子,我们假设源数据对象和目标数据对象都共享相同的公共属性名称。

这是使用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
}

然后是我们的目标文件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
}

我们需要确保包含默认或零参数构造函数,因为Dozer在底层使用了反射。

并且,出于性能方面的考虑,让我们将映射器变为全局的,并创建一个在整个测试中将使用的单个对象:

DozerBeanMapper mapper;

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

现在,让我们运行第一个测试来确认当我们创建一个Source对象时,我们可以将它直接映射到Dest对象:

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

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

我们可以看到,经过Dozer映射之后,结果将是Dest对象的新实例,其中包含与Source对象具有相同字段名的所有字段的值。

或者,我们可以不将Dest类传递给mapper,而是直接创建Dest对象并将其引用传递给mapper:

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

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

3. Maven设置

现在我们对Dozer的工作原理有了基本的了解,让我们在pom.xml中添加以下依赖:

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

最新版本可在此处获取。

4. 数据转换示例

我们已经知道,只要在两个类中找到同名的属性,Dozer就可以将现有对象映射到另一个对象。

然而,情况并非总是如此;因此,如果任何映射属性属于不同的数据类型,则Dozer映射引擎将自动执行数据类型转换

让我们看看这个新概念的实际应用:

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
}

以及目标类型:

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
}

请注意,属性名称相同,但数据类型不同。

在源类中,id是String而points是double,而在目标类中,id和points都是int。

现在让我们看看Dozer如何正确处理转换:

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

我们将“320”和15.2(一个String和一个double数)传递给源对象,结果为320和15,目标对象中均为int。

5. 通过XML进行基本自定义映射

在我们看到的所有先前的例子中,源数据对象和目标数据对象都具有相同的字段名称,这使得我们可以轻松进行映射。

然而,在实际应用中,无数次我们映射的两个数据对象不具有共享公共属性名称的字段。

为了解决这个问题,Dozer为我们提供了在XML中创建自定义映射配置的选项。

在这个XML文件中,我们可以定义类映射条目,Dozer映射引擎将使用这些条目来决定将哪个源属性映射到哪个目标属性。

让我们看一个例子,并尝试将法国程序员构建的应用程序中的数据对象解组为英语风格的对象命名。

我们有一个Person对象,其具有name、nickname和age字段:

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
}

我们正在解组的对象名为Personne,具有字段nom、surnom和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
}

这些对象实际上实现了相同的目的,但我们存在语言障碍。为了克服这一障碍,我们可以使用Dozer将法语Personne对象映射到我们的Person对象。

我们只需要创建一个自定义映射文件来帮助Dozer做到这一点,我们将其命名为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>cn.tuyucheng.taketoday.dozer.Personne</class-a>
        <class-b>cn.tuyucheng.taketoday.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

这是我们可以拥有的自定义XML映射文件的最简单的示例。

现在,只需注意到我们有<mappings>作为根元素,它有一个子元素<mapping>,我们可以在<mappings>内拥有尽可能多的子元素,因为需要自定义映射的类对的数量是固定的。

还请注意我们如何在<mapping></mapping>标签内指定源类和目标类。后面跟着一个<field></field>,用于需要自定义映射的每个源和目标字段对。

最后,请注意,我们尚未在自定义映射文件中包括字段age。法语中age的单词仍然是age,这让我们想到了Dozer的另一个重要特性。

同名的属性不需要在映射XML文件中指定,Dozer会自动将所有具有相同属性名称的字段从源对象映射到目标对象。

然后,我们将自定义XML文件直接放在src文件夹下的类路径上。但是,无论我们将它放在类路径的哪个位置,Dozer都会搜索整个类路径来查找指定的文件。

让我们创建一个辅助方法来将映射文件添加到我们的映射器:

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

现在让我们测试一下代码:

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

如测试所示,DozerBeanMapper接收自定义XML映射文件列表,并在运行时决定何时使用每个文件。

假设我们现在开始在英语应用程序和法语应用程序之间来回解组这些数据对象。我们不需要在XML文件中创建另一个映射,Dozer足够智能,只需一个映射配置就可以双向映射对象

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

因此,这个示例测试使用了Dozer的另一个特性-Dozer映射引擎是双向的,所以如果我们想将目标对象映射到源对象,我们不需要在XML文件中添加另一个类映射。

如果需要,我们还可以从类路径之外加载自定义映射文件,在资源名称中使用“file:”前缀。

在Windows环境中(例如下面的测试),我们当然会使用Windows特定的文件语法。

在Linux上,我们可以将文件存储在/home下,然后:

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

在Mac OS上:

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

如果你正在从Github项目运行单元测试(你应该这样做),你可以将映射文件复制到适当的位置并更改configureMapper方法的输入。

映射文件位于GitHub项目的test/resources文件夹下:

@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. 通配符和进一步的XML自定义

让我们创建第二个自定义映射文件,名为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>cn.tuyucheng.taketoday.dozer.Personne</class-a>
        <class-b>cn.tuyucheng.taketoday.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

请注意,我们向<mapping></mapping>元素添加了之前没有的属性wildcard。

默认情况下,wildcard为true,它告诉Dozer引擎我们希望将源对象中的所有字段映射到其适当的目标字段。

当我们将其设置为false时,我们告诉Dozer仅映射我们在XML中明确指定的字段。

因此,在上述配置中,我们仅希望映射两个字段,而忽略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);
}

正如我们在最后一个断言中看到的,目标age字段保持为0。

7. 通过注解进行自定义映射

对于简单的映射情况以及我们对想要映射的数据对象也有写访问权限的情况,我们可能不需要使用XML映射。

通过注解映射不同名称的字段非常简单,我们需要编写的代码比XML映射少得多,但只能在简单的情况下帮助我们。

我们将数据对象复制到Person2.java和Personne2.java中,完全不改变字段。

为了实现这一点,我们只需要在源对象的Getter方法上添加@mapper(“destinationFieldName”)注解。如下所示:

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

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

这次我们将Personne2作为源,但由于DozerEngine的双向特性,这并不重要。

现在,所有与XML相关的代码都被删除了,我们的测试代码更短了:

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

我们还可以测试双向性:

@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. 自定义API映射

在我们之前的例子中,我们从法国应用程序解组数据对象,我们使用XML和注解来定制我们的映射。

Dozer中提供的另一种替代方案与注解映射类似,即API映射。它们之所以类似,是因为我们消除了XML配置并严格使用Java代码。

在这种情况下,我们使用BeanMappingBuilder类,在最简单的情况下定义如下:

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

如我们所见,我们有一个抽象方法configure(),我们必须重写它才能定义我们的配置。然后,就像XML中的<mapping></mapping>标签一样,我们根据需要定义任意数量的TypeMappingBuilder。

这些构建器告诉Dozer我们要将哪些源字段映射到目标字段。然后,我们将BeanMappingBuilder传递给DozerBeanMapper,就像传递XML映射文件一样,只是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());
}

映射API也是双向的:

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

或者我们可以选择仅使用此构建器配置映射明确指定的字段:

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

我们的age == 0测试又回来了:

@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. 自定义转换器

在映射中我们可能遇到的另一种情况是我们想在两个对象之间执行自定义映射

我们已经研究过源字段名称和目标字段名称不同的情况,例如法语Personne对象。本节将解决另一个问题。

如果我们正在解组的数据对象表示日期和时间字段(例如长格式或Unix时间),如下所示:

1182882159000

但是我们自己的等效数据对象以这种ISO格式表示相同的日期和时间字段和值,例如字符串:

2007-06-26T21:22:39Z

默认转换器只会将long值映射到string,如下所示:

"1182882159000"

这肯定会导致我们的应用程序出现错误,那么我们如何解决这个问题呢?我们通过在映射XML文件中添加配置块并指定我们自己的转换器来解决这个问题。

首先,让我们复制远程应用程序的Person DTO,其中包含名称、出生日期和时间、dtob字段:

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
}

这是我们自己的:

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
}

注意源和目标DTO中dtob的类型差异。

我们还创建自己的CustomConverter以在映射XML中传递给Dozer:

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

我们只需重写convert()方法,然后返回我们想要返回的内容,我们可以使用源对象和目标对象及其类类型。

请注意,我们是如何通过假设源可以是我们正在映射的两个类中的任何一个来处理双向性的。

为了清楚起见,我们将创建一个新的映射文件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="cn.tuyucheng.taketoday.dozer.MyCustomConvertor">
                <class-a>cn.tuyucheng.taketoday.dozer.Personne3</class-a>
                <class-b>cn.tuyucheng.taketoday.dozer.Person3</class-b>
            </converter>
        </custom-converters>
    </configuration>
</mappings>

这是我们在前面章节中看到的普通映射文件,我们只添加了一个<configuration></configuration>块,在其中我们可以根据需要定义任意数量的自定义转换器及其各自的源和目标数据类。

让我们测试一下新的CustomConverter代码:

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

我们还可以进行测试以确保它是双向的:

@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. 总结

在本教程中,我们介绍了Dozer映射库的大部分基础知识以及如何在我们的应用程序中使用它。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章