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对象,所以这里重点看看另外几个比较重要的对象:MethodField

还是以前面例子的一个简单请求类为例,父类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)

现在写一个测试方法来验证一下:

@Test
    public 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
setCode


getClientId
setClientId
setClientSecret
getClientSecret
finalize
wait
wait
wait
equals
toString
hashCode
getClass
clone
notify
notifyAll
registerNatives

从输出可以看出,GetTokenReq类里面2个属性grantType、code对应的get、set方法都打印出来了,但是除此以外,还多了很多方法,它们又是从哪里来的呢?

把上面的输出打印改一下:

@Test
    public 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.
	 */
	@FunctionalInterface
	public 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
	 */
	@Nullable
	public static Method findMethod(Class<?> clazz, String name) {
		return findMethod(clazz, name, EMPTY_CLASS_ARRAY);
	}

方法有2个参数,一个是目标Class,一个是目标方法名称,返回值即是Method。写个例子来看看,获取getCode()方法:

@Test
    public 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()方法:

@Test
    public 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,另一个是被执行方法的目标对象。这个要怎么理解呢,还是写一个例子来看:

@Test
    public void testInvokeMethod()
    {
        
        // 创建一个对象,所有参数都是null
        GetTokenReq getTokenReq = new GetTokenReq();
        Class clazz = getTokenReq.getClass();
        System.out.println("clazz:" + clazz.getName());
        
        // 执行setCode方法,参数为"ss";由于setCode返回void,此时得到的执行结果为null
        Method 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");
	}
  • 调用静态方法 

 通过反射执行目标方法,对于静态方法有一点区别!还是写一个例子来看:

  • GetTokenReq类增加一个静态方法buildV2()
    public static String buildV2()
    {
        return "buildV2这是一个static方法";
    }
  •  按普通的反射的调用方法来执行
    @Test
    public void testInvokeStaticMethod() {
        // 创建一个对象,所有参数都是null
        GetTokenReq getTokenReq = new GetTokenReq();
        Class clazz = getTokenReq.getClass();
        System.out.println("clazz:" + clazz.getName());

        // 执行buildV2方法,无参数传入空数组
        Method setCodeMethod = ReflectionUtils.findMethod(clazz, "buildV2", new Class[] {});
        Object setResult = ReflectionUtils.invokeMethod(setCodeMethod, getTokenReq, new Object[] {});
        System.out.println("buildV2方法执行返回结果:" + setResult);
    }

 运行一下,输出打印结果:

clazz:com.tw.tsm.auth.dto.request.GetTokenReq
buildV2方法执行返回结果:buildV2这是一个static方法

但是对于很多静态方法来说,都是出现工具类里面,比如ReflectionUtils。这种工具类也不建议进行实例化,所以很多时候就是你上面的方式调用,更多的时候是直接class.method。那这种该如何调用呢?

@Test
    public void testInvokeStaticMethod() {
        // 不创建一个对象,直接通过类字面量获取class对象
        Class clazz = GetTokenReq.class;
        System.out.println("clazz:" + clazz.getName());

        // 执行buildV2方法,无参数传入空数组
        Method setCodeMethod = ReflectionUtils.findMethod(clazz, "buildV2", new Class[] {});
        // 对于目标对象直接传入null,不再是实例化的对象了
        Object setResult = ReflectionUtils.invokeMethod(setCodeMethod, null, new Object[] {});
        System.out.println("buildV2方法执行返回结果:" + setResult);
    }
  •  工具类不创建对象实例,直接通过类字面量获取class对象:类.class
  • 对于目标对象直接传入null,不再是实例化的对象了:method.invoke(target, args),target直接传入null

而这一切在JDK的API下了无秘密:

public Object invoke(Object obj,
                     Object... args)
              throws IllegalAccessException,
                     IllegalArgumentException,
                     InvocationTargetException

在具有指定参数的方法对象上调用此方法对象表示的基础方法。 个别参数自动解包以匹配原始形式参数,原始参考参数和参考参数都需要进行方法调用转换。

如果底层方法是静态的,则指定的obj参数将被忽略。 它可能为null。

如果底层方法所需的形式参数的数量为0,则提供的args数组的长度为0或为空。

如果底层方法是一个实例方法,它将使用动态方法查找来调用,如“Java语言规范”第二版,第15.12.4.4节所述; 特别是将会发生基于目标对象的运行时类型的覆盖。

如果底层方法是静态的,则如果尚未初始化该方法,那么声明该方法的类将被初始化。

如果方法正常完成,则返回的值将返回给调用者; 如果值具有原始类型,则首先将其适当地包装在对象中。 但是,如果该值具有基本类型的数组的类型,则该数组的元素不会包含在对象中; 换句话说,返回一个原始类型的数组。 如果底层方法返回类型为void,则调用返回null。

参数

obj - 从底层方法被调用的对象

args - 用于方法调用的参数

结果

由该对象表示的方法在 obj上调用 args

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。

@FunctionalInterface
	public 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目的:

@Test
    public 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.add
					fc.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
	 */
	@Nullable
	public static Field findField(Class<?> clazz, String name) {
		return findField(clazz, name, null);
	}

 方法有2个参数,一个是目标Class,一个是目标字段名称,返回值即是Field。写个例子来看看,获取code属性:

    @Test
    public 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属性:

@Test
    public void testGetField()
    {
        // 创建一个对象,所有参数都是null
        GetTokenReq 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字段属性值:理论上应该是password
        Object 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

继续修改上面的例子,增加一行代码:

@Test
    public void testGetField()
    {
        // 创建一个对象,所有参数都是null
        GetTokenReq 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字段属性值:理论上应该是password
        Object 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);
		}
	}

