Java MapStruct

1、什么是MapStruct

1.1 JavaBean 的困扰

在开发的时候业务代码之间有很多的 JavaBean 之间的相互转化, 非常的影响观感,却又不得不存在。后来想的一个办法就是通过反射,或者自己写很多的转换器。
第一种通过反射的方法确实比较方便,但是现在无论是 BeanUtils, BeanCopier 等在使用反射的时候都会影响到性能。虽然可以进行反射信息的缓存来提高性能。但是像这种的话,需要类型和名称都一样才会进行映射,有很多时候,由于不同的团队之间使用的名词不一样,还是需要很多的手动 set/get 等功能。
第二种的话就是会很浪费时间,而且在添加新的字段的时候也要进行方法的修改。不过,由于不需要进行反射,其性能是很高的。

1.2 MapStruct 带来的改变

MapSturct 是一个生成类型安全,高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

  • 注解处理器
  • 可以生成 JavaBean 之间那的映射代码
  • 类型安全,高性能,无依赖性

    2、MapStruct 入门

    2.1 添加依赖

    1. <dependency>
    2. <groupId>org.projectlombok</groupId>
    3. <artifactId>lombok</artifactId>
    4. <version>1.16.20</version>
    5. <scope>provided</scope>
    6. </dependency>
    7. <dependency>
    8. <groupId>org.mapstruct</groupId>
    9. <artifactId>mapstruct-jdk8</artifactId>
    10. <version>${org.mapstruct.version}</version>
    11. </dependency>
    12. <dependency>
    13. <groupId>org.mapstruct</groupId>
    14. <artifactId>mapstruct-processor</artifactId>
    15. <version>${org.mapstruct.version}</version>
    16. </dependency>
    17. <dependency>
    18. <groupId>cn.hutool</groupId>
    19. <artifactId>hutool-all</artifactId>
    20. <version>5.1.0</version>
    21. </dependency>
    22. <dependency>
    23. <groupId>junit</groupId>
    24. <artifactId>junit</artifactId>
    25. <version>4.12</version>
    26. <scope>test</scope>
    27. </dependency>

    2.2 po类

    1. @Data
    2. public class User {
    3. private Integer id;
    4. private String name;
    5. private String address;
    6. private Date birth;
    7. }

    2.3 dto类

    1. @Data
    2. public class UserDto implements Serializable {
    3. private Integer id;
    4. private String name;
    5. private String address;
    6. private Date birth;
    7. }

    2.4 创建转换接口

    1. //可以使用abstract class代替接口
    2. @Mapper
    3. public interface UserMapper {
    4. UserDto userToUserDto(User user);
    5. //集合
    6. List<UserDto> userToUserDto(List<User> users);
    7. }

    2.5 测试方法

    1. @Test
    2. public void userPoToUserDto() {
    3. User user =new User();
    4. user.setId(1);
    5. user.setName("myx");
    6. user.setAddress("河北沧州");
    7. user.setBirth(new Date());
    8. UserMapper mapper = Mappers.getMapper(UserMapper.class);
    9. UserDto userDto = mapper.userToUserDto(user);
    10. System.out.println(userDto);
    11. }

    2.6 运行效果

    2021-09-04-13-56-49-437039.png

    2.7 查看编译的class

    底层通过自动取值赋值操作完成
    2021-09-04-13-56-49-702036.png

    3、MapStruct优点分析

    3.1 性能高

    这是相对反射来说的,反射需要去读取字节码的内容,花销会比较大。而通过 MapStruct 来生成的代码,其类似于人手写。速度上可以得到保证。

    3.2 使用简单

    如果是完全映射的,使用起来肯定没有反射简单。用类似 BeanUtils 这些工具一条语句就搞定了。但是,如果需要进行特殊的匹配(特殊类型转换,多对一转换等),其相对来说也是比较简单的。
    基本上,使用的时候,只需要声明一个接口,接口下写对应的方法,就可以使用了。当然,如果有特殊情况,是需要额外处理的。

    3.3 代码独立

    生成的代码是对立的,没有运行时的依赖。

    3.4 易于 debug

    在生成的代码中,可以轻易的进行 debug。

    4、MapStruct使用案例

    4.1 属性名称相同

    在实现类的时候,如果属性名称相同,则会进行对应的转化。通过此种方式,可以快速的编写出转换的方法。(入门案例)

    4.2 属性名不相同

    属性名不相同,在需要进行互相转化的时候,则可以通过@Mapping 注解来进行转化。 ```java @Data public class UserDto implements Serializable { private Integer id; private String name; private String address; private Date birth; private String password; }

@Data public class User { private Integer id; private String name; private String address; private Date birth; private String pwd; }

@Mapper public interface UserMapper { //单个属性 //@Mapping(source = “pwd”,target = “password”) //多个属性 @Mappings({ @Mapping(source = “pwd”,target = “password”) }) UserDto userToUserDto(User user); }

  1. - source 需要转换的对接,通常是入参
  2. - target 转换的对接,通常是出参
  3. - ignore 忽略,默认false不忽略,需要忽略设置为true
  4. - defaultValue 默认值
  5. - expressions 可以通过表达式来构造一些简单的转化关系。虽然设计的时候想兼容很多语言,不过目前只能写Java代码。
  6. ```java
  7. @Mappings({
  8. @Mapping(source = "birthdate", target = "birth"),//属性名不一致映射
  9. @Mapping(target = "birthformat", expression = "java(org.apache.commons.lang3.time.DateFormatUtils.format(person.getBirthdate(),\"yyyy-MM-dd HH:mm:ss\"))"),//自定义属性通过java代码映射
  10. })
  11. public PersonVo PersonToPersonVo(Person person);

