收录于热门专栏Java基础教程系列(进阶篇)

在实际的项目开发中,对象间赋值普遍存在,随着双十一、秒杀等电商过程愈加复杂,数据量也在不断攀升,效率问题,浮出水面。

问:如果是你来写对象间赋值的代码,你会怎么做? 答:想都不用想,直接代码走起来,get、set即可。

问:下图这样?

答:对啊,你怎么能把我的代码放到网上?

问:没,我只是举个例子

答:这涉及到商业机密,是很严重的问题

问:我发现你挺能扯皮啊,直接回答问题行吗?

答:OK,OK,我也觉得这样写很low,上次这么写之后,差点挨打 对象太多,ctrl c + strl v,键盘差点没敲坏; 而且很容易出错,一不留神,属性没对应上,赋错值了; 代码看起来很傻缺,一个类好几千行,全是get、set复制,还起个了自以为很优雅的名字transfer; 如果属性名不能见名知意,还得加上每个属性的含义注释(基本这种赋值操作,都是要加的,注释很重要,注释很重要,注释很重要); 代码维护起来很麻烦; 如果对象过多,会产生类爆炸问题,如果属性过多,会严重违背阿里巴巴代码规约(一个方法的实际代码最多20行); 问:行了,行了,说说,怎么解决吧。

答:很简单啊,可以通过工具类Beanutils直接赋值啊

问:我听说工具类最近很卷,你用的哪个啊? 答:就Apache自带的那个啊,贼简单。我手写一个,给你欣赏一下。 问:你这代码报错啊,避免用Apache Beanutils进行属性的copy。

答:没报错,只是严重警告而已,代码能跑就行,有问题再优化呗

问:你这什么态度?人事在哪划拉的人,为啥会出现严重警告? 答:拿多少钱,干多少活,我又不是XXX,应该是性能问题吧

问:具体什么原因导致的呢?

答:3000块钱还得手撕一下 apache copyProperties的源代码呗?

通过单例模式调用copyProperties,但是,每一个方法对应一个BeanUtilsBean.getInstance()实例,每一个类实例对应一个实例,这不算一个真正的单例模式。

public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {

BeanUtilsBean.getInstance().copyProperties(dest, orig);

}

性能瓶颈 --> 日志太多也是病 通过源码可以看到,每一个copyProperties都要进行多次类型检查,还要打印日志。

/**

* org.apache.commons.beanutils.BeanUtils.copyProperties方法源码解析

*/

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;

// 类型检查

// DanyBean 提供了可以动态修改实现他的类的属性名称、属性值、属性类型的功能

if (orig instanceof DynaBean) {

// 获取源对象所有属性

DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties();

DynaProperty[] var4 = origDescriptors;

var5 = origDescriptors.length;

for(var6 = 0; var6 < var5; ++var6) {

DynaProperty origDescriptor = var4[var6];

// 获取源对象属性名

name = origDescriptor.getName();

// 判断源对象是否可读、判断目标对象是否可写

if (this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {

// 获取对应的值

value = ((DynaBean)orig).get(name);

// 每个属性都调用一次copyProperty

this.copyProperty(dest, name, value);

}

}

} else if (orig instanceof Map) {

Map propMap = (Map)orig;

Iterator var13 = propMap.entrySet().iterator();

while(var13.hasNext()) {

Map.Entry entry = (Map.Entry)var13.next();

String name = (String)entry.getKey();

if (this.getPropertyUtils().isWriteable(dest, name)) {

this.copyProperty(dest, name, entry.getValue());

}

}

} else {

PropertyDescriptor[] origDescriptors = this.getPropertyUtils().getPropertyDescriptors(orig);

PropertyDescriptor[] var14 = origDescriptors;

var5 = origDescriptors.length;

for(var6 = 0; var6 < var5; ++var6) {

PropertyDescriptor origDescriptor = var14[var6];

name = origDescriptor.getName();

if (!"class".equals(name) && this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {

try {

value = this.getPropertyUtils().getSimpleProperty(orig, name);

this.copyProperty(dest, name, value);

} catch (NoSuchMethodException var10) {

}

}

}

}

}

}

通过 jvisualvm.exe 检测代码性能 再通过jvisualvm.exe检测一下运行情况,果然,logging.log4j赫然在列,稳居耗时Top1。

问:还有其它好的方式吗?性能好一点的 答:当然有,据我了解有 4 种工具类,实际上,可能会有更多,话不多说,先简单介绍一下。 org.apache.commons.beanutils.BeanUtils; org.apache.commons.beanutils.PropertyUtils; org.springframework.cglib.beans.BeanCopier; org.springframework.beans.BeanUtils; 问:那你怎么不用? 答:OK,我来演示一下

