Java MapStruct
官网地址:https://mapstruct.org/
pom 配置:

  1. <properties>
  2. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  3. <maven.compiler.source>1.8</maven.compiler.source>
  4. <maven.compiler.target>1.8</maven.compiler.target>
  5. <org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
  6. <org.projectlombok.version>1.18.12</org.projectlombok.version>
  7. </properties>
  8. <dependencies>
  9. <dependency>
  10. <groupId>org.mapstruct</groupId>
  11. <artifactId>mapstruct</artifactId>
  12. <version>${org.mapstruct.version}</version>
  13. </dependency>
  14. <!-- lombok dependencies should not end up on classpath -->
  15. <dependency>
  16. <groupId>org.projectlombok</groupId>
  17. <artifactId>lombok</artifactId>
  18. <version>${org.projectlombok.version}</version>
  19. <scope>provided</scope>
  20. </dependency>
  21. <!-- idea 2018.1.1 之前的版本需要添加下面的配置,后期的版本就不需要了,可以注释掉,
  22. 这里用的2019.3 -->
  23. <dependency>
  24. <groupId>org.mapstruct</groupId>
  25. <artifactId>mapstruct-processor</artifactId>
  26. <version>${org.mapstruct.version}</version>
  27. <scope>provided</scope>
  28. </dependency>
  29. </dependencies>
  30. <build>
  31. <plugins>
  32. <plugin>
  33. <groupId>org.apache.maven.plugins</groupId>
  34. <artifactId>maven-compiler-plugin</artifactId>
  35. <version>3.8.1</version>
  36. <configuration>
  37. <source>1.8</source>
  38. <target>1.8</target>
  39. <annotationProcessorPaths>
  40. <path>
  41. <groupId>org.projectlombok</groupId>
  42. <artifactId>lombok</artifactId>
  43. <version>${org.projectlombok.version}</version>
  44. </path>
  45. <path>
  46. <groupId>org.mapstruct</groupId>
  47. <artifactId>mapstruct-processor</artifactId>
  48. <version>${org.mapstruct.version}</version>
  49. </path>
  50. </annotationProcessorPaths>
  51. </configuration>
  52. </plugin>
  53. </plugins>
  54. </build>

关于lombok和mapstruct的版本兼容问题,maven插件要使用3.6.0版本以上、lombok使用1.16.16版本以上,另外编译的lombok mapstruct的插件不要忘了。否则会出现下面的错误:No property named “aaa” exists in source parameter(s). Did you mean “null”?
这种异常就是lombok编译异常导致缺少get setter方法造成的。还有就是缺少构造函数也会抛异常。

  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class Student {
  6. private String name;
  7. private int age;
  8. private GenderEnum gender;
  9. private Double height;
  10. private Date birthday;
  11. }
  12. public enum GenderEnum {
  13. Male("1", "男"),
  14. Female("0", "女");
  15. private String code;
  16. private String name;
  17. public String getCode() {
  18. return this.code;
  19. }
  20. public String getName() {
  21. return this.name;
  22. }
  23. GenderEnum(String code, String name) {
  24. this.code = code;
  25. this.name = name;
  26. }
  27. }
  28. @Data
  29. @Builder
  30. @AllArgsConstructor
  31. @NoArgsConstructor
  32. public class StudentVO {
  33. private String name;
  34. private int age;
  35. private String gender;
  36. private Double height;
  37. private String birthday;
  38. }
  39. @Mapper
  40. public interface StudentMapper {
  41. StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
  42. @Mapping(source = "gender.name", target = "gender")
  43. @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
  44. StudentVO student2StudentVO(Student student);
  45. }

实体类是开发过程少不了的,就算是用工具生成肯定也是要有的,需要手写的部分就是这个Mapper的接口,编译完成后会自动生成相应的实现类
2021-09-18-12-19-39-501373.png
然后就可以直接用mapper进行实体的转换了

  1. public class Test {
  2. public static void main(String[] args) {
  3. Student student = Student.builder().name("小明").age(6).gender(GenderEnum.Male).height(121.1).birthday(new Date()).build();
  4. System.out.println(student);
  5. //这行代码便是实际要用的代码
  6. StudentVO studentVO = StudentMapper.INSTANCE.student2StudentVO(student);
  7. System.out.println(studentVO);
  8. }
  9. }

2021-09-18-12-19-44-402370.png
mapper可以进行字段映射,改变字段类型,指定格式化的方式,包括一些日期的默认处理。
可以手动指定格式化的方法:

  1. @Mapper
  2. public interface StudentMapper {
  3. StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
  4. @Mapping(source = "gender", target = "gender")
  5. @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
  6. StudentVO student2StudentVO(Student student);
  7. default String getGenderName(GenderEnum gender) {
  8. return gender.getName();
  9. }
  10. }

上面只是最简单的实体映射处理,下面介绍一些高级用法

1、List 转换

