本文介绍下Java对象属性复制组件(MapStruct),以及项目中引入遇到的坑。
日常编程中,经常会碰到对象属性复制的场景,就比如下面这样一个常见的三层MVC架构。
前端请求通过VO对象接收,并通过DTO对象进行流转,最后转换成DO对象与数据库DAO层进行交互,反之亦然。
当业务简单的时候,可以通过手动编码getter/setter函数来复制对象属性。但是当业务变的复杂,对象属性变得很多,那么手写复制属性代码不仅十分繁琐,非常耗时间,并且还可能容易出错。
为了解决这个痛点,在项目初期,小辉项目的解决方法是随手写的转换工具函数:根据变量名进行反射,对基础类型和枚举的变量进行赋值。
总结下目前该工具函数的优缺点:
优点:
缺点:
那如果想要更强大的功能,有哪些开源组件可以选择呢?
下面小辉收集并盘点下相关开源组件的特点。
上面介绍的这些工具类,不管使用反射,还是使用字节码技术,这些都需要在代码运行期间动态执行,所以相对于手写硬编码这种方式,上面这些工具类执行速度都会慢很多。
而MapStruct与上面五个组件原理都不同。
以上提到的属性无法复制,都是在不使用手动写Convert函数的情况下进行讨论的
接下来就要介绍MapStruct 这个工具类,这个工具类之所以运行速度与硬编码差不多,这是因为MapStruct在编译期间就生成属性复制的代码,运行期间就无需使用反射或者字节码技术,从而确保了高性能。
另外,由于编译期间就生成了代码,所以如果有任何问题,编译期间就可以提前暴露,这对于开发人员来讲就可以提前解决问题,而不用等到代码应用上线了,运行之后才发现错误。
所以,为了克服项目中当前函数的被提到的五个缺点,笔者引入了MapStruct。
只需要引入MapStruct的依赖,同时由于MapStruct需要在编译器期间生成代码,所以我们需要maven-compiler-plugin插件中配置。
如果项目中没有用到lombok,下面的lombok相关配置可以删除;如果用到lombok,由于MapStruct和Lombok都会在编译期间生成代码,为解决冲突使用如下配置即可。
// pom.xml
<dependency>
<groupId>org.MapStruct</groupId>
<artifactId>MapStruct</artifactId>
<version>1.4.1.Final</version>
</dependency>
// pom.xml
// 为了防止lombok和MapStruct的冲突,在pom.xml加入如下配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${plugin.compiler.version}</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.MapStruct</groupId>
<artifactId>MapStruct-processor</artifactId>
<version>${MapStruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
使用MapStruct很简单,只需要创建一个mapper文件,然后在需要使用转换的地方,注入调用即可。
下面列举了两个文件,涵盖项目中绝大多数的mapper文件写法。
DO转成DTO的mapper:
/**
* componentModel = "spring":表明该类是一个 spring 组件,之后调用处只需要使用@Autowired,即可引入该类实例
* NullValuePropertyMappingStrategy.IGNORE:如果遇到旧对象属性为null,则跳过该属性赋值给新对象
*/
@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserTransMapper {
/**
* 这个对象可用于非Spring环境下获取当前对象实例。如果在Spring环境下,该行代码可删除
*/
UserTransMapper INSTANCE = Mappers.getMapper(UserTransMapper.class);
/**
* 将Userinfo对象中非null的属性转化为UserDto的对象
* @param userInfo 从数据库读取的用户信息
* @return
*/
UserDto userInfo2userDto(UserInfo userInfo);
/**
* 将Userinfo对象中非null的属性更新到UserDto的对象
* @param userInfo 从数据库读取的用户信息
* @param userDto 用户信息的dto
* 如果改void为UserDto,则函数会返回更新后的UserDto对象
*/
void updateUserInfo2userDto(UserInfo userInfo, @MappingTarget UserDto userDto);
/**
* 将UserDto对象中非null的属性转化为LoginEventDto的对象
* @param userDto 用户信息的dto
* @return LoginEventDto继承UserDto
*/
LoginEventDto userDto2loginEventDto(UserDto userDto);
}
DTO转成VO的mapper:
@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserTransMapper {
/**
* UserDto对象中非null的属性转化为UserInfoVo的对象
* @param userDto 用户信息的dto
* @return UserInfoVo继承与UserBaseInfoVo,都是用了@Data,没有异常报错。
*/
UserInfoVo userDto2userVo(UserDto userDto);
/**
* 直接写嵌套List等集合类,同样可以生效
* @param userDtoList
* @return
*/
List<UserInfoVo> userDto2userVo(List<UserDto> userDtoList);
/**
* 如果UserDto存在成员变量是类UserSubDto,而UserInfoVo存在成员变量是类UserSubVo,想在上面转化的同时,让这两个成员变量进行赋值,只需要定义下面的函数即可。
*
* @param userSubDto 用户信息的dto中的成员变量,类型为UserSubDto
* @return
*/
UserSubVo userSubDto2userSubVo(UserSubDto userSubDto);
/**
* UserDto对象和FollowInfoDto对象中非null的属性转化为UserInfoVo的对象
* @param userDto 用户信息的dto
* @param followInfoDto 关注粉丝的dto
* @param hn 房子数量
* @return
*/
@Mappings({
@Mapping(source = "userDto.regionId",target = "regionId"),
@Mapping(source = "followInfoDto.price", target = "price", numberFormat = "0.00"),
@Mapping(source = "hn",target = "houseNumber")
})
/**
* @Mapping也就是手动映射字段的操作,使用简单,读者可自行研究
*/
UserInfoVo userDto2userVo(UserDto userDto, FollowInfoDto followInfoDto, Integer hn);
/**
* 假设从映射Person到PersonDto需要一些MapStruct无法生成的特殊逻辑,可以定义一个default函数
*/
default PersonDto personToPersonDto(Person person) {
// 手动写映射逻辑
}
}
这次改造中相关依赖的版本:
说明:
虽然本文极力推荐MapStruct,但如果是老项目的话,尤其是大项目的话,还是考虑下改造后的测试成本。本人在第一次引入的时候,过于自信,在父pom引入MapStruct并提升了lombok版本,直接导致开发环境的微服务集体报错。后来改为在单个微服务实验,并且放在开发环境长期观察(主要这个改动影响测试覆盖面太大,也不想让QA为了技术优化来加班),之后才敢放到生产。
当然如果是新项目,非常推荐尝试下MapStruct。
参考