在Java语言中,动态代理是一种在运行期间动态创建代理类并拦截方法调用的技术。JDK自带的动态代理严格依赖接口,通常也被称为接口代理。它与手写代理类或使用第三方字节码工具不同,不需要在编译期生成.java或.class文件,而是由虚拟机在内存中直接构造字节码。

一、接口代理的基本前提
JDK动态代理要求被代理的对象必须实现一个或多个接口。代理对象本身也会实现这些接口,因此在调用方看来,代理对象和真实对象可以互换使用。如果目标类没有接口,那么只能选择CGLIB等基于继承的代理方案,而不能直接使用java.lang.reflect.Proxy。
这种设计带来一个明显限制:只能拦截接口中声明的方法,无法拦截类自己定义但接口没有的方法。理解这一点有助于避免在开发中误以为代理能捕获所有调用。从底层看,Proxy在运行期生成的类会实现给定接口,并把所有方法调用统一转发到同一个InvocationHandler。
二、定义接口与真实实现
使用接口代理的第一步是声明业务接口,并提供一个普通实现类作为被代理的真实对象。下面示例定义了一个简单的支付接口:
public interface PaymentService {
void pay(String orderId, double amount);
String query(String orderId);
}
public class RealPaymentService implements PaymentService {
@Override
public void pay(String orderId, double amount) {
System.out.println("真实支付: " + orderId + " 金额 " + amount);
}
@Override
public String query(String orderId) {
return "订单" + orderId + "已支付";
}
}
上面的代码没有任何代理相关逻辑,只是纯粹的业务实现。真实对象后续会交给处理器持有,代理对象在方法执行前后可以插入日志、权限校验等横切逻辑。
把真实实现与接口分离,是接口代理能够工作的基础。调用方依赖接口编程,不关心拿到的是RealPaymentService还是Proxy生成的代理实例,这也符合面向接口设计的原则。
三、编写InvocationHandler处理器
处理器是动态代理的核心,它实现InvocationHandler接口,并在invoke方法中统一接收代理对象的方法调用。通过Method参数可以拿到方法签名,通过args拿到参数数组。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class LogInvocationHandler implements InvocationHandler {
private Object target;
public LogInvocationHandler(Object target) {
this.target = target;
}
@Override
public 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;
}
}
在invoke方法中,我们先用method.getName输出方法名,再通过method.invoke把调用转给真实对象。这样就在不修改RealPaymentService源码的情况下,增加了日志能力。如果需要在特定方法上做特殊处理,可以用if判断method.getName的值。
需要注意的是,invoke里的proxy参数一般是当前代理实例,通常不要在invoke内部再通过proxy调用方法,否则会造成无限递归。绝大多数场景只需要使用target和method即可。
四、使用Proxy创建代理对象
完成接口、真实对象和处理器后,就可以用Proxy.newProxyInstance生成代理实例。该方法需要类加载器、接口数组以及处理器实例。
import java.lang.reflect.Proxy;
public class ProxyDemo {
public static void main(String[] args) {
PaymentService real = new RealPaymentService();
LogInvocationHandler handler = new LogInvocationHandler(real);
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
real.getClass().getClassLoader(),
new Class<?>[]{PaymentService.class},
handler
);
proxy.pay("A1001", 99.5);
System.out.println(proxy.query("A1001"));
}
}
上述代码运行后,控制台会先打印调用前日志,再执行真实支付逻辑,最后打印调用后日志。代理对象proxy的类型在编译期是PaymentService,但实际指向的是运行期生成的代理类。
由于代理类是在内存中动态拼装字节码,因此第一次创建某种接口的代理时会有一点性能开销。后续再创建同接口代理则相对较快。这也是接口代理适合长期复用、而不适合极端高频瞬时创建的原因。
五、接口代理的优缺点分析
接口代理最大的优点是标准化和零侵入。业务类不需要继承特定父类,也不需要加注解,只要实现接口即可被代理。它与Spring AOP等框架天然契合,便于做事务、日志、监控等统一处理。
缺点同样明显:必须基于接口,无法代理普通类;每次方法调用都要经过反射invoke,性能比直接调用略低;生成的代理类不能断点调试源码。在需要代理没有接口的老旧类时,就要考虑其他字节码增强方案。但在大多数分层架构中,服务层本就面向接口,因此JDK动态代理依然是最常用、最稳定的选择。
| 对比维度 | JDK接口代理 | 普通手写代理 |
|---|---|---|
| 是否需接口 | 必须 | 不必 |
| 代码生成时机 | 运行期 | 编译期 |
| 方法拦截方式 | InvocationHandler统一处理 | 每个方法单独重写 |
通过上面表格可以看出,接口代理把重复的横切代码收敛到处理器中,比手工为每个方法写代理更易于维护。掌握其基本语法,是理解Java代理生态和AOP原理的重要一步。