本文主要是介绍Java反射系列(3):从spring反射工具ReflectionUtils说起,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
传送门
兼容性引发的"血案"
ReflectionUtils的原理
目的有三
ReflectionUtils的API使用
Method
getAllDeclaredMethods
findMethod
invokeMethod
Field
getDeclaredFields
findField
getField
makeAccessible
Constructor
accessibleConstructor
传送门
在比较早的时候,就讨论过java反射的一些用法及概念:
Java反射系列(1):入门基础
以及反射的基石Class对象!
Java反射系列(2):从Class获取父类方法说起
今天就从工作中实际的例子来看看反射的应用。
兼容性引发的"血案"
对于Java程序员来说,调用别人写的类库、使用开源的Jar包似乎无可厚非,甚至是"天经地义"!正是开源的这种无私精神带来了这门语言的繁荣,打造了其遍地开花的生态。而github更是诸多极客们一展风采的宝库舞台,但是这也带来了问题:里面的各种库质量良莠不齐,既有spring这种Java体系"基石"类的产品,也有很多"小而美"的工具包,比如hutool包!
但凡事有利就有弊,最近系统就出现了因为升级hutool包导致功能不可用的场景:
系统里面有一个对db数据自动加解密Mybatis插件,存储时将表里面的数据自动加密,读取时自动解密。其实现原理就是通过Mybatis的拦截器,拦截请求之后,得到原始里面数据,加解密之后用反射机制替换原始数据以此实现动态加解密功能。功能并不复杂,抛开效率性能等因素之外,也是一个比较合理的数据"脱敏"实现机制。但是里面的的反射机制用到了hutool的包,升级时版本不兼容导致功能不可能用,最终用sprig包的ReflectionUtils工具进行了紧急替换,评估之后最终移除了该hutool的引用,后续不再使用!
由此可见,一个好的开源产品,除了功能强大易用之外,升级的版本兼容性也是必不可少的条件之一。这里引用一下网上对此的观点:
ReflectionUtils的原理
对于上面提到的加解密插件,后续可以提供一个实现到码云供参考,这里仅仅表明它是反射机制使用的一个具体场景,不是这里的重点,就不做过多讨论。
目的有三
而这里重点目标有三个:
- 使用开源包时要慎重,尤其是在生产级应用,越是大公司对此要求越严格
- ReflectionUtils的使用,熟悉一下里面的API
- 最终了解反射的用途,深入反射机制
ReflectionUtils的API使用
ReflectionUtils是spring的核心包提供工具类,路径为:org.springframework.util.ReflectionUtils。
在Java反射系列(2):从Class获取父类方法说起里面讨论了Class对象,所以这里重点看看另外几个比较重要的对象:Method,Field。
还是以前面例子的一个简单请求类为例,父类BaseAuthReq :
public class BaseAuthReq
{/** 应用ID */private String clientId;/** 应用身份密钥 */private String clientSecret;public String getClientId(){return clientId;}public void setClientId(String clientId){this.clientId = clientId;}public String getClientSecret(){return clientSecret;}public void setClientSecret(String clientSecret){this.clientSecret = clientSecret;}
}
GetTokenReq继承BaseAuthReq:
public class GetTokenReq extends BaseAuthReq
{/** 授权码类型 */private String grantType;/** 授权码 */private String code;public String getGrantType(){return grantType;}public void setGrantType(String grantType){this.grantType = grantType;}public String getCode(){return code;}public void setCode(String code){this.code = code;}
}
Method
GetTokenReq类里面有2个属性,grantType、code及对应的get、set方法,那通过ReflectionUtils工具类如何获取这些方法呢?
getAllDeclaredMethods
观察下它的API,里面有一个方法,看起来可以满足需求:
public static Method[] getAllDeclaredMethods(Class<?> leafClass)
现在写一个测试方法来验证一下:
@Testpublic void testMethod() {// 获取当前类的Class对象Class clazz = GetTokenReq.class;System.out.println("clazz:" + clazz.getName());Method[] allDeclaredMethods = ReflectionUtils.getAllDeclaredMethods(clazz);for (Method method : allDeclaredMethods) {System.out.println(method.getName());}}
运行一下,输出打印结果:
clazz:com.tw.tsm.auth.dto.request.GetTokenReq
// 以下4个为目标方法
setGrantType
getCode
getGrantType
setCodegetClientId
setClientId
setClientSecret
getClientSecret
finalize
wait
wait
wait
equals
toString
hashCode
getClass
clone
notify
notifyAll
registerNatives
从输出可以看出,GetTokenReq类里面2个属性grantType、code对应的get、set方法都打印出来了,但是除此以外,还多了很多方法,它们又是从哪里来的呢?
把上面的输出打印改一下:
@Testpublic void testMethod(){// 获取当前类的Class对象Class clazz = GetTokenReq.class;System.out.println("clazz:" + clazz.getName());Method[] allDeclaredMethods = ReflectionUtils.getAllDeclaredMethods(clazz);for (Method method : allDeclaredMethods){// 输出方法所在类,及方法名称System.out.println("class:" + method.getDeclaringClass().getName() + ", method:" + method.getName());}}
运行一下,输出打印结果:
clazz:com.tw.tsm.auth.dto.request.GetTokenReq
class:com.tw.tsm.auth.dto.request.GetTokenReq, method:getGrantType
class:com.tw.tsm.auth.dto.request.GetTokenReq, method:setCode
class:com.tw.tsm.auth.dto.request.GetTokenReq, method:getCode
class:com.tw.tsm.auth.dto.request.GetTokenReq, method:setGrantType
class:com.tw.tsm.auth.dto.BaseAuthReq, method:setClientId
class:com.tw.tsm.auth.dto.BaseAuthReq, method:setClientSecret
class:com.tw.tsm.auth.dto.BaseAuthReq, method:getClientSecret
class:com.tw.tsm.auth.dto.BaseAuthReq, method:getClientId
class:java.lang.Object, method:finalize
class:java.lang.Object, method:wait
class:java.lang.Object, method:wait
class:java.lang.Object, method:wait
class:java.lang.Object, method:equals
class:java.lang.Object, method:toString
class:java.lang.Object, method:hashCode
class:java.lang.Object, method:getClass
class:java.lang.Object, method:clone
class:java.lang.Object, method:notify
class:java.lang.Object, method:notifyAll
class:java.lang.Object, method:registerNatives
从上面的输出可以清晰的看到,getAllDeclaredMethods可以打印类自身及所有父类的方法:
直接看下源码,看下到底是怎么实现的:
/*** Get all declared methods on the leaf class and all superclasses.* Leaf class methods are included first.* @param leafClass the class to introspect* @throws IllegalStateException if introspection fails*/public static Method[] getAllDeclaredMethods(Class<?> leafClass) {// 声明一个Method集合,现在是空的final List<Method> methods = new ArrayList<>(20);// 如何获取Class的Method集合,就在这里面实现的doWithMethods(leafClass, methods::add);return methods.toArray(EMPTY_METHOD_ARRAY);}
所以跟进doWithMethods,注意这个方法要求传一个lambda函数MethodCallback,
/*** Action to take on each method.*/@FunctionalInterfacepublic interface MethodCallback {/*** Perform an operation using the given method.* @param method the method to operate on*/void doWith(Method method) throws IllegalArgumentException, IllegalAccessException;}
这里的实现就是将Method添加到刚才的methods集合里面(要了解lambda函数的见JAVA8-lambda表达式1:什么是lambda表达式)
public static void doWithMethods(Class<?> clazz, MethodCallback mc, @Nullable MethodFilter mf) {// 获得Method方法列表,包括所有继承Method[] methods = getDeclaredMethods(clazz, false);for (Method method : methods) {// 条件过滤,这里为null,先不管它,都是一些辅助功能if (mf != null && !mf.matches(method)) {continue;}try {// 执行方法回调,这里就是将前面的methods::add:将当前方法添加到集合中mc.doWith(method);}catch (IllegalAccessException ex) {throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex);}}// 以下递归调用,获取父类的Method方法列表if (clazz.getSuperclass() != null && (mf != USER_DECLARED_METHODS || clazz.getSuperclass() != Object.class)) {doWithMethods(clazz.getSuperclass(), mc, mf);}else if (clazz.isInterface()) {for (Class<?> superIfc : clazz.getInterfaces()) {doWithMethods(superIfc, mc, mf);}}}
跟进getDeclaredMethods(clazz, false),马上就看到庐山真面了:
private static Method[] getDeclaredMethods(Class<?> clazz, boolean defensive) {Assert.notNull(clazz, "Class must not be null");// 本地缓存,出于性能设计,先不管它Method[] result = declaredMethodsCache.get(clazz);if (result == null) {try { // 这几个方法里面最核心的代码,通过Class获取声明的方法列表,jdk自带,其它的都是辅助功能Method[] declaredMethods = clazz.getDeclaredMethods();List<Method> defaultMethods = findConcreteMethodsOnInterfaces(clazz);if (defaultMethods != null) {result = new Method[declaredMethods.length + defaultMethods.size()];System.arraycopy(declaredMethods, 0, result, 0, declaredMethods.length);int index = declaredMethods.length;for (Method defaultMethod : defaultMethods) {result[index] = defaultMethod;index++;}}else {result = declaredMethods;}// 本地缓存,出于性能设计,先不管它declaredMethodsCache.put(clazz, (result.length == 0 ? EMPTY_METHOD_ARRAY : result));}catch (Throwable ex) {throw new IllegalStateException("Failed to introspect Class [" + clazz.getName() +"] from ClassLoader [" + clazz.getClassLoader() + "]", ex);}}return (result.length == 0 || !defensive) ? result : result.clone();
可通过debug看到具体代码执行验证:
有了getAllDeclaredMethods方法,要获得某一指定方法名称的就很容易了,直接从getAllDeclaredMethods列表里面过滤,这是很自然的事情。ReflectionUtils也提供了类似的method查找方法
findMethod
/*** Attempt to find a {@link Method} on the supplied class with the supplied name* and no parameters. Searches all superclasses up to {@code Object}.* <p>Returns {@code null} if no {@link Method} can be found.* @param clazz the class to introspect* @param name the name of the method* @return the Method object, or {@code null} if none found*/@Nullablepublic static Method findMethod(Class<?> clazz, String name) {return findMethod(clazz, name, EMPTY_CLASS_ARRAY);}
方法有2个参数,一个是目标Class,一个是目标方法名称,返回值即是Method。写个例子来看看,获取getCode()方法:
@Testpublic void testFindMethod() {// 获取当前类的Class对象Class clazz = GetTokenReq.class;System.out.println("clazz:" + clazz.getName());// 获得getCode方法Method method = ReflectionUtils.findMethod(clazz, "getCode");System.out.println(method.getName());}
直接看下源码,看下到底是怎么实现的:
// 声明一个空对象数组
private static final Class<?>[] EMPTY_CLASS_ARRAY = new Class<?>[0];public static Method findMethod(Class<?> clazz, String name) {return findMethod(clazz, name, EMPTY_CLASS_ARRAY);}public static Method findMethod(Class<?> clazz, String name, @Nullable Class<?>... paramTypes) {Assert.notNull(clazz, "Class must not be null");Assert.notNull(name, "Method name must not be null");Class<?> searchType = clazz;// 通过while循环,从当前类递归向上查找while (searchType != null) {// 果不其然,通过前面介绍的getDeclaredMethods获得方法列表,再进行方法名称过滤Method[] methods = (searchType.isInterface() ? searchType.getMethods() :getDeclaredMethods(searchType, false));for (Method method : methods) {if (name.equals(method.getName()) && (paramTypes == null || hasSameParams(method, paramTypes))) {return method;}}// 向上查找父类searchType = searchType.getSuperclass();}return null;}
现在再改一下上面的例子,获取setCode()方法:
@Testpublic void testFindMethod() {// 获取当前类的Class对象Class clazz = GetTokenReq.class;System.out.println("clazz:" + clazz.getName());Method getCodeMethod = ReflectionUtils.findMethod(clazz, "getCode");System.out.println(getCodeMethod.getName());Method setCodeMethod = ReflectionUtils.findMethod(clazz, "setCode");System.out.println(setCodeMethod.getName());}
运行一下,输出打印结果,会发现出错了:
这是怎么回事呢?观察一下getCode()与setCode(String code方法) :
public String getCode(){return code;}public void setCode(String code){this.code = code;}
细心的你发现没有,区别在于,setCode是有一个String参数的,而getCode是无参数的。所以对于有参数的方法,获得Method需要如下写法:
// 指明参数的类型,用一个数组表示
Method setCodeMethod = ReflectionUtils.findMethod(clazz, "setCode", new Class[]{String.class});
invokeMethod
对于反射机制来说,非常重要的一个功能就是通过反射执行目标方法,否则大多数场景下的反射都没意义。看看ReflectionUtils提供的方法:
// 默认的空参数列表
private static final Object[] EMPTY_OBJECT_ARRAY = new Object[0];public static Object invokeMethod(Method method, @Nullable Object target) {return invokeMethod(method, target, EMPTY_OBJECT_ARRAY);}
有2个参数:一个是要执行的方法method,另一个是被执行方法的目标对象。这个要怎么理解呢,还是写一个例子来看:
@Testpublic void testInvokeMethod(){// 创建一个对象,所有参数都是nullGetTokenReq getTokenReq = new GetTokenReq();Class clazz = getTokenReq.getClass();System.out.println("clazz:" + clazz.getName());// 执行setCode方法,参数为"ss";由于setCode返回void,此时得到的执行结果为nullMethod setCodeMethod = ReflectionUtils.findMethod(clazz, "setCode", new Class[] {String.class});Object setResult = ReflectionUtils.invokeMethod(setCodeMethod, getTokenReq, new Object[] {"ss"});System.out.println("setCode方法执行返回结果:" + setResult);// 执行getCode方法,由于getCode返回String,此时得到的执行结果为为"ss"Method getCodeMethod = ReflectionUtils.findMethod(clazz, "getCode");Object getResult = ReflectionUtils.invokeMethod(getCodeMethod, getTokenReq);System.out.println("getCode方法执行返回结果:" + getResult);}
运行一下,输出打印结果:
clazz:com.tw.tsm.auth.dto.request.GetTokenReq
setCode方法执行返回结果:null
getCode方法执行返回结果:ss
可以看出,invokeMethod方法同findMethod一样,当有参数时,需要传递参数类型的。这一点从源码就能发现:
public static Object invokeMethod(Method method, @Nullable Object target, @Nullable Object... args) {try {
// 最后一个参数args为动态参数,指的就是方法要接收的参数列表return method.invoke(target, args);}catch (Exception ex) {handleReflectionException(ex);}throw new IllegalStateException("Should never get here");}
Field
GetTokenReq类里面有2个属性,grantType、code及对应的get、set方法;前面通过ReflectionUtils工具类获取了对应的方法,依赖的是反射机制提供的Method对象。那通过ReflectionUtils工具类如何获取这些属性呢?这就要依赖反射机制提供的Field对象了。
getDeclaredFields
观察下它的API,里面有一个方法,看起来可以满足需求:
private static Field[] getDeclaredFields(Class<?> clazz)
但是很遗憾,这个方法是私有的,ReflectionUtils类不能直接调用。不过它提供了另外的一个方法:
public static void doWithFields(Class<?> clazz, FieldCallback fc) { doWithFields(clazz, fc, null); }
其中FieldCallback也是一个lambda函数,有点类似MethodCallback。
@FunctionalInterfacepublic interface FieldCallback {/*** Perform an operation using the given field.* @param field the field to operate on*/void doWith(Field field) throws IllegalArgumentException, IllegalAccessException;}
所以可以仿照getAllDeclaredMethods来达到getDeclaredFields目的:
@Testpublic void testGetDeclaredFields(){// 获取当前类的Class对象Class clazz = GetTokenReq.class;System.out.println("clazz:" + clazz.getName());List<Field> fieldList = Lists.newArrayList();ReflectionUtils.doWithFields(clazz, fieldList::add);for (Field field : fieldList){// 输出属性所在类,及属性名称System.out.println("class:" + field.getDeclaringClass().getName() + ", field:" + field.getName());}}
运行一下,输出打印结果:
clazz:com.tw.tsm.auth.dto.request.GetTokenReq
class:com.tw.tsm.auth.dto.request.GetTokenReq, field:grantType
class:com.tw.tsm.auth.dto.request.GetTokenReq, field:code
class:com.tw.tsm.auth.dto.BaseAuthReq, field:clientId
class:com.tw.tsm.auth.dto.BaseAuthReq, field:clientSecret
从输出可以看出,GetTokenReq类里面2个属性grantType、code及父类clientId、clientSecret都打印出来了,但是Object类的相关属性却没有,这是为什么呢?直接看下源码,看下到底是怎么实现的:
public static void doWithFields(Class<?> clazz, FieldCallback fc, @Nullable FieldFilter ff) {// Keep backing up the inheritance hierarchy.Class<?> targetClass = clazz;do {// 获取当前类所有声明的属性列表Field[] fields = getDeclaredFields(targetClass);for (Field field : fields) {if (ff != null && !ff.matches(field)) {continue;}try {// 执行回调函数,这里就是测试方法的里面的list.addfc.doWith(field);}catch (IllegalAccessException ex) {throw new IllegalStateException("Not allowed to access field '" + field.getName() + "': " + ex);}}targetClass = targetClass.getSuperclass();}// 递归调用,向上查找,排除Object类,所以前面输出没有Object的相关属性while (targetClass != null && targetClass != Object.class);
通过此方法可以发现,最终的关键回到上面的私有方法getDeclaredFields:
private static Field[] getDeclaredFields(Class<?> clazz) {Assert.notNull(clazz, "Class must not be null");// 本地缓存,出于性能设计,先不管它Field[] result = declaredFieldsCache.get(clazz);if (result == null) {try {// 最关键的代码:通过反射机制,获取所有声明的属性列表result = clazz.getDeclaredFields();// 本地缓存,出于性能设计,先不管它declaredFieldsCache.put(clazz, (result.length == 0 ? EMPTY_FIELD_ARRAY : result));}catch (Throwable ex) {throw new IllegalStateException("Failed to introspect Class [" + clazz.getName() +"] from ClassLoader [" + clazz.getClassLoader() + "]", ex);}}return result;}
findField
/*** Attempt to find a {@link Field field} on the supplied {@link Class} with the* supplied {@code name}. Searches all superclasses up to {@link Object}.* @param clazz the class to introspect* @param name the name of the field* @return the corresponding Field object, or {@code null} if not found*/@Nullablepublic static Field findField(Class<?> clazz, String name) {return findField(clazz, name, null);}
方法有2个参数,一个是目标Class,一个是目标字段名称,返回值即是Field。写个例子来看看,获取code属性:
@Testpublic void testFindField() {// 获取当前类的Class对象Class clazz = GetTokenReq.class;System.out.println("clazz:" + clazz.getName());Field field = ReflectionUtils.findField(clazz, "code");System.out.println("class:" + field.getDeclaringClass().getName() + ", field:" + field.getName());}
运行一下,输出打印结果:
clazz:com.tw.tsm.auth.dto.request.GetTokenReq
class:com.tw.tsm.auth.dto.request.GetTokenReq, field:code
getField
上面通过findField方法可以获取对象的属性,那怎么获取这个属性值呢?写个例子来看看,获取grantType属性:
@Testpublic void testGetField(){// 创建一个对象,所有参数都是nullGetTokenReq getTokenReq = new GetTokenReq();// 设置grantType为密码模式getTokenReq.setGrantType("password");Class clazz = getTokenReq.getClass();System.out.println("clazz:" + clazz.getName());// 获取grantType字段属性Field field = ReflectionUtils.findField(clazz, "grantType");System.out.println("class:" + field.getDeclaringClass().getName() + ", field:" + field.getName());// 获得grantType字段属性值:理论上应该是passwordObject grantType = ReflectionUtils.getField(field, getTokenReq);System.out.println("通过反射获取属性:" + field.getName() + ", 值为:" + grantType);}
运行一下,输出打印结果,会发现出错了:
翻译一下这个错误信息:
无法访问方法或字段:Class org.springframework.util。ReflectionUtils无法访问com.tw.tsm.auth.dto.request类的成员。带有修饰符“private”的GetTokenReq
说直白点就是:不能直接访问私有字段属性,因为grantType在GetTokenReq类中是private:
public class GetTokenReq extends BaseAuthReq
{/** 授权码类型 */private String grantType;
}
直接看源码:
public static Object getField(Field field, @Nullable Object target) {try {// 最关键的代码:通过反射机制,获取属性值return field.get(target);}catch (IllegalAccessException ex) {handleReflectionException(ex);}throw new IllegalStateException("Should never get here");}public static void handleReflectionException(Exception ex) {if (ex instanceof NoSuchMethodException) {throw new IllegalStateException("Method not found: " + ex.getMessage());}if (ex instanceof IllegalAccessException) {// 就是这里出现的错误throw new IllegalStateException("Could not access method or field: " + ex.getMessage());}if (ex instanceof InvocationTargetException) {handleInvocationTargetException((InvocationTargetException) ex);}if (ex instanceof RuntimeException) {throw (RuntimeException) ex;}throw new UndeclaredThrowableException(ex);}
要解决这个问题,就需要用到下面这个方法。
makeAccessible
继续修改上面的例子,增加一行代码:
@Testpublic void testGetField(){// 创建一个对象,所有参数都是nullGetTokenReq getTokenReq = new GetTokenReq();// 设置grantType为密码模式getTokenReq.setGrantType("password");Class clazz = getTokenReq.getClass();System.out.println("clazz:" + clazz.getName());// 获取grantType字段属性Field field = ReflectionUtils.findField(clazz, "grantType");System.out.println("class:" + field.getDeclaringClass().getName() + ", field:" + field.getName());// 设置属性可以访问ReflectionUtils.makeAccessible(field);// 获得grantType字段属性值:理论上应该是passwordObject grantType = ReflectionUtils.getField(field, getTokenReq);System.out.println("通过反射获取属性:" + field.getName() + ", 值为:" + grantType);}
运行一下,输出打印结果,发现正确输出了:
clazz:com.tw.tsm.auth.dto.request.GetTokenReq
class:com.tw.tsm.auth.dto.request.GetTokenReq, field:grantType
通过反射获取属性:grantType, 值为:password
直接看下源码,看下到底是怎么实现的:
public static void makeAccessible(Field field) {if ((!Modifier.isPublic(field.getModifiers()) ||!Modifier.isPublic(field.getDeclaringClass().getModifiers()) ||Modifier.isFinal(field.getModifiers())) && !field.isAccessible()) {
// 最关键的代码:通过反射机制,设置属性可以访问
field.setAccessible(true);}}
Constructor
accessibleConstructor
写个例子来看看,如何创建一个对象:
try {Class clazz = getTokenReq.getClass();System.out.println("clazz:" + clazz.getName());Constructor<GetTokenReq> constructor = ReflectionUtils.accessibleConstructor(clazz, String.class, String.class);GetTokenReq req = constructor.newInstance("a", "b");System.out.println(req);} catch (Exception e) {e.printStackTrace();}
运行一下,输出打印结果:
GetTokenReq{grantType='a', code='b'}
这篇关于Java反射系列(3):从spring反射工具ReflectionUtils说起的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!