序言·关于MapStruct
Tips:本文旨在探讨反思作者在使用
MapStruct
及其有关扩展,进行一些易用性封装所遇到的问题和看法,MapStruct
的使用教学不是文章的主旨,观看本文章可能需要一定的MapStruct
使用经验。另外,MapStruct
与BeanUtil
的比较不是文章的主题,但会放到文章末尾。文章中的一些观点仅限于我个人在使用MapStruct
的一些个人看法见解,不一定对也不一定适合所有业务。文章中关键的核心代码已经打包到我的Gitee个人仓库,有兴趣的可以看一下,如果方便的话,也请提供一些改进建议:gitee.com/ColorDreams...
MapStruct
是一个用于数据对象转换场景(如实体类Entity
与数据传输对象类DTO
之间的映射等)的对象映射框架。其原理是基于JSR-269
规范编译时生成接近手写、无反射开销的类型安全赋值映射代码(直接调用 getter/setter
),对比BeanUtil
和其它基于反射的对象映射框架, 具有更好的性能和健壮性,且在Native
编译时也仍然适用,对于Quarkus
、Spring Native
等Java Native
框架支持良好。
MapStruct Plus 使用反思:MapStruct Mapper的创建和调用应该是明确的
MapStruct Plus是我迄今为止唯一一个接触过的基于MapStruct增强类库(如果lombok-mapstruct-binding
不算的话),它的实现思想应该是来源于京东云技术团队的一篇技术文章:基于AbstractProcessor扩展MapStruct自动生成实体映射工具类。
MapStruct Plus
的具体使用方式不是文章的主旨,这里就不再过多赘述,有兴趣的可以去了解一下。
简单来说就是:它做了一系列的封装,提供了一个Converter
,使用Converter
可以让你跟调用BeanUtil
一样方便。而你需要做的是,在转换的类上面添加特定的注解,即可帮你自动生成MapStruct Mapper
接口,再由MapStruct
的注解处理器去生成对应的Mapper
实现(具体原理可以看上面京东云技术团队的那篇文章)。 如:
java
// User
public class User{
private Long userId;
// 其它字段......
}
// UserDTO
@AutoMapper(target = User.class)
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 = ... // 初始化 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, @MappingTarget 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
// UserBo
public class UserBo{
private Long userId;
// 其它字段......
}
// 使用
import io.github.linpeilie.Converter;
private Converter converter = ... // 初始化 Converter 伪代码...
User user = converter.convert(userBo,User.class);
// 编译不报错,运行时报 ConvertException ,提示找不到转换器
细心的你已经发现了,这种做法可能会导致出现一些不可控的黑盒代码 ,MapStruct
编译时就能排查出问题的优势一下子就没有了,并且代码的入侵性并没有减少,反而更强了(MapStruct Mapper
接口不需要修改转换的数据类,只关注接口本身) 。而在需要添加自定义转换逻辑时,你如果使用MapStruct Plus
,仍需要在实体类上堆屎山,例如:
java
// 以下代码来自 mapstruct plus官方文档 https://www.mapstruct.plus/guide/class-convert.html
// 定义一个类型转换器 ------ `StringToListString`
@Component
public class StringToListString {
public List<String> stringToListString(String str) {
return StrUtil.split(str);
}
}
// 使用该类型转换器
@AutoMapper(target = User.class, uses = StringToListStringConverter.class)
public class UserDTO {
private Long userId;
@AutoMapping(target = "nameList")
private String name;
// ......
}
可以发现,自定义的转换逻辑并没有减少,只是从Mapper
接口转移到了UserDTO
上面,造成了严重的强入侵和强依赖。
当然,你也可以自己去创建一个Mapper
接口去做自定义转换逻辑,问题是,既然我都要自己创建Mapper
接口了,我使用MapStruct Plus
的意义是什么呢?它给我带来了什么便利?
基于以上,我个人认为: MapStruct Mapper的创建和调用,应该是明确的,不应该走捷径,这一块没有捷径可言。
MapStruct简单易用性封装:可以像BeanUtil但不应该止于BeanUtil
那么,MapStruct Plus
就完全没有可取之处吗?有的兄弟,有的。我们虽然强调Mapper的创建和调用,应该是明确的 ,但是没说不能提供通用的Mapper啊!
数据对象映射的场景无非就两个:
- 数据类自身的copy/clone (即不发生类型转换,但自身数据需要拷贝一份用于业务。应用场景如:主租户数据同步到子租户等)
- 两个数据类之间的互转 (从一个数据类转为另一个数据类,例如上面的例子,从
User
转为UserDTO
,或UserDTO
转User
)
基于以上两点,我们可以创建3个接口(为什么是3个别问,先创建)
点击查看代码
java
// 最顶层的Mapper接口,主要方便用于 Mapper Factory 对Mapper进行管理,本身不提供抽象方法
public interface MapperAware {}
/**
* BeanCopyMapper Bean拷贝接口,提供Bean拷贝能力
*
* @param <T> Bean类型
*/
public interface BeanCopyMapper<T> extends MapperAware {
/**
* 拷贝对象
*
* @param bean 对象
* @return 拷贝对象
*/
T copy(T bean);
/**
* 拷贝对象
*
* @param bean 对象
* @param targetBean 拷贝后的目标对象
*/
void copy(T bean, @MappingTarget T targetBean);
/**
* 批量拷贝对象
*
* @param beans 对象列表
* @return 拷贝对象列表
*/
default List<T> copyList(List<T> beans) {
return beans.stream()
.map(this::copy)
.collect(Collectors.toList());
}
}
/**
* SourceTargetMapper接口 提供源(S)对象和目标(T)对象之间的转换功能
*
* @param <S> 源类型 S
* @param <T> 目标类型 T
*/
public interface SourceTargetMapper<S, T> extends MapperAware {
/**
* 将源对象转换为目标对象
*
* @param source 源对象
* @return 目标对象
*/
T toTarget(S source);
/**
* 将源对象转换为目标对象
*
* @param source 源对象
* @param target 目标对象
*/
void toTarget(S source,@MappingTarget T target);
/**
* 将源对象列表转换为目标对象列表
*
* @param sourceList 源对象列表
* @return 目标对象列表
*/
default List<T> toTargetList(List<S> sourceList) {
return sourceList.stream()
.map(this::toTarget)
.collect(Collectors.toList());
}
/**
* 将目标对象转换成源对象
*
* @param target 目标对象
* @return 源对象
*/
S toSource(T target);
/**
* 将目标对象转换成源对象
*
* @param target 目标对象
* @param source 源对象
*/
void toSource(T target,@MappingTarget S source);
/**
* 将目标对象列表转换为源对象列表
*
* @param targeteList 目标对象列表
* @return 源对象列表
*/
default List<S> toSourceList(List<T> targeteList) {
return sourceList.stream()
.map(this::toSource)
.collect(Collectors.toList());
}
}
为了方便对这些通用的Mapper
实例进行管理,我们还可以创建一个 MapperFactory
接口,并提供默认的实现:
点击查看代码
java
/**
* Mapper工厂接口
*/
public interface MapperFactory {
/**
* 注册Mapper
*
* @param mapper Mapper
*/
<M extends MapperAware> void registerMapper(M mapper);
/**
* 注册Mapper
*
* @param mapperName Mapper名称
* @param mapper Mapper
*/
<M extends MapperAware> void registerMapper(String mapperName,M mapper);
/**
* 获取Mapper
*
* @param mapperClass Mapper Class
* @return Mapper
*/
<M extends MapperAware> M getMapper(Class<M> mapperClass);
/**
* 获取Mapper
*
* @param mapperName Mapper名称
* @return Mapper
*/
<M extends MapperAware> M getMapper(String mapperName);
}
/**
* DefaultMapperFactory 默认Mapper工厂
*/
public class DefaultMapperFactory implements MapperFactory {
private static final Map<String, MapperAware> MAPPER_MAPS = new ConcurrentHashMap<>();
@Override
public <M extends MapperAware> void registerMapper(M mapper) {
registerMapper(mapper.getClass().getCanonicalName(), mapper);
}
@Override
public <M extends MapperAware> void registerMapper(String mapperName, M mapper) {
MAPPER_MAPS.put(mapperName, mapper);
}
@SuppressWarnings({"unchecked"})
@Override
public <M extends MapperAware> M getMapper(final Class<M> mapperClass) {
return (M) MAPPER_MAPS.computeIfAbsent(mapperClass.getCanonicalName(), mapperName -> Mappers.getMapper(mapperClass));
}
@SuppressWarnings({"unchecked"})
@Override
public <M extends MapperAware> M getMapper(String mapperName) {
return (M) MAPPER_MAPS.get(mapperName);
}
}
通常来说,我们在实际开发中都是通过框架的IOC容器去管理Mapper
,比如Spring
,所以我们可以提供一个基于Spring IOC
容器管理的MapperFactory
:
点击查看代码
java
/**
* SpringBeanMapperFactory SpringIOC容器管理的Mapper工厂
*/
public class SpringBeanMapperFactory implements MapperFactory, ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
@Override
public <M extends MapperAware> void registerMapper(M mapper) {
// 让 Spring IOC 容器管理Mapper注册
}
@Override
public <M extends MapperAware> void registerMapper(String mapperName, M mapper) {
// 让 Spring IOC 容器管理Mapper注册
}
@Override
public <M extends MapperAware> M getMapper(Class<M> mapperClass) {
return applicationContext.getBean(mapperClass);
}
@SuppressWarnings({"unchecked"})
@Override
public <M extends MapperAware> M getMapper(String mapperName) {
return (M) applicationContext.getBean(mapperName);
}
}
这样一来,我们就具备了三个通用的Mapper
接口,以及MapperFactory
及其实现。但是要想实现类似BeanUtil
那种效果,我们还需要一个类似的工具类,如:
点击查看代码
java
/**
* Bean转换工具类
*/
@Slf4j
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BeanConvertUtil {
/**
* 默认MapperFactory
*/
public static final MapperFactory DEFAULT_MAPPER_FACTORY = new DefaultMapperFactory();
/**
* MapperFactory
*/
private static MapperFactory MAPPER_FACTORY = DEFAULT_MAPPER_FACTORY;
/**
* 设置MapperFactory
*
* @param mapperFactory MapperFactory
*/
public static void setMapperFactory(final MapperFactory mapperFactory) {
MAPPER_FACTORY = mapperFactory;
}
/**
* 获取Mapper
*
* @param mapperClass Mapper字节码实例
* @return Mapper
*/
public static <M extends MapperAware> M getMapper(final Class<M> mapperClass) {
return MAPPER_FACTORY.getMapper(mapperClass);
}
/**
* 将源对象转换为目标对象
*
* @param mapperClass Mapper字节码实例
* @param source 源对象
* @return 目标对象
*/
public static <S, T, M extends SourceTargetMapper<S, T>> T toTarget(final Class<M> mapperClass, final S source) {
return getMapper(mapperClass).toTarget(source);
}
/**
* 将源对象转换为目标对象
*
* @param mapperClass Mapper字节码实例
* @param source 源对象
* @param target 目标对象
*/
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);
}
/**
* 将源对象列表转换为目标对象列表
*
* @param mapperClass Mapper字节码实例
* @param sourceList 源对象列表
* @return 目标对象列表
*/
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);
}
/**
* 将目标对象转换成源对象
*
* @param mapperClass Mapper字节码实例
* @param target 目标对象
* @return 源对象
*/
public static <S, T, M extends SourceTargetMapper<S, T>> S toSource(final Class<M> mapperClass, final T target) {
return getMapper(mapperClass).toSource(target);
}
/**
* 将目标对象转换成源对象
*
* @param mapperClass Mapper字节码实例
* @param target 目标对象
* @param source 源对象
*/
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);
}
/**
* 将目标对象列表转换成源对象列表
*
* @param mapperClass Mapper字节码实例
* @param targetList 目标对象列表
* @return 源对象列表
*/
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);
}
/**
* 拷贝对象
*
* @param mapperClass Mapper字节码实例
* @param bean 对象
* @return 拷贝对象
*/
public static <T, M extends BeanCopyMapper<T>> T copy(final Class<M> mapperClass, final T bean) {
return getMapper(mapperClass).copy(bean);
}
/**
* 拷贝对象
*
* @param mapperClass Mapper字节码实例
* @param bean 对象
* @param targetBean 拷贝后的目标对象
*/
public static <T, M extends BeanCopyMapper<T>> void copy(final Class<M> mapperClass, final T bean, final T targetBean) {
getMapper(mapperClass).copy(bean, targetBean);
}
/**
* 批量拷贝对象
*
* @param mapperClass Mapper字节码实例
* @param beans 对象列表
* @return 拷贝对象列表
*/
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
/**
* 用户对象转换映射Mapper
* componentModel = MappingConstants.ComponentModel.SPRING 表示使用Spring IOC容器管理Mapper
* mappingControl = DeepClone.class 表示开启深克隆
*/
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, mappingControl = DeepClone.class)
public interface UserConverter extends BeanCopyMapper<User> {
/**
* 用户对象BO转换映射Mapper
*/
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, mappingControl = DeepClone.classs)
interface UserToBoConverter extends SourceTargetMapper<User, UserBo> {}
/**
* 用户对象DTO转换映射Mapper
*/
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, mappingControl = DeepClone.classs)
interface UserToDTOConverter extends SourceTargetMapper<User, UserDTO> {}
}
// 使用
UserDTO userDto = BeanConvertUtil.toTarget(UserConverter.UserToDTOConverter.class,user);
除此之外,我们还可以添加一些增强性的方法,比如在List
映射的时候,对每一项元素进行转换后的处理、提供Function
函数,让使用者更灵活的去调用,也方便在Stream.map
方法中进行处理。如:
java
/**
* 批量拷贝对象
*
* @param beans 对象列表
* @param eachAfter 每一个元素拷贝后操作
* @return 拷贝对象列表
*/
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());
}
/**
* 批量拷贝对象
*
* @param eachAfter 每一个元素拷贝后操作
* @return 拷贝对象函数
*/
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;
/**
* ${ClassName}转换器接口
*
* @author ${author}
* @date ${datetime}
*/
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, mappingControl = DeepClone.class)
public interface ${ClassName}Converter extends BeanCopyMapper<${ClassName}> {
/**
* ${ClassName}转${ClassName}Bo对象转换器接口
*
* @author ${author}
* @date ${datetime}
*/
@Mapper(config = DeepCloneMapperConfig.class)
interface ${ClassName}ToBoConverter extends SourceTargetMapper<${ClassName}, ${ClassName}Bo> {
}
/**
* ${ClassName}转${ClassName}Bo对象转换器接口
*
* @author ${author}
* @date ${datetime}
*/
@Mapper(config = DeepCloneMapperConfig.class)
interface ${ClassName}ToVoConverter extends SourceTargetMapper<${ClassName}, ${ClassName}Vo> {
}
}
另外,如果Mapper
被框架的IOC
容器管理了,我们还可以在将Mapper
注入到我们的业务代码中,以Spring IOC
为例子:
java
public class UserService{
@Autowired
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 )会自动进行基本类型和常用类型的转换,易造成黑盒。 |
基于项目使用建议 | 中大型、多人协作项目首选 ,小型项目推荐。 | 需要快速迭代的小型项目首选 ,中大型、多人协作项目不建议使用) |