Why does the Ali Code Specification require you to avoid copying attributes using Apache BeanUtils

Posted by m7_b5 on Fri, 31 May 2019 21:03:35 +0200

Statement: This article is an original article, originated from the public name: Programmer's way of self-learning, and published synchronously in https://blog.csdn.net/dadiyang Hereby, synchronized release to sf, reproduced please indicate the source.

origin

In one development process, we just saw a small partner calling the set method to copy the attributes of PO objects queried in a database into Vo objects, similar to this:

As you can see, most of the fields of the Po and Vo classes are the same. We call the set method one by one and only do some repetitive and lengthy operations. This operation is very error-prone because there are too many attributes of the object, one or two may be omitted, and it is difficult to detect with the naked eye.

It is easy to imagine that such an operation can be solved by reflection. In fact, such a common function, a BeanUtils tool class can be completed.

So I suggested that this little buddy use Apache BeanUtils.copyProperties to copy attributes, which digs a hole in our program!

Ali Code Specification

When we open the Ali code scan plug-in, if you use Apache BeanUtils.copyProperties to copy attributes, it will give you a very serious warning. Because Apache Bean Utils has poor performance, it can be replaced by Spring Bean Utils or Cglib Bean Copier.

It's a bit uncomfortable to see such a warning. The package provided by the well-known Apache actually has performance problems, so that Ali gave a serious warning.

So how serious is this performance problem? After all, in our application scenarios, if only a small performance loss, but can bring great convenience, it is acceptable.

Take this question with you. Let's do an experiment and verify it.

If you are not interested in specific testing methods, you can skip looking at the results directly.~

Test Method Interface and Implementation Definition

First, for testing convenience, let's define an interface and unify several implementations:

public interface PropertiesCopier {
    void copyProperties(Object source, Object target) throws Exception;
}
public class CglibBeanCopierPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        BeanCopier copier = BeanCopier.create(source.getClass(), target.getClass(), false);
        copier.copy(source, target, null);
    }
}
// Global static BeanCopier to avoid generating new objects every time
public class StaticCglibBeanCopierPropertiesCopier implements PropertiesCopier {
    private static BeanCopier copier = BeanCopier.create(Account.class, Account.class, false);
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        copier.copy(source, target, null);
    }
}
public class SpringBeanUtilsPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        org.springframework.beans.BeanUtils.copyProperties(source, target);
    }
}
public class CommonsBeanUtilsPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        org.apache.commons.beanutils.BeanUtils.copyProperties(target, source);
    }
}
public class CommonsPropertyUtilsPropertiesCopier implements PropertiesCopier {
    @Override
    public void copyProperties(Object source, Object target) throws Exception {
        org.apache.commons.beanutils.PropertyUtils.copyProperties(target, source);
    }
}

unit testing

Then write a parameterized unit test:

@RunWith(Parameterized.class)
public class PropertiesCopierTest {
    @Parameterized.Parameter(0)
    public PropertiesCopier propertiesCopier;
    // Test times
    private static List<Integer> testTimes = Arrays.asList(100, 1000, 10_000, 100_000, 1_000_000);
    // The test results are output in the form of a markdown table
    private static StringBuilder resultBuilder = new StringBuilder("|Realization|100|1,000|10,000|100,000|1,000,000|\n").append("|----|----|----|----|----|----|\n");

    @Parameterized.Parameters
    public static Collection<Object[]> data() {
        Collection<Object[]> params = new ArrayList<>();
        params.add(new Object[]{new StaticCglibBeanCopierPropertiesCopier()});
        params.add(new Object[]{new CglibBeanCopierPropertiesCopier()});
        params.add(new Object[]{new SpringBeanUtilsPropertiesCopier()});
        params.add(new Object[]{new CommonsPropertyUtilsPropertiesCopier()});
        params.add(new Object[]{new CommonsBeanUtilsPropertiesCopier()});
        return params;
    }

    @Before
    public void setUp() throws Exception {
        String name = propertiesCopier.getClass().getSimpleName().replace("PropertiesCopier", "");
        resultBuilder.append("|").append(name).append("|");
    }

    @Test
    public void copyProperties() throws Exception {
        Account source = new Account(1, "test1", 30D);
        Account target = new Account();
        // Preheat once
        propertiesCopier.copyProperties(source, target);
        for (Integer time : testTimes) {
            long start = System.nanoTime();
            for (int i = 0; i < time; i++) {
                propertiesCopier.copyProperties(source, target);
            }
            resultBuilder.append((System.nanoTime() - start) / 1_000_000D).append("|");
        }
        resultBuilder.append("\n");
    }

    @AfterClass
    public static void tearDown() throws Exception {
        System.out.println("Test results:");
        System.out.println(resultBuilder);
    }
}

