推荐一款Java对象映射神器,别再傻傻手动转换了!

共 7027字,需浏览 15分钟

 ·

2020-11-13 16:30

原文地址:https://nullpointer.pw/mapstruct最佳实践.html

前几天发的《一份热乎的 SpringBoot 前后端分离后台管理系统分析!分模块开发、RBAC 权限控制...》这篇文章中我推荐了 MapStruct 来做对象映射。这篇文章就带着小伙伴们详细的看一下这个好用的工具库。

前言

按照日常开发习惯,对于不同领域层使用不同JavaBean对象传输数据,避免相互影响,因此基于数据库实体对象User衍生出比如UserDtoUserVo等对象,于是在不同层之间进行数据传输时,不可避免地需要将这些对象进行互相转换操作。

常见的转换方式有:

  • 调用getter/setter方法进行属性赋值
  • 调用BeanUtil.copyPropertie进行反射属性赋值

第一种方式不必说,属性多了就需要写一大坨getter/setter代码。第二种方式比第一种方式要简便很多,但是坑巨多,比如 sources 与 target 写反,难以定位某个字段在哪里进行的赋值,同时因为用到反射,导致性能也不佳。

鉴于此,今天写一写第三种对象转换方式,本文使用的是 MapStruct 工具进行转换,MapStruct 原理也很简单,就是在代码编译阶段生成对应的赋值代码,底层原理还是调用getter/setter方法,但是这是由工具替我们完成,MapStruct在不影响性能的情况下,解决了前面两种方式弊端,很赞~

准备工作

为了讲解 MapStruct 工具的使用,本文使用常见的 User 类以及对应 UserDto 对象来演示。

@Data
@Accessors(chain = true)
public class User {
    private Long id;
    private String username;
    private String password; // 密码
    private Integer sex;  // 性别
    private LocalDate birthday; // 生日
    private LocalDateTime createTime; // 创建时间
    private String config; // 其他扩展信息,以JSON格式存储
   private String test; // 测试字段
}

@Data
@Accessors(chain = true)
public class UserVo {
    private Long id;
    private String username;
    private String password;
    private Integer gender;
    private LocalDate birthday;
    private String createTime;
    private List config;
   private String test; // 测试字段
    @Data
    public static class UserConfig {
        private String field1;
        private Integer field2;
    }
}

注意观察这两个类的区别。

一、MapStruct 配置以及基础使用

项目中引入 MapStruct 的依赖

<dependency>
  <groupId>org.mapstructgroupId>
  <artifactId>mapstructartifactId>
  <version>1.3.1.Finalversion>
dependency>
<dependency>
  <groupId>org.mapstructgroupId>
  <artifactId>mapstruct-processorartifactId>
  <version>1.3.1.Finalversion>
dependency>

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

@MapperConfig
public interface BaseMapping<SOURCETARGET{

    /**
     * 映射同名属性
     */

    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    TARGET sourceToTarget(SOURCE var1);

    /**
     * 反向,映射同名属性
     */

    @InheritInverseConfiguration(name = "sourceToTarget")
    SOURCE targetToSource(TARGET var1);

    /**
     * 映射同名属性,集合形式
     */

    @InheritConfiguration(name = "sourceToTarget")
    List sourceToTarget(List var1);

    /**
     * 反向,映射同名属性,集合形式
     */

    @InheritConfiguration(name = "targetToSource")
    List targetToSource(List var1);

    /**
     * 映射同名属性,集合流形式
     */

    List sourceToTarget(Stream stream);

    /**
     * 反向,映射同名属性,集合流形式
     */

    List targetToSource(Stream stream);
}

实现 User 与 UserVo 对象的转换器

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring")
public interface UserMapping extends BaseMapping<UserUserVo{

    @Mapping(target = "gender", source = "sex")
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Override
    UserVo sourceToTarget(User var1);

    @Mapping(target = "sex", source = "gender")
    @Mapping(target = "password", ignore = true)
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Override
    User targetToSource(UserVo var1);

    default List strConfigToListUserConfig(String config) {
        return JSON.parseArray(config, UserConfig.class);
    }

    default String listUserConfigToStrConfig(List list) {
        return JSON.toJSONString(list);
    }
}

本文示例使用的是 Spring 的方式,@Mapper注解的 componentModel 属性值为 spring,不过应该大多数都用的此模式进行开发。

@Mapping用于配置对象的映射关系,示例中 User 对象性别属性名为 sex,而UserVo对象性别属性名为gender,因此需要配置 targetsource 属性。

password 字段不应该返回到前台,可以采取两种方式不进行转换,第一种就是在 vo 对象中不出现 password 字段,第二种就是在@Mapping中设置该字段ignore = true

