MapStruct使用反思与简单易用性封装

序言·关于MapStruct

Tips:本文旨在探讨反思作者在使用MapStruct及其有关扩展,进行一些易用性封装所遇到的问题和看法,MapStruct的使用教学不是文章的主旨,观看本文章可能需要一定的MapStruct使用经验。另外,MapStructBeanUtil的比较不是文章的主题,但会放到文章末尾。文章中的一些观点仅限于我个人在使用MapStruct的一些个人看法见解,不一定对也不一定适合所有业务。

文章中关键的核心代码已经打包到我的Gitee个人仓库,有兴趣的可以看一下,如果方便的话,也请提供一些改进建议:gitee.com/ColorDreams...

MapStruct是一个用于数据对象转换场景(如实体类Entity与数据传输对象类DTO之间的映射等)的对象映射框架。其原理是基于JSR-269规范编译时生成接近手写、无反射开销的类型安全赋值映射代码(直接调用 getter/setter),对比BeanUtil和其它基于反射的对象映射框架, ‌具有更好的性能和健壮性,且在Native编译时也仍然适用,对于QuarkusSpring NativeJava 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并继承于它封装好的BaseMapperBaseMapper源码如下:

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啊!

数据对象映射的场景无非就两个:

  1. 数据类自身的copy/clone (即不发生类型转换,但自身数据需要拷贝一份用于业务。应用场景如:主租户数据同步到子租户等)
  2. 两个数据类之间的互转 (从一个数据类转为另一个数据类,例如上面的例子,从User转为UserDTO,或UserDTOUser

基于以上两点,我们可以创建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)会自动进行基本类型和常用类型的转换,易造成黑盒
基于项目使用建议 中大型、多人协作项目首选 ,小型项目推荐 需要快速迭代的小型项目首选 ,中大型、多人协作项目不建议使用
相关推荐
侠客行03176 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪6 小时前
深入浅出LangChain4J
java·langchain·llm
老毛肚8 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎9 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码9 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚9 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂9 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang9 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐9 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG9 小时前
JavaTuples 库分析
java