在日常开发过程中,我们可能需要用到对象复制,那么我们如何优雅的进行对象复制呢?
在日常开发中如何优雅的实现对象复制。
首先看看为什么需要对象复制?
为什么需要对象复制
如上,是平时开发中最常见的三层MVC架构模型,编辑操作时Controller层接收到前端传来的DTO对象,在Service层需要将DTO转换成DO,然后在数据库中保存。
查询操作时Service层查询到DO对象后需要将DO对象转换成VO对象,然后通过Controller层返回给前端进行渲染。
这中间会涉及到大量的对象转换,很明显不能直接使用getter/setter复制对象属性,这看上去太low了。
所以必须要找一个第三方工具来实现对象转换。
看到这里有同学可能会问,为什么不能前后端都统一使用DO对象呢?这样就不存在对象转换呀?
设想一下如果不想定义 DTO 和 VO,直接将 DO 用到数据访问层、服务层、控制层和外部访问接口上。
此时该表删除或则修改一个字段,DO 必须同步修改,这种修改将会影响到各层,这并不符合高内聚低耦合的原则。
通过定义不同的 DTO 可以控制对不同系统暴露不同的属性,通过属性映射还可以实现具体的字段名称的隐藏。
不同业务使用不同的模型,当一个业务发生变更需要修改字段时,不需要考虑对其它业务的影响,如果使用同一个对象则可能因为 “不敢乱改” 而产生很多不优雅的兼容性行为。
对象复制工具类推荐
对象复制的类库工具有很多,除了常见的Apache的BeanUtils,Spring的BeanUtils,Cglib BeanCopier,还有重量级组件MapStruct,Orika,Dozer,ModelMapper等。
如果没有特殊要求,这些工具类都可以直接使用,除了Apache的BeanUtils。
原因在于Apache BeanUtils底层源码为了追求完美,加了过多的包装,使用了很多反射,做了很多校验,所以导致性能较差,并在阿里巴巴开发手册上强制规定避免使用 Apache BeanUtils。
强制规定避免使用 Apache BeanUtils。
至于剩下的重量级组件,综合考虑其性能还有使用的易用性,这里更推荐使用Orika。
Orika底层采用了javassist类库生成Bean映射的字节码,之后直接加载执行生成的字节码文件,在速度上比使用反射进行赋值会快很多。
国外大神 baeldung 已经对常见的组件性能进行过详细测试,大家可以查看。
Orika基本使用
要使用Orika很简单,只需要简单四步:
引入依赖
1 | <dependency> |
构造一个MapperFactory
1 | MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); |
注册字段映射
1 | mapperFactory.classMap(SourceClass.class, TargetClass.class) |
当字段名在两个实体不一致时可以通过.field()
方法进行映射,如果字段名都一样则可省略,byDefault()
方法用于注册名称相同的属性,如果不希望某个字段参与映射,可以使用exclude
方法。
进行映射
1 | MapperFacade mapper = mapperFactory.getMapperFacade(); |
经过上面四步就完成了SourceClass到TargetClass的转换。至于Orika的其他使用方法大家可以参考
每次都要这先创建MapperFactory
,建立字段映射关系,才能进行映射转换
别急,这里准备了一个工具类OrikaUtils
提供了五个公共方法:
1 | import ma.glasnost.orika.MapperFacade; |
分别对应:
- 字段一致实体转换
- 字段不一致实体转换(需要字段映射)
- 字段一致集合转换
- 字段不一致集合转换(需要字段映射)
- 字段属性转换注册
接下来通过单元测试案例重点介绍此工具类的使用。
Orika工具类使用文档
先准备两个基础实体类,Student,Teacher。
1 |
|
TC1,基础实体映射
1 | /** |
输出结果:
1 | Teacher(id=1, name=java, emailAddress=null) |
此时由于属性名不一致,无法映射字段email。
TC2,实体映射 - 字段转换
1 | /** |
输出结果:
1 | Teacher(id=1, name=java, emailAddress=2427259171@qq.com) |
此时由于对字段做了映射,可以将email映射到emailAddress。
注意这里的refMap中key放置的是源实体的属性,而value放置的是目标实体的属性,不要弄反了。
TC3,基础集合映射
1 | /** |
输出结果:
1 | [Teacher(id=1, name=java, emailAddress=null), Teacher(id=2, name=LiJing, emailAddress=null)] |
此时由于属性名不一致,集合中无法映射字段email。
TC4,集合映射 - 字段映射
1 | /** |
输出结果:
1 | [Teacher(id=1, name=java, emailAddress=2427259171@qq.com), Teacher(id=2, name=LiJing, emailAddress=2427259171@qq.com)] |
也可以通过这样映射:
1 | Map<String,String> refMap = new HashMap<>(2); |
TC5,集合与实体映射
有时候需要将集合数据映射到实体中,如Person类
1 |
|
现在需要将Person类nameParts的值映射到Student中,可以这样做
1 | /** |
输出结果:
1 | Student(id=1, name=java, email=2427259171@qq.com) |
TC6,类类型映射
有时候需要类类型对象映射,如BasicPerson类
1 |
|
现在需要将BasicPerson映射到Teacher
1 | /** |
输出结果:
1 | Teacher(id=1, name=java, emailAddress=2427259171@qq.com) |
TC7,多重映射
有时候会遇到多重映射,如将StudentGrade
映射到TeacherGrade
1 |
|
这种场景稍微复杂,Student与Teacher的属性有email字段不相同,需要做转换映射;StudentGrade与TeacherGrade中的属性也需要映射。
1 | /** |
多重映射的场景需要根据情况调用OrikaUtils.register()
注册字段映射。
输出结果:
1 | TeacherGrade(teacherGradeName=硕士, teacherList=[Teacher(id=1, name=java, emailAddress=2427259171 .com), Teacher(id=2, name=LiJing, emailAddress=2427259171 .com)]) |
TC8,MyBaits plus分页映射
如果使用的是mybatis的分页组件,可以这样转换
1 | public IPage<UserDTO> selectPage(UserDTO userDTO, Integer pageNo, Integer pageSize) { |
小结
在MVC架构中肯定少不了需要用到对象复制,属性转换的功能,借用Orika组件,可以很简单实现这些功能。