public class Test {

public static void main(String[] args) {

User user = new User();

user.setUserId("1");

user.setUserName("测试编程");

user.setCardId("123");

user.setCreateTime("2023-01-03");

user.setEmail("666666666@qq.com");

user.setOperate("测试");

user.setOrgId("46987916");

user.setPassword("123456");

user.setPhone("10086");

user.setRemark("456");

user.setSex(1);

user.setStatus("1");

user.setTel("110");

user.setType("0");

user.setUpdateTime("2023-01-05");

User target = new User();

int sum = 10000000;

apacheBeanUtilsCopyTest(user,target,sum);

commonsPropertyCopyTest(user,target,sum);

cglibBeanCopyTest(user,target,sum);

springBeanCopyTest(user,target,sum);

}

private static void apacheBeanUtilsCopyTest(User source, User target, int sum) {

StopWatch stopWatch = new StopWatch();

stopWatch.start();

for (int i = 0; i < sum; i++) {

apacheBeanUtilsCopy(source,target);

}

stopWatch.stop();

System.out.println("使用org.apache.commons.beanutils.BeanUtils方式赋值"+sum+"个user对象,耗时:"+stopWatch.getLastTaskTimeMillis()+"毫秒");

}

/**

* org.apache.commons.beanutils.BeanUtils方式

*/

private static void apacheBeanUtilsCopy(User source, User target) {

try {

BeanUtils.copyProperties(source, target);

} catch (Exception e) {

}

}

private static void commonsPropertyCopyTest(User source, User target, int sum) {

StopWatch stopWatch = new StopWatch();

stopWatch.start();

for (int i = 0; i < sum; i++) {

commonsPropertyCopy(source,target);

}

stopWatch.stop();

System.out.println("使用org.apache.commons.beanutils.PropertyUtils方式赋值"+sum+"个user对象,耗时:"+stopWatch.getLastTaskTimeMillis()+"毫秒");

}

/**

* org.apache.commons.beanutils.PropertyUtils方式

*/

private static void commonsPropertyCopy(User source, User target) {

try {

PropertyUtils.copyProperties(target, source);

} catch (Exception e) {

}

}

private static void cglibBeanCopyTest(User source, User target, int sum) {

StopWatch stopWatch = new StopWatch();

stopWatch.start();

for (int i = 0; i < sum; i++) {

cglibBeanCopy(source,target);

}

stopWatch.stop();

System.out.println("使用org.springframework.cglib.beans.BeanCopier方式赋值"+sum+"个user对象,耗时:"+stopWatch.getLastTaskTimeMillis()+"毫秒");

}

/**

* org.springframework.cglib.beans.BeanCopier方式

*/

static BeanCopier copier = BeanCopier.create(User.class, User.class, false);

private static void cglibBeanCopy(User source, User target) {

copier.copy(source, target, null);

}

private static void springBeanCopyTest(User source, User target, int sum) {

StopWatch stopWatch = new StopWatch();

stopWatch.start();

for (int i = 0; i < sum; i++) {

springBeanCopy(source,target);

}

stopWatch.stop();

System.out.println("使用org.springframework.beans.BeanUtils.copyProperties方式赋值"+sum+"个user对象,耗时:"+stopWatch.getLastTaskTimeMillis()+"毫秒");

}

/**

* org.springframework.beans.BeanUtils.copyProperties方式

*/

private static void springBeanCopy(User source, User target) {

org.springframework.beans.BeanUtils.copyProperties(source, target);

}

}

“四大金刚” 性能统计

不测不知道,一测吓一跳,差的还真的多。

spring cglib BeanCopier性能最好,apache BeanUtils性能最差。

性能走势 --> spring cglib BeanCopier优于 spring copyProperties优于 apache PropertyUtils优于 apache BeanUtils

避免用Apache Beanutils进行属性的copy的问题 上面分析完了,下面再看看其它的方法做了哪些优化。

Apache PropertyUtils 源码分析 从源码可以清晰的看到,类型检查变成了非空校验,去掉了每一次copy的日志记录,性能肯定更好了。

类型检查变成了非空校验 去掉了每一次copy的日志记录 实际赋值的地方由copyProperty变成了DanyBean + setSimpleProperty; DanyBean 提供了可以动态修改实现他的类的属性名称、属性值、属性类型的功能。

