一行代理告别反射:Java 动态代理新选择 APS
写 Java 久了,几乎人人都碰过这类需求:给某个方法加日志、做鉴权、统计耗时、织入事务切面。这时候「动态代理」是最顺手的工具,但 JDK 生态里一直摆着一个尴尬的两难:
java.lang.reflect.Proxy只能代理接口。你的业务对象一旦是具体 class,要么先抽接口,要么放弃。- CGLib 能代理类,但基于
FastClass/MethodProxy那一套,性能一般,还有 ClassLoader 泄漏的历史包袱。
更关键的是,无论选哪边,拦截器里想调用「原来的方法」都绕不开反射:
java
// JDK 动态代理:Method.invoke 是反射调用
Object result = method.invoke(target, args);
反射慢在哪?Method.invoke 每次调用都要做访问检查、参数装箱,而且它是一个 JVM 无法内联的边界------这意味着本来能被 JIT 优化成几条机器指令的热点方法,一旦套上代理就变成了一次反射派发。
APS(Accelerated Proxy Solution) 想解决的就是这件事:它在运行时用 ASM 直接生成代理类的字节码 ,拦截器里调用原方法用的是一条 INVOKESPECIAL 指令直接调用父类方法------没有反射、没有 MethodHandle、能被 JIT 完全内联。
直接说结果:类代理比 CGLib 快约 3--5 倍 ,接口代理和 java.lang.reflect.Proxy 打平、default 方法快约 6 倍。
一个 API,类和接口通吃
APS 只有一个入口 AcceleratedProxy.proxy(...),无论目标是类还是接口:
java
// 类代理:拦截 Greeter 的每个方法
Greeter proxy = AcceleratedProxy.proxy(Greeter.class,
(obj, method, args) -> {
System.out.println("before " + method.getName());
Object result = AcceleratedProxy.invokeSuper(obj, method, args);
System.out.println("after " + method.getName());
return result;
});
// 接口代理:Calculator 连实现类都没有,运行时直接给行为
Calculator calc = AcceleratedProxy.proxy(Calculator.class,
(obj, method, args) -> (int) args[0] + (int) args[1]);
注意两处细节,它们是 APS 设计里最关键的约定:
Interceptor是一个函数式接口 ,签名只有一个方法Object intercept(Object proxy, Method method, Object[] args),所以可以直接用 lambda。- 调用原方法用
AcceleratedProxy.invokeSuper(proxy, method, args),而不是method.invoke(...)。这一行在生成的字节码里就是一条直接调用父类方法的指令,是整个性能优势的来源。
完整示例
下面这段代码完整可跑(源码在 examples/src/main/java/io/github/lamspace/blog/ch01/):
java
public class Greeter {
public String hello(String name) { return "Hello, " + name; }
public String getGreeting() { return "Hi there"; }
}
java
public interface Calculator {
int add(int a, int b);
}
java
import io.github.lamspace.AcceleratedProxy;
public class Main {
public static void main(String[] args) {
// 1. 类代理:拦截 Greeter 的每个方法
Greeter greeter = AcceleratedProxy.proxy(Greeter.class,
(proxy, method, mArgs) -> {
System.out.println("before " + method.getName());
Object result = AcceleratedProxy.invokeSuper(proxy, method, mArgs);
System.out.println("after " + method.getName());
return result;
});
System.out.println(greeter.hello("World"));
// 2. 接口代理:Calculator 没有实现类,运行时给行为
Calculator calc = AcceleratedProxy.proxy(Calculator.class,
(proxy, method, mArgs) -> {
System.out.println("calling " + method.getName());
return (int) mArgs[0] + (int) mArgs[1];
});
System.out.println(calc.add(10, 20));
}
}
运行输出:
text
before hello
after hello
Hello, World
---
calling add
30
APS 到底提供了什么
除了「快」,APS 还补齐了一堆原生方案没有的能力,这些是它区别于 CGLib / JDK Proxy 的地方:
| 能力 | APS | CGLib | JDK Proxy |
|---|---|---|---|
| 代理具体类 | ✅ | ✅ | ❌ |
| 代理接口 | ✅ | ✅ | ✅ |
| 调用原方法的机制 | 直接 INVOKESPECIAL |
MethodProxy + FastClass |
Method.invoke 反射 |
| GC 安全(隐藏类) | ✅ | ❌ | ✅ |
| 选择性拦截(方法分组) | ✅ Group |
✅ CallbackFilter |
❌ |
| 多接口代理 | ✅ | ❌ | ✅ |
| 构造器拦截 | ✅ | ❌ | ❌ |
| 静态方法代理 | ✅ | ❌ | ❌ |
| 热重载 / 热替换 | ✅ | ❌ | ❌ |
| 注解驱动 API | ✅ | ❌ | ❌ |
这张表里每一行,本系列后续都会拆开讲。
环境与安装
APS 要求 Java 25+ ,编译期依赖 ASM 9.7.1。目前还没上 Maven Central,先 git clone 下来 mvn install 到本地仓库即可:
bash
git clone https://github.com/LamSpace/aps.git
cd aps
mvn install -DskipTests
小结与预告
这一篇只做了一件事:让你用几行代码跑起来第一个 APS 代理,并记住一个核心约定------用 invokeSuper 而不是反射。
下一篇我们看它的第一个杀手级特性:方法分组 Group ------如何只拦截你关心的方法,而让其余方法以零开销直通父类。
本文示例代码见 aps-blog/examples,框架源码与完整文档见 APS 仓库。