Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]最近这几天在搞 sofa4 升级 sofaboot 的事情,突然有个小伙伴找到了我,说遇到了一个非常奇怪的问题:一些 spring bean +

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

最近这几天在搞 sofa4 升级 sofaboot 的事情,突然有个小伙伴找到了我,说遇到了一个非常奇怪的问题:一些 spring bean + aop 的代码在 sofa4 中好好的跑着呢好几年都没动了,升级到 sofaboot 之后这段代码突然就跑挂了,具体问题就是一个 bean 的依赖注入全都变成 null 了。而且我仔细看了下代码也没发现啥问题,真是非常非常奇怪的一件事。

我先把这段伪代码放在这里,复制一下这段代码到 spring boot 项目中就能复现出来问题,看看有没有聪明的读者能发现问题所在。

问题代码

首先我们有一个负责业务逻辑的 Service,主要作用就是采集 BizTask 实现,遍历执行任务:

@Service
public class BizService {
    
    @Resource
    private List<BizTask> tasks;
    
    public void biz(String input) {
        for (BizTask task : tasks) {
            task.exec(input);
        }
    }
}

BizTask相关的代码也非常简单:

public interface BizTask {
    /**
     * task 业务执行入口抽象
     **/
    void exec(String input);
}

BizTask 有个抽象实现类,可以在任务前做一些准备或者检查工作:

public abstract class AbstractTask implements BizTask {
    
    @Resource
    protected PrepareService prepareService;


    @Override
    public final void exec(String input) {
        // 资源准备
        prepare();


        // 执行任务
        doExecute(input);        
    }


    protected abstract void doExecute(String input);


    private void prepare() {
        // 这里会发生问题,prepareService 无法注入,为 null
        prepareService.prepare();
    }
}

AbstractTask 有多个实现,都是继承 AbstractTask 然后实现 doExecute() 抽象方法的,我就不一一列举代码,这里只列举一个出了问题的实现:

@Component
public class Task2 extends AbstractTask {
    
    @Override
    @Profile // 加了性能采样的注解
    protected void doExecute(String input) {
        System.out.println("Task2 doExecute()...");
    }
}

这里解释一下,为了监测任务的执行效率,这里我们做了一个 @Profile 自定义注解(意为性能采样),并为它实现一个 Around 环绕类型的 AOP,里面代码简单点就是打个日志记录下时间:

@Slf4j
@Aspect
@Component
public class ProfilerAopAspect {


    @Around("@annotation(Profile)")
    public Object profiler(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return joinPoint.proceed();
        } finally {
            log.info("Method cost: {} millis.", (System.currentTimeMillis() - start));
        }
    }
}

好了,主要问题代码都列出来了,那么哪里会有问题呢?

将上面的代码复制到一个 spring boot 项目中可以复现出来问题。

问题出在 AbstractTask 的 prepare() 方法,在执行中会遇到 PrepareService 无法注入,报错 NPE。

那么我们的问题就来了:

  • 上面的代码到底哪里有问题啊,为什么PrepareService 无法注入还不报错?
  • 同样的代码为啥 sofa4 的时候好好的,改成 sofaboot 就不行了呢?

分析排查问题过程

首先通过 debug,我们可以看到注入的 bean Task2 不是普通的 Task2 的实例,而是一个叫做Task2$EnhancerBySpringCGLIB$bb4730a1 的一个实例,这里很容易理解,因为我们做了 @Profile AOP,所以 spring 为我们生成了一个代理,这个代理类在调用 doExecute() 的方法时会进入我们编写的切面。

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

既然我们的 bean 被 CGLIB 增强成了一个代理类,那我自然可以想到,我们先去除 AOP 对我们的影响看看,尝试把 Task2 的 @Profile注解去掉,让 Task2 变成一个非代理类,果然,无法注入的问题得到了解决。

看起来,无法注入应该是跟 AOP 或者 CGLIB 有关系。难道说被 AOP 或者 CGLIB 代理会导致无法注入依赖?

