【JavaEE】Spring AOP(二)

前情提要:前文书我们说到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;
    }
}

这里有三个非常关键的参数:

  1. proxy:代理对象本身
  2. method:被调用的方法
  3. 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

它有几个天然限制:

  1. final类不能代理
  2. final方法不能增强
  3. 构造方法无法增强

原因很好理解,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动态代理呢?

  1. JDK动态代理更符合面向接口编程的理念
  2. JDK动态代理更"轻",侵入性更低(基于Java标准库,不修改目标类本身)
  3. JDK动态代理的限制,在Spring场景下并不致命(绝大多数Bean本就有接口)

AOP暂时就介绍到这里啦~接下来登场的是Spring 事务!

相关推荐
岁岁种桃花儿2 小时前
Spring Boot项目核心配置:parent父项目详解(附实操指南)
java·spring boot·spring
YYHPLA2 小时前
【无标题】
java·spring boot·后端·缓存
木易 士心2 小时前
加密与编码算法全解:从原理到精通(Java & JS 实战版)
java·javascript·算法
专注于大数据技术栈2 小时前
java学习--ArrayList
java·学习
编程大师哥2 小时前
JavaEE初阶的核心组件
java·java-ee
华如锦2 小时前
MongoDB作为小型 AI智能化系统的数据库
java·前端·人工智能·算法
q***44152 小时前
C++跨平台开发挑战的技术文章大纲编译器与工具链差异
java·后端
stillaliveQEJ3 小时前
【javaEE】Spring AOP(一)
java·spring·java-ee
麦兜*3 小时前
SpringBoot进阶:深入理解SpringBoot自动配置原理与源码解析
java·spring boot·spring·spring cloud