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:
- Performance comparison of several copyProperties tooling classes: https://www.jianshu.com/p/bcb...
- BeanCopier source code implementation in CGLIB: https://www.jianshu.com/p/f8b...
- Java Bean Copy Framework Performance Comparison: https://yq.aliyun.com/article...
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.