0%

使用Dozer处理类转换

使用Dozer处理类转换

什么是Dozer

dozer是用于将Java的一个对象递归映射成另一个Java对象的工具。通常来说,这些JavaBean都有各自特定的类型。

dozer提供了一个简单的属性的映射、复杂类型的映射、双向属性映射、内外属性映射也有递归映射。他们包含了集合下各个元素属性映射。

为什么要映射框架

映射框架被用于在分层体系中通过分装对特定数据对象的更改来创建抽象层,将分装好的对象传播到其他层级中去。
例如外部服务对象,领域对象DO,数据传输对象DTO。一个映射框架非常适合在一个对象转换到另一个对象的数据中使用。
对于分布式系统,又一个副作用就是需要在不同的系统之间传递对象,通常的,不能够把内部对象直接暴露到外部,不与许将外部对象直接引入到你当前的系统中去。

一直以来我们都是通过手动编码的方式在两个对象之间进行参数映射。大多数程序员都会开发一些常用的映射框架,并且话费无数的时间和数以千计的代码去处理这些关系映射。

一个通用的框架需要解决这些问题。Dozer是一个开源的映射框架,而且Dozer非常健壮,通用,灵活,可重复,可配置的。

数据对象是分层架构的一个重要组成部分。仔细的去确定每一层的边界,但是不要划分的太过,因为去维护这些对象会产生维护和性能的成本。

并行对象层级结构

有不同的原因解释为何需要支持并行对象层级结构,例如:

  • 与外部代码集成
  • 满足序列化要求
  • 集成框架
  • 划分体系结构层级

在某些情况下,当你无法直接控制代码的时候,保护你的代码频繁的改变对象层级结构说带来的侵害。因此,Dozer搭建起了一个桥梁用于连接应用和外部程序。
映射是采用反射的方式去执行的,并不会迫害你原有的API。例如如果当一个对象从Number变成String的时候,代码仍然会工作,这种问题会自动被修复。

一些框架强制要求可序列化的约束,他们不允许通过网络发送任何Java类对象。一个流行的谷歌框架GWT框架,他们强制要求开发者只能够发送只能够编译成能够序列化的JS对象。

在复杂的企业级开发中,划分成多个层次结构是非常有意义的。每个东西都会有其自己的抽象等级。一个典型的例子就是展示层,领域成和持久层。每一个层级都有
一组JavaBean标示这一层特定的数据。所有的数据都需要上传到上一个层级这种方式是不必要的,例如一些领域对象和最终的展示层对象会有所区别。

开始使用

如果是使用Maven, 在项目中引入下面的依赖:

1
2
3
4
5
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>6.4.0</version>
</dependency>

简单映射

我们假设有两种数据类,并且他们的属性名称相同

1
2
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
DestinationObject destObject = mapper.map(sourceObject, DestinationObject.class);

在执行演示程序之后,结果将会创建一个新的目标对象并且新的对象有了和原对象相同的属性。如果任何的映射属性有不同的数据类型,Dozer引擎将会自动处理数据的转化。
你已经完成了第一个Dozer的映射,后面的部分将去展现如何使用xml进行对象转换。

Dozer操作包含了两个默认的模型,显示的和隐示的。

注: 在实际的应用中不建议每次去创建一个新的mapper去转换对像,可以使用重复的替代。

通过XML指定特定的转换

如果两种不同的数据类型,并且他们的类型名字也是不同的,你这需要新增转换规则新增到自定义的一个xml文件中。
Dozer运行中使用可以使用这个映射文件。

从原字段数据转换到目标字段数据,Dozer会自动的运行类型转换。Dozer映射引擎是双向的,因此,如果想去映射目标对象到原对象,你无需再次新建另一个xml文件。

注: 字段如果有相同的名称,这无需在xml文件中特殊指定。Dozer会自动处理相同名称之间的映射关系

1
2
3
4
5
6
7
8
<mapping>
<class-a>yourpackage.yourSourceClassName</class-a>
<class-b>yourpackage.yourDestinationClassName</class-b>
<field>
<a>yourSourceFieldName</a>
<b>yourDestinationFieldName</b>
</field>
</mapping>

完整的Dozer映射文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping http://dozermapper.github.io/schema/bean-mapping.xsd">
<configuration>
<stop-on-errors>true</stop-on-errors>
<date-format>MM/dd/yyyy HH:mm</date-format>
<wildcard>true</wildcard>
</configuration>
<mapping>
<class-a>yourpackage.yourSourceClassName</class-a>
<class-b>yourpackage.yourDestinationClassName</class-b>
<field>
<A>yourSourceFieldName</A>
<B>yourDestinationFieldName</B>
</field>
</mapping>
</mappings>

