MapstructPlus类转换利器

2,378 阅读7分钟

MapstructPlus简介

MapstructPlus是Mapstruct的增强工具。而Mapstruct是一个简化Java Bean之间映射的框架。在我们日常代码过程中,经常会遇到实体类到VO的转换,或者请求类到实体类的映射转换操作。在long long ago的时候,我们经常使用BeanUtils的copyProperties或者用FastJson的JSON.parseObject来映射。那这两种方式有什么问题呢?

  • 字段名不一样无法映射
  • 字段类型不同也往往映射不成功
  • 性能比get,set方式差很多。

正式由于上面的不便,Mapstruct应运而生。

Mapstruct Github地址:github.com/mapstruct/m…

Mapstruct官网:mapstruct.org/

MapstructPlus官网地址:mapstruct.plus/introductio…

特别提醒

MapstructPlus的根基是Mapstruct。如果想进一步的研究可能还需要对Mapstruct有更深入的了解。

不过本文仅仅在使用层面上做介绍,并且结合实际的使用场景做了总结。

Mapstruct

这里我们先试用一下Mapstruct,从而对比下MapstructPlus增强在什么地方。为了更贴合实际开发的场景,这里会把maven-compiler-plugin配置项也一同贴出。

1. 添加依赖

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <!-- 目前最新的版本 -->
    <version>1.6.3</version>
</dependency>
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.13.0</version>
            <configuration>
                <source>8</source>
                <target>8</target>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>1.18.30</version>
                    </path>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>1.6.3</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

这里解释一下maven-compiler-plugin中的annotationProcessorPaths部分的配置内容。这个主要是用来指定注解处理器的路径。它的主要作用是告诉编译器在编译过程中使用哪些注解处理器来处理项目中的注解。

处理器作用
lombok编译时把lombok的@Data这种生成class时,加上getter、setter方法
mapstruct-processor编译时能根据@Mapper注解,生成一个实现该接口的类
lombok-mapstruct-binding协调lombok和mapstruct注解。保证lombok注解处理先于Mapstruct

2. 准备的测试Bean

// 源
@Data
public class SysUserEntity {
    private Long id;
    private String username;
}
// 目标
@Data
public class SysUserVo {
    private String userId;
    private String userName;
}

3. 定义映射接口

就是将@Mapper注解到一个接口上,在接口中定义映射方法,而这些方法会自动生成实现。

@Mapper
public interface SysUserMapper {
    @Mapping(source = "id", target = "userId", numberFormat = "#0")
    @Mapping(source = "username", target = "userName")
    SysUserVo entityToVo(SysUserEntity entity);
}

稍作解释:

由于SysUserEntity类中的id是Long类型,而SysUserVo类中的userId是字符型的,这里不但字段名对应不上而且字段类型也对不上。所以这里用source、taget标明了源和目标类的字段名称;并且还用numberFormat把源类中的Long类型给转换为字符串类型。

4. 测试类

public class MapstructTest {
    public static void main(String[] args) {
        SysUserEntity entity = new SysUserEntity();
        entity.setId(1L);
        entity.setUsername("admin");
        SysUserMapper sysUserMapper = Mappers.getMapper(SysUserMapper.class);
        SysUserVo sysUserVo = sysUserMapper.entityToVo(entity);
        System.out.println(JSONUtil.toJsonPrettyStr(sysUserVo));
    }
}

输出结果:

image-20241122145524043

5. 不方便的地方

通过上面的例子可以看到,要自定义一个映射器接口。这个就增加了额外的代码量,并且对于简单映射场景来说,这个映射器接口的定义多少有些过于繁重了。

Mapstruct Plus

1. Mapstruct Plus增强

