侧边栏壁纸
博主头像
Xee博主等级

为了早日退休而学

  • 累计撰写 44 篇文章
  • 累计创建 8 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

Spring 那点东西

Xee
Xee
2022-04-06 / 0 评论 / 0 点赞 / 963 阅读 / 6,382 字

OK,紧接上篇的Spring,这次我们来聊聊 AOP 相关的东西

说下 AOP

AOP,Aspect Oriented Programming,面向切面编程。

将一些通用的逻辑集中实现,然后通过 AOP 进行逻辑的切入,减少了零散的碎片化代码,提高了系统的可维护性

具体是含义可以理解为:通过代理的方式,在调用想要的对象方法时候,进行拦截处理,执行切入的逻辑,然后再调用真正的方法实现。

例如,你实现了一个 A 对象,里面有 addUser 方法,此时你需要记录该方法的调用次数。

那么你就可以搞个代理对象,这个代理对象也提供了 addUser 方法,最终你调用的是代理对象的 addUser ,在这个代理对象内部填充记录调用次数的逻辑,最终的效果就类似下面代码:

class A代理 {
    A a;// 被代理的 A
   void addUser(User user) {
     count();// 计数
     a.addUser(user);
   }
}
最终使用的是:
A代理.addUser(user);

这就叫做面向切面编程,当然具体的代理的代码不是像上面这样写死的,而是动态切入

实现上代理大体上可以分为:动态代理静态代理

  • 动态代理,即在运行时将切面的逻辑进去,按照上面的逻辑就是你实现 A 类,然后定义要代理的切入点和切面的实现,程序会自动在运行时生成类似上面的代理类。
  • 静态代理,在编译时或者类加载时进行切面的织入,典型的 AspectJ 就是静态代理。

Spring AOP默认用的是什么动态代理,两者的区别

Spring AOP 的动态代理实现分别是:JDK 动态代理与 CGLIB

默认的实现是 JDK 动态代理。

ok,这个问题没毛病(对实际应用来说其实不太准确),然后面试官接着问那你平时有调试过吗,确定你得到的代理对象是 JDK 动态代理实现的?

然后你信誓旦旦的说,对,我们都实现接口的,所以是 JDK 动态代理。
然而你简历上写着项目使用的框架是 SpringBoot,我问你 SpringBoot 是什么版本,你说2.x。

然后我就可以推断,你没看过,你大概率仅仅只是网上看了相关的面试题。

要注意上面说的默认实现是 Spring Framework (最新版我没去验证),而 SpringBoot 2.x 版本已经默认改成了 CGLIB

而我们现在公司大部分使用的都是 SpringBoot 2.x 版本,所以你要说默认 JDK 动态代理也没错,但是不符合你平日使用的情况,对吧?

如果你调试过,或者看过调用栈,你肯定能发现默认用的是 CGLIB(当然你要是没用 SpringBoot 当我没说哈):
image.png

市面上大部分面试题答案写的就是 JDK 动态代理,是没错,Spring 官网都这样写的。

但是咱们现在不都是用 SpringBoot 了嘛,所以这其实不符合我们当下使用的情况。

因此,面试时候不要只说 Spring AOP 默认用的是 JDK 动态代理,把 SpringBoot 也提一嘴,这不就是让面试官刮目一看嘛(不过指不定面试官也不知道~)

如果要修改 SpringBoot 使用 JDK 动态代理,那么设置 spring.aop.proxy-target-class=false

如果你提了这个,那面试官肯定会追问:那为什么要改成默认用 CGLIB?

直接看大佬怎么回答的:
image.png

大佬说 JDK 动态代理要求接口,所以没有接口的话会有报错,很令人讨厌,并且让 CGLIB 作为默认也没什么副作用,特别是 CGLIB 已经被重新打包为 Spring 的一部分了,所以就默认 CGLIB

好吧,其实也没有什么很特殊的含义,就是效果没差多少,还少报错,方便咯。

JDK 动态代理

JDK 动态代理是基于接口的,也就是被代理的类一定要实现了某个接口,否则无法被代理。

主要实现原理就是:

  • 1、首先通过实现一个 InvocationHandler 接口得到一个切面类。
  • 2、然后利用 Proxy 糅合目标类的类加载器、接口和切面类得到一个代理类。
  • 3、代理类的逻辑就是执行切入逻辑,把所有接口方法的调用转发到 InvocationHandlerinvoke() 方法上,然后根据反射调用目标类的方法。

image.png

我们再深入一点点了解下原理实现。

如果你反编译的话,你能看到生成的代理类是会先在静态块中通过反射把所有方法都拿到存在静态变量中,大致长这样:
image.png

上面就是把 getUserInfo 方法缓存了,然后在调用代理类的 getUserInfo 的时候,会调用你之前实现的 InvocationHandler 里面的 invoke

这样就执行到切入的逻辑了,且最终执行了被代理类的 getUserInfo 方法。

就是中间商拦了一道咯,道理就是这个道理。

CGLIB

Spring 里面,如果被代理的类没有实现接口,那么就用 CGLIB 来完成动态代理。