test result

Unit of time milliseconds

Realization The 100 time The 1000 time The 10000 time The 100000 time The 1000000 time
StaticCglibBeanCopier 0.055022 0.541029 0.999478 2.754824 9.88556
CglibBeanCopier 5.320798 11.086323 61.037446 72.484607 333.384007
SpringBeanUtils 5.180483 21.328542 30.021662 103.266375 966.439272
CommonsPropertyUtils 9.729159 42.927356 74.063789 386.127787 1955.5437
CommonsBeanUtils 24.99513 170.728558 572.335327 2970.3068 27563.3459

The results show that Cglib's Bean Copier is the fastest copy speed, even a million copies only take 10 milliseconds!
By contrast, the worst is the BeanUtils.copyProperties method of the Commons package, with 100 copy tests 400 times worse than the best Cglib. Millions of copies are 2800 times the performance difference!

It turned out to be a big surprise.

But why are they so different?

Cause analysis

Looking at the source code, we will find that Commons Bean Utils mainly has the following time-consuming places:

  • Output a lot of log debugging information
  • Duplicate object type checking
  • Type conversion
  public void copyProperties(final Object dest, final Object orig)
        throws IllegalAccessException, InvocationTargetException {
        // Type checking 
        if (orig instanceof DynaBean) {
            ...
        } else if (orig instanceof Map) {
           ...
        } else {
            final PropertyDescriptor[] origDescriptors = ...
            for (PropertyDescriptor origDescriptor : origDescriptors) {
                ...
                // Here each property is called copyProperty once.
                copyProperty(dest, name, value);
            }
        }
    }

    public void copyProperty(final Object bean, String name, Object value)
        throws IllegalAccessException, InvocationTargetException {
        ...
        // Here's another type check
        if (target instanceof DynaBean) {
            ...
        }
        ...
        // Attributes need to be converted to target types
        value = convertForCopy(value, type);
        ...
    }
    // This conversion method has a lot of string splicing when the log level is debug.
    public <T> T convert(final Class<T> type, Object value) {
        if (log().isDebugEnabled()) {
            log().debug("Converting" + (value == null ? "" : " '" + toString(sourceType) + "'") + " value '" + value + "' to type '" + toString(targetType) + "'");
        }
        ...
        if (targetType.equals(String.class)) {
            return targetType.cast(convertToString(value));
        } else if (targetType.equals(sourceType)) {
            if (log().isDebugEnabled()) {
                log().debug("No conversion required, value is already a " + toString(targetType));
            }
            return targetType.cast(value);
        } else {
            // Type checking is also required in this convertToType method
            final Object result = convertToType(targetType, value);
            if (log().isDebugEnabled()) {
                log().debug("Converted to " + toString(targetType) + " value '" + result + "'");
            }
            return targetType.cast(result);
        }
    }

Specific performance and source code analysis can be referred to in these articles:

One more thing

In addition to performance issues, there are other pits that require special care when using Commons Bean Utils!

Default values for wrapper classes

When copying attributes, although CommonsBeanUtils does not default to the original wrapper class by default, if your class has a Date type attribute and the attribute value in the source object is null, an exception will occur when using a lower version (1.8.0 and below):

org.apache.commons.beanutils.ConversionException: No value specified for 'Date'

The solution to this problem is to register a DateConverter:

ConvertUtils.register(new DateConverter(null), java.util.Date.class);

However, this statement will cause the wrapper type to be given the default value of the original type, such as Integer attribute, which is 0 by default, even though the value of the field of your source object is null.

In the high version (1.9.3), the problem of date null values and the problem of wrapper class default values were fixed.

This scenario has a special meaning when our packing class attribute is null value. It's very easy to trample! For example, when searching for a conditional object, a null value means that the field is unrestricted, and a zero means that the value of the field must be zero.

When switching to other tools

When we see Ali's tips, or after reading this article, we know the performance problems of Commons Bean Utils. When we want to switch to Spring's Bean Utils, we should be careful:

org.apache.commons.beanutils.BeanUtils.copyProperties(Object target, Object source);
org.springframework.beans.BeanUtils.copyProperties(Object source, Object target);

From the method signature, we can see that the names of the two tool classes are the same, the method names are the same, even the number, type and name of parameters are the same. But the position of the parameter is opposite. So, if you want to change it, remember to switch the target and source parameters too!

In addition, due to various reasons, the stack information you get is not complete enough to find the problem, so here is a reminder by the way:

If you encounter abnormal information like java.lang.Illegal ArgumentException: Source must not be null or java.lang.Illegal ArgumentException: Target must not be null, you can't find it anywhere, because you passed null values when copying Properties.

Topics: Java Apache Attribute Spring