mapstruct为什么性能这么高

575 阅读6分钟

传统bean转换问题

随着应用程序变得越来越复杂,处理大量数据对象之间的转换成为一项常见任务。在处理这些转换时,性能往往是一个关键因素。

以三层系统为例:假设我们正在构建一个活动管理系统,其中有三个实体对象:Activity(活动)、ActivityDTO(活动的数据传输对象)和ActivityEntity(活动的持久化实体)。它们在不同的层中使用。 controller层代码

@RestController
@RequestMapping("/activities")
public class ActivityController {
  
  @Autowired
  private ActivityService activityService;
  
  @PostMapping
  public BaseResponse createActivity(@RequestBody ActivityDTO activityDTO) {
    // 将ActivityDTO转换为Activity对象
    Activity activity = convertToActivity(activityDTO);
    
    // 调用Service层创建活动
    activityService.createActivity(activity);
    
    return BaseResponse.SUCCESS();
  }
  
  // 将ActivityDTO转换为Activity对象的方法
  private Activity convertToActivity(ActivityDTO activityDTO) {
    Activity activity = new Activity();
    activity.setTitle(activityDTO.getTitle());
    activity.setDescription(activityDTO.getDescription());
    // 其他属性的转换...
    return activity;
  }
  
  // 将Activity转换为ActivityDTO对象的方法
  private ActivityDTO convertToActivityDTO(Activity activity) {
    ActivityDTO activityDTO = new ActivityDTO();
    activityDTO.setId(activity.getId());
    activityDTO.setTitle(activity.getTitle());
    // 其他属性的转换...
    return activityDTO;
  }
}

service层

@Service
public class ActivityService {
  
  @Autowired
  private ActivityRepository activityRepository;
  
  public void createActivity(Activity activity) {
    // 直接使用Repository层保存活动对象
    activityRepository.save(activity);
  }
}

Repository层

@Repository
public class ActivityRepository {
  @Autowired
  private ActivityMapper activityMapper;
  
  public void save(Activity activity){
      ActivityEntity activityentity = convertToActivityEntity(activity);
      activityMapper.insert(activityentity);
  }
  
  private ActivityEntity convertToActivityEntity(Activity activity) {
    ActivityEntity activityentity = new ActivityEntity();
    activityentity.setTitle(activity.getTitle());
    activityentity.setDescription(activity.getDescription());
    // 其他属性的转换...
    return activityentity;
  }
  // Repository层的其他方法...
}

经常看到上述对象拷贝的重复代码,这样编写代码有 3 类问题:

  • 代码设计不合理:对象拷贝代码分散到不同层级,遍布不同的角落,从代码设计的角度看来不合理,而且代码中大段的同名同类型字段的重复设置,代码不够整洁;

  • 开发枯燥又容易犯错:RD 编写大量同名同类型字段的拷贝代码很枯燥又容易犯错;

  • 可扩展性差、不易维护:类新增、修改、删除字段时,不仅需要修改 DTO、DO、ENTITY 等类定义,还需要确保散落在各层的对象拷贝方法都修改一遍,只要有任何一处遗漏或者字段设置错误就会出现问题。

为了简化对象拷贝的工作,我们使用过各种bean copy工具,下面是常用的一些工具性能对比:

数据来源这里参考了:www.jianshu.com/p/20a1c04e9…

Benchmark                              Mode  Cnt       Score       Error   Units
BeanCopyTest.apacheBeanUtilsCopyTest  thrpt    9     542.549 ±    43.868  ops/ms
BeanCopyTest.cglibBeanCopierTest      thrpt    9  250689.491 ±  3690.880  ops/ms
BeanCopyTest.getSetTest               thrpt    9  250695.090 ±  4487.843  ops/ms
BeanCopyTest.mapstructTest            thrpt    9  249712.148 ±  4686.468  ops/ms
BeanCopyTest.springBeanCopierTest     thrpt    9  238908.247 ± 37465.537  ops/ms
BeanCopyTest.springBeanUtilsCopyTest  thrpt    9   11528.375 ±   797.580  ops/ms

