Skip to main content

Java常见的模型转换方法

· 11 min read
Zeffon Wu

在进行不同领域对象转换时,原对象和目标对象相同属性的类型不一样,所以对象转换时一些需要考虑的问题。 我在进行不同领域对象转换,一直都是用 BeanUtils.copyProperties()搭配 Set()使用的。听了张老师讲解之后,才知道方法如此之多。

转化方法

我们的原对象OrderDTO的内容如下:

{
"orderDate": 1570558718699,
"orderId": 201909090001,
"orderStatus": "CREATED",
"orderedProducts": [
{
"price": 799.990000000000009094947017729282379150390625,
"productId": 1,
"productName": "吉他",
"quantity": 1
},
{
"price": 30,
"productId": 2,
"productName": "变调夹",
"quantity": 1
}
],
"paymentType": "CASH",
"shopInfo": {
"shopId": 20000101,
"shopName": "慕课商铺"
},
"totalMoney": 829.990000000000009094947017729282379150390625,
"userInfo": {
"userId": 20100001,
"userLevel": 2147483647,
"userName": "张小喜"
}
}

期望转换后得到的目标对象OrderVO如下:

{
"orderDate": "2019-10-09 15:49:24.619",
"orderStatus": "CREATED",
"orderedProducts": [
{
"productName": "吉他",
"quantity": 1
},
{
"productName": "变调夹",
"quantity": 1
}
],
"paymentType": "CASH",
"shopName": "慕课商铺",
"totalMoney": "829.99",
"userName": "张小喜"
}

第 1 种:Get/Set 操作。

Get/Set 直接对对象 优点:直观、简单、处理速度快; 缺点:属性过多时,比较浪费表情,而且代码不简洁

第 2 种:FastJson

利用序列化和反序列化,这里我们采用先使用 FastJson 的 toJSONString 的方法将原对象序列化为字符串,再使用 parseObject 方法将字符串反序列化为目标对象。 缺点:属性转化后不符合预期的,属性名也不一致问题

使用方式:

// JSON.toJSONString将对象序列化成字符串,JSON.parseObject将字符串反序列化为OderVO对象
orderVO = JSON.parseObject(JSON.toJSONString(orderDTO), OrderVO.class);

结果:

// 目标对象
{
"orderDate": "1570558718699",
"orderId": 201909090001,
"orderStatus": "CREATED",
"orderedProducts": [
{
"productName": "吉他",
"quantity": 1
},
{
"productName": "变调夹",
"quantity": 1
}
],
"paymentType": "CASH",
"totalMoney": "829.990000000000009094947017729282379150390625"
}

可以看到

  1. 日期不符合我们的要求
  2. 金额也有问题
  3. 最严重的是,当属性名不一样时,不复制

第 3 种:Apache 工具包 PropertyUtils 工具类

缺点:属性类型不一样会报错,不能部分属性复制,得到的目标对象部分属性成功、部分失败

使用方式:

PropertyUtils.copyProperties(orderVO, orderDTO);

转换过程中报错

java.lang.IllegalArgumentException: Cannot invoke com.imooc.demo.OrderVO.setTotalMoney on bean class 'class com.imooc.demo.OrderVO' - argument type mismatch - had objects of type "java.math.BigDecimal" but expected signature "java.lang.String"

结果:

// 目标对象
{
"orderId": 201909090001
}

结论:

  1. 属性类型不一样时报错
  2. 不能部分属性复制
  3. 得到的目标对象部分属性成功(这点很要命,部分成功,部分失败!)

第 4 种:Apache 工具包 BeanUtils 工具类

缺点:属性转化后不符合预期的,属性名也不一致问题

使用方式:

BeanUtils.copyProperties(orderVO, orderDTO);

结果:

// 目标对象
{
"orderDate": "Wed Oct 09 02:36:25 CST 2019",
"orderId": 201909090001,
"orderStatus": "CREATED",
"orderedProducts": [
{
"price": 799.990000000000009094947017729282379150390625,
"productId": 1,
"productName": "吉他",
"quantity": 1
},
{
"price": 30,
"productId": 2,
"productName": "变调夹",
"quantity": 1
}
],
"paymentType": "CASH",
"totalMoney": "829.990000000000009094947017729282379150390625"
}

结论:

  1. 日期不符合要求
  2. 属性名不一样时不复制
  3. 目标对象中的商品集合变成了 DTO 的对象,这是因为 List 的泛型被擦除了,而且是浅拷贝,所以造成这种现象。

第 5 种:Spring 封装 BeanUtils 工具类

缺点:会出现属性丢失

使用方式:

/** 对象属性转换,忽略orderedProducts字段 */
BeanUtils.copyProperties(orderDTO, orderVO, "orderedProducts");

结果:

/** 目标对象 */
{
"orderId":201909090001
}

结论:

  1. 可以忽略部分属性
  2. 属性类型不同,不能转换
  3. 属性名称不同,不能转换

apache 的BeanUtils和 spring 的BeanUtils中拷贝方法的原理都是先用 jdk 中 java.beans.Introspector类的getBeanInfo()方法获取对象的属性信息及属性 get/set 方法,接着使用反射(Methodinvoke(Object obj, Object... args))方法进行赋值。

第 6 种:BeanCopier

cglib 工具包的BeanCopier采用了不同的方法:它不是利用反射对属性进行赋值,而是直接使用 ASM 的MethodVisitor直接编写各属性的get/set方法生成 class 文件,然后进行执行。