正是由于上面提到的Mapstruct不方便的地方,Mapstruct Plus应运而生。不用你再定义映射器接口了。只要在源或者目的类上直接通过注解即可实现他们之间的转换工作。Mapstruct Plus增强的点:

  • 自动生成Mapper接口

    开发者在类上添加@AutoMapper注解,即可自动生成转换关系。不用再写映射器了,并且支持一个源到多个目标类。

    @Data
    @AutoMappers({
        @AutoMapper(target = UserDto.class),
        @AutoMapper(target = UserVO.class)
    })
    public class User {
    ​
        private String username;
        // ....
    }
    
  • 支持复杂类型的转换。如:嵌套对象或者集合的映射。

  • 框架提供了 Converter 类,来执行自定义的转换逻辑

2. 用Mapstruct Plus实现上面例子

  • 添加依赖

    <dependencies>
        <dependency>
            <groupId>io.github.linpeilie</groupId>
            <artifactId>mapstruct-plus-spring-boot-starter</artifactId>
            <version>1.4.6</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.13.0</version>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                            <version>1.18.30</version>
                        </path>
                        <dependency>
                            <groupId>io.github.linpeilie</groupId>
                            <artifactId>mapstruct-plus-processor</artifactId>
                            <version>1.4.6</version>
                        </dependency>
                        <path>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok-mapstruct-binding</artifactId>
                            <version>0.2.0</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
  • 准备的测试Bean

    // 源
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @AutoMappers(
            @AutoMapper(target = SysUserVo.class)
    )
    public class SysUserEntity {
        @AutoMapping(target = "userId", numberFormat = "#0")
        private Long id;
        @AutoMapping(target = "userName")
        private String username;
    }
    // 目标
    @Data
    public class SysUserVo {
        private String userId;
        private String userName;
    }
    
  • 测试类

    @SpringBootTest
    public class MapStructPlusTest {
        @Test
        public void test() {
            SysUserEntity source = SysUserEntity.builder().id(2L).username("laoma").build();
            // Mapstruct plus 的 Converter
            Converter converter = SpringUtil.getBean(Converter.class);
            SysUserVo target = converter.convert(source, SysUserVo.class);
            System.out.println(JSONUtil.toJsonPrettyStr(target));
        }
    }
    

    输出结果:

    image-20241122145524043

3.使用注意

  • 依赖的不同

    这里依赖的是mapstruct-plus-spring-boot-starter,而直接使用Mapstruct依赖的是mapstruct

  • 注解处理器不同

    这里是mapstruct-plus-processor,而直接使用Mapstruct时,使用的是mapstruct-processor

  • 用最新的Mapstruct Plus它底层依赖的Mapstruct版本是1.5.5.Final,而不是最新的1.6.3版本

4. 底层原理

原理很简单,就是根据你的注解,Mapstruct Plus帮你生成了对应的转换类。你的注解:

@AutoMappers(
        @AutoMapper(target = SysUserVo.class)
)
public class SysUserEntity {
    //...省略代码
}

你这个注解告诉Mapstruct Plus我要做SysUserEntity和SysUserVo两个类的互转操作。结果在你不知不觉中,它帮在target/classes中生成了对应转换接口类和实现类。如下图:

image-20241125101739713

而生成的实现类convert其实就是帮你new出来一个目标对象,然后调用目标对象的set方法,进行设置值。生成的代码如下:

@Component
public class SysUserEntityToSysUserVoMapperImpl implements SysUserEntityToSysUserVoMapper {
    public SysUserEntityToSysUserVoMapperImpl() {
    }
​
    public SysUserVo convert(SysUserEntity source) {
        if (source == null) {
            return null;
        } else {
            SysUserVo sysUserVo = new SysUserVo();
            sysUserVo.setUserName(source.getUsername());
            if (source.getId() != null) {
                sysUserVo.setUserId((new DecimalFormat("#0")).format(source.getId()));
            }
​
            return sysUserVo;
        }
    }
​
    public SysUserVo convert(SysUserEntity source, SysUserVo target) {
        if (source == null) {
            return target;
        } else {
            target.setUserName(source.getUsername());
            if (source.getId() != null) {
                target.setUserId((new DecimalFormat("#0")).format(source.getId()));
            } else {
                target.setUserId((String)null);
            }
​
            return target;
        }
    }
}

