Java对象转换方案分析与mapstruct实践

简介: 随着系统模块分层不断细化,在Java日常开发中不可避免地涉及到各种对象的转换,如:DO、DTO、VO等等,编写映射转换代码是一个繁琐重复且还易错的工作,一个好的工具辅助,减轻了工作量、提升开发工作效率的同时还能减少bug的发生

image.png

作者 | 久贤
来源 | 阿里技术公众号

一 前言

随着系统模块分层不断细化,在Java日常开发中不可避免地涉及到各种对象的转换,如:DO、DTO、VO等等,编写映射转换代码是一个繁琐重复且还易错的工作,一个好的工具辅助,减轻了工作量、提升开发工作效率的同时还能减少bug的发生。

二 常用方案及分析

1 fastjson

CarDTO entity = JSON.parseObject(JSON.toJSONString(carDO), CarDTO.class);

这种方案因为通过生成中间json格式字符串,然后再转化成目标对象,性能非常差,同时因为中间会生成json格式字符串,如果转化过多,gc会非常频繁,同时针对复杂场景支持能力不足,基本很少用。

2 BeanUtil类

BeanUtil.copyProperties()结合手写get、set,对于简单的转换直接使用BeanUtil,复杂的转换自己手工写get、set。该方案的痛点就在于代码编写效率低、冗余繁杂还略显丑陋,并且BeanUtil因为使用了反射invoke去赋值性能不高。

只能适合bean数量较少、内容不多、转换不频繁的场景。

apache.BeanUtils

org.apache.commons.beanutils.BeanUtils.copyProperties(do, entity);

这种方案因为用到反射的原因,同时本身设计问题,性能比较差。集团开发规约明确规定禁止使用。

image.png

spring.BeanUtils

org.springframework.beans.BeanUtils.copyProperties(do, entity);

这种方案针对apache的BeanUtils做了很多优化,整体性能提升不少,不过还是使用反射实现比不上原生代码处理,其次针对复杂场景支持能力不足。

image.png

3 beanCopier

BeanCopier copier = BeanCopier.create(CarDO.class, CarDTO.class, false); 
copier.copy(do, dto, null);

这种方案动态生成一个要代理类的子类,其实就是通过字节码方式转换成性能最好的get和set方式,重要的开销在创建BeanCopier,整体性能接近原生代码处理,比BeanUtils要好很多,尤其在数据量很大时,但是针对复杂场景支持能力不足。

4 各种Mapping框架

分类

Object Mapping 技术从大的角度来说分为两类,一类是运行期转换,另一类则是编译期转换:

  • 运行期反射调用 set/get 或者是直接对成员变量赋值。这种方式通过invoke执行赋值,实现时一般会采用beanutil, Javassist等开源库。运行期对象转换的代表主要是Dozer和ModelMaper。
  • 编译期动态生成 set/get 代码的class文件,在运行时直接调用该class的 set/get 方法。该方式实际上仍会存在 set/get 代码,只是不需要开发人员自己写了。这类的代表是:MapStruct,Selma,Orika。

分析

  • 无论哪种Mapping框架,基本都是采用xml配置文件 or 注解的方式供用户配置,然后生成映射关系。
  • 编译期生成class文件方式需要DTO仍然有set/get方法,只是调用被屏蔽;而运行期反射方式在某些直接填充 field的方案中,set/get代码也可以省略。
  • 编译期生成class方式会有源代码在本地,方便排查问题。
  • 编译期生成class方式因为在编译期才出现java和class文件,所以热部署会受到一定影响。
  • 反射型由于很多内容是黑盒,在排查问题时,不如编译期生成class方式方便。参考GitHub上工程java-object-mapper-benchmark可以看出主要框架性能比较。
  • 反射型调用由于是在运行期根据映射关系反射执行,其执行速度会明显下降N个量级。
  • 通过编译期生成class代码的方式,本质跟直接写代码区别不大,但由于代码都是靠模板生成,所以代码质量没有手工写那么高,这也会造成一定的性能损失。

image.png

综合性能、成熟度、易用性、扩展性,mapstruct是比较优秀的一个框架。

三 Mapstruct使用指南

1 Maven引入

f3ec68fd4f6c796b0c2e0ef5e8c600b.jpg

2 简单入门案例

DO和DTO

这里用到了lombok简化代码,lombok的原理也是在编译时去生成get、set等被简化的代码。

@Data 
public class Car {     
    private String make;     
    private int numberOfSeats;     
    private CarType type; 
}
@Data 
public class CarDTO {     
    private String make;     
    private int seatCount;     
    private String type; 
}

定义Mapper

@Mapper中描述映射,在编辑的时候mapstruct将会根据此描述生成实现类:

  • 当属性与其目标实体副本同名时,它将被隐式映射。
  • 当目标实体中的属性具有不同名称时,可以通过@Mapping注释指定其名称。
@Mapper 
public interface CarMapper {     
    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); }

使用Mapper

