• 告别BeanUtils,Mapstruct从入门到精通


    9989cbd6572770e5d25848f90e9c5f82.gif

    如果你现在还在使用BeanUtils,看了本文,也会像我一样,从此改用Mapstruct。

    对象之间的属性拷贝,之前用的是Spring的BeanUtils,有一次,在学习领域驱动设计的时候,看了一位大佬的文章,他在文章中提到使用Mapstruct做DO和Entity的相互转换,出于好奇,后来就去了解了一下Mapstruct,发现这个工具确实优秀,所以果断弃用BeanUtils。

    如果你现在还在使用BeanUtils,看了本文,也会像我一样,从此改用Mapstruct

    先上结论,Mapstruct的性能远远高于BeanUtils,这应该是大佬使用Mapstruct的主要原因,下面是我的测试结果,可以看出随着属性个数的增加,BeanUtils的耗时也在增加,并且BeanUtils的耗时跟属性个数成正比,而Mapstruct的耗时却一直是1秒,所以从对比数据可以看出Mapstruct是非常优秀的,其性能远远超过BeanUtils。

    下文会讲到Mapstruct性能好的根本原因。

    对象转换次数

    属性个数

    BeanUtils耗时

    Mapstruct耗时

    5千万次

    6

    14秒

    1秒

    5千万次

    15

    36秒

    1秒

    5千万次

    25

    55秒

    1秒

    Mapstruct 依赖

    使用Mapstruct需要依赖的包如下,mapstruct、mapstruct-processor、lombok,可以去仓库中查看最新版本。

    1. <dependency>
    2. <groupId>org.mapstructgroupId>
    3. <artifactId>mapstructartifactId>
    4. <version>1.5.0.Finalversion>
    5. dependency>
    6. <dependency>
    7. <groupId>org.mapstructgroupId>
    8. <artifactId>mapstruct-processorartifactId>
    9. <version>1.5.0.Finalversion>
    10. dependency>
    11. <dependency>
    12. <groupId>org.projectlombokgroupId>
    13. <artifactId>lombokartifactId>
    14. <version>1.18.12version>
    15. dependency>

    简单的属性拷贝

    下面我们先来看下Mapstruct最简单的使用方式。

    当两个对象的属性类型和名称完全相同时,Mapstruct会自动拷贝;假设我们现在需要把UserPo的属性值拷贝到UserEntity中,我们需要做下面几件事情:

    1. 定义UserPo和UserEntity

    2. 定义转换接口

    3. 编写测试main方法

      首先定义UserPo和UserEntity

    UserPo和UserEntity的属性类型和名称完全相同。

    1. package mapstruct;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Builder;
    4. import lombok.Data;
    5. import lombok.NoArgsConstructor;
    6. import java.util.Date;
    7. @Data
    8. @Builder
    9. @AllArgsConstructor
    10. @NoArgsConstructor
    11. public class UserPo {
    12. private Long id;
    13. private Date gmtCreate;
    14. private Date createTime;
    15. private Long buyerId;
    16. private Long age;
    17. private String userNick;
    18. private String userVerified;
    19. }
    1. package mapstruct;
    2. import lombok.Data;
    3. import java.util.Date;
    4. @Data
    5. public class UserEntity {
    6. private Long id;
    7. private Date gmtCreate;
    8. private Date createTime;
    9. private Long buyerId;
    10. private Long age;
    11. private String userNick;
    12. private String userVerified;
    13. }
      定义转换接口

    定义mapstruct接口,在接口上打上@Mapper注解。

    接口中有一个常量和一个方法,常量的值是接口的实现类,这个实现类是Mapstruct默认帮我们实现的,下文会讲到。定义了一个po2entity的转换方法,表示把入参UserPo对象,转换成UserEntity。

    注意@Mapper是Mapstruct的注解,不要引错了。

    1. package mapstruct;
    2. import org.mapstruct.Mapper;
    3. import org.mapstruct.factory.Mappers;
    4. @Mapper
    5. public interface IPersonMapper {
    6. IPersonMapper INSTANCT = Mappers.getMapper(IPersonMapper.class);
    7. UserEntity po2entity(UserPo userPo);
    8. }
      测试类

    创建一个UserPo对象,并使用Mapstruct做转化。

    1. package mapstruct;
    2. import org.springframework.beans.BeanUtils;
    3. import java.util.Date;
    4. public class MapStructTest {
    5. public static void main(String[] args) {
    6. testNormal();
    7. }
    8. public static void testNormal() {
    9. System.out.println("-----------testNormal-----start------");
    10. UserPo userPo = UserPo.builder()
    11. .id(1L)
    12. .gmtCreate(new Date())
    13. .buyerId(666L)
    14. .userNick("测试mapstruct")
    15. .userVerified("ok")
    16. .age(18L)
    17. .build();
    18. System.out.println("1234" + userPo);
    19. UserEntity userEntity = IPersonMapper.INSTANCT.po2entity(userPo);
    20. System.out.println(userEntity);
    21. System.out.println("-----------testNormal-----ent------");
    22. }
    23. }
      测试结果

    可以看到,所有赋值的属性都做了处理,且两边的值都一样,结果符合预期。

    

    97324b552de351a925f8200791647e0e.png

    Mapstruct 性能优于 BeanUtils 的原因

    Java程序执行的过程,是由编译器先把java文件编译成class字节码文件,然后由JVM去解释执行class文件。Mapstruct正是在java文件到class这一步帮我们实现了转换方法,即做了预处理,提前编译好文件,如果用过lombok的同学一定能理解其好处,通过查看class文件,可以看出IPersonMapper被打上org.mapstruct.Mapper注解后,编译器自动会帮我们生成一个实现类IPersonMapperImpl,并实现了po2entity这个方法,看下面的截图。

      IPersonMapperImpl代码

    从生成的代码可以看出,转化过程非常简单,只使用了UserPo的get方法和UserEntity的set方法,没有复杂的逻辑处理,清晰明了,所以性能很高。

    下面再去看BeanUtils的默认实现。

    4a6d53a6ed2d43a016963eab04ce3b1c.png

      Spring的BeanUtils源

    BeanUtils部分源码如下,转换的原理是使用的反射,反射的效率相对来说是低的,因为jvm优化在这种场景下有可能无效,所以在对性能要求很高或者经常被调用的程序中,尽量不要使用。我们平时在研发过程中,也会遵守这个原则,非必要,不反射。

    从下面的BeanUtils代码中可以看出,转化逻辑非常复杂,有很多的遍历,去获取属性,获取方法,设置方法可访问,然后执行,所以执行效率相对Mapstruct来说,是非常低的。回头看Mapstruct自动生成的实现类,简洁、高效。

    1. private static void copyProperties(Object source, Object target, Class editable, String... ignoreProperties)
    2. throws BeansException {
    3. Assert.notNull(source, "Source must not be null");
    4. Assert.notNull(target, "Target must not be null");
    5. Class actualEditable = target.getClass();
    6. if (editable != null) {
    7. if (!editable.isInstance(target)) {
    8. throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
    9. "] not assignable to Editable class [" + editable.getName() + "]");
    10. }
    11. actualEditable = editable;
    12. }
    13. PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
    14. List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
    15. for (PropertyDescriptor targetPd : targetPds) {
    16. Method writeMethod = targetPd.getWriteMethod();
    17. if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
    18. PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
    19. if (sourcePd != null) {
    20. Method readMethod = sourcePd.getReadMethod();
    21. if (readMethod != null &&
    22. ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
    23. try {
    24. if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
    25. readMethod.setAccessible(true);
    26. }
    27. Object value = readMethod.invoke(source);
    28. if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
    29. writeMethod.setAccessible(true);
    30. }
    31. writeMethod.invoke(target, value);
    32. }
    33. catch (Throwable ex) {
    34. throw new FatalBeanException(
    35. "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
    36. }
    37. }
    38. }
    39. }
    40.     }

    属性类型相同名称不同

    对于属性名称不同的属性进行处理时,需要使用@Mapping,比如修改UserEntity中的userNick为userNick1,然后进行转换。

      修改UserEntity属性userNick1
    1. package mapstruct;
    2. import lombok.Data;
    3. import java.util.Date;
    4. @Data
    5. public class UserEntity {
    6. private Long id;
    7. private Date gmtCreate;
    8. private Date createTime;
    9. private Long buyerId;
    10. private Long age;
    11. private String userNick1;
    12. private String userVerified;
    13. }
      @Mapping注解指定source和target字段名称对应关系

    @Mapping(target = "userNick1", source = "userNick"),此处的意思就是在转化的过程中,将UserPo的userNick属性值赋值给UserEntity的userNick1属性。

    1. package mapstruct;
    2. import org.mapstruct.Mapper;
    3. import org.mapstruct.Mapping;
    4. import org.mapstruct.factory.Mappers;
    5. @Mapper
    6. public interface IPersonMapper {
    7. IPersonMapper INSTANCT = Mappers.getMapper(IPersonMapper.class);
    8. @Mapping(target = "userNick1", source = "userNick")
    9. UserEntity po2entity(UserPo userPo);
    10. }
      执行结果

    可以看到,正常映射,符合预期。

    4a05ef3026c5fde6b23772fc9d8a8252.png

      查看class文件

    我们再来看实现类,可以看到,Mapstruct帮我们做了处理,把po的userNick属性赋值给了entity的userNick1。

    5d7d96f100453f22256369a0c7a1a6ca.png

    String转日期&String转数字&忽略某个字端&给默认值等

    1. @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
    2. @Mapping(target = "age", source = "age", numberFormat = "#0.00")
    3. @Mapping(target = "id", ignore = true)
    4. @Mapping(target = "userVerified", defaultValue = "defaultValue-2")
      查看class实现类

    1. createTime:可以看到对日期使用了SimpleDateFormat进行转换,这里建议不要使用这个,因为每次都创建了一个SimpleDateFormat,可以参考《阿里巴巴Java开发手册》关于日期转换的建议。

    2. age:字符串转数字,也是帮忙做了处理

    3. id:字段赋值没有了

    4. userVerified:如果为null赋值默认值

    801aa82458d10a99e3576bdfe36fc9de.png

    自定义转换

    如果现有的能力都不能满足需要,可以自定义一个转换器,比如我们需要把一个字符串使用JSON工具转换成对象。

      添加属性

    我们在po中加入一个字符串的attributes属性,在entity中加入Attributes类型的属性

    1. package mapstruct;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Builder;
    4. import lombok.Data;
    5. import lombok.NoArgsConstructor;
    6. @Data
    7. @Builder
    8. @AllArgsConstructor
    9. @NoArgsConstructor
    10. public class Attributes {
    11. private Long id;
    12. private String name;
    13. }
    1. package mapstruct;
    2. import lombok.AllArgsConstructor;
    3. import lombok.Builder;
    4. import lombok.Data;
    5. import lombok.NoArgsConstructor;
    6. import java.util.Date;
    7. @Data
    8. @Builder
    9. @AllArgsConstructor
    10. @NoArgsConstructor
    11. public class UserPo {
    12. private Long id;
    13. private Date gmtCreate;
    14. private String createTime;
    15. private Long buyerId;
    16. private String age;
    17. private String userNick;
    18. private String userVerified;
    19. private String attributes;
    20. }
    1. package mapstruct;
    2. import lombok.Data;
    3. import java.util.Date;
    4. @Data
    5. public class UserEntity {
    6. private Long id;
    7. private Date gmtCreate;
    8. private Date createTime;
    9. private Long buyerId;
    10. private Long age;
    11. private String userNick1;
    12. private String userVerified;
    13. private Attributes attributes;
    14. }
      编写自定义转换处理类

    转换器很简单,就是一个普通的Java类,只要在方法上打上Mapstruct的注解@Named。

    1. package mapstruct;
    2. import com.alibaba.fastjson.JSONObject;
    3. import org.apache.commons.lang3.StringUtils;
    4. import org.mapstruct.Named;
    5. public class AttributeConvertUtil {
    6. /**
    7. * json字符串转对象
    8. *
    9. * @param jsonStr
    10. * @return
    11. */
    12. @Named("jsonToObject")
    13. public Attributes jsonToObject(String jsonStr) {
    14. if (StringUtils.isEmpty(jsonStr)) {
    15. return null;
    16. }
    17. return JSONObject.parseObject(jsonStr, Attributes.class);
    18. }
    19. }
      修改转换接口
    1. 在@Mapper上引用我们的自定义转换代码类AttributeConvertUtil

    2. 使用qualifiedByName指定我们使用的自定义转换方法

    1. package mapstruct;
    2. import org.mapstruct.Mapper;
    3. import org.mapstruct.Mapping;
    4. import org.mapstruct.factory.Mappers;
    5. /**
    6. * @author jiangzhengyin
    7. */
    8. @Mapper(uses = AttributeConvertUtil.class)
    9. public interface IPersonMapper {
    10. IPersonMapper INSTANCT = Mappers.getMapper(IPersonMapper.class);
    11. @Mapping(target = "attributes", source = "attributes", qualifiedByName = "jsonToObject")
    12. @Mapping(target = "userNick1", source = "userNick")
    13. @Mapping(target = "createTime", source = "createTime", dateFormat = "yyyy-MM-dd")
    14. @Mapping(target = "age", source = "age", numberFormat = "#0.00")
    15. @Mapping(target = "id", ignore = true)
    16. @Mapping(target = "userVerified", defaultValue = "defaultValue-2")
    17. UserEntity po2entity(UserPo userPo);
    18. }
      测试类及结果

    可以看出我们将把String转成了JSON对象

    1. public class MapStructTest {
    2. public static void main(String[] args) {
    3. testNormal();
    4. }
    5. public static void testNormal() {
    6. System.out.println("-----------testNormal-----start------");
    7. String attributes = "{\"id\":2,\"name\":\"测试123\"}";
    8. UserPo userPo = UserPo.builder()
    9. .id(1L)
    10. .gmtCreate(new Date())
    11. .buyerId(666L)
    12. .userNick("测试mapstruct")
    13. .userVerified("ok")
    14. .age("18")
    15. .attributes(attributes)
    16. .build();
    17. System.out.println("1234" + userPo);
    18. UserEntity userEntity = IPersonMapper.INSTANCT.po2entity(userPo);
    19. System.out.println(userEntity);
    20. System.out.println("-----------testNormal-----ent------");
    21. }
    22. }

    632cf757d3393bd50df41da98089239b.png

      查看实现类

    可以看到,在实现类中Mapstruct帮我们new了一个AttributeConvertUtil的对象,并调用了该对象的jsonToObject方法,将字符串转成JSON,最终赋值给了UserEntity的attributes属性,实现很简单,也是我们可以猜到的。

    c8326aaaece9e17def55c8fcfef36a20.png

    性能对比

    代码很简单,循环的创建UserPo对象,使用两种方式,转换成UserEntity对象,最终输出两种方式的执行耗时。可以加减属性或者修改转换次数,对比不同场景下的执行耗时。

    1. public static void testTime() {
    2. System.out.println("-----------testTime-----start------");
    3. int times = 50000000;
    4. final long springStartTime = System.currentTimeMillis();
    5. for (int i = 0; i < times; i++) {
    6. UserPo userPo = UserPo.builder()
    7. .id(1L)
    8. .gmtCreate(new Date())
    9. .buyerId(666L)
    10. .userNick("测试123")
    11. .userVerified("ok")
    12. .build();
    13. UserEntity userEntity = new UserEntity();
    14. BeanUtils.copyProperties(userPo, userEntity);
    15. }
    16. final long springEndTime = System.currentTimeMillis();
    17. for (int i = 0; i < times; i++) {
    18. UserPo userPo = UserPo.builder()
    19. .id(1L)
    20. .gmtCreate(new Date())
    21. .buyerId(666L)
    22. .userNick("测试123")
    23. .userVerified("ok")
    24. .build();
    25. UserEntity userEntity = IPersonMapper.INSTANCT.po2entity(userPo);
    26. }
    27. final long mapstructEndTime = System.currentTimeMillis();
    28. System.out.println("BeanUtils use time=" + (springEndTime - springStartTime) / 1000 + "秒" +
    29. "; Mapstruct use time=" + (mapstructEndTime - springEndTime) / 1000 + "秒");
    30. System.out.println("-----------testTime-----end------");
    31. }

    b89030eb982f2f9e30e10619997db254.png

    总结

    通过本次调研,Mapstruct的高性能是毋庸置疑的,这也是我选择使用他的根本原因。在使用方式上和BeanUtils对比,Mapstruct需要创建mapper接口和自定义转换工具类,其实上手成本并不高,但是我们换取了高性能,这是非常值得的,所以强烈推荐大家使用Mapstruct,是时候和BeanUtils说再见了。

    保持好奇,不断探索,让程序更友好!

    团队介绍

    TMALL CAMPUS (天猫校园) 是阿里巴巴旗下重要的业务单元,天猫校园整合阿里巴巴大生态,将新理念、新技术、新业态、新模式落地到校园,为师生提供多方位、多形态的服务,协助高校后勤服务升级;致力于打造购物、学习、生活、实践为一体的校园生活新方式,实现校园商业的服务育人。
    天猫校园,让校园学习生活更美好

    ✿  拓展阅读

    8f1aec5f834eba868dccd592e35504e4.jpeg

    28df8007fc3490f8a05a8575e3218aab.jpeg

    作者|蒋政印(不习)

    编辑|橙子君

    e0a2a5f23420a1a86ae329369fcdcd58.png

  • 相关阅读:
    python---设计模式(单例模式和工厂模式)
    数据结构与算法:堆排序和TOP-K问题
    进程概念(跑路人笔记)
    内功心法:深入研究整型数(下)
    无涯教程-JavaScript - MATCH函数
    ADworld reverse wp easyre-153
    【Mac】KeyKey — Typing Practice for mac软件介绍及安装
    Spark RDD、DataFrame和Dataset的区别和联系
    家居行业如何借助AI营销数智化转型?《2023 家居行业AI营销第一课(重庆站)》给你答案
    5.Maven实战 --- 坐标和依赖
  • 原文地址:https://blog.csdn.net/Taobaojishu/article/details/126653926