这里用到演示了如何使用TimeAndFormattimeformat操作,这里必须要指定需要使用的Java类的完整包名,不然编译的时候不知道使用哪个Java类,会报错。

  1. @Test
  2. public void userPoToUserDto() {
  3. User user =new User();
  4. user.setId(1);
  5. user.setName("myx");
  6. user.setAddress("河北沧州");
  7. user.setBirth(new Date());
  8. user.setPwd("123456");
  9. UserMapper mapper = Mappers.getMapper(UserMapper.class);
  10. UserDto userDto = mapper.userToUserDto(user);
  11. System.out.println(userDto);
  12. }

2021-09-04-13-56-49-854037.png

4.3 转换非基础类型属性

如果subUser与subUserDto字段名称相同直接配置即可完成(对象类型,包括list)

  1. @Data
  2. public class UserDto implements Serializable {
  3. private Integer id;
  4. private String name;
  5. private String address;
  6. private Date birth;
  7. private String password;
  8. private List<SubUserDto> subUserDto;
  9. }
  10. @Data
  11. public class User {
  12. private Integer id;
  13. private String name;
  14. private String address;
  15. private Date birth;
  16. private String pwd;
  17. private List<SubUser> subUser;
  18. }
  19. @Mappings({
  20. @Mapping(source = "pwd",target = "password"),
  21. @Mapping(source = "subUser", target = "subUserDto")
  22. })
  23. UserDto userToUserDto(User user);

4.4 Mapper 中使用自定义的转换

有时候,对于某些类型,无法通过代码生成器的形式来进行处理。那么, 就需要自定义的方法来进行转换。这时候,可以在接口(同一个接口,后续还有调用别的 Mapper 的方法)中定义默认方法(Java8及之后)。

  1. @Data
  2. public class UserDto implements Serializable {
  3. private Integer id;
  4. private String name;
  5. private String address;
  6. private Date birth;
  7. private String password;
  8. private SubUserDto subUserDto;
  9. }
  10. @Data
  11. public class SubUserDto {
  12. private Boolean result;
  13. private String name;
  14. }
  15. @Data
  16. public class User {
  17. private Integer id;
  18. private String name;
  19. private String address;
  20. private Date birth;
  21. private String pwd;
  22. private SubUser subUser;
  23. }
  24. @Data
  25. public class SubUser {
  26. private Integer deleted;
  27. private String name;
  28. }
  29. @Mapper
  30. public interface UserMapper {
  31. @Mappings({
  32. @Mapping(source = "pwd",target = "password"),
  33. @Mapping(source = "subUser", target = "subUserDto")
  34. })
  35. UserDto userToUserDto(User user);
  36. default SubUserDto subSource2subTarget(SubUser subUser) {
  37. if (subUser == null) {
  38. return null;
  39. }
  40. SubUserDto subUserDto = new SubUserDto();
  41. subUserDto.setResult(!subUser.getDeleted().equals(0));
  42. subUserDto.setName(subUser.getName()==null?"":subUser.getName());
  43. return subUserDto;
  44. }
  45. }

只能存在一个default修饰的方法

  1. @Test
  2. public void userPoToUserDto() {
  3. User user =new User();
  4. user.setId(1);
  5. user.setName("myx");
  6. user.setAddress("河北沧州");
  7. user.setBirth(new Date());
  8. user.setPwd("123456");
  9. SubUser subUser =new SubUser();
  10. subUser.setDeleted(0);
  11. subUser.setName("rkw");
  12. user.setSubUser(subUser);
  13. UserMapper mapper = Mappers.getMapper(UserMapper.class);
  14. UserDto userDto = mapper.userToUserDto(user);
  15. System.out.println(userDto);
  16. }

