在日常开发中,对象赋值是常见的需求,通常我们会调用对象的 set/get 方法。当需要转换的两个对象属性大致相同,我们可能会考虑使用属性拷贝工具。许多博客分析了各种属性拷贝工具,其中许多推荐使用 MapStruct,主要因为其高效率,几乎等同于直接使用 set/get 方法。但是我个人并不推荐使用 MapStruct,本文将详细解释原因。

MapStruct 是什么

MapStruct 是一个代码生成器,它简化了 Java 应用程序中对象之间的映射/转换。以下是一个简单的使用示例:

假设有以下几个类:

public class Customer {
    private String name;
    private Address address;
    // getters and setters
}

public class Address {
    private String street;
    private String city;
    // getters and setters
}

public class CustomerDTO {
    private String name;
    private String street;
    private String city;
    // getters and setters
}

我们想要将 Customer 对象转换为 CustomerDTO 对象,其中 Address 的属性需要展平到 CustomerDTO 中。同时,我们想要将 Customername 转换为大写。

我们可以创建一个 CustomerMapper 接口,如下所示:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

@Mapper
public interface CustomerMapper {
    CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);

    @Mappings({
        @Mapping(source = "address.street", target = "street"),
        @Mapping(source = "address.city", target = "city"),
        @Mapping(source = "name", target = "name", qualifiedByName = "toUpperCase")
    })
    CustomerDTO customerToCustomerDTO(Customer customer);

    @Named("toUpperCase")
    default String toUpperCase(String name) {
        return name.toUpperCase();
    }
}

在这个接口中,我们使用 @Mappings@Mapping 注解来定义复杂的映射规则。我们还定义了一个 toUpperCase 方法来实现自定义的类型转换。

然后,我们就可以在代码中使用 CustomerMapper 了:

Customer customer = new Customer();
customer.setName("John Doe");
Address address = new Address();
address.setStreet("123 Main St");
address.setCity("Springfield");
customer.setAddress(address);

CustomerDTO customerDTO = CustomerMapper.INSTANCE.customerToCustomerDTO(customer);

MapStruct 原理分析

MapStruct 的原理其实比较简单,就是基于 APT(Annotation Processing Tool)。

APT是Java的一个工具,它可以在编译时扫描和处理注解。MapStruct就是使用APT来处理@Mapper注解,并生成相应的映射代码。

当你在接口或抽象类上使用@Mapper注解,并且编译你的项目时,APT会调用MapStruct的注解处理器。然后,MapStruct的注解处理器会分析这个接口或抽象类,找出所有的映射方法,并为每个映射方法生成实现代码。

这种在编译时生成代码的方式,使得MapStruct的运行效率非常高,因为所有的映射逻辑都已经在编译时确定,运行时不需要进行任何反射或动态代理。

比如在上文的例子中,MapStruct 生成了一个 mapper 接口的实现类:

在这里插入图片描述

为什么我不推荐使用 MapStruct

下图是在网上找到的对十一种属性转换操作的性能时间对比:

在这里插入图片描述

get_setSpring BeanUtilsMapStruct
一百次040
一千次060
一万次1171
十万次4683
一百万次822615

从上述数据可以看出:

  • MapStruct的性能表现出色,与直接使用set/get方法相比几乎没有差距。
  • Spring的BeanUtils虽然稍慢,但这种微小的差距对系统运行影响微乎其微。

然而,正是这种微不足道的性能差异,导致许多人选择使用MapStruct。尽管MapStruct在业务代码中的转换非常简单,但它需要新增一个Mapper接口,而且接口中的逻辑并不简单,对于不熟悉MapStruct的开发者来说,这增加了使用成本。大多数Java Web开发都会使用Spring,而为了使用MapStruct,我们放弃了无需添加任何外部依赖就可以直接使用的Spring BeanUtils,反而增加了MapStruct的依赖,使项目变得更加庞大。

MapStruct的高速原理类似于我们为了解决MySQL查询速度慢的问题而添加缓存,这就需要考虑缓存预热、缓存与数据库的一致性等问题。为了解决一个可能并不那么重要的性能问题,我们反而使架构变得更复杂。

这让我想起了计算机编程领域的一句经典名言:“过早优化是万恶之源”:

在这里插入图片描述

这句话的含义有两层:

  1. 关注点不当:在软件开发的早期,更重要的是关注功能的实现和整体架构,而不是过度关注性能优化。过早地陷入优化细节可能会使开发者失去对整体结构和设计的关注,从而影响最终的软件质量。
  2. 资源浪费:早期的优化往往是基于假设和预测进行的,这可能导致资源在不必要的地方被浪费。在软件的实际使用情况变得明确之前,进行过早的优化可能会导致资源被用在了不必要的地方,而不是真正需要优化的地方。

这与本文的观点相吻合,我们为了一个“没有很大意义”的性能优化(关注点不当),引入了一个依赖,而且增加了代码的复杂度和其他同学的学习成本,甚至于可能引出其他的坑,比如经典的与Lombok冲突问题,这又需要我们花费时间去处理(资源浪费)。

而提到性能,我又想起了一位业界大佬说的一句话“性能在大规模工程化的时候,或者技术成为流行趋势的时候,永远不是第一顺位的选择”。

拿编程语言来说,Java/Golang 一般认为没有Rust/C++快,如果想更快可以直接ASM。但是实际上90%的业务系统,特别是大规模业务系统基本上都是Java来实现的。大型银行核心业务之前的技术栈都是COBOL或C,目前都在转Java了。证券保险类的业务也是这个大趋势。为什么这样呢,一般情况高性能的东西会具有更大的复杂度,一个大型系统如果用汇编语言来写,复杂度和协作程度要严重很多倍,生产率就下降了。

拿出门旅行来说,飞机比较快,战斗机更快,目前还是火车/大巴/自驾为主要方式。开车比电动车和自行车快,但是我在小区附近活动的时候,自行车还是最佳方式。为啥,足够用,足够灵活,性价比高,这往往是比性能更重要的指标。

还是回到那句话,“脱离场景谈性能都是耍流氓”。

要注意的是,我个人虽然不推荐使用 MapStruct,但并不代表我不关注它的原理,反而这个框架的实现原理是值得我们学习的。

总结

本文深入探讨了MapStruct的性能和使用成本。虽然MapStruct在性能上表现出色,但其引入的复杂度和学习成本可能会超过其性能优势带来的收益。在大规模工程化或技术成为流行趋势的情况下,性能并非首要考虑因素,更重要的是实用性、灵活性和性价比。因此,我们需要根据具体的场景和需求来选择最合适的工具,而不是盲目追求性能。要注意,即使我们选择不使用某个工具,但也可以从其设计和实现中学习有价值的知识。

References

  • https://blog.csdn.net/qq_41692766/article/details/122491333
  • https://www.zhihu.com/question/270355472/answer/3238160454

欢迎关注公众号:
在这里插入图片描述

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