但是有时我们明确的知道,只有Entity类到VO类的一个方向上的转换,那么我们可以加上reverseConvertGenerate = false从而避免生成过多无用的转换接口和实现类。

@AutoMappers(
        @AutoMapper(target = SysUserVo.class, reverseConvertGenerate = false)
)
public class SysUserEntity {
    //...省略代码
}    

再次编译代码,在target中看到就只有entity到vo的接口类和实现类了。

image-20241125102216698

Mapstruct Plus更多的使用姿势

1. 将Map转换为对象

  • 添加依赖

    MapStructPlus 1.4.0 及以后版本,不再内置 Hutool框架。所以要使用该功能时,需要额外添加hutool的依赖

    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-core</artifactId>
        <version>${hutool.version}</version>
    </dependency>
    
  • 准备两个类

    这里试验两级对象映射。也就A类中套着B类。

    @Data
    @AutoMapMapper
    public class A {
        private String name;
        private Integer age;
        private B b;
    }
    ​
    @Data
    @AutoMapMapper
    public class B {
        private Date birthday;
    }
    

    注意:

    这里添加的是@AutoMapMapper而不是我们常用的@AutoMapper

  • 测试方法

    @Test
    public void testMap() {
        Map<String, Object> mapA = new HashMap<>();
        mapA.put("name", "小明");
        mapA.put("age", 18);
        Map<String, Object> mapB = new HashMap<>();
        mapB.put("birthday", DateUtil.parse("1972-11-03", "yyyy-MM-dd").toJdkDate());
        mapA.put("b", mapB);
        Converter converter = SpringUtil.getBean(Converter.class);
        A a = converter.convert(mapA, A.class);
        // 采用hutool,打印A对象,打印日期格式为yyyy-MM-dd
        System.out.println(JSONUtil.toJsonStr(a, JSONConfig.create().setDateFormat("yyyy-MM-dd")));
    }
    

    输出结果:

    {"name":"小明","age":18,"b":{"birthday":"1972-11-03"}}
    

2. 枚举类型转换

  • 枚举类

    @Getter
    @AllArgsConstructor
    @AutoEnumMapper("code")
    public enum CommonStatus {
    ​
        OK(0, "正常"),
        DISABLE(1, "禁用");
    ​
        private final Integer code;
        private final String desc;
    }
    

    注意:

    枚举类是上要有@AutoEnumMapper注解,当前枚举必须有一个可以保证唯一的字段,并在使用当前注解时,将该字段名,添加到注解提供的 value 属性中。

  • 源、目标类

    // 源类
    @Data
    @AutoMapper(target = ProductVo.class, reverseConvertGenerate = false)
    public class Product {
        private CommonStatus status;
    }
    ​
    // 目标类
    @Data
    public class ProductVo {
        private Integer status;
    }
    
  • 测试类

    @Test
    public void testEnum() {
        Product product = new Product();
        product.setStatus(CommonStatus.OK);
        Converter converter = SpringUtil.getBean(Converter.class);
        ProductVo productVo = converter.convert(product, ProductVo.class);
        System.out.println(JSONUtil.toJsonPrettyStr(productVo));
        product.setStatus(CommonStatus.DISABLE);
        System.out.println("=======================");
        productVo = converter.convert(product, ProductVo.class);
        System.out.println(JSONUtil.toJsonPrettyStr(productVo));
    }
    

    输出结果:

    image-20241125105251685

    补充说明:

    从Mapstruct Plus 1.4.2 版本开始,枚举类也可以跨模块使用了。当枚举与要使用的类型,不在同一个模块(module)中时,需要通过useEnums指定关系。这个我们在多模块项目可能会遇到类似情景。

