前情提要:前文书我们说到Spring AOP并不会修改原始Bean,而是通过代理对象,接管方法调用过程
那么问题来了,这个代理对象是怎么来的?是谁生成的?为什么它看起来像原始对象?
答案就俩字:代理
在Spring AOP中,承担这项工作的,有两种技术路线:
- JDK动态代理
- CGLib代理
这篇文章,我们就来详细介绍一下这两种机制以及他们之间的区别
一、为什么非得搞一个"代理对象"?
在聊具体技术之前,我们回到一个最淳朴的问题:Spring为什么不直接在方法里面插代码?
原因其实很简单:
- Java语言层面不支持在运行时修改已有方法的字节码
- Spring也不可能要求你在业务代码里手写日志、事务、权限控制
于是,只剩下了一条路:在对象外面包一层,所有调用都先经过这一层
这就是代理模式最经典的使用场景
二、代理到底代理什么?
代理并不是复制一个对象,也不是增强一个对象,它代理的只有一件事:方法调用
也就是说,代理对象存在的唯一目的,就是拦着这一步:
java
userService.save();
然后决定:这次方法要不要执行?执行前做点什么?执行后做点什么?出异常怎么办?
至于save这个方法本身,Spring从头到尾都没动过
知道了什么是代理,代理什么,就来到了我们的重头戏:两种代理方式
三、JDK动态代理
我们先从JDK动态代理说起
上面的save方法我希望最终执行的结果是
java
打印日志
执行save()
打印日志
3.1 前提:必须有接口
JDK动态代理的核心特点只有一句话:只能代理接口
也就是说,它生成的代理对象,必须是某个接口的实现类
就像这样:
java
// 定义一个UserService接口
public interface UserService {
void save();
}
// 定义一个类继承上面的接口
public class UserServiceImpl implements UserService {
public void save() {
System.out.println("执行业务逻辑:save user");
}
}
现在我们有了接口,有了接口的实现类,JDK就可以帮我们生成代理类
3.2 总开关:InvocationHandler
JDK动态代理的核心只有一个接口:InvocationHandler
源码附上~
java
// 这个接口中,真正和动态代理有关的只有invoke方法,其余default方法用于支持Java8的接口默认方法调用,与AOP增强本身无关,这里就不粘了
public interface InvocationHandler {
Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}
所有的方法调用都会被转发到这里,也就是这个invoke()方法
大致查看一下源码后,我们可以来自己动手实现一个
java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class TestInvocationHandler implements InvocationHandler {
private final Object target;
public TestInvocationHandler(Object target){
this.target = target;
}
// 重写invoke方法
@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;
}
}
这里有三个非常关键的参数:
- proxy:代理对象本身
- method:被调用的方法
- target:真正的业务对象
3.3 生成代理对象
java
public class JdkProxyDemo {
public static void main(String[] args) {
UserService target = new UserServiceImpl();
UserService proxty = (UserService) Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
new TestInvocationHandler(target)
);
proxty.save();
}
}
运行结果:

发生了什么?
我们来打断点看一下

首先初始化我们定义好的TestInvocationHandler

执行save()方法

方法调用被invoke()拦截住,并执行里面的逻辑

执行到save()的业务逻辑

invoke()中的所有逻辑执行完,返回

回到主函数,程序运行结束

如果你也打断点一行一行看下来会发现,最终的控制台输出了一堆方法调用,而不是只有save一个方法,就像这样,这是因为Debug模式下,IDEA为了展示对象信息(也就是图片中灰色的那部分)主动调用了这些toString()方法。算是一种调试的副作用?
总结一下,真实的调用链是:
java
proxy.save()
↓
InvocationHandler.invoke()
↓
method.invoke(target)
也就是说,JDK动态代理拦截了所有接口方法调用,并把所有方法统一转发到InvocationHandler
那么问题又又又来了,没有接口怎么办?
就比如这样一个类:
java
public class OrderService {
public void createOrder() {
System.out.println("创建订单");
}
}
很明显,这个类没有接口,此时JDK动态代理到此为止,直接GG。于是CGLIB登场!!!!
四、CGLIB动态代理
没有接口?那我直接继承你!!
CGLIB的核心思路就是:动态生成一个子类,重写父类方法,在方法里插入逻辑
4.1 拦截点:MethodInterceptor
类似的,我们来自己实现一个拦截类
java
public class LogMethodInterceptor implements MethodInterceptor {
@Override
public Object intercept(
Object obj,
Method method,
Object[] args,
MethodProxy proxy
) throws Throwable {
System.out.println("【前置日志】" + method.getName());
Object result = proxy.invokeSuper(obj, args);
System.out.println("【后置日志】" + method.getName());
return result;
}
}
4.2 创建代理对象
java
public class CglibProxyDemo {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
// 设置父类(代理的目标类)
enhancer.setSuperclass(OrderService.class);
// 设置方法拦截器
enhancer.setCallback(new TestMethodInterceptor());
// 创建代理对象
OrderService proxy = (OrderService) enhancer.create();
proxy.createOrder();
}
}
这里的Enhancer是CGLIB用来在运行期生成目标类子类的核心工具
程序运行一下,可以看到输出为:

同样,我们来打断点看一下

执行过setSuperclass后,enhancer中的superclass已经变成了OrderService

继续执行,指定了我们自定义的拦截器
运行到createOrder()方法

被拦截下来,跳转到了这里

运行intercept中的逻辑,可以看到控制台打印了对应信息,和JDK动态代理一样,由于debug出现了很多toString方法的调用

继续执行,调用了业务代码

回到拦截器中,继续后续代码

方法返回,回到主函数,程序运行结束

4.3 CGLIB的本质结构
java
OrderService$$EnhancerByCGLIB extends OrderService
它有几个天然限制:
- final类不能代理
- final方法不能增强
- 构造方法无法增强
原因很好理解,final修饰的类不能被继承,而CGLIB代理生成的代理对象本质上就是目标类的一个子类;final修饰的方法不允许被子类重写,CGLIB的增强恰好是通过重写方法实现的;构造方法在对象被创建时就已经调用了,而代理对象是构造方法执行完成之后才出现的
这三点其实限制的都是同一件事,那就是代理只能拦截"已经存在,可以被重写的方法调用"
五、JDK动态代理 vs CGLIB动态代理
| 对比点 | JDK 动态代理 | CGLIB |
|---|---|---|
| 是否需要接口 | 必须 | 不需要 |
| 实现方式 | 实现接口 | 继承类 |
| 方法拦截点 | InvocationHandler | MethodInterceptor |
| final 方法 | 无影响 | 无法增强 |
| Spring 默认 | ✅ | 兜底方案 |
六、Spring 如何自动选择的
我们在实际使用AOP时并没有手动指定使用JDK代理还是CGLIB代理,那么Spring是怎么选择使用哪种代理的呢?
其实很简单,如果Bean实现了接口,使用JDK动态代理;否则使用CGLIB
当然,你也可以强制指定:
java
@EnableAspectJAutoProxy(proxyTargetClass = true)
那么为什么Spring会优先使用JDK动态代理呢?
- JDK动态代理更符合面向接口编程的理念
- JDK动态代理更"轻",侵入性更低(基于Java标准库,不修改目标类本身)
- JDK动态代理的限制,在Spring场景下并不致命(绝大多数Bean本就有接口)
AOP暂时就介绍到这里啦~接下来登场的是Spring 事务!
