现状

对于分布式系统,需要在不同系统之间传递与转换域对象。因为我们不希望外部公开内部域对象,也不允许外部域对象渗入系统。传统上,数据对象之间的映射通过手工编码(getter/setter)的方式实现,或对象组装器(或转换器)来解决。我们可能会开发某种自定义映射框架来满足我们的映射转换需求,但这一切都显得不够灵巧。

Dozer

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

通常,这些 Java Bean 将具有不同的复杂类型。Dozer 支持简单属性映射,复杂类型映射,双向映射,隐式和显式映射以及递归映射。

Dozer不仅支持属性名称之间的映射,还支持在类型之间自动转换。大多数转换方案都是开箱即用的,但 Dozer 还允许您通过 XML / API 的方式指定自定义转换。

下图描绘了 Dozer 可以插入到架构中的一些常见区域。请注意,它通常用于边界(进入/退出)。 Dozer 将确保数据库中的内部域对象不会流入外部表示层或外部使用者。它还可以帮助将域对象映射到外部 API 调用,反之亦然,现在不用纠结这个图,看完下面的测试用例回看该图,柳暗花明, 文末有完整测试用例

集成 Dozer

使用 Dozer 的方式很简单,如果你使用 Maven,添加依赖到 pom.xml 中即可

<dependency><groupId>com.github.dozermapper</groupId><artifactId>dozer-core</artifactId><version>6.4.0</version>
</dependency>

如果你使用 Spring Boot,引入 Dozer starter 即可:

<dependency><groupId>com.github.dozermapper</groupId><artifactId>dozer-spring-boot-starter</artifactId><version>6.2.0</version>
</dependency>

本文主要讲述在 Spring Boot 下如何通过 Dozer 帮助我们搞定 DTO 那点事

使用 Dozer

默认使用

Dozer starter 默认为我们注入了 Dozer Mapper,可以直接使用,另外,文章中所有测试用例中使用 Lombok 注解简化代码
新建 StudentDomain.java 类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class StudentDomain {// 身份IDprivate Long id;// 姓名private String name;// 年龄private Integer age;// 电话private String mobile;
}

新建 StudentVo.java 类,内容同 StudentDomain.java
编写测试用例:

@Autowired
private Mapper dozerMapper;@Test
public void testDefault(){StudentDomain studentDomain = new StudentDomain(1024L, "tan日拱一兵", 18, "13996996996");StudentVo studentVo = dozerMapper.map(studentDomain, StudentVo.class);log.info("StudentVo: [{}]", studentVo.toString());studentVo.setAge(16);log.info("StudentDomain: [{}]", dozerMapper.map(studentVo, StudentDomain.class));
}

运行结果:

StudentVo: [StudentVo(id=1024, name=tan日拱一兵, age=18, mobile=13996996996)]
StudentDomain: [StudentDomain(id=1024, name=tan日拱一兵, age=16, mobile=13996996996)]

结论:

Dozer 默认支持同名 field 的双向映射,即隐式映射
如果仅满足这点需求,就没必要写该文章了,应用 Dozer 也为了满足我们更多定制化的需求

定制化使用

为满足更多的转换需求,我们需要针对 Dozer 定制化,即需要我们声明自己的 Mapper,新建 DozerConfig.java 类