另外我们都知道,AOP 底层是动态代理技术,而 Java 中的创建动态代理有两种实现方式:

  • 基于 JDK 的动态代理,要求你的类要实现一个接口
  • 基于 CGLIB 的动态代理,不要求你的类要实现一个接口

我们的 Task2 是实现了 BizTask 接口的,符合使用 JDK 创建动态代理的条件,那么接下来我们试试看将 CGLIB 的实现换成 JDK 实现试试看。

很简单,只需要在 application.properties 中增加一行配置:

spring.aop.proxy-target-class=false

这样可以让 spring aop 默认使用 JDK 创建动态代理。

增加配置后重启,果然问题也消失了。JDK 创建的动态代理,debug 起来长这样:

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

以 $Proxy 开头。

接着,我们去确认在 sofa4 项目中,是不是默认用的是 JDK 创建的动态代理,果然,debug 起来就是 $Proxy 的样子,确认是 JDK 的动态代理。

至此,我们的问题变成了如下:

  • 上面的代码为什么使用 CGLIB 创建动态代理会有无法注入的问题,而 JDK 就不存在问题?
  • 什么时候 spring(sofa4)的 AOP 默认实现由 JDK 换成了 spring boot (sofaboot)中的 CGLIB

别急,我们一个一个问题来。

为什么无法注入

为什么 CGLIB 创建动态代理会有无法注入的问题?

为了搞清楚这个问题,我们就需要 debug 看下 CGLIB 给我们创建出来的动态代理类长什么样子。

CGLIB 采用了直接操作自节码的技术,动态生成一个被代理类的子类,动态生成的这个类型我们是没有源码的,因此我们就需要一种将 jvm 中正在运行的 class dump 出来,并且反编译出来源码的方式来进行分析代码。类似的工具有很多,HSDB,Arthas 都可以,Arthas 自带 dump + CFR 反编译,我们用起来更方便一些,这里就用 Arthas 好了。

我们启动 Arthas attach 上我们的应用之后,可以先通过 sc 命令搜一下,不然你不知道被 CGLIB 动态生成的那个类叫啥名字。

sc com.alipay.web.task.Task2*

通过这个命令,输出如下:

[arthas@25826]$ sc com.alipay.web.task.Task2*
com.alipay.web.task.Task2
com.alipay.web.task.Task2$EnhancerBySpringCGLIB$585accf5
Affect(row-cnt:2) cost in 10 ms.

通过输出,我们知道了com.alipay.web.task.Task2$EnhancerBySpringCGLIB$585accf5这就是我们要看的目标类。

使用如下命令:

jad com.alipay.web.task.Task2$EnhancerBySpringCGLIB$585accf5

输出了 CGLIB 代理的类

public class Task2$EnhancerBySpringCGLIB$585accf5 extends Task2 implements SpringProxy, Advised, Factory {
    // ...
  
    @Override
    protected final void doExecute(String string) {
        MethodInterceptor methodInterceptor = this.CGLIB$CALLBACK_0;
        if (methodInterceptor == null) {
            Task2$EnhancerBySpringCGLIB$585accf5.CGLIB$BIND_CALLBACKS((Object)this);
            methodInterceptor = this.CGLIB$CALLBACK_0;
        }
        if (methodInterceptor != null) {
            Object object = methodInterceptor.intercept((Object)this, CGLIB$doExecute$0$Method, new Object[]{string}, CGLIB$doExecute$0$Proxy);
            return;
        }
        super.doExecute(string);
    }


    public final String taskName() {
        // 同样也生成了代理方法略
    }


    // ...
}

我们可以看到 Task2 中的所有 public 方法都生成了代理方法,调用被代理方法前,会先调用 MethodInterceptor 中的逻辑,但除了 AbstractTask 中的 exec !

为啥 AbstractTask 的 exec 没有产生代理方法?因为用了 final 修饰。

