Spring AOP源码分析-代理方式的选择

baojunzh
发布于 2022-11-18 10:58
浏览
0收藏

在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。


AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区


Spring AOP的引入

接口调用耗时

现在我们有个接口要在日志中记录接口耗时,我们会怎么做呢?一般我们会在接口开始和接口结束时获取系统时间,然后二者一减就是接口耗时时间了。如下,在20行我们打印出接口耗时。

@RestController
@Slf4j
public class LoginController {
    @Autowired
    LoginService loginService;

    @RequestMapping("/login/{id}")
    public Map<String,Object> login(@PathVariable("id") Integer id){
        long start = System.currentTimeMillis();
        Map<String,Object> result = new HashMap<>();

        result.put("status","0");
        result.put("msg" , "失败");
        if (loginService.login(id)) {
            result.put("status","1");
            result.put("msg" , "成功");
        }

        long end = System.currentTimeMillis();
        log.info("耗时=>{}ms",end-start);
        return result;
    }
}

启动类:

@SpringBootApplication
public class SpringaopSbApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringaopSbApplication.class, args);
    }
}

但是,如果所有接口都要记录耗时时间呢?我们还按这种方式吗?显然不行,这种要在每个接口都加上同样的代码,而且如果后期你老板说去掉的话,你还有一个个的删掉么?简直是不可想象。。

所以对于这种需求,其实是可以提炼出来的。我们想,统计接口的耗时时间,无非就是在接口的执行前后记录一下时然后相减打印出来即可,然后在这样的地方去加入我们提炼出来的公共的代码。这就好比在原来的业务代码的基础上,把原来的代码横切开来,在需要的地方加入公共的代码,对原来的业务代码起到功能增强的作用。

这就是AOP的作用。

Spring AOP应用场景-接口耗时记录

下面我们来看看使用Spring AOP怎么满足这个需求。

首先定义一个​​切面类​​​TimeMoitor,其中pointCut()方法(修饰一组连接点)是一个​​切点​​​,@Pointcut定义了一组​​连接点​​​(使用表达式匹配)
aroundTimeCounter()是要加入的功能,被@Around注解修饰,是一个​​​环绕通知​​(Spring AOP通知的一种),其实就是上面说的在方法执行前后记录时间然后相减再打印出来耗时时间。

@Aspect
@Component
@Slf4j
public class TimeMoitor {
    @Pointcut(value = "execution(* com.walking.springaopsb.controller.*.*(..))")
    public void pointCut(){}
    
    @Around(value = "com.walking.springaopsb.aop.TimeMoitor.pointCut()")
    public Object aroundTimeCounter(ProceedingJoinPoint jpx){
        long start = System.currentTimeMillis();
        Object proceed = null;
        try {
             proceed = jpx.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        long end = System.currentTimeMillis();
        log.info("耗时=>{}ms",end-start);
        return proceed;
    }
}

然后在LoginController#login方法里我们就可以把日志打印耗时时间的代码删掉了。

@RestController
@Slf4j
public class LoginController {
    @Autowired
    LoginService loginService;

    @RequestMapping("/login/{id}")
    public Map<String,Object> login(@PathVariable("id") Integer id){
        Map<String,Object> result = new HashMap<>();
        result.put("status","0");
        result.put("msg" , "失败");
        if (loginService.login(id)) {
            result.put("status","1");
            result.put("msg" , "成功");
        }
        return result;
    }
}

再比如,LoginController里若是还有别的方法,也一样可以应用到。
使用Spring AOP的控制台日志:

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

Spring AOP源码分析

Spring AOP的原理

以上就是Spring AOP的一个应用场景。那Spring AOP的原理是什么呢,用的什么技术呢?
其实就是反射+动态代理。代理用的就是JDK动态代理或cglib,​​​那么Spring AOP什么时候用JDK动态代理什么时候用cglib?​​​​默认使用哪种?​

因为Spring的源码非常的复杂,方法调用栈很深,很多同学都无从下手,debug进去之后就出不来了,会非常的乱。所以下面我们就针对上述问题来根据源码探究一下吧

首先我们将启动类改一下,方便我们对源码debug。

启动类:

@ComponentScan("com.walking.springaopsb.*")
@EnableAspectJAutoProxy
public class SpringaopSbApplication {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringaopSbApplication.class);
        LoginController loginController = (LoginController) applicationContext.getBean("loginController");
        loginController.login(123);
    }
}

