This is the 24th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021
Background In the recent project, we are coordinating an interface with a third party. We send HTTP requests to the other party and then receive the response from the other party. The codes are all old codes. According to the comment, there was a bug in the Request class written in the SDK of the other party that could not be serialized, so a new Request class was written here. The basic attributes are the same, but the important point is that one attribute is static inner class, and the other two are list attributes, similar to the following:
private List<Order> orders;
private AddRequest.Ticket ticket;
private List<Payment> payments;
Copy the code
AddRequest is the request class that we rewrite ourselves, and the request class in their SDK is MixAddRequest, After we have assembled the request parameters, we use Spring’s BeanUtils copyProperties method to copy the properties from AddRequest to MixAddRequest and send the request. That’s it. It’s supposed to be perfect
The request failed. Nani? The other party said that a necessary field was missing and the parameter verification failed. When I checked the field name, it was a field in the Ticket class. I immediately looked at the code, feeling confident about the old code, thinking that there must be some mistake, or they secretly changed the code and changed the field from optional to mandatory
Sure enough, I found a setting in the code, which should be their problem for sure, and opened a debugging, ready to sentence them to death. Turns out the request sent to them just doesn’t have this field… The method with only one Spring copy property in the middle felt weird at the time
Since there is only one line of code in the middle, there must be something wrong with it. I suspect that the two static inner classes are different, so I wrote my own Demo, and prepared to use the BeanUtils copyProperties method. I wrote two classes and a Main. @data and @toString are lombok plugin annotations that are used to automatically generate getter and setter methods and ToString primitive classes:
@ToString
@Data
public class CopyTest1 {
public String outerName;
public CopyTest1.InnerClass innerClass;
public List<CopyTest1.InnerClass> clazz;
@ToString
@Data
public static class InnerClass {
publicString InnerName; }}Copy the code
The target class:
@ToString
@Data
public class CopyTest2 {
public String outerName;
public CopyTest2.InnerClass innerClass;
public List<CopyTest2.InnerClass> clazz;
@ToString
@Data
public static class InnerClass {
publicString InnerName; }}Copy the code
Test code:
CopyTest1 test1 = new CopyTest1();
test1.outerName = "hahaha";
CopyTest1.InnerClass innerClass = new CopyTest1.InnerClass();
innerClass.InnerName = "hohoho";
test1.innerClass = innerClass;
System.out.println(test1.toString());
CopyTest2 test2 = new CopyTest2();
BeanUtils.copyProperties(test1, test2);
System.out.println(test2.toString());
Copy the code
Test2: @data = null; test2: @data = null; test2: @data = null; The base property (String) is copied over, but the inner class is still null in Test2. This verifies that it is really an internal class problem. I can’t believe my eyes, after all the code running online for so long.
If the inner class has a lot of bean properties or a lot of recursive bean properties, then you can encapsulate a method for recursive copying. I only have one layer here, so I can simply copy it once more
CopyTest1 test1 = new CopyTest1();
test1.outerName = "hahaha";
CopyTest1.InnerClass innerClass = new CopyTest1.InnerClass();
innerClass.InnerName = "hohoho";
test1.innerClass = innerClass;
System.out.println(test1.toString());
CopyTest2 test2 = new CopyTest2();
test2.innerClass = new CopyTest2.InnerClass();
BeanUtils.copyProperties(test1, test2);
BeanUtils.copyProperties(test1.innerClass, test2.innerClass);
System.out.println(test2.toString());
Copy the code
Remember that inner class properties also need to have setter methods, otherwise copy will fail, remember I said there were two List properties, why did I mention that? Can you guess what
In fact, the two classes in the list are also rewrite inner classes, they are also different, they were copied over, why? Since Java generics only work at compile time, at run time, the list property is a collection of objects, and after copy, the Orders property of MixAddRequest is actually a collection of Order classes, but not a collection of its own inner classes. Is a collection of AddRequest’s inner class Order, but because the other side parses JSON, there is no error…
conclusion
- Spring’s BeanUtils CopyProperties method requires getter and setter methods for corresponding properties;
- If there are inner classes with identical properties, but not the same inner class, that is, separate inner classes, Spring will assume that the properties are different and will not copy.
- Generics only work at compile time and cannot be relied upon for runtime limitations;
- Finally, the spring and Apache copy properties have the method source and destination arguments in opposite positions, so be careful when you guide packages and call them.
The last
GetWriteMethod is a JDK method that fetches methods that start with a set, so it doesn’t work without setters.
private static void copyProperties(Object source, Object target, @NullableClass<? > editable,@Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be 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; } PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); List<String> ignoreList = ignoreProperties ! =null ? Arrays.asList(ignoreProperties) : null;
PropertyDescriptor[] var7 = targetPds;
int var8 = targetPds.length;
for(int var9 = 0; var9 < var8; ++var9) {
PropertyDescriptor targetPd = var7[var9];
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();
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 var15) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
}
}
}
}
}
}
Copy the code