目录
传送门
在比较早的时候,就讨论过java反射的一些用法及概念:
以及反射的基石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)
现在写一个测试方法来验证一下:
@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'}