在实际的项目开发中,对象间赋值普遍存在,随着双十一、秒杀等电商过程愈加复杂,数据量也在不断攀升,效率问题,浮出水面。
问:如果是你来写对象间赋值的代码,你会怎么做?
答:想都不用想,直接代码走起来,get、set即可。
问:下图这样?
答:对啊,你怎么能把我的代码放到网上?
问:没,我只是举个例子
答:这涉及到商业机密,是很严重的问题
问:我发现你挺能扯皮啊,直接回答问题行吗?
答:OK,OK,我也觉得这样写很low,上次这么写之后,差点挨打
问:行了,行了,说说,怎么解决吧。
答:很简单啊,可以通过工具类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方法源码解析 * @author 哪吒编程 * @time 2023-01-07 */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<String, Object> propMap = (Map)orig; Iterator var13 = propMap.entrySet().iterator(); while(var13.hasNext()) { Map.Entry<String, Object> 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 种工具类,实际上,可能会有更多,话不多说,先简单介绍一下。
问:那你怎么不用?
答:OK,我来演示一下
package com.nezha.copy;import org.apache.commons.beanutils.BeanUtils;import org.apache.commons.beanutils.PropertyUtils;import org.springframework.cglib.beans.BeanCopier;import org.springframework.util.StopWatch;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); }}"四大金刚" 性能统计
方法
1000
10000
100000
1000000
apache BeanUtils
906毫秒
807毫秒
1892毫秒
11049毫秒
apache PropertyUtils
17毫秒
96毫秒
648毫秒
5896毫秒
spring cglib BeanCopier
0毫秒
1毫秒
3毫秒
10毫秒
spring copyProperties
87毫秒
90毫秒
123毫秒
482毫秒
不测不知道,一测吓一跳,差的还真的多。
spring cglib BeanCopier性能最好,apache BeanUtils性能最差。
性能走势 --> spring cglib BeanCopier 优于 spring copyProperties 优于 apache PropertyUtils 优于 apache BeanUtils
避免用Apache Beanutils进行属性的copy的问题 上面分析完了,下面再看看其它的方法做了哪些优化。
Apache PropertyUtils 源码分析
从源码可以清晰的看到,类型检查变成了非空校验,去掉了每一次copy的日志记录,性能肯定更好了。
DanyBean 提供了可以动态修改实现他的类的属性名称、属性值、属性类型的功能。
/** * org.apache.commons.beanutils.PropertyUtils方式源码解析 * @author 哪吒编程 * @time 2023-01-07 */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 源码分析
总结
阿里的友情提示,避免用Apache Beanutils进行对象的copy,还是很有道理的。
Apache Beanutils 的性能问题出现在类型校验和每一次copy的日志记录;