属性映射基于上面的mapping配置

  1. @Mapper
  2. public interface StudentMapper {
  3. StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
  4. @Mapping(source = "gender.name", target = "gender")
  5. @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
  6. StudentVO student2StudentVO(Student student);
  7. List<StudentVO> students2StudentVOs(List<Student> studentList);
  8. }
  9. public static void main(String[] args) {
  10. Student student = Student.builder().name("小明").age(6).gender(GenderEnum.Male).height(121.1).birthday(new Date()).build();
  11. List<Student> list = new ArrayList<>();
  12. list.add(student);
  13. List<StudentVO> result = StudentMapper.INSTANCE.students2StudentVOs(list);
  14. System.out.println(result);
  15. }

2021-09-18-12-19-49-492167.png

2、多对象转换到一个对象

  1. @Data
  2. @Builder
  3. @AllArgsConstructor
  4. @NoArgsConstructor
  5. public class Student {
  6. private String name;
  7. private int age;
  8. private GenderEnum gender;
  9. private Double height;
  10. private Date birthday;
  11. }
  12. @Data
  13. @AllArgsConstructor
  14. @Builder
  15. @NoArgsConstructor
  16. public class Course {
  17. private String courseName;
  18. private int sortNo;
  19. private long id;
  20. }
  21. @Data
  22. @Builder
  23. @AllArgsConstructor
  24. @NoArgsConstructor
  25. public class StudentVO {
  26. private String name;
  27. private int age;
  28. private String gender;
  29. private Double height;
  30. private String birthday;
  31. private String course;
  32. }
  33. @Mapper
  34. public interface StudentMapper {
  35. StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
  36. @Mapping(source = "student.gender.name", target = "gender")
  37. @Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
  38. @Mapping(source = "course.courseName", target = "course")
  39. StudentVO studentAndCourse2StudentVO(Student student, Course course);
  40. }
  41. public class Test {
  42. public static void main(String[] args) {
  43. Student student = Student.builder().name("小明").age(6).gender(GenderEnum.Male).height(121.1).birthday(new Date()).build();
  44. Course course = Course.builder().id(1L).courseName("语文").build();
  45. StudentVO studentVO = StudentMapper.INSTANCE.studentAndCourse2StudentVO(student, course);
  46. System.out.println(studentVO);
  47. }
  48. }

2021-09-18-12-20-59-717165.png

3、默认值

  1. @Mapper
  2. public interface StudentMapper {
  3. StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);
  4. @Mapping(source = "student.gender.name", target = "gender")
  5. @Mapping(source = "student.birthday", target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
  6. @Mapping(source = "course.courseName", target = "course")
  7. @Mapping(target = "name", source = "student.name", defaultValue = "张三")
  8. StudentVO studentAndCourse2StudentVO(Student student, Course course);
  9. }

2021-09-18-12-20-59-886146.png

4、转换基类

准备工作

使用常见的 User 类以及对应 UserDto 对象来演示。

  1. @Data
  2. @Accessors(chain = true)
  3. public class User {
  4. private Long id;
  5. private String username;
  6. private String password;
  7. private Integer sex;
  8. private LocalDate birthday;
  9. private LocalDateTime createTime;
  10. private String config;
  11. private String test;
  12. }
  13. @Data
  14. @Accessors(chain = true)
  15. public class UserVo {
  16. private Long id;
  17. private String username;
  18. private String password;
  19. private Integer gender;
  20. private LocalDate birthday;
  21. private String createTime;
  22. private List<UserConfig> config;
  23. private String test;
  24. @Data
  25. public static class UserConfig {
  26. private String field1;
  27. private Integer field2;
  28. }
  29. }

注意观察这两个类的区别。
因为项目中的对象转换操作基本都一样,因此抽取除了一个转换基类,不同对象如果只是简单转换可以直接继承该基类,而无需覆写基类任何方法,即只需要一个空类即可。如果子类覆写了基类的方法,则基类上的 @Mapping 会失效。

  1. @MapperConfig
  2. public interface BaseMapping<SOURCE, TARGET> {
  3. /**
  4. * 映射同名属性
  5. */
  6. @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
  7. TARGET sourceToTarget(SOURCE var1);
  8. /**
  9. * 反向,映射同名属性
  10. */
  11. @InheritInverseConfiguration(name = "sourceToTarget")
  12. SOURCE targetToSource(TARGET var1);
  13. /**
  14. * 映射同名属性,集合形式
  15. */
  16. @InheritConfiguration(name = "sourceToTarget")
  17. List<TARGET> sourceToTarget(List<SOURCE> var1);
  18. /**
  19. * 反向,映射同名属性,集合形式
  20. */
  21. @InheritConfiguration(name = "targetToSource")
  22. List<SOURCE> targetToSource(List<TARGET> var1);
  23. /**
  24. * 映射同名属性,集合流形式
  25. */
  26. List<TARGET> sourceToTarget(Stream<SOURCE> stream);
  27. /**
  28. * 反向,映射同名属性,集合流形式
  29. */
  30. List<SOURCE> targetToSource(Stream<TARGET> stream);
  31. }

