Java博文
JAVA 21 都体验了吧
Java程序员必备的Intellij插件(长期更新,截止到2018-05-03) - 掘金
32.6k star🔥原来国内的独立开发者都在做这些事情
工作六年,我学会了用 Arthas 来辅助我的日常工作
太方便了!Arthas,生产问题大杀器 - 掘金
新一代Java高性能构建工具Maven-mvnd【实践可行版】
怎么在业务团队写好发消息的代码?
Intellij 开源热加载插件 HotSwapHelper 发布,兼容若依、jeecg 等框架
SpringBoot多环境日志配置_Java_快乐非自愿限量之名_InfoQ写作社区
VSCode配置JAVA开发环境_Java_IT蜗壳-Tango_InfoQ写作社区
Java虚拟线程探究与性能解析
Jakarta EE 11 发布,增强企业 Java 开发人员生产力和性能
重要:Java25正式发布(长期支持版)!
Access Token + Refresh Token 全解析:前后端分离架构的认证与安全方案
设计一个支持千万级用户的 IM 系统:消息推送如何保证可靠性
Spring Boot + CRaC 启动速度提升了10倍!
Java 25 新特性 更简洁、更高效、更现代
玩转 Java8 Stream,让你代码更高效紧凑简洁文章目录前言一、Stream特性二、Stream创建2.1用集合创 - 掘金
Guava 简介:让 Java 开发更高效
横空出世!MyBatis-Plus 同款 ES ORM 框架,用起来够优雅!
一个Java工程师的17个日常效率工具
Quarkus:轻量级 Java 的未来?
OpenJDK、Temurin、GraalVM...到底该装哪个?
Lombok坑哭了!若依框架一行@Data炸出Param为null,我卡了一下午才发现BaseEntity的猫腻
缓存性能王者,阿里巴巴二级缓存JetCache框架
MapStruct使用反思与简单易用性封装
Dockerfile 构建 Java 应用瘦身优化
还在手动搭Maven多模块?这款IDEA插件让我效率提升10倍(真实体验)
本文档使用 MrDoc 发布
-
+
MapStruct使用反思与简单易用性封装
> Tips:本文旨在探讨反思作者在使用`MapStruct`及其有关扩展,进行一些易用性封装所遇到的问题和看法,`MapStruct`的使用教学不是文章的主旨,观看本文章可能需要一定的`MapStruct`使用经验。另外,`MapStruct`与`BeanUtil`的比较不是文章的主题,但会放到文章末尾。文章中的一些观点仅限于我个人在使用`MapStruct`的一些个人看法见解,不一定对也不一定适合所有业务。 > > 文章中关键的核心代码已经打包到我的Gitee个人仓库,有兴趣的可以看一下,如果方便的话,也请提供一些改进建议:[gitee.com/ColorDreams…](https://link.juejin.cn?target=https%3A%2F%2Fgitee.com%2FColorDreams%2Fmapstruct-convert) `MapStruct`是一个用于数据对象转换场景(如实体类`Entity`与数据传输对象类`DTO`之间的映射等)的对象映射框架。其原理是基于`JSR-269`规范编译时生成接近手写、无反射开销的类型安全赋值映射代码(直接调用 `getter/setter`),对比`BeanUtil`和其它基于反射的对象映射框架, 具有更好的性能和健壮性,且在`Native`编译时也仍然适用,对于`Quarkus`、`Spring Native`等`Java Native`框架支持良好。 ### MapStruct Plus 使用反思:MapStruct Mapper的创建和调用应该是明确的 [**MapStruct Plus**](https://link.juejin.cn?target=https%3A%2F%2Fwww.mapstruct.plus)是我迄今为止唯一一个接触过的基于MapStruct增强类库(如果`lombok-mapstruct-binding`不算的话),它的实现思想应该是来源于京东云技术团队的一篇技术文章:[基于AbstractProcessor扩展MapStruct自动生成实体映射工具类](https://link.juejin.cn?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F601338931)。 `MapStruct Plus`的具体使用方式不是文章的主旨,这里就不再过多赘述,有兴趣的可以去了解一下。 简单来说就是:它做了一系列的封装,提供了一个`Converter`,使用`Converter`可以让你跟调用`BeanUtil`一样方便。而你需要做的是,在转换的类上面添加特定的注解,即可帮你自动生成`MapStruct Mapper`接口,再由`MapStruct`的注解处理器去生成对应的`Mapper`实现(具体原理可以看上面京东云技术团队的那篇文章)。 如: ```java public class User{ private Long userId; } public class UserDTO{ private Long userId; } import io.github.linpeilie.BaseMapper; public interface UserToUserDTOMapper extends BaseMapper<User, UserDTO> {} import io.github.linpeilie.Converter; private Converter converter = ... User user = converter.convert(userDto,User.class); ``` 上面的代码中,你不需要手动去做`Mapper`接口的编写,它会自动帮你生成一个`Mapper`并继承于它封装好的`BaseMapper`。`BaseMapper`源码如下: ```java public interface BaseMapper<S, T> { T convert(S source); T convert(S source, T target); default List<T> convert(List<S> sourceList) { if (CollectionUtils.isEmpty(sourceList)) { return new ArrayList<>(); } return sourceList.stream().map(this::convert).collect(Collectors.toList()); } } ``` `Converter`本质上就是调用了`BeanMapper`,从而实现类似`BeanUtil#copy`的效果: ```java public <S, T> T convert(S source, Class<T> targetType) { if (source == null) { return null; } BaseMapper<S, T> mapper = (BaseMapper<S, T>) converterFactory.getMapper(source.getClass(), targetType); if (mapper != null) { return mapper.convert(source); } throw new ConvertException( "cannot find converter from " + source.getClass().getSimpleName() + " to " + targetType.getSimpleName()); } ``` 到目前为止,还都很美好,对不对?接下来就要来点不美好的地方了。假设我现在又新建了一个`UserBo`类,但忘记加上注解了,就会出现这种情况: ```java public class UserBo{ private Long userId; } import io.github.linpeilie.Converter; private Converter converter = ... User user = converter.convert(userBo,User.class); ``` 细心的你已经发现了,这种做法可能会导致出现一些**不可控的黑盒代码**,`MapStruct`编译时就能排查出问题的优势一下子就没有了,并且**代码的入侵性并没有减少,反而更强了(`MapStruct Mapper`接口不需要修改转换的数据类,只关注接口本身)**。而在需要添加自定义转换逻辑时,你如果使用`MapStruct Plus`,仍需要在实体类上堆屎山,例如: ```java public class StringToListString { public List<String> stringToListString(String str) { return StrUtil.split(str); } } public class UserDTO { private Long userId; private String name; } ``` 可以发现,**自定义的转换逻辑并没有减少,只是从`Mapper`接口转移到了`UserDTO`上面,造成了严重的强入侵和强依赖**。 当然,你也可以自己去创建一个`Mapper`接口去做自定义转换逻辑,问题是,既然我都要自己创建`Mapper`接口了,我使用`MapStruct Plus`的意义是什么呢?它给我带来了什么便利? 基于以上,我个人认为: **MapStruct Mapper的创建和调用,应该是明确的,不应该走捷径,这一块没有捷径可言。** ### MapStruct简单易用性封装:可以像BeanUtil但不应该止于BeanUtil 那么,`MapStruct Plus`就完全没有可取之处吗?有的兄弟,有的。我们虽然强调**Mapper的创建和调用,应该是明确的**,但是没说不能**提供通用的Mapper**啊! 数据对象映射的场景无非就两个: 1. **数据类自身的copy/clone** (即不发生类型转换,但自身数据需要拷贝一份用于业务。应用场景如:主租户数据同步到子租户等) 2. **两个数据类之间的互转**(从一个数据类转为另一个数据类,例如上面的例子,从`User`转为`UserDTO`,或`UserDTO`转`User`) 基于以上两点,我们可以**创建3个接口**(为什么是3个别问,先创建) 点击查看代码 ```java public interface MapperAware {} public interface BeanCopyMapper<T> extends MapperAware { T copy(T bean); void copy(T bean, T targetBean); default List<T> copyList(List<T> beans) { return beans.stream() .map(this::copy) .collect(Collectors.toList()); } } public interface SourceTargetMapper<S, T> extends MapperAware { T toTarget(S source); void toTarget(S source, T target); default List<T> toTargetList(List<S> sourceList) { return sourceList.stream() .map(this::toTarget) .collect(Collectors.toList()); } S toSource(T target); void toSource(T target, S source); default List<S> toSourceList(List<T> targetList) { return targetList.stream() .map(this::toSource) .collect(Collectors.toList()); } } ``` 为了方便对这些通用的`Mapper`实例进行管理,我们还可以创建一个 `MapperFactory`接口,并提供默认的实现: 点击查看代码 ```java public interface MapperFactory { <M extends MapperAware> void registerMapper(M mapper); <M extends MapperAware> void registerMapper(String mapperName,M mapper); <M extends MapperAware> M getMapper(Class<M> mapperClass); <M extends MapperAware> M getMapper(String mapperName); } public class DefaultMapperFactory implements MapperFactory { private static final Map<String, MapperAware> MAPPER_MAPS = new ConcurrentHashMap<>(); public <M extends MapperAware> void registerMapper(M mapper) { registerMapper(mapper.getClass().getCanonicalName(), mapper); } public <M extends MapperAware> void registerMapper(String mapperName, M mapper) { MAPPER_MAPS.put(mapperName, mapper); } public <M extends MapperAware> M getMapper(final Class<M> mapperClass) { return (M) MAPPER_MAPS.computeIfAbsent(mapperClass.getCanonicalName(), mapperName -> Mappers.getMapper(mapperClass)); } public <M extends MapperAware> M getMapper(String mapperName) { return (M) MAPPER_MAPS.get(mapperName); } } ``` 通常来说,我们在实际开发中都是通过框架的IOC容器去管理`Mapper`,比如`Spring`,所以我们可以提供一个基于`Spring IOC`容器管理的`MapperFactory`: 点击查看代码 ```java public class SpringBeanMapperFactory implements MapperFactory, ApplicationContextAware { private ApplicationContext applicationContext; public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } public <M extends MapperAware> void registerMapper(M mapper) { } public <M extends MapperAware> void registerMapper(String mapperName, M mapper) { } public <M extends MapperAware> M getMapper(Class<M> mapperClass) { return applicationContext.getBean(mapperClass); } public <M extends MapperAware> M getMapper(String mapperName) { return (M) applicationContext.getBean(mapperName); } } ``` 这样一来,我们就具备了三个通用的`Mapper`接口,以及`MapperFactory`及其实现。但是要想实现类似`BeanUtil`那种效果,我们还需要一个类似的工具类,如: 点击查看代码 ```java public class BeanConvertUtil { public static final MapperFactory DEFAULT_MAPPER_FACTORY = new DefaultMapperFactory(); private static MapperFactory MAPPER_FACTORY = DEFAULT_MAPPER_FACTORY; public static void setMapperFactory(final MapperFactory mapperFactory) { MAPPER_FACTORY = mapperFactory; } public static <M extends MapperAware> M getMapper(final Class<M> mapperClass) { return MAPPER_FACTORY.getMapper(mapperClass); } public static <S, T, M extends SourceTargetMapper<S, T>> T toTarget(final Class<M> mapperClass, final S source) { return getMapper(mapperClass).toTarget(source); } public static <S, T, M extends SourceTargetMapper<S, T>> void toTarget(final Class<M> mapperClass, final S source, final T target) { getMapper(mapperClass).toTarget(source, target); } public static <S, T, M extends SourceTargetMapper<S, T>> List<T> toTargetList(final Class<M> mapperClass, final List<S> sourceList) { return getMapper(mapperClass).toTargetList(sourceList); } public static <S, T, M extends SourceTargetMapper<S, T>> S toSource(final Class<M> mapperClass, final T target) { return getMapper(mapperClass).toSource(target); } public static <S, T, M extends SourceTargetMapper<S, T>> void toSource(final Class<M> mapperClass, final T target, final S source) { getMapper(mapperClass).toSource(target, source); } public static <S, T, M extends SourceTargetMapper<S, T>> List<S> toSourceList(final Class<M> mapperClass, final List<T> targetList) { return getMapper(mapperClass).toSourceList(targetList); } public static <T, M extends BeanCopyMapper<T>> T copy(final Class<M> mapperClass, final T bean) { return getMapper(mapperClass).copy(bean); } public static <T, M extends BeanCopyMapper<T>> void copy(final Class<M> mapperClass, final T bean, final T targetBean) { getMapper(mapperClass).copy(bean, targetBean); } public static <T, M extends BeanCopyMapper<T>> List<T> copyList(final Class<M> mapperClass, final List<T> beans) { return getMapper(mapperClass).copyList(beans); } } ``` 这样,我们就可以通过`BeanConvertUtil`工具类,实现类似于`BeanUtil`的效果,只要使用者自行编写的接口继承了我们的通用`Mapper`都可以获得通用`Mapper`的能力。如: ```java public interface UserConverter extends BeanCopyMapper<User> { interface UserToBoConverter extends SourceTargetMapper<User, UserBo> {} interface UserToDTOConverter extends SourceTargetMapper<User, UserDTO> {} } UserDTO userDto = BeanConvertUtil.toTarget(UserConverter.UserToDTOConverter.class,user); ``` 除此之外,我们还可以添加一些增强性的方法,比如在`List`映射的时候,对每一项元素进行转换后的处理、提供`Function`函数,让使用者更灵活的去调用,也方便在`Stream.map`方法中进行处理。如: ```java default List<T> copyList(List<T> beans, Consumer<T> eachAfter) { if (eachAfter == null) { return copyList(beans); } return beans.stream() .map(bean -> copy(bean, eachAfter)) .collect(Collectors.toList()); } default Function<List<T>, List<T>> copyListFunction(Consumer<T> eachAfter) { return beans -> copyList(beans, eachAfter); } ``` 甚至于我们还可以用`Velocity`或者`FreeMarker`这类模版框架,自己实现一个简单的`Mapper`生成器,去帮我们生成一些业务上的`Mapper`接口,比如我用`Velocity`模版框架实现了一个简单的生成器: ```java package ${packageName}.convert; import ${packageName}.domain.${ClassName}; import ${packageName}.domain.bo.${ClassName}Bo; import ${packageName}.domain.vo.${ClassName}Vo; import io.gitee.colordreams.mapstruct.convert.mapper.BeanCopyMapper; import io.gitee.colordreams.mapstruct.convert.mapper.SourceTargetMapper; import org.mapstruct.Mapper; import org.mapstruct.MappingConstants; import org.mapstruct.control.DeepClone; public interface ${ClassName}Converter extends BeanCopyMapper<${ClassName}> { interface ${ClassName}ToBoConverter extends SourceTargetMapper<${ClassName}, ${ClassName}Bo> { } interface ${ClassName}ToVoConverter extends SourceTargetMapper<${ClassName}, ${ClassName}Vo> { } } ``` 另外,如果`Mapper`被框架的`IOC`容器管理了,我们还可以在将`Mapper`注入到我们的业务代码中,以`Spring IOC`为例子: ```java public class UserService{ private UserConverter converter; UserDTO userDTO = converter.toTarget(user) } ``` 至此,我们完成了`MapStruct`的简单易用性封装,在不破坏`MapStruct`原有的任何功能下,提供了类似`BeanUtil`的使用体验,还提供了一些增强的转换方法。 ### MapStruct与BeanUtil对比 | | MapStruct | BeanUtil(或其它基于反射的对象映射框架) | | --- | --- | --- | | **性能对比** | **极高**,通过编译生成接近手写的映射代码,无反射开销 | **较低**,反射操作带来额外开销,尤其在大数据量或高频调用时性能明显下降。(可以通过`LambdaMetafactory`+`缓存Class/Field getter/setter`进行优化,在少数据量场景除了第一次调用,可以获得接近手写代码的性能,但在大数据量或高频调用时仍不如`MapStruct`) | | **类型安全** | **强类型检查**:字段名或类型不匹配时,**编译时报错**,避免运行时错误。 | **无编译时检查**:字段名或类型不匹配时,**静默失败**,跳过字段或抛出异常。(一些`BeanUtil`,如`Hutool BeanUtil`会自动进行基本类型和常用类型的转换,**易造成黑盒**) | | **易用性** | **需要一些的学习成本**,取决于需要映射实体的复杂性,一般而言只需要了解基本的使用即可。 | **简单易用**,基本无学习成本。 | | **代码入侵性** | **显式映射规则**,需要定义映射接口或抽象类。 | **无需侵入代码**,直接调用工具类方法。 | | **复杂映射支持** | **支持深克隆**,支持嵌套对象、字段重命名、自定义转换器(通过 `@AfterMapping` 等注解)。 | 大部分的`BeanUtil`仅支持简单字段映射(`Hutool BeanUtil`支持深克隆),复杂逻辑需手动处理。 | | **维护性与扩展性** | **编译前黑盒,编译后生成代码接近手写,可读性强**,可以自定义转换逻辑(如枚举转换、集合映射),**可扩展性和可维护性仅次于手写代码**。 | **运行时反射,全程黑盒**,错误隐蔽,**维护成本高**(如字段名变更后静默失败)。 | | | MapStruct | BeanUtil(或其它基于反射的对象映射框架) | | --- | --- | --- | | **Native原生编译** | **支持** | **不支持** | | **高性能需求** | **首选**,性能与手写代码相差无几,适合高并发、大数据量的场景(如 Web 请求处理、微服务接口)。 | **慎用**,反射性能较低,不适合高频调用或大数据量处理。 | | **简单映射** | **推荐**,需要一定的配置和接口定义,略微繁琐。 | **首选**,适合快速开发,字段名一致时无需额外配置。 | | **复杂映射** | **首选**,支持嵌套对象、字段别名、自定义转换等复杂逻辑。 | **慎用**,需手动处理复杂逻辑,代码冗余且易出错。 | | **类型安全要求高** | **首选**,编译时检查确保类型匹配。 | **慎用**,运行时反射错误风险高,难以保证类型安全。一些`BeanUtil`(如`Hutool`)会自动进行基本类型和常用类型的转换,**易造成黑盒**。 | | **基于项目使用建议** | 中大型、多人协作项目**首选**,小型项目**推荐**。 | 需要快速迭代的小型项目**首选**,中大型、多人协作项目**不建议使用**) |
admin
2025年10月5日 10:20
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码