点击蓝色“程序猿DD”关注我哟

加个“星标”,不忘签到哦

转载自公众号:日拱一兵


关注我,回复口令获取可获取独家整理的学习资料:

001:领取《Spring Boot基础教程》

002:领取《Spring Cloud基础教程》

- 003:领取《Java开发规范1.5》(最新版)

现状

对于分布式系统,需要在不同系统之间传递与转换域对象。因为我们不希望外部公开内部域对象,也不允许外部域对象渗入系统。传统上,数据对象之间的映射通过手工编码(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 {    // 身份ID private 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 {  @Bean  public Mapper dozerMapper(){    Mapper mapper = DozerBeanMapperBuilder.create()    //指定 dozer mapping 的配置文件(放到 resources 类路径下即可),可添加多个 xml 文件,用逗号隔开  .withMappingFiles("dozerBeanMapping.xml") .withMappingBuilder(beanMappingBuilder())               .build();   return mapper;  }   @Bean  public BeanMappingBuilder beanMappingBuilder() {    return new BeanMappingBuilder() {   @Override  protected 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;  // 课程Id private 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);  }   @Override  public 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; }   }   @Override  public 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 {    @Override  public void mappingStarted(DozerEvent dozerEvent) { log.info("mappingStarted");   }   @Override  public void preWritingDestinationValue(DozerEvent dozerEvent) { log.info("preWritingDestinationValue");   }   @Override  public void postWritingDestinationValue(DozerEvent dozerEvent) {    log.info("postWritingDestinationValue");  }   @Override  public 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。如果你在业务中需要一些特殊的转换规则,欢迎留言交流,我们一起探讨实现


推荐阅读

  • 19条效率至少提高3倍的MySQL技巧

  • 一次心惊肉跳的服务器误删文件的恢复过程

  • 阿里P10、腾讯T4、华为18都是怎样的神级收入?

  • Git 自救指南:这些坑你都跳得出吗?

  • 用了那么多年MySQL不知道Explain?怪不得性能那么

自律到极致 - 人生才精致:第9期

正在筹备中!

关注我,加个星标,不忘签到哦~

点一点“阅读原文”小惊喜在等你

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

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

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

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

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

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

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

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

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

  5. WCF Data transfer buffered VS streamed

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

  6. Error in eval(predvars, data, env) : object ‘**‘ not found

    Error in eval(predvars, data, env) : object '**' not found 目录 Error in eval(predvars, data, env) : o ...

  7. Dao设计模式(Data Access Object)

    目    录(本篇字数:1858) 介绍 通用Dao 一.Dao泛型接口 二.JavaBean 三.Dao接口实现类 四.单元测试 五.反射工具类 介绍 Dao设计模式(Data Access Obj ...

  8. Java Data Access Object Pattern(数据访问对象模式)

    数据访问对象模式(Data Access Object Pattern)或 DAO 模式用于把低级的数据访问 API 或操作从高级的业务服务中分离出来.以下是数据访问对象模式的参与者. 数据访问对象接 ...

  9. 5G NR RLC:Data Transfer ARQ

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

最新文章

  1. linux硬件配置软件,洞悉Linux系统软硬件配置
  2. pythonssl双向认证_Python 上的 ssl 提示验证失败,有什么办法深究具体失败的原因么?...
  3. post multipart/form-data 类型表单如何获取File外其他参数
  4. 用JAVA制作小游戏——推箱子(二)
  5. python 迭代器相关知识
  6. Netty核心模块组件
  7. web响应式图片设计实现
  8. 最小二乘法拟合圆公式推导及vc实现[r]
  9. 5.3(将千克转换成磅)
  10. rk3399_android7.1调试USB蓝牙模块小结
  11. Android 经常使用设计模式(一)
  12. Android Studio3.5 JNIDemo实现步骤详解
  13. 【离散数学】第二章 命题逻辑的推理理论
  14. java开灯问题_算法题-开灯问题
  15. 在c语言中 char型数据在内存中的存储形式是,在c语言中char型数据在内存中的存储形式是什么?...
  16. java 中vo、po、dto、bo、pojo、entity、mode如何区分
  17. 陈慧琳“《江山美人》是我的代表作”
  18. 成为计算机网络管理员必修课(一)
  19. win10如何手动强制关联默认文件打开方式应用
  20. Caesers Cipher (凯撒密码)

热门文章

  1. websocket ws 协议 简介
  2. python3 导入上级目录中的模块
  3. golang 中string和int类型相互转换
  4. linux c glob使用(文件路径模式查找函数)
  5. linux shell 2 /dev/null的解释
  6. 编写 Debugging Tools for Windows 扩展,第 3 部分:客户端和回调 (windbg 插件 扩展)
  7. 遍历系统的所有ObjectType和TypeIndex
  8. 常用电脑密码破解技巧
  9. C语言 string.h 中函数的实现
  10. Linux0.11中对文本文件进行修改的策略