与此同理,JVM除了对属性的安全性控制有要求外(这里即是不能随便调用声明为private的属性),对Method也有一样的安全机制:

public static void makeAccessible(Method method) {
		if ((!Modifier.isPublic(method.getModifiers()) ||
				!Modifier.isPublic(method.getDeclaringClass().getModifiers())) && !method.isAccessible()) {
            // 最关键的代码:通过反射机制,设置方法可以访问
			method.setAccessible(true);
		}
	}

 写个例子来看看,如何访问私有方法,在GetTokenReq中增加一个build方法,返grantType+code:

    private String build() {
        return "授权码类型:" + grantType + ",授权码:" + code;
    }

然后写个测试方法,直接访问一下试试看会有什么结果:

@Test
    public void testMakeAccessMethod()
    {
        // 创建一个对象,所有参数都是null
        GetTokenReq getTokenReq = new GetTokenReq();
        getTokenReq.setCode("code");
        getTokenReq.setGrantType("Grant");
        Class clazz = getTokenReq.getClass();
        System.out.println("clazz:" + clazz.getName());

        Method getCodeMethod = ReflectionUtils.findMethod(clazz, "build");
        System.out.println(getCodeMethod.getName());

        Object obj = ReflectionUtils.invokeMethod(getCodeMethod, getTokenReq);
        System.out.println("getCode方法执行返回结果:" + obj);
    }

运行一下,输出打印结果,会发现出错了: 

翻译一下这个错误信息:

 无法访问方法或字段:Class org.springframework.util。ReflectionUtils无法访问com.tw.tsm.auth.dto.request类的成员。带有修饰符“private”的GetTokenReq

跟getField类似,解决方法就是加一行代码:ReflectionUtils.makeAccessible(getCodeMethod)

让私有方法能访问即可! 这一切在源码中的体现是method.setAccessible(true)时:

// 指示此对象是否覆盖语言级别的访问检查。初始化为“false”。此字段由字段、方法和构造函数使用。
boolean override;

public void setAccessible(boolean flag) throws SecurityException {
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) sm.checkPermission(ACCESS_PERMISSION);
        setAccessible0(this, flag);
    }

private static void setAccessible0(AccessibleObject obj, boolean flag)
        throws SecurityException
    {
        if (obj instanceof Constructor && flag == true) {
            Constructor<?> c = (Constructor<?>)obj;
            if (c.getDeclaringClass() == Class.class) {
                throw new SecurityException("Cannot make a java.lang.Class" +
                                            " constructor accessible");
            }
        }
        // 将访问标志修改为true
        obj.override = flag;
    }

 之后在反射调用此方法是会进行override标志检查:

public static Object invokeMethod(Method method, @Nullable Object target, @Nullable Object... args) {
		try {
			return method.invoke(target, args);
		}
		catch (Exception ex) {
			handleReflectionException(ex);
		}
		throw new IllegalStateException("Should never get here");
	}


public Object invoke(Object obj, Object... args)
        throws IllegalAccessException, IllegalArgumentException,
           InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, obj, modifiers);
            }
        }
        MethodAccessor ma = methodAccessor;             // read volatile
        if (ma == null) {
            ma = acquireMethodAccessor();
        }
        return ma.invoke(obj, args);
    }

debug一下:

这里就很明显了,如果开启了检验(默认为false)的时候,会执行checkAccess方法,不仅为进行安全检验,也会额外消耗资源导致效率下降,这也是网上很多文章说关闭Method,Field的安全验证可以提升性能的由来,不过满天飞的什么20倍,我猜测多半到处抄最后都不知道谁测出来的:

void checkAccess(Class<?> caller, Class<?> clazz, Object obj, int modifiers)
        throws IllegalAccessException
    {
        if (caller == clazz) {  // quick check
            return;             // ACCESS IS OK
        }
        Object cache = securityCheckCache;  // read volatile
        Class<?> targetClass = clazz;
        if (obj != null
            && Modifier.isProtected(modifiers)
            && ((targetClass = obj.getClass()) != clazz)) {
            // Must match a 2-list of { caller, targetClass }.
            if (cache instanceof Class[]) {
                Class<?>[] cache2 = (Class<?>[]) cache;
                if (cache2[1] == targetClass &&
                    cache2[0] == caller) {
                    return;     // ACCESS IS OK
                }
                // (Test cache[1] first since range check for [1]
                // subsumes range check for [0].)
            }
        } else if (cache == caller) {
            // Non-protected case (or obj.class == this.clazz).
            return;             // ACCESS IS OK
        }

        // If no return, fall through to the slow path.
        // 如果执行到这里,源码已经注明执行会变慢了...
        slowCheckMemberAccess(caller, clazz, obj, modifiers, targetClass);
    }

从类的关系角度来观察此特性,真实的类图如下:

  • AccessibleObject类是Field,Method和Constructor对象的基类
  • 它提供了将反射对象标记为在使用它时抑制默认Java语言访问控制检查的功能
  • 当使用Fields,Methods或Constructors来设置或获取字段,调用方法,或创建和初始化新的类实例时,执行访问检查(对于public,默认(包)访问,受保护和私有成员)。

    在反射对象中设置accessible标志允许具有足够权限的复杂应用程序(如Java对象序列化或其他持久性机制)以通常被禁止的方式操纵对象。

  • 默认情况下,反射对象不可访问。

关于安全检查的臆断

对于上面谈到的反射安全检查,个人认为正是因为反射太强大了,可以执行任何源程序(本地文件,war,tar包,甚至是从网络传来的流)而引进的安全机制。 这样增强了JVM自身的安全,但不可避免的会降低执行效能。这是一个trade-off平衡后的典型例子。但是一般在写程序的时候,对于单个服务代码一般都是相关的人写的,所以关闭了影响并不大

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'}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值