- Spring bean的生命流程
- 何为循环依赖
- 造成的问题
- 循环依赖发生的时机
- Spring 如何解决的
- 回顾一下如何解决的?
- 还有纰漏吗?
- 自己动手实现的 Spring IOC 和 AOP - 上篇
- 自己动手实现的 Spring IOC 和 AOP - 下篇
- Spring MVC 原理探秘 - 一个请求的旅行过程
- Spring MVC 原理探秘 - 容器的创建过程
- Spring 事务实现分析
- 6 一些细节
- 从Spring Boot到SpringMVC(非注解方式)
- SpringBoot 中 @SpringBootApplication注解背后的三体结构探秘
- SpringBoot 应用程序启动过程探秘
Spring bean的生命流程
Spring 是一个轻量级的 J2EE 开源框架,其目标是降低企业级应用开发难度,提高企业级应用开发效率。在日程开发中,我们会经常使用 Spring 框架去构建应用。所以作为一个经常使用的框架,了解其原理还是很有必要的。接下来我们就从宏观层面上,来看看 Spring 中的 bean 由实例化到销毁的过程。在详细讨论 bean 生命周期前,先上一张图,后面也会围绕这张图展开讨论。
图1 bean实例化过程
接下来对照上图,一步一步对 singleton 类型 bean 的生命周期进行解析:
- 实例化 bean 对象,类似于 new XXObject()
- 将配置文件中配置的属性填充到刚刚创建的 bean 对象中。
- 检查 bean 对象是否实现了 Aware 一类的接口,如果实现了则把相应的依赖设置到 bean 对象中。比如如果 bean 实现了 BeanFactoryAware 接口,Spring 容器在实例化bean的过程中,会将 BeanFactory 容器注入到 bean 中。
- 调用 BeanPostProcessor 前置处理方法,即 postProcessBeforeInitialization(Object bean, String beanName)。
- 检查 bean 对象是否实现了 InitializingBean 接口,如果实现,则调用 afterPropertiesSet 方法。或者检查配置文件中是否配置了 init-method 属性,如果配置了,则去调用 init-method 属性配置的方法。
- 调用 BeanPostProcessor 后置处理方法,即 postProcessAfterInitialization(Object bean, String beanName)。我们所熟知的 AOP 就是在这里将 Adivce 逻辑织入到 bean 中的。
- 注册 Destruction 相关回调方法。
- bean 对象处于就绪状态,可以使用了。
- 应用上下文被销毁,调用注册的 Destruction 相关方法。如果 bean 实现了 DispostbleBean 接口,Spring 容器会调用 destroy 方法。如果在配置文件中配置了 destroy 属性,Spring 容器则会调用 destroy 属性对应的方法。
上述流程从宏观上对 Spring 中 singleton 类型 bean 的生命周期进行了描述,接下来说说所上面流程中的一些细节问题。
先看流程中的第二步 – 设置对象属性。在这一步中,对于普通类型的属性,例如 String,Integer等,比较容易处理,直接设置即可。但是如果某个 bean 对象依赖另一个 bean 对象,此时就不能直接设置了。Spring 容器首先要先去实例化 bean 依赖的对象,实例化好后才能设置到当前 bean 中。大致流程如下:
图2 依赖实例化流程图
上面图片描述的依赖比较简单,就是 BeanA 依赖 BeanB。现在考虑这样一种情况,BeanA 依赖 BeanB,BeanB 依赖 BeanC,BeanC 又依赖 BeanA。三者形成了循环依赖,如下所示:
图3 循环依赖
对于这样的循环依赖,根据依赖注入方式的不同,Spring 处理方式也不同。如果依赖靠构造器方式注入,则无法处理,Spring 直接会报循环依赖异常。这个理解起来也不复杂,构造 BeanA 时需要 BeanB 作为构造器参数,此时 Spring 容器会先实例化 BeanB。构造 BeanB 时,BeanB 又需要 BeanC 作为构造器参数,Spring 容器又不得不先去构造 BeanC。最后构造 BeanC 时,BeanC 又依赖 BeanA 才能完成构造。此时,BeanA 还没构造完成,BeanA 要等 BeanB 实例化好才能完成构造,BeanB 又要等 BeanC,BeanC 等 BeanA。这样就形成了死循环,所以对于以构造器注入方式的循环依赖是无解的,Spring 容器会直接报异常。对于 setter 类型注入的循环依赖则可以顺利完成实例化并依次注入,这里具体细节就不说了,详细可以参考《Spring源码深度解析》一书相关章节。
循环依赖问题说完,接下来 bean 实例化流程中的第6步 – 调用 BeanPostProcessor 后置处理方法。先介绍一下 BeanPostProcessor 接口,BeanPostProcessor 接口中包含了两个方法,其定义如下:
public interface BeanPostProcessor { Object postProcessBeforeInitialization(Object bean, String beanName) throws Exception; Object postProcessAfterInitialization(Object bean, String beanName) throws Exception;} |
---|
BeanPostProcessor 是一个很有用的接口,通过实现接口我们就可以插手 bean 的实例化过程,为拓展提供了可能。我们所熟知的 AOP 就是在这里进行织如入,具体点说是在 postProcessAfterInitialization(Object bean, String beanName) 执行织入逻辑的。下面就来说说 Spring AOP 织入的流程,以及 AOP 是怎样和 IOC 整合的。先说 Spring AOP 织入流程,大致如下:
- 查找实现了 PointcutAdvisor 类型的切面类,切面类包含了 Pointcut 和 Advice 实现类对象。
- 检查 Pointcut 中的表达式是否能匹配当前 bean 对象。
- 如果匹配到了,表明应该对此对象织入 Advice。
- 将 bean,bean class对象,bean实现的interface的数组,Advice对象传给代理工厂 ProxyFactory。代理工厂创建出 AopProxy 实现类,最后由 AopProxy 实现类创建 bean 的代理类,并将这个代理类返回。此时从 postProcessAfterInitialization(Object bean, String beanName) 返回的 bean 此时就不是原来的 bean 了,而是 bean 的代理类。原 bean 就这样被无感的替换掉了,是不是有点偷天换柱的感觉。
大家现在应该知道 AOP 是怎样作用在 bean 上的了,那么 AOP 是怎样和 IOC 整合起来并协同工作的呢?下面就来简单说一下。
Spring AOP 生成代理类的逻辑是在 AbstractAutoProxyCreator 相关子类中实现的,比如 DefaultAdvisorAutoProxyCreator、AspectJAwareAdvisorAutoProxyCreator 等。上面说了 BeanPostProcessor 为拓展留下了可能,这里 AbstractAutoProxyCreator 就将可能变为了现实。AbstractAutoProxyCreator 实现了 BeanPostProcessor 接口,这样 AbstractAutoProxyCreator 可以在 bean 初始化时做一些事情。光继承这个接口还不够,继承这个接口只能获取 bean,要想让 AOP 生效,还需要拿到切面对象(包含 Pointcut 和 Advice)才行。所以 AbstractAutoProxyCreator 同时继承了 BeanFactoryAware 接口,通过实现该接口,AbstractAutoProxyCreator 子类就可拿到 BeanFactory,有了 BeanFactory,就可以获取 BeanFactory 中所有的切面对象了。有了目标对象 bean,所有的切面类,此时就可以为 bean 生成代理对象了。
图4 AbstractAutoProxyCreator继承图(删掉了一些不关心的继承分支)
到这里,从宏观上已经对 bean 的生命流程进行了较为详细的描述。由于暂时能力有限,只能从宏观上分析,以前尝试过去看 Spring IOC 的实现代码,感觉还是太复杂了,细节太多,跟踪了十几二十个方法后就开始凌乱了。在几次失败的尝试后,终于放弃了。后来总结了一下失败的原因,当时自己刚工作不是很久,代码写的少,经验不足。并且在对 Spring 很多特性不熟悉的情况下就去看 Spring 源码,结果只能到处碰壁,陷入 Spring 各种细节之中久久不能自拔😂。所以对于想看某个框架代码的同学,一定要在熟练使用这个框架的基础上再去看。不要像我这样急于求成,不然到最后只能失败啊。本人这篇博客建立在仿写了 Spring IOC 和 AOP的基础上写出来的,在仿写过程中参考了黄亿华前辈的 tiny-spring 项目,有兴趣的同学可以读读 tiny-spring。我自己仿写的项目也放在了github上,传送门 —> toy-spring。
本篇博客到此结束,如果有写错的地方,欢迎指出来,谢谢!如果错误的地方对你造成了困扰,我表示很抱歉。
参考:
何为循环依赖
造成的问题
来一串代码说明问题
public class A {
private B b;
}
public class B {
private A a;
}
/**********************/
<bean id="beanA" class="xyz.coolblog.BeanA">
<property name="beanB" ref="beanB"/>
</bean>
<bean id="beanB" class="xyz.coolblog.BeanB">
<property name="beanA" ref="beanA"/>
</bean>
IOC 按照上面所示的
循环依赖发生的时机
Bean 实例化主要分为三步,如图:
问题出现在:第一步和第二步的过程中,也就是填充属性 / 方法的过程中
Spring 如何解决的
- Spring 为了解决单例的循环依赖问题,使用了 三级缓存 ,递归调用时发现 Bean 还在创建中即为循环依赖
单例模式的 Bean 保存在如下的数据结构中:
/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<String, Object>(256);
/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects = new HashMap<String, Object>(16);
/** 三级级缓存:存放 bean 工厂对象,用于解决循环依赖 */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<String, ObjectFactory<?>>(16);
/**
bean 的获取过程:先从一级获取,失败再从二级、三级里面获取
创建中状态:是指对象已经 new 出来了但是所有的属性均为 null 等待被 init
*/
检测循环依赖的过程如下:
A 创建过程中需要 B,于是 A 将自己放到三级缓里面 ,去实例化 B
- B 实例化的时候发现需要 A,于是 B 先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了!
- 然后把三级缓存里面的这个 A 放到二级缓存里面,并删除三级缓存里面的 A
- B 顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态)
- 然后回来接着创建 A,此时 B 已经创建结束,直接从一级缓存里面拿到 B ,然后完成创建,并将自己放到一级缓存里面
- 如此一来便解决了循环依赖的问题
// 以上叙述的源代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
this.earlySingletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}
回顾一下如何解决的?
一句话:先让最底层对象完成初始化,通过三级缓存与二级缓存提前曝光创建中的 Bean,让其他 Bean 率先完成初始化。还有纰漏吗?
Spring 还是有一些无法解决的循环依赖,需要我们写代码的时候注意,例如:使用构造器注入其他 Bean 的实例,这个就没办法了。要手动改代码
自己动手实现的 Spring IOC 和 AOP - 上篇
1. 背景
我在大四实习的时候开始接触 J2EE 方面的开发工作,也是在同时期接触并学习 Spring 框架,到现在也有快有两年的时间了。不过之前没有仿写过 Spring IOC 和 AOP,只是宏观上对 Spring IOC 和 AOP 原理有一定的认识。所以为了更进一步理解 Spring IOC 和 AOP 原理。在工作之余,参考了一些资料和代码,动手实现了一个简单的 IOC 和 AOP,并实现了如下功能:
- 根据 xml 配置文件加载相关 bean
- 对 BeanPostProcessor 类型的 bean 提供支持
- 对 BeanFactoryAware 类型的 bean 提供支持
- 实现了基于 JDK 动态代理的 AOP
- 整合了 IOC 和 AOP,使得二者可很好的协同工作
在实现自己的 IOC 和 AOP 前,我的想法比较简单,就是实现一个非常简单的 IOC 和 AOP,哪怕是几十行代码实现的都行。后来实现后,感觉还很有意思的。不过那个实现太过于简单,和 Spring IOC,AOP 相去甚远。后来想了一下,不能仅满足那个简单的实现,于是就有了这个仿写项目。相对来说仿写的代码要复杂了一些,功能也多了一点,看起来也有点样子的。尽管仿写出的项目仍然是玩具级,不过写仿写的过程中,还是学到了一些东西。总体上来说,收获还是很大的。在接下来文章中,我也将从易到难,实现不同版本的 IOC 和 AOP。好了,不多说了,开始干活。
2. 简单的 IOC 和 AOP 实现
2.1 简单的 IOC
先从简单的 IOC 容器实现开始,最简单的 IOC 容器只需4步即可实现,如下:
- 加载 xml 配置文件,遍历其中的标签
- 获取标签中的 id 和 class 属性,加载 class 属性对应的类,并创建 bean
- 遍历标签中的标签,获取属性值,并将属性值填充到 bean 中
- 将 bean 注册到 bean 容器中
如上所示,仅需4步即可,是不是觉得很简单。好了,Talk is cheap, Show me the code. 接下来要上代码了。不过客官别急,上代码前,容我对代码结构做一下简单介绍:
SimpleIOC // IOC 的实现类,实现了上面所说的4个步骤SimpleIOCTest // IOC 的测试类Car // IOC 测试使用的 beanWheel // 同上 ioc.xml // bean 配置文件 |
---|
容器实现类 SimpleIOC 的代码:
public class SimpleIOC { private Map |
---|
容器测试使用的 bean 代码:
public class Car { private String name; private String length; private String width; private String height; private Wheel wheel; // 省略其他不重要代码}public class Wheel { private String brand; private String specification ; // 省略其他不重要代码} |
---|
bean 配置文件 ioc.xml 内容:
IOC 测试类 SimpleIOCTest:
public class SimpleIOCTest { @Test public void getBean() throws Exception { String location = SimpleIOC.class.getClassLoader().getResource(“spring-test.xml”).getFile(); SimpleIOC bf = new SimpleIOC(location); Wheel wheel = (Wheel) bf.getBean(“wheel”); System.out.println(wheel); Car car = (Car) bf.getBean(“car”); System.out.println(car); }} |
---|
测试结果:
以上是简单 IOC 实现的全部内容,难度不大,代码也不难看懂,这里不再多说了。下面说说简单 AOP 的实现。
2.2 简单的 AOP 实现
AOP 的实现是基于代理模式的,这一点相信大家应该都知道。代理模式是AOP实现的基础,代理模式不难理解,这里就不花篇幅介绍了。在介绍 AOP 的实现步骤之前,先引入 Spring AOP 中的一些概念,接下来我们会用到这些概念。
通知(Advice)
通知定义了要织入目标对象的逻辑,以及执行时机。Spring 中对应了 5 种不同类型的通知:· 前置通知(Before):在目标方法执行前,执行通知· 后置通知(After):在目标方法执行后,执行通知,此时不关系目标方法返回的结果是什么· 返回通知(After-returning):在目标方法执行后,执行通知· 异常通知(After-throwing):在目标方法抛出异常后执行通知· 环绕通知(Around): 目标方法被通知包裹,通知在目标方法执行前和执行后都被会调用 |
---|
切点(Pointcut)
如果说通知定义了在何时执行通知,那么切点就定义了在何处执行通知。所以切点的作用就是通过匹配规则查找合适的连接点(Joinpoint),AOP 会在这些连接点上织入通知。 |
---|
切面(Aspect)
切面包含了通知和切点,通知和切点共同定义了切面是什么,在何时,何处执行切面逻辑。 |
---|
说完概念,接下来我们来说说简单 AOP 实现的步骤。这里 AOP 是基于 JDK 动态代理实现的,只需3步即可完成:
- 定义一个包含切面逻辑的对象,这里假设叫 logMethodInvocation
- 定义一个 Advice 对象(实现了 InvocationHandler 接口),并将上面的 logMethodInvocation 和 目标对象传入
- 将上面的 Adivce 对象和目标对象传给 JDK 动态代理方法,为目标对象生成代理
上面步骤比较简单,不过在实现过程中,还是有一些难度的,这里要引入一些辅助接口才能实现。接下来就来介绍一下简单 AOP 的代码结构:
MethodInvocation 接口 // 实现类包含了切面逻辑,如上面的 logMethodInvocationAdvice 接口 // 继承了 InvocationHandler 接口BeforeAdvice 类 // 实现了 Advice 接口,是一个前置通知SimpleAOP 类 // 生成代理类SimpleAOPTest // SimpleAOP 从测试类HelloService 接口 // 目标对象接口HelloServiceImpl // 目标对象 |
---|
MethodInvocation 接口代码:
public interface MethodInvocation { void invoke();} |
---|
Advice 接口代码:
public interface Advice extends InvocationHandler {} |
---|
BeforeAdvice 实现代码:
public class BeforeAdvice implements Advice { private Object bean; private MethodInvocation methodInvocation; public BeforeAdvice(Object bean, MethodInvocation methodInvocation) { this.bean = bean; this.methodInvocation = methodInvocation; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 在目标方法执行前调用通知 methodInvocation.invoke(); return method.invoke(bean, args); }} |
---|
SimpleAOP 实现代码:
public class SimpleAOP { public static Object getProxy(Object bean, Advice advice) { return Proxy.newProxyInstance(SimpleAOP.class.getClassLoader(), bean.getClass().getInterfaces(), advice); }} |
---|
HelloService 接口,及其实现类代码:
public interface HelloService { void sayHelloWorld();}public class HelloServiceImpl implements HelloService { @Override public void sayHelloWorld() { System.out.println(“hello world!”); }} |
---|
SimpleAOPTest 代码:
public class SimpleAOPTest { @Test public void getProxy() throws Exception { // 1. 创建一个 MethodInvocation 实现类 MethodInvocation logTask = () -> System.out.println(“log task start”); HelloServiceImpl helloServiceImpl = new HelloServiceImpl(); // 2. 创建一个 Advice Advice beforeAdvice = new BeforeAdvice(helloServiceImpl, logTask); // 3. 为目标对象生成代理 HelloService helloServiceImplProxy = (HelloService) SimpleAOP.getProxy(helloServiceImpl,beforeAdvice); helloServiceImplProxy.sayHelloWorld(); }} |
---|
输出结果:
以上实现了简单的 IOC 和 AOP,不过实现的 IOC 和 AOP 还很简单,且只能独立运行。在下一篇文章中,我将实现一个较为复杂的 IOC 和 AOP,大家如果有兴趣可以去看看。好了,本篇文章到此结束。
自己动手实现的 Spring IOC 和 AOP - 下篇
1. 背景
本文承接上文,来继续说说 IOC 和 AOP 的仿写。在上文中,我实现了一个很简单的 IOC 和 AOP 容器。上文实现的 IOC 和 AOP 功能很单一,且 IOC 和 AOP 两个模块没有整合到一起。IOC 在加载 bean 过程中,AOP 不能对 bean 织入通知。在本文中,我们详细说一下升级版 IOC 和 AOP。这个版本的实现包含了在上篇中所说的功能,这里再重述一下,如下:
- 根据 xml 配置文件加载相关 bean
- 对 BeanPostProcessor 类型的 bean 提供支持
- 对 BeanFactoryAware 类型的 bean 提供支持
- 实现了基于 JDK 动态代理的 AOP
- 整合了 IOC 和 AOP,使得二者可很好的协同工作
上面罗列了5个功能点,虽然看起来不多,但是对于新手来说,实现起来还是不很容易的。所以接下来,我将围绕上面的功能点展开详细的描述。如果大家有兴趣,我还是很建议大家跟着写一遍,因为很多时候能看懂,但是写的却不一定能写出来。仿写一遍能够加深对 Spring IOC 和 AOP 原理的理解,多动手是有好处的。
另外需要说明的是,黄亿华前辈实现的 tiny-spring 项目时间节点是 2014.1,当时应该是参照 Spring 3.x 版本编写的。部分类的设计思想可能会与现在最新稳定版 4.3.10 有一定的出入,由于我暂时没有阅读 Spring 源码的计划,所以这里不能告知大家 tiny-spring 哪些类与 Spring 最新的源码有出入,见谅。
好了,本章内容先介绍到这,接下来进入正文。
2. IOC 的实现
2.1 BeanFactory 的生命流程
上面简述了 toy-spring 项目的编码背景,接下来,在本节中,我将向大家介绍 toy-spring 项目中 IOC 部分的实现原理。在详细介绍 IOC 的实现原理前,这里先简单说一下 BeanFactory 的生命流程:
- BeanFactory 加载 Bean 配置文件,将读到的 Bean 配置封装成 BeanDefinition 对象
- 将封装好的 BeanDefinition 对象注册到 BeanDefinition 容器中
- 注册 BeanPostProcessor 相关实现类到 BeanPostProcessor 容器中
- BeanFactory 进入就绪状态
- 外部调用 BeanFactory 的 getBean(String name) 方法,BeanFactory 着手实例化相应的 bean
- 重复步骤 3 和 4,直至程序退出,BeanFactory 被销毁
上面简单罗列了 BeanFactory 的生命流程,也就是 IOC 容器的生命流程。接下来就来围绕上面的流程展开讨论。
2.2 BeanDefinition 及其他一些类的介绍
在详细介绍 IOC 容器的工作原理前,这里先介绍一下实现 IOC 所用到的一些辅助类,包括BeanDefinition、BeanReference、PropertyValues、PropertyValue。这些类与接下来的 2.3 节 xml 的解析紧密相关。按照顺序,先从 BeanDefinition 开始介绍。
BeanDefinition,从字面意思上翻译成中文就是 “Bean 的定义”。从翻译结果中就可以猜出这个类的用途,即根据 Bean 配置信息生成相应的 Bean 详情对象。举个例子,如果把 Bean 比作是电脑 💻,那么 BeanDefinition 就是这台电脑的配置清单。我们从外观上无法看出这台电脑里面都有哪些配置,也看不出电脑的性能咋样。但是通过配置清单,我们就可了解这台电脑的详细配置。我们可以知道这台电脑是不是用了牙膏厂的 CPU,BOOM 厂的固态硬盘等。透过配置清单,我们也就可大致评估出这台电脑的性能。
图1 电脑和配置清单
上面那个例子还是比较贴切的,但是只是个例子,和实际还是有差距的。那么在具体实现中,BeanDefinition 和 xml 是怎么对应的呢?答案在下面:
图2 根据 bean 配置生成 BeanDefinition
看完上图,我想大家对 BeanDefinition 的用途有了更进一步的认识。接下来我们来说说上图中的 ref 对应的 BeanReference 对象。BeanReference 对象保存的是 bean 配置中 ref 属性对应的值,在后续 BeanFactory 实例化 bean 时,会根据 BeanReference 保存的值去实例化 bean 所依赖的其他 bean。
接下来说说 PropertyValues 和 PropertyValue 这两个长的比较像的类,首先是PropertyValue。PropertyValue 中有两个字段 name 和 value,用于记录 bean 配置中的标签的属性值。然后是PropertyValues,PropertyValues 从字面意思上来看,是 PropertyValue 复数形式,在功能上等同于 List。那么为什么 Spring 不直接使用 List,而自己定义一个新类呢?答案是要获得一定的控制权,看下面的代码:
public class PropertyValues { private final List |
---|
好了,辅助类介绍完了,接下来我们继续 BeanFactory 的生命流程探索。
2.3 xml 的解析
BeanFactory 初始化时,会根据传入的 xml 配置文件路径加载并解析配置文件。但是加载和解析 xml 配置文件这种脏活累活,BeanFactory 可不太愿意干,它只想高冷的管理容器中的 bean。于是 BeanFactory 将加载和解析配置文件的任务委托给专职人员 BeanDefinitionReader 的实现类 XmlBeanDefinitionReader 去做。那么 XmlBeanDefinitionReader 具体是怎么做的呢?XmlBeanDefinitionReader 做了如下几件事情:
- 将 xml 配置文件加载到内存中
- 获取根标签下所有的标签
- 遍历获取到的标签列表,并从标签中读取 id,class 属性
- 创建 BeanDefinition 对象,并将刚刚读取到的 id,class 属性值保存到对象中
- 遍历标签下的标签,从中读取属性值,并保持在 BeanDefinition 对象中
- 将
键值对缓存在 Map 中,留作后用 - 重复3、4、5、6步,直至解析结束
上面的解析步骤并不复杂,实现起来也不难,就是解析 xml 而已,这里就不过多叙述了。
2.4 注册 BeanPostProcessor
BeanPostProcessor 接口是 Spring 对外拓展的接口之一,其主要用途提供一个机会,让开发人员能够插手 bean 的实例化过程。通过实现这个接口,我们就可在 bean 实例化时,对bean 进行一些处理。比如,我们所熟悉的 AOP 就是在这里将切面逻辑织入相关 bean 中的。正是因为有了 BeanPostProcessor 接口作为桥梁,才使得 AOP 可以和 IOC 容器产生联系。关于这一点,我将会在后续章节详细说明。
接下来说说 BeanFactory 是怎么注册 BeanPostProcessor 相关实现类的。
XmlBeanDefinitionReader 在完成解析工作后,BeanFactory 会将它解析得到的
- 根据 BeanDefinition 记录的信息,寻找所有实现了 BeanPostProcessor 接口的类。
- 实例化 BeanPostProcessor 接口的实现类
- 将实例化好的对象放入 List中
- 重复2、3步,直至所有的实现类完成注册
上面简述了 BeanPostProcessor 接口的用途以及注册的过程。BeanPostProcessor 是一个比较常用接口,相信大家都很熟悉了,这里就不过多叙述了。
2.5 getBean 过程解析
在完成了 xml 的解析、BeanDefinition 的注册以及 BeanPostProcessor 的注册过程后。BeanFactory 初始化的工作算是结束了,此时 BeanFactory 处于就绪状态,等待外部程序的调用。
外部程序一般都是通过调用 BeanFactory 的 getBean(String name) 方法来获取容器中的 bean。BeanFactory 具有延迟实例化 bean 的特性,也就是等外部程序需要的时候,才实例化相关的 bean。这样做的好处是比较显而易见的,第一是提高了 BeanFactory 的初始化速度,第二是节省了内存资源。下面我们就来详细说说 bean 的实例化过程:
图3 Spring bean实例化过程
上图是一个完整的 Spring bean 实例化过程图。在我的仿写项目中,没有做的这么复杂,简化了 bean 实例化的过程,如下:
图4 toy-spring bean实例化过程
接下来我将按照简化后的 bean 实例化过程介绍,如果想了解完整的 bean 实例化过程,可以参考我的另一篇文章:Spring bean的生命流程。简化后的实例化流程如下:
- 实例化 bean 对象,类似于 new XXObject()
- 将配置文件中配置的属性填充到刚刚创建的 bean 对象中
- 检查 bean 对象是否实现了 Aware 一类的接口,如果实现了则把相应的依赖设置到 bean 对象中。toy-spring 目前仅对 BeanFactoryAware 接口实现类提供了支持
- 调用 BeanPostProcessor 前置处理方法,即 postProcessBeforeInitialization(Object bean, String beanName)
- 调用 BeanPostProcessor 后置处理方法,即 postProcessAfterInitialization(Object bean, String beanName)
- bean 对象处于就绪状态,可以使用了
上面 6 步流程并不复杂,源码实现的也较为简单,这里就不在贴代码说明了。大家如果想了解细节,可以去 github 上下载 toy-spring 源码阅读。
3. 实现 AOP
3.1 AOP 原理
AOP 是基于动态代理模式实现的,具体实现上可以基于 JDK 动态代理或者 Cglib 动态代理。其中 JDK 动态代理只能代理实现了接口的对象,而 Cglib 动态代理则无此限制。所以在为没有实现接口的对象生成代理时,只能使用 Cglib。在 toy-spring 项目中,暂时只实现了基于 JDK 动态代理的代理对象生成器。
关于 AOP 原理这里就不多说了,下面说说 toy-spring 中 AOP 的实现步骤。还是像上面一样,先列流程:
- AOP 逻辑介入 BeanFactory 实例化 bean 的过程
- 根据 Pointcut 定义的匹配规则,判断当前正在实例化的 bean 是否符合规则
- 如果符合,代理生成器将切面逻辑 Advice 织入 bean 相关方法中,并为目标 bean 生成代理对象
- 将生成的 bean 的代理对象返回给 BeanFactory 容器,到此,AOP 逻辑执行结束
对于上面的4步流程,熟悉 Spring AOP 的朋友应该能很容易理解。如果有朋友不理解也没关系,在后续章节,我会详细介绍相关流程的具体实现。
3.2 基于 JDK 动态代理的 AOP 实现
本节说说基于 JDK 动态代理的代理对象生成器具体实现。在 toy-spring 项目中,代理对象生成器的逻辑主要写在了 JdkDynamicAopProxy 类中,这个类的有两个方法,其中 getProxy 方法用于生成代理对象。invoke 方法是 InvocationHandler 接口的具体实现,包含了将通知(Advice)织入相关方法中,是3.1节所列流程中第3步流程的具体实现。好了,接下来,对着源码讲解 JdkDynamicAopProxy:
JdkDynamicAopProxy 实现代码:
public abstract class AbstractAopProxy implements AopProxy { protected AdvisedSupport advised; public AbstractAopProxy(AdvisedSupport advised) { this.advised = advised; }}/ 基于 JDK 动态代理的代理对象生成器 Created by code4wt on 17/8/16. */final public class JdkDynamicAopProxy extends AbstractAopProxy implements InvocationHandler { public JdkDynamicAopProxy(AdvisedSupport advised) { super(advised); } / 为目标 bean 生成代理对象 @return bean 的代理对象 / @Override public Object getProxy() { return Proxy.newProxyInstance(getClass().getClassLoader(), advised.getTargetSource().getInterfaces(), this); } /** InvocationHandler 接口中的 invoke 方法具体实现,封装了具体的代理逻辑 @param proxy @param method @param args @return 代理方法或原方法的返回值 @throws Throwable / @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodMatcher methodMatcher = advised.getMethodMatcher(); // 1. 使用方法匹配器 methodMatcher 测试 bean 中原始方法 method 是否符合匹配规则 if (methodMatcher != null && methodMatcher.matchers(method, advised.getTargetSource().getTargetClass())) { // 获取 Advice。MethodInterceptor 的父接口继承了 Advice MethodInterceptor methodInterceptor = advised.getMethodInterceptor(); / 2. 将 bean 的原始方法 method 封装在 MethodInvocation 接口实现类对象中, 并把生成的对象作为参数传给 Adivce 实现类对象,执行通知逻辑 / return methodInterceptor.invoke( new ReflectiveMethodInvocation(advised.getTargetSource().getTarget(), method, args)); } else { // 2. 当前 method 不符合匹配规则,直接调用 bean 的原始方法 method return method.invoke(advised.getTargetSource().getTarget(), args); } }} |
---|
上面贴的代码,已经对 JdkDynamicAopProxy 实现代码进行了逐行介解释,这里不再多说。下面用个流程图对通知织入逻辑进行总结:
图5 toy-spring AOP 通知织入流程图
最后对 JdkDynamicAopProxy 进行简单的测试,测试代码及结果如下
测试类:
public class LogInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation invocation) throws Throwable { System.out.println(invocation.getMethod().getName() + “ method start”); Object obj= invocation.proceed(); System.out.println(invocation.getMethod().getName() + “ method end”); return obj; }}public class JdkDynamicAopProxyTest { @Test public void getProxy() throws Exception { System.out.println(“————— no proxy —————“); HelloService helloService = new HelloServiceImpl(); helloService.sayHelloWorld(); System.out.println(“\n—————- proxy —————-“); AdvisedSupport advisedSupport = new AdvisedSupport(); advisedSupport.setMethodInterceptor(new LogInterceptor()); TargetSource targetSource = new TargetSource( helloService, HelloServiceImpl.class, HelloServiceImpl.class.getInterfaces()); advisedSupport.setTargetSource(targetSource); advisedSupport.setMethodMatcher((Method method, Class beanClass) -> true); helloService = (HelloService) new JdkDynamicAopProxy(advisedSupport).getProxy(); helloService.sayHelloWorld(); }} |
---|
测试结果:
为了控制文章篇幅,上面代码中用到的其他辅助类,这里就不贴出来了,想看的朋友可以到 github 上下载源码。
3.3 AOP 与 IOC 协作
上一节介绍了3.1节所列流程中第3步流程的具体实现,这一节则会介绍1、2、4步流程的具体实现。在介绍之前,还要再次提一下 BeanPostProcessor接口。在之前2.4节 注册 BeanPostProcessor 中我已经介绍过 BeanPostProcessor 的作用,也说到了 AOP 是通过 BeanPostProcessor 接口与 IOC 产生联系的。不过2.4节,只是蜻蜓点水提了一下,没有详细展开说明。在本节中,我将详细讲解 toy-spring 项目中 AOP 和 IOC 是怎样被整合到一起的。
Spring 从2.0版本开始集成 AspectJ,通过集成 AspectJ,也使得 Spring AOP 的功能得到了很大的增强。我们在平时开发中,很多时候是使用基于 AspectJ 表达式及其他配置来实现切面功能。所以我在编写 toy-spring 项目时,也在项目中简单集成了 AspectJ。通过集成 AspectJ,使得 toy-spring AOP 可以基于 AspectJ 表达式完成复杂的匹配逻辑。接下来就让我们看看袖珍版 Spring AOP 是怎样实现的吧。
在 toy-spring 中,AOP 和 IOC 产生联系的具体实现类是 AspectJAwareAdvisorAutoProxyCreator(下面简称 AutoProxyCreator),这个类实现了 BeanPostProcessor 和 BeanFactoryAware 接口。BeanFactory 在注册 BeanPostProcessor 接口相关实现类的阶段,会将其本身注入到 AutoProxyCreator 中,为后面 AOP 给 bean 生成代理对象做准备。BeanFactory 初始化结束后,AOP 与 IOC 桥梁类 AutoProxyCreator 也完成了实例化,并被缓存在 BeanFactory 中,静待 BeanFactory 实例化 bean。当外部产生调用,BeanFactory 开始实例化 bean 时。AutoProxyCreator 就开始悄悄的工作了,工作细节如下:
- 从 BeanFactory 查找实现了 PointcutAdvisor 接口的切面对象,切面对象中包含了实现 Pointcut 和 Advice 接口的对象。
- 使用 Pointcut 中的表达式对象匹配当前 bean 对象。如果匹配成功,进行下一步。否则终止逻辑,返回 bean。
- JdkDynamicAopProxy 对象为匹配到的 bean 生成代理对象,并将代理对象返回给 BeanFactory。
经过上面3步,AutoProxyCreator 就悄无声息的把原来的 bean 替换为代理对象了,是不是有种偷天换日的感觉。最后把 toy-spring AOP 剩余的实现代码贴出来:
public class AspectJAwareAdvisorAutoProxyCreator implements BeanPostProcessor, BeanFactoryAware { private XmlBeanFactory xmlBeanFactory; @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws Exception { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws Exception { / 这里两个 if 判断很有必要,如果删除将会使程序进入死循环状态, 最终导致 StackOverflowError 错误发生 */ if (bean instanceof AspectJExpressionPointcutAdvisor) { return bean; } if (bean instanceof MethodInterceptor) { return bean; } // 1. 从 BeanFactory 查找 AspectJExpressionPointcutAdvisor 类型的对象 List |
---|
ProxyFactory 实现代码:
/* AopProxy 实现类的工厂类 */public class ProxyFactory extends AdvisedSupport implements AopProxy { @Override public Object getProxy() { return createAopProxy().getProxy(); } private AopProxy createAopProxy() { return new JdkDynamicAopProxy(this); }} |
---|
测试类:
public class XmlBeanFactoryTest { @Test public void getBean() throws Exception { System.out.println(“————- AOP test —————“); String location = getClass().getClassLoader().getResource(“spring.xml”).getFile(); XmlBeanFactory bf = new XmlBeanFactory(location); HelloService helloService = (HelloService) bf.getBean(“helloService”); helloService.sayHelloWorld(); }} |
---|
4. 写在最后
到此,本文的主要内容写完了。如果你耐心的读完了文章,并感觉不错的话,欢迎猛点赞和收藏按钮。这篇文章花了我一天的时间,写的实在有点累,也深感认真写博客的不易。本篇文章与 仿照 Spring 实现简单的 IOC 和 AOP - 上篇,Spring bean的生命流程 共三篇文章,对 Spring IOC 和 AOP 的实现原理进行了较为详细的结束。也是通过认真编写这三篇文章,使得我对 Spring 框架原理有了更进一步的认识。当然限于我的经验和能力,以上三篇文章中可能存在着一些错误。如果这些错误给大家造成了干扰,我表示很抱歉。所以文章若有疏漏不妥之处,还请指出来,如果能不吝赐教,那就更好了。好了,最后感谢大家耐心读完我的文章,下次再见。
参考:
- 《Spring揭秘》
- tiny-spring
Spring MVC 原理探秘 - 一个请求的旅行过程
1.简介
在前面的文章中,我较为详细的分析了 Spring IOC 和 AOP 部分的源码,并写成了文章。为了让我的 Spring 源码分析系列文章更为丰富一些,所以从本篇文章开始,我将来向大家介绍一下 Spring MVC 的一些原理。在本篇文章中,你将会了解到 Spring MVC 处理请求的过程。同时,你也会了解到 Servlet 相关的知识。以及 Spring MVC 的核心 DispatcherServlet 类的源码分析。在掌握以上内容后,相信大家会对 Spring MVC 的原理有更深的认识。
如果大家对上面介绍的知识点感兴趣的话,那下面不妨和我一起来去探索 Spring MVC 的原理。Let`s Go。
2.一个请求的旅行过程
在探索更深层次的原理之前,我们先来了解一下 Spring MVC 是怎么处理请求的。弄懂了这个流程后,才能更好的理解具体的源码。这里我把 Spring MVC 处理请求的流程图画了出来,一起看一下吧:
如上,每一个重要的步骤上面都有编号。我先来简单分析一下上面的流程,然后再向大家介绍图中出现的一些组件。我们从第一步开始,首先,用户的浏览器发出了一个请求,这个请求经过互联网到达了我们的服务器。Servlet 容器首先接待了这个请求,并将该请求委托给 DispatcherServlet 进行处理。接着 DispatcherServlet 将该请求传给了处理器映射组件 HandlerMapping,并获取到适合该请求的拦截器和处理器。在获取到处理器后,DispatcherServlet 还不能直接调用处理器的逻辑,需要进行对处理器进行适配。处理器适配成功后,DispatcherServlet 通过处理器适配器 HandlerAdapter 调用处理器的逻辑,并获取返回值 ModelAndView。之后,DispatcherServlet 需要根据 ModelAndView 解析视图。解析视图的工作由 ViewResolver 完成,若能解析成功,ViewResolver 会返回相应的视图对象 View。在获取到具体的 View 对象后,最后一步要做的事情就是由 View 渲染视图,并将渲染结果返回给用户。
以上就是 Spring MVC 处理请求的全过程,上面的流程进行了一定的简化,比如拦截器的执行时机就没说。不过这并不影响大家对主过程的理解。下来来简单介绍一下图中出现的一些组件:
组件 | 说明 |
---|---|
DispatcherServlet | Spring MVC 的核心组件,是请求的入口,负责协调各个组件工作 |
HandlerMapping | 内部维护了一些 <访问路径, 处理器> 映射,负责为请求找到合适的处理器 |
HandlerAdapter | 处理器的适配器。Spring 中的处理器的实现多变,比如用户处理器可以实现 Controller 接口,也可以用 @RequestMapping 注解将方法作为一个处理器等,这就导致 Spring 不止到怎么调用用户的处理器逻辑。所以这里需要一个处理器适配器,由处理器适配器去调用处理器的逻辑 |
ViewResolver | 视图解析器的用途不难理解,用于将视图名称解析为视图对象 View。 |
View | 视图对象用于将模板渲染成 html 或其他类型的文件。比如 InternalResourceView 可将 jsp 渲染成 html。 |
从上面的流程中可以看出,Spring MVC 对各个组件的职责划分的比较清晰。DispatcherServlet 负责协调,其他组件则各自做分内之事,互不干扰。经过这样的职责划分,代码会便于维护。同时对于源码阅读者来说,也会很友好。可以降低理解源码的难度,使大家能够快速理清主逻辑。这一点值得我们学习。
3.知其然,更要知其所以然
3.1 追根溯源之 Servlet
本章要向大家介绍一下 Servlet,为什么要介绍 Servlet 呢?原因不难理解,Spring MVC 是基于 Servlet 实现的。所以要分析 Spring MVC,首先应追根溯源,弄懂 Servlet。Servlet 是 J2EE 规范之一,在遵守该规范的前提下,我们可将 Web 应用部署在 Servlet 容器下。这样做的好处是什么呢?我觉得可使开发者聚焦业务逻辑,而不用去关心 HTTP 协议方面的事情。比如,普通的 HTTP 请求就是一段有格式的文本,服务器需要去解析这段文本才能知道用户请求的内容是什么。比如我对个人网站的 80 端口抓包,然后获取到的 HTTP 请求头如下:
如果我们为了写一个 Web 应用,还要去解析 HTTP 协议相关的内容,那会增加很多工作量。有兴趣的朋友可以考虑使用 Java socket 编写实现一个 HTTP 服务器,体验一下解析部分 HTTP 协议的过程。也可以参考我之前写的文章 - 基于 Java NIO 实现简单的 HTTP 服务器。
如果我们写的 Web 应用不大,不夸张的说,项目中对 HTTP 提供支持的代码会比业务代码还要多,这岂不是得不偿失。当然,在现实中,有现成的框架可用,并不需要自己造轮子。如果我们基于 Servlet 规范实现 Web 应用的话,HTTP 协议的处理过程就不需要我们参与了。这些工作交给 Servlet 容器就行了,我们只需要关心业务逻辑怎么实现即可。
下面,我们先来看看 Servlet 接口及其实现类结构,然后再进行更进一步的说明。
如上图,我们接下来按照从上到下顺序进行分析。先来看看最顶层的两个接口是怎么定义的。
3.1.1 Servlet 与 ServletConfig
先来看看 Servlet 接口的定义,如下:
public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy();} |
---|
init 方法会在容器启动时由容器调用,也可能会在 Servlet 第一次被使用时调用,调用时机取决 load-on-start 的配置。容器调用 init 方法时,会向其传入一个 ServletConfig 参数。ServletConfig 是什么呢?顾名思义,ServletConfig 是一个和 Servlet 配置相关的接口。举个例子说明一下,我们在配置 Spring MVC 的 DispatcherServlet 时,会通过 ServletConfig 将配置文件的位置告知 DispatcherServlet。比如:
如上,标签内的配置信息最终会被放入 ServletConfig 实现类对象中。DispatcherServlet 通过 ServletConfig 接口中的方法,就能获取到 contextConfigLocation 对应的值。
Servlet 中的 service 方法用于处理请求。当然,一般情况下我们不会直接实现 Servlet 接口,通常是通过继承 HttpServlet 抽象类编写业务逻辑的。Servlet 中接口不多,也不难理解,这里就不多说了。下面我们来看看 ServletConfig 接口定义,如下:
public interface ServletConfig { public String getServletName(); public ServletContext getServletContext(); public String getInitParameter(String name); public Enumeration |
---|
先来看看 getServletName 方法,该方法用于获取 servlet 名称,也就是标签中配置的内容。getServletContext 方法用于获取 Servlet 上下文。如果说一个 ServletConfig 对应一个 Servlet,那么一个 ServletContext 则是对应所有的 Servlet。ServletContext 代表当前的 Web 应用,可用于记录一些全局变量,当然它的功能不局限于记录变量。我们可通过标签向 ServletContext 中配置信息,比如在配置 Spring 监听器(ContextLoaderListener)时,就可以通过该标签配置 contextConfigLocation。如下:
关于 ServletContext 就先说这么多了,继续介绍 ServletConfig 中的其他方法。getInitParameter 方法用于获取标签中配置的参数值,getInitParameterNames 则是获取所有配置的名称集合,这两个方法用途都不难理解。
以上是 Servlet 与 ServletConfig 两个接口的说明,比较简单。说完这两个接口,我们继续往下看,接下来是 GenericServlet。
3.1.2 GenericServlet
GenericServlet 实现了 Servlet 和 ServletConfig 两个接口,为这两个接口中的部分方法提供了简单的实现。比如该类实现了 Servlet 接口中的 void init(ServletConfig) 方法,并在方法体内调用了内部提供了一个无参的 init 方法,子类可覆盖该无参 init 方法。除此之外,GenericServlet 还实现了 ServletConfig 接口中的 getInitParameter 方法,用户可直接调用该方法获取到配置信息。而不用先获取 ServletConfig,然后再调用 ServletConfig 的 getInitParameter 方法获取。下面我们来看看 GenericServlet 部分方法的源码:
public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable { // 省略部分代码 private transient ServletConfig config; public GenericServlet() { } / 有参 init 方法 */ public void init(ServletConfig config) throws ServletException { this.config = config; // 调用内部定义的无参 init 方法 this.init(); } / 无参 init 方法,子类可覆盖该方法 / public void init() throws ServletException { } /** 未给 service 方法提供具体的实现 / public abstract void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public void destroy() { } /* 通过 getInitParameter 可直接从 ServletConfig 实现类中获取配置信息 / public String getInitParameter(String name) { ServletConfig sc = getServletConfig(); if (sc == null) { throw new IllegalStateException( lStrings.getString(“err.servlet_config_not_initialized”)); } return sc.getInitParameter(name); } public ServletConfig getServletConfig() { return config; } // 省略部分代码} |
---|
如上,GenericServlet 代码比较简单,配合着我写注释,很容易看懂。
GenericServlet 是一个协议无关的 servlet,是一个比较原始的实现,通常我们不会直接继承该类。一般情况下,我们都是继承 GenericServlet 的子类 HttpServlet,该类是一个和 HTTP 协议相关的 Servlet。那下面我们来看一下这个类。
3.1.3 HttpServlet
HttpServlet,从名字上就可看出,这个类是和 HTTP 协议相关。该类的关注点在于怎么处理 HTTP 请求,比如其定义了 doGet 方法处理 GET 类型的请求,定义了 doPost 方法处理 POST 类型的请求等。我们若需要基于 Servlet 写 Web 应用,应继承该类,并覆盖指定的方法。doGet 和 doPost 等方法并不是处理的入口方法,所以这些方法需要由其他方法调用才行。其他方法是哪个方法呢?当然是 service 方法了。下面我们看一下这个方法的实现。如下:
@Overridepublic void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) { throw new ServletException(“non-HTTP request or response”); } request = (HttpServletRequest) req; response = (HttpServletResponse) res; // 调用重载方法,该重载方法接受 HttpServletRequest 和 HttpServletResponse 类型的参数 service(request, response);}protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); // 处理 GET 请求 if (method.equals(METHOD_GET)) { long lastModified = getLastModified(req); if (lastModified == -1) { // 调用 doGet 方法 doGet(req, resp); } else { long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE); if (ifModifiedSince < lastModified) { maybeSetLastModified(resp, lastModified); doGet(req, resp); } else { resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED); } } // 处理 HEAD 请求 } else if (method.equals(METHOD_HEAD)) { long lastModified = getLastModified(req); maybeSetLastModified(resp, lastModified); doHead(req, resp); // 处理 POST 请求 } else if (method.equals(METHOD_POST)) { // 调用 doPost 方法 doPost(req, resp); } else if (method.equals(METHOD_PUT)) { doPut(req, resp); } else if (method.equals(METHOD_DELETE)) { doDelete(req, resp); } else if (method.equals(METHOD_OPTIONS)) { doOptions(req,resp); } else if (method.equals(METHOD_TRACE)) { doTrace(req,resp); } else { String errMsg = lStrings.getString(“http.method_not_implemented”); Object[] errArgs = new Object[1]; errArgs[0] = method; errMsg = MessageFormat.format(errMsg, errArgs); resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg); }} |
---|
如上,第一个 service 方法覆盖父类中的抽象方法,并没什么太多逻辑。所有的逻辑集中在第二个 service 方法中,该方法根据请求类型分发请求。我们可以根据需要覆盖指定的处理方法。
以上所述只是 Servlet 规范中的一部分内容,这些内容是和本文相关的内容。对于 Servlet 规范中的其他内容,大家有兴趣可以自己去探索。好了,关于 Servlet 方面的内容,这里先说这么多。
3.2 DispatcherServlet 族谱
我在前面说到,DispatcherServlet 是 Spring MVC 的核心。所以在分析这个类的源码前,我们有必要了解一下它的族谱,也就是继承关系图。如下:
如上图,红色框是 Servlet 中的接口和类,蓝色框中则是 Spring 中的接口和类。关于 Servlet 内容前面已经说过,下面来简单介绍一下蓝色框中的接口和类,我们从最顶层的接口开始。
● Aware
在 Spring 中,Aware 类型的接口用于向 Spring “索要”一些框架中的信息。比如当某个 bean 实现了 ApplicationContextAware 接口时,Spring 在运行时会将当前的 ApplicationContext 实例通过接口方法 setApplicationContext 传给该 bean。下面举个例子说明,这里我写一个 SystemInfo API,通过该 API 返回一些系统信息。代码如下:
@RestController@RequestMapping(“/systeminfo”)public class SystemInfo implements ApplicationContextAware, EnvironmentAware { private ApplicationContext applicationContext; private Environment environment; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { System.out.println(applicationContext.getClass()); this.applicationContext = applicationContext; } @Override public void setEnvironment(Environment environment) { this.environment = environment; } @RequestMapping(“/env”) public String environment() { StandardServletEnvironment sse = (StandardServletEnvironment) environment; Map |
---|
如上,SystemInfo 分别实现了 ApplicationContextAware 和 EnvironmentAware 接口,因此它可以在运行时获取到 ApplicationContext 和 Environment 实例。下面我们调一下接口看看结果吧:
如上,我们通过接口拿到了环境变量、配置信息以及容器中所有 bean 的数据。这说明,Spring 在运行时向 SystemInfo 中注入了 ApplicationContext 和 Environment 实例。
● EnvironmentCapable
EnvironmentCapable 仅包含一个方法定义 getEnvironment,通过该方法可以获取到环境变量对象。我们可以将 EnvironmentCapable 和 EnvironmentAware 接口配合使用,比如下面的实例:
public class EnvironmentHolder implements EnvironmentCapable, EnvironmentAware { private Environment environment; @Override public void setEnvironment(Environment environment) { this.environment = environment; } @Override public Environment getEnvironment() { return environment; }} |
---|
● HttpServletBean
HttpServletBean 是 HttpServlet 抽象类的简单拓展。HttpServletBean 覆写了父类中的无参 init 方法,并在该方法中将 ServletConfig 里的配置信息设置到子类对象中,比如 DispatcherServlet。
● FrameworkServlet
FrameworkServlet 是 Spring Web 框架中的一个基础类,该类会在初始化时创建一个容器。同时该类覆写了 doGet、doPost 等方法,并将所有类型的请求委托给 doService 方法去处理。doService 是一个抽象方法,需要子类实现。
● DispatcherServlet
DispatcherServlet 主要的职责相信大家都比较清楚了,即协调各个组件工作。除此之外,DispatcherServlet 还有一个重要的事情要做,即初始化各种组件,比如 HandlerMapping、HandlerAdapter 等。
3.3 DispatcherServlet 源码简析
在第二章中,我们知道了一个 HTTP 请求是怎么样被 DispatcherServlet 处理的。本节,我们从源码的角度对第二章的内容进行补充说明。这里,我们直入主题,直接分析 DispatcherServlet 中的 doDispatch 方法。这里我把请求的处理流程图再贴一遍,大家可以对着流程图阅读源码。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // 获取可处理当前请求的处理器 Handler,对应流程图中的步骤② mappedHandler = getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { noHandlerFound(processedRequest, response); return; } // 获取可执行处理器逻辑的适配器 HandlerAdapter,对应步骤③ HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // 处理 last-modified 消息头 String method = request.getMethod(); boolean isGet = “GET”.equals(method); if (isGet || “HEAD”.equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (logger.isDebugEnabled()) { logger.debug(“Last-Modified value for [“ + getRequestUri(request) + “] is: “ + lastModified); } if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } // 执行拦截器 preHandle 方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // 调用处理器逻辑,对应步骤④ mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } // 如果 controller 未返回 view 名称,这里生成默认的 view 名称 applyDefaultViewName(processedRequest, mv); // 执行拦截器 preHandle 方法 mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { dispatchException = ex; } catch (Throwable err) { dispatchException = new NestedServletException(“Handler dispatch failed”, err); } // 解析并渲染视图 processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException(“Handler processing failed”, err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } }}private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception { boolean errorView = false; if (exception != null) { if (exception instanceof ModelAndViewDefiningException) { logger.debug(“ModelAndViewDefiningException encountered”, exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } if (mv != null && !mv.wasCleared()) { // 渲染视图 render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } else { if (logger.isDebugEnabled()) {… } if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { return; } if (mappedHandler != null) { mappedHandler.triggerAfterCompletion(request, response, null); }}protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { Locale locale = this.localeResolver.resolveLocale(request); response.setLocale(locale); View view; / 若 mv 中的 view 是 String 类型,即处理器返回的是模板名称, 这里将其解析为具体的 View 对象 / if (mv.isReference()) { // 解析视图,对应步骤⑤ view = resolveViewName(mv.getViewName(), mv.getModelInternal(), locale, request); if (view == null) { throw new ServletException(“Could not resolve view with name ‘“ + mv.getViewName() + “‘ in servlet with name ‘“ + getServletName() + “‘“); } } else { view = mv.getView(); if (view == null) { throw new ServletException(“ModelAndView [“ + mv + “] neither contains a view name nor a “ + “View object in servlet with name ‘“ + getServletName() + “‘“); } } if (logger.isDebugEnabled()) {…} try { if (mv.getStatus() != null) { response.setStatus(mv.getStatus().value()); } // 渲染视图,并将结果返回给用户。对应步骤⑥和⑦ view.render(mv.getModelInternal(), request, response); } catch (Exception ex) { if (logger.isDebugEnabled()) {…} throw ex; }} |
---|
以上就是 doDispatch 方法的分析过程,我已经做了较为详细的注释,这里就不多说了。需要说明的是,以上只是进行了简单分析,并没有深入分析每个方法调用。大家若有兴趣,可以自己去分析一下 doDispatch 所调用的一些方法,比如 getHandler 和 getHandlerAdapter,这两个方法比较简单。从我最近所分析的源码来看,我个人觉得处理器适配器 RequestMappingHandlerAdapter 应该是 Spring MVC 中最为复杂的一个类。该类用于对 @RequestMapping 注解的方法进行适配。该类的逻辑我暂时没看懂,就不多说了,十分尴尬。关于该类比较详细的分析,大家可以参考《看透Spring MVC》一书。
4.总结
到此,本篇文章的主体内容就说完了。本篇文章从一个请求的旅行过程进行分析,并在分析的过程中补充了 Servlet 和 DispatcherServlet 方面的知识。在最后,从源码的角度分析了 DispatcherServlet 处理请求的过程。总的来算,算是做到了循序渐进。当然,限于个人能力,以上内容可能会有一些讲的不好的地方,这里请大家见谅。同时,也希望大家多多指教。
好了,本篇文章先到这里。谢谢大家的阅读。
参考
Spring MVC 原理探秘 - 容器的创建过程
1.简介
在上一篇文章中,我向大家介绍了 Spring MVC 是如何处理 HTTP 请求的。Spring MVC 可对外提供服务时,说明其已经处于了就绪状态。再次之前,Spring MVC 需要进行一系列的初始化操作。正所谓兵马未动,粮草先行。这些操作包括创建容器,加载 DispatcherServlet 中用到的各种组件等。本篇文章就来和大家讨论一下这些初始化操作中的容器创建操作,容器的创建是其他一些初始化过程的基础。那其他的就不多说了,我们直入主题吧。
2.容器的创建过程
一般情况下,我们会在一个 Web 应用中配置两个容器。一个容器用于加载 Web 层的类,比如我们的接口 Controller、HandlerMapping、ViewResolver 等。在本文中,我们把这个容器叫做 web 容器。另一个容器用于加载业务逻辑相关的类,比如 service、dao 层的一些类。在本文中,我们把这个容器叫做业务容器。在容器初始化的过程中,业务容器会先于 web 容器进行初始化。web 容器初始化时,会将业务容器作为父容器。这样做的原因是,web 容器中的一些 bean 会依赖于业务容器中的 bean。比如我们的 controller 层接口通常会依赖 service 层的业务逻辑类。下面举个例子进行说明:
如上,我们将 dao 层的类配置在 application-dao.xml 文件中,将 service 层的类配置在 application-service.xml 文件中。然后我们将这两个配置文件通过标签导入到 application.xml 文件中。此时,我们可以让业务容器去加载 application.xml 配置文件即可。另一方面,我们将 Web 相关的配置放在 application-web.xml 文件中,并将该文件交给 Web 容器去加载。
这里我们把配置文件进行分层,结构上看起来清晰了很多,也便于维护。这个其实和代码分层是一个道理,如果我们把所有的代码都放在同一个包下,那看起来会多难受啊。同理,我们用业务容器和 Web 容器去加载不同的类也是一种分层的体现吧。当然,如果应用比较简单,仅用 Web 容器去加载所有的类也不是不可以。
2.1 业务容器的创建过程
前面说了一些背景知识作为铺垫,那下面我们开始分析容器的创建过程吧。按照创建顺序,我们先来分析业务容器的创建过程。业务容器的创建入口是 ContextLoaderListener 的 contextInitialized 方法。顾名思义,ContextLoaderListener 是用来监听 ServletContext 加载事件的。当 ServletContext 被加载后,监听器的 contextInitialized 方法就会被 Servlet 容器调用。ContextLoaderListener Spring 框架提供的,它的配置方法如下:
如上,ContextLoaderListener 可通过 ServletContext 获取到 contextConfigLocation 配置。这样,业务容器就可以加载 application.xml 配置文件了。那下面我们来分析一下 ContextLoaderListener 的源码吧。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener { // 省略部分代码 @Override public void contextInitialized(ServletContextEvent event) { // 初始化 WebApplicationContext initWebApplicationContext(event.getServletContext()); }}public WebApplicationContext initWebApplicationContext(ServletContext servletContext) { / 如果 ServletContext 中 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 属性值 不为空时,表明有其他监听器设置了这个属性。Spring 认为不能替换掉别的监听器设置 的属性值,所以这里抛出异常。 / if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) { throw new IllegalStateException( “Cannot initialize context because there is already a root application context present - “ + “check whether you have multiple ContextLoader definitions in your web.xml!”); } Log logger = LogFactory.getLog(ContextLoader.class); servletContext.log(“Initializing Spring root WebApplicationContext”); if (logger.isInfoEnabled()) {…} long startTime = System.currentTimeMillis(); try { if (this.context == null) { // 创建 WebApplicationContext this.context = createWebApplicationContext(servletContext); } if (this.context instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context; if (!cwac.isActive()) { if (cwac.getParent() == null) { / 加载父 ApplicationContext,一般情况下,业务容器不会有父容器, 除非进行配置 / ApplicationContext parent = loadParentContext(servletContext); cwac.setParent(parent); } // 配置并刷新 WebApplicationContext configureAndRefreshWebApplicationContext(cwac, servletContext); } } // 设置 ApplicationContext 到 servletContext 中 servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context); ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl == ContextLoader.class.getClassLoader()) { currentContext = this.context; } else if (ccl != null) { currentContextPerThread.put(ccl, this.context); } if (logger.isDebugEnabled()) {…} if (logger.isInfoEnabled()) {…} return this.context; } catch (RuntimeException ex) { logger.error(“Context initialization failed”, ex); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex); throw ex; } catch (Error err) { logger.error(“Context initialization failed”, err); servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err); throw err; }} |
---|
如上,我们看一下上面的创建过程。首先 Spring 会检测 ServletContext 中 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 属性有没有被设置,若被设置过,则抛出异常。若未设置,则调用 createWebApplicationContext 方法创建容器。创建好后,再调用 configureAndRefreshWebApplicationContext 方法配置并刷新容器。最后,调用 setAttribute 方法将容器设置到 ServletContext 中。经过以上几步,整个创建流程就结束了。流程并不复杂,可简单总结为创建容器 → 配置并刷新容器 → 设置容器到 ServletContext 中
。这三步流程中,最后一步就不进行分析,接下来分析一下第一步和第二步流程对应的源码。如下:
protected WebApplicationContext createWebApplicationContext(ServletContext sc) { // 判断创建什么类型的容器,默认类型为 XmlWebApplicationContext Class<?> contextClass = determineContextClass(sc); if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException(“Custom context class [“ + contextClass.getName() + “] is not of type [“ + ConfigurableWebApplicationContext.class.getName() + “]”); } // 通过反射创建容器 return (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);}protected Class<?> determineContextClass(ServletContext servletContext) { / 读取用户自定义配置,比如: |
---|
简单说一下 createWebApplicationContext 方法的流程,该方法首先会调用 determineContextClass 判断创建什么类型的容器,默认为 XmlWebApplicationContext。然后调用 instantiateClass 方法通过反射的方式创建容器实例。instantiateClass 方法就不跟进去分析了,大家可以自己去看看,比较简单。
继续往下分析,接下来分析一下 configureAndRefreshWebApplicationContext 方法的源码。如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // 从 ServletContext 中获取用户配置的 contextId 属性 String idParam = sc.getInitParameter(CONTEXT_ID_PARAM); if (idParam != null) { // 设置容器 id wac.setId(idParam); } else { // 用户未配置 contextId,则设置一个默认的容器 id wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath())); } } wac.setServletContext(sc); // 获取 contextConfigLocation 配置 String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM); if (configLocationParam != null) { wac.setConfigLocation(configLocationParam); } ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(sc, null); } customizeContext(sc, wac); // 刷新容器 wac.refresh();} |
---|
上面的源码不是很长,逻辑不是很复杂。下面简单总结 configureAndRefreshWebApplicationContext 方法主要做了事情,如下:
- 设置容器 id
- 获取 contextConfigLocation 配置,并设置到容器中
- 刷新容器
到此,关于业务容器的创建过程就分析完了,下面我们继续分析 Web 容器的创建过程。
2.2 Web 容器的创建过程
前面说了业务容器的创建过程,业务容器是通过 ContextLoaderListener。那 Web 容器是通过什么创建的呢?答案是通过 DispatcherServlet。我在上一篇文章介绍 HttpServletBean 抽象类时,说过该类覆写了父类 HttpServlet 中的 init 方法。这个方法就是创建 Web 容器的入口,那下面我们就从这个方法入手。如下:
// -☆- org.springframework.web.servlet.HttpServletBeanpublic final void init() throws ServletException { if (logger.isDebugEnabled()) {…} // 获取 ServletConfig 中的配置信息 PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties); if (!pvs.isEmpty()) { try { / 为当前对象(比如 DispatcherServlet 对象)创建一个 BeanWrapper, 方便读/写对象属性。 / BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this); ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext()); bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment())); initBeanWrapper(bw); // 设置配置信息到目标对象中 bw.setPropertyValues(pvs, true); } catch (BeansException ex) { if (logger.isErrorEnabled()) {…} throw ex; } } // 进行后续的初始化 initServletBean(); if (logger.isDebugEnabled()) {…}}protected void initServletBean() throws ServletException {} |
---|
上面的源码主要做的事情是将 ServletConfig 中的配置信息设置到 HttpServletBean 的子类对象中(比如 DispatcherServlet),我们并未从上面的源码中发现创建容器的痕迹。不过如果大家注意看源码的话,会发现 initServletBean 这个方法稍显奇怪,是个空方法。这个方法的访问级别为 protected,子类可进行覆盖。HttpServletBean 子类 FrameworkServlet 覆写了这个方法,下面我们到 FrameworkServlet 中探索一番。
// -☆- org.springframework.web.servlet.FrameworkServletprotected final void initServletBean() throws ServletException { getServletContext().log(“Initializing Spring FrameworkServlet ‘“ + getServletName() + “‘“); if (this.logger.isInfoEnabled()) {…} long startTime = System.currentTimeMillis(); try { // 初始化容器 this.webApplicationContext = initWebApplicationContext(); initFrameworkServlet(); } catch (ServletException ex) { this.logger.error(“Context initialization failed”, ex); throw ex; } catch (RuntimeException ex) { this.logger.error(“Context initialization failed”, ex); throw ex; } if (this.logger.isInfoEnabled()) {…}}protected WebApplicationContext initWebApplicationContext() { // 从 ServletContext 中获取容器,也就是 ContextLoaderListener 创建的容器 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; / 若下面的条件成立,则需要从外部设置 webApplicationContext。有两个途径可以设置 webApplicationContext,以 DispatcherServlet 为例: 1. 通过 DispatcherServlet 有参构造方法传入 WebApplicationContext 对象 2. 将 DispatcherServlet 配置到其他容器中,由其他容器通过 setApplicationContext 方法进行设置 途径1 可参考 AbstractDispatcherServletInitializer 中的 registerDispatcherServlet 方法源码。一般情况下,代码执行到此处, this.webApplicationContext 为 null,大家可自行调试进行验证。 */ if (this.webApplicationContext != null) { wac = this.webApplicationContext; if (wac instanceof ConfigurableWebApplicationContext) { ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac; if (!cwac.isActive()) { if (cwac.getParent() == null) { // 设置 rootContext 为父容器 cwac.setParent(rootContext); } // 配置并刷新容器 configureAndRefreshWebApplicationContext(cwac); } } } if (wac == null) { // 尝试从 ServletContext 中获取容器 wac = findWebApplicationContext(); } if (wac == null) { // 创建容器,并将 rootContext 作为父容器 wac = createWebApplicationContext(rootContext); } if (!this.refreshEventReceived) { onRefresh(wac); } if (this.publishContext) { String attrName = getServletContextAttributeName(); // 将创建好的容器设置到 ServletContext 中 getServletContext().setAttribute(attrName, wac); if (this.logger.isDebugEnabled()) {…} } return wac;}protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) { // 获取容器类型,默认为 XmlWebApplicationContext.class Class<?> contextClass = getContextClass(); if (this.logger.isDebugEnabled()) {…} if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) { throw new ApplicationContextException( “Fatal initialization error in servlet with name ‘“ + getServletName() + “‘: custom WebApplicationContext class [“ + contextClass.getName() + “] is not of type ConfigurableWebApplicationContext”); } // 通过反射实例化容器 ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass); wac.setEnvironment(getEnvironment()); wac.setParent(parent); wac.setConfigLocation(getContextConfigLocation()); // 配置并刷新容器 configureAndRefreshWebApplicationContext(wac); return wac;}protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) { if (ObjectUtils.identityToString(wac).equals(wac.getId())) { // 设置容器 id if (this.contextId != null) { wac.setId(this.contextId); } else { // 生成默认 id wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(getServletContext().getContextPath()) + ‘/‘ + getServletName()); } } wac.setServletContext(getServletContext()); wac.setServletConfig(getServletConfig()); wac.setNamespace(getNamespace()); wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener())); ConfigurableEnvironment env = wac.getEnvironment(); if (env instanceof ConfigurableWebEnvironment) { ((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig()); } // 后置处理,子类可以覆盖进行一些自定义操作。在 Spring MVC 未使用到,是个空方法。 postProcessWebApplicationContext(wac); applyInitializers(wac); // 刷新容器 wac.refresh();} |
---|
以上就是创建 Web 容器的源码,下面总结一下该容器创建的过程。如下:
- 从 ServletContext 中获取 ContextLoaderListener 创建的容器
- 若 this.webApplicationContext != null 条件成立,仅设置父容器和刷新容器即可
- 尝试从 ServletContext 中获取容器,若容器不为空,则无需执行步骤4
- 创建容器,并将 rootContext 作为父容器
- 设置容器到 ServletContext 中
到这里,关于 Web 容器的创建过程就讲完了。总的来说,Web 容器的创建过程和业务容器的创建过程大致相同,但是差异也是有的,不能忽略。
3.总结
本篇文章对 Spring MVC 两种容器的创建过程进行了较为详细的分析,总的来说两种容器的创建过程并不是很复杂。大家在分析这两种容器的创建过程时,看的不明白的地方,可以进行调试,这对于理解代码逻辑还是很有帮助的。当然阅读 Spring MVC 部分的源码最好有 Servlet 和 Spring IOC 容器方面的知识,这些是基础,Spring MVC 就是在这些基础上构建的。
限于个人能力,文章叙述有误,还望大家指明。也请多多指教,在这里说声谢谢。好了,本篇文章就到这里了。感谢大家的阅读。
参考
Spring 事务实现分析
1. Spring 事务简介
Spring 本身并不实现事务,Spring事务 的本质 还是 底层数据库 对事务的支持,没有 数据库 事务的支持,Spring事务就不会生效。
Spring 事务 提供一套抽象的事务管理,并且结合 Spring IOC 和 Spring AOP,简化了应用程序使用数据库事务,通过声明式事务,可以做到对应用程序无侵入的实现事务功能。例如 使用JDBC 操作数据库,想要使用事务的步骤为:
1、获取连接 Connection con = DriverManager.getConnection()
2、开启事务con.setAutoCommit(true/false);
3、执行CRUD
4、提交事务/回滚事务 con.commit() / con.rollback();
5、关闭连接 conn.close();
采用Spring 事务后,只需要 关注第3步的实现,其他的步骤 都是Spring 完成。
Spring事务的本质 其实就是 AOP 和 数据库事务,Spring 将数据库的事务操作提取为 切面,通过AOP的方式 增强 事务方法。
2. 事务传播行为
事务传播行为类型 | 说明 |
---|---|
PROPAGATION_REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。这是最常见的选择。 |
PROPAGATION_SUPPORTS | 支持当前事务,如果当前没有事务,就以非事务方式执行。 |
PROPAGATION_MANDATORY | 使用当前的事务,如果当前没有事务,就抛出异常。 |
PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 |
PROPAGATION_NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。 |
PROPAGATION_NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常。 |
PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。 |
3. 使用Spring事务
3.1. 通过 PlatformTransactionManager使用(不推荐)
@Test
public void test12(){
// 默认的事务定义
DefaultTransactionDefinition defaultTransactionDefinition = new DefaultTransactionDefinition();
//开启事务。
TransactionStatus transaction = transactionManager.getTransaction(defaultTransactionDefinition);
try {
Account account = new Account();
account.setUserName("wokalakala");
List<Account> accountList = accountMapper.queryAccountList(account);
}catch (Exception e){
//事务回滚
transactionManager.rollback(transaction);
}
//事务提交
transactionManager.commit(transaction);
}
3.2. 通过TransactionTemplate 使用事务
@Test
public void testTransactionTemplate(){
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
List<Account> accountList = transactionTemplate.execute(status -> {
Account account = new Account();
account.setUserName("wokalakala");
return accountMapper.queryAccountList(account);
});
}
3.3. 声明式事务
我们经常使用的 方式,通过AOP 实现,对应用程序 侵入较少,采用注解的方式比较简单方便,省去XML 繁琐的配置。
4. 组件介绍
4.1事务管理 PlatformTransactionManager
PlatformTransactionManager 是 Spring 事务结构中的核心接口,Spring并不直接管理事务,而是提供了多种事务管器,他们将事务管理的职责委托给Hibernate或者JTA等持久化机制所提供的相关平台框架的事务来实现.Spring事务管理器的接口是org.springframework.transaction.PlatformTransactionManager,通过这个接口,Spring为各个平台如JDBC、Hibernate等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。此接口的内容如下:
Public interface PlatformTransactionManager(){
// 由TransactionDefinition得到TransactionStatus对象
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
// 提交
Void commit(TransactionStatus status) throws TransactionException;
// 回滚
Void rollback(TransactionStatus status) throws TransactionException;
}
通过PlatformTransactionManager的接口 可以看出,Spring 事务的 的三个核心方法,事务开启 ,提交事务,回滚事务,Spring 事务功能的实现 都是围绕这三个方法来实现。
20160324011156424 (1).jpg
4.2 TransactionInfo
事务信息对象,包括一个事务所有的信息,持有 事务管理器,事务定义对象,目标方法唯一标识,事务状态对象,外层的TransactionInfo,外层的TransactionInfo 用于在应用程序中获取 当前的TransactionInfo 对象。
未命名文件 (2).png
4.3 TransactionStatus
表示一个事务状态,在应用程序中可以通过 TransactionInterceptor.currentTransactionStatus() 的静态函数获取到。
- 持有事务对象 ,使用JDBC 事务时 ,事务对象为 DataSourceTransactionObject,
- 对保存点的支持,可以在应用程序中 通过 设置保存点 ,在事务回滚时,回滚到 保存点,而不是回滚部分。
4.4 TransactionDefinition
事务定义对象,封装了@Transactional 注解中设置的各种信息,通过收集@Transactional属性信息,获取到 一个 TransationalDefinition ,有了事务定义信息 就可以通过PlatformTransactionManager开启事务 或者 复用一个事务了。4.5 TransationSynchronization 事务同步回调接口
事务同步回调接口,在事务 周期的各个点 执行回调 方法。比如 挂起 ,继续,提交前后 ,完成前后 。用于 管理 应用程序在事务周期中绑定的资源。在Spring - Mybatis 整合时,正式Mybatis 正式利用了TransationSynchronization同步器,才让Mybatis 的事务管理交给了 Spring 事务来管理。4.6 TransactionSynchronizationManager 事务同步回调的管理器
在事务运行过程中,需要保存一些状态,比如 数据库连接,
ThreadLocal<Map<Object, Object>> resources - 应用代码随事务的声明周期绑定的对象
ThreadLocal<Set<TransactionSynchronization>> synchronizations-使用的同步器,用于应用扩展
ThreadLocal<String> currentTransactionName-事务的名称
ThreadLocal<Boolean> currentTransactionReadOnly-事务是否是只读
ThreadLocal<Integer> currentTransactionIsolationLevel-事务的隔离级别
ThreadLocal<Boolean> actualTransactionActive-是否实际的开启了事务,如果加入 到 别的事务,就不是实际开启事务。
4.7 SuspendedResourceHolder 挂起的资源持有对象
在挂起一个事务时,用于记录被挂起事务的运行时信息,这些信息就是TransactionSynchronizationManager中记录的事务信息。然后将这些信息 保存在 新的DefaultTransactionStatus对象中,便于内部事务运行结束后,恢复外层事务。
5. Spring 事务实现
Spring事务 把 整个事务流程 模板化,采用AOP的形式 增强到 需要事务 的方法,所以 按照 AOP 的 实现 一定存在 一个事务的增强器,这个增强器就是 BeanFactoryTransactionAttributeSourceAdvisor,该增强器中有个环绕通知TransactionInterceptor,所有的事务逻辑 都在这个类的Invoke 方法中,分析Spring 事务实现就从这个函数开始。
- invoke
//invocation 维护了 AOP 拦截器链 ,执行 invocation.prcess 方法 会沿着拦截器链执行下去直到目标方法。
public Object invoke(final MethodInvocation invocation) throws Throwable {
//获取目标对象
Class<?> targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null);
// 执行 父类 TransactionAspectSupport's invokeWithinTransaction...
return invokeWithinTransaction(invocation.getMethod(), targetClass, new InvocationCallback() {
@Override
public Object proceedWithInvocation() throws Throwable {
//继续一下执行下拦截器 也可能式目标方法
return invocation.proceed();
}
});
}
- TransactionAspectSupport’s invokeWithinTransaction
protected Object invokeWithinTransaction(Method method, Class<?> targetClass, final InvocationCallback invocation)
throws Throwable {
1. 准备事务的基本信息
// If the transaction attribute is null, the method is non-transactional.
//事务定义 TransactionAttribute 是 TransationDefinition 的子类
final TransactionAttribute txAttr = getTransactionAttributeSource().getTransactionAttribute(method, targetClass);
//获取 事务管理器 ,这里是一个策略模式,根据 事务定义 指定的 事务管理器 获取到 指定的 事务管理器。
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
//连接点 标识
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
2. 开启事务
//如果必要 才会开启事务,这里会根据 事务的传播行为 信息 来决定是否开启事务 还是 加入一个已经存在的事务。这里会涉及到事务的挂起
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
执行目标方法或者 执行AOP 拦截器链中的下一个拦截器。
// This is an around advice: Invoke the next interceptor in the chain.
// This will normally result in a target object being invoked.
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// target invocation exception
3. 事务的回滚
//是否回滚 会根据 rollback 属性 判断
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
清理事务信息
cleanupTransactionInfo(txInfo);
}
4. 提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
}
//省略部代码
5.1 准备事务
准备事务 主要是做了两件事情
- 收集@Transactional注解属性信息,生成 事务定义对象。
由于@Transactional可以作用在类上 又可以作用在方法上,所以在收集属性信息的时候,就考虑到这种情况。
AnnotationTransactionAttributeSource 类就是用来解析类和方法上面的@Transactional 注解属性。
那么到底 先解析类上面的 还是 先解析 方法上面的注解呢?如果方法上面 和 类上面 同时存在呢,是完整替换? 还是取并集?
定义解析@Transactional 注解的 逻辑定义在 其父类AbstractFallbackTransactionAttributeSource的computeTransactionAttribute,通过查看代码 便可以一目了然:
protected TransactionAttribute computeTransactionAttribute(Method method, Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Ignore CGLIB subclasses - introspect the actual user class.
Class<?> userClass = ClassUtils.getUserClass(targetClass);
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass);
// If we are dealing with method with generic parameters, find the original method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
//首先 解析方法上面 的 属性信息
// First try is the method in the target class.
TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
//如果方法上面存在 就返回。
if (txAttr != null) {
return txAttr;
}
//其次 解析作用在类上面的注解属性信息,如果找到 就返回。
// Second try is the transaction attribute on the target class.
txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
//解析接口方法上面的注解属性信息 ,如果找到返回。
if (specificMethod != method) {
// Fallback is to look at the original method.
txAttr = findTransactionAttribute(method);
if (txAttr != null) {
return txAttr;
}
//最后 解析接口上面的注解信息。
// Last fallback is the class of the original method.
txAttr = findTransactionAttribute(method.getDeclaringClass());
if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
return txAttr;
}
}
return null;
}
通过上面的代码可以看出来,@Transactional 注解定义在不同位置的 优先级
为 ,实列方法,实列类,接口方法,接口类。不会取并集 ,也不会覆盖,按照优先级查找,直到找到为止。
- 用到了缓存
虽然解析注解属性不是那么的耗时,但是也不能每次执行事务方法都要解析一次注解属性,所以在解析注解的时候Spring采用了缓存,这样就只需要一次解析注解,而后的每次执行都会存缓存中获取。这是一个典型的拿空间换时间的列子。采用缓存的代码在其父类 AbstractFallbackTransactionAttributeSource 的getTransactionAttribute函数。
在使用缓存的时候 难免遇到缓存穿透的现象,就是用key 获取缓存的时候 没有 获取到对象,然后就要去解析@Transactional ,结果发现 还是没有,此后的每次调用都会持续这个现象,所以Spring 在发现 不存在的时候 就会定义一个特殊的 value 放到缓存中,以标识 这个已经解析过了,确实不存在。
- 解析注解的时机
解析的时机 是在IOC 第一次初始化 Bean的时候,具体点就是 在为目标对象匹配增强器的时候,会触发解析注解。
- 获取事务管理器
如果使用@Transactional 指定了使用哪个事务管理器 ,就会获取响应的事务管理器。如果没有 就从IOC 容器中获取。
5.2 开启事务
收集到了事务定义信息,和 事务管理器之后,就可以 利用PlatformTransactionManager.getTransactional 开启事务了,但是开启事务,有很多情况需要考虑,比如繁多的事务传播行为,比如是否已经存在事务,不同的条件都会影响是否要开启一个新事务。有的传播行为 还会设计到 挂起 已经存在的事务。也是相当复杂的。
protected TransactionInfo createTransactionIfNecessary(
PlatformTransactionManager tm, TransactionAttribute txAttr, final String joinpointIdentification) {
// 采用 委托的方式 包装 事务定义 对象.
if (txAttr != null && txAttr.getName() == null) {
txAttr = new DelegatingTransactionAttribute(txAttr) {
@Override
public String getName() {
return joinpointIdentification;
}
};
}
TransactionStatus status = null;
if (txAttr != null) {
if (tm != null) {
//调用 事务管理器开启事务。
status = tm.getTransaction(txAttr);
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Skipping transactional joinpoint [" + joinpointIdentification +
"] because no transaction manager has been configured");
}
}
}
//封装成事务信息对象。
return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status);
}
AbstractPlatformTransactionManager. getTransaction
这是一个模版方法 ,提供了两个抽象方法 供 子类实现。
该方法主要逻辑
- 判断当前线程是否存在事务
- 存在事务 根据 事务传播行为 创建事务 或 加入当前事务 或 抛出不支持异常
- 不存在事务 判断 传播行为是否 为 TransactionDefinition.PROPAGATION_MANDATORY ,如果是抛出异常
- PROPAGATION_REQUIRED,PROPAGATION_REQUIRED_NEW ,TransactionDefinition.PROPAGATION_NESTED创建事务
- 不运行在事务中,创建空事务。
public final TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException {
//获取事务,从线程绑定的信息中获取 事务,抽象方法 留给子类实现。
Object transaction = doGetTransaction();
// Cache debug flag to avoid repeated checks.
boolean debugEnabled = logger.isDebugEnabled();
if (definition == null) {
// Use defaults if no transaction definition given.
definition = new DefaultTransactionDefinition();
}
// 判断是否 已经存在事务
if (isExistingTransaction(transaction)) {
//已经存在事务 根据传播行为 创建事务 或者 加入事务
return handleExistingTransaction(definition, transaction, debugEnabled);
}
// 检查超时时间 的设置 是否合法
if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) {
throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout());
}
//如果传播行为 强制 PROPAGATION_MANDATORY
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
//开启新事务
else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition);
}
try {
boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER);
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);
//抽象方法 开启事务。留给子类实现。
doBegin(transaction, definition);
prepareSynchronization(status, definition);
return status;
}
catch (RuntimeException ex) {
resume(null, suspendedResources);
throw ex;
}
catch (Error err) {
resume(null, suspendedResources);
throw err;
}
}
else {
// Create "empty" transaction: no actual transaction, but potentially synchronization.
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) {
logger.warn("Custom isolation level specified but no actual transaction initiated; " +
"isolation level will effectively be ignored: " + definition);
}
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null);
}
}
DataSourceTransactionManager.doBegin
这里实际获取数据库 连接 开启事务的地方,从DataSource 中获取连接,并且设置 自动提交为false ,
- 获取 数据库连接
- 设置 数据库连接自动提交 为 false,开启事务
- 绑定数据库连接到 线程
/**
* This implementation sets the isolation level but ignores the timeout.
*/
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction;
Connection con = null;
try {
//如果当前不存在数据库
if (!txObject.hasConnectionHolder() ||
txObject.getConnectionHolder().isSynchronizedWithTransaction()) {
//获取数据库连接,如果采用数据库连接池 这里就是连接池对象。
Connection newCon = this.dataSource.getConnection();
//设置连接到 事务对象中。
txObject.setConnectionHolder(new ConnectionHolder(newCon), true);
}
txObject.getConnectionHolder().setSynchronizedWithTransaction(true);
con = txObject.getConnectionHolder().getConnection();
//记录上一个 事务的隔离级别 ,如果没有外层事务 ,隔离级别就是null
Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition);
txObject.setPreviousIsolationLevel(previousIsolationLevel);
//设置自动提交 为false,如果使用 连接池,连接池 或许已经设置自动提交为false了,所以这里先判断一下。
if (con.getAutoCommit()) {
txObject.setMustRestoreAutoCommit(true);
if (logger.isDebugEnabled()) {
logger.debug("Switching JDBC Connection [" + con + "] to manual commit");
}
con.setAutoCommit(false);
}
//如果事务是只读事务 ,那么就会执行 SQL "SET TRANSACTION READ ONLY".
prepareTransactionalConnection(con, definition);
txObject.getConnectionHolder().setTransactionActive(true);
int timeout = determineTimeout(definition);
if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) {
txObject.getConnectionHolder().setTimeoutInSeconds(timeout);
}
// 如果是一个新的连接 ,绑定数据库连接到当前线程
if (txObject.isNewConnectionHolder()) {
//调用 事务同步回调管理器 的 绑定资源方法,key= dataSource ,key = ConnectionHodler
TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder());
}
}
catch (Throwable ex) {
if (txObject.isNewConnectionHolder()) {
//异常之后 释放连接,
DataSourceUtils.releaseConnection(con, this.dataSource);
txObject.setConnectionHolder(null, false);
}
throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex);
}
}
事务挂起
当线程中已经存在事务,在某些事务传播行为下就需要挂起外层事务,
比如PROPAGATION_NOT_SUPPORTED:不能运行在一个事务中,如果存在事务就挂起当前事务,执行。
PROPAGATION_REQUIRES_NEW: 必须运行在一个新事务中,如果当前存在事务,则挂起当前事务,开启新事务执行。
如何实现挂起一个事务呢?
挂起事务需要完成几项工作:
1.TransactionSynchronizationManager 中解除绑定的 TransactionSynchronization 集合
2.重置当前事务名称绑定
3.重置事务只读属性绑定
4.重置事务隔离级别绑定
5.重置实际事务激活标志绑定
6.记录以上几部的数据,封装到 SuspendedResourceHolder对象中。
7.将SuspendedResourceHolder对象,交给内部事务 ,以便内部事务执行结束后,恢复外层事务。
事务恢复
如果内部事务出现异常或者 内部事务提交 都会触发外层事务的恢复,事务的恢复就是将内存事务TransactionStauts 中记录的挂起事务的信息,重新绑定到 TransactionSynchronizationManager中去。
5.3 事务回滚
如果事务运行过程中出现某些异常 会导致事务回滚,在JDBC中 我们执行connection.rollback()回滚事务,Spring事务也不列外,只是Spring 事务 在JDBC 的基础之上提供了更多丰富的功能,比如 指定某些异常进行回滚。
关于事务回滚rollback 设置,还有一个容易被忽视 和 误解的地方。就是如果我们设置rollbackFor = IllegalArgumentException.class 那么事务运行期间出现了IndexOutOfBoundsException异常会不会导致事务回滚?出现了 Error 错误会不会回滚?
处理事务回滚的在TransactionAspectSupport . completeTransactionAfterThrowing 函数中
- 首先判断 异常是否需要回滚。判断逻辑 最终是委托给 RuleBasedTransactionAttribute.rollbackOn
public boolean rollbackOn(Throwable ex) {
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
if (this.rollbackRules != null) {
//遍历 查找 指定的 rollbackException 进行匹配
for (RollbackRuleAttribute rule : this.rollbackRules) {
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
// 如果没有匹配到 采用默认的回滚规则进行判断。
//默认的规则就是 ex instanceof RuntimeException || ex instanceof Error,所以 如果我们指定了rollback = IllegalArgumentException,当遇到 IndexOutOfBoundsException时 或者 Error 时也会回滚事务。
if (winner == null) {
logger.trace("No relevant rollback rule found: applying default rules");
return super.rollbackOn(ex);
}
return !(winner instanceof NoRollbackRuleAttribute);
}
- 如果需要回滚则会执行 AbstranctPlatformTransactionManager.processRollback函数
2.1 if (status.hasSavepoint())
如果存在保存点 则回滚到 保存点
2.2 else if (status.isNewTransaction())
如果 是一个新事务 执行回滚。
2.3 . else if (status.hasTransaction())
如果 是嵌套事务 设置 当前数据库链接 rollbackOnly - 如果不需要回滚 则提交事务
触发钩子函数
在回滚前后 会分别触发 TransactionSynchronuzation 的beforeCompletion,afterCompletion 函数,进行资源释放,连接关闭等。5.4 事务提交
只有当事务是一个 新事务的时候才会进行提交,就是如果 有一个 内嵌事务传播 行为 PROPAGATION_SUPPORTS ,PROPAGATION_REQUIRED ,PROPAGATION_MANDATORY 的事务 执行完之后 不会 提交,会随着外层事务的提交而提交。
事务的提交 最终 是调用 connect.commit 函数提交事务。
在事务提交前后 会触发TransactionSynchronuzation 钩子函数。进行资源释放操作。
mybatis 会在beforeCommit 中 执行Sqlsession commit6 一些细节
方法上面 @Transaction 注解 会覆盖 类上面的 @Transaction注解信息。是完全的覆盖,而不是 部分覆盖,就是说 ,如果类上设置了 事务超时时间 为 10秒,但是 方法上面没有设置事务超时时间,那么 最终 事务 就是没有超时时间,并不会 采用 类上面的 超时时间。
- 事务隔离级别 和 超时时间 只能作用于 一个 新事务,也就是说 ,当内部事务参与到一个已经存在的事务中时,事务隔离级别 和 超时时间将会被忽略。因为 内部事务 是参与到外层事务。
- 事务rollbackFor 的 含义是 默认异常 或 指定异常,就是说,默认回滚异常时 runtimeException 或 Error 或 自己指定的异常。
作者:白袜子先生
链接:https://www.jianshu.com/p/2449cd914e3c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
从Spring Boot到SpringMVC(非注解方式)
概述
用久了SpringBoot,深受其约定大于配置的便利性 “毒害” 之后,我想回归到SpringMVC时代,看看SpringMVC开发模式中用户是如何参与的。本文就来体验一下SpringMVC时代开发的流程。
注: 本文原载于 My Personal Blog:, CodeSheep · 程序羊 !
SpringMVC架构模式
SpringMVC请求处理流程
一个典型的SpringMVC请求流程如图所示,详细分为12个步骤:
- 用户发起请求,由前端控制器DispatcherServlet处理
- 前端控制器通过处理器映射器查找hander,可以根据XML或者注解去找
- 处理器映射器返回执行链
- 前端控制器请求处理器适配器来执行hander
- 处理器适配器来执行handler
- 处理业务完成后,会给处理器适配器返回ModeAndView对象,其中有视图名称,模型数据
- 处理器适配器将视图名称和模型数据返回到前端控制器
- 前端控制器通过视图解析器来对视图进行解析
- 视图解析器返回真正的视图给前端控制器
- 前端控制器通过返回的视图和数据进行渲染
- 返回渲染完成的视图
- 将最终的视图返回给用户,产生响应
整个过程清晰明了,下面我们将结合实际实验来理解这整个过程。
SpringMVC项目搭建
实验环境如下:
- IntelliJ IDEA 2018.1 (Ultimate Edition)
- SpringMVC 4.3.9.RELEASE
- Maven 3.3.9
这里我是用IDEA来搭建的基于Maven的SpringMVC项目,搭建过程不再赘述,各种点击并且下一步,最终创建好的项目架构如下:
基于Maven的SpringMVC项目
添加前端控制器配置
使用了SpringMVC,则所有的请求都应该交由SpingMVC来管理,即要将所有符合条件的请求拦截到SpringMVC的专有Servlet上。
为此我们需要在 web.xml
中添加SpringMVC的前端控制器DispatcherServlet:
<!--springmvc前端控制器-->
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:mvc-dispatcher.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>*.action</url-pattern>
</servlet-mapping>
该配置说明所有符合.action的url,都交由mvc-dispatcher这个Servlet来进行处理
编写SpringMVC核心XML配置文件
从上一步的配置可以看到,我们定义的mvc-dispatcher Servlet依赖于配置文件 mvc-dispatcher.xml
,在本步骤中我们需要在其中添加三个方面的配置
- 0x01. 添加处理器映射器
<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping" />
SpringMVC的处理器映射器有多种,这里的使用的BeanNameUrlHandlerMapping其映射规则是将bean的name作为url进行处理
- 0x02. 添加处理器适配器
<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" />
SpringMVC的处理器适配器也有多种,这里的使用的SimpleControllerHandlerAdapter是Controller实现类的适配器类,其本质是执行Controller中的handleRequest方法。
- 0x03. 添加试图解析器
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" />
这里配置了InternalResourceViewResolver视图解析器后,其会根据controller方法执行之后返回的ModelAndView中的视图的具体位置,来加载对应的界面并绑定数据
编写控制器
这里模拟的是一个打印学生名单的Service,我们编写的控制器需要将查询到的学生名单数据通过ModelAndView渲染到指定的JSP页面中
public class TestController implements Controller {
private StudentService studentService = new StudentService();
@Override
public ModelAndView handleRequest( HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws Exception {
List<Student> studentList = studentService.queryStudents();
ModelAndView modelAndView = new ModelAndView();
modelAndView.addObject("studentList",studentList);
modelAndView.setViewName("/WEB-INF/views/studentList.jsp");
return modelAndView;
}
}
class StudentService {
public List<Student> queryStudents() {
List<Student> studentList = new ArrayList<Student>();
Student hansonwang = new Student();
hansonwang.setName("hansonwang99");
hansonwang.setID("123456");
Student codesheep = new Student();
codesheep.setName("codesheep");
codesheep.setID("654321");
studentList.add(hansonwang);
studentList.add(codesheep);
return studentList;
}
}
编写视图文件
这里的视图文件是一个jsp文件,路径为:/WEB-INF/views/studentList.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<title>学生名单</title>
</head>
<body>
<h3>学生列表</h3>
<table width="300px;" border=1>
<tr>
<td>姓名</td>
<td>学号</td>
</tr>
<c:forEach items="${studentList}" var="student" >
<tr>
<td>${student.name}</td>
<td>${student.ID}</td>
</tr>
</c:forEach>
</table>
</body>
</html>
结合本步骤和上一步骤,视图和控制器都已编写完成,由于我们之前配置的处理器映射器为:BeanNameUrlHandlerMapping,因此接下来我们还需要在mvc-dispatcher.xml文件中配置一个可被url映射的controller的bean,供处理器映射器BeanNameUrlHandlerMapping查找:
<bean name="/test.action" class="cn.codesheep.controller.TestController" />
实验测试
启动Tomcat服务器,然后浏览器输入:
http://localhost:8080/test.action
实验结果
数据渲染OK。
备注:当然本文所使用的全是非注解的配置方法,即需要在XML中进行配置并且需要遵循各种实现原则。而更加通用、主流的基于注解的配置方法将在后续文章中详述。
呼,长舒一口气,这么个小Demo用SpringMVC完成的话,各种XML配置了半天,真麻烦啊,算了,还是回SpringBoot好了!
作者:CodeSheep
链接:https://www.jianshu.com/p/8483277e9011
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
SpringBoot 中 @SpringBootApplication注解背后的三体结构探秘
SpringBoot 约定大于配置 的功力让我们如沐春风,在我之前写的文章《从SpringBoot到SpringMVC》
也对比过 SpringBoot 和 SpringMVC 这两个框架,不过最终 SpringBoot 以超高的代码信噪比 和 易上手性 让我们映像颇深。
但归根结底,不论 SpringBoot 或者 SpringMVC 应用本质上依然是一个基于 Spring的应用,只不过在后者脸庞上蒙上了一层神秘的面纱而已!
回到 SpringBoot 的话题,我们在开发基于 SpringBoot 的应用时,用到了一些新的注解和类,正式由于其存在,才让JavaEE的开发如鱼得水。这其中我们用的最多的注解之一,当属 SpringBoot 应用启动类上的 @SpringBootApplication 注解了
本文就来看看它到底是个啥!
@SpringBootApplication 背后到底是什么?
@SpringBootApplication注解实际上是SpringBoot提供的一个复合注解,我们来看一看其源码:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication { …} |
---|
看得很清楚,其是一个合成体,但其中最重要的三个注解分别是:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
我们不妨称其为 “ 三体结构 ” 吧!
如果我们不怕麻烦,在 SpringBoot 应用的启动类上用这个三个注解代替@SpringBootApplication 注解发现也是没问题的:
@SpringBootConfiguration@EnableAutoConfiguration@ComponentScanpublic class TestSpringBootApplication { …} |
---|
下面分别剖析一下这三个注解的功效!
@SpringBootConfiguration
看代码吧,代码里是这样写的:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Configurationpublic @interface SpringBootConfiguration { } |
---|
这说明 @SpringBootConfiguration 也是来源于 @Configuration,二者功能都是将当前类标注为配置类,并将当前类里以 @Bean 注解标记的方法的实例注入到srping容器中,实例名即为方法名。
至于@Configuration,我想在非SpringBoot时代大家应该不陌生吧,作用是配置Spring容器,也即 JavaConfig 形式的 Spring IoC 容器的配置类所使用。
到目前来看,好像还没有什么新东西!!!
@EnableAutoConfiguration
再继续看代码,代码是这样的:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration { …} |
---|
@EnableAutoConfiguration 注解启用自动配置,其可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 IoC 容器之中,可以简要用图形示意如下:
@EnableAutoConfiguration 幕后的组件调用关系
接下来我们对照源码,来解释一下这个流程:
- @EnableAutoConfiguration 借助 AutoConfigurationImportSelector 的帮助,而后者通过实现 selectImports() 方法来导出 Configuration
- AutoConfigurationImportSelector 类的 selectImports() 方法里面通过调用Spring Core 包里 SpringFactoriesLoader 类的 loadFactoryNames()方法
SpringFactoriesLoader.loadFactoryNames()
- 最终通过 SpringFactoriesLoader.loadFactoryNames() 读取了 ClassPath 下面的 META-INF/spring.factories 文件来获取所有导出类。
而spring.factories 文件里关于 EnableAutoConfiguration 的配置其实就是一个键值对结构,样子大概长下面这样:
spring.factories
说了这么多,如果从稍微宏观一点的角度 概括总结 上述这一过程那就是:
从 ClassPath下扫描所有的 META-INF/spring.factories 配置文件,并将spring.factories 文件中的 EnableAutoConfiguration 对应的配置项通过反射机制实例化为对应标注了 @Configuration 的形式的IoC容器配置类,然后注入IoC容器。
@ComponentScan
@ComponentScan 对应于XML配置形式中的 context:component-scan,用于将一些标注了特定注解的bean定义批量采集注册到Spring的IoC容器之中,这些特定的注解大致包括:
- @Controller
- @Entity
- @Component
- @Service
- @Repository
等等
对于该注解,还可以通过 basePackages 属性来更细粒度的控制该注解的自动扫描范围,比如:
@ComponentScan(basePackages = {“cn.codesheep.controller”,”cn.codesheep.entity”}) |
---|
可见 这个注解也并不是什么新东西!
SpringBoot 应用程序启动过程探秘
本文共 946字,阅读大约需要 3分钟 !
概述
说到接触 SpringBoot 伊始,给我第一映像最深的是有两个关键元素:
对照上面的典型代码,这个两个元素分别是:
- @SpringBootApplication
- SpringApplication 以及 run() 方法
关于 @SpringBootApplication 注解的剖析已经在上文:《SpringBoot 中 @SpringBootApplication注解背后的三体结构探秘》 中完成了,其实它背后就是一个三体结构,只是 SpringBoot给了其一个包装而已。那么本文我们就来看看这个 SpringApplication 以及 run() 方法 到底是个什么鬼,它背后又隐藏了哪些奥秘呢?
注: 本文首发于 My Personal Blog,欢迎光临 小站
本文内容脑图如下:
SpringApplication 惊鸿一瞥
SpringApplication 这个类应该算是 SpringBoot 框架 的“创新”产物了,原始的 Spring中并没有这个类,SpringApplication 里面封装了一套 Spring 应用的启动流程,然而这对用户完全透明,因此我们上手 SpringBoot 时感觉简洁、轻量。
一般来说默认的 SpringApplication 执行流程已经可以满足大部分需求,但是 若用户想干预这个过程,则可以通过 SpringApplication 在流程某些地方开启的 扩展点 来完成对流程的扩展,典型的扩展方案那就是使用 set 方法。
我们来举一个栗子,把我们天天司空见惯的 SpringBoot 应用的启动类来拆解一下写出来:
@SpringBootApplication
public class CodeSheepApplication {
public static void main( String[] args ) {
// SpringApplication.run( CodeSheepApplication.class args ); // 这是传统SpringBoot应用的启动,一行代码搞定,内部默认做了很多事
SpringApplication app = new SpringApplication( CodeSheepApplication.class );
app.setXXX( ... ); // 用户自定的扩展在此 !!!
app.run( args );
}
}
复制代码
这样一拆解后我们发现,我们也需要先构造 SpringApplication 类对象,然后调用该对象的 run()
方法。那么接下来就讲讲 SpringApplication 的构造过程 以及其 run()
方法的流程,搞清楚了这个,那么也就搞清楚了SpringBoot应用是如何运行起来的!
SpringApplication 实例的初始化
我们对照代码来看:
四个关键的步骤已标注在图中,分别解释如下:
- ① 推断应用的类型:创建的是 REACTIVE应用、SERVLET应用、NONE 三种中的某一种
- ② 使用
SpringFactoriesLoader
查找并加载 classpath下META-INF/spring.factories
文件中所有可用的ApplicationContextInitializer
- ③ 使用
SpringFactoriesLoader
查找并加载 classpath下META-INF/spring.factories
文件中的所有可用的ApplicationListener
- ④ 推断并设置 main方法的定义类
SpringApplication 的run()方法探秘
先看看代码长啥样子:
各个主要步骤我已经标注在上图之中了,除此之外,我也按照自己的理解画了一个流程图如下所示,可以对照数字标示看一下:
我们将各步骤总结精炼如下:
- 通过
SpringFactoriesLoader
加载META-INF/spring.factories
文件,获取并创建SpringApplicationRunListener
对象 - 然后由
SpringApplicationRunListener
来发出 starting 消息 - 创建参数,并配置当前 SpringBoot 应用将要使用的 Environment
- 完成之后,依然由
SpringApplicationRunListener
来发出 environmentPrepared 消息 - 创建
ApplicationContext
- 初始化
ApplicationContext
,并设置 Environment,加载相关配置等 - 由
SpringApplicationRunListener
来发出contextPrepared
消息,告知SpringBoot 应用使用的ApplicationContext
已准备OK - 将各种 beans 装载入
ApplicationContext
,继续由SpringApplicationRunListener
来发出 contextLoaded 消息,告知 SpringBoot 应用使用的ApplicationContext
已装填OK - refresh ApplicationContext,完成IoC容器可用的最后一步
- 由
SpringApplicationRunListener
来发出 started 消息 - 完成最终的程序的启动
- 由
SpringApplicationRunListener
来发出 running 消息,告知程序已运行起来了
至此,全流程结束!
原文地址:https://www.yuque.com/lobotomy/java/mq0062