Java对象属性复制组件-Mapstruct项目改造指南

下面给大家介绍下Java对象属性复制组件(MapStruct),以及项目中引入遇到的坑。

1. 问题背景

日常编程中,经常会碰到对象属性复制的场景,就比如下面这样一个常见的三层MVC架构。

前端请求通过VO对象接收,并通过DTO对象进行流转,最后转换成DO对象与数据库DAO层进行交互,反之亦然。

当业务简单的时候,可以通过手动编码getter/setter函数来复制对象属性。但是当业务变的复杂,对象属性变得很多,那么手写复制属性代码不仅十分繁琐,非常耗时间,并且还可能容易出错。

为了解决这个痛点,在项目初期,小辉项目的解决方法是随手写的转换工具函数:根据变量名进行反射,对基础类型和枚举的变量进行赋值。

总结下目前该工具函数的优缺点:

优点:

开发效率高,随时想要转换的时候,传入源对象以及指定class,调用下函数即可。

缺点:

项目中大量的反射会严重影响代码执行效率

由于使用了反射,所以成员变量的使用被追踪就很麻烦

转换失败只有在运行中报错才会发现

对于嵌套对象字段的情况无能为力

只能对基础类型进行复制

对字段名不一致的属性无法赋值

2. 开源组件选择

那如果想要更强大的功能,有哪些开源组件可以选择呢?

下面小辉收集并盘点下相关开源组件的特点。

1. Apache BeanUtils

底层原理运用反射。

嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝。

字段名不一致的属性无法被复制。

类型不一致的字段,将会进行默认类型转化。

2. Spring BeanUtils:

底层原理同样运用反射,但相比Apache BeanUtils减少了反射校验,同时增加了缓存,所以提升了转换速度。

嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝。

字段名不一致,属性无法复制。

类型不一致的字段,将会进行默认类型转化。

3. Cglib BeanCopier

字节码技术动态生成一个代理类,代理类实现get和set方法。生成代理类过程存在一定开销,但是一旦生成,我们可以缓存起来重复使用。相比前两个更好用。

嵌套对象字段,将会与源对象使用同一对象,即使用浅拷贝。

字段名不一致,属性无法复制。

类型不一致的字段,将会进行默认类型转化。

4. Dozer

运用反射。

嵌套对象字段,不会与源对象使用同一对象,即深拷贝。

默认支持类型不一致(基本类型/包装类型)转换。

通过配置字段名的映射关系,不一样字段的属性也被复制。

5. orika

底层其使用了javassist生成字段属性的映射的字节码,然后直接动态加载执行字节码文件,相比于使用反射的工具类,速度上会快很多。

支持深拷贝。

默认支持类型不一致(基本类型/包装类型)转换。

通过配置字段名的映射关系,不一样字段的属性也被复制。

上面介绍的这些工具类,不管使用反射,还是使用字节码技术,这些都需要在代码运行期间动态执行,所以相对于手写硬编码这种方式,上面这些工具类执行速度都会慢很多。

而MapStruct与上面五个组件原理都不同。

以上提到的属性无法复制,都是在不使用手动写Convert函数的情况下进行讨论的

3. MapStruct

1. 为什么选择MapStruct

接下来就要介绍MapStruct 这个工具类,这个工具类之所以运行速度与硬编码差不多,这是因为MapStruct在编译期间就生成属性复制的代码,运行期间就无需使用反射或者字节码技术,从而确保了高性能。

另外,由于编译期间就生成了代码,所以如果有任何问题,编译期间就可以提前暴露,这对于开发人员来讲就可以提前解决问题,而不用等到代码应用上线了,运行之后才发现错误。

所以,为了克服项目中当前函数的被提到的五个缺点,笔者引入了MapStruct。

2. 如何引入MapStruct

只需要引入MapStruct的依赖,同时由于MapStruct需要在编译器期间生成代码,所以我们需要maven-compiler-plugin插件中配置。

如果项目中没有用到lombok,下面的lombok相关配置可以删除;如果用到lombok,由于MapStruct和Lombok都会在编译期间生成代码,为解决冲突使用如下配置即可。

// pom.xml

org.MapStruct

MapStruct

1.4.1.Final

// 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>

3. MapStruct的常见使用方法

使用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的对象

* @paramuserInfo从数据库读取的用户信息

* @return

*/

UserDtouserInfo2userDto(UserInfouserInfo);

/**

*将Userinfo对象中非null的属性更新到UserDto的对象

* @paramuserInfo从数据库读取的用户信息

* @paramuserDto用户信息的dto

*如果改void为UserDto,则函数会返回更新后的UserDto对象

*/