优点:字节码技术,速度快,自定义地处理的属性,其他未处理的属性就不行,提供自己自定义转换逻辑的方式 缺点:转换逻辑自己写,比较复杂,繁琐;属性名称相同,类型不同,不会拷贝(原始类型和包装类型也被视为类型不同)

使用方式:

// 构造转换器对象,最后的参数表示是否需要自定义转换器
BeanCopier beanCopier = BeanCopier.create(orderDTO.getClass(), orderVO.getClass(), true);

// 转换对象,自定义转换器处理特殊字段
beanCopier.copy(orderDTO, orderVO, (value, target, context) -> {
// 原始数据value是Date类型,目标类型target是String
if (value instanceof Date) {
if ("String".equals(target.getSimpleName())) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return sdf.format(value);
}
}
// 未匹配上的字段,原值返回
return value;
});

报错

java.lang.ClassCastException: com.imooc.demo.OrderStatus cannot be cast to java.lang.String

结果:

// 目标对象
{
"orderDate":"2019-10-09 03:07:13.768",
"orderId":201909090001
}

结论:

  1. 字节码技术,速度快
  2. 提供自己自定义转换逻辑的方式
  3. 转换逻辑自己写,比较复杂,繁琐
  4. 属性名称相同,类型不同,不会拷贝(原始类型和包装类型也被视为类型不同)

第 7 种:Dozer 框架

使用以上类库虽然可以不用手动编写get/set方法,但是他们都不能对不同名称的对象属性进行映射。在定制化的属性映射方面做得比较好的有 Dozer,Dozer 支持简单属性映射、复杂类型映射双向映射隐式映射以及递归映射。可使用xml或者注解进行映射的配置,支持自动类型转换,使用方便。但Dozer底层是使用 reflect 包下 Field 类的 set(Object obj, Object value)方法进行属性赋值,执行速度上不是那么理想。

使用方式:

// 创建转换器对象,强烈建议创建全局唯一的,避免不必要的开销
DozerBeanMapper mapper = new DozerBeanMapper();

// 加载映射文件
mapper.addMapping(TransferTest.class.getResourceAsStream("/mapping.xml"));

// 转换
orderVO = mapper.map(orderDTO, OrderVO.class);

结果:

// 目标对象
{
"orderDate": "2019-10-09 15:49:24.619",
"orderStatus": "CREATED",
"orderedProducts": [
{
"productName": "吉他",
"quantity": 1
},
{
"productName": "变调夹",
"quantity": 1
}
],
"paymentType": "CASH",
"shopName": "慕课商铺",
"totalMoney": "829.99",
"userName": "张小喜"
}

配置的字段映射文件:

<mappings xmlns="http://dozer.sourceforge.net"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozer.sourceforge.net
http://dozer.sourceforge.net/schema/beanmapping.xsd">

<!-- 一组类映射关系 -->
<mapping>
<!-- 类A和类B -->
<class-a>com.imooc.demo.OrderDTO</class-a>
<class-b>com.imooc.demo.OrderVO</class-b>

<!-- 一组需要映射的特殊属性 -->
<field>
<a>shopInfo.shopName</a>
<b>shopName</b>
</field>

<!-- 将嵌套对象中的某个属性值映射到目标对象的指定属性上 -->
<field>
<a>userInfo.userName</a>
<b>userName</b>
</field>

<!-- 将Date对象映射成指定格式的日期字符串 -->
<field>
<a>orderDate</a>
<b date-format="yyyy-MM-dd HH:mm:ss.SSS">orderDate</b>
</field>

<!-- 自定义属性转化器 -->
<field custom-converter="com.imooc.demo.DozerCustomConverter">
<a>totalMoney</a>
<b>totalMoney</b>
</field>

<!-- 忽略指定属性 -->
<field-exclude>
<a>orderId</a>
<b>orderId</b>
</field-exclude>
</mapping>
</mappings>

自定义转换器:

public class DozerCustomConverter implements CustomConverter {

@Override
public Object convert(Object destination, Object source, Class<?> destClass, Class<?> sourceClass) {
// 如果原始属性为BigDecimal类型
if (source instanceof BigDecimal) {
// 目标属性为String类型
if ("String".equals(destClass.getSimpleName())) {
return String.valueOf(((BigDecimal) source).doubleValue());
}
}
return destination;
}
}

结论:

  1. 支持多种数据类型自动转换(双向的)
  2. 支持不同属性名之间转换
  3. 支持三种映射配置方式(注解方式,API 方式,XML 方式)
  4. 支持配置忽略部分属性
  5. 支持自定义属性转换器
  6. 嵌套对象深拷贝

第八种:MapStruct 框架:

基于 JSR269 的 Java 注解处理器,通过注解配置映射关系,在编译时自动生成接口实现类。类似于 Lombok 的原理一样。

第九种:Orika 框架:

支持在代码中注册字段映射,通过 javassist 类库生成 Bean 映射的字节码,之后直接加载执行生成的字节码文件。

第十种:ModelMapper 框架:

基于反射原理进行赋值或者直接对成员变量赋值。

总结

介绍的这些转换方法中,在性能上基本遵循:手动赋值 > cglib > 反射 > Dozer > 序列化。

在实际项目中,需要综合使用上述方法进行模型转换。 比如较低层的 DO,因为涉及到的嵌套对象少,改动也少,所以可以使用 BeanUtils 直接转。 如果是速度、稳定优先的系统,还是简单粗暴地使用 Set、Get 实现吧。

文献参考

  • 本篇学习于慕课网-张小喜老师手记