JDK 动态代理: 它的工作原理是什么?它有什么限制?
核心定义
JDK 动态代理是 Java 官方提供的一种在运行时动态创建代理对象的技术。它允许我们创建一个“替身”对象,这个替身可以在不修改原始业务代码的情况下,拦截对原始对象方法的调用,并在调用前后插入自定义的逻辑(即 AOP 中的“通知”)。
工作原理
JDK 动态代理的核心是两个组件:java.lang.reflect.Proxy
类和 java.lang.reflect.InvocationHandler
接口。
它的工作流程可以分解为以下几个步骤:
1. 前提:定义接口和目标类
首先,你必须有一个接口,以及一个实现了该接口的目标类(也就是你想要代理的原始业务类)。
// 1. 定义接口
public interface SmsService {String send(String message);
}// 2. 目标类,实现接口
public class SmsServiceImpl implements SmsService {@Overridepublic String send(String message) {System.out.println("【核心业务】: 正在发送短信: " + message);return "短信发送成功";}
}
2. 编写处理器 (InvocationHandler)
你需要创建一个实现了 InvocationHandler
接口的类。这个类是代理的灵魂,所有对代理对象方法的调用最终都会被转发到它的 invoke
方法里。你所有的增强逻辑(比如日志、计时、权限检查)都写在这里。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;// 3. 编写处理器,这是所有增强逻辑的所在地
public class MyInvocationHandler implements InvocationHandler {// 持有对目标对象的引用private final Object target;public MyInvocationHandler(Object target) {this.target = target;}/*** 当你调用代理对象的任何方法时,这个 invoke 方法就会被执行。* @param proxy 动态生成的代理对象本身 (很少使用)* @param method 被调用的目标方法 (例如 send 方法)* @param args 调用目标方法时传入的参数* @return 目标方法的返回值*/@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// --- 前置增强 ---System.out.println("【前置通知】: 方法 " + method.getName() + " 即将执行...");// 通过反射调用原始目标对象的方法Object result = method.invoke(target, args);// --- 后置增强 ---System.out.println("【后置通知】: 方法 " + method.getName() + " 执行完毕。");return result; // 返回原始方法的执行结果}
}
3. 生成并使用代理对象
最后,使用 Proxy.newProxyInstance()
这个静态方法来动态地创建代理对象。
public class JdkProxyFactory {public static Object getProxy(Object target) {return Proxy.newProxyInstance(// 1. 目标对象的类加载器 (ClassLoader)// 用于加载动态生成的代理类。target.getClass().getClassLoader(),// 2. 目标对象实现的所有接口 (Class<?>[])// 代理对象会实现这些接口,告诉代理它需要有哪些方法。target.getClass().getInterfaces(),// 3. 我们编写的处理器 (InvocationHandler)// 将方法调用分派到这里。new MyInvocationHandler(target));}
}// --- 客户端使用 ---
public class Main {public static void main(String[] args) {// 创建目标对象SmsService target = new SmsServiceImpl();// 获取代理对象SmsService proxy = (SmsService) JdkProxyFactory.getProxy(target);// 通过代理对象调用方法String response = proxy.send("Hello, JDK Proxy!");System.out.println("客户端收到响应: " + response);// 代理对象的真实类型是什么?System.out.println("代理对象的类型: " + proxy.getClass().getName());}
}
运行结果:
【前置通知】: 方法 send 即将执行...
【核心业务】: 正在发送短信: Hello, JDK Proxy!
【后置通知】: 方法 send 执行完毕。
客户端收到响应: 短信发送成功
代理对象的类型: com.sun.proxy.$Proxy0
从结果可以看出,我们调用 proxy.send()
时,MyInvocationHandler
中的前后置逻辑被成功执行了。并且代理对象的类型是一个由 JVM 在运行时动态生成的、名字类似 $Proxy0
的类。
限制与缺点
JDK 动态代理虽然强大,但它有一个非常重要的核心限制:
-
必须基于接口进行代理 (Most Important Limitation)
- 它只能代理实现了接口的类。 这是因为
Proxy.newProxyInstance()
的第二个参数要求传入一个接口数组。生成的代理类会实现这些接口,但它不会继承你的目标类。 - 因此,如果一个类没有实现任何接口(一个普通的 POJO),JDK 动态代理就无法为它创建代理。这是它最大的局限性,也是为什么 Spring AOP 需要引入 CGLIB 作为补充的原因。
- 它只能代理实现了接口的类。 这是因为
-
性能开销
- 由于其底层是基于 Java 反射 (Reflection) 机制(即
method.invoke(...)
),相比直接调用,会存在一定的性能开销。不过,在现代的 JVM 中,反射的性能已经得到了极大的优化,对于绝大多数业务场景来说,这点开销可以忽略不计。
- 由于其底层是基于 Java 反射 (Reflection) 机制(即
-
类型转换问题
- 代理对象
proxy
的类型是com.sun.proxy.$Proxy0
,它只实现了SmsService
接口。你不能将它强制类型转换为SmsServiceImpl
。 proxy instanceof SmsService
会返回true
。proxy instanceof SmsServiceImpl
会返回false
。- 这在某些需要具体实现类类型的场景下可能会导致
ClassCastException
。
- 代理对象
总结
特性 | 描述 |
---|---|
核心技术 | Java 反射 (java.lang.reflect.Proxy , InvocationHandler ) |
代理方式 | 在运行时动态地实现目标对象的所有接口,生成代理类。 |
最大限制 | 目标对象必须实现接口。无法代理没有接口的普通类。 |
优点 | Java 原生支持,无需引入任何第三方库。 |
缺点 | 必须有接口;存在一定的反射性能开销;无法强转为具体实现类。 |