public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {

// 判断数据源和目标对象不是null

if (dest == null) {

throw new IllegalArgumentException("No destination bean specified");

} else if (orig == null) {

throw new IllegalArgumentException("No origin bean specified");

} else {

// 删除了org.apache.commons.beanutils.BeanUtils.copyProperties中最为耗时的log日志记录

int var5;

int var6;

String name;

Object value;

// 类型检查

if (orig instanceof DynaBean) {

// 获取源对象所有属性

DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties();

DynaProperty[] var4 = origDescriptors;

var5 = origDescriptors.length;

for(var6 = 0; var6 < var5; ++var6) {

DynaProperty origDescriptor = var4[var6];

// 获取源对象属性名

name = origDescriptor.getName();

// 判断源对象是否可读、判断目标对象是否可写

if (this.isReadable(orig, name) && this.isWriteable(dest, name)) {

try {

// 获取对应的值

value = ((DynaBean)orig).get(name);

// 相对于org.apache.commons.beanutils.BeanUtils.copyProperties此处有优化

// DanyBean 提供了可以动态修改实现他的类的属性名称、属性值、属性类型的功能

if (dest instanceof DynaBean) {

((DynaBean)dest).set(name, value);

} else {

// 每个属性都调用一次copyProperty

this.setSimpleProperty(dest, name, value);

}

} catch (NoSuchMethodException var12) {

if (this.log.isDebugEnabled()) {

this.log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", var12);

}

}

}

}

} else if (orig instanceof Map) {

Iterator entries = ((Map)orig).entrySet().iterator();

while(true) {

Map.Entry entry;

String name;

do {

if (!entries.hasNext()) {

return;

}

entry = (Map.Entry)entries.next();

name = (String)entry.getKey();

} while(!this.isWriteable(dest, name));

try {

if (dest instanceof DynaBean) {

((DynaBean)dest).set(name, entry.getValue());

} else {

this.setSimpleProperty(dest, name, entry.getValue());

}

} catch (NoSuchMethodException var11) {

if (this.log.isDebugEnabled()) {

this.log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", var11);

}

}

}

} else {

PropertyDescriptor[] origDescriptors = this.getPropertyDescriptors(orig);

PropertyDescriptor[] var16 = origDescriptors;

var5 = origDescriptors.length;

for(var6 = 0; var6 < var5; ++var6) {

PropertyDescriptor origDescriptor = var16[var6];

name = origDescriptor.getName();

if (this.isReadable(orig, name) && this.isWriteable(dest, name)) {

try {

value = this.getSimpleProperty(orig, name);

if (dest instanceof DynaBean) {

((DynaBean)dest).set(name, value);

} else {

this.setSimpleProperty(dest, name, value);

}

} catch (NoSuchMethodException var10) {

if (this.log.isDebugEnabled()) {

this.log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", var10);

}

}

}

}

}

}

}

通过 jvisualvm.exe 检测代码性能 再通过jvisualvm.exe检测一下运行情况,果然,logging.log4j没有了,其他的基本不变。 Spring copyProperties 源码分析 判断数据源和目标对象的非空判断改为了断言; 每次copy没有日志记录; 没有if (orig instanceof DynaBean) {这个类型检查; 增加了放开权限的步骤;

private static void copyProperties(Object source, Object target, @Nullable Class editable,

@Nullable String... ignoreProperties) throws BeansException {

// 判断数据源和目标对象不是null

Assert.notNull(source, "Source must not be null");

Assert.notNull(target, "Target must not be null");

/**

* 若target设置了泛型,则默认使用泛型

* 若是 editable 是 null,则此处忽略

* 一般情况下editable都默认为null

*/

Class actualEditable = target.getClass();

if (editable != null) {

if (!editable.isInstance(target)) {

throw new IllegalArgumentException("Target class [" + target.getClass().getName() +

"] not assignable to Editable class [" + editable.getName() + "]");

}

actualEditable = editable;

}

// 获取target中全部的属性描述

PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);

// 需要忽略的属性

List ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);

for (PropertyDescriptor targetPd : targetPds) {

Method writeMethod = targetPd.getWriteMethod();

// 目标对象存在写入方法、属性不被忽略

if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {

PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());

if (sourcePd != null) {

Method readMethod = sourcePd.getReadMethod();

/**

* 源对象存在读取方法、数据是可复制的

* writeMethod.getParameterTypes()[0]:获取 writeMethod 的第一个入参类型

* readMethod.getReturnType():获取 readMethod 的返回值类型

* 判断返回值类型和入参类型是否存在继承关系,只有是继承关系或相等的情况下,才会进行注入

*/

if (readMethod != null &&

ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {

try {

// 放开读取方法的权限

if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {

readMethod.setAccessible(true);

}

// 通过反射获取值

Object value = readMethod.invoke(source);

// 放开写入方法的权限

if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {

writeMethod.setAccessible(true);

}

// 通过反射写入值

writeMethod.invoke(target, value);

}

catch (Throwable ex) {

throw new FatalBeanException(

"Could not copy property '" + targetPd.getName() + "' from source to target", ex);

}

}

}

}

}

}

总结

阿里的友情提示,避免用Apache Beanutils进行对象的copy,还是很有道理的。

Apache Beanutils的性能问题出现在类型校验和每一次copy的日志记录;

Apache PropertyUtils 进行了如下优化:

类型检查变成了非空校验

去掉了每一次copy的日志记录

实际赋值的地方由copyProperty变成了DanyBean + setSimpleProperty;

Spring copyProperties 进行了如下优化:

判断数据源和目标对象的非空判断改为了断言;

每次copy没有日志记录;

没有if (orig instanceof DynaBean) {这个类型检查;

增加了放开权限的步骤;

相关文章

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。