能坚持别人不能坚持的, 才能拥有别人未曾拥有的.
关注编程大道公众号, 让我们一同坚持心中所想, 一起成长!!
年前写了一个面试突击系列的文章, 目前只有 Redis 相关的. 在这个系列里, 我整理了一些面试题与大家分享, 帮助年后和我一样想要在金三银四准备跳槽的同学. 我们一起巩固, 突击面试官常问的一些面试题, 加油!!
《[面试突击] - Redis 篇》--Redis 数据类型? 适用于哪些场景?
《[面试突击] - Redis 篇》--Redis 的线程模型了解吗? 为啥单线程效率还这么高?
《[面试突击] - Redis 篇》-- Redis 的主从复制? 哨兵机制?
《[面试突击] - Redis 篇》-- Redis 哨兵原理及持久化机制
《[面试突击] - Redis 篇》--Redis Cluster 及缓存使用和架构设计的常见问题
什么是 AOP ?
在软件业, AOP 为 Aspect Oriented Programming 的缩写, 意为: 面向切面编程, 通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术.
AOP 是 OOP 的延续, 是软件开发中的一个热点, 也是 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 的原理是什么呢, 用的什么技术呢?
其实就是反射 + 动态代理. 代理用的就是 JDK 动态代理或 cglib, 那么 Spring AOP 什么时候用 JDK 动态代理什么时候用 cglib? 默认使用哪种?
源码分析
那么我们就通过源码来看一下吧. 首先我们将启动类改一下, 方便我们对源码 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 容器时生成的还是获取时产生的呢? 我们也跟随源码来看一下吧.
要知道的是, 我们现在要看的是第 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 的源码, 虽然看的不是非常的仔细, 但是通过这样看源码我们的理解更加的加深了, 也锻炼了看源码的能力.
来源: https://www.cnblogs.com/ibigboy/p/12266388.html