← 返回首页
Java学习#Java

10.Java反射与动态代理

2026-10-07·0 次浏览
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();
        }
    }
}

先看销售调用经过的三步:

  1. target 保存原手机对象。
  2. 收到调用后,输出“动态代理模式”。
  3. 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() 返回的是字符串。

Class API · Method API · Proxy API · InvocationHandler API

── 完 ──

评论

还没有评论,来说点什么
发表评论
0 / 1000