工程师工具箱系列(1)MapStruct
芸芸众生
在Java项目开发中,不管你是采用传统的MVC分层模式,还是DDD驱动的微服务模式,都免不了在各层级之间传递对象,在这个过程中会出现许多的对象概念性名词:VO,DTO,DO,Entity,ValueObj等等。我们先不管这些对象在你们各自项目里的作用,有一个共同的工作就是完成他们之间赋值转换。
靠手动赋值来完成对象转换的人毕竟已经很稀缺了,我们一般都知道借助一些工具去简化这部分重复劳动。
目前市面上用的比较常见的可能有下面这几种:
它们之间的性能对比大致如下:
结合性能和吞吐量来看,手动写性能肯定是最高的,省去中间商赚差价嘛,但是社会有分工才能进步,整体效能才能增加,所以我们应该借助工具。
综合分析下来,MapStruct的性能和吞吐量都是最好的,毕竟实现原理上决定了一切,接下来我们就上手下MapStruct。
初窥门径
mapstruct的使用和如何把大象放进冰箱的步骤是一样的:1 引入mapstruct;2 创建转换器与转换方法;3 获取转换实例进行使用
引入POM依赖
Maven
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
Gradle
plugins {
...
id "com.diffplug.eclipse.apt" version "3.26.0" // Only for Eclipse
}
dependencies {
...
compile 'org.mapstruct:mapstruct:1.4.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
testAnnotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final' // if you are using mapstruct in test code
}
创建转换器与方法
创建之前你肯定已经明确了需要转换的两个类,比如下面的代码示例,是将Car对象转换成一个CarDto对象
@Mapper //指定该类为mapstruct的映射器
public interface CarMapper {
// 通过ClassLoader加载
CarMapper INSTANCE = Mappers.getMapper( CarMapper.class );
//转换方法,自动匹配名称与类型相同的字段,不同的字段需要通过Mapping注解进行指定
// 这里就指定了将Cat对象的numberOfSeats属性转换赋值到CatDto的seatCount属性
@Mapping(target = "seatCount", source = "numberOfSeats")
CarDto carToCarDto(Car car);
}
进行使用
使用的时候就特别简单了,直接获取转换器的实例,调用转换方法,传入对应的参数即可
CarDto carDto = UserMapper.INSTANCE.carToCarDto(car);
好像还蛮简单的,但是实际开发的时候可没怎么简单,实际的业务和不用的开发人员有不同的习惯,有时候面临的场景就会复杂起来:
- 字段名称相同,但是类型不同怎么处理?mapstruct会帮我们自动转换吗,它怎么知道怎么转换?
- 对象和字符串之间转换怎么处理?实际开发
- 列表和列表之间转换怎么处理?难道我也循环遍历吗?
- 灵活的自定义转换怎么处理?
另外喜欢偷懒的小伙伴可能还会有一个疑问:虽然说用起来只有三步,但是每次都要为两个转换对象创建一个转换器的话,那岂不是会有很多的转换器了?
这些问题都会游刃有余小节中得到解答,该小结中利用了面向对象设计方法,省去了编写大量转换器与方法工作,并利用java8新特性方便实现灵活的自定义转换。
IDEA好基友
为了更好使用mapstruct,如果你使用的是Intellij IDEA编辑器,那么建议你安装个插件,它可以为我们提供一些遍历操作。
安装时候直接在IDEA的插件市场上搜索mapstruct,安装重启即可,插件为我们提供了一下几个便捷操作:
-
自动填充属性与枚举常量
-
点击可以直达注解使用的声明字段
-
可以查找使用过的地方
PS:插件地址:https://plugins.jetbrains.com/plugin/10036-mapstruct-support
游刃有余
示例说明
为了更好说明示例,我们定义两个需要转换的对象类,我把它们之间字段的区别也列了出来
- 相同字段:指的是名词和类型都相同,工具会自动转换
- 原始类特有:指的是原始类UserE所特有的,可能有3种情况:类型一致但是名称不一致,类型不一致名称也不一致,类型不一致名称一致
- 目标类特有:指的是目标类UserVO所特有的,它同时也对应上面原始类的三种情况
避免编写重复转换器
要避免编写重复的转换器接口,类似我们要避免编写不同类型的字段进行某种相同计算一样。很自然的就想到使用泛型来解决。
我们可以定义一个基础接口,包含了通用的映射方法,只要是字段类型相同的对象需要转换,这个基础接口就满足了,通过继承基础接口,传入具体的转换类型,无需任何实现与配置。
这里我提供了三种通用转换方法:1 单对象的转换;2 列表对象的转换;3 Stream对象转换,因为每种类型存在互相转换,所以基础接口包含了6个方法
同时,你可以把项目中约定好的一些字段约束加到其中,比如创建日期的格式等等
实现复杂灵活转换
接下来就是解决上面表格中的3种情况,它们的解决方案分别如下:
首先定义个UserMapping接口,继承BaseMapping,传入转换的类型,注意你自己规定的SOURCE和TARGET参数,不要搞混就行
@Mapper(componentModel = "spring")//spring注入方式
public interface UserMapping extends BaseMapping<UserE,UserVO>{
重载接口的方法,比如现在我们把UserE转换成为UserVO,解决类型一致,名称不一致的Mapping示例
@Mappings({
@Mapping(source = "etest", target = "vtest"),
@Mapping(source = "sex", target = "gender"),
})
@Override
UserVO sourceToTarget(UserE var1);
去掉@Mappings,直接把多个@Mapping加在方法上面作用是相同的
那怎么解决cteateTime名称一致,类型不一致呢?
从UserE到UserVO是把时间类型转换为String类型,这是一种很常见的转换常见,注意看我们在基础接口中定义了目标字段的cteateTime时间格式,这就给工具提供了自动转换的可能性,主要给出的格式符合这个要求,那么工具会自动帮助我们完成转换
/**
* 映射同名属性
*/
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
TARGET sourceToTarget(SOURCE var1);
最后看名称不一致,类型也不一致的字段,UserE中的字符串如何变成UserVO中的一个对象,首先容易想到的一点是我们可以通过@Mapping配置建立二者之间的转换关系,但是工具肯定不知道怎么转换了,所以还我们需要提供如何转换方法。
@Mappings({
@Mapping(source = "etest", target = "vtest"),
@Mapping(source = "sex", target = "gender"),
@Mapping(source = "configE",target = "configs")
})
@Override
UserVO sourceToTarget(UserE var1);
那么如何提供呢?假设我们已经写好一个转换方法,应该如何告知工具去选择使用?我相信你已经想到了,只要指定入参和出参类型,再结合mapping指定映射关系工具应该就能完成转换了。
于是我们再利用java8种接口可以使用默认方法的特性,我们直接在接口里增加
/**
* 映射string config 到 List<UserVO.UserConfig> list的转换
* 会被自动调用
*/
default List<UserVO.UserConfig> strConfigToListUserConfig(String config) {
return JSONUtil.toList(config,UserVO.UserConfig.class);
}
这两步加起来就构成完成了类型不一致,名称不一致属性之间的转换
但是其实还是存在一个问题,如果存在多个指定转换关系,入参和出参也一致的情况,那工具就不知道具体采用哪个默认方法了。所以我们还需要知道如何完全自定义转换。
自定义一个转换类
public class CustmMapping {
public static String convertFiled1(UserVO.UserConfig userConfig){
return "自定义" + userConfig.getField1();
}
}
在接口类中导入转换类(1处),在@Mapping中指定目标字段的转换类函数(2处)
@Mapper(componentModel = "spring",imports = CustmMapping.class)//1处
public interface UserMapping extends BaseMapping<UserE,UserVO>{
...
@Mapping(target = "sex", source = "gender")
@Mapping(target = "password", ignore = true)
@Mapping(target = "etest", source = "vtest")
@Mapping(target="configE",expression="java(CustmMapping.convertFiled1(var1.getConfigs().get(0)))")//2处
@Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
@Override
UserE targetToSource(UserVO var1);
...
对应的测试代码:
@Log4j2
@DisplayName("使用MapStruct进行对象赋值转换")
public class MapStructTest {
private static UserE userE;
private static UserVO newUserVO;
private static UserMapping userMapping;
@BeforeAll
public static void init() {
userE = new UserE()
.setId(100L)
.setBirthday(LocalDate.of(1988,02,25))
.setUsername("临江仙")
.setCreateTime(LocalDateTime.now())
.setSex(1)
.setEtest(Arrays.asList("a","b","c"))
.setConfigE("[{\"field1\":\"Test Field1\",\"field2\":500}]");
userMapping = Mappers.getMapper(UserMapping.class);
List<UserVO.UserConfig> userConfigs = new ArrayList<>();
for (int i = 0; i < 5; i++) {
UserVO.UserConfig userConfig = new UserVO.UserConfig("字段"+i, i+10);
userConfigs.add(userConfig);
}
newUserVO = new UserVO()
.setId(200L)
.setUsername("鹊桥仙")
.setPassword("123321")
.setBirthday(LocalDate.of(1988,07,06))
.setCreateTime("1988-02-25 12:00:00")
.setGender(2)
.setVtest(Arrays.asList("备注1","备注2","备注3"))
.setConfigs(userConfigs);
log.info("@BeforeAll: init()");
}
@DisplayName("准备好UserE和UserVO")
@Test
public void testHasUserEandUserVO(){
System.out.println("准备好的userE:" + userE);
System.out.println("准备好的newUserVO:" + newUserVO);;
}
@DisplayName("将UserE转换成UserVO")
@Test
public void testEtoVO() {
UserVO userVO = userMapping.sourceToTarget(userE);
System.out.println("转化后得到的userVO: " + userVO);
}
@DisplayName("将UserVO转换成UserE")
@Test
public void testVOtoE(){
UserE userE = userMapping.targetToSource(newUserVO);
System.out.println("转化后得到的userE:" + userE);
}
}
测试结果:
转化后得到的userVO: UserVO(id=100, username=临江仙, password=null, gender=1, birthday=1988-02-25, createTime=2021年6月1号, vtest=[a, b, c], configs=[UserVO.UserConfig(field1=Test Field1, field2=500)])
转化后得到的userE:UserE(id=200, username=鹊桥仙, password=null, sex=2, birthday=1988-07-06, createTime=1988-02-25T12:00, etest=[备注1, 备注2, 备注3], configE=自定义字段0)
准备好的userE:UserE(id=100, username=临江仙, password=null, sex=1, birthday=1988-02-25, createTime=2021-06-03T13:22:42.203, etest=[a, b, c], configE=[{"field1":"Test Field1","field2":500}])
准备好的newUserVO:UserVO(id=200, username=鹊桥仙, password=123321, gender=2, birthday=1988-07-06, createTime=1988-02-25 12:00:00, vtest=[备注1, 备注2, 备注3], configs=[UserVO.UserConfig(field1=字段0, field2=10), UserVO.UserConfig(field1=字段1, field2=11), UserVO.UserConfig(field1=字段2, field2=12), UserVO.UserConfig(field1=字段3, field2=13), UserVO.UserConfig(field1=字段4, field2=14)])
温故知新
最后我们对mapstruct工具做个小结:
- 核心特点 :基于 JSR 269 的 Java 注解处理器实现,用纯java方法而不是反射进行属性赋值,做到了编译时类型安全,相当于编译时的代码生成器。
- 性能更高:使用简单的Java方法调用代替反射,无需手动 set/get 或 implements Serializable 以达到深拷贝
- 编译时类型安全:只能映射相同名称或带映射标记的属性,编译时如果映射不完整(存在未被映射的目标属性)或映射不正确(找不到合适的映射方法或类型转换)则会在编译时抛出异常
使用技巧:
- 技巧一:定义一个公共的转换器接口,使用泛型定义好常用的方法,如果字段完全一样公共接口就满足要求了
- 技巧二:同类型不同名称的转换直接使用Mapping在转换方法上指定
- 技巧三:不同类型同名称的,可以使用Mapping也可以使用default方法的方式
- 技巧四:不同类型不同名称,可以使用Mapping+default方式或自定义转换类方式
运用这些技巧你还可以实现多个bean之间映射,复杂数据结构之间映射等,充分满足多种业务场景下使用。
PS:文中源码是示例地址:https://gitee.com/hzqiuxm/middleware-projects.git [java-base模块]-[mapstruct包]