Dozer 轻松高效玩转DTO(Data Transfer Object)
现状
对于分布式系统,需要在不同系统之间传递与转换域对象。因为我们不希望外部公开内部域对象,也不允许外部域对象渗入系统。传统上,数据对象之间的映射通过手工编码(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)相关推荐
- 合理使用DTO(Data Transfer Object)
文章目录 1. DTO简介 2. 到底什么是DTO? 3. 将DTO用作POJO 4. Java 中使用DTO的例子 5. 反例: 滥用DTO 6. 小结 相关链接 DTO, 全称为 Data Tra ...
- 轻松高效玩转DTO(Data Transfer Object)
点击蓝色"程序猿DD"关注我哟 加个"星标",不忘签到哦 转载自公众号:日拱一兵 关注我,回复口令获取可获取独家整理的学习资料: - 001:领取<Spr ...
- Java DTO(data transfer object)的理解,为什么要用DTO
DTO即数据传输对象. 现状 对于分布式系统,需要在不同系统之间传递与转换域对象.因为我们不希望外部公开内部域对象,也不允许外部域对象渗入系统.传统上,数据对象之间的映射通过手工编码(getter/s ...
- 零拷贝实现高效的数据传输 -Efficient data transfer through zero copy
点击参考原文 - 1.概述 许多Web应用程序提供大量静态内容,相当于从磁盘读取数据原封不动地写回响应套接字. 这个操作似乎只需要相对较少的CPU时间,但它的效率有点低:内核从磁盘读取数据,并跨越内核 ...
- 5G NR RLC:Data Transfer ARQ
其他相关内容 RLC PDU and Parameters RLC架构和RLC entity 一 RLC entity handling RLC entity有建立.重建和释放的过程(establis ...
- AS91创建历史资产卡片报错:消息号 FAA_MD036 segment 00001: Status ‘In Preparation‘; no data transfer possible
文章目录 一.业务场景-AS91 二.问题分析和解决-FAA_CMP_LDT 一.业务场景-AS91 T-CODE: AS91 公司代码:2A00 参看详细信息 消息号 FAA_MD036 CoCd ...
- 天宝Trimble Data Transfer安装并传输数据
天宝Trimble Data Transfer安装并传输数据 软件下载 问题描述 解决办法 软件下载 (若读者没有CSDN账号或者不方便下载,可以移步至本人同名博客园免费下载相关软件:https:// ...
- 使用AWS MVP方案[Data Transfer Hub]从Global S3同步文件到中国区S3
本文主要描述在AWS Global区部署Data Transfer Hub方案,并创建从global S3同步文件到中国区S3的任务 本次实验架构图 1. 实验准备 1.1 AWS Global账号 ...
- WCF Data transfer buffered VS streamed
WCF Data Transfer buffered VS streamed 在Data Transfer中,我们会经常听到开发提到buffer mode和stream mode.对于不了解Data ...
最新文章
- 如何有效落地企业目标管理方法论?
- 邮件整体解决方案_面向未来的冻干机进出料解决方案:阿尔法(ALUS)系列自动进出料系统...
- [Android学习系列8]数据库ormlite笔记
- 绝不能错过的10款最新OpenStack网络运维 监控工具
- 【收藏】电气设计相关计算公式大全(附举例)
- 《Haskell函数式编程入门》——导读
- android 中 Proguard 和JNI 相关
- 数据挖掘10大算法(1)——PageRank
- 19.敏捷项目管理流程实例 - 变更管理
- 如何快速发现XSS跨站攻击漏洞
- 多比特杯第四届ACM程序设计竞赛同步赛
- 为什么要用同花顺程序化交易接口
- 《增长黑客》:针对产品增长---思维导图
- w ndows7怎样连接无线网,windows7电脑如何连接wifi
- 微信公众号监听手机返回键事件jssdk—wx.closeWindow
- 新手必备AutoCAD练习图纸,分分钟提高你的绘图效率!
- Android安卓原生接入微信app支付PHP服务端
- No result defined for action com.frank.action.RegistAction and result success
- unity网络实战开发(丛林战争)-前期知识准备(011-c#连接数据库并实现增删改查以及sql注入问题)
- 软件工程期末复习(极限拉扯沉浸式备考)
热门文章
- android 串口参数设置,Android-SerialPort
- 货场RFID智能称重管理系统
- linux 设置邮件提醒,linux 定时邮件提醒
- latex 图片_LaTeX入门(八)——图片
- 瑞星卡卡助手爆重大bug OE用户损失惨重
- EBS系统慢问题 - _undo_autotune=false
- PEIF图片文件及python解析
- Photoshop-八步教你设计扁平化图标阴影部分
- 2023年四川省艺术基金青年艺术人才项目申报条件流程及申请奖励补助
- android recyclerview流式布局,Android FlexboxLayout流式布局