@Configuration
public class DozerConfig {@Beanpublic Mapper dozerMapper(){Mapper mapper = DozerBeanMapperBuilder.create()//指定 dozer mapping 的配置文件(放到 resources 类路径下即可),可添加多个 xml 文件,用逗号隔开.withMappingFiles("dozerBeanMapping.xml").withMappingBuilder(beanMappingBuilder())           .build();return mapper;}@Beanpublic BeanMappingBuilder beanMappingBuilder() {return new BeanMappingBuilder() {@Overrideprotected void configure() {// 个性化配置添加在此} };}
}

Dozer 完成映射有三种方式 XML, API, 注解,因官网多数都是 XML 样例,以及注解方式的局限性所在,所以本文主要使用 API 这种方式,为更好的体现 Dozer 的特性,现阶段必须以 XML API 二者结合的方式来编写测试用例,因为官网说明:

Global config is not supported via APIMappings, API mappings are not 100% feature comparable with XML

测试用例(共 10 个)

用例 1

如果两个待映射的 field 不同名,Dozer 默认不会帮我们完成映射,忽略该值,所以我们需要显示映射该 field
向 StudentDomain.java 中添加学生地址信息

// 地址
private String address;

而 StudentVo.java 中表示学生地址的信息是

// 地址
private String addr;

我们需要在 configure 方法中显示指定映射关系

@Override
protected void configure() {//测试所有properties,为不同名的 property 手动配置映射关系mapping(StudentDomain.class, StudentVo.class).fields("address", "addr");
}

修改测试用例:

@Test
public void testDifferentAddress(){StudentDomain studentDomain = new StudentDomain(1024L, "tan日拱一兵", 18, "13996996996", "中国");StudentVo studentVo = dozerMapper.map(studentDomain, StudentVo.class);log.info("StudentVo: [{}]", studentVo.toString());
}

运行结果:

StudentVo: [StudentVo(id=1024, name=tan日拱一兵, age=18, mobile=13996996996, addr=中国)]

用例 2

Dozer 默认是隐式匹配,如果我们关闭隐士匹配,Dozer 只会为我们匹配我们显式指定的 field
修改 configure

//关闭隐式匹配
mapping(StudentDomain.class, StudentVo.class, TypeMappingOptions.wildcard(false)).fields("address", "addr");

重新运行 用例1的测试方法 ,运行结果(只有地址做了映射):

StudentVo: [StudentVo(id=null, name=null, age=null, mobile=null, addr=中国)]

用例 3

默认我们要使用 Dozer 的隐式匹配(同名字段全部匹配),但我们不想将学生的 mobile 字段做映射,我们可以通过 exclude 方法排除不想映射的字段
修改 configure

//测试所有properties,为不同名的 property 手动配置映射关系,排除 mobile 字段
mapping(StudentDomain.class, StudentVo.class).exclude("mobile").fields("address", "addr");

重新运行 用例1的测试方法 ,运行结果:

StudentVo: [StudentVo(id=1024, name=tan日拱一兵, age=18, mobile=null, addr=中国)]

用例 4

对象通常嵌套对象或者集合对象,Dozer 可以递归完成相关映射
将学生地址封装,同时为学生添加多门课程
新增 AddressDomain.java 和 AdressVo.java,除详细地址外所有字段相同

@Data
public class AddressDomain {// 省private String province;// 市private String city;// 区private String district;// 详细private String detail;
}
@Data
public class AddressVo {... 省略省市区,同 AddressDomain// 详细private String detailAddr;
}

同时创建课程类 CourseDomian.java 和 CourseVo.java, 内容相同

@Data
public class CourseDomain {// 课程编码private String courseCode;// 课程Idprivate Integer courseId;// 课程名称private String courseName;// 老师名称private String teacherName;
}

同时修改 StudentDomain.java 和 StudentVo.java, 添加地址和课程集合字段:

// 地址
private AddressDomain address;
// 课程集合
private List<CourseDomain> courses;

修改configure 配置

mapping(AddressDomain.class, AddressVo.class).fields("detail", "detailAddr");

修改测试用例:

@Test
public void testCascadeObject(){StudentDomain studentDomain = getStudentDomain();StudentVo studentVo = dozerMapper.map(studentDomain, StudentVo.class);log.info("StudentVo: [{}]", studentVo.toString());
}

运行结果:

StudentVo: [StudentVo(id=1024, name=tan日拱一兵, age=18, mobile=13996996996, address=AddressVo(province=北京, city=北京, district=海淀区, detailAddr=西二旗), courses=[CourseVo(courseCode=English, courseId=1, courseName=英语, teacherName=京晶), CourseVo(courseCode=Chinese, courseId=2, courseName=语文, teacherName=水寒)])]

结论:
Dozer 会隐式递归匹配所有 field,甚至集合

用例 5

深度匹配需求,英语老师是辅导员,需要单独匹配到 StudentVo.java 的 counsellor 字段
添加 configure mapping

//测试深度索引匹配
mapping(StudentDomain.class, StudentVo.class).fields("courses[0].teacherName", "counsellor");

重新运行测试用例,运行结果:

StudentVo: [StudentVo(id=1024, name=tan日拱一兵, age=18, mobile=13996996996, address=AddressVo(province=北京, city=北京, district=海淀区, detailAddr=西二旗), courses=[CourseVo(courseCode=English, courseId=1, courseName=英语, teacherName=京晶), CourseVo(courseCode=Chinese, courseId=2, courseName=语文, teacherName=水寒)], counsellor=京晶)]

结论:
我们可以通过深度匹配指定字段,匹配方式以 “.” 号进行分割,集合属性可以指定索引

用例 6

修改 StudentDomain.java 的 age 字段为 Integer 类型,修改 StudentVo.java 的 age 字段为 String 类型
重新运行上述测试用例,双向映射,一切正常
结论:
Dozer 开箱即用的功能之一就是类型转换,多数类型我们不需要手动转换类型,完全交给 Dozer即可

用例 7

上面说到多数类型 Dozer 可以默认做转换,但是 Date 和 String 不可以,我们需要指定 date-formate 格式
为学生添加入学日期 entrollmentDate,在 StudentDomain.java 中是 String 类型,在 StudentVo.java 中是 java.util.Date 类型

// 入学日期 设置为"2019-09-01 10:00:00"
private String entrollmentDate;

为 entrollmentDate 字段配置 date-formate ,修改 configure

mapping(StudentDomain.class, StudentVo.class TypeMappingOptions.dateFormat("yyyy-MM-dd")).fields("courses[0].teacherName", "counsellor");

运行结果:

StudentVo: [StudentVo(id=1024, name=tan日拱一兵, age=18, mobile=13996996996, address=AddressVo(province=北京, city=北京, district=海淀区, detailAddr=西二旗), courses=[CourseVo(courseCode=English, courseId=1, courseName=英语, teacherName=京晶), CourseVo(courseCode=Chinese, courseId=2, courseName=语文, teacherName=水寒)], counsellor=京晶, entrollmentDate=Sun Sep 01 00:00:00 CST 2019)]

我们同样可以设置全局 date-formate 和 field 级别 date-formate,全局设置,修改 dozerBeanMapping.xml, 并添加如下内容:

<?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><!-- 默认是 true,当发生转换错误时抛出异常,停止转换,这里设置成false,如果转换错误,继续转换 --><stop-on-errors>false</stop-on-errors><date-format>yyyy-MM-dd HH:mm:ss</date-format></configuration>
</mappings>

如果同时设置了全局/类/Field 级别的 date-format,按照优先级最高的进行格式化:Field > 类 > 全局

用例 8

我们可以为 mapping 设置 mapId, 在转换的时候指定 mapId,mapId 可以设置在类级别,也可以设置在 field 级别,实现一次定义,多处使用,同时也可以设置转换方向从默认的双向变为单向(one way):

mapping(StudentDomain.class, StudentVo.class, TypeMappingOptions.mapId("userFieldOneWay"))
.fields("age", "age", FieldsMappingOptions.useMapId("addrAllProperties"), FieldsMappingOptions.oneWay());

修改测试用例,指定 mapId

StudentVo studentVo = dozerMapper.map(studentDomain, StudentVo.class, "userFieldOneWay");

用例 9

当有些字段需要特殊处理的时候,我们需要实现自定义转换,也就是需要自定义 Converter
假设 StudentDomain.java 有 Integer 类型的 score 字段,StudentVo.java 中表示的分数则是 Enum 类型,分为 A/B/C/D 四个等级
自定义Converter,继承 DozerConverter<A, B>, 并实现其方法:

public class ScoreConverter extends DozerConverter<Integer, ScoreEnum> {public ScoreConverter() {super(Integer.class, ScoreEnum.class);}@Overridepublic ScoreEnum convertTo(Integer score, ScoreEnum scoreEnum) {if (60 <= score && score < 80){return ScoreEnum.C;}else if (80 <= score && score < 90){return ScoreEnum.B;}else if (90 <= score){return ScoreEnum.A;}else {return ScoreEnum.D;}}@Overridepublic Integer convertFrom(ScoreEnum scoreEnum, Integer integer) {return null;}
}

修改 configure,添加 mapping:

mapping(StudentDomain.class, StudentVo.class).fields("score", "score", customConverter(ScoreConverter.class));

运行结果:

StudentVo: [StudentVo(id=1024, name=tan日拱一兵, age=18, mobile=13996996996, address=AddressVo(province=北京, city=北京, district=海淀区, detailAddr=西二旗), courses=[CourseVo(courseCode=English, courseId=1, courseName=英语, teacherName=京晶), CourseVo(courseCode=Chinese, courseId=2, courseName=语文, teacherName=水寒)], counsellor=null, entrollmentDate=Sun Sep 01 10:00:00 CST 2019, score=A)]

用例 10

Dozer 可以通过实现 DozerEventListener 接口实现 mapping 的事件监听,在 mapping 的时候做全局业务:

@Slf4j
public class StudentListener implements DozerEventListener {@Overridepublic void mappingStarted(DozerEvent dozerEvent) {log.info("mappingStarted");}@Overridepublic void preWritingDestinationValue(DozerEvent dozerEvent) {log.info("preWritingDestinationValue");}@Overridepublic void postWritingDestinationValue(DozerEvent dozerEvent) {log.info("postWritingDestinationValue");}@Overridepublic void mappingFinished(DozerEvent dozerEvent) {log.info("mappingFinished");}
}

Dozer 可使用的样例远不止这些,这里罗列出我们最常见的一些业务问题;看完这些用例之后,请回看文章开头的图,我们可以在系统边界处充分利用 Dozer,满足 DTO 的一切需求

Dozer 支持的数据类型转换(双向)

在此列举出 Dozer 支持的数据类型转换(双向)

