Contents

Dozer Mapping 库简介

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("Blogdemo", 10);
    Dest dest = mapper.map(source, Dest.class);
    assertEquals(dest.getName(), "Blogdemo");
    assertEquals(dest.getAge(), 10);
}

正如我们所见,在 Dozer 映射之后,结果将是Dest对象的一个新实例,其中包含与Source对象具有相同字段名称的所有字段的值。

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

@Test
public void givenSourceObjectAndDestObject_whenMapsSameNameFieldsCorrectly_
  thenCorrect() {
    Source source = new Source("Blogdemo", 10);
    Dest dest = new Dest();
    mapper.map(source, dest);
    assertEquals(dest.getName(), "Blogdemo");
    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
}

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

在源类中,idStringpointsdouble,而在目标类中,idpoints都是integer

现在让我们看看 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);
}

我们将 32015.2、一个String和一个double传递给源对象,结果有32015,它们都是目标对象中的integer

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

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

然而,在现实世界的应用程序中,我们映射的两个数据对象将有无数次没有共享公共属性名称的字段。

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

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

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

我们有一个Person对象,其中包含namenicknameage字段:

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并具有字段nomsurnomage

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 将 French 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>com.blogdemo.dozer.Personne</class-a>
        <class-b>com.blogdemo.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。年龄的法语单词仍然是年龄,这将我们带到了推土机的另一个重要特征。

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

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

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

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 项目的 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>com.blogdemo.dozer.Personne</class-a>
        <class-b>com.blogdemo.dozer.Person</class-b>
        <field>
            <a>nom</a>
            <b>name</b>
        </field>
        <field>
            <a>surnom</a>
            <b>nickname</b>
        </field>
    </mapping>
</mappings>

请注意,我们在<mapping></mapping>元素中添加了一个以前没有的属性通配符。

默认情况下,通配符为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.javaPersonne2.java中,而不需要更改字段。

为了实现这一点,我们只需要在源对象的getter方法上添加*@mapper(“destinationFieldName”)*注解。像这样:

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

这次我们将Personne2视为源,但由于Dozer引擎的双向特性,这并不重要。

现在去掉了所有与 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对象。本节解决了一个不同的问题。

如果我们正在解组的数据对象表示一个日期和时间字段,例如long或 Unix 时间,如下所示:

1182882159000

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

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 和目标 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="com.blogdemo.dozer.MyCustomConvertor">
                <class-a>com.blogdemo.dozer.Personne3</class-a>
                <class-b>com.blogdemo.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());
}