MapStruct在项目中封装使用

Posted DH镔

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MapStruct在项目中封装使用相关的知识,希望对你有一定的参考价值。

MapStruct是一个对象属性复制工具,一般作用于不同的分层模型的对象属性复制。

从网上copy了下别人测试的性能对比

pc配置:i7,16G内存
各种Bean拷贝工具比较

工具十个对象复制1次一万个对象复制1次一百万个对象复制1次一百万个对象复制5次
mapStruct0ms3ms96ms281ms
hutools的BeanUtil23ms102ms1734ms8316ms
spring的BeanUtils2ms47ms726ms3676ms
apache的BeanUtils20ms156ms10658ms52355ms
apache的PropertyUtils5ms68ms6767ms30694ms
来源:MapStruct使用及性能测试,秒杀BeanUtil

基础使用

依赖配置

pom.xml配置以下内容,例子中使用了lombok,所以我把lombok的配置也加上了

<project>
    <properties>
        <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
        <lombok.version>1.18.20</lombok.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency> 
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>
    </dependencies>
    
    <build>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>${lombok.version}</version>
                        </path>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
</project>

官方例子

@Data
public class Car {
 
    private String make;
    private int numberOfSeats;
    private CarType type;

}

@Data
public class CarDto {
 
    private String make;
    private int seatCount;
    private String type;
 
}

@Mapper
public interface CarMapper {
 
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
 
    // 字段名不同时,可以使用@Mapping配置关系
    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto carToCarDto(Car car);
}

@Test
public void shouldMapCarToDto() {
    //given
    Car car = new Car("Morris", 5, CarType.SEDAN);
 
    //when
    CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
 
    //then
    assertThat(carDto).isNotNull();
    assertThat(carDto.getMake()).isEqualTo( "Morris");
    assertThat(carDto.getSeatCount()).isEqualTo(5);
    assertThat(carDto.getType()).isEqualTo("SEDAN");
}

封装

从上面的例子中,每次使用都需要调用一次Mapper.INSTANCE才能获取到Mapper,这样Mapper就会和业务代码耦合在一起,不利于以后替换其他工具。我们可以把对象属性复制的功能抽象成一个接口Convert,所有Bean都是Convert的子类,这样每个Bean都有对象转换的能力。

public interface Convert extends Serializable {
    /**
     * 获取自动转换后的JavaBean对象
     *
     * @param clazz class类型
     * @param <T>   类型
     * @return 对象
     */
    @SuppressWarnings({"unchecked", "rawtypes"})
    default <T> T convert(Class<T> clazz) {
        BeanConvertMapper mapper = BeanConvertMappers.getMapper(this.getClass(), clazz);
        return (T) mapper.to(this);
    }
}

BeanConvertMapper定义了一个对象转换的接口

public interface BeanConvertMapper<SOURCE, TARGET> {

    /**
     * source to target
     *
     * @param source source
     * @return target
     */
    TARGET to(SOURCE source);

}

BeanConvertMappers是一个工具类,提供通过源对象Class目标对象Class获取Mapper方法。

@SuppressWarnings({"rawtypes", "unchecked"})
public class BeanConvertMappers {

    public static <S, T> BeanConvertMapper<S, T> getMapper(Class<S> sourceClass, Class<T> targetClass) {
        String key = MapperDefinition.generateKey(sourceClass, targetClass);
        Class mapperClass = MapperDefinition.getMappers().get(key);
        if (mapperClass == null) {
            throw new IllegalArgumentException(StrUtil.format("找不到{}转{}的Mapper", sourceClass.getName(), targetClass.getName()));
        }
        return (BeanConvertMapper<S, T>) Mappers.getMapper(mapperClass);
    }

}

MapperDefinition维护所有Mapper,新增Mapper只需要注册到map即可。

@SuppressWarnings("rawtypes")
public class MapperDefinition {

    private static Map<String, Class> MAPPERS = new HashMap<>(16);

    static {
        registerMapper(CarDto.class, Car.class, CarDtoToCarMapper.class);
        // 新增的Mapper在这注册
        MAPPERS = MapUtil.unmodifiable(MAPPERS);
    }



    /* Mapper定义 */

    @Mapper
    public interface CarDtoToCarMapper extends BeanConvertMapper<LabelingReq, LabelingBO> {
    }
    
    /* Mapper定义 */


    public static Map<String, Class> getMappers() {
        return MAPPERS;
    }


    public static <S, T> String generateKey(Class<S> sourceClass, Class<T> targetClass) {
        return sourceClass.getName() + targetClass.getName();
    }

    private static <S, T> void registerMapper(Class<S> sourceClass, Class<T> targetClass, Class<? extends BeanConvertMapper<S, T>> mapperClass) {
        MAPPERS.put(generateKey(sourceClass, targetClass), mapperClass);
    }
}

进一步优化

上面的封装解决了Mapper耦合的问题,但是在定义Mapper的时候,还是存在大量的模板接口,是否有更好的方式解决呢?

我想到的方案是:

和mapstruct原理一样,在mapstruct的注解处理器之前,通过注解来生成BeanConvertMapper接口,注解大致如下,同时自动注入到map中。新增一个Mapper只需要定义一个注解即可。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface MapperDefinition {

    /**
     * 源对象的Class
     * 
     * @return Class
     */
    Class<?> source();

    /**
     * 目标对象的Class
     * 
     * @return Class
     */
    Class<?> target();
}

你有更好的方案吗,一起分享下

以上是关于MapStruct在项目中封装使用的主要内容,如果未能解决你的问题,请参考以下文章

属性映射工具——MapStruct

回归 | js实用代码片段的封装与总结(持续更新中...)

Java对象转换与mapstruct实践

VsCode 代码片段-提升研发效率

在 Eclipse 项目中使用 MapStruct 将“脏”字符串字段映射为双精度

MapStruct类型之间映射的实现