才发觉好久没有写过博客了,服务器一直在续费但是都是做测试使用,今天终于下定了决心一定要更新一次,就拿BeanUtils开刀吧。
这几天在做项目的时候,看到学长们留下来了一堆getter和setter方法用于将从持久层获取到的数据po类转交给dto,过多的getter和setter使得代码“又臭又长”。于是想起来以前初学Spring的时候有个叫BeanUtils的东西,好像是可以复制类内的成员变量数据,一直没用上过就进行了一番研究和尝试。
BeanUtils的使用场景
假设查询一个学生信息,持久层返回的对象是Student类型,它包含以下成员变量:
private String name;
private int id;
private String password;
private String classes;
//省略getter and setter
但是返回到前台的数据中,还需要加入一个Date,并且不需要password。如果按MVC的思想,那么在各层传输应该使用DTO,因此便存在一个StudentDto类,包含所需传输的成员变量,如:
private String name;
private int id;
private String classes;
private Date date;
//省略getter and setter
这个时候想在Service层将PO对象的成员变量值赋值给DTO对象,传统的方法当然是在用DTO的各个setter传入PO类的getter方法,然而如果成员变量过多,不免会带来许多setter代码,加大工作负担。
BeanUtils的copyProperties方法就是用来解决这个问题的,只需将原对象(PO)及目标对象(DTO)作为参数传入,copyProperties便会将两者相同类型相同变量名(不含基础类型与其装箱类型)的变量从源对象赋值到目标对象,从而实现对象间变量值的复制。
源类和目标类可以各自独立,并不要求有同一父类,或是继承统一接口等。
看网上有些评论说String和Date可以互转,但是实在没试出来…..
第一次接触这个东西的时候大概能反应过来是通过反射的机制实现的,也不知道从什么时候开始喜欢上研究别人写的代码,于是抱着好奇之心研究了一下实现的过程。
BeanUtils.copyProperties的实现过程
关于copyProperties,官方的JavaDoc是这么解释这个方法的:
Copy the property values of the given source bean into the given target bean.
Note: The source and target classes do not have to match or even be derived
from each other, as long as the properties match. Any bean properties that the
source bean exposes but the target bean does not will silently be ignored.
简单的进行翻译便是:这个方法可以将所给源bean的值赋给目标bean,并且这两个bean的类不需要有任何的匹配或是关联关系,只需要属性名匹配即可,如果源bean有的而目标bean没有的值将会默认的进行忽视。
copyProperties有四个重载的方法,主要的核心代码是这个方法
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);
}
}
}
}
}
}
首先,方法先判断传入的source和target是否为空,紧接着通过getClass()方法获得目标对象的Class,并通过getPropertyDescriptors方法获取到一个PropertyDescriptor的对象,虽然不知道这个对象的作用但是代码中频频出现,猜测是存储类中方法的一个渠道?然后以目标bean作为for的对象进行循环,查找source的bean中同名且不在ignoreList列表中的变量进行赋值,由于是从目标bean中取变量名的值,且每步都有null判断,因此无须担心两个类之间成员变量个数不同可能引发的问题,只需保证同名即可进行赋值。
读完BeanUtils代码引发的思考
1. 提供多个重载方法
翻阅代码,可以发现copyProperties有四个重载方法,分别是
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, null, (String[]) null);
}
public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {
copyProperties(source, target, editable, (String[]) null);
}
public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
copyProperties(source, target, null, ignoreProperties);
}
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {...}
可以看到,其实前面三个重载方法都是调用第四个重载方法进行执行的,只是在前三个重载方法中,将第四个方法所需要的值传入null
值。然后在第四个重载方法中,进行null值的判断。
刚看到这个的时候还是有点意外的,因为平时用到的重载方法可能是相同功能但是传入参数的意义不同,于是进行转换后再复用核心方法,而以参数个数作重构再填null
值调用核心方法的方式确实令我很惊讶,第一反应便是 调用者难道不懂得自己填null
值吗?
后来经过一阵思考,我觉得这样写有这样写的道理。
面向对象的三大特性中,一个特性就是封装,封装以我的理解便是隐藏掉所需要隐藏的东西,只向外界暴露封装者认为的最简单的编程接口,所以对于调用者来说,他在调用时只需要需要传入参数即可,使用者现在有两个参数就传两个,未来有三个参数就传三个,无须关心内部代码的执行逻辑,至于将不需要的参数传null
值,使用者哪知道可以传null
值哟,万一Exception
了呢?
之所以一开始看到时不能理解,或许就是我这种菜鸡跟大神的区别吧…
2. 不要相信调用者的输入
在参数最多的那个重构方法(也就是最核心的重构方法)中,有一段这样的代码
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;
}
这段代码其实简单易懂,它的意义就在于判断目标bean是否为editable的实例,不是就报错。这其实是代码健壮性的一种保证——谁知道调用者会不会调皮传进来一个跟目标bean完全不相干的Class呢?
这样的情况其实还是挺多见的,尤其是在Controller里,调用者传入的数据不一定就能按照你的意愿走,所以,多判断,多判断,多判断,万一就有人故意那么调皮呢?
不过不传这个参数程序也能运行,那传入的意义是什么呢?写文章的时候才意识到这个问题,再研读了一下,发现方法中获取目标bean方法跟actualEditable
有关,而如果传入了Class的参数editable
会赋值给actualEditable
所以猜测了一下如果目标bean和源bean有同名的变量但是不想被拷贝的话,可以写一个新的类定义需要拷贝的变量然后让目标bean继承他,同时将这个新类的Class传值给copyProperties
方法,这样就会以这个新类的成员变量(当然取决于getter/setter方法)为准进行值拷贝,当然这些都是我猜测的,还没实验过,不过读了下代码感觉八九不离十=。=
BeanUtils还能干嘛?
BeanUtils这个类总共600多行,而copyProperties仅占了100多一点(当然我比较懒这些都是包含JavaDoc的计算),那剩下的500多行都在干嘛? 确实,有些代码是为了copyProperties这个方法而服务的,不过有些方法在copyProperties中确实没有调用,而这些方法,都是public static
的
百度了一下BeanUtils,相关的资料都是跟copyPropertiesy有关的,没有人提及到其他的方法,但这些方法定义为public static
就证明他们一定有可以在外界调用的作用,于是我大致的看了一下,发现了这个
/**
* Find a method with the given method name and the given parameter types,
* declared on the given class or one of its superclasses. Prefers public methods,
* but will return a protected, package access, or private method too.
* <p>Checks {@code Class.getMethod} first, falling back to
* {@code findDeclaredMethod}. This allows to find public methods
* without issues even in environments with restricted Java security settings.
* @param clazz the class to check
* @param methodName the name of the method to find
* @param paramTypes the parameter types of the method to find
* @return the Method object, or {@code null} if not found
* @see Class#getMethod
* @see #findDeclaredMethod
*/
public static Method findMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) {
try {
return clazz.getMethod(methodName, paramTypes);
}
catch (NoSuchMethodException ex) {
return findDeclaredMethod(clazz, methodName, paramTypes);
}
}
/**
* Find a method with the given method name and the given parameter types,
* declared on the given class or one of its superclasses. Will return a public,
* protected, package access, or private method.
* <p>Checks {@code Class.getDeclaredMethod}, cascading upwards to all superclasses.
* @param clazz the class to check
* @param methodName the name of the method to find
* @param paramTypes the parameter types of the method to find
* @return the Method object, or {@code null} if not found
* @see Class#getDeclaredMethod
*/
public static Method findDeclaredMethod(Class<?> clazz, String methodName, Class<?>... paramTypes) {
try {
return clazz.getDeclaredMethod(methodName, paramTypes);
}
catch (NoSuchMethodException ex) {
if (clazz.getSuperclass() != null) {
return findDeclaredMethod(clazz.getSuperclass(), methodName, paramTypes);
}
return null;
}
}
这不就是封装好的查找类中相关方法的方法吗!先在该类中用getMethod
查找,找不到的话尝试用getDeclaredMethod
方法查找,再找不到就找这个类的父类的getDeclaredMethod
方法……
至于getMethod
和getDeclaredMethod
方法的区别,百度上有很多,通俗的说就是
getMethod()
:获取当前类及所有继承的父类的public修饰的方法。仅包括public
getDeclaredMethod()
:获取当前类的所有方法,包括public/private/protected/default修饰的方法。但不包括继承的方法。
再仔细瞅瞅上面的这段代码
catch (NoSuchMethodException ex) {
if (clazz.getSuperclass() != null) {
return findDeclaredMethod(clazz.getSuperclass(), methodName, paramTypes);
}
return null;
}
是不是就发现了它其他的妙用了呢?
在略读了BeanUtils的代码后,不免让我有些感慨:什么时候我才能这么优秀!