开头
关于 AOP 的概念和使用请看我的上一篇博客:Spring AOP:从 注解配置 到 通知执行顺序 详解
关于 Aspect 切面
在 AOP 的使用中,Aspect 切面由 Pointcut 和 Advice 组成,但是没有写到如何进行切入的,这就是本篇博客所介绍的 " Proxy " ,即 代理 。
Proxy 代理
首先,当在 bean 的生命周期中,spring在给 bean 执行初始化后置,即方法postProcessAfterInitialization()的时候(一般情况下是这样,如果是出现了bean循环依赖,会在实例化结束后即调用了createBeanInstance()反射方法之后依赖注入之前进行生成 Proxy),去进行 AOP 的生成。
因为初始化后置方法是每生成一个 Bean 就要执行一次这个方法,正好对应了 AOP 的需求,即为每一个需要 AOP 的 Bean 进行 面向切面编程 。
而就是这个时候 AOP 就会生成 Bean 的代理对象 Proxy 。
关于Proxy的实现方式
这里运用到了一个技术叫做 "动态代理" 。而对于动态代理的实现方式有两种,分别为 JDK 代理 和 CGLIB 代理 。
为了方便理解,这里先用 "静态代理" 做演示。
1. 静态代理
定义接口
- 首先先写一个接口。
java
// 定义接口类 UserService
public interface UserService {
void saveUser(String username);
}
接口实现
- 再写一个实现类去实现此接口。
java
// 接口实现类 UserServiceImpl
public class UserServiceImpl implements UserService {
@Override
public void saveUser(String username) {
System.out.println("【真实业务】正在保存用户: " + username);
}
}
静态代理类
- 这里是静态代理的重点,静态代理类创建。
java
// 静态代理类 UserServiceStaticProxy
public class UserServiceStaticProxy implements UserService {
private UserService target;
// 拿到目标对象
public UserServiceStaticProxy(UserService target) {
this.target = target;
}
// 运用重写的操作进行业务增强
@Override
public void saveUser(String username) {
// 前置增强
System.out.println("静态代理-前置操作...");
// 调用目标方法
target.saveUser(username);
// 后置增强
System.out.println("静态代理-后置操作...");
}
}
实际使用
- 使用静态代理进行工作
java
// 实际的使用
public class StaticProxyTest {
public static void main(String[] args) {
// 创建一个实现类为目标类 target
UserService target = new UserServiceImpl();
// 创建一个代理对象 proxy 进行对 target 的代理
UserService proxy = new UserServiceStaticProxy(target);
// 执行代理的方法(这是重写增强后的方法)
proxy.saveUser("张三");
}
}
2. 动态代理 之 JDK代理
定义接口
- 同上写一个接口。
java
// 定义接口类 UserService
public interface UserService {
void saveUser(String username);
}
接口实现
- 也是写一个实现类去实现此接口。
java
// 接口实现类 UserServiceImpl
public class UserServiceImpl implements UserService {
@Override
public void saveUser(String username) {
System.out.println("真实业务..." + username);
}
}
动态代理(重点)
java
// 实现动态代理
public class JdkProxyHandler implements InvocationHandler {
private Object target;
public Object bind(Object target) {
this.target = target;
return Proxy.newProxyInstance(
target.getClass().getClassLoader(),
target.getClass().getInterfaces(),
this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 前置增强
System.out.println("JDK代理-前置方法调用: " + method.getName());
// 核心:通过反射调用目标方法
Object result = method.invoke(target, args);
// 后置增强
System.out.println("JDK代理-后置方法调用");
// 返回原方法的执行结果
return result;
}
}
- 首先对于方法
invoke(),这是一种特殊的方法:每当调用代理 proxy 的时候,proxy 会自动执行invoke()方法 ,而这也是所谓proxy代理的核心作用。
invoke()的参数proxy指的是本身 ,method指的是原对象的方法 ,arg表是以列表的形式存放的原对象的方法所需的参数。 - 对于方法
Proxy.newProxyInstance:这里的 proxy 是一种 "伪造的实现类 " ,通过getClassLoader()和getInterfaces()将目标的 "类的加载" 和 "接口实现" 传给了 proxy 。并通过处理器new JdkProxyHandler(target)的处理从而使 proxy 成功 "伪装" 成了实现了对应接口的目标类,变成了原方法的 "替身" 。
实际使用
java
// 实际的使用
public class JdkProxyTest {
public static void main(String[] args) {
JdkProxyHandler jdkProxyHandler = new JdkProxyHandler();
// 用接口类去接收这个 proxy 代理类
UserService userService = (UserService)jdkProxyHandler.bind(new UserServiceImpl())
userService.saveUser("张三");
}
}
- 对于代码
UserService userService = (UserService)jdkProxyHandler.bind(new UserServiceImpl()),它将原实现类new UserServiceImpl()通过bind方法进行包装成一个proxy代理类,然后用接口类实例UserService userService接收。
问题来了: 那为什么不用原来的实现类接收,而是用接口类接收呢??
答 : proxy相当于这个接口的实现类,而原目标类也是这个接口的实现类。
虽然proxy是增强了目标类,但是和目标类本质上不一样* !所以相当于现在是有两个实现类,都实现了同一个接口,所以才用接口接收,以免报错。*
3. 动态代理 之 CGLIB代理
定义接口
- CGLIB 其实不强依赖接口。
java
// 不需要接口哦
普通类实现
- CGLIB 的强大之处在于,即使只有一个普通的类,没有接口,它也能进行代理。
java
// 普通类 UserServiceImpl (不需要 implements UserService 去实现接口)
public class UserServiceImpl {
public void saveUser(String username) {
System.out.println("真实业务...正在保存用户: " + username);
}
}
动态代理(重点)
java
// 实现动态代理
public class CglibProxyHandler implements MethodInterceptor {
// 创建代理对象的核心方法
public Object bind(Class<?> targetClass) {
// 1. 创建增强器(代理工厂)
Enhancer enhancer = new Enhancer();
// 2. 设置目标类(也就是父类)
enhancer.setSuperclass(targetClass);
// 3. 设置回调(也就是拦截器本身)
enhancer.setCallback(this);
// 4. 创建并返回代理对象
return enhancer.create();
}
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 前置增强
System.out.println("CGLIB代理-前置方法调用: " + method.getName());
// 通过 MethodProxy 调用父类(目标类)的方法
// 注意:这里使用 invokeSuper 而不是 method.invoke,效率更高
Object result = methodProxy.invokeSuper(proxy, args);
// 后置增强
System.out.println("CGLIB代理-后置方法调用");
return result;
}
}
- 关于
enhancer:这个所谓的 "增强器" 其实就是一个 "代理工厂 ",可以用enhancer.create()生成代理对象 proxy 。 - 关于
enhancer.setCallback()方法,参数为this。这个方法叫做 "回调 " ,是为了将下面的intercept增强的方法传递给代理工厂enhancer,等到需要生成代理对象 proxy 的时候,工厂会自动将这个intercept增强方法塞进代理对象 proxy 中。 - 对于方法
enhancer.create():这里的 proxy 是一种 "伪造的子类 " ,即将目标类作为父类,让代理类 proxy 去继承目标类 。CGLIB 会在内存中动态生成了一个 继承自目标类 的新类( 就是这个 proxy )。它通过重写父类的所有非final方法,并在方法中织入我们的增强逻辑,从而变成了原方法的"替身"。 - 对于方法
intercept(),这是 CGLIB 的拦截核心。每当调用代理对象的方法时,会执行此方法。proxy:指的是 CGLIB 生成的代理对象本身。method:指的是 目标类的方法。args:指的是 方法参数。methodProxy:这是 CGLIB 特有的参数,它是对目标方法的快速代理,比 JDK 的反射调用性能更好。
实际使用
java
// 实际的使用
public class CglibProxyTest {
public static void main(String[] args) {
CglibProxyHandler cglibProxyHandler = new CglibProxyHandler();
// 用原来的类去接收这个 proxy 代理类
UserServiceImpl userService = (UserServiceImpl) cglibProxyHandler.bind(UserServiceImpl.class);
userService.saveUser("张三");
}
}
- 对于代码
UserServiceImpl userService = (UserServiceImpl) cglibProxyHandler.bind(UserServiceImpl.class),它将原类UserServiceImpl.class通过bind方法包装成一个 proxy 代理类,然后用 原类实例UserServiceImpl userService接收。
问题来了:为什么 CGLIB 可以用原来的类接收,而 JDK 代理不行呢??
答 :因为 CGLIB 采用的是继承 的方式。
CGLIB 生成的代理类是目标类的子类,而目标类是父类 。
在 Java 中,子类对象是可以赋值给父类引用的(多态)。所以,代理对象(子类)可以直接被当作目标类(父类)来使用,自然就可以用原来的类去接收了。
关于动态代理使用
在 Spring 框架中,AOP 代理的创建是由 AopProxyFactory 决定的。
Spring 默认遵循以下策略:
-
如果目标对象实现了接口:默认使用 JDK 动态代理。
建议:在这种情况下,业务类应当实现接口,这是 Java 设计模式的最佳实践。
-
如果目标对象没有实现接口:必须使用 CGLIB。
-
强制使用 CGLIB:
即使目标对象实现了接口,你也可以通过配置强制 Spring 使用 CGLIB ,例如:
- 在 XML 中设置
<aop:config proxy-target-class="true"/> - 在注解配置中开启
proxyTargetClass=true。
- 在 XML 中设置
注意:使用 CGLIB 时,目标类不能是 final 的,且需要被代理的方法也不能是 final 或 private 的(因为子类无法重写这些方法)。
结语
本文结束
如果这篇文章帮你理清了思路,我也感到很开心。
这也同时是我的学习笔记,能帮到别人我不胜感谢,若哪里出错希望指出,同样不胜感激!
以上,@Aroaku