CGLIB 是基于ASM 字节码生成工具,它是通过继承的方式来实现代理类,所以要注意 final 方法,这种方法无法被继承。

简单理解下,就是生成代理类的子类,如何生成呢?

通过字节码技术动态拼接成一个子类,在其中织入切面的逻辑。
使用例子:

Enhancer en = new Enhancer();
//2.设置父类,也就是代理目标类,上面提到了它是通过生成子类的方式
en.setSuperclass(target.getClass());
//3.设置回调函数,这个this其实就是代理逻辑实现类,也就是切面,可以理解为JDK 动态代理的handler
en.setCallback(this);
//4.创建代理对象,也就是目标类的子类了。
return en.create();

JDK 动态代理CGLIB 两者经常还可能被面试官问性能对比,所以咱们也列一下:

  • jdk6 下,在运行次数较少的情况下,jdk动态代理与 cglib 差距不明显,甚至更快一些;而当调用次数增加之后, cglib 表现稍微更快一些
  • jdk7 下,情况发生了逆转!在运行次数较少(1,000,000)的情况下,jdk动态代理比 cglib 快了差不多30%;而当调用次数增加之后(50,000,000), 动态代理比 cglib 快了接近1倍
  • jdk8 表现和 jdk7 基本一致

能说下拦截链的实现吗

我们都知道 Spring AOP 提供了多种拦截点,便捷我们对 AOP 的使用,比如 @Before、@After、@AfterReturning、@AfterThrowing 等等。

方便我们在目标方法执行前、后、抛错等地方进行一些逻辑的切入。

那 Spring 具体是如何链起这些调用顺序的呢?

这就是拦截链干的事,实际上这些注解都对应着不同的 interceptor 实现。

然后 Spring 会利用一个集合把所有类型的 interceptor 组合起来,我在代码里用了 @Before、@After、@AfterReturning、@AfterThrowing 这几个注解。

于是集合里就有了这些 interceptor (多了一个 expose...等下解释),这是由 Spring 扫描到注解自动加进来的:
image.png

然后通过一个对象 CglibMethodInvocation 将这个集合封装起来,紧接着调用这个对象的 proceed 方法,可看到这个集合 chain 被传入了。
image.png

我们来看下 CglibMethodInvocation#proceed 方法逻辑。

要注意,这里就开始递归套娃了,核心调用逻辑就在这里:
image.png

可以看到有个 currentInterceptorIndex 变量,通过递归,每次新增这索引值,来逐得到下一个 interceptor

并且每次都传入当前对象并调用 interceptor#invoke ,这样就实现了拦截链的调用,所以这是个递归。

我们拿集合里面的 MethodBeforeAdviceInterceptor 来举例看下,这个是目标方法执行的前置拦截,我们看下它的 invoke 实现,有更直观的认识:
image.png

invoke 的实现是先执行切入的前置逻辑,然后再继续调用 CglibMethodInvocation#proceed(也就是mi.proceed),进行下一个 interceptor 的调用。

总结下:

Spring 根据 @Before、@After、@AfterReturning、@AfterThrowing 这些注解。

往集合里面加入对应的 Spring 提供的 MethodInterceptor 实现。

比如上面的 MethodBeforeAdviceInterceptor,如果你没用 @Before,集合里就没有 MethodBeforeAdviceInterceptor

然后通过一个对象 CglibMethodInvocation 将这个集合封装起来,紧接着调用这个对象的 proceed 方法。

具体是利用 currentInterceptorIndex下标,利用递归顺序地执行集合里面的 MethodInterceptor,这样就完成了拦截链的调用。

我截个调用链的堆栈截图,可以很直观地看到调用的顺序(从下往上看):
image.png

是吧,是按照顺序一个一个往后执行,然后再一个一个返回,就是递归呗。

然后我再解释下上面的 chain 集合我们看到第一个索引位置的 ExposeInvocationInterceptor

这个 Interceptor 作为第一个被调用,实际上就是将创建的 CglibMethodInvocation 这个对象存入 threadlocal 中,方便后面 Interceptor 调用的时候能得到这个对象,进行一些调用。
image.png

ok,更多细节还是得自己看源码的,应付面试了解到这个程度差不多的,上面几个关键点一抛,这个题绝对稳了!

Spring AOP 和 AspectJ有什么区别

从上面的题目我们已经知道,两者分别是动态代理和静态代理的区别。

Spring AOP 是动态代理,AspectJ 是静态代理。

从一个是运行时织入,一个在编译时织入,我们稍微一想到就能知道,编译时就准备完毕,那么在调用时候没有额外的织入开销,性能更好些。

且 AspectJ 提供完整的 AOP 解决方案,像 Spring AOP 只支持方法级别的织入,而 AspectJ 支持字段、方法、构造函数等等,所以它更加强大,当然也更加复杂。

说下 Spring Bean 的生命周期

在说具体的生命周期前,我们需要先知晓之所以 Bean 容易被添加一些属性,或者能在运行时被改造就是因为在生成 Bean 的时候,Spring 对外暴露出很多扩展点。

