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

能坚持别人不能坚持的,才能拥有别人不曾拥有的。
关注编程大道公众号,让咱们一同坚持心中所想,一块儿成长!!java

 

年前写了一个面试突击系列的文章,目前只有redis相关的。在这个系列里,我整理了一些面试题与你们分享,帮助年后和我同样想要在金三银四准备跳槽的同窗。咱们一块儿巩固、突击面试官常问的一些面试题,加油!!web

《【面试突击】— Redis篇》--Redis数据类型?适用于哪些场景?
《【面试突击】— Redis篇》--Redis的线程模型了解吗?为啥单线程效率还这么高?
《【面试突击】— Redis篇》-- Redis的主从复制?哨兵机制?
《【面试突击】— Redis篇》-- Redis哨兵原理及持久化机制
《【面试突击】— Redis篇》--Redis Cluster及缓存使用和架构设计的常见问题
面试

什么是 AOP ?

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

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

Spring AOP面向切面编程

接口调用耗时

如今咱们有个接口要在日志中记录接口耗时,咱们会怎么作呢?通常咱们会在接口开始和接口结束时获取系统时间,而后两者一减就是接口耗时时间了。以下,在20行咱们打印出接口耗时。编程

 1@RestController
2@Slf4j
3public class LoginController {
4    @Autowired
5    LoginService loginService;
6    @RequestMapping("/login/{id}")
7    public Map<String,Object> login(@PathVariable("id") Integer id){
8        long start = System.currentTimeMillis();
9        Map<String,Object> result = new HashMap<>();
10        result.put("status","0");
11        result.put("msg" , "失败");
12        if (loginService.login(id)) {
13            result.put("status","1");
14            result.put("msg" , "成功");
15        }
16        long end = System.currentTimeMillis();
17        log.info("耗时=>{}ms",end-start);
18        return result;
19    }
20}
复制代码

启动类:缓存

1@SpringBootApplication
2public class SpringaopSbApplication {
3    public static void main(String[] args) {
4        SpringApplication.run(SpringaopSbApplication.class, args);
5    }
6}
复制代码

可是,若是全部接口都要记录耗时时间呢?咱们还按这种方式吗?显然不行,这种要在每一个接口都加上一样的代码,并且若是后期你老板说去掉的话,你还有一个个的删掉么?简直是不可想象。。
因此对于这种需求,实际上是能够提炼出来的。咱们想,统计接口的耗时时间,无非就是在接口的执行先后记录一下时而后相减打印出来便可,而后在这样的地方去加入咱们提炼出来的公共的代码。这就比如在原来的业务代码的基础上,把原来的代码横切开来,在须要的地方加入公共的代码,对原来的业务代码起到功能加强的做用。
这就是AOP的做用。架构

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

下面咱们来看看使用Spring AOP怎么知足这个需求。app

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

 1@Aspect
2@Component
3@Slf4j
4public class TimeMoitor {
5    @Pointcut(value = "execution(* com.walking.springaopsb.controller.*.*(..))")
6    public void pointCut(){}
7
8    @Around(value = "com.walking.springaopsb.aop.TimeMoitor.pointCut()")
9    public Object aroundTimeCounter(ProceedingJoinPoint jpx){
10        long start = System.currentTimeMillis();
11        Object proceed = null;
12        try {
13             proceed = jpx.proceed();
14        } catch (Throwable throwable) {
15            throwable.printStackTrace();
16        }
17        long end = System.currentTimeMillis();
18        log.info("耗时=>{}ms",end-start);
19        return proceed;
20    }
21}
复制代码

而后在LoginController#login方法里咱们就能够把日志打印耗时时间的代码删掉了。

 1@RestController
2@Slf4j
3public class LoginController {
4    @Autowired
5    LoginService loginService;
6    @RequestMapping("/login/{id}")
7    public Map<String,Object> login(@PathVariable("id") Integer id){
8        Map<String,Object> result = new HashMap<>();
9        result.put("status","0");
10        result.put("msg" , "失败");
11        if (loginService.login(id)) {
12            result.put("status","1");
13            result.put("msg" , "成功");
14        }
15        return result;
16    }
17}
复制代码