  • Primitive to Primitive Wrapper
  • Primitive to Custom Wrapper
  • Primitive Wrapper to Primitive Wrapper
  • Primitive to Primitive
  • Complex Type to Complex Type
  • String to Primitive
  • String to Primitive Wrapper
  • String to Complex Type if the Complex Type contains a String constructor
  • String to Map
  • Collection to Collection
  • Collection to Array
  • Map to Complex Type
  • Map to Custom Map Type
  • Enum to Enum
  • Each of these can be mapped to one another: java.util.Date, java.sql.Date, java.sql.Time, java.sql.Timestamp, java.util.Calendar, java.util.GregorianCalendar
  • String to any of the supported Date/Calendar Objects.
  • Objects containing a toString() method that produces a long representing time in (ms) to any supported Date/Calendar object.

同时看官网 Release 版本,Dozer 现已支持 proto 类型的转换的支持,即支持 gRPC;

总结

Dozer 可以高效的处理我们日常 DTO 业务,一次 mapping 定义,多处使用, Dozer 与 Lombok 结合使用极大的简化了我们的代码编写量,代码更加工整简洁。同时 Dozer Github 也保持活跃更新,可以追踪更多新特性,本文 demo 地址:Dozer Demo Github。如果你在业务中需要一些特殊的转换规则,欢迎留言交流,我们一起探讨实现

提高效率工具

在写这篇文章的时候用到了两个比较好用的工具,后续文章也会将好用的工具分享出来,请持续关注

Octotree

当你在浏览 Github 的源码时,多层级目录的切换浏览非常麻烦,Octotree 是 Chrome 浏览器的一个扩展,用以展示源码的属性结构,安装该插件后,再看 Dozer 的源码,一切轻松多了,也少了些许烦恼

Reader View

源码和文档更配,当我们阅读文档时,我们需要一个友好的或专注的阅读模式,这样让我们思想更加集中,心无旁骛。Reader View 同样是 Chrome 浏览器的一个扩展,以最简洁的方式将文档展现出来,当然我们可以自定义主题模式


欢迎关注公众号,趣谈coding那些事,你有一个思想,我有一个思想,我们交换后就都有两个思想

Dozer 轻松高效玩转DTO(Data Transfer Object)相关推荐