基于这些点我们可以设置一些逻辑,Spring 会在 Bean 创建的某些阶段根据这些扩展点,基于此进行 Bean 的改造。

有了上面的认识,我们再来看 Spring Bean 的生命周期,我用一幅图先总结一下:
image.png

大致了解生命周期之后,我们再来看详细的操作,可以看到有好多扩展点可以搞事情:
image.png

注意细节,这幅图的颜色和上面那副有对应关系的。

我再用文字描述一下:

  • 实例化Bean
  • 根据属性,注入需要的 Bean
  • 如果 Bean 实现了 BeanNameAware 等 aware 接口,则执行 aware 注入
  • 如果有 BeanPostProcessor,则执行BeanPostProcessor#postProcessBeforeInitialization 方法
  • 如果 Bean 是 InitializingBean,则执行 afterPropertiesSet 方法
  • 如果有 initMethod ,则执行
  • 如果有 BeanPostProcessor,执行BeanPostProcessor#postProcessAfterInitialization 方法
  • 使用 Bean
  • 如果 Bean 是 DisposableBean,则执行 destroy 方法
  • 如果有 destroy 方法,则执行

说下对 Spring MVC 的理解

Spring MVC 是基于 Servlet API 构建的,可以说核心就是 DispatcherServlet,即一个前端控制器。

还有几个重要的组件:处理器映射、控制器、视图解析器等。

由这几个组件让我们与 Servlet 解耦,不需要写一个个 Servlet ,基于 Spring 的管理就可以很好的实现 web 应用,简单,方便。

然后关于 MVC 的解释就不提了,什么 Model,View,Controller 啥的。

Spring MVC 具体的工作原理

当一个请求过来的时候,由 DispatcherServlet 接待,它会根据处理器映射(HandlerMapping)找到对应的 HandlerExecutionChain (这里面包含了很多定义的 HandlerInterceptor,拦截器)。

然后通过 HandlerAdapter 适配器的适配(适配器模式了解一下)后,执行 handler,即通过 controller 的调用,返回 ModelAndView

然后 DispatcherServlet 解析得到 ViewName,将其传给 ViewResoler视图解析器,解析后获得 View 视图。

然后 DispatcherServletmodel 数据填充到 view ,得到最终的 Responose 返回给用户。

我们常用的视图有 jsp、freemaker、velocity 等。

Spring MVC 父子容器是什么

image.png

这是官网上的一张图,可以看到,servicesrepositories 是属于父容器的,而 Controllers 等是属于子容器的。

那为什么会有父子之分?

其实 Spring 容器在启动的时候,不会有 SpringMVC 这个概念,只会扫描文件然后创建一个 context ,此时就是父容器。

然后发现是 web 服务需要生成 DispatcherServlet,此时就会调用 DispatcherServlet#init,这个方法里面最会生成一个新的 context,并把之前的 context置为自己的 Parent

这样就有了父子之分,这样指责就更加清晰,子容器就负责 web 部分,父容器则是通用的一些 bean

也正是有了父子之分,如果有些人没把 controller 扫包的配置写在 spring-servlet.xml ,而写到了 service.xml 里,那就会把 controller 添加到父容器里,这样子容器里面就找不到了,请求就 404了。

当然,如果你把 services repositories 添加到子容器是没影响的,不过没必要,分层还是比较好的方式。

对了,子容器可以用父容器的 Bean,父容器不能用子容器的 Bean。

Spring 用到的设计模式

  • 工厂模式,从名字就看出来了 BeanFacotry。
  • 模板方法,什么 JdbcTemplate、RestTemplate 。
  • 代理模式,AOP 整的都是代理。
  • 单例,这都不需要说了。
  • 责任链模式,比如拦截器
  • 观察者模式,Spring里的监听器
  • 适配器模式...SpringMVC 提到的 handlerApdaper
  • 太多啦...

Spring 事务的隔离级别

image.png

从源码定义我们可以看到,一共有 5 种隔离级别,而 DEFAULT 就是使用数据库定义的隔离级别。

其他几种分别是:读未提交、读已提交、可重复读、序列化

具体几个隔离级别的概念我就不介绍了,应该都很清楚。

Spring 的事务传播行为

image.png

从源码来看,一共有 7 种事务传播行为:

  • PROPAGATION_REQUIRED(默认) 如果当前存在事务,则用当前事务,如果没有事务则新起一个事务
  • PROPAGATION_SUPPORTS 支持当前事务,如果不存在,则以非事务方式执行
  • PROPAGATION_MANDATORY 支持当前事务,如果不存在,则抛出异常
  • PROPAGATION_REQUIRES_NEW 创建一个新事务,如果存在当前事务,则挂起当前事务
  • PROPAGATION_NOT_SUPPORTED 不支持当前事务,始终以非事务方式执行
  • PROPAGATION_NEVER 不支持当前事务,如果当前存在事务,则抛出异常
  • PROPAGATION_NESTED 如果当前事务存在,则在嵌套事务中执行,内层事务依赖外层事务,如果外层失败,则会回滚内层,内层失败不影响外层。

Spring 事务传播行为的作用

其实答案就几个字:控制事务的边界

0

评论区