Apache Beanutils로 속성을 복사하지 마십시오. 왜요? 함께 알아봅시다.

실제 프로젝트 개발에서는 객체간 할당이 흔한데, 더블일레븐, 세킬 등 전자상거래 프로세스가 복잡해지면서 데이터 양도 늘어나고 효율성 문제도 대두됐다.

Q: 객체 간의 할당을 위한 코드를 작성한다면 어떻게 하시겠습니까?

대답: 그것에 대해 생각하지 말고 코드를 직접 시작하고 가져오고 설정하십시오.

Q: 아래 그림처럼?

A: 네, 어떻게 제 코드를 온라인에 올릴 수 있나요?

Q: 아니오, 단지 예를 들었습니다.

A: 이것은 상업적 비밀과 관련이 있으며 매우 심각한 문제입니다.

Q: 당신이 말을 아주 잘한다는 것을 알았습니다. 질문에 직접 대답할 수 있습니까?

답변: 좋아요 좋아요 저도 이런 글이 너무 낮다는 생각이 듭니다 저번에 이렇게 글을 쓰고 나서 거의 맞을 뻔 했습니다

  1. 개체가 너무 많습니다. ctrl c + strl v, 키보드가 거의 고장났습니다.
  2. 그리고 실수하기 쉽습니다. 주의를 기울이지 않으면 속성이 일치하지 않고 잘못된 값이 할당됩니다.
  3. 코드는 어리석어 보이고, 클래스는 수천 줄로 구성되어 있고, 모두 복사본을 가져오고 설정하며, 심지어 매우 우아한 이름 전송을 제공했습니다.
  4. 이름에서 속성 이름이 보이지 않는 경우 각 속성의 의미에 대한 주석을 추가해야 합니다(기본적으로 이런 할당 작업을 추가해야 합니다. 매우 중요);
  5. 코드는 유지 관리하기 번거롭습니다.
  6. 개체가 너무 많으면 클래스 폭발 문제가 발생하고 속성이 너무 많으면 Alibaba 코드 사양을 심각하게 위반합니다(메소드의 실제 코드는 최대 20줄).

질문: 알겠습니다. 해결 방법을 알려주세요.

답변: 매우 간단합니다. 도구 클래스 Beanutils를 통해 직접 값을 할당할 수 있습니다.

질문: 최근 도구가 매우 인기가 있다고 들었습니다. 어떤 도구를 사용하십니까?

대답: Apache당신과 함께 제공되는 것만으로도 정말 간단합니다. 감사의 마음을 담아 손글씨로 적어보았습니다.

질문: 코드에서 오류를 보고합니다. Apache Beanutils를 사용하여 속성을 복사하지 마십시오.

답변: 오류가 없습니다. 심각한 경고일 뿐입니다.代码能跑就行,有问题再优化呗

Q. 태도는? 인사에 휘말리는 사람에게 엄중한 경고가 나오는 이유는?

답: 돈을 얼마나 벌고, 일을 얼마나 하느냐, 나는 XXX가 아니다. 성능 문제일 것이다.

Q. 구체적인 이유는 무엇인가요?

답변: 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시간 소모성에서 1위로 인상적으로 나열됩니다.

Q. 다른 좋은 방법은 없나요? 더 나은 성능

답변: 물론 있습니다. 제가 아는 한 4가지 도구가 있습니다. 사실 더 있을 수도 있습니다. 먼저 간단하게 소개하겠습니다.

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

Q: 그럼 왜 사용하지 않습니까?

답: 알겠습니다. 시연해 보겠습니다.

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("[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 = 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
아파치 BeanUtils 906밀리초 807밀리초 1892밀리초 11049밀리초
아파치 PropertyUtils 17밀리초 96밀리초 648밀리초 5896밀리초
스프링 cglib BeanCopier 0밀리초 1밀리초 3밀리초 10밀리초
스프링 카피 속성 87밀리초 90밀리초 123밀리초 482밀리초

의외로 잘 모르겠습니다. 테스트를 했을 때 충격을 받았습니다. 그 차이는 정말 훨씬 더 심합니다.

spring cglib BeanCopier최고의 apache BeanUtils성능, 최악의 성능.

실적 추세 --> spring cglib BeanCopier우수 spring copyProperties대비 apache PropertyUtils우수apache BeanUtils

Apache Beanutils로 속성 복사를 피하는 문제는 위에서 분석한 바 있으며, 다른 방법에 의한 최적화를 살펴보겠습니다.

Apache PropertyUtils 소스 코드 분석

유형 검사가 null이 아닌 검사가 되었고 각 복사본의 로그 레코드가 제거되었으며 성능이 더 좋아졌음을 소스 코드에서 명확하게 볼 수 있습니다.

  1. 유형 검사가 널이 아닌 검사로 변경됨
  2. 각 사본에 대한 로그 기록 제거
  3. 실제 할당은 copyProperty에서 DanyBean + setSimpleProperty로 변경됩니다.

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 소스 코드 분석

  1. 데이터 소스와 대상 개체를 판단하는 비어 있지 않은 판단이 주장으로 변경됩니다.
  2. 각 사본에 대한 로그 레코드가 없습니다.
  3. if (orig instanceof DynaBean) { 이 유형 검사 없이 ;
  4. 권한을 해제하는 단계를 추가했습니다.
/**
 * org.springframework.beans.BeanUtils.copyProperties方法源码解析
 * @author 哪吒编程
 * @time 2023-01-07
 */
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<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) {
    
    
                        throw new FatalBeanException(
                                "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                    }
                }
            }
        }
    }
}

요약하다

Ali의 친근한 알림, Apache Beanutils객체 사용 copy을 피하는 것은 여전히 ​​매우 합리적 입니다.

Apache Beanutils각 복사본의 형식 확인 및 로깅에서 성능 문제가 발생합니다.

Apache PropertyUtils는 다음과 같이 최적화되었습니다.

  1. 유형 검사가 널이 아닌 검사로 변경됨
  2. 각 사본에 대한 로그 기록 제거
  3. 실제 할당은 copyProperty에서 DanyBean + setSimpleProperty로 변경됩니다.

Spring copyProperties는 다음과 같이 최적화되었습니다.

  1. 데이터 소스와 대상 개체를 판단하는 비어 있지 않은 판단이 주장으로 변경됩니다.
  2. 각 사본에 대한 로그 레코드가 없습니다.
  3. if (orig instanceof DynaBean) { 이 유형 검사 없이 ;
  4. 권한을 해제하는 단계를 추가했습니다.

рекомендация

отblog.csdn.net/guorui_java/article/details/128561652