前言
我们先对前几天的学习进行总结,前几天我们主要结合源码学习了java中的集合,重点分析了HashMap散列桶的实现,还让大伙儿去看红黑树。今天就来学习java反射相关的东西,反射可是java一个很重要的高级特性,很多框架都是基于反射实现的,提高对反射相关机制的了解也有利于我们将来造轮子。接下来我们结合源码以及java虚拟机来分析反射。
定义
JAVA反射机制是在运行状态中,对于任意一个实体类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。这就意味着我们在对一个类未知的情况下可以通过多种方式获取类,调用方法和属性,这是JDK中动态代理实现的基础,当然还有别的动态代理实现的方式,例如CGLIB等
基础复习
不知道各位看官对反射的基本使用忘记没有,我在这里带大家复习一下。
首先,我们都知道java中的实例对象都是通过对应的Class对象创建的,而Class对象的加载在我的java虚拟机类加载机制中已经有了分析,感兴趣的可以看https://blog.csdn.net/qq_41861259/article/details/103050011。所以如果我们要操作一个对象,首先要获取到他的Class对象,那么获取Class对象的方式有哪些呢?主要有三种方式,如代码所示:
//通过class常量获取
Class c = Integer.class;
//通过类的全限定名获取
Class c1 = Class.forName("java.lang.Integer");
//通过实例获取Class对象
Integer integer = new Integer(1);
integer.getClass();
当我们获取到Class对象,那么我们要操作一个实例对象还需要什么呢?那就是方法,我们通过Class对象得到实例,通过方法名获取方法,调用方法,当然我们的构造方法和私有方法也都是可以获取的,需要设置setAccessible(true),还可以通过getSuperclass()获取父类
//得到Class对象之后
//获取方法,指定方法名和参数
Method method = c.getMethod(”方法名“,"参数“);
//实例化对象
Object o = c.newInstance();
//调用invoke方法执行方法
method.invoke(o,"hahah");
正题
先看下反射的整体框架,这是我自己画的,也就这个水平了,哈哈!其实最开始我的想法是通过Method、Filed这两个类成员来展开画正题的框架,并且实际画的过程中两者也都整体对称的感觉,但是当加入一些依赖的时候,渐渐的Reflection就成为了中心,此时真是感概那些大佬的设计啊!!!
我们先来看Class对象的获取,这里我使用Class的forName方法Class c = Class.forName("com.reflect.Person");
我们先补充说明下@CallerSensitive注解,我在网上一个解释:
这个注解是为了堵住漏洞用的。曾经有黑客通过构造双重反射来提升权限,
原理是当时反射只检查固定深度的调用者的类,看它有没有特权,
例如固定看两层的调用者(getCallerClass(2))。如果我的类本来没足够
权限群访问某些信息,那我就可以通过双重反射去达到目的:反射相关
的类是有很高权限的,而在 我->反射1->反射2 这样的调用链上,反射2
检查权限时看到的是反射1的类,这就被欺骗了,导致安全漏洞。
使用CallerSensitive后,getCallerClass不再用固定深度去寻找
actual caller(“我”),而是把所有跟反射相关的接口方法都标注上
CallerSensitive,搜索时凡看到该注解都直接跳过,这样就有效解决了
前面举例的问题
通过native方法forName0()获取到我们需要的Class对象,然后通过这个对象获取方法,例如:Method method = c.getMethod("getUsername");
然后进入我们的getMethod0方法,其中MethodArray这个静态内部类就是维护了一个Method数组
深入到此都没有看到具体获取method方法的实现,但是不要着急,我们继续看,注释我已经写在代码中,总体来说,我们是通过查询ReflectionData而得到的,这个私有的静态内部类中有我们需要的很多信息,如代码后的贴图所示:
private Method privateGetMethodRecursive(String name,
Class<?>[] parameterTypes,
boolean includeStaticMethods,
MethodArray allInterfaceCandidates) {
// Search declared public methods
//查询公共的方法
if ((res = searchMethods(privateGetDeclaredMethods(true),
name,
parameterTypes)) != null) {
if (includeStaticMethods || !Modifier.isStatic(res.getModifiers()))
return res;
}
// Search superclass's methods
//查询父类中的方法
if (!isInterface()) {
Class<? super T> c = getSuperclass();
if (c != null) {
//这里产生递归查询
if ((res = c.getMethod0(name, parameterTypes, true)) != null) {
return res;
}
}
}
//查询父类接口中的方法
// Search superinterfaces' methods
Class<?>[] interfaces = getInterfaces();
for (Class<?> c : interfaces)
if ((res = c.getMethod0(name, parameterTypes, false)) != null)
allInterfaceCandidates.add(res);
// Not found
return null;
}
一直到这里我们只是知道了方法的获取,那么方法是如何执行的?大家一定要耐心看,我们在开始查询方法的时候,将方法的Class对象以及方法的信息都记录了下来,下面我们来调用invoke方法method.invoke(c)
分析到这里过后,就主要是我们的MethodAccessor的具体实现了。
invoke:在其内部交给MethodAccessor处理,而这个接口又有两种实现,本地方法来实现反射调用 和委派模式,method实例的第一次反射调用会生成一个委派实现,具体的就是本地实现(进入虚拟机内部以后,便拥有了Methos实例所指向方法的具体地址,然后就可以调用目标方法本地方法来实现反射调用,其中调用的顺序:
- Method.invoke
- 委派实现(DelegatingMethodAccessorImpl)
- 本地方法(NativeMethodAccessorImpl),到达目标方法采用委派做中间层
那么采用委派做中间层,为啥要用委派,不直接调用本地方法?
-
其实,Java的反射调用机制还设立了另一种动态生成字节码的实现(动态实现),直接使用invoke指令调用目标方法,采用委派方便本地实现和动态实现中切换
-
动态实现效率比本地方法快几十倍,因为动态不需要java中调用C++,在回到java,但是生成字节码非常耗时,调用一次的话,本地放法快三四倍
-
Inflation :许多反射仅调用一次,所以设置了一个阀值15,达到十五就动态生成字节码,委派模式切换为动态实现,调用GeneratedMethodAccessor1
4. 反射调用的开销
-
Class.forName会调用本地方法,Class.getMethod 会遍历类的所有共有方法,甚至是父类方法
-
getMethod为代表的查找方法会返回结果的Copy,要避免在热点代码中使用Method,返回数组的方法
-
Method.invoke是一个可变长参数方法,字节码层面他的最后一个参数会是Object数组,Java编译器会在调用放出生成一个长度为传入参数数量的Object数组,切Object数组不能存储基本类型Java编译器还要对基本类型进行封装,性能开销,还占用堆内存,Gc更加频繁
最后属性的获取就相对简单一些了,方法与Method使用的都差不多,这里就不多说了,最后祝大家学习愉快