2021-09-04-13-56-49-993083.png

4.5 多转一

在实际的业务中少不了将多个对象转换成一个的场景。MapStruct 当然也支持多转一的操作。

  1. @Data
  2. public class SubUser {
  3. private Integer deleted;
  4. private String name;
  5. }
  6. @Data
  7. public class User {
  8. private Integer id;
  9. private String name;
  10. private String address;
  11. private Date birth;
  12. private String pwd;
  13. }
  14. @Mapper
  15. public interface UserMapper {
  16. @Mappings({
  17. @Mapping(source = "user.pwd",target = "password"),
  18. @Mapping(source = "subUser.name", target = "name")
  19. })
  20. NewUserDto userToUserDto(User user,SubUser subUser);
  21. }
  22. @Test
  23. public void userPoToUserDto() {
  24. User user =new User();
  25. user.setId(1);
  26. user.setName("myx");
  27. user.setAddress("河北沧州");
  28. user.setBirth(new Date());
  29. user.setPwd("123456");
  30. SubUser subUser =new SubUser();
  31. subUser.setDeleted(0);
  32. subUser.setName("rkw");
  33. UserMapper mapper = Mappers.getMapper(UserMapper.class);
  34. NewUserDto userDto = mapper.userToUserDto(user,subUser);
  35. System.out.println(userDto);
  36. }

2021-09-04-13-56-50-169309.png

4.5.1 遵循原则

  • 当多个对象中, 有其中一个为 null, 则会直接返回 null
  • 如一对一转换一样, 属性通过名字来自动匹配。因此, 名称和类型相同的不需要进行特殊处理
  • 当多个原对象中,有相同名字的属性时,需要通过 @Mapping 注解来具体的指定, 以免出现歧义(不指定会报错)。如上面的 name

属性也可以直接从传入的参数来赋值

  1. @Mapping(source = "person.description", target = "description")
  2. @Mapping(source = "name", target = "name")
  3. DeliveryAddress personAndAddressToDeliveryAddressDto(Person person, String name);

4.6 更新 Bean 对象

有时候,不是想返回一个新的 Bean 对象,而是希望更新传入对象的一些属性。这个在实际的时候也会经常使用到。

  1. @Mapper
  2. public interface UserMapper {
  3. NewUserDto userToNewUserDto(User user);
  4. /**
  5. * 更新, 注意注解 @MappingTarget
  6. * 注解 @MappingTarget后面跟的对象会被更新。
  7. */
  8. void updateDeliveryAddressFromAddress(SubUser subUser,@MappingTarget NewUserDto newUserDto);
  9. }
  10. @Test
  11. public void userPoToUserDto() {
  12. User user =new User();
  13. user.setId(1);
  14. user.setName("myx");
  15. user.setAddress("河北沧州");
  16. user.setBirth(new Date());
  17. SubUser subUser =new SubUser();
  18. subUser.setDeleted(0);
  19. subUser.setName("rkw");
  20. UserMapper mapper = Mappers.getMapper(UserMapper.class);
  21. NewUserDto userDto = mapper.userToNewUserDto(user);
  22. mapper.updateDeliveryAddressFromAddress(subUser,userDto);
  23. System.out.println(userDto);
  24. }

4.7 map映射

  1. @MapMapping(valueDateFormat ="yyyy-MM-dd HH:mm:ss")
  2. public Map<String ,String> DateMapToStringMap(Map<String,Date> sourceMap);
  3. @Test
  4. public void mapMappingTest(){
  5. Map<String,Date> map=new HashMap<>();
  6. map.put("key1",new Date());
  7. map.put("key2",new Date(new Date().getTime()+9800000));
  8. Map<String, String> stringObjectMap = TestMapper.MAPPER.DateMapToStringMap(map);
  9. }

4.8 多级嵌套

只需要在mapper接口中定义相关的类型转换方法即可,list类型也适用

