0


跟同事杠上了,Apache Beanutils为什么被禁止使用?

在这里插入图片描述

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

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

问:如果是你来写对象间赋值的代码,你会怎么做?

答:想都不用想,直接代码走起来,get、set即可。

问:下图这样?

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

问:没,我只是举个例子

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

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

答:OK,OK,我也觉得这样写很low,上次这么写之后,差点挨打

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

问:行了,行了,说说,怎么解决吧。

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

问:我听说工具类最近很卷,你用的哪个啊?

答:就

Apache

自带的那个啊,贼简单。我手写一个,给你欣赏一下。

问:你这代码报错啊,避免用Apache Beanutils进行属性的copy。

答:没报错,只是严重警告而已,

代码能跑就行,有问题再优化呗

问:你这什么态度?人事在哪划拉的人,为啥会出现严重警告?

答:拿多少钱,干多少活,我又不是XXX,应该是性能问题吧

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

答:3000块钱还得手撕一下

apache copyProperties

的源代码呗?

通过单例模式调用

copyProperties

,但是,每一个方法对应一个

BeanUtilsBean.getInstance()

实例,每一个类实例对应一个实例,这不算一个真正的单例模式。

publicstaticvoidcopyProperties(Object dest,Object orig)throwsIllegalAccessException,InvocationTargetException{BeanUtilsBean.getInstance().copyProperties(dest, orig);}

性能瓶颈 --> 日志太多也是病

通过源码可以看到,每一个

copyProperties

都要进行多次类型检查,还要打印日志。