MapStruct 提供了时间格式化的属性 dataFormat,支持DateLocalDateLocalDateTime等时间类型与String的转换。示例中birthday 属性为 LocalDate 类型,可以无需指定dataFormat自动完成转换,而LocalDateTime类型默认使用的是 ISO 格式时间,在国内往往不符合需求,因此需要手动指定一下 dataFormat

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

一般常用的类型字段转换 MapStruct都能替我们完成,但是有一些是我们自定义的对象类型,MapStruct就不能进行字段转换,这就需要我们编写对应的类型转换方法,笔者使用的是 JDK8,支持接口中的默认方法,可以直接在转换器中添加自定义类型转换方法。

示例中User对象的 config 属性是一个 JSON 字符串,UserVo对象中是 List 类型的,这需要实现 JSON 字符串与对象的互转。

default List strConfigToListUserConfig(String config) {
  return JSON.parseArray(config, UserConfig.class);
}

default String listUserConfigToStrConfig(List list) {
  return JSON.toJSONString(list);
}

如果是 JDK8 以下的,不支持默认方法,可以另外定义一个 转换器,然后再当前转换器的 @Mapper 中通过 uses = XXX.class 进行引用。

定义好方法之后,MapStruct当匹配到合适类型的字段时,会调用我们自定义的转换方法进行转换。

三、单元测试

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class MapStructTest 
{

  @Resource
  private UserMapping userMapping;

  @Test
  public void tetDomain2DTO() {
    User user = new User()
      .setId(1L)
      .setUsername("zhangsan")
      .setSex(1)
      .setPassword("abc123")
      .setCreateTime(LocalDateTime.now())
      .setBirthday(LocalDate.of(1999927))
      .setConfig("[{\"field1\":\"Test Field1\",\"field2\":500}]");
    UserVo userVo = userMapping.sourceToTarget(user);
    log.info("User: {}", user);
    //        User: User(id=1, username=zhangsan, password=abc123, sex=1, birthday=1999-09-27, createTime=2020-01-17T17:46:20.316, config=[{"field1":"Test Field1","field2":500}])
    log.info("UserVo: {}", userVo);
    //        UserVo: UserVo(id=1, username=zhangsan, gender=1, birthday=1999-09-27, createTime=2020-01-17 17:46:20, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
  }

  @Test
  public void testDTO2Domain() {
    UserConfig userConfig = new UserConfig();
    userConfig.setField1("Test Field1");
    userConfig.setField2(500);

    UserVo userVo = new UserVo()
      .setId(1L)
      .setUsername("zhangsan")
      .setGender(2)
      .setCreateTime("2020-01-18 15:32:54")
      .setBirthday(LocalDate.of(1999927))
      .setConfig(Collections.singletonList(userConfig));
    User user = userMapping.targetToSource(userVo);
    log.info("UserVo: {}", userVo);
    //        UserVo: UserVo(id=1, username=zhangsan, gender=2, birthday=1999-09-27, createTime=2020-01-18 15:32:54, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
    log.info("User: {}", user);
    //        User: User(id=1, username=zhangsan, password=null, sex=2, birthday=1999-09-27, createTime=2020-01-18T15:32:54, config=[{"field1":"Test Field1","field2":500}])
  }

四、常见问题

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

代码下载

本文涉及代码已上传到 Github,以供参考。

  • mapstruct 最佳实践示例代码[1]

参考

  • 官方文档[2]
  • 官方 FAQ[3]
  • 官方 Examples[4]
  • 机翻中文版文档[5]
  • 5 种常见 Bean 映射工具的性能比对[6]

参考资料

[1]

mapstruct 最佳实践示例代码: https://github.com/Mosiki/learning-modules/tree/master/learning-mapstruct

[2]

官方文档: https://mapstruct.org/documentation/stable/reference/html/

[3]

官方 FAQ: https://mapstruct.org/faq/

[4]

官方 Examples: https://github.com/mapstruct/mapstruct-examples

[5]

机翻中文版文档: http://www.kailing.pub/MapStruct1.3/index.html

[6]

5 种常见 Bean 映射工具的性能比对: https://www.cnblogs.com/javaguide/p/11861749.html

最近写的一些干货,每篇都很用心,欢迎各位小伙伴阅读/点赞/分享:

  1. Spring Boot搭建的一个在线文件预览系统!支持ppt、doc等多种类型文件预览

  2. 系统设计面试题指北

  3.  大二逃课总结的1.2w字的计算机网络知识!扫盲!

  4.  5分“白嫖”我使用Github 5 年总结的这些骚操作

  5.  真香!被阿里老兄安利了一个轻量级的日志追踪框架。10 分钟即可接入!

  6.  一份热乎的 SpringBoot 前后端分离后台管理系统分析!分模块开发、RBAC权限控制...


我是Guide哥,Java后端开发,会一点前端知识,喜欢烹饪,自由的少年。一个三观比主角还正的技术人。我们下期再见!


浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报