无聊的笔记:之二(再来看看到底怎么高性能的使用BeanUtils)
简介
可以跳过直接看测试结果
现在开发一个系统,常常会用到各种各样的模式(MVC,MVP,MVVM...等等)。就算没有全用过,也至少听说过用MVC模式来开发系统。
这时候就会用到各种 领域模型 (大佬总是喜欢用这么高大上的名字,个人理解就是有特殊用途或者特殊命名规范的java类,比如:DO,VO,DTO...等等)。这些类在各个层中,当作参数传入,或者当作返回值返回。
常见的领域模型:
名称 | 描述 | |
---|---|---|
DTO (Data Transfer Object) | 数据传输对象 | 一般前端传输到后端的值可以封装成要给DTO对象,用过SpringMVC就知道@ReqeustBody 后面就是一个对象。或者在不同服务间传输数据,可以封装成一个DTO。 |
VO(Value Object/View Object) | 值对象/表现层对象 | 主要封装了前端页面显示需要的一些数据 |
AO(Application Object) | 应用对象 | 在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。(PS:不是很清除这个对象有什么必要。) |
BO(Business Object) | 业务对象 | 业务逻辑封装对象,可能包含多个对象。 |
PO(Persistent Object)/DO( Data Object) | 持久对象 /数据源对象 | 一般用于表示数据库表格中的一条数据,从数据库中取出的数据用这个对象表示。如:UserPO/UserDO |
POJO(plain ordinary java object) | 简单无规则 java 对象 | 就一个Java类,其实上面的几种都可以是一个POJO。 |
可能不同的地方有不同的领域模型规范。这里仅供参考。
这样就涉及到各种对象的属性拷贝(或者领域对象的转换)。比如 UserDTO -> UserVO , UserVO -> UserDTO。
当类的属性比较少的时候,我们可以这样:
/**
* 将 UserDTO转换成 UserVO
*/
public static UserVO userConvert(UserDTO userDTO){
UserVO userVO = new UserVO();
userVO.setUserName(userDTO.getUserName());
userVO.setAge(userDTO.getAge());
userVO.setAddress(userDTO.getAddress());
userVO.setSex(userDTO.isSex());
userVO.setProperties(userDTO.getProperties());
return userVO;
}
但是如果类的属性较多,就要写很多行的getter/setter的调用。而且这玩意写的到处都是也不好看。
这时候就可以用几个工具来帮我去掉这些“丑”代码。
常用的属性拷贝工具:
- org.apache.commons.beanutils.BeanUtils.copyProperties
- org.apache.commons.beanutils.PropertyUtils.copyProperties
- org.springframework.beans.BeanUtils.copyProperties
- org.springframework.cglib.beans.BeanCopier.copy
- org.mapstruct
当然可能还有其他的,这里不再多讨论
分析
可以跳过这里,直接看测试结果和结论。
为什么apache的工具包性能差
大家都说org.apache.commons.beanutils.BeanUtils.copyProperties
的性能差。
这里截取一段org.apache.commons.beanutils.BeanUtils
工具包里的一段代码来瞅瞅:
public class BeanUtilsBean {
//省略其他代码...
public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
if (dest == null) {
throw new IllegalArgumentException("No destination bean specified");
} else if (orig == null) {
throw new IllegalArgumentException("No origin bean specified");
} else {
if (this.log.isDebugEnabled()) {
this.log.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")");
}
int var5;
int var6;
String name;
Object value;
if (orig instanceof DynaBean) {
//省略代码...
} else if (orig instanceof Map) {
//省略代码
} else {
//省略代码
}
}
}
//省略其他代码...
}
可以看到上面的代码充斥着各种类型判断,如orig instanceof DynaBean
。还会有日志输出。。。。这些虽然功能很全,但是损耗了大量的效率。再加上使用的是反射,效率当然高不上去。
为什么Spring的BeanUtils比Apache的BeanUtils效率高。
Spring的BeanUtils也是使用的反射,但是没有很多的额外操作(比如上面的日志、类型检测等等)。所以效率会高很多。
为什么Cglib的BeanCopier效率高
对Cglib就不看源码了,我也讲不清。只能简单的描述一下。
BeanCopier是一种基于字节码的方式,其实就是在字节码层面生成了性能最好的get、set方式。
简单的说,就是Cglib帮你手写了vo.setValue(dto.getValue())
。所以性能很快,但是Cglib帮你创建这些东西是很浪费时间的,也就是调用BeanCopier beanCopier = BeanCopier.create(...);
这个方法会很费时,所以不要频繁的调用这个方法,最好把创建好的一个当作静态变量缓存起来,或者设计个单例模式。
为什么mapstruct效率高
我们在使用mapstruct的时候需要写一个映射接口。如下代码:
UserMapper.java
import org.mapstruct.Mapper;
@Mapper
public interface UserMapper {
UserVO toVO(UserDTO dto);
}
上面的代码是将
UserDTO
的属性复制成UserVO
。(详细的可以看下面的测试)
然后我们编译完成后看编译结果,也就是.class文件。
UserMapper.class
UserMapperImpl.class
可以看到我并没有编写UserMapperImpl.java
文件,却出现了UserMapperImpl.class
。这其实就是mapstruct帮我生成的实现类。
用IDEA(或者其他的能反编译字节码的工具)把这个class文件打开看看mapstruct到底生成了什么。
public class UserMapperImpl implements UserMapper {
public UserMapperImpl() {
}
public UserVO toVO(UserDTO dto) {
if (dto == null) {
return null;
} else {
UserVO userVO = new UserVO();
userVO.setUserName(dto.getUserName());
userVO.setAge(dto.getAge());
userVO.setSex(dto.isSex());
userVO.setAddress(dto.getAddress());
userVO.setProperties(dto.getProperties());
return userVO;
}
}
}
这就是mapstruct帮我们在编译器生成的代码。跟手写getter/setter一样了,能达到跟手写一样的效率。(NB)
mapstruct和 Cglib BeanCopier
这两个工具都是帮我们自动生成代码,不过前者是在编译期生成,后者是在程序运行的时候当调用了BeanCopier.create(...)
才生成。
- mapstruct需要额外的写接口,例子中的UserMapper.java。
- Cglib BeanCopier需要调用方法去创建。(避免多次的调用create方法)。
测试环境
- 系统:windows10 x64
- 处理器:AMD Ryzen 3
- 内存:16G
- 台式电脑
- JDK1.8
测试代码
- UserDTO.java
public class UserDTO {
private String userName;
private int age;
private boolean sex; //性别:true = 男;false = 女;
private String address ;
private Object properties;//其他属性
//省略 getter/setter
}
- UserVO.java
public class UserVO {
private String userName;
private int age;
private boolean sex; //性别:true = 男;false = 女;
private String address ;
private Object properties;//其他属性
//省略 getter/setter
}
- UserMapper.java
mapstruct复制属性跟其他几个工具包不一样需要手动编写一个转换接口,在编译器会自动实现接口中的转换方法。
import org.mapstruct.Mapper;
@Mapper
public interface UserMapper {
UserVO toVO(UserDTO dto);
}
- Tester.java
测试代码
public class Tester {
private final int count = 10000000;//转换次数
private UserDTO getDTO(){
UserDTO userDTO = new UserDTO();
userDTO.setUserName("bob");
userDTO.setAge(18);
userDTO.setAddress("china");
userDTO.setSex(true);
return userDTO;
}
/**
* 手动
*/
@Test
public void manual(){
UserDTO dto = getDTO();
long startTime = System.currentTimeMillis();
List<UserVO> vos = new ArrayList<UserVO>(count);
for(int i = 0 ; i < count ; ++i){
UserVO userVO = new UserVO();
userVO.setUserName(dto.getUserName());
userVO.setAge(dto.getAge());
userVO.setAddress(dto.getAddress());
userVO.setSex(dto.isSex());
userVO.setProperties(dto.getProperties());
vos.add(userVO);
}
System.out.println("用时:"+(System.currentTimeMillis() - startTime));
}
/**
* 测试 mapstruct
*/
@Test
public void mapstruct()throws Exception{
UserMapper struct = Mappers.getMapper(UserMapper.class);
UserDTO dto = getDTO();
long startTime = System.currentTimeMillis();
List<UserVO> vos = new ArrayList<UserVO>(count);
for(int i = 0 ; i < count ; ++i){
//mapstruct
UserVO vo = struct.toVO(dto);
vos.add(vo);
}
System.out.println("用时:"+(System.currentTimeMillis() - startTime));
}
/**
* 测试 BeanCopier
*/
@Test
public void beanCopier()throws Exception{
UserDTO dto = getDTO();
BeanCopier beanCopier = BeanCopier.create(UserDTO.class,UserVO.class,false);
long startTime = System.currentTimeMillis();
List<UserVO> vos = new ArrayList<UserVO>(count);
for(int i = 0 ; i < count ; ++i){
//BeanCopier
UserVO vo = new UserVO();
beanCopier.copy(dto,vo,null);
vos.add(vo);
}
System.out.println("用时:"+(System.currentTimeMillis() - startTime));
}
/**
* 测试spring BeanUtils
*/
@Test
public void springBeanUtils()throws Exception{
UserDTO dto = getDTO();
long startTime = System.currentTimeMillis();
List<UserVO> vos = new ArrayList<UserVO>(count);
for(int i = 0 ; i < count ; ++i){
UserVO vo = new UserVO();
//spring BeanUtils
org.springframework.beans.BeanUtils.copyProperties(dto,vo);
vos.add(vo);
}
System.out.println("用时:"+(System.currentTimeMillis() - startTime));
}
/**
* 测试 Apache BeanUtils
*/
@Test
public void apacheBeanUtils()throws Exception{
UserDTO dto = getDTO();
long startTime = System.currentTimeMillis();
List<UserVO> vos = new ArrayList<UserVO>(count);
for(int i = 0 ; i < count ; ++i){
UserVO vo = new UserVO();
//Apache BeanUtils
org.apache.commons.beanutils.BeanUtils.copyProperties(vo,dto);
vos.add(vo);
}
System.out.println("用时:"+(System.currentTimeMillis() - startTime));
}
/**
* 测试 Apache PropertyUtils
*/
@Test
public void apachePropertyUtils()throws Exception{
UserDTO dto = getDTO();
long startTime = System.currentTimeMillis();
List<UserVO> vos = new ArrayList<UserVO>(count);
for(int i = 0 ; i < count ; ++i){
UserVO vo = new UserVO();
//Apache PropertyUtils
org.apache.commons.beanutils.PropertyUtils.copyProperties(vo,dto);
vos.add(vo);
}
System.out.println("用时:"+(System.currentTimeMillis() - startTime));
}
}
测试结果
工具/次数 | 1000次 | 100000次 | 10000000次 |
---|---|---|---|
手动编写getter/setter | 2ms | 31ms | 2739ms |
mapstruct | 2ms | 11ms | 2794ms |
cglib BeanCopier | 2ms | 14ms | 2650ms |
spring BeanUtils | 170ms | 282ms | 6965ms |
apache BeanUtils | 165ms | 835ms | 47463ms |
apache PropertyUtils | 135ms | 485ms | 30523ms |
测试结果区3次/1次的平均结果。不同机器不同配置和不同版本会有少许区别。
结论
- 从测试结果来看,mapstruct明显是效率最高的(手动写getter和setter不算)。但是需要额外写一个接口。所以适合那种需要大量转换的类使用。因为需要再添加一个转换接口,加大了代码编写的量,没必要对那些转换次数少的使用。
- mapstruct最灵活,功能也非常丰富。如果不在乎代码编写量,推荐使用。
- cglib 的 BeanCopier 工具性能也很高。但是有个
BeanCopier.create(.....)
操作,如果大量使用的时候推荐先把这个对象实现成单例,不要重复的调用这个方法去创建,使用起来性能才能提升上去。 - apache这两个工具包不推荐使用(阿里大佬们也不推荐使用),而且我们通常使用Spring框架,还需要额外的导入相关包。没必要使用。少量的属性拷贝使用Spring的
BeanUtils
就好。
作者:BobC
文章原创。如你发现错误,欢迎指正,在这里先谢过了。博主的所有的文章、笔记都会在优化并整理后发布在个人公众号上,如果我的笔记对你有一定的用处的话,欢迎关注一下,我会提供更多优质的笔记的。