通常在程序中对象类型都是编译期就确定下来的,而 Java
反射机制的核心是 JVM
在运行时才动态加载类或调用方法、属性,这样对象的类型在编译期是未知的,也就是可以通过反射机制直接创建编译期未知的对象。
反射并没有太多理论基础,主要是熟悉各种 API
,通过反射在运行时获得程序或程序集中每一个类型成员和成员变量的信息。
基本概念
反射能做什么
- 对于任意一个类,都能够知道这个类的所有属性和方法
- 对于任意一个对象,都能够调用它的任意一个方法和属性
反射常见用途
- 编译期已知类名
如果编译器已知类名、类对象,可以通过反射简写代码(比如工厂模式中去掉条件判断等),或者获取类的私有属性、方法、构造方法等。 - 编译期未知类名
无法导入到当前类,可以通过反射动态加载类。通过配置文件或者泛型动态加载。
反射最重要的用途就是开发各种通用框架,动态加载类。很多框架(比如Spring
)都是配置化的(通过XML
文件配置JavaBean, Action
等),为了保证框架的通用性,他们可能根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。
基本数据类型类对象
- 基本数据类型
boolean.class, char.class, byte.class, short.class, int.class, long.class, float.class, double.class
void
类型void.class
获取类对象的方法
- 编译器已知类名
Class<?> myObjectClass = MyObject.class;
- 已有类对象
Class<?> clazz = object.getClass();
- 已知完整类全名反射
Class<?> myObjectClass = Class.forName("com.simple.User");
- 类加载器加载二进制字节流
Class clazz = classLoader.loadClass("com.***.User");
,注意类加载器的双亲委派模型确保能找到该类。只要能拿到.class
文件对应的二进制字节流,就能通过反射获取Class
的所有信息。 - 类的内部类
Class API
可以遍历内部类或者指定类。或者使用完整类全名反射,注意:内部类和外部类之间使用美元符连接$
:Class<?> myObjectClass = Class.forName("com.simple.User$InnerClass");
每个 Class
在类加载过程中,会将类对象加载到方法区中,确保 JVM
中只存在一个类对象,它保存了类相关的类型信息,属性,方法,构造方法等等。
常见类和对应 API
AnnotatedElement
AnnotatedElement
注解元素贯穿了整个反射的基础类。
1 | public interface AnnotatedElement { |
Member
Member
表示类的成员:字段属性、构造方法、普通方法。
1 | public interface Member { |
AccessibleObject
AccessibleObject
表示可访问对象,用来修改查询访问控制符。
1 | public class AccessibleObject implements AnnotatedElement { |
GenericDeclaration
GenericDeclaration
声明类型变量的接口,代表着泛型。
1 | public interface GenericDeclaration extends AnnotatedElement { |
Field
Field
代表类中的字段、属性。
1 | public final class Field extends AccessibleObject implements Member { |
Executable
Executable
1.8 新增的抽象类,是构造方法和普通方法的基类。
1 | public abstract class Executable extends AccessibleObject |
Constructor
Constructor
代表类中的构造方法。
1 | public final class Constructor<T> extends Executable { |
Method
Method
代表类中的普通方法。
1 | public final class Method extends Executable { |
Class
Class
反射基石,可以对 .class
文件全解析,获取字段、构造方法、普通方法、内部类、注解等功能。
1 | public final class Class<T> implements java.io.Serializable, |
get***
和 getDeclared***
的区别
getDeclared***
获取当前类中所有的字段属性,方法等,包含私有的,但不包含父类。get***
获取公共的字段属性,方法等,包含父类的。
总结
- 泛型中类型变量定义声明
GenericDeclaration
表示类型变量声明的接口,其实现类为:Class, Constructor, Method
,也就是说只有在类、构造方法、普通方法定义时才能声明类型变量,其他地方不允许。
- 访问权限控制
AccessibleObject
表示可以控制访问权限的对象,其实现类为:Field, Constructor, Method
。也就是说只有在字段、构造方法、普通方法上可以设置访问权限AccessibleObject.setAccessible(true)
,并访问非public
类型。
数组
辅助类 Array
Array
提供一系列静态方法用来动态创建和访问数组。
1 | public final class Array { |
定义数组
Java
中可以明确类型定义数组,也可以使用 Object/Class<?>
来表示数组:
1 | String[] strings = {"a", "b", "c"}; |
反射中通常使用 Object/Class<?>
来表示参数或返回值,注意:它们同时代表了数组类型。
判断是否为数组
判断当前类对象或实例是否表示数组,可以使用 Class.isArray()
来判断:
1 | System.out.println(int[].class.isArray()); // true |
反射中转换为数组
通过反射调用方法后返回 Object/Class<?>
,可以先判断是否为数组,然后再做转换。转换可以显示强制转换为对应类型数组,也可以通过 java.lang.reflect.Array
辅助类来处理。
1 | // 判断是否为数组 |
示例
实例化
反射创建类实例,有两种方法:
Constructor.newInstance()
构造方法实例化,可以传递构造方法的参数。Class.newInstance()
直接通过类来实例化,相当于构造方法空参数来实例化。
1 | Class<?> clazz = Class.forName("com.ymzs.javabase.reflect.ReflectedClass"); |
获取和设置字段属性
先将类实例化,然后使用该实例修改字段属性;如果是 static
字段,它属于类的字段属性,所以将实例设置为 null
。
1 | // 1. 先将类实例化 |
调用方法
先将类实例化,然后使用该实例调用方法;如果是 static
字段,它属于类方法,所以将实例设置为 null
。
1 | Class<?> clazz = Class.forName("com.ymzs.javabase.reflect.ReflectedClass"); |
内部类
通过反射接口可以获取内部类是在哪个类/构造方法/普通方法中定义,以及判断它们属于成员/匿名/局部内部类的哪一种。
1 | // 内部类定义 |
反射常见场景
- 反射与泛型
反射与泛型的混合使用在很多框架中都会出现,应用非常广泛。基础知识点可以参考Java Type类型 ,主要是通过反射获取泛型相关信息。 - 反射与注解
反射是注解解析方式的一种,在运行时解析注解并实现对应功能。 - 动态代理
代理模式的一种,通过反射动态生成代理对象。设计模式参考:代理模式
动态代理
代理模式:为其他对象提供一种代理以控制对这个对象的访问。 这是设计模式中对代理模式的介绍,代理模式分为静态代理和动态代理。静态代理即编译期前代码及代理关系就已经明确存在;动态代理是通过反射机制动态地生成代理对象,也就是代码编译中并不知道代理关系,动态代理将代理和被代理对象进行了解耦。Java
反射技术是在内存中,动态生成一个新类来实现动态代理。
Java
中只能为接口interface
实现动态代理。
基础类
1 | // 动态代理必须通过这个接口来实现代理方法调用 |
示例
1 | // 1. 目标接口,被动态代理 |
InvocationHandler.invoke
在这里实现动态代理,同时可以在动态代理前增加权限检查,或者添加功能(相当于装饰模式)。com.sun.proxy.$Proxy0
从输出的Log
可以看出,动态代理是重新生成了一个新的类com.sun.proxy.$Proxy0
,并实现了动态代理的功能。新类命名格式:包名 + $Proxy + id 序号
。
ClassDump
工具
工具介绍
ClassDump
是 HotSpot
虚拟机特有的,它是 HotSpot SA: Serviceability Agent
中的一个工具。ClassDump
可以在运行时 dump
类文件,特别是动态生成或者运行时被修改了字节码的类。HotSpot
有一套私有 API
提供了对 JVM
内部数据结构的审视功能,称为 Serviceability Agent
。可以通过 API
直接写 Java
代码来查看一个跑在 HotSpot
上的 Java
进程的内部状态。它也提供了一些封装好的工具,可以直接在命令行上跑,包括 ClassDump
工具。SA
的一个重要特征是它是“进程外审视工具”。也就是说 SA
并不运行在要审视的目标进程中,而是运行在一个独立的 Java
进程中,通过操作系统上提供的调试 API
来连接到目标进程上。这样 SA
的运行不会受到目标进程状态的影响,因而可以用于审视一个已经挂起的 Java
进程。一个被调试器连接上的进程会被暂停下来。所以在 SA
连接到目标进程时,目标进程也是一直处于暂停状态的,直到 SA
解除连接。如果需要在线上使用SA的话需要小心,不要通过 SA
做过于耗时的分析,宁可先把数据都抓出来,把连接解除掉之后再离线分析。目前的使用经验是,连接上一个小 Java
进程的话很快就好了,但连接上一个“大”的 Java
进程(堆比较大、加载的类比较多)可能会在连接阶段卡住好几分钟,线上需要慎用。ClassDump
的特点:需要目标 Java
进程必须在运行中;连接时会导致目标进程暂停。
示例过滤器
示例:使用 ClassDump
工具 Dump
出动态代理生成的类,其类特点是文件名都是 com.sun.proxy.$Proxy
开头的 class
文件。
1 | import sun.jvm.hotspot.oops.InstanceKlass; |
注意:过滤器中需要过滤的类名是以 /
分割的而不是 .
,如:com/sun/proxy/$Proxy
。
执行过程
当前是在
Ubuntu
环境中运行的,执行时必须使用root
权限(使用sudo
也会报错)。
修改动态代理源文件,确保进程持续运行。
1 | public class TestDynamicProxy { |
运行该测试程序,并查看对应的进程名:
1 | // 找到目标进程 id: 81249 |
MyClassNameFilter.java
文件并不需要编译(也编译不过导入的包名找不到),在 MyClassNameFilter.java
文件所在目录运行,命令使用方法:
1 | // 0. root 用户下执行 |
执行完毕后,会生成三个代码目录:com, java, sun
,其中 com
为动态代理中生成的类,其他为加载的系统类。
没有使用 root
账号运行出现的错误:
1 | // 1. 直接使用其他账户运行,无法 attach 到目标进程 |
结果分析
使用 jd-gui
打开 $Proxy0.class
文件分析动态代理生成的类实现了哪些功能:
1 | // 继承了 Proxy 类,并实现了 Subject 接口, |
可以看出生成的代理类,最终是通过 InvocationHandler
来调用目标方法的。