Java 反射源码学习之旅

1 背景
前段时间组内针对 “拷贝实例属性是应该用 BeanUtils.copyProperties()还是 MapStruct” 这个问题进行了一次激烈的 battle 。支持 MapStruct 的同学给出了他嫌弃 BeanUtils 的理由:因为用了反射 , 所以慢 。
这个理由一下子拉回了我遥远的记忆 , 在我刚开始了解反射这个 JAVA 特性的时候 , 几乎看到的每一篇文章都会有 “Java 反射不能频繁使用”、“反射影响性能” 之类的话语 , 当时只是当一个结论记下了这些话 , 却没有深究过为什么 , 所以正好借此机会来探究一下 Java 反射的代码 。
2 反射包结构梳理
反射相关的代码主要在 jdk rt.jar 下的 java.lang.reflect 包下 , 还有一些相关类在其他包路径下 , 这里先按下不表 。按照继承和实现的关系先简单划分下 java.lang.reflect 包:
① Constructor、Method、Field 三个类型分别可以描述实例的构造方法、普通方法和字段 。三种类型都直接或间接继承了 AccessibleObject 这个类型 , 此类型里主要定义两种方法 , 一种是通用的、对访问权限进行处理的方法 , 第二种是可供继承重写的、与注解相关的方法 。

Java 反射源码学习之旅

文章插图
② 只看选中的五种类型 , 我们平常所用到的普通类型 , 譬如 Integer、String , 又或者是我们自定义的类型 , 都可以用 Class 类型的实例来表示 。Java 引入泛型之后 , 在 JDK1.5 中扩充了其他四种类型 , 用于泛型的表示 。分别是 ParameterizedType (参数化类型)、WildcardType(通配符类型)、TypeVariable(类型变量)、GenericArrayType(泛型数组) 。
Java 反射源码学习之旅

文章插图
③ 与②中描述的五种基本类型对应 , 下图这五个接口 / 类分别用来表示五种基本类型的注解相关数据 。
Java 反射源码学习之旅

文章插图
④ 下图为实现动态代理的相关类与接口 。java.lang.reflect.Proxy 主要是利用反射的一些方法获取代理类的类对象 , 获取其构造方法 , 由此构造出一个实例 。
java.lang.reflect.InvocationHandler 是代理类需要实现的接口 , 由代理类实现接口内的 invoke 方法 , 此方法会负责代理流程和被代理流程的执行顺序组织 。
Java 反射源码学习之旅

文章插图

Java 反射源码学习之旅

文章插图
3 目标类实例的构造源码
以 String 类的对象实例化为例 , 看一下反射是如何进行对象实例化的 。
Class<?> clz = Class.forName("java.lang.String");
String s =(String)clz.newInstance();
Class 对象的构造由 native 方法完成 , 以 java.lang.String 类为例 , 先看看构造好的 Class 对象都有哪些属性:
Java 反射源码学习之旅

文章插图
可以看到目前只有 name 一个属性有值 , 其余属性暂时都是 null 或者默认值的状态 。
下图是 clz.newInstance () 方法逻辑的流程图 , 接下来对其中主要的两个方法进行说明:
Java 反射源码学习之旅

文章插图
从上图可以看出整个流程有两个核心部分 。因为通常情况下 , 对象的构造都需要依靠类里的构造方法来实现 , 所以第一部分就是拿到目标类对应的 Constructor 对象;第二部分就是利用 Constructor 对象 , 构造目标类的实例 。
3.1 获取 Constructor 对象
首先上一张 Constructor 对象的属性图:
Java 反射源码学习之旅

文章插图
java.lang.Class#getConstructor0
此方法中主要做的工作是首先拿到目标类的 Constructor 实例数组 (主要由 native 方法实现) , 数组里每一个对象都代表了目标类的一个构造方法 。然后对数组进行遍历 , 根据方法入参提供的 parameterTypes, 找到符合的 Constructor 对象 , 然后重新创造一个 Constructor 对象 , 属性值与原 Constructor 一致(称为副本 Constructor) , 并且副本 Constructor 的属性 root 指向源 Constructor , 相当于对源 Constructor 对象进行了一层封装 。
由于在 getConstructor0 () 方法将返回值返回给调用方之后 , 调用方在后续的流程里进行了 constructor.setAccesssible (true) 的操作 , 这个方法的作用是关闭对 constructor 这个对象访问时的 Java 语言访问检查 。语言访问检查是个耗时的操作 , 所以合理猜测是为了提高反射性能关闭了这个检查 , 又出于安全考虑 , 所以将最原始的对象进行了封装 。


推荐阅读