接下来我们尝试去除 AbstractTask 的 exec 上的 final 修饰,看看我们的问题能不能被修复。

果然,去掉 final 后通过 debug,我们进入了 MethodInterceptor 代理逻辑,这段代理逻辑位于org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor#intercept()(不同的 AOP 增强可能有不同),但有一段核心逻辑是在 MethodInterceptor 是可以拿到一个所谓的 target,这个 target 才是被代理的那个对象,这个 target 就是我们的 Task2 实现,可以看到它内部的 prepareService 是有注入的。

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

前面的 exec 方法由于增加了 final 修饰,导致无法进入 MethodInterceptor,因为 MethodInterceptor 内部才有真正被代理的 target,所以 exec 方法成了一个普通方法,也自然没有注入,所以产生了 NPE。

至于为什么使用 JDK 的动态代理没有这个问题,因为 JDK 的动态代理就不是靠继承的方式创建一个代理类的,所以 final 修饰自然也不会影响代理。这里也用 Arthas 看一下 JDK 生成的代理类你就明白了:

同样,用 sc 命令搜一下,我们搜到了 com.sun.proxy.$Proxy78,同样我们用 jad 看下源码:

public final class $Proxy78 extends Proxy implements BizTask, SpringProxy, Advised, DecoratingProxy {


    public final String taskName() {
       // 同样代理
    }


    public $Proxy78(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }


    static {
        // ...
    }


    // ... 


    public final void exec(String string) {
        try {
            this.h.invoke(this, m4, new Object[]{string});
            return;
        } catch (Error | RuntimeException throwable) {
            throw throwable;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

可以看到,exec 交给了 InvocationHandler,InvocationHandler 创建的过程是要求传入一个被代理对象 target 的,所以 JDK 的代理也没问题。

至于为什么 Spring 生成的代理对象中包含的被代理对象 target 中是做了依赖注入的,想了解这部分细节的话可以从

AbstractAutoProxyCreator#postProcessAfterInitialization 的相关代码可以找到答案:

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

从上图可以清楚地看到,经过 AnnotationAwareAspectJAutoProxyCreator ( 实现了 BeanPostProcessor 接口)处理后,task2 变成了 cglib 的增强实现。而 task2 在之前的 bean 生命周期中已经完成了依赖注入。

Spring Boot 中的 AOP 到底是 JDK 动态代理还是 Cglib 动态代理?

至于为啥默认用 CGLIB 替代 JDK 的动态代理,我猜这种方式的性能可能更好一些,网上有一些测试分析,具体我也没有实际测过就不下结论了。

结论

CGLIB 通过创建一个类继承代理类的方式,来创建动态代理。

final 方法无法被继承,自然也无法被重写。

Spring Boot 2.x 版本后,改用 CGLIB 来做默认的动态代理实现工具。

至此,这些小知识点,你都 get 了吗?


为帮助开发者们提升面试技能、有机会入职BATJ等大厂公司,特别制作了这个专辑——这一次整体放出。

大致内容包括了: Java 集合、JVM、多线程、并发编程、设计模式、Spring全家桶、Java、MyBatis、ZooKeeper、Dubbo、Elasticsearch、Memcached、MongoDB、Redis、MySQL、RabbitMQ、Kafka、Linux、Netty、Tomcat等大厂面试题等、等技术栈!

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

欢迎大家关注公众号【Java烂猪皮】,回复【666】,获取以上最新Java后端架构VIP学习资料以及视频学习教程,然后一起学习,一文在手,面试我有。

每一个专栏都是大家非常关心,和非常有价值的话题,如果我的文章对你有所帮助,还请帮忙点赞、好评、转发一下,你的支持会激励我输出更高质量的文章,非常感谢!

Spring Boot AOP 的这个坑你踩了吗?[通俗易懂]

作者:刘顺(萧易)

出处:Spring Boot AOP 的这个坑你踩了吗? (qq.com)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/12016.html

(0)

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注