/**
 * org.apache.commons.beanutils.BeanUtils.copyProperties方法源码解析
 * @author 哪吒编程
 * @time 2023-01-07
 */publicvoidcopyProperties(Object dest,Object orig)throwsIllegalAccessException,InvocationTargetException{// 类型检查if(dest ==null){thrownewIllegalArgumentException("No destination bean specified");}elseif(orig ==null){thrownewIllegalArgumentException("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 instanceofDynaBean){// 获取源对象所有属性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);// 每个属性都调用一次copyPropertythis.copyProperty(dest, name, value);}}}elseif(orig instanceofMap){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 种工具类,实际上,可能会有更多,话不多说,先简单介绍一下。

  1. org.apache.commons.beanutils.BeanUtils;
  2. org.apache.commons.beanutils.PropertyUtils;
  3. org.springframework.cglib.beans.BeanCopier;
  4. org.springframework.beans.BeanUtils;

问:那你怎么不用?

答:OK,我来演示一下

packagecom.nezha.copy;importorg.apache.commons.beanutils.BeanUtils;importorg.apache.commons.beanutils.PropertyUtils;importorg.springframework.cglib.beans.BeanCopier;importorg.springframework.util.StopWatch;publicclassTest{publicstaticvoidmain(String[] args){User user =newUser();
        user.setUserId("1");
        user.setUserName("哪吒编程");
        user.setCardId("123");
        user.setCreateTime("2023-01-03");
        user.setEmail("[email protected]");
        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 =newUser();int sum =10000000;apacheBeanUtilsCopyTest(user,target,sum);commonsPropertyCopyTest(user,target,sum);cglibBeanCopyTest(user,target,sum);springBeanCopyTest(user,target,sum);}privatestaticvoidapacheBeanUtilsCopyTest(User source,User target,int sum){StopWatch stopWatch =newStopWatch();
        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方式
     */privatestaticvoidapacheBeanUtilsCopy(User source,User target){try{BeanUtils.copyProperties(source, target);}catch(Exception e){}}privatestaticvoidcommonsPropertyCopyTest(User source,User target,int sum){StopWatch stopWatch =newStopWatch();
        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方式
     */privatestaticvoidcommonsPropertyCopy(User source,User target){try{PropertyUtils.copyProperties(target, source);}catch(Exception e){}}privatestaticvoidcglibBeanCopyTest(User source,User target,int sum){StopWatch stopWatch =newStopWatch();
        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方式
     */staticBeanCopier copier =BeanCopier.create(User.class,User.class,false);privatestaticvoidcglibBeanCopy(User source,User target){
        copier.copy(source, target,null);}privatestaticvoidspringBeanCopyTest(User source,User target,int sum){StopWatch stopWatch =newStopWatch();
        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方式
     */privatestaticvoidspringBeanCopy(User source,User target){org.springframework.beans.BeanUtils.copyProperties(source, target);}}

“四大金刚” 性能统计

方法1000100001000001000000apache BeanUtils906毫秒807毫秒1892毫秒11049毫秒apache PropertyUtils17毫秒96毫秒648毫秒5896毫秒spring cglib BeanCopier0毫秒1毫秒3毫秒10毫秒spring copyProperties87毫秒90毫秒123毫秒482毫秒
不测不知道,一测吓一跳,差的还真的多。

spring cglib BeanCopier

性能最好,

apache BeanUtils

性能最差。

性能走势 -->

spring cglib BeanCopier

优于

spring copyProperties

优于

apache PropertyUtils

优于

apache BeanUtils

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

Apache PropertyUtils 源码分析

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

  1. 类型检查变成了非空校验
  2. 去掉了每一次copy的日志记录
  3. 实际赋值的地方由copyProperty变成了DanyBean + setSimpleProperty;

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

/**
 * org.apache.commons.beanutils.PropertyUtils方式源码解析
 * @author 哪吒编程
 * @time 2023-01-07
 */publicvoidcopyProperties(Object dest,Object orig)throwsIllegalAccessException,InvocationTargetException,NoSuchMethodException{// 判断数据源和目标对象不是nullif(dest ==null){thrownewIllegalArgumentException("No destination bean specified");}elseif(orig ==null){thrownewIllegalArgumentException("No origin bean specified");}else{// 删除了org.apache.commons.beanutils.BeanUtils.copyProperties中最为耗时的log日志记录int var5;int var6;String name;Object value;// 类型检查if(orig instanceofDynaBean){// 获取源对象所有属性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 instanceofDynaBean){((DynaBean)dest).set(name, value);}else{// 每个属性都调用一次copyPropertythis.setSimpleProperty(dest, name, value);}}catch(NoSuchMethodException var12){if(this.log.isDebugEnabled()){this.log.debug("Error writing to '"+ name +"' on class '"+ dest.getClass()+"'", var12);}}}}}elseif(orig instanceofMap){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 instanceofDynaBean){((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 instanceofDynaBean){((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 源码分析

  1. 判断数据源和目标对象的非空判断改为了断言;
  2. 每次copy没有日志记录;
  3. 没有if (orig instanceof DynaBean) {这个类型检查;
  4. 增加了放开权限的步骤;
/**
 * org.springframework.beans.BeanUtils.copyProperties方法源码解析
 * @author 哪吒编程
 * @time 2023-01-07
 */privatestaticvoidcopyProperties(Object source,Object target,@NullableClass<?> editable,@NullableString... ignoreProperties)throwsBeansException{// 判断数据源和目标对象不是nullAssert.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)){thrownewIllegalArgumentException("Target class ["+ target.getClass().getName()+"] not assignable to Editable class ["+ editable.getName()+"]");}
        actualEditable = editable;}// 获取target中全部的属性描述PropertyDescriptor[] targetPds =getPropertyDescriptors(actualEditable);// 需要忽略的属性List<String> 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){thrownewFatalBeanException("Could not copy property '"+ targetPd.getName()+"' from source to target", ex);}}}}}}

总结

阿里的友情提示,避免用

Apache Beanutils

进行对象的

copy

,还是很有道理的。

Apache Beanutils

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

Apache PropertyUtils 进行了如下优化:

  1. 类型检查变成了非空校验
  2. 去掉了每一次copy的日志记录
  3. 实际赋值的地方由copyProperty变成了DanyBean + setSimpleProperty;

Spring copyProperties 进行了如下优化:

  1. 判断数据源和目标对象的非空判断改为了断言;
  2. 每次copy没有日志记录;
  3. 没有if (orig instanceof DynaBean) {这个类型检查;
  4. 增加了放开权限的步骤;

在这里插入图片描述
在这里插入图片描述

Java学习路线总结,搬砖工逆袭Java架构师

10万字208道Java经典面试题总结(附答案)

Java基础教程系列

Java基础教程系列(进阶篇)

标签: java spring 学习

本文转载自: https://blog.csdn.net/guorui_java/article/details/128994880
版权归原作者 哪 吒 所有, 如有侵权,请联系我们删除。

“跟同事杠上了,Apache Beanutils为什么被禁止使用?”的评论:

还没有评论