一、 Spring 对象拷贝的具体实现 Spring 对象拷贝,基于反射和内省,将源对象字段值装填到目标对象字段上。主要分以下两步:
通过内省,获取源对象和目标对象的属性描述器;
通过反射,解析源属性值,赋值到目标属性中;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 private static void copyProperties (Object source, Object target, Class<?> editable, 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 ); 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(); 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); } } } } } }
二、 BeanUtils.copyProperties实现原理 根据以上分析,整合出 Spring 对象拷贝的实现原理:
通过内省机制,对 Bean 进行拆分,得到每个属性的描述器,缓存在 Map 中,Key为变量名,Value为属性描述器。属性描述器主要包括:属性名称、读取属性值的方法、设置属性值的方法。拷贝过程中,先获取目标属性的写入方法,再获取对应源属性的读取方法,最后通过反射拷贝属性值。
三、JavaBean内省机制 JavaBean 内省,是建立在反射基础上的,通过解析 Bean各个属性的描述器,以便通过属性描述器来操作 Bean 的一种机制。反射是将 Java 类中的各种成分映射成相应的 Java 类,可以获取所有属性以及调用任何方法。与反射不同的是,内省是通过属性描述器来暴露一个 Bean 的属性、方法和时间的,而且只有符合 JavaBean 规则的类的成员才可以调用内生 API 进行操作。
内省在 java.beans.Introspector中的具体实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 Method methodList[] = getPublicDeclaredMethods(beanClass); Method method = methodList[0 ]; if (method == null ) { continue ; } int mods = method.getModifiers();if (Modifier.isStatic(mods)) { continue ; } String name = method.getName(); Class<?>[] argTypes = method.getParameterTypes(); Class<?> resultType = method.getReturnType(); int argCount = argTypes.length;PropertyDescriptor pd = null ; if (argCount == 0 ) { if (name.startsWith(GET_PREFIX)) { pd = new PropertyDescriptor(this .beanClass, name.substring(3 ), method, null ); } else if (resultType == boolean .class && name .startsWith (IS_PREFIX )) { pd = new PropertyDescriptor(this .beanClass, name.substring(2 ), method, null ); } } else if (argCount == 1 ) { if (int.class.equals(argTypes[0]) && name.startsWith(GET_PREFIX)) { pd = new IndexedPropertyDescriptor(this .beanClass, name.substring(3 ), null , null , method, null ); } else if (void .class .equals (resultType ) && name .startsWith (SET_PREFIX )) { pd = new PropertyDescriptor(this .beanClass, name.substring(3 ), null , method); if (throwsException(method, PropertyVetoException.class )) { pd.setConstrained(true ); } } } else if (argCount == 2 ) { if (void.class.equals(resultType) && int.class.equals(argTypes[0]) && name.startsWith(SET_PREFIX)) { pd = new IndexedPropertyDescriptor(this .beanClass, name.substring(3 ), null , null , null , method); if (throwsException(method, PropertyVetoException.class )) { pd.setConstrained(true ); } } } return PropertyDescriptor;
由此可以看出,一个类的方法名称、入参个数、反参类型是JavaBean 内省的主要要素,可以总结为:
只能内省一个类暴露的 public 非静态方法;
可以内省标准化的 set 方法,如 void setAge(Integer age);
可以内省标准化的 get 方法,如 ResultType getAge();
可以内省设置索引属性的方法,如 setChild(Integer index, Child child);
可以内省获取索引属性的方法,如 getChild(Integer index);
可以内省获取基本类型布尔值的且以 is 开头的方法,如 boolean isMale();
五、总结 Spring 对象拷贝,基于反射和内省机制,通过属性描述器,将源属性值写入目标属性。如今 Spring 架构已被广泛使用,旗下各种好用的工具也是顺手拈来,但无端的滥用也潜藏着一些问题。比如 Spring 对象拷贝,要求操作的对象必须符合 JavaBean 规范,否则将无法拷贝。如拷贝包装类型的布尔值,其读取方法为 Boolean isMale ,不符合 JavaBean 规范,对应的目标属性值一定是 null。