我们修改了一下启动类,把断点打在第6行,启动,往下走一步,看loginController这个变量。

我们发现是cglib方式产生的代理类,说明从IoC容器里拿到的是代理类,到底是初始化IoC容器时生成的还是getBean获取时产生的呢?我们也跟随源码来看一下吧。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

要知道的是,我们现在要看的是第5行还是第6行生成的代理类。因为如果直接看第5行里面的方法调用比较多,不知道到底在哪个方法是初始化实例,所以我们从getBean入手,先看第6 行的getBean吧,进入这个方法

​org.springframework.context.support.AbstractApplicationContext#getBean(java.lang.String)​

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区


然后我们只看有return的地方,在进入这个

​getBean(org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String))​​。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

再看

​doGetBean(org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean)​

第120行sharedInstance已经变成了代理类

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

所以我们进入

​org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)​​​方法看看,重新运行,然后再加个断点,打到​​org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)​​里。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

走过88行后,singletonObject变成了代理类,所以关键点就是在

​this.singletonObjects.get(beanName);​

我们可以看到singletonObjects 是一个ConcurrentHashMap。原来IoC的实例在这个ConcurrentHashMap里。

private final Map<String, Object> singletonObjects = new ConcurrentHashMap(256);

所以到这里我们就可以知道,这个代理类不是在getBean的时候生成的,即不是在启动类的第6行生成的,那就是在第5行生成的,即在IoC容器初始化时产生的代理类。
刚才那个ConcurrentHashMap是get的,那就肯定有put的时候。搜一下,还在这个类里,发现一个addSingleton方法,有俩地方调用,一个是在

​org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#registerSingleton​

调用的,一个是在

​org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, org.springframework.beans.factory.ObjectFactory<?>)​

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

那就把断点打到这俩方法里,看会走到哪个,把别的断点都去掉,当然了,因为spring还有别的自己的实例要获取,IoC容器里还有spring自己的实例,所以这个断点要加上条件,当beanName是loginController时进去断点,这样就方便多了。我们只保留第5行的代码,因为getBean里面也会调getSingleton。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

运行启动类,发现进入了getSingleton方法,但​​Object singletonObject = this.singletonObjects.get(beanName);​​返回的为null,所以继续往下走。发现在第127行返回了代理类,看这行的getObject方法又不知道是那个实现类,所以我们去左下角看方法栈,找一下这个方法的上一个方法,

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

就是上图左下角的第二个方法doGetBean,发现传的是一个匿名内部类,这个匿名内部类里调的是

​org.springframework.beans.factory.support.AbstractBeanFactory#createBean​

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

所以我们把断点走完,进到这个createBean里打断点,同样加条件。

断点走过324行时变成代理类,即进入

​org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean​​看看,打个断点同样加条件

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

断点走过doCreateBean方法第380行后产生了代理类,所以把断点打到这个​​org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#initializeBean(java.lang.String, java.lang.Object, org.springframework.beans.factory.support.RootBeanDefinition)​​方法里,同样加上条件,把别的断点去掉,重新运行。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

当走过1240行时已经变成了代理类,所以把断点打到这个

​org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization​​方法,同样加上条件,把别的断点去掉,重新运行。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

我们发现,这里有个循环,迭代的是this.getBeanPostProcessors()的结果,我们看看这个是什么,是List,下图是这个list的数据

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

经过几次debug发现当BeanPostProcessor为第四个元素时​​AnnotationAwareAspectJAutoProxyCreator​​​的实例,result变成了代理类。关键就是在​​processor.postProcessAfterInitialization()​​这个方法,把断点打进去。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

发现没有​​AnnotationAwareAspectJAutoProxyCreator​​这个实现类

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

那就看看这个​​AnnotationAwareAspectJAutoProxyCreator​​​的父类吧,​​Ctrl + Alt + Shift + U​​​查看​​AnnotationAwareAspectJAutoProxyCreator​​的类图依赖关系

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

发现AbstractAutoProxyCreator在上上个图中,并且

​AnnotationAwareAspectJAutoProxyCreator​​没有重写

​postProcessAfterInitialization​​方法,

所以我们就看​​AbstractAutoProxyCreator​​的这个方法。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

