前言
反射是Java底层框架的灵魂技术,学习反射非常有必要,本文将由浅入深讲解反射,希望对大家有帮助。
官方解析
Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.
Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法;并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。
作用
- 在运行时判断任意一个对象所属的类;
- 在运行时构造任意一个类的对象;
- 在运行时判断任意一个类所具有的成员变量和方法;
- 在运行时调用任意一个对象的方法;生成动态代理。
Class
要理解Class对象,我们先来了解一下RTTI吧。RTTI(Run-Time Type Identification)运行时类型识别,其作用是在运行时识别一个对象的类型和类的信息。
Java是如何让我们在运行时识别对象和类的信息的?主要有两种方式:一种是传统的RRTI,它假定我们在编译期已知道了所有类型。另一种是反射机制,它允许我们在运行时发现和使用类的信息。
要想使用反射,首先需要获得待操作的类所对应的 Class 对象。Java 中,无论生成某个类的多少个对象,这些对象都会对应于同一个 Class 对象。这个 Class 对象是由 JVM 生成的,通过它能够获悉整个类的结构。所以,java.lang.Class 可以视为所有反射 API 的入口点。
Class类的对象作用是运行时提供或获得某个对象的类型信息。
举例来说,假如定义了以下代码:
Student student = new Student(); 复制代码
步骤说明:
- JVM 加载方法的时候,遇到 new Student(),JVM 会根据 Student 的全限定名去加载 Student.class 。
- JVM 会去本地磁盘查找 Student.class 文件并加载 JVM 内存中。
- JVM 通过调用类加载器自动创建这个类对应的 Class 对象,并且存储在 JVM 的方法区。注意:一个类有且只有一个Class对象。
获取Class类对象
第一种,使用 Class.forName 静态方法。
Class class1 = Class.forName("reflection.TestReflection"); 复制代码
第二种,直接获取某一个对象的 class
Class class2 = TestReflection.class; 复制代码
第三种,调用 Object 的 getClass 方法
TestReflection testReflection = new TestReflection(); Class class3 = testReflection.getClass(); 复制代码
反射的基本使用
Java 中的 java.lang.reflect 包提供了反射功能。java.lang.reflect 包中的类都没有 public 构造方法。
Constructor
Class 对象提供以下方法获取对象的构造方法(Constructor):
- getConstructor - 返回类的特定 public 构造方法。参数为方法参数对应 Class 的对象。
- getDeclaredConstructor - 返回类的特定构造方法。参数为方法参数对应 Class 的对象。
- getConstructors - 返回类的所有 public 构造方法。
- getDeclaredConstructors - 返回类的所有构造方法。
获取一个 Constructor 对象后,可以用 newInstance 方法来创建类实例。
示例如下:
public class ReflectConstructorDemo { public static void main(String[] args) throws Exception { Constructor<?>[] constructors1 = String.class.getDeclaredConstructors(); System.out.println("String getDeclaredConstructors 清单(数量 = " + constructors1.length + "):"); for (Constructor c : constructors1) { System.out.println(c); } Constructor<?>[] constructors2 = String.class.getConstructors(); System.out.println("String getConstructors 清单(数量 = " + constructors2.length + "):"); for (Constructor c : constructors2) { System.out.println(c); } Constructor constructor = String.class.getConstructor(String.class); System.out.println(constructor); String str = (String) constructor.newInstance("bbb"); System.out.println(str); } } 复制代码
Field
Class 对象提供以下方法获取对象的成员(Field):
- getFiled - 根据名称获取公有的(public)类成员。
- getDeclaredField - 根据名称获取已声明的类成员。但不能得到其父类的类成员。
- getFields - 获取所有公有的(public)类成员。
- getDeclaredFields - 获取所有已声明的类成员。
示例如下:
public class ReflectFieldDemo { class FieldSpy<T> { public boolean[][] b = {{false, false}, {true, true}}; public String name = "Alice"; public List<Integer> list; public T val; } public static void main(String[] args) throws NoSuchFieldException { Field f1 = FieldSpy.class.getField("b"); System.out.format("Type: %s%n", f1.getType()); Field f2 = FieldSpy.class.getField("name"); System.out.format("Type: %s%n", f2.getType()); Field f3 = FieldSpy.class.getField("list"); System.out.format("Type: %s%n", f3.getType()); Field f4 = FieldSpy.class.getField("val"); System.out.format("Type: %s%n", f4.getType()); } } 复制代码
输出如下:
Type: class [[Z Type: class java.lang.String Type: interface java.util.List Type: class java.lang.Object 复制代码
Method
Class 对象提供以下方法获取对象的方法(Method):
- getMethod - 返回类或接口的特定方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
- getDeclaredMethod - 返回类或接口的特定声明方法。其中第一个参数为方法名称,后面的参数为方法参数对应 Class 的对象。
- getMethods - 返回类或接口的所有 public 方法,包括其父类的 public 方法。
- getDeclaredMethods - 返回类或接口声明的所有方法,包括 public、protected、默认(包)访问和 private 方法,但不包括继承的方法。
获取一个 Method 对象后,可以用 invoke 方法来调用这个方法。
invoke 方法的原型为:
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException 复制代码
示例如下:
public class ReflectMethodDemo { public static void main(String[] args) throws Exception { // 返回所有方法 Method[] methods1 = System.class.getDeclaredMethods(); System.out.println("System getDeclaredMethods 清单(数量 = " + methods1.length + "):"); for (Method m : methods1) { System.out.println(m); } // 返回所有 public 方法 Method[] methods2 = System.class.getMethods(); System.out.println("System getMethods 清单(数量 = " + methods2.length + "):"); for (Method m : methods2) { System.out.println(m); } // 利用 Method 的 invoke 方法调用 System.currentTimeMillis() Method method = System.class.getMethod("currentTimeMillis"); System.out.println(method); System.out.println(method.invoke(null)); } } 复制代码
Array
数组在 Java 里是比较特殊的一种类型,它可以赋值给一个对象引用。下面我们看一看利用反射创建数组的例子:
public class ReflectArrayDemo { public static void main(String[] args) throws ClassNotFoundException { Class<?> cls = Class.forName("java.lang.String"); Object array = Array.newInstance(cls, 25); //往数组里添加内容 Array.set(array, 0, "Scala"); Array.set(array, 1, "Java"); Array.set(array, 2, "Groovy"); Array.set(array, 3, "Scala"); Array.set(array, 4, "Clojure"); //获取某一项的内容 System.out.println(Array.get(array, 3)); } } 复制代码
输出如下:
Scala 复制代码
其中的 Array 类为 java.lang.reflect.Array 类。我们通过 Array.newInstance 创建数组对象,它的原型是:
public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException { return newArray(componentType, length); } 复制代码
动态代理
动态代理是反射的一个非常重要的应用场景。动态代理常被用于一些 Java 框架中。例如 Spring 的 AOP ,Dubbo 的 SPI 接口,就是基于 Java 动态代理实现的。
为了解决静态代理的问题,就有了创建动态代理的想法:
在运行状态中,需要代理的地方,根据 Subject 和 RealSubject,动态地创建一个 Proxy,用完之后,就会销毁,这样就可以避免了 Proxy 角色的 class 在系统中冗杂的问题了。
Java 动态代理基于经典代理模式,引入了一个 InvocationHandler,InvocationHandler 负责统一管理所有的方法调用。
动态代理步骤:
- 获取 RealSubject 上的所有接口列表;
- 确定要生成的代理类的类名,默认为:com.sun.proxy.$ProxyXXXX;
- 根据需要实现的接口信息,在代码中动态创建 该 Proxy 类的字节码;
- 将对应的字节码转换为对应的 class 对象;
- 创建 InvocationHandler 实例 handler,用来处理 Proxy 所有方法调用;
- Proxy 的 class 对象 以创建的 handler 对象为参数,实例化一个 proxy 对象。
从上面可以看出,JDK 动态代理的实现是基于实现接口的方式,使得 Proxy 和 RealSubject 具有相同的功能。
但其实还有一种思路:通过继承。即:让 Proxy 继承 RealSubject,这样二者同样具有相同的功能,Proxy 还可以通过重写 RealSubject 中的方法,来实现多态。CGLIB 就是基于这种思路设计的。
在 Java 的动态代理机制中,有两个重要的类(接口),一个是 InvocationHandler 接口、另一个则是 Proxy 类,这一个类和一个接口是实现我们动态代理所必须用到的。
InvocationHandler 接口
public interface InvocationHandler { public Object invoke(Object proxy, Method method, Object[] args) throws Throwable; } 复制代码
每一个动态代理类都必须要实现 InvocationHandler 这个接口,并且每个代理类的实例都关联到了一个 Handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由 InvocationHandler 这个接口的 invoke 方法来进行调用。
我们来看看 InvocationHandler 这个接口的唯一一个方法 invoke 方法:
Object invoke(Object proxy, Method method, Object[] args) throws Throwable 复制代码
参数说明:
- proxy - 代理的真实对象。
- method - 所要调用真实对象的某个方法的 Method 对象
- args - 所要调用真实对象某个方法时接受的参数
如果不是很明白,等下通过一个实例会对这几个参数进行更深的讲解。
Proxy 类
Proxy 这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但是我们用的最多的就是 newProxyInstance 这个方法:
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException 复制代码
这个方法的作用就是得到一个动态的代理对象。
参数说明:
- loader - 一个 ClassLoader 对象,定义了由哪个 ClassLoader 对象来对生成的代理对象进行加载。
- interfaces - 一个 Interface 对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
- h - 一个 InvocationHandler 对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个 InvocationHandler 对象上
动态代理实例
首先我们定义了一个 Subject 类型的接口,为其声明了两个方法:
public interface Subject { void hello(String str); String bye(); } 复制代码
接着,定义了一个类来实现这个接口,这个类就是我们的真实对象,RealSubject 类:
public class RealSubject implements Subject { @Override public void hello(String str) { System.out.println("Hello " + str); } @Override public String bye() { System.out.println("Goodbye"); return "Over"; } } 复制代码
下一步,我们就要定义一个动态代理类了,前面说个,每一个动态代理类都必须要实现 InvocationHandler 这个接口,因此我们这个动态代理类也不例外:
public class InvocationHandlerDemo implements InvocationHandler { // 这个就是我们要代理的真实对象 private Object subject; // 构造方法,给我们要代理的真实对象赋初值 public InvocationHandlerDemo(Object subject) { this.subject = subject; } @Override public Object invoke(Object object, Method method, Object[] args) throws Throwable { // 在代理真实对象前我们可以添加一些自己的操作 System.out.println("Before method"); System.out.println("Call Method: " + method); // 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用 Object obj = method.invoke(subject, args); // 在代理真实对象后我们也可以添加一些自己的操作 System.out.println("After method"); System.out.println(); return obj; } } 复制代码
最后,来看看我们的 Client 类:
public class Client { public static void main(String[] args) { // 我们要代理的真实对象 Subject realSubject = new RealSubject(); // 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的 InvocationHandler handler = new InvocationHandlerDemo(realSubject); /* * 通过Proxy的newProxyInstance方法来创建我们的代理对象,我们来看看其三个参数 * 第一个参数 handler.getClass().getClassLoader() ,我们这里使用handler这个类的ClassLoader对象来加载我们的代理对象 * 第二个参数realSubject.getClass().getInterfaces(),我们这里为代理对象提供的接口是真实对象所实行的接口,表示我要代理的是该真实对象,这样我就能调用这组接口中的方法了 * 第三个参数handler, 我们这里将这个代理对象关联到了上方的 InvocationHandler 这个对象上 */ Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject .getClass().getInterfaces(), handler); System.out.println(subject.getClass().getName()); subject.hello("World"); String result = subject.bye(); System.out.println("Result is: " + result); } } 复制代码
我们先来看看控制台的输出:
com.sun.proxy.$Proxy0 Before method Call Method: public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String) Hello World After method Before method Call Method: public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye() Goodbye After method Result is: Over 复制代码
我们首先来看看 com.sun.proxy.$Proxy0 这东西,我们看到,这个东西是由 System.out.println(subject.getClass().getName()); 这条语句打印出来的,那么为什么我们返回的这个代理对象的类名是这样的呢?
Subject subject = (Subject)Proxy.newProxyInstance(handler.getClass().getClassLoader(), realSubject.getClass().getInterfaces(), handler); 复制代码
可能我以为返回的这个代理对象会是 Subject 类型的对象,或者是 InvocationHandler 的对象,结果却不是,首先我们解释一下为什么我们这里可以将其转化为 Subject 类型的对象?
原因就是:在 newProxyInstance 这个方法的第二个参数上,我们给这个代理对象提供了一组什么接口,那么我这个代理对象就会实现了这组接口,这个时候我们当然可以将这个代理对象强制类型转化为这组接口中的任意一个,因为这里的接口是 Subject 类型,所以就可以将其转化为 Subject 类型了。
同时我们一定要记住,通过 Proxy.newProxyInstance 创建的代理对象是在 jvm 运行时动态生成的一个对象,它并不是我们的 InvocationHandler 类型,也不是我们定义的那组接口的类型,而是在运行是动态生成的一个对象,并且命名方式都是这样的形式,以$开头,proxy 为中,最后一个数字表示对象的标号。
接着我们来看看这两句
subject.hello("World"); String result = subject.bye(); 复制代码
这里是通过代理对象来调用实现的那种接口中的方法,这个时候程序就会跳转到由这个代理对象关联到的 handler 中的 invoke 方法去执行,而我们的这个 handler 对象又接受了一个 RealSubject 类型的参数,表示我要代理的就是这个真实对象,所以此时就会调用 handler 中的 invoke 方法去执行。
我们看到,在真正通过代理对象来调用真实对象的方法的时候,我们可以在该方法前后添加自己的一些操作,同时我们看到我们的这个 method 对象是这样的:
public abstract void io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.hello(java.lang.String) public abstract java.lang.String io.github.dunwu.javacore.reflect.InvocationHandlerDemo$Subject.bye() 复制代码
正好就是我们的 Subject 接口中的两个方法,这也就证明了当我通过代理对象来调用方法的时候,起实际就是委托由其关联到的 handler 对象的 invoke 方法中来调用,并不是自己来真实调用,而是通过代理的方式来调用的。
反射的实现原理
Java 虚拟机可以通过称为运行时类型信息(RTTI, Run Time Type Information)的技术在运行时检查任何类,这是通过一种称为 Class 对象的特殊对象完成的,该对象包含有关类的信息。
虚拟机为每个类管理一个独一无二的 Class 对象。也就是说,每个类都有一个 Class 对象实例。在运行程序的时候,JVM 首先需要会去检测所需加载的类的 Class 是否已经完成加载。如果没有加载在 JVM 中,那么 JVM 回去寻找对应类名的 .class 文件,完成对 Class 对象的加载。
通过 Class 对象,我们可以实例化对应的 Class 类对象,调用其构造器(Constructor)、调用类的成员方法(Method)、访问或者修改类的成员属性(Field)。通过 AccessibleObject#setAccessible(boolean flag) 可以访问到类的非 public 权限的其他成员,在上文提到通过 AccessibleObject#setAccessible(boolean flag) 可以在程序运行时修改类成员的访问限制。实际上,AccessibleObject#setAccessible(boolean flag) 关闭了权限的访问检查,使得通过 Class#invoke() 可以访问到任意权限的类成员。
通过以上学习,我们已经知道反射的基本API用法了。接下来,跟着一个例子,学习反射方法的执行链路。
public class TestReflection { public static void main(String[] args) throws Exception { Class clazz = Class.forName("reflection.TestReflection"); Method method = clazz.getMethod("target", String.class); method.invoke(null, "123456"); } public static void target(String str) { //输出堆栈信息 new Exception("#" +str).printStackTrace(); System.out.println("invoke target method"); } } 复制代码
堆栈信息反映出反射调用链路:
java.lang.Exception: #123456 invoke target method at reflection.TestReflection.target(TestReflection.java:17) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at reflection.TestReflection.main(TestReflection.java:11) 复制代码
invoke方法执行时序图
我们跟着反射链路去看一下源码,先看Method的invoke方法:
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; if (ma == null) { ma = acquireMethodAccessor(); //获取MethodAccessor } //返回MethodAccessor.invoke return ma.invoke(obj, args); } 复制代码
由上可知道,Method 的 invoke 方法,其实是返回接口MethodAccessor的invoke方法。MethodAccessor接口有三个实现类,到底调用的是哪个类的 invoke 方法呢?
进入acquireMethodAccessor方法,可以看到MethodAccessor由ReflectionFactory 的 newMethodAccessor方法决定。
再进ReflectionFactory的newMethodAccessor方法,我们可以看到返回的是DelegatingMethodAccessorImpl对象,也就是说调用的是它的invoke方法。
再看DelegatingMethodAccessorImpl的invoke方法
DelegatingMethodAccessorImpl的invoke方法返回的是MethodAccessorImpl的invoke方法,而MethodAccessorImpl的invoke方法,由它的子类NativeMethodAccessorImpl重写,这时候返回的是本地方法invoke0,如下
因此,Method的invoke方法,是由本地方法invoke0决定的,再底层就是c++相关了,有兴趣的朋友可以继续往下研究。
反射的一些应用
在JDBC连接数据库中,一般包括加载驱动,获得数据库连接等步骤。而加载驱动,就是引入相关Jar包后,通过Class.forName() 即反射技术,加载数据库的驱动程序。
Spring 通过 XML 配置模式装载 Bean,也是反射的一个典型例子。
装载过程:
- 将程序内XML 配置文件加载入内存中
- Java类解析xml里面的内容,得到相关字节码信息
- 使用反射机制,得到Class实例
- 动态配置实例的属性
这样做当然是有好处的: 不用每次都去new实例了,并且可以修改配置文件,比较灵活。
反射效率低的原因
1. Method#invoke 方法会对参数做封装和解封操作
我们可以看到,invoke 方法的参数是 Object[] 类型,也就是说,如果方法参数是简单类型的话,需要在此转化成 Object 类型,例如 long ,在 javac compile 的时候 用了Long.valueOf() 转型,也就大量了生成了Long 的 Object, 同时 传入的参数是Object[]数值,那还需要额外封装object数组。
而在上面 MethodAccessorGenerator#emitInvoke 方法里我们看到,生成的字节码时,会把参数数组拆解开来,把参数恢复到没有被 Object[] 包装前的样子,同时还要对参数做校验,这里就涉及到了解封操作。
因此,在反射调用的时候,因为封装和解封,产生了额外的不必要的内存浪费,当调用次数达到一定量的时候,还会导致 GC。
2. 需要检查方法可见性
通过上面的源码分析,我们会发现,反射时每次调用都必须检查方法的可见性(在 Method.invoke 里)
3. 需要校验参数
反射时也必须检查每个实际参数与形式参数的类型匹配性(在NativeMethodAccessorImpl.invoke0 里或者生成的 Java 版 MethodAccessor.invoke 里);
4. 反射方法难以内联
Method#invoke 就像是个独木桥一样,各处的反射调用都要挤过去,在调用点上收集到的类型信息就会很乱,影响内联程序的判断,使得 Method.invoke() 自身难以被内联到调用方。
5. JIT 无法优化
在 JavaDoc 中提到:
Because reflection involves types that are dynamically resolved, certain Java virtual machine optimizations can not be performed. Consequently, reflective operations have slower performance than their non-reflective counterparts, and should be avoided in sections of code which are called frequently in performance-sensitive applications.
因为反射涉及到动态加载的类型,所以无法进行优化。
反射存在的安全问题
我们知道单例模式的设计过程中,会强调将构造器设计为私有,因为这样可以防止从外部构造对象。但是反射可以获取类中的域、方法、构造器,修改访问权限。所以这样并不一定是安全的。
看下面的例子,通过反射使用私有构造器实例化。
public class Student { private String name; private Student(String name) { System.out.println("我是私有构造器,我被实例化了"); this.name = name; } public void doHomework(String subject) { System.out.println("我的名字是" + name); System.out.println("我正在在做"+subject+"作业"); } } public class TestReflection { public static void main(String[] args) throws Exception { Class clazz = Class.forName("reflection.Student"); // 获取私有构造方法对象 Constructor constructor = clazz.getDeclaredConstructor(String.class); // true指示反射的对象在使用时应该取消Java语言访问检查。 constructor.setAccessible(true); Student student = (Student) constructor.newInstance("java@axuan"); student.doHomework("英语"); } } 复制代码
运行结果:
显然,反射不管你是不是私有,一样可以调用。所以,使用反射通常需要程序的运行没有安全限制。如果一个程序对安全性有强制要求,最好不要使用反射啦。