  1. 合理使用DTO(Data Transfer Object)

    文章目录 1. DTO简介 2. 到底什么是DTO? 3. 将DTO用作POJO 4. Java 中使用DTO的例子 5. 反例: 滥用DTO 6. 小结 相关链接 DTO, 全称为 Data Tra ...

  2. 轻松高效玩转DTO(Data Transfer Object)

    点击蓝色"程序猿DD"关注我哟 加个"星标",不忘签到哦 转载自公众号:日拱一兵 关注我,回复口令获取可获取独家整理的学习资料: - 001:领取<Spr ...

  3. Java DTO(data transfer object)的理解,为什么要用DTO

    DTO即数据传输对象. 现状 对于分布式系统,需要在不同系统之间传递与转换域对象.因为我们不希望外部公开内部域对象,也不允许外部域对象渗入系统.传统上,数据对象之间的映射通过手工编码(getter/s ...

  4. 零拷贝实现高效的数据传输 -Efficient data transfer through zero copy

    点击参考原文 - 1.概述 许多Web应用程序提供大量静态内容,相当于从磁盘读取数据原封不动地写回响应套接字. 这个操作似乎只需要相对较少的CPU时间,但它的效率有点低:内核从磁盘读取数据,并跨越内核 ...

  5. 5G NR RLC:Data Transfer ARQ

    其他相关内容 RLC PDU and Parameters RLC架构和RLC entity 一 RLC entity handling RLC entity有建立.重建和释放的过程(establis ...

  6. AS91创建历史资产卡片报错:消息号 FAA_MD036 segment 00001: Status ‘In Preparation‘; no data transfer possible

    文章目录 一.业务场景-AS91 二.问题分析和解决-FAA_CMP_LDT 一.业务场景-AS91 T-CODE: AS91 公司代码:2A00 参看详细信息 消息号 FAA_MD036 CoCd ...

  7. 天宝Trimble Data Transfer安装并传输数据

    天宝Trimble Data Transfer安装并传输数据 软件下载 问题描述 解决办法 软件下载 (若读者没有CSDN账号或者不方便下载,可以移步至本人同名博客园免费下载相关软件:https:// ...

  8. 使用AWS MVP方案[Data Transfer Hub]从Global S3同步文件到中国区S3

    本文主要描述在AWS Global区部署Data Transfer Hub方案,并创建从global S3同步文件到中国区S3的任务 本次实验架构图 1. 实验准备 1.1 AWS Global账号 ...

  9. WCF Data transfer buffered VS streamed

    WCF Data Transfer buffered VS streamed 在Data Transfer中,我们会经常听到开发提到buffer mode和stream mode.对于不了解Data ...

最新文章

  1. 如何有效落地企业目标管理方法论?
  2. 邮件整体解决方案_面向未来的冻干机进出料解决方案:阿尔法(ALUS)系列自动进出料系统...
  3. [Android学习系列8]数据库ormlite笔记
  4. 绝不能错过的10款最新OpenStack网络运维 监控工具
  5. 【收藏】电气设计相关计算公式大全(附举例)
  6. 《Haskell函数式编程入门》——导读
  7. android 中 Proguard 和JNI 相关
  8. 数据挖掘10大算法(1)——PageRank
  9. 19.敏捷项目管理流程实例 - 变更管理
  10. 如何快速发现XSS跨站攻击漏洞
  11. 多比特杯第四届ACM程序设计竞赛同步赛
  12. 为什么要用同花顺程序化交易接口
  13. 《增长黑客》:针对产品增长---思维导图
  14. w ndows7怎样连接无线网,windows7电脑如何连接wifi
  15. 微信公众号监听手机返回键事件jssdk—wx.closeWindow
  16. 新手必备AutoCAD练习图纸,分分钟提高你的绘图效率!
  17. Android安卓原生接入微信app支付PHP服务端
  18. No result defined for action com.frank.action.RegistAction and result success
  19. unity网络实战开发(丛林战争)-前期知识准备(011-c#连接数据库并实现增删改查以及sql注入问题)
  20. 软件工程期末复习(极限拉扯沉浸式备考)

热门文章

  1. android 串口参数设置,Android-SerialPort
  2. 货场RFID智能称重管理系统
  3. linux 设置邮件提醒,linux 定时邮件提醒
  4. latex 图片_LaTeX入门(八)——图片
  5. 瑞星卡卡助手爆重大bug OE用户损失惨重
  6. EBS系统慢问题 - _undo_autotune=false
  7. PEIF图片文件及python解析
  8. Photoshop-八步教你设计扁平化图标阴影部分
  9. 2023年四川省艺术基金青年艺术人才项目申报条件流程及申请奖励补助
  10. android recyclerview流式布局,Android FlexboxLayout流式布局