4.8.1 方式1

  1. @Data
  2. public class User {
  3. private Integer id;
  4. private String name;
  5. private String address;
  6. private Date birth;
  7. private Boolean isDisable;
  8. private List<SubUser> user;
  9. }
  10. @Data
  11. public class SubUser {
  12. private Integer deleted;
  13. private String name;
  14. private List<SubSubUser> subUser;
  15. }
  16. @Data
  17. public class SubSubUser {
  18. private String aaa;
  19. private String ccc;
  20. }
  21. @Data
  22. public class UserDto implements Serializable {
  23. private Integer id;
  24. private String name;
  25. private String address;
  26. private Date birth;
  27. private String isDisable;
  28. private List<SubUserDto> user;
  29. }
  30. @Data
  31. public class SubUserDto {
  32. private Integer deleted;
  33. private String name;
  34. private List<SubSubUserDto> subUser;
  35. }
  36. @Data
  37. public class SubSubUserDto {
  38. private String aaa;
  39. private String bbb;
  40. }
  41. @Mapper
  42. public interface UserMapper {
  43. UserDto userToNewUserDto(User user);
  44. //子集字段相同方法不用编写会自动生成
  45. //孙子集字段不相同(list会自动读取此方法生成list)
  46. @Mapping(source = "ccc",target = "bbb")
  47. SubSubUserDto bbb(SubSubUser subSubUser);
  48. }

4.8.2 方式2

通过uses配置类型转换

  1. @Mapper(uses = {TestMapper.class})
  2. public interface UserMapper {
  3. UserDto userToNewUserDto(User user);
  4. }
  5. @Mapper
  6. public interface TestMapper {
  7. @Mapping(source = "ccc",target = "bbb")
  8. SubSubUserDto bbb(SubSubUser subSubUser);
  9. }

5、获取 mapper

5.1 通过 Mapper 工厂获取

都是通过 Mappers.getMapper(xxx.class) 的方式来进行对应 Mapper 的获取。此种方法为通过 Mapper 工厂获取。
如果是此种方法,约定俗成的是在接口内定义一个接口本身的实例 INSTANCE, 以方便获取对应的实例。

  1. @Mapper
  2. public interface SourceMapper {
  3. SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);
  4. // ......
  5. }

这样在调用的时候,就不需要在重复的去实例化对象了。类似下面

  1. Target target = SourceMapper.INSTANCE.source2target(source);

5.2 使用依赖注入

对于 Web 开发,依赖注入应该很熟悉。MapSturct 也支持使用依赖注入,同时也推荐使用依赖注入。
2021-09-04-13-56-50-326334.png

  1. @Mapper(componentModel = "spring")

5.3 依赖注入策略

可以选择是通过构造方法或者属性注入,默认是属性注入。

  1. public enum InjectionStrategy {
  2. /** Annotations are written on the field **/
  3. FIELD,
  4. /** Annotations are written on the constructor **/
  5. CONSTRUCTOR
  6. }

类似如此使用

  1. @Mapper(componentModel = "cdi" injectionStrategy = InjectionStrategy.CONSTRUCTOR)

5.4 自定义类型转换

有时候,在对象转换的时候可能会出现这样一个问题,就是源对象中的类型是Boolean类型,而目标对象类型是String类型,这种情况可以通过@Mapper的uses属性来实现:

  1. @Data
  2. public class User {
  3. private Integer id;
  4. private String name;
  5. private String address;
  6. private Date birth;
  7. private Boolean isDisable;
  8. }
  9. @Data
  10. public class UserDto implements Serializable {
  11. private Integer id;
  12. private String name;
  13. private String address;
  14. private Date birth;
  15. private String isDisable;
  16. }
  17. @Mapper(uses = {BooleanStrFormat.class})
  18. public interface UserMapper {
  19. UserDto userToNewUserDto(User user);
  20. }
  21. public class BooleanStrFormat {
  22. public String toStr(Boolean isDisable) {
  23. if (isDisable) {
  24. return "Y";
  25. } else {
  26. return "N";
  27. }
  28. }
  29. public Boolean toBoolean(String str) {
  30. if (str.equals("Y")) {
  31. return true;
  32. } else {
  33. return false;
  34. }
  35. }
  36. }

要注意的是,如果使用了例如像spring这样的环境,Mapper引入uses类实例的方式将是自动注入,那么这个类也应该纳入Spring容器

  1. @Test
  2. public void userPoToUserDto() {
  3. User user =new User();
  4. user.setId(1);
  5. user.setName("myx");
  6. user.setAddress("河北沧州");
  7. user.setBirth(new Date());
  8. user.setIsDisable(true);
  9. SubUser subUser =new SubUser();
  10. subUser.setDeleted(0);
  11. subUser.setName("rkw");
  12. UserMapper mapper = Mappers.getMapper(UserMapper.class);
  13. UserDto userDto = mapper.userToNewUserDto(user);
  14. System.out.println(userDto);
  15. }