Dozer和框架集成

Dozer可以不依赖于任何依赖注入框架。然而Dozer能够支持特定的一些现成包装,如Spring。

通过XML映射

这个部分将会介绍如何使用xml配置映射关系。如果有两种不同类型的数据对象,并且他们的字段不同,你将要手动添加一个类型转换文件。
从原字段数据转换到目标字段数据,Dozer会自动的运行类型转换。Dozer映射引擎是双向的,因此,如果想去映射目标对象到原对象,你无需再次新建另一个xml文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping http://dozermapper.github.io/schema/bean-mapping.xsd">
<mapping>
<class-a>com.github.dozermapper.core.vo.TestObject</class-a>
<class-b>com.github.dozermapper.core.vo.TestObjectPrime</class-b>
<field>
<a>one</a>
<b>onePrime</b>
</field>
</mapping>
<mapping wildcard="false">
<class-a>com.github.dozermapper.core.vo.TestObjectFoo</class-a>
<class-b>com.github.dozermapper.core.vo.TestObjectFooPrime</class-b>
<field>
<a>oneFoo</a>
<b>oneFooPrime</b>
</field>
</mapping>
</mappings>

一个映射元素可能对应多个映射元素,每个类的映射声明和每个属性的映射声明都有可能有这种情况。
通用属性wildcard如果设置为true则说明这是默认的转换规则。这意味着它将自动尝试这两个对象的中每个属性。当这个属性被设置为false的时候,它只会转换指定的字段。

如何加载xml的文件

Dozer将会搜索整个资源目录去寻找特定的文件。通常的做法是将配置文件打包在应用程序中。另一方面,你也可以从外部文件加载文件,可以在resource中加上file:c:\somedozermapping.xml

自从5.4.0之后,也支持从输入流中加载xml文件

使用注解

使用Dozer不好的一个地方是吃啊用XML进行配置。Dozer开始于大约五年之前XML最流行的那个年代,XML在当时是一个显而易见的选择。
在Java5之后给我们带来了注解并且这是新的一种行业公认的领域配置方法。DSL提供了映射的API,在5.3.2之后Dozer也同样支持使用注解配置。

使用注解的一个显而易见的原因是在映射代码中可以避免重复的属性和方法名。注解可以放在自身的属性上从而减少代码量。然而在某种情景下应该避免甚至不去使用Dozer。

例如:

  • 你需要映射的类不在你的控制下,可能是jar包中的
  • 映射规则十分复杂并且需要很多的配置

在第一种情况下,第三方的DTO你不可能加上注解。第二种则是需要写很多注解代码,或者某些相同的实体名隔离开来。过度注解的bean可能在阅读理解上造成困难。

使用方式

注:Dozer的注解目前是一个实现性的东西,还没有复杂转换的用例。然而在这是比使用XML和API方式更加容易实施。

