Spring AOP源码分析--代理方式的选择
能坚持别人不能坚持的,才能拥有别人未曾拥有的。
关注编程大道
公众号,让我们一同坚持心中所想,一起成长!!年前写了一个面试突击系列的文章,目前只有redis相关的。在这个系列里,我整理了一些面试题与大家分享,帮助年后和我一样想要在金三银四准备跳槽的同学。我们一起巩固、突击面试官常问的一些面试题,加油!!
《【面试突击】— Redis篇》--Redis数据类型?适用于哪些场景?
《【面试突击】— Redis篇》--Redis的线程模型了解吗?为啥单线程效率还这么高?
《【面试突击】— Redis篇》-- Redis的主从复制?哨兵机制?
《【面试突击】— Redis篇》-- Redis哨兵原理及持久化机制
《【面试突击】— Redis篇》--Redis Cluster及缓存使用和架构设计的常见问题
什么是 AOP ?
在软件业,AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。
AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
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怎么满足这个需求。
首先定义一个切面类
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行拿出来写个测试类
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();
}
}
我们会看到生成了指定的文件
看到这个文件你是不是就明白为啥JDK动态代理只能是接口了吗?原因就是java中是单继承多实现,$Proxy#MyLoginController类已经继承了Proxy类,所以不能在继承别的类了只能实现接口,所以JDK动态代理只能是接口。
总结
通过以上的源码分析我们弄清楚了,Spring AOP使用的代理机制了,并且是没有默认的代理,不是JDK动态代理就是cglib,以及为啥java的动态代理只能是接口。并且我们还看了一下spring的源码,虽然看的不是非常的仔细,但是通过这样看源码我们的理解更加的加深了,也锻炼了看源码的能力。
Spring AOP源码分析--代理方式的选择的更多相关文章
- Spring AOP 源码分析 - 创建代理对象
1.简介 在上一篇文章中,我分析了 Spring 是如何为目标 bean 筛选合适的通知器的.现在通知器选好了,接下来就要通过代理的方式将通知器(Advisor)所持有的通知(Advice)织入到 b ...
- Spring AOP源码分析(三):基于JDK动态代理和CGLIB创建代理对象的实现原理
AOP代理对象的创建 AOP相关的代理对象的创建主要在applyBeanPostProcessorsBeforeInstantiation方法实现: protected Object applyBea ...
- 5.2 spring5源码--spring AOP源码分析二--切面的配置方式
目标: 1. 什么是AOP, 什么是AspectJ 2. 什么是Spring AOP 3. Spring AOP注解版实现原理 4. Spring AOP切面原理解析 一. 认识AOP及其使用 详见博 ...
- spring AOP源码分析(三)
在上一篇文章 spring AOP源码分析(二)中,我们已经知道如何生成一个代理对象了,那么当代理对象调用代理方法时,增强行为也就是拦截器是如何发挥作用的呢?接下来我们将介绍JDK动态代理和cglib ...
- Spring AOP 源码分析 - 拦截器链的执行过程
1.简介 本篇文章是 AOP 源码分析系列文章的最后一篇文章,在前面的两篇文章中,我分别介绍了 Spring AOP 是如何为目标 bean 筛选合适的通知器,以及如何创建代理对象的过程.现在我们的得 ...
- Spring AOP 源码分析 - 筛选合适的通知器
1.简介 从本篇文章开始,我将会对 Spring AOP 部分的源码进行分析.本文是 Spring AOP 源码分析系列文章的第二篇,本文主要分析 Spring AOP 是如何为目标 bean 筛选出 ...
- Spring AOP 源码分析系列文章导读
1. 简介 前一段时间,我学习了 Spring IOC 容器方面的源码,并写了数篇文章对此进行讲解.在写完 Spring IOC 容器源码分析系列文章中的最后一篇后,没敢懈怠,趁热打铁,花了3天时间阅 ...
- 5.2 Spring5源码--Spring AOP源码分析二
目标: 1. 什么是AOP, 什么是AspectJ 2. 什么是Spring AOP 3. Spring AOP注解版实现原理 4. Spring AOP切面原理解析 一. 认识AOP及其使用 详见博 ...
- spring aop 源码分析(三) @Scope注解创建代理对象
一.源码环境的搭建: @Component @Scope(scopeName = ConfigurableBeanFactory.SCOPE_SINGLETON,proxyMode = ScopedP ...
随机推荐
- 在Linux CentOS下如何安装tar.gz和RPM软件包
1.安装tar.gz软件包: 在Linuxr(Centos下)如何安装tar.gz软件包,该方式实质上就是源代码安装方式,具体如下: 在Linux中使用wget命令下载要安装的文件,命令格式如下:wg ...
- RabbitMQ、Kafka、RocketMQ的优劣势
今天我们一起来探讨: 全量的消息队列究竟有哪些? Kafka.RocketMQ.RabbitMQ的优劣势比较 以及消息队列的选型 最全MQ消息队列有哪些 那么目前在业界有哪些比较知名的消息引擎呢?如下 ...
- DEVOPS技术实践_11:Jenkins集成Sonar
前言 前面已经有介绍sonar的安装,简单应用,下面在简答的研究一下sonar和jenkins集成的简单使用,对于sonar的安装不做介绍 一 sonar的简单介绍 持续检查避免了低质量的代码,比如S ...
- 使用rapidjson把文本json数据解析到树状结构
一个递归搞定 无聊的时候练练手就写了一个 头文件什么的我就不贴了 demo程序是MFC写的 void ParseObject(rapidjson::Value dc, CTreeCtrl * pTre ...
- 洛谷$P1600$ 天天爱跑步 树上差分
正解:树上差分 解题报告: 传送门$QwQ$! 这题还挺妙的,,,我想了半天才会$kk$ 首先对一条链$S-T$,考虑先将它拆成$S-LCA$和$LCA-T$,分别做.因为总体上来说差不多接下来我就只 ...
- JAVA字节码文件之常量池
一.常量池的内容 一个java类中定义的很多信息都是由常量池来维护和描述的,可以将常量池看作是class文件的资源仓库,比如java类中定义的方法与变量信息.常量池中主要存储两类常量:字面量(文本字符 ...
- Netty快速入门(03)Java NIO 介绍-Buffer
NIO 介绍 NIO,可以说是New IO,也可以说是non-blocking IO,具体怎么解释都可以. NIO 1是在JSR51里面定义的,在JDK1.4中引入,因为BolckingIO不支持高并 ...
- 快速搭建一个自己的个人博客(Github Pages~二次元主题)
前言 本次的一个布局技术都写的非常详细了,只要按着来就行,不过,先说明本次主题为二次元主题. 如果真的喜欢本主题的不妨可以试一试(==建议跟据目录来看==) 在很久很久以前.... 嘛,就在前不久我正 ...
- await Task.Yield()和await Task.CompletedTask有什么不同
有时候我们在代码中要执行一些非常耗时的操作,我们不希望这些操作阻塞调用线程(主线程)的执行,因为调用线程(主线程)可能还有更重要的工作要做,我们希望将这些非常耗时的操作由另外一个线程去执行,这个时候就 ...
- ASP.Net MVC 引用动态 js 脚本
希望可以动态生成 js 发送给客户端使用. layout页引用: <script type="text/javascript" src="@Url.Action( ...