通过Mappers 工厂生成静态实例使用。

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

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}
Car car = new Car(...); 
CarDTO carDTO = CarMapper.INSTANCE.CarToCarDTO(car);

getMapper会去load接口的Impl后缀的实现类。

image.png

通过生成spring bean注入使用,Mapper注解加上spring配置,会自动生成一个bean,直接使用bean注入即可访问。

@Mapper(componentModel = "spring") 
public interface CarMapper {     
    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}

自动生成的MapperImpl内容

如果配置了spring bean访问会在注解上自动加上@Component。

image.png

3 进阶使用

逆向映射

如果是双向映射,例如 从DO到DTO以及从DTO到DO,正向方法和反向方法的映射规则通常是相似的,并且可以通过切换源和目标来简单地逆转。

使用注解@InheritInverseConfiguration 指示方法应继承相应反向方法的反向配置。

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

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car);    

    @InheritInverseConfiguration     
    Car CarDTOToCar(CarDTO carDTO); 
}

更新bean映射

有些情况下不需要映射转换产生新的bean,而是更新已有的bean。

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

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    void updateDTOFromCar(Car car, @MappingTarget CarDTO carDTO);

集合映射

集合类型(List,Set,Map等)的映射以与映射bean类型相同的方式完成,即通过在映射器接口中定义具有所需源类型和目标类型的映射方法。MapStruct支持Java Collection Framework中的多种可迭代类型。

生成的代码将包含一个循环,该循环遍历源集合,转换每个元素并将其放入目标集合。如果在给定的映射器或其使用的映射器中找到用于集合元素类型的映射方法,则将调用此方法以执行元素转换,如果存在针对源元素类型和目标元素类型的隐式转换,则将调用此转换。

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

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car);     

    List<CarDTO> carsToCarDtos(List<Car> cars);     

    Set<String> integerSetToStringSet(Set<Integer> integers);     

    @MapMapping(valueDateFormat = "dd.MM.yyyy")     
    Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source); 
}

编译时生成的实现类:

image.png

多个源参数映射

MapStruct 还支持具有多个源参数的映射方法。例如,将多个实体组合成一个数据传输对象。

在原案例新增一个Person对象,CarDTO中新增driverName属性,根据Person对象获得。

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

    @Mapping(source = "car.numberOfSeats", target = "seatCount")     
    @Mapping(source = "person.name", target = "driverName")     
    CarDTO CarToCarDTO(Car car, Person person); }

编译生成的代码:

image.png

默认值和常量映射

如果相应的源属性是null ,则可以指定默认值以将预定义值设置为目标属性。在任何情况下,都可以指定常量来设置这样的预定义值。默认值和常量被指定为字符串值。当目标类型是原始类型或装箱类型时,String 值将采用字面量,在这种情况下允许位/八进制/十进制/十六进制模式,只要它们是有效的文字即可。在所有其他情况下,常量或默认值会通过内置转换或调用其他映射方法进行类型转换,以匹配目标属性所需的类型。

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

    @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")     
    @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1")     
    @Mapping(target = "stringConstant", constant = "Constant Value")     
    @Mapping(target = "integerConstant", constant = "14")     
    @Mapping(target = "longWrapperConstant", constant = "3001")     
    @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014")     
    @Mapping(target = "stringListConstants", constant = "jack-jill-tom")     
    Target sourceToTarget(Source s); 
}

自定义映射方法或映射器

在某些情况下,可能需要手动实现 MapStruct 无法生成的从一种类型到另一种类型的特定映射。

可以在Mapper中定义默认实现方法,生成转换代码将调用相关方法:

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

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    @Mapping(source = "length", target = "lengthType")     
    CarDTO CarToCarDTO(Car car);     

    default String getLengthType(int length) {         
        if (length > 5) {             
            return "large";         
        } else {             
            return "small";         
        }     
    } 
}

也可以定义其他映射器,如下案例Car中Date需要转换成DTO中的String:

public class DateMapper {     
    public String asString(Date date) {         
        return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;     
    }     

    public Date asDate(String date) {         
        try {             
            return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).parse( date ) : null;         
        } catch ( ParseException e ) {             
            throw new RuntimeException( e );         
        }     
    } 
}
@Mapper(uses = DateMapper.class) 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}

编译生成的代码:

image.png

若遇到多个类似的方法调用时会出现模棱两可,需使用@qualifiedBy指定:

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

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    @Mapping(source = "length", target = "lengthType", qualifiedByName = "newStandard")     
    CarDTO CarToCarDTO(Car car);     

    @Named("oldStandard")     
    default String getLengthType(int length) {         
        if (length > 5) {             
            return "large";         
        } else {             
            return "small";         
        }     
    }     
    @Named("newStandard")     
    default String getLengthType2(int length) {         
        if (length > 7) {             
            return "large";         
        } else {             
            return "small";         
        }     
    } 
}

表达式自定义映射

通过表达式,可以包含来自多种语言的结构。

目前仅支持 Java 作为语言。例如,此功能可用于调用构造函数,整个源对象都可以在表达式中使用。应注意仅插入有效的 Java 代码:MapStruct 不会在生成时验证表达式,但在编译期间生成的类中会显示错误。

