前言
在多层应用中,不管是之前的经典三层模型、还是现在非常流行的DDD
模型,都需要对各种不同的分层对象(JavaBean
)进行转换,也就是JavaBean
的属性映射问题。最常见和最简单的方式是编写对象属性转换函数,即普通的 Getter
/Setter
方法。除此之外各种各种属性映射工具。
几种常见的 Java 属性映射工具及原理
工具
org.springframework.beans.BeanUtils#copyProperties
org.apache.commons.beanutils.BeanUtils#copyProperties
net.sf.cglib.beans.BeanCopier#copy
mapstruct
原理
反射技术
Spring
框架的BeanUtils
与Appach
的BeanUtils
都是使用的Java
反射技术实现映射。效率相对较差。
字节码生成技术
cglib
的BeanCopier
使用的是动态生成字节码的技术,动态生成对象间的转换器,与手动写getter
、setter
性能相当。
编译期技术
类似Lombok
,mapstruct
其实也是一种代码生成技术,在编译期生成真实的转换代码,所以性能也与手动写getter
、setter
性能相当。
因此从性能来讲首推 Getter/Setter
方式(含 MapStruct
),其次是 cglib
。
选用哪种技术?
属性转换工具的优势:用起来方便,往往一行行代码就实现多属性的转换。
像mapstruct
在属性不对应的情况下,还可以通过注解或者修改配置方式自动适配,功能非常强大。
属性转换工具的缺点:
- 多次对象映射(从 A 映射到 B,再从 B 映射到 C)如果属性不完全一致容易出错;
- 有些转换工具,属性类型不一致自动转换容易出现意想不到的 BUG;
- 基于反射和字节码增强技术的映射工具实现的映射,对一个类属性的修改不容易感知到对其它转换类的影响。
我们可以想想这样一个场景:
一个
UserDO
如果属性超多,转换到UserDTO
再被转换成UserVO
。如果你修改UserDTO
的一个属性命名,其它类待映射的类新增的对应属性有一个字母写错了,编译期间不容易发现问题,造成 BUG。如果使用原始的
Getter/Setter
方式转换,修改了UserDO
的属性,那么转换代码就会报错,编译都不通过,这样就可以逆向提醒我们注意到属性的变动的影响。
因此强烈建议使用定义转换类和转换函数,使用插件实现转换,不需要引入其它库,降低了复杂性,可以支持更灵活的映射。
大家可以想想这种场景:
如果一个 A 映射到 B,B 有两个属性来自 C,一个属性来自于传参或者计算等。
此时自定义转换函数就更方便。
如果使用属性映射工具推荐使用 MapStruct,更安全一些,转换效率也很高。
但是MapStruct
也有他的缺点,对代码有一定的侵入性。及时两个对象拥有类型、名称相同属性,每两个转换对象需要定义Mapper
。
oneyoung-converter
oneyoung-converter
结合了两种主流的映射方案。使用方式简单方便,开箱即用。
- 支持自动映射,像
BeanUtils
一样,只需要传入源对象(Object
)和目标对象(Class
),即可完成转换。- 底层使用了Java动态编译技术,自动生成转换器代码,编译并载入
JVM
- 两个对象间,自动转换器只会生成一次,然后就会装入
Cache
复用 - 性能与自定义
Getter/Setter
一致。
- 底层使用了Java动态编译技术,自动生成转换器代码,编译并载入
- 支持自定义映射,你可以自定义转换器,只需要实现它提供的接口
top.oneyoung.converter.Converter
,并注册到转换器工厂。ConverterFactory.register(new CreateSchoolRequestToCreateSchoolServiceRequestConverter());
- 提供自动映射启停开关,若开启自动映射且添加了自定义映射,将优先使用自定义的转换器完成映射。若关闭了自动映射且没有自定义转换器,则会抛出找不到转换器的异常
ConvertException
.
不管你是自动映射还是自定义映射,oneyoung-converter
的使用方式只有下面这几种。
- 直接转换两个对象
1 | CreateSchoolRequest request = CreateSchoolRequest.builder().name("test").build(); |
- 转换集合对象
1 | CreateSchoolRequest input = CreateSchoolRequest.builder() |
oneyoung-converter集成spring-boot
引入依赖
依赖已发布至Maven中央仓库
https://repo1.maven.org/maven2/top/oneyoung/
1 | <dependency> |
配置
1 | # 控制自动映射开闭 |
添加自定义映射
创建一个自定义转换器,只需要实现接口top.oneyoung.converter.Converter
,并注册为spring bean
,即会载入转换器。
1 |
|
spring
在加载初期就会自动扫描需要加载的bean
,通过实现BeanPostProcessor
我们做了拦截,将Converter
载入ConverterFactory
看到日志下面这种日志,说明我们成功注册了自定义转换器。
1 | 2022-04-16 12:26:56.584 INFO 75693 --- [ main] t.o.c.starter.SpringFactoryRegister : ConverterFactory Register: class top.oneyoung.portal.entity.Request-->class top.oneyoung.portal.entity.Response By class top.oneyoung.portal.converter.RequestToResponseConverter |
使用
与上面列举的使用方式一致。
开源
这个转换器已经被我开源。欢迎Star
https://github.com/oneyoungg/oneyoung