void updateUserInfo2userDto(UserInfouserInfo, @MappingTarget UserDtouserDto);

/**

*将UserDto对象中非null的属性转化为LoginEventDto的对象

* @paramuserDto用户信息的dto

* @return LoginEventDto继承UserDto

*/

LoginEventDtouserDto2loginEventDto(UserDtouserDto);

}

DTO转成VO的mapper:

@Mapper(componentModel ="spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)

public interface UserTransMapper {

/**

* UserDto对象中非null的属性转化为UserInfoVo的对象

* @paramuserDto用户信息的dto

* @return UserInfoVo继承与UserBaseInfoVo,都是用了@Data,没有异常报错。

*/

UserInfoVouserDto2userVo(UserDtouserDto);

/**

*直接写嵌套List等集合类,同样可以生效

* @paramuserDtoList

* @return

*/

List<UserInfoVo>userDto2userVo(List<UserDto>userDtoList);

/**

*如果UserDto存在成员变量是类UserSubDto,而UserInfoVo存在成员变量是类UserSubVo,想在上面转化的同时,让这两个成员变量进行赋值,只需要定义下面的函数即可。

*

* @paramuserSubDto用户信息的dto中的成员变量,类型为UserSubDto

* @return

*/

UserSubVouserSubDto2userSubVo(UserSubDtouserSubDto);

/**

* UserDto对象和FollowInfoDto对象中非null的属性转化为UserInfoVo的对象

* @paramuserDto用户信息的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也就是手动映射字段的操作,使用简单,读者可自行研究

*/

UserInfoVouserDto2userVo(UserDtouserDto, FollowInfoDtofollowInfoDto, Integer hn);

/**

*假设从映射Person到PersonDto需要一些MapStruct无法生成的特殊逻辑,可以定义一个default函数

*/

defaultPersonDtopersonToPersonDto(Person person) {

//手动写映射逻辑

}

}

4. 项目改造与踩坑提示

这次改造中相关依赖的版本:

lombok版本1.16.22,改造时升级为1.18.12

项目原有依赖fastjson版本1.2.62

引入MapStruct版本为1.4.1.Final

说明:

之所以要升级lombok版本,是因为上面UserDto对象转化为LoginEventDto对象时,原有项目只在UserDto上添加@Builder,但是继承类LoginEventDto无法继承@Builder,导致MapStruct实例化的时候实例一个UserDto对象。

解决方法:在继承层次结构的所有类(即LoginEventDto和UserDto)都需要使用@SuperBuilder可以,(类UserDto的@Builder要去掉)但这个@SuperBuilder只在更高的lombok版本才有,所以才升级了lombok版本。

项目中使用了fastjson,因此业务代码中出现很多处需要反射调用无参构造函数。但在上面一步升级lombok的过程中,lombok对于@Builder的实现出现了一些修改:在1.16.22的生成代码中,是存在private级别的无参构造函数;而在1.18.12的生成代码中,并没有私有无参构造函数,从而导致了业务代码大量出现缺少默认构造函数的报错。

解决方法:@Builder注解跟构造函数之间的冲突很常见。最佳实践是:在所有使用@Builder或者@SupserBuilder的类,增加@NoArgsConstructor和@AllArgsConstructor。

虽然本文极力推荐MapStruct,但如果是老项目的话,尤其是大项目的话,还是考虑下改造后的测试成本。本人在第一次引入的时候,过于自信,在父pom引入MapStruct并提升了lombok版本,直接导致开发环境的微服务集体报错。后来改为在单个微服务实验,并且放在开发环境长期观察(主要这个改动影响测试覆盖面太大,也不想让QA为了技术优化来加班),之后才敢放到生产。

当然如果是新项目,非常推荐尝试下MapStruct。

5. Q&A

在项目引入MapStruct时,有人会提出现在反射的性能消耗已经很低了,Spring、Mybatis等各种框架中大量使用反射,为什么还要使用MapStruct这种编译期生成代码的组件?

主要有如下考虑:

1.反射本身的性能损耗还是很大的,但由于开源库对反射进行了缓存等优化处理,才减少反射对性能损耗的影响。然而,相比调用MapStruct生成的方法,优化后的性能还是差很多。

2.开源库使用反射是为了通用性考虑,但在具体的业务场景,对象之间的转换是很确定的。

3.MapStruct组件本身使用很简单(看完这篇博客之后,可以解决大部分应用场景)。同时, MapStruct组件还能处理一些反射无法处理或者更加灵活解决一些应用问题。

总结:

本文给大家带来Java对象属性复制组件-Mapstruct项目改造指南希望大家能够喜欢,期待下期有更好的文章给大家

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容