下面的用例非常简单。在属性的get方法上面直接加上@Mapping注解。如果Dozer发现了它的双向映射,这意味着一个注释将会创建两种转换类型。
类型转换是自动的,全局的转换会很好的解析他们,注解只工作在定义了通配符的属性上面,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SourceBean {

private Long id;

private String name;

@Mapping("binaryData")
private String data;

@Mapping("pk")
public Long getId() {
return this.id;
}

public String getName() {
return this.name;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TargetBean {

private String pk;

private String name;

private String binaryData;

public void setPk(String pk) {
this.pk = pk;
}

public void setName(String name) {
this.name = name;
}
}

三个字段将会被转换。name将走默认的转换,id会被转换成pk,data将会把数据变成binaryData,不要担心私有属性,它也会被自动转换。

当前Dozer只提供了一个注解,但是下一个release版本会添加一个新的进去。

使用API

基于XML的配置方法是非常稳定的,他被用于很多真实的生产项目,然而它也有诸多限制。

  • 第一点也是最重要的一点就是它无法动态的生成,所有的XML映射配置必须在Dozer启动之前就弄好,而且之后还不能修改。又一个很棘手的地方,当你生成并且把映射规则放到你当前模板引擎的文件系统中,Dozer不支持这种操作。
  • 第二个问题是你不得不在XML中填写多次相同的类名字。这导致的大量的复制粘贴编程,这个可以在xml中使用特定表达式处理,但是仍然没有解决所有的问题。
  • 当你删除或者重命名这些被引用的类,但不是所有的IDE能够支持这种重构操作。自动填充的功能也不是所有的IDE都支持。

使用API进行映射:

API映射目的是去解决所有提出的问题。为了保持向后兼容,API映射同时也可以和XML结合在一起使用。事实上,某些配置部门只能够使用XML,比如全局模块配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import com.github.dozermapper.core.classmap.RelationshipType;
import com.github.dozermapper.core.loader.api.BeanMappingBuilder;
import com.github.dozermapper.core.loader.api.FieldsMappingOptions;
import com.github.dozermapper.core.loader.api.TypeMappingOptions;

import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.collectionStrategy;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.copyByReference;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.customConverter;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.customConverterId;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.hintA;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.hintB;
import static com.github.dozermapper.core.loader.api.FieldsMappingOptions.useMapId;
import static com.github.dozermapper.core.loader.api.TypeMappingOptions.mapId;
import static com.github.dozermapper.core.loader.api.TypeMappingOptions.mapNull;

public class MyClass {

public void create() {
BeanMappingBuilder builder = new BeanMappingBuilder() {
protected void configure() {
mapping(Bean.class, Bean.class,
TypeMappingOptions.oneWay(),
mapId("A"),
mapNull(true)
)
.exclude("excluded")
.fields("src", "dest",
copyByReference(),
collectionStrategy(true, RelationshipType.NON_CUMULATIVE),
hintA(String.class),
hintB(Integer.class),
FieldsMappingOptions.oneWay(),
useMapId("A"),
customConverterId("id")
)
.fields("src", "dest",
customConverter("com.github.dozermapper.core.CustomConverter")
);
}
};
}
}
1
2
3
Mapper mapper = DozerBeanMapperBuilder.create()
.withMappingBuilder(builder)
.build();

配置

通过XML配置

XML文件是Dozer的最主要的配置方式。可以有五种不同的配置范围,全局、每个类、单个类、每个字段、特定字段。

全局配置

配置块被用于配置全局的设置。全局的设置是可选的,配合自定义的转换器使用。

Dozer支持多个文件映射的功能。每个映射文件都有他们独有的配置区域。配置文件可以从存储中的已存在的映射文件集成。隐式映射将会集成默认的转换配置。

1
2
3
4
5
6
7
8
9
10
11
12
<configuration>
<date-format>MM/dd/yyyy HH:mm</date-format>
<stop-on-errors>true</stop-on-errors>
<wildcard>true</wildcard>
<custom-converters>
<!-- these are always bi-directional -->
<converter type="com.github.dozermapper.core.converters.TestCustomConverter">
<class-a>com.github.dozermapper.core.vo.TestCustomConverterObject</class-a>
<class-b>another.type.to.Associate</class-b>
</converter>
</custom-converters>
</configuration>

所有全局的设置都有其独特的XML标记。

全局配置可以被单个映射配置所覆盖隐式。这里,配置属性可以使用name=”value”的方式设置属性。他们会影响这两个类之间的操作。

1
2
3
4
5
6
7
8
<mapping wildcard="false" date-format="MM/dd/yyyy HH:mm">
<class-a>com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field>
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

一个独立的类等级,一些时候,你可能想去只设置两个类之间的关系。他们可以使用class-a和class-b标签

1
2
3
4
5
6
7
8
<mapping>
<class-a is-accessible="true">com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field>
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

每个属性的映射,同样的你可以改变两个属性之间的映射行为

1
2
3
4
5
6
7
8
<mapping>
<class-a>com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field remove-orphans="false">
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

单个的属性,一些设置可以被应用于单个属性

1
2
3
4
5
6
7
8
<mapping>
<class-a>com.github.dozermapper.core.vo.SpringBean</class-a>
<class-b>com.github.dozermapper.core.vo.SpringBeanPrime</class-b>
<field remove-orphans="false">
<a>anAttributeToMap</a>
<b>anAttributeToMapPrime</b>
</field>
</mapping>

默认情况下,如果Dozer在执行字段映射的时候出现错误,异常将会被抛出并且终止映射。尽管这种行为是推荐行为,但是Dozer仍然可以通过设置stop-on-errors
去忽略这种异常并且继续映射下一个属性。

你还可以指定导致Dozer停止运行的特殊的异常并且抛出,即使stop-on-errors设置成false也是如此。

还能够通过trim-strings属性在执行setter之前将字符串自动修整,就像执行String.trim()

如果是使用MapperBuilder去定义映射规则,则能够通过编码的方式定义类、和属性的映射规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Mapper mapper = DozerBeanMapperBuilder.create()
.withMappingBuilder(new BeanMappingBuilder() {
@Override
protected void configure() {
mapping(type(A.class).mapEmptyString(true),
type(B.class),
TypeMappingOptions.wildcardCaseInsensitive(true)
).fields(
field("fieldOfA").getMethod("getTheField"),
field("fieldOfB"),
FieldsMappingOptions.oneWay()
);
}
})
.build();