@Data 
@AllArgsConstructor 
public class Driver {     
    private String name;     
    private int age; 
}
@Mapper 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "car.numberOfSeats", target = "seatCount")     
    @Mapping(target = "driver", expression = "java( new com.alibaba.my.mapstruct.example4.beans.Driver(person.getName(), person.getAge()))")     
    CarDTO CarToCarDTO(Car car, Person person); 
} 

默认表达式是默认值和表达式的组合:

@Mapper( imports = UUID.class )
public interface SourceTargetMapper {     
    SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );     

    @Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )")     
    Target sourceToTarget(Source s); 
}

装饰器自定义映射

在某些情况下,可能需要自定义生成的映射方法,例如在目标对象中设置无法由生成的方法实现设置的附加属性。

实现起来也很简单,用装饰器模式实现映射器的一个抽象类,在映射器Mapper中添加注解@DecoratedWith指向装饰器类,使用时还是正常调用。

@Mapper 
@DecoratedWith(CarMapperDecorator.class) 
public interface CarMapper {     
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     

    @Mapping(source = "numberOfSeats", target = "seatCount")     
    CarDTO CarToCarDTO(Car car); 
}
public abstract class CarMapperDecorator implements CarMapper {     
    private final CarMapper delegate;     
    protected CarMapperDecorator(CarMapper delegate) {         
        this.delegate = delegate;     
    }     
    @Override     
    public CarDTO CarToCarDTO(Car car) {         
        CarDTO dto = delegate.CarToCarDTO(car);         
        dto.setMakeInfo(car.getMake() + " " + new SimpleDateFormat( "yyyy-MM-dd" ).format(car.getCreateDate()));         
        return dto;     
    } 
}
相关参考

https://mapstruct.org/

https://github.com/arey/java-object-mapper-benchmark

https://github.com/mapstruct/mapstruct-examples


技术公开课

Java高级编程

本课程共162课时,包含Java多线程编程、常用类库、IO编程、网络编程、类集框架、JDBC等实用开发技术,帮助同学们掌握系统提供的类库并熟练使用JavaDoc文档。同时考虑到对面向对象的理解以及常用类的设计模式,在课程讲解中还将进行源代码的使用分析与结构分析。

点击这里,快去学习吧~

相关文章
|
29天前
|
存储 Java 开发者
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
本文详细介绍了 Java 中 `toString()` 方法的重写技巧及其重要
49 10
【潜意识Java】深入详细理解分析Java中的toString()方法重写完整笔记总结,超级详细。
|
6天前
|
存储 Java
Java中判断一个对象是否是空内容
在 Java 中,不同类型的对象其“空内容”的定义和判断方式各异。对于基本数据类型的包装类,空指对象引用为 null;字符串的空包括 null、长度为 0 或仅含空白字符,可通过 length() 和 trim() 判断;集合类通过 isEmpty() 方法检查是否无元素;数组的空则指引用为 null 或长度为 0。
|
26天前
|
Java
Java快速入门之类、对象、方法
本文简要介绍了Java快速入门中的类、对象和方法。首先,解释了类和对象的概念,类是对象的抽象,对象是类的具体实例。接着,阐述了类的定义和组成,包括属性和行为,并展示了如何创建和使用对象。然后,讨论了成员变量与局部变量的区别,强调了封装的重要性,通过`private`关键字隐藏数据并提供`get/set`方法访问。最后,介绍了构造方法的定义和重载,以及标准类的制作规范,帮助初学者理解如何构建完整的Java类。
|
25天前
|
安全 Java
Object取值转java对象
通过本文的介绍,我们了解了几种将 `Object`类型转换为Java对象的方法,包括强制类型转换、使用 `instanceof`检查类型和泛型方法等。此外,还探讨了在集合、反射和序列化等常见场景中的应用。掌握这些方法和技巧,有助于编写更健壮和类型安全的Java代码。
38 17
|
29天前
|
Java 应用服务中间件 API
【潜意识Java】javaee中的SpringBoot在Java 开发中的应用与详细分析
本文介绍了 Spring Boot 的核心概念和使用场景,并通过一个实战项目演示了如何构建一个简单的 RESTful API。
38 5
|
29天前
|
人工智能 自然语言处理 搜索推荐
【潜意识Java】了解并详细分析Java与AIGC的结合应用和使用方式
本文介绍了如何将Java与AIGC(人工智能生成内容)技术结合,实现智能文本生成。
53 5
|
29天前
|
Java 数据库连接 数据库
【潜意识Java】深度分析黑马项目《苍穹外卖》在Java学习中的重要性
《苍穹外卖》项目对Java学习至关重要。它涵盖了用户管理、商品查询、订单处理等模块,涉及Spring Boot、MyBatis、Redis等技术栈。
77 4
|
3天前
|
Java 程序员 开发者
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
36 14
|
6天前
|
安全 Java 程序员
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
34 13
|
7天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。