提高生产力效率-实体映射工具推荐

前言

声明:

1、DO(业务实体对象),DTO(数据传输对象)。

在一个成熟的工程中,尤其是现在的分布式系统中,应用与应用之间,还有单独的应用细分模块之后,DO 一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO 用于对象传输,也即是 DO 对象对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。

  • 在我们对外暴露的Dubbo接口,一般这样定义接口类
1
2
3
4
5
6
7
8
/**
* 获取营销信息
*
* @param hallId 场馆编号
* @param modelCode 车型代码
* @return
*/
BrandMarketInfoDTO getBrandMarketInfo(String hallId, String modelCode);
  • 在和数据持久层映射实现的接口中,一般我们会这么写
1
2
3
4
5
6
7
8
/**
* 获取水牌营销信息
*
* @param hallId
* @param modelCode
* @return
*/
HallCarManageDO getBoardMarketInfo(@Param("hallId") String hallId, @Param("modelCode") String modelCode);

但是我们HallCarManageDO和BrandMarketInfoDTO中的属性和属性类型不是相等的。这种 对象与对象之间的互相转换,就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都 get/set 会很麻烦。

常见工具类及实现

以下列举被广大开发工程师常用的Bean属性复制工具

  • Spring.BeanUtils
  • Cglib.BeanCopier
  • MapStruct

以下选取属性赋值的功能来对比每个工具类的不同

BeanUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import org.springframework.beans.BeanUtils;

/**
* @author james mu
* @date 2019/10/22
*/
public class PojoUtils {

/**
*
* @param source 源对象
* @param clazz 目标对象
*
* @return 复制属性后的对象
*/
public static <T> T copyProperties(Object source, Class<T> clazz) {
if (source == null) {
return null;
}
T target;
try {
target = clazz.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException("通过反射创建对象失败");
}
BeanUtils.copyProperties(source, target);
return target;
}

}

springframework的BeanUtils也是通过java内省机制获取getter/setter,然后通过反射调用从而实现属性复制。

BeanCopier

依赖引用

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency>

工具类实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import net.sf.cglib.beans.BeanCopier;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* @author james mu
* @date 2019/10/22
*/
public class BeanCopierUtils {

/**
* BeanCopier拷贝速度快,性能瓶颈出现在创建BeanCopier实例的过程中。
* 所以,把创建过的BeanCopier实例放到缓存中,下次可以直接获取,提升性能
*/
public static Map<String, BeanCopier> beanCopierMap = new ConcurrentHashMap<String, BeanCopier>();

/**
* cp 对象赋值
*
* @param source 源对象
* @param target 目标对象
*/
public static void copyProperties(Object source, Object target) {
String beanKey = generateKey(source.getClass(), target.getClass());
BeanCopier copier = null;
if (!beanCopierMap.containsKey(beanKey)) {
copier = BeanCopier.create(source.getClass(), target.getClass(), false);
beanCopierMap.put(beanKey, copier);
} else {
copier = beanCopierMap.get(beanKey);
}
copier.copy(source, target, null);
}

private static String generateKey(Class<?> class1, Class<?> class2) {
return class1.toString() + class2.toString();
}
}
  1. 使用动态代理,生成字节码类,再通过Java反射成Class,调用其copy方法。

  1. 大家可以看到这里用到了ConcurrentHashMap存取copier,因为BeanCopier.create使用了缓存,该过程也消耗资源,建议全局只初始化一次。

自定义转换器

  1. 支持自定义转换器。

MapStruct

MapSturct 是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

抓一下重点:

  1. 注解处理器
  2. 可以生成 JavaBean 之间那的映射代码
  3. 类型安全, 高性能, 无依赖性

从字面的理解, 我们可以知道, 该工具可以帮我们实现 JavaBean 之间的转换, 通过注解的方式。

同时, 作为一个工具类,相比于手写, 其应该具有便捷, 不容易出错的特点。

入门

依赖引用

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.0.Final</version>
<scope>provided</scope>
</dependency>

案例演示

UserDO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import lombok.Data;
import lombok.ToString;

import java.util.Date;

/**
* @author james mu
* @date 2019/10/22
*/
@Data
@ToString
public class UserDO {
private String name;
private String password;
private Integer age;
private Date birthday;
private String sex;
}

UserDTO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import lombok.Data;
import lombok.ToString;


/**
* @author james mu
* @date 2019/10/22
*/
@Data
@ToString
public class UserDTO {
private String name;
private String age;
private String birthday;
private String gender;
}

UserConvertUtils

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import com.sanshengshui.javabeanconvert.DO.UserDO;
import com.sanshengshui.javabeanconvert.DTO.UserDTO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
* @author james mu
* @date 2019/10/22
*/
@Mapper
public interface UserConvertUtils {

UserConvertUtils INSTANCE = Mappers.getMapper(UserConvertUtils.class);

/**
* 类型转换
*
* @param userDO UserDO数据持久层类
* @return 数据传输类
*/
@Mappings({
@Mapping(target = "gender", source = "sex")
})
UserDTO doToDTO(UserDO userDO);
}
  1. DTO与DO中属性名相同时候默认映射,(比如name),属性名相同属性类型不同也会映射,(比如birthday,一个Data,一个String)
  2. DTO与DO中属性名不同的,需要通过@Mapping明确关系来形成映射(如sex对应gender)
  3. 无映射关系属性被忽略(如UserEntity的password)

结果如下:

1
2
3
UserDO(name=snow, password=123, age=20, birthday=Tue Oct 22 17:10:19 CST 2019, sex=男)
+-+-+-+-+-+-+-+-+-+-+-
UserDTO(name=snow, age=20, birthday=19-10-22 下午5:10, gender=男)

MapStruct分析

上面中, 我写了3个步骤来实现了从 UserDTOUserDO 的转换。

那么, 作为一个注解处理器, 通过MapStruct 生成的代码具有怎么样的优势呢?

高性能

Java反射原理和反射低的原因:https://juejin.im/post/5da33b2351882509334fc0d3

这是相对反射来说的, 反射需要去读取字节码的内容, 花销会比较大。 而通过 MapStruct 来生成的代码, 其类似于人手写。 速度上可以得到保证。

前面例子中生成的代码可以在编译后看到。 在 target/generated-sources/annotations 里可以看到。

对应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2019-10-22T17:10:17+0800",
comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_222 (Azul Systems, Inc.)"
)
public class UserConvertUtilsImpl implements UserConvertUtils {

@Override
public UserDTO doToDTO(UserDO userDO) {
if ( userDO == null ) {
return null;
}

UserDTO userDTO = new UserDTO();

userDTO.setGender( userDO.getSex() );
userDTO.setName( userDO.getName() );
if ( userDO.getAge() != null ) {
userDTO.setAge( String.valueOf( userDO.getAge() ) );
}
if ( userDO.getBirthday() != null ) {
userDTO.setBirthday( new SimpleDateFormat().format( userDO.getBirthday() ) );
}

return userDTO;
}
}

可以看到其生成了一个实现类, 而代码也类似于我们手写, 通俗易懂。

性能比较

测试在两个简单的Bean之间转换的耗时,执行次数分别为10、100、1k、10k、100k,时间单位为ms。

总结

虽然反射效率低,但这个时间是很小很小的。根据不同工具的性能及功能维度,个人建议当对象转换操作较少或者应用对性能要求较高时,尽量不采用工具,而是手写getter/setter;在不考虑性能的情况下,普通的对象转换可以使用Cglib.BeanCopier,复杂的对象转换使用MapStruct。

给辛苦的作者点杯咖啡☕️!