深入理解Java中的反射
- 反射的概念
- 反射的原理
- 反射的主要用途
- 反射的运用
- 获得Class对象
- 判断是否是某个类的实例
- 创建实例
- 获取方法
- 获取 构造器 信息
- 获取类的成员变量信息
- 调用方法
- 利用反射创建数组
- invoke方法
- invoke执行过程
- 权限检查
- 调用MethodAccessor的invoke方法
- JVM层invoke0方法
- Java版的实现
- invoke总结
- 反射注意点
反射的概念
- 反射: Ref election,反射是Java的特征之一,允许 运行 中的Java程序获取自身信息,并可以操作类或者对象的内部属性通过反射,可以在运行时获得程序或者程序中的每一个类型的成员或成成员的信息程序中的对象一般都是在编译时就确定下来,Java 反射机制 可以动态地创建对象并且调用相关属性,这些对象的类型在编译时是未知的也就是说 ,可以通过反射机制直接创建对象,即使这个对象类型在编译时是未知的
- Java反射提供下列功能: 在运行时判断任意一个对象所属的类在运行时构造任意一个类的对象在运行时判断任意一个类所具有的成员变量和方法,可以通过反射调用 private 方法在运行时调用任意一个对象的方法
反射的原理
- 反射的核心: JVM在运行时才动态加载类或者调用方法以及访问属性,不需要事先(比如编译时)知道运行对象是什么
- 类的加载: Java反射机制是围绕Class类展开的 首先要了解类的加载机制:JVM 使用 ClassLoader 将字节码文件,即 class 文件加载到方法区内存中
ClassLoader 类根据类的完全限定名加载类并返回一个 Class 对象
- Reflection Data: 为了提高反射的性能,必须要提供缓存 class 类内部使用一个 use cache s 静态变量来标记是否使用缓存这个值可以通过外部的 sun.reflect.noCaches 配置是否禁用缓存 class 类内部提供了一个 reflection Data 内部类用来存放反射数据的缓存,并声明了一个 reflectionData 域由于稍后进行按需延迟加载并缓存,所以这个域并没有指向一个实例化的 ReflectionData 对象
反射的主要用途
- 反射最重要的用途就是开发各种通用框架 很多框架都是配置化的,通过 XML 文件配置 Bean 为了保证框架的通用性,需要根据配置文件加载不同的对象或者类,调用不同的方法要运用反射,运行时动态加载需要加载的对象
- 示例: 在运用 Struts 2 框架的开发中会在 struts.xml 中配置 Action
- 配置文件与 Action 建立映射关系
- 当 View 层发出请求时,请求会被 StrutsPrepareAndExecuteFilter 拦截
- StrutsPrepareAndExecuteFilter 会动态地创建 Action 实例请求 login.actionStrutsPrepareAndExecuteFilter 会解析 struts.xml 文件检索 action 中 name 为 login 的 Action 根据 class 属性创建 SimpleLoginAction 实例使用 invoke 方法调用 execute 方法
- 反射是各种容器实现的核心
反射的运用
- 反射相关的类在 StrutsPrepareAndExecuteFilter 包
- 反射可以用于:
- 判断对象所属的类
- 获得class对象
- 构造任意一个对象
- 调用一个对象
获得Class对象
- 使用Class类的forName静态方法
- 直接获取一个对象的class
- 调用对象的getClass()方法
判断是否是某个类的实例
- 一般来说,使用 instanceof 关键字判断是否为某个类的实例
- 在反射中,可以使用 Class 对象的 isInstance() 方法来判断是否为某个类的实例,这是一个 native 方法
创建实例
通过反射生成对象的实例主要有两种方式:
- 使用Class对象的newInstance()方法来创建Class对象对应类的实例
- 先通过Class对象获取指定的 Constructor 对象,再调用Constructor对象的newInstance()方法来创建实例: 可以用指定的构造器构造类的实例
获取方法
获取Class对象的方法集合,主要有三种方法:
- getDeclaredMethods(): 返回类或接口声明的所有方法:包括公共,保护,默认(包)访问和私有方法不包括继承的方法
- getMethods(): 返回某个类所有的 public 方法包括继承类的 public 方法
- getMethod(): 返回一个特定的方法第一个参数 :方法名称 后面的参数 :方法的参数对应Class的对象
通过 getMethods() 获取的方法可以获取到父类的方法
获取构造器信息
- 通过 Class 类的 getConstructor 方法得到 Constructor 类的一个实例
- Constructor 类中 newInstance 方法可以创建一个对象的实例
newInstance 方法可以根据传入的参数来调用对应的 Constructor 创建对象的实例
获取类的成员变量信息
- getFileds: 获取公有的成员变量
- getDeclaredFields: 获取所有已声明的成员变量,但是不能得到父类的成员变量
调用方法
- 从类中获取一个方法后,可以使用 invoke() 来调用这个方法
利用反射创建数组
- 数组是Java中一种特殊的数据类型,可以赋值给一个 Object Reference
- 利用反射创建数组的示例
Array 类是 java.lang.reflect.Array 类,通过 Array.newInstance() 创建数组对象
newArray 方法是一个 native 方法,具体实现在 HotSpot JVM 中,源码如下
- Array 类的 set 和 get 方法都是 native 方法,具体实现在 HotSpot JVM 中,对应关系如下: set: Reflection::array_set get: Reflection::array_get
invoke方法
- 在Java中很多方法都会调用 invoke 方法,很多异常的抛出多会定位到 invoke 方法
invoke执行过程
- invoke 方法用来在运行时动态地调用某个实例的方法,实现如下
权限检查
- AccessibleObject 类是 Field,Method 和 Constructor 对象的基类: 提供将反射的对象标记为在使用时取消默认Java语言访问控制检查的能力
- invoke 方法会首先检查 AccessibleObject 的 override 属性的值: override默认值为false: 表示需要权限调用规则,调用方法时需要检查权限也可以使用 setAccessible() 设置为 trueoverride如果值为true: 表示忽略权限规则,调用方法时无需检查权限也就是说,可以调用任意private方法,违反了封装
- 如果 override 属性为默认值false,则进行进一步的权限检查
- 首先用 Reflection.quickCheckMemberAccess(clazz, modifiers) 方法检查方法是否为 public
1.1 如果是public方法的话,就跳过本步
1.2 如果不是public方法的话,就用Reflection.getCallerClass()方法获取调用这个方法的Class对象,这是一个 native 方法 - 获取 Class 对象 caller 后使用 checkAccess 方法进行一次快速的权限校验 ,checkAccess 方法实现如下
首先先执行一次快速校验,一旦 Class 正确则权限检查通过;如果未通过,则创建一个缓存,中间再进行检查
- 如果上面所有的权限检查都未通过,将会执行更详细的检查
用 Reflection.ensureMemberAccess 方法继续检查权限.若检查通过就更新缓存,这样下一次同一个类调用同一个方法时就不用执行权限检查了,这是一种简单的 缓存 机制
由于 JMM 的 happens-before 规则能够保证缓存初始化能够在写缓存之间发生,因此两个cache不需要声明为volatile
- 检查权限的工作到此结束.如果没有通过检查就会抛出异常,如果没有通过检查就会到下一步
调用MethodAccessor的invoke方法
- Method.invoke() 不是自身实现反射调用逻辑,而是通过 sun.refelect.MethodAccessor 来处理
- Method对象的基本构成: 每个 Java 方法有且只有一个 Method 对象作为 root, 相当于根对象,对用户不可见当创建 Method 对象时,代码中获得的 Method 对象相当于其副本或者引用 root 对象持有一个 MethodAccessor 对象,所有获取到的 Method 对象都共享这一个 MethodAccessor 对象必须保证 MethodAccessor 在内存中的可见性
- root对象及其声明
- MethodAccessor
MethodAccessor 是一个接口,定义了 invoke() 方法,通过 Usage 可以看出 MethodAccessor 的具体实现类:
- sun.reflect.DelegatingMethodAccessorImpl
- sun.reflect.MethodAccessorImpl
- sun.reflect.NativeMethodAccessorImpl
- 第一次调用 Java 方法对应的 Method 对象的 invoke()方法之前,实现调用逻辑的MethodAccess 对象还没有创建
- 第一次调用时,才开始创建 MethodAccessor 并更新为 root, 然后调用 MethodAccessor.invoke() 完成反射调用
- methodAccessor 实例由 reflectionFactory 对象操控生成 ,reflectionFactory 是在 AccessibleObject 中声明的
- sun.reflect.ReflectionFactory 方法
- 实际的 MethodAccessor 实现有两个版本,一个是 Java 版本,一个是 native 版本,两者各有特点:初次启动时 Method.invoke() 和 Constructor.newInstance() 方法采用native方法要比Java方法快3-4倍启动后 native 方法又要消耗额外的性能而慢于 Java 方法Java实现的版本在初始化时需要较多时间,但长久来说性能较好
- 这是HotSpot的优化方式带来的性能特性:
- 跨越native边界会对优化有阻碍作用
- 为了尽可能地减少性能损耗,HotSpot JDK采用inflation方式: Java方法在被反射调用时,开头若干次使用native版等反射调用次数超过 阈值 时则生成一个专用的 MethodAccessor 实现类,生成其中的 invoke() 方法的字节码以后对该Java方法的反射调用就会使用Java版本
- ReflectionFactory.newMethodAccessor() 生成 MethodAccessor 对象的逻辑: native 版开始会会生成 NativeMethodAccessorImpl 和 DelegatingMethodAccessorImpl 两个对象
- DelegatingMethodAccessorImpl
DelegatingMethodAccessorImpl 对象是一个中间层,方便在 native 版与 Java 版的 MethodAccessor 之间进行切换
- native版MethodAccessor的Java方面的声明: sun.reflect.NativeMethodAccessorImpl
- 每次 NativeMethodAccessorImpl.invoke() 方法被调用时,程序调用 计数器 都会增加 1, 看看是否超过阈值
- 如果超过则调用 MethodAccessorGenerator.generateMethod() 来生成 Java 版的 MethodAccessor 的实现类
- 改变 DelegatingMethodAccessorImpl 所引用的 MethodAccessor 为 Java 版
- 经由 DelegatingMethodAccessorImpl.invoke() 调用到的就是 Java 版的实现
JVM层invoke0方法
- invoke0 方法是一个 native 方法,在 HotSpot JVM 里调用 JVM_InvokeMethod 函数
- openjdk/hotspot/src/share/vm/prims/ JVM .cpp
- 关键部分为 Reflection::invoke_method: openjdk/hotspot/src/share/vm/runtime/reflection.cpp
Java的对象模型 :klass 和 oop
Java版的实现
- Java 版 MethodAccessor 的生成使用 MethodAccessorGenerator 实现
运用了 asm 动态生成字节码技术 – sun.reflect.ClassFileAssembler
invoke总结
- invoke方法的过程:
- MagicAccessorImpl: 原本Java的安全机制使得不同类之间不是任意信息都可见,但JDK里面专门设了个 MagicAccessorImpl 标记类开了个后门来允许不同类之间信息可以互相访问,由JVM管理
- @CallerSensitive注解
- 用 @CallerSensitive 注解修饰的方法从一开始就知道具体调用此方法的对象不用再经过一系列的检查就能确定具体调用此方法的对象实际上是调用 sun.reflect.Reflection.getCallerClass 方法
- Reflection 类位于调用栈中的0帧位置 sun.reflect.Reflection.getCallerClass() 方法返回调用栈中从0帧开始的第x帧中的类实例该方法提供的机制可用于确定调用者类,从而实现”感知调用者(Caller Sensitive)”的行为即允许应用程序根据调用类或调用栈中的其它类来改变其自身的行为
反射注意点
- 反射会额外消耗系统资源,如果不需要动态地创建一个对象,就不要使用反射
- 反射调用方法时可以忽略权限检查.可能会破坏封装性而导致安全问题