再好比,LoginController里如果还有别的方法,也同样能够应用到。
使用Spring AOP的控制台日志:

Spring AOP的原理

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

源码分析

那么咱们就经过源码来看一下吧。首先咱们将启动类改一下,方便咱们对源码debug。

启动类:

1@ComponentScan("com.walking.springaopsb.*")
2@EnableAspectJAutoProxy
3public class SpringaopSbApplication {
4    public static void main(String[] args) {
5        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringaopSbApplication.class);
6        LoginController loginController = (LoginController) applicationContext.getBean("loginController");
7        loginController.login(123);
8    }
9}
复制代码

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

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

要知道的是,咱们如今要看的是第5行仍是第6行生成的代理类。先看第6 行的getBean吧,进入这个方法org.springframework.context.support.AbstractApplicationContext#getBean(java.lang.String)

而后咱们只看有return的地方,在进入这个getBean(org.springframework.beans.factory.support.AbstractBeanFactory#getBean(java.lang.String))

再看doGetBean(org.springframework.beans.factory.support.AbstractBeanFactory#doGetBean)
第120行sharedInstance已经变成了代理类

因此咱们进入org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String)方法看看,从新运行,而后再加个断点,打到org.springframework.beans.factory.support.DefaultSingletonBeanRegistry#getSingleton(java.lang.String, boolean)里。

走过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还有别的本身的实例要获取,IoC容器里还有spring本身的实例,因此这个断点要加上条件,当beanName是loginController时进去断点,这样就方便多了。咱们只保留第5行的代码,由于getBean里面也会调getSingleton。

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

就是左下角的第二个方法doGetBean,发现传的是一个匿名内部类,这个匿名内部类里调的是org.springframework.beans.factory.support.AbstractBeanFactory#createBean
因此咱们把断点走完,进到这个createBean里打断点,一样加条件。
断点走过324行时变成代理类,即进入org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean看看,打个断点一样加条件

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

当走过1240行时已经变成了代理类,因此把断点打到这个org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#applyBeanPostProcessorsAfterInitialization方法,一样加上条件,把别的断点去掉,从新运行。

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

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

发现没有AnnotationAwareAspectJAutoProxyCreator这个实现类

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

发现AbstractAutoProxyCreator在上上个图中,而且AnnotationAwareAspectJAutoProxyCreator没有重写postProcessAfterInitialization方法,因此咱们就看AbstractAutoProxyCreator的这个方法。

打断点时发现Object bean不是代理类,那就看看org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#wrapIfNecessary方法。在这个方法中调用了createProxy()建立代理类,进去看下。

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

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

那么究竟建立哪种,就是咱们今天要看的关键之处,因此咱们进入createAopProxy()方法看看。

再进去org.springframework.aop.framework.DefaultAopProxyFactory#createAopProxy方法看看。

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

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

因此Spring AOP使用JDK动态代理仍是cglib取决因而否是接口,并无默认的方式。
咱们改一下LoginController让其实现接口

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

为何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行是关键

进去

进入proxyClassCache.get方法

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

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

咱们须要进这个get

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

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

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

通过屡次debug发现639-643行是关键,其中第639行是获取字节码,而后第642行调用defineClass0(一个native方法)​建立实例。​

 

这里加个小插曲,为何java的动态代理生成的代理类前面有个$Proxy呢,在这里能够获得答案。

 

 

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

 1public class Test {
2    public static void main(String[] args) throws Exception {
3        //获取ILoginBaseController的字节码
4        byte[] bytes = ProxyGenerator.generateProxyClass("$Proxy#MyLoginController"new Class[]{ILoginBaseController.class});
5        //输出到MyLoginController.class文件
6        FileOutputStream fileOutputStream = new FileOutputStream(new File("MyLoginController.class"));
7        fileOutputStream.write(bytes);
8        fileOutputStream.flush();
9        fileOutputStream.close();
10    }
11}
复制代码

 

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

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

 

总结

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

相关文章
相关标签/搜索