打断点时发现Object bean不是代理类,那就看看

​org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary​方法。在这个方法中调用了createProxy()创建代理类,进去看下。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

这个方法最后​​return proxyFactory.getProxy(getProxyClassLoader());​​进入getProxy方法看看

所以​​createAopProxy()​​​方法返回AopProxy类型的实例,有俩实现类可供创建​​CglibAopProxy ​​​和 ​​JdkDynamicAopProxy​​,及cglib和jdk动态代理两种

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

那么究竟创建哪一种,就是我们今天要看的关键之处,所以我们进入createAopProxy()方法看看。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

再进去

​org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy​方法看看。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区


​config.isOptimize()​​​和​​config.isProxyTargetClass()​​​都默认false
这里创建​​​logincontroller​​​时​​config​​的数据如下

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

然后判断targetClass是否为接口,这里我们的LoginController不是接口,就走了下面的return

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

所以Spring AOP使用JDK动态代理还是cglib取决于是否是接口,并没有默认的方式。

我们改一下LoginController让其实现接口

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

debug启动,这时得到的代理类就是JDK动态代理。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

为什么JDK动态代理必须是接口?

我们看一下这个问题,首先把LoginController改为实现ILoginBaseController接口,然后根据咱们上面的debug分析,在

​org.springframework.aop.framework.ProxyFactory#getProxy(java.lang.ClassLoader)​方法里createAopProxy().getProxy就是我们解决这个问题的入口,我们在getProxy里打上断点,

​JdkDynamicAopProxy#getProxy(java.lang.ClassLoader)​方法里断点加到return语句上

return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);

然后在Proxy.newProxyInstance进来加断点,一步步往下走,在719行是关键

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

进去

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

进入proxyClassCache.get方法

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

然后第120行时关键,我们看这个apply方法是BiFunction接口的方法,有如下实现类,把鼠标放到subKeyFactory上去发现是KeyFactory类型的,进debug去看,没有我们想要的

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

然后继续往下走,有个while循环,经过几次debug,发现这个循环是关键,具体看图中标注

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

我们需要进这个get

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

进来get之后发现有一行关键点,就是下图的230行,还是有个apply方法

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

刚才也说过了他有如下实现类

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

通过看valueFactory的类型知道他是ProxyClassFactory类型的,然后进入这个类。他是Proxy类的一个静态内部类。

经过多次debug发现639-643行是关键,其中第639行是获取字节码,然后第642行调用defineClass0(一个native方法创建实例。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区


这里加个小插曲,为什么java的动态代理生成的代理类前面有个$Proxy呢,在这里可以得到答案。

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区


回到刚才,字节码我们看不懂,但是可以反编译我们把639行拿出来写个测试类

public class Test {
    public static void main(String[] args) throws Exception {
        //获取ILoginBaseController的字节码
        byte[] bytes = ProxyGenerator.generateProxyClass(
          "$Proxy#MyLoginController", 
          new Class[]{ILoginBaseController.class});
        //输出到MyLoginController.class文件
        FileOutputStream fileOutputStream = new FileOutputStream(new File("MyLoginController.class"));
        fileOutputStream.write(bytes);
        fileOutputStream.flush();
        fileOutputStream.close();
    }
}

我们会看到生成了指定的文件

Spring AOP源码分析-代理方式的选择-鸿蒙开发者社区


看到这个文件你是不是就明白为啥JDK动态代理只能是接口了吗?原因就是java中是单继承多实现,$Proxy#MyLoginController类已经继承了Proxy类,所以不能在继承别的类了只能实现接口,所以JDK动态代理只能是接口。

总结

通过以上的源码分析我们弄清楚了,Spring AOP使用的代理机制了,并且是没有默认的代理,不是JDK动态代理就是cglib,以及为啥java的动态代理只能是接口。并且我们还看了一下spring的源码,虽然看的不是非常的仔细,但是通过这样看源码我们的理解更加的加深了,也锻炼了看源码的能力。


本文转载自公众号:biggerboy

标签
已于2022-11-18 10:59:25修改
1
收藏
回复
举报
1条回复
按时间正序
/
按时间倒序
wx638ef01a1e889
wx638ef01a1e889

调试发现实现接口之后还是走的CGLIB动态代理,为啥?

回复
2023-1-6 15:50:30
回复
    相关推荐