java中的反射机制
java中的反射机制
zhangzhang一、反射概述
1. 什么是反射?
反射(Reflection),Java 中的反射机制是指,Java 程序在运行期间可以获取到一个对象的全部信息。
反射机制一般用来解决Java 程序运行期间,对某个实例对象一无所知的情况下,如何调用该对象内部的方法问题。
2. 反射机制原理
反射机制允许 Java 程序在运行时调用Reflection API取得任何类的内部信息(比如成员变量、构造器、成员方法等),并能操作类的实例对象的属性以及方法。
在Java 程序中,JVM 加载完一个类后,在堆内存中就会产生该类的一个 Class 对象,一个类在堆内存中最多只会有一个 Class 对象,这个Class 对象包含了该类的完整结构信息,我们通过这个 Class 对象便可以得到该类的完整结构信息。
这个 Class 对象就像是一面镜子,我们透过这面镜子可以清楚地看到类的结构信息。因此,我们形象的将获取Class对象的过程称为:反射。
==Java 反射机制原理示意图:==
3. 反射优点和缺点
- 优点:可以
动态地创建和使用对象,反射机制是 Java 框架的底层核心,其使用灵活,没有反射机制,底层框架就失去支撑。- 缺点:使用反射基本是解释执行,对程序执行速度有影响。
4. 类加载概述
==在深入讲解反射前,先来介绍一下 Java中类的加载与反射机制。==
反射机制是 Java实现动态语言的关键,也就是通过反射实现类的动态加载。
- 静态加载:编译时就加载相关的类,如果程序中不存在该类则编译报错,依赖性太强。
- 动态加载:运行时加载相关的类,即使程序中不存在该类,但如果运行时未使用到该类,也不会编译错误,依赖性较弱。
==举个例子:==
1 | public class ClassLoad { |
- 上面代码中,根据 key 的值选择创建 Cat/Dog 对象,但是在代码编译时,编译器会先检查程序中是否存在 Cat 类,如果没有,则会编译报错;编译器不会检查是否存在 Dog 类,因为 Dog 类是使用反射的方式创建的,所以即使程序中不存在 Dog 类,也不会编译报错,而是等到程序运行时,我们真正选择了 key = 1 后,才会去检查 Dog 类是否存在。
类加载的时机:
- 静态加载
- 当新创建一个对象时(new),该类会被加载;
- 当调用类中的静态成员时,该类会被加载;
- 当子类被加载时,其超类也会被加载;
- 动态加载
- 通过反射的方式,在程序运行时使用到哪个类,该类才会被加载;
==类加载的过程图:==
5. 类加载各阶段完成的功能
- 加载阶段:将类的 class 文件读入内存,并为之创建一个 java.lang.Class 对象,此过程由类加载器完成。
- 连接阶段:又分为验证、准备、解析三个小阶段,此阶段会将类的二进制数据合并到 JRE 中。
- 初始化阶段:JVM 负责对类的
静态成员进行初始化。
==如下图所示:==
5.1 加载阶段
JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、jar 包、甚至网络文件)转换为
二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象。
5.2 连接阶段——验证
5.3 连接阶段——准备
JVM 会在该阶段对静态变量分配内存并进行默认初始化(不同数据类型会有其默认初始值,如:int —- 0,boolean —- false 等)。这些变量的内存空间会在方法区中分配。
==举例如下:==
1 | public class ClassLoad { |
- 代码说明:
- n1 是实例属性, 不是静态变量,因此在准备阶段,是不会分配内存
- n2 是静态变量,在该阶段 JVM 会为其分配内存,n2 默认初始化的值为 0 ,而不是 20
- n3 被 static final 修饰,是常量, 它和静态变量不一样, 其一旦赋值后值就不变,因此其默认初始化 n3 = 30
5.4 连接阶段——解析
JVM 将常量池内的符号引用替换为直接引用的过程。
5.5 初始化阶段
- 在初始化阶段,JVM 才会真正执行类中定义的 Java程序代码,此阶段是执行
() 方法的过程。 () 方法是由编译器按语句在源文件中出现的顺序,依次自动收集类中的所有 静态变量的赋值操作和静态代码块中的语句,并进行合并的过程。- JVM 会保证一个类的
() 方法 在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 () 方法,其他线程都要阻塞等待,直到活动线程执行 () 方法完毕。
==举例如下:==
1 | public class ClassLoad { |
==输出如下:==
1 | B 静态代码块被执行 |
代码说明:
- 加载阶段:加载 B类,并生成 B的 class对象
- 连接阶段:进行默认初始化 num = 0
- 初始化阶段:执行
<clinit>() 方法,该方法会依次自动收集类中的所有静态变量的赋值操作和静态代码块中的语句,并合并。如下:
1 | clinit() { |
- 合并后: num = 100
注意:加载类的时候,具有同步机制控制。如下:
1 | protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { |
二、Class 类
Cat 类在「类加载阶段」(JVM 加载 Cat.class 文件时),会自动创建一个唯一的 Class<Cat> 对象(存放在 JVM 的方法区);而每个通过 new Cat() 创建的 Cat 实例对象,都 “知道” 自己对应的这个 Class<Cat> 对象
- 你写的
class Cat{}是 “图纸”; - JVM 加载图纸后,自动生成 “图纸说明书”(
Class<Cat>对象); - 用图纸造出来的 “具体汽车”(
new Cat()),每个都带着 ‘说明书索引卡’”—— 所有汽车都指向同一份唯一的说明书(Class<Cat>对象)(能通过getClass()找到说明书)。
Class也是一个类,其类名就叫Class,因此它也继承 Object 类Class类对象不是由我们程序员创建(new)出来的,而是在类加载时由 JVM 自动创建的- 在
堆内存中最多只会存在某个类的唯一的Class对象,因为类只会加载一次 - 每个类的实例对象都会知道自己对应的
Class对象 - 通过
Class类对象可以完整地得到其对应的类的信息,通过一系列反射 API - 类的字节码二进制数据,是存放在方法区的,又称为
类的元数据(包括方法代码、变量名、方法名、访问权限等等)
除了int等基本类型外,Java的其他类型全部都是class(包括interface)。例如:
StringObjectRunnableException- …
仔细思考,我们可以得出结论:类
class(包括接口interface)的本质是数据类型(Type)。无继承关系的数据类型无法赋值:
1 | Number n = new Double(123.456); // 编译成功 |
而类
class是由 JVM 在执行过程中==动态加载==的。JVM在第一次读取到一种类class时,会将其加载进内存。
每加载一种
class,JVM就为其创建一个Class类的对象,并将两者关联起来。注意:这里的Class类是一个名字叫Class的类class。它长这样:
1 | public final class Class { |
以
String类为例,当 JVM 加载String类时,它首先读取String.class文件到内存,然后,在堆中为String类创建一个Class类对象并将两者关联起来:
1 | Class cls = new Class(String); |
注意:这个
Class类对象是 JVM 内部创建的,如果我们查看 JDK 源码,可以发现Class类的构造方法是private,即只有 JVM 能创建Class类对象,我们程序员自己的 Java 程序是无法创建Class类对象的。String类的源码里,没有任何 “创建Class<String>对象” 的代码 ——Class<String>对象的创建,完全是 JVM 的 “自发行为”,和String类本身的代码逻辑无关。具体创建时机:JVM 加载类时(类加载阶段)
当 JVM 需要使用某个类(比如
String、Cat)时,会执行「类加载流程」,流程的最后一步就是:JVM 自动创建该类对应的Class对象,并存放在 JVM 的「方法区」(元空间)。以
String类为例,创建流程是:- JVM 启动时,会默认加载
java.lang.String类(因为 String 是核心基础类,必须提前加载); - JVM 读取
String.class文件(字节码文件),解析里面的元信息(类名、字段、方法等); - JVM 底层调用
Class类的私有构造方法(private Class()),创建一个Class<String>对象; - 把这个
Class<String>对象存到方法区,后续所有String实例(new String("abc"))都通过 “指针” 指向它。
- JVM 启动时,会默认加载
所以,JVM持有的每个
Class类对象都指向一个数据类型(class或interface):
1 | ┌───────────────────────────┐ |
一个
Class类对象包含了其对应的类class的所有完整信息:
1 | ┌───────────────────────────┐ |
由于JVM为每个加载的类class创建了对应的Class类对象,并在实例中保存了该类class的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class类对象,我们就可以通过这个Class类对象获取到其对应的类class的所有信息。
这种通过Class实例获取类class信息的方法称为反射(Reflection)。
如何获取一个class的Class实例?有5个方法:
方法一:直接通过一个类
class中的静态变量class获取:
1 | Class cls = String.class;// class 是 String 类中的一个静态变量 |
方法二:如果我们有一个类
class的对象,可以通过该对象引用提供的getClass()方法获取:
1 | String s = "Hello"; |
方法三:如果知道一个类
class的完整类名,可以通过Class类的静态方法Class.forName()获取:
1 | Class cls = Class.forName("java.lang.String");// java.lang.String 是 String 类的完整类名 |
方法四:对于基本数据类型(int、char、boolean、float 等),通过 基本数据类型.class 获取:
1 | Class integerClass = int.class; |
方法五:对于基本数据类型对应的包装类,可以通过类中的静态变量
TYPE获取到Class类对象:
1 | Class type1 = Integer.TYPE; |
- 注意:对于基本数据类型获取到的
Class类对象和基本数据类型对应的包装类获取到的Class类对象,是同一个Class类对象:
1 | System.out.println(integerClass.hashCode()); |
因为
Class类对象在 JVM 中是唯一的,所以,上述方法获取的Class类对象是同一个对象。可以用==比较两个Class类对象:
1 | Class cls1 = String.class; |
注意一下用
==比较Class类对象和用instanceof的差别:
1 | Integer n = new Integer(123); |
用instanceof
不但匹配指定类型,还匹配指定类型的子类。而用==比较class类对象可以精确地判断数据类型,但不能用作子类型比较。
- 通常情况下,我们应该用
instanceof判断数据类型,因为面向抽象编程的时候,我们不关心具体的子类型。 - 只有在需要精确判断一个类型是不是某个
class的时候,我们才使用==判断class实例。
- 通常情况下,我们应该用
因为反射的目的是为了获得某个类的实例对象的信息。因此,当我们拿到某个
Object对象时,可以通过反射直接获取该Object的class信息,而不需要使用向下转型:
1 | void printObjectInfo(Object obj) { |
要从
Class实例获取获取的基本信息,参考下面的代码(只是简单示范,后面会具体介绍):
1 | public class Main { |
- 注意到数组(例如
String[])也是一种类,而且不同于String.class,它的类名是[Ljava.lang.String;。此外,JVM为每一种基本类型如int也创建了Class实例,通过int.class访问。
如果获取到了一个
Class类对象,我们就可以通过该Class类对象来创建其对应类的实例对象:
1 | // 获取 String 的 Class 类对象: |
- 上述代码相当于
new String()。通过Class.newInstance()可以创建类的实例对象,它的局限是:只能调用public的无参数构造方法。带参数的构造方法,或者非public的构造方法都无法通过Class.newInstance()被调用。
1. 动态加载
JVM在执行 Java程序的时候,并不是一次性把所有用到的
class全部加载到内存,而是第一次需要用到class时才加载。例如:
1 | public class Main { |
- 当执行
Main.java时,由于用到了Main类,因此,JVM 首先会把Main类对应的Class类对象Main.class加载到内存中。然而,并不会加载Person.class,除非程序执行到create()方法,JVM 发现需要加载Person类时,才会首次加载Person类对应的Class类对象Person.class。如果没有执行create()方法,那么Person.class根本就不会被加载。 - 这就是 JVM动态加载
class的特性。
动态加载类
class的特性对于 Java 程序非常重要。利用 JVM 动态加载class的特性,我们才能在运行期根据条件去加载不同的实现类。例如,Commons Logging 总是优先使用 Log4j,只有当 Log4j 不存在时,才使用 JDK 的 logging。利用 JVM 动态加载特性,大致的实现代码如下:
1 | // Commons Logging优先使用Log4j: |
- 这就是为什么我们只需要把 Log4j 的 jar 包放到 classpath 中,Commons Logging 就会自动使用 Log4j 的原因。
2. 小结
- JVM为每个加载的类
class及接口interface创建了对应的Class类对象来保存class及interface的所有信息; - 获取一个类
class对应的Class类对象后,就可以获取该类class的所有信息; - 通过 Class类对象获取
class信息的方法称为反射(Reflection); - JVM 总是动态加载
class,可以在运行期根据条件来控制加载类class。
三、访问字段
对任意的一个Object实例,只要我们获取了它对应的Class类对象,就可以获取它的一切信息。
我们先看看如何通过
Class类对象获取其对应的类定义的字段信息。Class类提供了以下几个方法来获取字段:
Field getField(name):根据字段名获取某个 public 的 field(包括父类)Field getDeclaredField(name):根据字段名获取当前类的某个 field(不包括父类)Field[] getFields():获取所有 public 的 field(包括父类)Field[] getDeclaredFields():获取当前类的所有 field(不包括父类)
==我们来看一下示例代码:==
1 | public class Main { |
- 上述代码首先获取
Student的Class实例,然后,分别获取public字段、继承的public字段以及private字段,打印出的Field类似下面:
1 | public int Student.score |
- 一个
Field对象包含了一个字段的所有信息:getName():返回字段名称,例如,"name";getType():返回字段类型,也是一个Class类对象,例如,String.class;getModifiers():返回字段的修饰符,它是一个int,不同的 bit 表示不同的含义。
以
String类的value字段为例,它的定义是:
1 | public final class String { |
我们用反射获取该字段的信息,代码如下:
1 | Field f = String.class.getDeclaredField("value"); |
1. 获取字段值
利用反射拿到字段的一个Field类对象只是第一步,我们还可以拿到一个实例对象对应的该字段的值。
例如,对于一个
Person类对象,我们可以先拿到其name字段对应的Field,再获取这个Person类对象的name字段的 值:
1 | import java.lang.reflect.Field; |
- 上述代码先获取
Person类对应的Class类对象,再通过该Class类对象获取Field类对象,然后,用Field.get(Object)获取指定Person类对象的指定字段的值。 - 运行代码,如果不出意外,会得到一个
IllegalAccessException异常,这是因为name被定义为一个private字段,正常情况下,Main类无法访问Person类的private字段。要修复错误,可以将private改为public,或者,在调用Object value = f.get(p);前,先写一句:
1 | f.setAccessible(true); |
- 调用
Field.setAccessible(true)的意思是,别管这个字段是不是public,一律允许访问。 - 可以试着加上上述语句,再运行代码,就可以打印出
private字段的值。
有童鞋会问:如果使用反射可以获取private字段的值,那么类的封装还有什么意义?
- 答案是一般情况下,我们总是通过
p.name来访问Person的name字段,编译器会根据public、protected和private这些访问权限修饰符决定是否允许访问字段,这样就达到了数据封装的目的。 - 而反射是一种非常规的用法,使用反射,首先代码非常繁琐;其次,它更多地是给工具或者底层框架来使用,目的是在不知道目标对象任何信息的情况下,获取特定字段的值。
此外,setAccessible(true)可能会失败。 如果 JVM 运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证 JVM 核心库的安全。
2. 设置字段值
通过 Field 类对象既然可以获取到指定对象的字段值,自然也可以设置字段的值。
设置字段值是通过
Field.set(Object, Object)实现的,其中第一个Object参数是指定的对象,第二个Object参数是待修改的值。示例代码如下:
1 | import java.lang.reflect.Field; |
- 运行上述代码,输出的
name字段从Xiao Ming变成了Xiao Hong,说明通过反射可以直接修改指定对象的字段的值。 - 同样的,修改非
public字段,需要调用setAccessible(true)。
3. 小结
- Java 的反射 API 提供的
Field类封装了对应的类定义的全部字段的所有信息: - 通过
Class类对象的方法可以获取Field类对象:getField(),getFields(),getDeclaredField(),getDeclaredFields(); - 通过
Field类对象可以获取类定义字段信息:getName(),getType(),getModifiers(); - 通过
Field类对象可以读取或设置某个对象的字段的值,如果存在访问限制,则需要调用setAccessible(true)来访问非public字段。 - 通过反射读写字段是一种非常规的方法,它会破坏对象的封装。
四、调用方法
我们已经能通过
Class类的Field类对象获取其对应的类class中定义的所有字段信息,同样的,可以通过Class类获取所有Method信息。Class类提供了以下几个方法来获取类class中定义的Method:
Method getMethod(name, Class...):获取某个public的Method(包括父类)Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)Method[] getMethods():获取所有public的Method(包括父类)Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
==我们来看一下示例代码:==
1 | public class Main { |
- 上述代码首先获取
Student的Class类对象,然后,分别获取Student类中定义的public方法、继承的public方法以及private方法,打印出的Method类似:
1 | public int Student.getScore(java.lang.String) |
一个Method类对象包含一个方法的所有信息:
getName():返回方法名称,例如:"getScore";getReturnType():返回方法的返回值类型,也是一个Class实例,例如:String.class;getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class};getModifiers():返回方法的修饰符,它是一个int,不同的 bit 表示不同的含义。
1. 调用方法
当我们获取到一个
Method类对象时,就可以对它进行调用。我们以下面的代码为例:
1 | // 一般情况下调用 String 类的 substring() 方法 |
如果用反射来调用
substring方法,需要以下代码:
1 | import java.lang.reflect.Method; |
- 注意到
substring()有两个重载方法,我们获取的是String substring(int)这个方法(即形参类型为 int,且只有一个)。思考一下如何获取String substring(int, int)方法。 - 对
Method类对象调用invoke方法就相当于调用该substring(int)方法,invoke的第一个参数是实例对象(即在哪个实例对象上调用该方法),后面的实参要与方法参数的类型一致,否则将报错。
2. 调用静态方法
如果获取到的
Method表示一个静态方法,调用静态方法时,由于无需指定实例对象,所以invoke方法传入的第一个参数永远为null。我们以Integer.parseInt(String)方法为例:
1 | import java.lang.reflect.Method; |
3. 调用非 public方法
和
Field类对象类似,对于非 public 方法,我们虽然可以通过Class.getDeclaredMethod()获取该方法的实例对象,但直接对其调用将得到一个IllegalAccessException异常。为了调用非 public 方法,我们通过Method.setAccessible(true)允许其调用:
1 | import java.lang.reflect.Method; |
- 同样,
setAccessible(true)可能会失败。如果 JVM 运行期存在SecurityManager,那么它会根据规则进行检查,有可能阻止setAccessible(true)。例如,某个SecurityManager可能不允许对java和javax开头的package的类调用setAccessible(true),这样可以保证 JVM 核心库的安全。
4. 多态
我们来考率这样一种情况:一个
Person类定义了hello()方法,并且它的子类Student也重写了hello()方法,那么,从Person.class获取的Method,作用于Student类对象时,调用的hello()方法到底是哪个?
1 | import java.lang.reflect.Method; |
- 运行上述代码,发现输出的是
Student:hello,因此,使用反射调用方法时,仍然遵循多态原则:即总是调用实际类型的重写方法(如果存在)。 上述的反射代码:
1 | Method m = Person.class.getMethod("hello"); |
- 实际上相当于:
1 | Person p = new Student(); |
5. 小结
- Java 的反射 API 提供的
Method类对象封装了类定义的全部方法的所有信息: - 通过
Class类对象的方法可以获取Method类对象:getMethod(),getMethods(),getDeclaredMethod(),getDeclaredMethods(); - 通过
Method类对象可以获取方法信息:getName(),getReturnType(),getParameterTypes(),getModifiers(); - 通过
Method类对象可以调用某个对象的方法:Object invoke(Object instance, Object... parameters); - 通过设置
setAccessible(true)来访问非public方法; - 通过反射调用方法时,仍然遵循多态原则。
五、调用构造方法
一般情况下,我们通常使用
new操作符创建新的对象:
1 | Person p = new Person(); |
如果通过反射来创建新的对象,可以调用
Class提供的newInstance()方法:
1 | Person p = Person.class.newInstance(); |
- 调用
Class.newInstance()的局限是,它只能调用该类的public无参构造方法。如果构造方法带有参数,或者不是public,就无法直接通过Class.newInstance()来调用。
为了调用任意的构造方法,Java 的反射 API 提供了
Constructor类对象,它包含一个构造方法的所有信息,通过Constructor类对象可以创建一个类的实例对象。Constructor类对象和Method类对象非常相似,不同之处仅在于它是一个构造方法,并且,调用结果总是返回一个类的实例对象:
1 | import java.lang.reflect.Constructor; |
通过Class实例获取Constructor的方法如下:
getConstructor(Class...):获取某个public的Constructor;getDeclaredConstructor(Class...):获取某个Constructor;getConstructors():获取所有public的Constructor;getDeclaredConstructors():获取所有Constructor。
注意:Constructor类对象只含有当前类定义的构造方法,和父类无关,因此不存在多态的问题。
同样,调用非public的Constructor时,必须首先通过setAccessible(true)设置允许访问。但setAccessible(true)也可能会失败。
小结
Constructor类对象封装了其对应的类定义的构造方法的所有信息;- 通过
Class类对象可以获取Constructor类对象:getConstructor(),getConstructors(),getDeclaredConstructor(),getDeclaredConstructors(); - 通过
Constructor类对象可以创建一个对应类的实例对象:newInstance(Object... parameters); 通过设置setAccessible(true)来访问非public构造方法。
六、获取继承方法
当我们获取到某个
Class类对象时,实际上就获取到了一个类的类型:
1 | Class cls = String.class; // 获取到 String 的 Class类对象 |
还可以用类对象的
getClass()方法获取:
1 | String s = ""; |
最后一种获取
Class的方法是通过Class.forName(""),传入Class的完整类名获取:
1 | Class s = Class.forName("java.lang.String"); |
这三种方式获取的Class类对象都是同一个对象,因为 JVM 对每个加载的Class只创建一个Class类对象来表示它的类型。
1. 获取父类的Class
有了Class类对象,我们还可以获取它的父类的Class类对象:
1 | public class Main { |
- 运行上述代码,可以看到,
Integer的父类类型是Number,Number的父类是Object,Object的父类是null。除Object外,其他任何非接口interface的Class类对象都必定存在一个父类类型。
2. 获取interface
由于一个类可能实现一个或多个接口,通过
Class我们就可以查询到实现的接口类型。例如,查询Integer实现的接口:
1 | import java.lang.reflect.Method; |
运行上述代码可知,Integer实现的接口有:
- java.lang.Comparable
- java.lang.constant.Constable
- java.lang.constant.ConstantDesc
要特别注意:
getInterfaces()方法只返回当前类直接实现的接口类型,并不包括其父类实现的接口类型:
1 | // reflection |
Integer的父类是Number,Number类实现的接口是java.io.Serializable。
此外,对所有接口
interface的Class类对象调用getSuperclass()返回的是null,获取接口的父接口要用getInterfaces():
1 | System.out.println(java.io.DataInputStream.class.getSuperclass()); |
- 如果一个类没有实现任何
interface,那么getInterfaces()返回空数组。
3. 继承关系
当我们判断一个对象是否是某个类型时,正常情况下,使用
instanceof操作符:
1 | Object n = Integer.valueOf(123); |
如果是两个
Class类对象,要判断一个向上转型是否成立,可以调用isAssignableFrom()方法:
1 | // Integer i = ? |
4. 小结
- 通过
Class对象可以获取继承关系:Class getSuperclass():获取父类类型;Class[] getInterfaces():获取当前类实现的所有接口。
- 通过
Class对象的isAssignableFrom()方法可以判断一个向上转型是否可以实现。
七、动态代理
我们来比较 Java 的类class和接口interface的区别:
- 可以实例化类
class(非abstract); - 不能实例化接口
interface。
所有接口
interface类型的变量总是通过某个实现了接口的类的对象向上转型再赋值给接口类型的变量:
1 | CharSequence cs = new StringBuilder(); |
有没有可能不编写实现类,直接在运行期创建某个interface的实例呢?
这是可能的,因为 Java 标准库提供了一种动态代理(Dynamic Proxy)的机制:可以在运行期动态创建某个interface的实例。
什么叫运行期动态创建?听起来好像很复杂。所谓动态代理,是和静态相对应的。我们来看静态代理代码怎么写:
一、定义接口:
1 | public interface Hello { |
二、编写实现类:
1 | public class HelloWorld implements Hello { |
三、创建实例,转型为接口并调用:
1 | Hello hello = new HelloWorld(); |
- 这种方式就是我们通常编写代码的方式。
还有一种方式是动态代码,我们仍然先定义了接口Hello,但是我们并不去编写实现类,而是直接通过 JDK 提供的一个Proxy.newProxyInstance()方法创建了一个Hello接口对象。这种没有实现类但是在运行期动态创建了一个接口对象的方式,我们称为动态代理。JDK 提供的动态创建接口对象的方式,就叫动态代理。
一个最简单的动态代理实现如下:
1 | import java.lang.reflect.InvocationHandler; |
在运行期动态创建一个interface实例的方法如下:
定义一个
InvocationHandler实例,它负责实现接口的方法调用;通过
1
Proxy.newProxyInstance()
创建
1
interface
实例,它需要3个参数:
- 使用的
ClassLoader,通常就是接口类的ClassLoader; - 需要实现的接口数组,至少需要传入一个接口进去;
- 用来处理接口方法调用的
InvocationHandler实例。
- 使用的
将返回的
Object强制转型为接口。
动态代理实际上是JVM在运行期动态创建class字节码并加载的过程,它并没有什么黑魔法,把上面的动态代理改写为静态实现类大概长这样:
1 | public class HelloDynamicProxy implements Hello { |
- 其实就是 JVM 帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。
小结
- Java 标准库提供了动态代理功能,允许在运行期动态创建一个接口的实例;
- 动态代理是通过
Proxy创建代理对象,然后将接口方法“代理”给InvocationHandler完成的。
看不懂再看:
一、先看 “不用动态代理” 和 “用动态代理” 的对比(最直观)
假设你要实现Hello接口的morning方法,做同样的事情(打印 “Good morning, XXX”):
方式 1:不用动态代理(你肯定懂)
1 | // 1. 写一个接口(规则) |
流程:写接口 → 写实现类 → new 对象 → 调用方法。
方式 2:用动态代理(核心是 “不用写实现类”)
1 | // 1. 写一个接口(和上面一样,规则不变) |
流程:写接口 → 写逻辑处理器 → JVM 生成对象 → 调用方法。
核心区别:不用写HelloImpl这个实现类了!JVM 帮你 “偷偷造” 了一个能干活的对象。
二、一步步看动态代理是怎么 “干活” 的(像看动画片)
我们把hello.morning("Bob")这一步拆成 3 帧,看每一步到底发生了什么:
帧 1:你调用hello.morning("Bob")
你以为在调用HelloImpl的morning方法,但其实hello不是HelloImpl的对象 —— 是 JVM 给你造的 “临时对象”(代理对象)。
帧 2:代理对象 “转手”
这个代理对象自己啥也不会干,它收到morning调用后,立刻转头喊:“handler,你来处理!” 并把 3 个信息传给handler:
- 我是谁(proxy,不用管);
- 要做什么(method:就是
morning方法); - 用什么参数(args:就是
"Bob")。
帧 3:handler “干活”
handler的invoke方法被触发,里面就是你写的逻辑(打印 “Good morning, Bob”),执行完就结束了。
一句话总结这个过程:你找代理对象办事,代理对象自己不办,转手交给 handler 办 ——handler 才是真正干活的。
三、动态代理到底省了什么事?(解决实际问题)
假设你现在有 3 个接口,都要加 “调用前打印日志” 的逻辑:
1 | interface Hello { void morning(String name); } |
不用动态代理:你要写 3 个实现类
1 | class HelloImpl implements Hello { |
每个实现类都要写一遍 “打印日志”,重复劳动!
用动态代理:只写 1 个 handler,管所有接口
1 | // 1个handler搞定所有接口的日志逻辑 |
- 不用写 3 个实现类,省了大量重复代码;
- 如果要改日志逻辑(比如加时间戳),只改 1 个 handler 就行,不用改 3 个地方。
四、最后记住 3 个 “不用懂但要会用” 的关键点
- 动态代理只能代理 “接口”,不能代理普通类;
- 你只需要写
InvocationHandler(里面是真正要做的事); - 用
Proxy.newProxyInstance就能拿到接口对象,直接调用方法就行。






