10.Java反射与动态代理

Java 反射与动态代理
前面学的是:知道某个类,就创建对象、调用方法。现在换个场景——直到程序运行时,才知道要使用哪个类或方法。这就是我们学习反射的起点。
我们先认识 Class 和类中的成员,再实际操作对象,最后通过代理给原功能加上日志。
1. 什么是反射
反射让程序在运行时取得类的信息,也可以据此创建对象、读写字段、调用方法。
平时写 student.show(),编写代码时就知道要调用 show。框架却可能从配置里拿到方法名,只在运行时知道该找哪个方法,这就需要反射。
可以先记住这条路线:
text
取得 Class → 找到构造方法、字段或方法 → 执行操作
反射不一定要先有对象,也不是把 Java 变成完全动态的语言。明确知道类型时,直接调用通常更简单。
2. Class:描述一个类型
Class 对象保存一个类型的信息,例如类名、父类、字段和方法。要通过反射找成员,先拿到对应的 Class。
使用 User,先看三种获取方式是不是指向同一份类型信息。为了下一节能观察接口,这个小例子让它实现 Serializable:
java
import java.io.Serializable;
public class User implements Serializable {
private static final long serialVersionUID = 1L;
}
分别通过类名、类字面量和已有对象获取:
java
// 通过类名文字获取类型信息,有包时需要填写完整类名。
Class<?> first = Class.forName("User");
Class<User> second = User.class;
User user = new User();
Class<?> third = user.getClass();
System.out.println(first == second); // true
System.out.println(second == third); // true
三种方式拿到的是同一个 Class。这里指的是同一个类加载器加载的同一个类,不是所有名字相同的类都一定相同。
User.class 适合编写代码时就知道类型;getClass() 适合已经有对象;forName() 适合只拿到类名文字。这个例子没有包,有包时要写完整名称,例如 "com.example.User"。
3. 获取接口与父类
getInterfaces() 返回直接实现的接口,getSuperclass() 返回直接父类。
有时我们想知道“这个类实现了什么接口,继承了谁”,就可以用它们查询。继续查看刚才的 User:
java
Class<User> type = User.class;
// 取得直接实现的接口,不递归列出所有继承关系。
for (Class<?> api : type.getInterfaces()) {
System.out.println(api.getSimpleName()); // Serializable
}
System.out.println(type.getSuperclass().getSimpleName()); // Object
一个类可以实现多个接口,所以接口用数组返回。这里查的是直接关系,不会递归列出全部父类和接口继承链。
4. 获取构造方法、方法与字段
反射分别用 Constructor、Method、Field 表示这三类成员。
取得它们的描述后,才能进行创建、调用或读写。先把查找方式认清,下一节就能看懂执行步骤。
| 要找的内容 | 公有成员 | 当前类自己声明的成员 |
|---|---|---|
| 构造方法数组 | getConstructors() |
getDeclaredConstructors() |
| 指定构造方法 | getConstructor(...) |
getDeclaredConstructor(...) |
| 方法数组 | getMethods() |
getDeclaredMethods() |
| 指定方法 | getMethod(...) |
getDeclaredMethod(...) |
| 字段数组 | getFields() |
getDeclaredFields() |
| 指定字段 | getField(...) |
getDeclaredField(...) |
公有方法、字段的查找可以包含继承来的成员。getDeclared*() 查当前类自己声明的成员,包括 private,但不查继承成员。
下面使用学生类,只保留编号,省去与当前操作无关的姓名字段。有参构造补上赋值,避免创建后编号仍然为空:
java
public class Student {
private String id;
public Student() {
}
public Student(String id) {
// 创建时把编号保存到对象中,否则字段仍然是 null。
this.id = id;
}
public void setId(String id) {
this.id = id;
}
public void show() {
System.out.println("学生编号:" + id);
}
}
先看看 Student 自己声明了哪些字段:
java
import java.lang.reflect.Field;
// 查当前类自己声明的字段,也能找到 private 字段。
for (Field field : Student.class.getDeclaredFields()) {
System.out.println(field.getName()); // id
}
找到了 private 字段,不等于已经获得操作它的权限。第六节会处理这个区别。
5. 反射调用方法
Method 表示一个方法,invoke() 用来调用这个方法。
这次把学生编号从 1008611 改成 9527,再调用 show()。平时直接调用 setId();现在假设方法名是在运行时拿到的,就根据名称找方法,再调用它。
java
import java.lang.reflect.Method;
Student student = new Student("1008611");
// 找接收 String 参数的方法,这里写的是参数类型。
Method setId = Student.class.getMethod("setId", String.class);
// 在这个学生对象上调用方法,这里传的是实际参数。
setId.invoke(student, "9527");
Method show = Student.class.getMethod("show");
show.invoke(student); // 学生编号:9527
getMethod("setId", String.class) 查找一个接收 String 的公有方法;invoke(student, "9527") 在这个学生对象上执行它。一个写参数类型,一个传实际参数。
无参方法查找时不用写参数类型,调用时只传目标对象。查找的类型要与方法声明一致,int.class 和 Integer.class 不能随意替换。
6. 反射读写字段
Field 表示一个字段。通过它,可以读出某个对象的值,也可以修改值。
我们仍使用同一个学生类,把编号从 1008611 改成 9527。这次故意选择 private 字段,好看清访问权限这一步。
java
import java.lang.reflect.Field;
Student student = new Student("1008611");
Field field = Student.class.getDeclaredField("id");
// 找到了私有字段,还要先尝试取得访问权限。
if (!field.trySetAccessible()) {
throw new IllegalAccessException("无法访问 id");
}
// 修改的是这个 student 对象里的 id,不是所有学生的编号。
field.set(student, "9527");
System.out.println(field.get(student)); // 9527
student.show(); // 学生编号:9527
trySetAccessible() 尝试开放访问,失败会返回 false。普通业务更推荐用构造方法、公开修改方法维护数据,不要为了方便就绕过类自己的规则。
它也不是万能钥匙,JDK 内部类或未开放的模块可能不允许访问。
7. 反射调用构造方法
Constructor 表示构造方法,调用 newInstance() 会创建对象。
这样即使实例还不存在,我们也能先取得类的信息,再根据构造方法创建学生。
java
import java.lang.reflect.Constructor;
Constructor<Student> constructor = Student.class.getConstructor(String.class);
// 通过构造方法创建对象,参数交给 Student(String id)。
Student student = constructor.newInstance("1008611");
student.show(); // 学生编号:1008611
String.class 表示寻找“接收一个字符串”的构造方法;"1008611" 是创建时真正传入的值。
无参构造写 getConstructor(),创建时写 newInstance(),不用传 null。不要使用已经弃用的 Class.newInstance()。
上面这些反射操作可能抛出受检异常,运行它们的方法可以声明 throws ReflectiveOperationException,或按具体原因捕获。
8. 静态代理
代理就是让调用先经过代理对象,再由它调用原对象。代理可以在原操作前后增加处理。
使用卖手机的例子:苹果和华为各自负责销售,现在想在销售前加一句日志。如果分别修改它们,重复代码就会散在多个类里。可以让代理统一记录,手机类继续只管销售。
先定义手机销售接口:
java
public interface Phone {
// 调用方只依赖这个接口,不必固定使用某一种手机实现。
String salePhone();
}
苹果负责卖苹果手机:
java
public class Apple implements Phone {
@Override
public String salePhone() {
return "卖苹果手机";
}
}
华为负责卖华为手机:
java
public class Huawei implements Phone {
@Override
public String salePhone() {
return "卖华为手机";
}
}
代理也实现 Phone,先记录,再把调用交给原手机对象:
java
public class PhoneProxy implements Phone {
private final Phone phone;
public PhoneProxy(Phone phone) {
this.phone = phone;
}
@Override
public String salePhone() {
System.out.println("代理模式");
// 销售仍由原手机对象完成,代理把原返回值直接交给调用方。
return phone.salePhone();
}
}
创建代理并调用:
java
// 调用方使用代理对象,后面的销售调用才会经过代理。
Phone phone = new PhoneProxy(new Apple());
System.out.println(phone.salePhone());
先输出“代理模式”,再输出“卖苹果手机”。想卖华为,只把 new Apple() 换成 new Huawei()。两个手机类都不需要添加日志代码。
这叫静态代理,因为 PhoneProxy 是我们事先写好的代理类。
9. 动态代理
动态代理是在运行时按接口生成代理对象。Proxy 负责创建代理,InvocationHandler 负责收到调用后做什么。
除了手机,汽车也有销售业务。如果两种业务都想加同一句日志,分别写代理类就会重复。动态代理让我们把这段处理写一次,再用于不同接口。
先定义处理器 MyInvocationHandle,重点是“输出日志,再调用原对象”:
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MyInvocationHandle implements InvocationHandler {
// 每个处理器固定自己的目标,避免创建汽车代理时改绑旧手机代理。
private final Object target;
public MyInvocationHandle(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 比较、哈希码和打印代理单独处理,不当作销售调用记录。
if (method.getDeclaringClass() == Object.class) {
return switch (method.getName()) {
case "equals" -> proxy == args[0];
case "hashCode" -> System.identityHashCode(proxy);
case "toString" -> "代理对象";
default -> throw new AssertionError(method.getName());
};
}
// 附加功能放在这里,原手机类和汽车类不需要加日志。
System.out.println("动态代理模式");
try {
// 只调用原目标一次,参数原样转交,结果直接返回;不要改成调用 proxy。
return method.invoke(target, args);
} catch (InvocationTargetException e) {
// 去掉反射的异常包装,继续抛出原业务异常。
throw e.getCause();
}
}
}
先看销售调用经过的三步:
target保存原手机对象。- 收到调用后,输出“动态代理模式”。
method.invoke(target, args)执行原方法,把原结果返回。
最后的 catch 是反射调用的收尾:目标方法抛出的异常会被包在 InvocationTargetException 里,取出原异常再抛出,调用方才能继续按原来的异常类型处理。
开头的 Object 分支处理比较、哈希码和打印代理这三个普通操作。这里约定“代理和自己相等,使用自己的身份”,它们不参与业务日志;先记住用途,不影响理解销售的三步。
现在创建手机代理。每个代理都 new 一个自己的处理器:
java
import java.lang.reflect.Proxy;
Phone target = new Apple();
// 生成实现 Phone 的代理,并交给一个独立的处理器。
Phone phone = (Phone) Proxy.newProxyInstance(
Phone.class.getClassLoader(),
new Class<?>[]{Phone.class},
new MyInvocationHandle(target));
System.out.println(phone.salePhone());
输出:
text
动态代理模式
卖苹果手机
三个参数分别是:使用哪个类加载器、代理实现哪些接口、由哪个处理器接收调用。生成的对象实现了 Phone,因此调用方仍然通过 Phone 使用它。
同一个处理器类也能代理汽车
汽车也有自己的销售接口,这里用宝马演示:
java
public interface Car {
String saleCar();
}
java
public class BMW implements Car {
@Override
public String saleCar() {
return "卖宝马汽车";
}
}
换成汽车接口和汽车目标,处理器的代码不需要改:
java
import java.lang.reflect.Proxy;
// 汽车也用同一个处理器类,但新建自己的处理器对象。
Car car = (Car) Proxy.newProxyInstance(
Car.class.getClassLoader(),
new Class<?>[]{Car.class},
new MyInvocationHandle(new BMW()));
System.out.println(car.saleCar());
输出“动态代理模式”和“卖宝马汽车”。我们复用的是处理器类,每个代理持有自己的处理器对象。
如果先后用同一个处理器 bind(Apple)、bind(BMW),会把它的 target 改成宝马。旧手机代理仍指向这个处理器,后面再卖手机就出错了。这里把目标放进构造方法并设为 final,避免旧代理被悄悄改绑。
10. 怎样改功能,又保留原来的销售逻辑
代理可以在原业务外面增加处理。现在有一个小需求:卖手机之前先记录一句日志,后来又想把日志改成“收到销售请求”,但销售结果仍然是“卖苹果手机”。
如果把日志写进 Apple.salePhone(),以后每次改日志都要修改手机类。把日志放进代理,就可以只改代理,手机类继续负责原来的销售。
我们分两步做:**先让调用经过代理,再修改代理里的附加功能。**下面继续使用上一节的 Phone、Apple 和 MyInvocationHandle。
第一步:让销售调用先经过代理
原来直接创建苹果手机对象:
java
Phone phone = new Apple();
System.out.println(phone.salePhone()); // 卖苹果手机
这次调用不会进入代理,所以只有销售结果,没有日志。
把创建对象的地方换成下面的写法:
java
import java.lang.reflect.Proxy;
Phone target = new Apple(); // 原手机对象,继续负责销售
Phone phone = (Phone) Proxy.newProxyInstance(
Phone.class.getClassLoader(),
new Class<?>[]{Phone.class},
new MyInvocationHandle(target));
// 使用代理调用,才会先记录日志,再执行原销售方法。
System.out.println(phone.salePhone());
使用第九节的处理器时,输出:
text
动态代理模式
卖苹果手机
创建对象的方式变了,后面仍然调用 phone.salePhone()。这就是接入代理的地方,实际项目中通常放在统一创建或装配对象的入口。
第二步:修改代理里的日志
现在想把日志改成“收到销售请求”。在第九节的 MyInvocationHandle.invoke() 中,找到这一行:
java
System.out.println("动态代理模式");
把它替换成下面这一行,处理器的其他代码保持原样:
java
// 只修改附加日志,后面的目标调用和返回值处理保持原样。
System.out.println("收到销售请求");
然后重新编译运行第一步的代理调用代码,输出就变成:
text
收到销售请求
卖苹果手机
对照两次结果:第一行日志变了,第二行销售结果没变。Apple.salePhone() 没有修改,仍然返回“卖苹果手机”。同一个处理器也用于华为或宝马时,它们经过代理的调用也会使用新的日志文字。
这里修改的是日志这项附加功能,不是销售规则。以后还要改日志内容,就在处理器里改;不用到每个销售类里分别改一遍。
原来的销售行为怎样保留下来
上一节的处理器还做了几件事,保证日志修改不会顺便改变销售调用:
| 要保留什么 | 怎么做 |
|---|---|
| 原目标 | 每个代理独立保存目标,不能改绑旧处理器 |
| 参数 | 有参数时把 args 原样传过去,不修改参数 |
| 返回值 | 直接返回 method.invoke() 的结果 |
| 业务异常 | 解包后抛出原异常,不改成默认结果 |
| 执行次数 | 目标只调用一次,不额外重试 |
本例的销售方法没有参数,所以 args 可能为 null;直接交给 invoke() 即可,不要先访问它的长度。
如果其他地方仍直接创建 Apple 并调用它,就不会经过代理,也不会出现日志。代理不会自动接管所有旧对象,使用方需要拿到代理对象。
补充:以后换成日志工具,记录失败时怎么办
上面先用控制台输出看清修改过程。如果以后换成可能抛出异常的日志工具,而业务允许丢失一次日志,就要把日志失败单独处理。把处理器里的日志输出替换成下面这段,目标调用仍放在它后面:
java
// 只处理附加日志的失败,不要把原销售调用也放进这个 catch 范围。
try {
System.out.println("收到销售请求");
} catch (RuntimeException e) {
// 允许丢失这次日志,继续执行后面的销售调用。
}
这里只有附加日志允许失败;原销售方法的异常仍然继续抛出,不能把两者一起吞掉。控制台输出通常不抛这种异常,这个结构主要是给以后更换日志工具时用的。
“不改原业务”指保留上面的参数、结果、异常和执行次数。日志仍会占用时间,代理也不是原对象本身。如果要增加权限检查、重试或改价格,那就可能改变业务行为,需要按新的规则设计。
补充:代理的几个边界
- JDK 动态代理基于接口,返回的对象可以当
Phone,不能当作Apple的子类。 - 无参调用时,
args可能是null,不要直接访问它的长度。 - 委托目标必须是
target。写成method.invoke(proxy, args)会再次进入自己,造成递归。 - 直接访问字段、静态方法、绕过代理调用目标,都不会进入这个处理器。
- 目标对象内部通过
this调自己的其他方法,通常不会重新经过外部代理。 - 处理器抛出接口未允许的受检异常时,会被包装成
UndeclaredThrowableException。 final target只保证不会换目标,不保证目标内部数据线程安全。- 同名类由不同类加载器定义时,可以是不同类型。
Class.forName(name)通常会初始化类,而取得Type.class本身不触发初始化。 - 反射成员数组不保证按源码顺序排列,
getPackageName()返回的是字符串。

评论