3.自定义类型转换器

  • 自定义转换器

    @Component
    public class CourseConverter {
    ​
        @Named("courseToList")
        public List<String> courseToList(String str) {
            return Arrays.asList(str.split(","));
        }
    ​
        @Named("courseToString")
        public String courseToString(List<String> list) {
            return String.join(",", list);
        }
    ​
    }
    
  • 源、目标类

    @Data
    @AutoMapper(target = StudentVo.class, uses = CourseConverter.class)
    public class Student {
        private String name;
        @AutoMapping(qualifiedByName = "courseToList")
        private String courses;
    }
    ​
    @Data
    @AutoMapper(target = Student.class, uses = CourseConverter.class)
    public class StudentVo {
        private String name;
        @AutoMapping(qualifiedByName = "courseToString")
        private List<String> courses;
    }
    
  • 测试类

    @Test
    public void testCustomConverter() {
        Student student = new Student();
        student.setName("小明");
        student.setCourses("语文,数学,英语");
        Converter converter = SpringUtil.getBean(Converter.class);
        StudentVo studentVo = converter.convert(student, StudentVo.class);
        System.out.println(JSONUtil.toJsonPrettyStr(studentVo));
        System.out.println("=======================");
        studentVo.setCourses(Arrays.asList("语文", "数学", "英语", "物理", "化学"));
        Student student1 = converter.convert(studentVo, Student.class);
        System.out.println(JSONUtil.toJsonPrettyStr(student1));
    }
    

    输出结果:

    image-20241125105251685

4.表达式

有时候比较简单的调用java的基础方法,或者调用已经存在的现有方法类的时候,可以用表达式直接进行转换。

  • 现成的枚举类

    @Getter
    @AllArgsConstructor
    public enum SexEnum {
        MALE(1, "男"),
        FEMALE(2, "女"),
        UNKNOWN(3, "未知");
    ​
        private final Integer code;
        private final String desc;
    ​
        /**
         * 通过code获取desc
         */
        public static String getDescByCode(Integer code) {
            for (SexEnum item : SexEnum.values()) {
                if (item.getCode().equals(code)) {
                    return item.getDesc();
                }
            }
            return SexEnum.UNKNOWN.desc;
        }
        /**
         * 通过desc获取code
         */
        public static Integer getCodeByDesc(String desc) {
            for (SexEnum item : SexEnum.values()) {
                if (item.getDesc().equals(desc)) {
                    return item.getCode();
                }
            }
            return SexEnum.UNKNOWN.code;
        }
    }
    
  • 源、目标类

    @Data
    @AutoMapper(target = StudentVo.class, uses = CourseConverter.class, imports = SexEnum.class)
    public class Student {
        private String name;
        @AutoMapping(qualifiedByName = "courseToList")
        private String courses;
        @AutoMapping(expression = "java(SexEnum.getDescByCode(source.getSex()))")
        private Integer sex;
    }
    ​
    @Data
    @AutoMapper(target = Student.class, uses = CourseConverter.class, imports = SexEnum.class)
    public class StudentVo {
        private String name;
        @AutoMapping(qualifiedByName = "courseToString")
        private List<String> courses;
        @AutoMapping(expression = "java(SexEnum.getCodeByDesc(source.getSex()))")
        private String sex;
    }
    

    说明:

    imports = SexEnum.class表示导入某个类。这样可以缩短方法调用的书写长度。写了imports后:

    @AutoMapping(expression = "java(SexEnum.getDescByCode(source.getSex()))")这里就可以直接用SexEnum这个类的静态方法了。如果不写imports,这里就要写包的全路径。

  • 测试类

    @Test
    public void testExpression() {
        Student student = new Student();
        student.setName("小明");
        student.setCourses("语文,数学,英语");
        student.setSex(1);
        Converter converter = SpringUtil.getBean(Converter.class);
        StudentVo studentVo = converter.convert(student, StudentVo.class);
        System.out.println(JSONUtil.toJsonPrettyStr(studentVo));
        System.out.println("=======================");
        studentVo.setSex("女");
        Student student1 = converter.convert(studentVo, Student.class);
        System.out.println(JSONUtil.toJsonPrettyStr(student1));
    }
    

    输出结果:

    image-20241125144737565