对比常用几种bean copy得出如下结论:

  • 通过Set/Get、cglib bean copier、mapstruct进行属性copy,其性能基本持平
  • spring bean包中的bean utils与apache common-beanutils的bean utils的copy,性能比较差。

MapStruct

简介

MapStruct 是一个对象映射库,旨在提供与手动对象拷贝相当的性能。它被设计用于支持不同的业务系统,特别是对执行速度要求高的 API 系统。

主要特性

MapStruct 提供以下特性,以简化对象映射并减少代码重复:

  • 性能优化:MapStruct 致力于实现与手动对象拷贝相当的性能,适用于对执行速度敏感的 API 系统。
  • 简化代码:MapStruct 提供易于使用的 API,大大减少了重复代码的编写量。
  • 多样化的对象映射:MapStruct 支持各种对象映射场景,包括自动拷贝相同命名属性、隐式拷贝不同命名属性、嵌套属性和扁平属性的映射、自定义转换逻辑以及字段忽略等功能。

MapStruct 原理

MapStruct 采用两种方式进行对象映射:命令式编程和声明式编程。在 Java 中,声明式编程常常通过注解实现,注解提供了更简洁的方式来实现相同的功能。

MapStruct 利用注解处理器、字节码增强和反射等技术来实现对象映射:

  • 注解处理器:利用 Java 编译器提供的注解处理工具(APT),MapStruct 可以在编译期扫描和处理注解。这使得它可以生成新的 Java 文件或修改抽象语法树(AST)和字节码文件。
  • 字节码增强:字节码增强指的是在运行时修改或生成全新的字节码文件的技术。常用的支持字节码增强的框架包括 Cglib 和 Javassist。
  • 反射:反射是指在运行时获取类的 Class 对象,创建类的实例,以及获取类的属性、方法和构造器等完整结构。

尽管这三种技术都可以实现对象映射,但它们的性能有所不同:

  • 注解处理器技术在编译期处理注解并生成代码,因此在运行时可以提供与手写对象拷贝相当的性能。
  • 字节码增强技术在运行时修改字节码,会带来一定的性能开销,但性能仍优于反射。
  • 反射涉及动态类型解析、自动装箱拆箱操作、方法可见性检查和参数校验等,因此运行时性能较差。JavaDoc 建议在对性能要求较高的系统中避免频繁使用反射。

MapStruct 使用注解处理器技术实现对象映射:

  1. 编译时生成映射代码:MapStruct在编译时生成优化的Java代码来实现对象映射。这种静态代码生成的方式消除了运行时的反射开销,提高了转换的效率。生成的代码是类型安全的,不会引入潜在的运行时错误。因此,MapStruct能够在运行时提供快速而可靠的对象映射,实现卓越的性能表现。
  2. 基于注解的配置:MapStruct通过注解来配置对象之间的映射关系。开发人员只需在需要映射的字段或方法上添加相应的注解,MapStruct就能够自动为其生成转换逻辑。这种基于注解的配置方式简化了转换代码的编写,并且提高了开发效率。同时,MapStruct的注解处理器能够在编译时进行静态检查,确保映射配置的正确性,进一步提高了性能和可靠性。
  3. 优化的转换算法:MapStruct使用一系列优化算法来提高转换的效率。例如,它能够在转换过程中进行字段映射的缓存,避免重复的计算。此外,MapStruct还支持高级特性,如集合映射、嵌套映射和条件映射等,使得复杂对象之间的转换变得简单而高效。

总结

MapStruct之所以拥有卓越的性能表现,主要得益于其编译时生成的映射代码、基于注解的配置、优化的转换算法等关键因素。相较于手动转换和其他常见的对象映射框架,MapStruct在性能上具备显著的优势。它不仅提供了高效的转换功能,还能减少开发人员的工作量,提高开发效率。因此,MapStruct成为开发人员首选的对象映射框架之一,为复杂数据对象之间的转换提供了可靠而高效的解决方案。