实现 User 与 UserVo 对象的转换器

  1. import org.mapstruct.Mapper;
  2. import org.mapstruct.Mapping;
  3. @Mapper(componentModel = "spring")
  4. public interface UserMapping extends BaseMapping<User, UserVo> {
  5. @Mapping(target = "gender", source = "sex")
  6. @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
  7. @Override
  8. UserVo sourceToTarget(User var1);
  9. @Mapping(target = "sex", source = "gender")
  10. @Mapping(target = "password", ignore = true)
  11. @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
  12. @Override
  13. User targetToSource(UserVo var1);
  14. default List<UserConfig> strConfigToListUserConfig(String config) {
  15. return JSON.parseArray(config, UserConfig.class);
  16. }
  17. default String listUserConfigToStrConfig(List<UserConfig> list) {
  18. return JSON.toJSONString(list);
  19. }
  20. }

这里使用的是 Spring 的方式,@Mapper 注解的 componentModel 属性值为 spring,不过应该大多数都用的此模式进行开发。
@Mapping用于配置对象的映射关系,示例中 User 对象性别属性名为 sex,而UserVo对象性别属性名为gender,因此需要配置 target 与 source 属性。
password 字段不应该返回到前台,可以采取两种方式不进行转换,第一种就是在vo对象中不出现password字段,第二种就是在@Mapping中设置该字段 ignore = true
MapStruct 提供了时间格式化的属性 dataFormat,支持DateLocalDateLocalDateTime等时间类型与String的转换。示例中birthday 属性为 LocalDate 类型,可以无需指定dataFormat自动完成转换,而LocalDateTime类型默认使用的是ISO格式时间,在国内往往不符合需求,因此需要手动指定一下 dataFormat。

5、自定义属性类型转换方法

一般常用的类型字段转换 MapStruct都能完成,但是有一些是自定义的对象类型,MapStruct就不能进行字段转换,这就需要编写对应的类型转换方法,这里使用的是JDK8,支持接口中的默认方法,可以直接在转换器中添加自定义类型转换方法。
示例中User对象的config属性是一个JSON字符串,UserVo对象中是List类型的,这需要实现JSON字符串与对象的互转。

  1. default List<UserConfig> strConfigToListUserConfig(String config) {
  2. return JSON.parseArray(config, UserConfig.class);
  3. }
  4. default String listUserConfigToStrConfig(List<UserConfig> list) {
  5. return JSON.toJSONString(list);
  6. }

如果是 JDK8以下的,不支持默认方法,可以另外定义一个 转换器,然后再当前转换器的 @Mapper 中通过 uses = XXX.class 进行引用。
定义好方法之后,MapStruct当匹配到合适类型的字段时,会调用自定义的转换方法进行转换。

单元测试

  1. @Slf4j
  2. @RunWith(SpringRunner.class)
  3. @SpringBootTest
  4. public class MapStructTest {
  5. @Resource
  6. private UserMapping userMapping;
  7. @Test
  8. public void tetDomain2DTO() {
  9. User user = new User()
  10. .setId(1L)
  11. .setUsername("zhangsan")
  12. .setSex(1)
  13. .setPassword("abc123")
  14. .setCreateTime(LocalDateTime.now())
  15. .setBirthday(LocalDate.of(1999, 9, 27))
  16. .setConfig("[{\"field1\":\"Test Field1\",\"field2\":500}]");
  17. UserVo userVo = userMapping.sourceToTarget(user);
  18. log.info("User: {}", user);
  19. log.info("UserVo: {}", userVo);
  20. }
  21. @Test
  22. public void testDTO2Domain() {
  23. UserConfig userConfig = new UserConfig();
  24. userConfig.setField1("Test Field1");
  25. userConfig.setField2(500);
  26. UserVo userVo = new UserVo()
  27. .setId(1L)
  28. .setUsername("zhangsan")
  29. .setGender(2)
  30. .setCreateTime("2020-01-18 15:32:54")
  31. .setBirthday(LocalDate.of(1999, 9, 27))
  32. .setConfig(Collections.singletonList(userConfig));
  33. User user = userMapping.targetToSource(userVo);
  34. log.info("UserVo: {}", userVo);
  35. log.info("User: {}", user);
  36. }
  37. }

6、常见问题

当两个对象属性不一致时,比如User对象中某个字段不存在与UserVo当中时,在编译时会有警告提示,可以在@Mapping中配置 ignore = true,当字段较多时,可以直接在@Mapper中设置unmappedTargetPolicy属性或者unmappedSourcePolicy属性为 ReportingPolicy.IGNORE即可。
如果项目中也同时使用到了 Lombok,一定要注意 Lombok的版本要等于或者高于1.18.10,否则会有编译不通过的情况发生。