在我们第一次学Servlet编程,学java web的时候,还没有那么多框架。我们开发一个简单的功能要做的事情很简单,就是继承HttpServlet,根据需要重写一下doGet,doPost方法,跳转到我们定义好的jsp页面。Servlet类编写完之后在web.xml里注册这个Servlet类。
除此之外,没有其他了。我们启动web服务器,在浏览器中输入地址,就可以看到浏览器上输出我们写好的页面。为了更好的理解上面这个过程,你需要学习关于Servlet生命周期的三个阶段,就是所谓的“init-service-destroy”。
以上的知识,我觉得对于你理解SpringMVC的设计思想,已经足够了。SpringMVC当然可以称得上是一个复杂的框架,但是同时它又遵循Servlet世界里最简单的法则,那就是“init-service-destroy”。我们要分析SpringMVC的初始化流程,其实就是分析DispatcherServlet类的init()方法,让我们带着这种单纯的观点,打开DispatcherServlet的源码一窥究竟吧。
1.<init-param>配置元素读取
用Eclipse IDE打开DispatcherServlet类的源码,ctrl+T看一下。

DispatcherServlet类的初始化入口方法init()定义在HttpServletBean这个父类中,HttpServletBean类作为一个直接继承于HttpServlet类的类,覆写了HttpServlet类的init()方法,实现了自己的初始化行为。
02 |
public final void init() throws ServletException
{ |
03 |
if (logger.isDebugEnabled())
{ |
04 |
logger.debug( "Initializing
servlet '" +
getServletName() + "'" ); |
07 |
//
Set bean properties from init parameters. |
09 |
PropertyValues
pvs = new ServletConfigPropertyValues(getServletConfig(), this .requiredProperties); |
10 |
BeanWrapper
bw = PropertyAccessorFactory.forBeanPropertyAccess( this ); |
11 |
ResourceLoader
resourceLoader = new ServletContextResourceLoader(getServletContext()); |
12 |
bw.registerCustomEditor(Resource. class , new ResourceEditor(resourceLoader, this .environment)); |
14 |
bw.setPropertyValues(pvs, true ); |
16 |
catch (BeansException
ex) { |
17 |
logger.error( "Failed
to set bean<span></span> properties on servlet '" +
getServletName() + "'" ,
ex); |
21 |
//
Let subclasses do whatever initialization they like. |
24 |
if (logger.isDebugEnabled())
{ |
25 |
logger.debug( "Servlet
'" +
getServletName() + "'
configured successfully" ); |
这里的initServletBean()方法在HttpServletBean类中是一个没有任何实现的空方法,它的目的就是留待子类实现自己的初始化逻辑,也就是我们常说的模板方法设计模式。SpringMVC在此生动的运用了这个模式,init()方法就是模版方法模式中的模板方法,SpringMVC真正的初始化过程,由子类FrameworkServlet中覆写的initServletBean()方法触发。
再看一下init()方法内被try,catch块包裹的代码,里面涉及到BeanWrapper,PropertyValues,ResourceEditor这些Spring内部非常底层的类。要深究具体代码实现上面的细节,需要对Spring框架源码具有相当深入的了解。我们这里先避繁就简,从代码效果和设计思想上面来分析这段try,catch块内的代码所做的事情:
- 注册一个字符串到资源文件的编辑器,让Servlet下面的<init-param>配置元素可以使用形如“classpath:”这种方式指定SpringMVC框架bean配置文件的来源。
- 将web.xml中在DispatcherServlet这个Servlet下面的<init-param>配置元素利用JavaBean的方式(即通过setter方法)读取到DispatcherServlet中来。
这两点,我想通过下面一个例子来说明一下。
我在web.xml中注册的DispatcherServlet配置如下:
01 |
<!--
springMVC配置开始 --> |
03 |
< servlet-name >appServlet</ servlet-name > |
04 |
< servlet-class >org.springframework.web.servlet.DispatcherServlet</ servlet-class > |
06 |
< param-name >contextConfigLocation</ param-name > |
07 |
< param-value >classpath:spring/spring-servlet.xml</ param-value > |
09 |
< load-on-startup >1</ load-on-startup > |
12 |
< servlet-name >appServlet</ servlet-name > |
13 |
< url-pattern >/</ url-pattern > |
15 |
<!--
springMVC配置结束 --> |
可以看到,我注册了一个名为contextConfigLocation的<init-param>元素,其值为“classpath:spring/spring-servlet.xml”,这也是大家常常用来指定SpringMVC配置文件路径的方法。上面那段try,catch块包裹的代码发挥的作用,一个是将“classpath:spring/spring-servlet.xml”这段字符串转换成classpath路径下的一个资源文件,供框架初始化读取配置元素。在我的工程中是在spring文件夹下面的配置文件spring-servlet.xml。

另外一个作用,就是将contextConfigLocation的值读取出来,然后通过setContextConfigLocation()方法设置到DispatcherServlet中,这个setContextConfigLocation()方法是在FrameworkServlet类中定义的,也就是上面继承类图中DispatcherServlet的直接父类。
我们在setContextConfigLocation()方法上面打上一个断点,启动web工程,可以看到下面的调试结果。

HttpServletBean类的作者是大名鼎鼎的Spring之父Rod Johnson。作为POJO编程哲学的大师,他在HttpServletBean这个类的设计中,运用了依赖注入思想完成了<init-param>配置元素的读取。他抽离出HttpServletBean这个类的目的也在于此,就是“以依赖注入的方式来读取Servlet类的<init-param>配置信息”,而且这里很明显是一种setter注入。
明白了HttpServletBean类的设计思想,我们也就知道可以如何从中获益。具体来说,我们继承HttpServletBean类(就像DispatcherServlet做的那样),在类中定义一个属性,为这个属性加上setter方法后,我们就可以在<init-param>元素中为其定义值。在类被初始化后,值就会被注入进来,我们可以直接使用它,避免了样板式的getInitParameter()方法的使用,而且还免费享有Spring中资源编辑器的功能,可以在web.xml中,通过“classpath:”直接指定类路径下的资源文件。
注意,虽然SpringMVC本身为了后面初始化上下文的方便,使用了字符串来声明和设置contextConfigLocation参数,但是将其声明为Resource类型,同样能够成功获取。鼓励读者们自己继承HttpServletBean写一个测试用的Servlet类,并设置一个参数来调试一下,这样能够帮助你更好的理解获取配置参数的过程。
2.容器上下文的建立
上一篇文章中提到过,SpringMVC使用了Spring容器来容纳自己的配置元素,拥有自己的bean容器上下文。在SpringMVC初始化的过程中,非常关键的一步就是要建立起这个容器上下文,而这个建立上下文的过程,发生在FrameworkServlet类中,由上面init()方法中的initServletBean()方法触发。
02 |
protected final void initServletBean() throws ServletException
{ |
03 |
getServletContext().log( "Initializing
Spring FrameworkServlet '" +
getServletName() + "'" ); |
04 |
if ( this .logger.isInfoEnabled())
{ |
05 |
this .logger.info( "FrameworkServlet
'" +
getServletName() + "':
initialization started" ); |
07 |
long startTime
= System.currentTimeMillis(); |
10 |
this .webApplicationContext
= initWebApplicationContext(); |
11 |
initFrameworkServlet(); |
13 |
catch (ServletException
ex) { |
14 |
this .logger.error( "Context
initialization failed" ,
ex); |
17 |
catch (RuntimeException
ex) { |
18 |
this .logger.error( "Context
initialization failed" ,
ex); |
22 |
if ( this .logger.isInfoEnabled())
{ |
23 |
long elapsedTime
= System.currentTimeMillis() - startTime; |
24 |
this .logger.info( "FrameworkServlet
'" +
getServletName() + "':
initialization completed in " + |
initFrameworkServlet()方法是一个没有任何实现的空方法,除去一些样板式的代码,那么这个initServletBean()方法所做的事情已经非常明白:
1 |
this .webApplicationContext
= initWebApplicationContext(); |
这一句简单直白的代码,道破了FrameworkServlet这个类,在SpringMVC类体系中的设计目的,它是 用来抽离出建立WebApplicationContext 上下文这个过程的。
initWebApplicationContext()方法,封装了建立Spring容器上下文的整个过程,方法内的逻辑如下:
- 获取由ContextLoaderListener初始化并注册在ServletContext中的根上下文,记为rootContext
- 如果webApplicationContext已经不为空,表示这个Servlet类是通过编程式注册到容器中的(Servlet 3.0+中的ServletContext.addServlet() ),上下文也由编程式传入。若这个传入的上下文还没被初始化,将rootContext上下文设置为它的父上下文,然后将其初始化,否则直接使用。
- 通过wac变量的引用是否为null,判断第2步中是否已经完成上下文的设置(即上下文是否已经用编程式方式传入),如果wac==null成立,说明该Servlet不是由编程式注册到容器中的。此时以contextAttribute属性的值为键,在ServletContext中查找上下文,查找得到,说明上下文已经以别的方式初始化并注册在contextAttribute下,直接使用。
- 检查wac变量的引用是否为null,如果wac==null成立,说明2、3两步中的上下文初始化策略都没成功,此时调用createWebApplicationContext(rootContext),建立一个全新的以rootContext为父上下文的上下文,作为SpringMVC配置元素的容器上下文。大多数情况下我们所使用的上下文,就是这个新建的上下文。
- 以上三种初始化上下文的策略,都会回调onRefresh(ApplicationContext context)方法(回调的方式根据不同策略有不同),onRefresh方法在DispatcherServlet类中被覆写,以上面得到的上下文为依托,完成SpringMVC中默认实现类的初始化。
- 最后,将这个上下文发布到ServletContext中,也就是将上下文以一个和Servlet类在web.xml中注册名字有关的值为键,设置为ServletContext的一个属性。你可以通过改变publishContext的值来决定是否发布到ServletContext中,默认为true。
以上面6点跟踪FrameworkServlet类中的代码,可以比较清晰的了解到整个容器上下文的建立过程,也就能够领会到FrameworkServlet类的设计目的,它是用来建立一个和Servlet关联的Spring容器上下文,并将其注册到ServletContext中的。跳脱开SpringMVC体系,我们也能通过继承FrameworkServlet类,得到与Spring容器整合的好处,FrameworkServlet和HttpServletBean一样,是一个可以独立使用的类。整个SpringMVC设计中,处处体现开闭原则,这里显然也是其中一点。
3.初始化SpringMVC默认实现类
初始化流程在FrameworkServlet类中流转,建立了上下文后,通过onRefresh(ApplicationContext context)方法的回调,进入到DispatcherServlet类中。
2 |
protected void onRefresh(ApplicationContext
context) { |
3 |
initStrategies(context); |
DispatcherServlet类覆写了父类FrameworkServlet中的onRefresh(ApplicationContext context)方法,提供了SpringMVC各种编程元素的初始化。当然这些编程元素,都是作为容器上下文中一个个bean而存在的。具体的初始化策略,在initStrategies()方法中封装。
01 |
protected void initStrategies(ApplicationContext
context) { |
02 |
initMultipartResolver(context); |
03 |
initLocaleResolver(context); |
04 |
initThemeResolver(context); |
05 |
initHandlerMappings(context); |
06 |
initHandlerAdapters(context); |
07 |
initHandlerExceptionResolvers(context); |
08 |
initRequestToViewNameTranslator(context); |
09 |
initViewResolvers(context); |
10 |
initFlashMapManager(context); |
我们以其中initHandlerMappings(context)方法为例,分析一下这些SpringMVC编程元素的初始化策略,其他的方法,都是以类似的策略初始化的。
01 |
private void initHandlerMappings(ApplicationContext
context) { |
02 |
this .handlerMappings
= null ; |
04 |
if ( this .detectAllHandlerMappings)
{ |
05 |
//
Find all HandlerMappings in the ApplicationContext, including ancestor contexts. |
06 |
Map<String,
HandlerMapping> matchingBeans = |
07 |
BeanFactoryUtils.beansOfTypeIncludingAncestors(context,
HandlerMapping. class , true , false ); |
08 |
if (!matchingBeans.isEmpty())
{ |
09 |
this .handlerMappings
= new ArrayList<HandlerMapping>(matchingBeans.values()); |
10 |
//
We keep HandlerMappings in sorted order. |
11 |
OrderComparator.sort( this .handlerMappings); |
16 |
HandlerMapping
hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping. class ); |
17 |
this .handlerMappings
= Collections.singletonList(hm); |
19 |
catch (NoSuchBeanDefinitionException
ex) { |
20 |
//
Ignore, we'll add a default HandlerMapping later. |
24 |
//
Ensure we have at least one HandlerMapping, by registering |
25 |
//
a default HandlerMapping if no other mappings are found. |
26 |
if ( this .handlerMappings
== null )
{ |
27 |
this .handlerMappings
= getDefaultStrategies(context, HandlerMapping. class ); |
28 |
if (logger.isDebugEnabled())
{ |
29 |
logger.debug( "No
HandlerMappings found in servlet '" +
getServletName() + "':
using default" ); |
detectAllHandlerMappings变量默认为true,所以在初始化HandlerMapping接口默认实现类的时候,会把上下文中所有HandlerMapping类型的Bean都注册在handlerMappings这个List变量中。如果你手工将其设置为false,那么将尝试获取名为handlerMapping的Bean,新建一个只有一个元素的List,将其赋给handlerMappings。如果经过上面的过程,handlerMappings变量仍为空,那么说明你没有在上下文中提供自己HandlerMapping类型的Bean定义。此时,SpringMVC将采用默认初始化策略来初始化handlerMappings。
点进去getDefaultStrategies看一下。
01 |
@SuppressWarnings ( "unchecked" ) |
02 |
protected <T>
List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) { |
03 |
String
key = strategyInterface.getName(); |
04 |
String
value = defaultStrategies.getProperty(key); |
06 |
String[]
classNames = StringUtils.commaDelimitedListToStringArray(value); |
07 |
List<T>
strategies = new ArrayList<T>(classNames.length); |
08 |
for (String
className : classNames) { |
10 |
Class<?>
clazz = ClassUtils.forName(className, DispatcherServlet. class .getClassLoader()); |
11 |
Object
strategy = createDefaultStrategy(context, clazz); |
12 |
strategies.add((T)
strategy); |
14 |
catch (ClassNotFoundException
ex) { |
15 |
throw new BeanInitializationException( |
16 |
"Could
not find DispatcherServlet's default strategy class [" +
className + |
17 |
"]
for interface [" +
key + "]" ,
ex); |
19 |
catch (LinkageError
err) { |
20 |
throw new BeanInitializationException( |
21 |
"Error
loading DispatcherServlet's default strategy class [" +
className + |
22 |
"]
for interface [" +
key + "]:
problem with class file or dependent class" ,
err); |
28 |
return new LinkedList<T>(); |
它是一个范型的方法,承担所有SpringMVC编程元素的默认初始化策略。方法的内容比较直白,就是以传递类的名称为键,从defaultStrategies这个Properties变量中获取实现类,然后反射初始化。
需要说明一下的是defaultStrategies变量的初始化,它是在DispatcherServlet的静态初始化代码块中加载的。
01 |
private static final Properties
defaultStrategies; |
04 |
//
Load default strategy implementations from properties file. |
05 |
//
This is currently strictly internal and not meant to be customized |
06 |
//
by application developers. |
08 |
ClassPathResource
resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH,
DispatcherServlet. class ); |
09 |
defaultStrategies
= PropertiesLoaderUtils.loadProperties(resource); |
11 |
catch (IOException
ex) { |
12 |
throw new IllegalStateException( "Could
not load 'DispatcherServlet.properties': " +
ex.getMessage()); |
1 |
private static final String
DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties" ; |
这个DispatcherServlet.properties里面,以键值对的方式,记录了SpringMVC默认实现类,它在spring-webmvc-3.1.3.RELEASE.jar这个jar包内,在org.springframework.web.servlet包里面。
01 |
#
Default implementation classes for DispatcherServlet's
strategy interfaces. |
02 |
#
Used as fallback when no matching beans are found in the DispatcherServlet context. |
03 |
#
Not meant to be customized by application developers. |
05 |
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver |
07 |
org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver |
09 |
org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ |
10 |
org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping |
12 |
org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ |
13 |
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ |
14 |
org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter |
16 |
org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\ |
17 |
org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ |
18 |
org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver |
20 |
org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator |
22 |
org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver |
24 |
org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager |
至此,我们分析完了initHandlerMappings(context)方法的执行过程,其他的初始化过程与这个方法非常类似。所有初始化方法执行完后,SpringMVC正式完成初始化,静静等待Web请求的到来。
4.总结
回顾整个SpringMVC的初始化流程,我们看到,通过HttpServletBean、FrameworkServlet、DispatcherServlet三个不同的类层次,SpringMVC的设计者将三种不同的职责分别抽象,运用模版方法设计模式分别固定在三个类层次中。其中HttpServletBean完成的是<init-param>配置元素的依赖注入,FrameworkServlet完成的是容器上下文的建立,DispatcherServlet完成的是SpringMVC具体编程元素的初始化策略。
- 2.SpringMVC源码分析:DispatcherServlet的初始化与请求转发
一.DispatcherServlet的初始化 在我们第一次学Servlet编程,学java web的时候,还没有那么多框架.我们开发一个简单的功能要做的事情很简单,就是继承HttpServlet,根 ...
- SpringMVC源码剖析(四)- DispatcherServlet请求转发的实现
SpringMVC完成初始化流程之后,就进入Servlet标准生命周期的第二个阶段,即“service”阶段.在“service”阶段中,每一次Http请求到来,容器都会启动一个请求线程,通过serv ...
- SpringMVC源码剖析(二)- DispatcherServlet的前世今生
上一篇文章<SpringMVC源码剖析(一)- 从抽象和接口说起>中,我介绍了一次典型的SpringMVC请求处理过程中,相继粉墨登场的各种核心类和接口.我刻意忽略了源码中的处理细节,只列 ...
- SpringMVC源码情操陶冶-DispatcherServlet父类简析
阅读源码有助于陶冶情操,本文对springmvc作个简单的向导 springmvc-web.xml配置 <servlet> <servlet-name>dispatch< ...
- SpringMVC源码情操陶冶-DispatcherServlet类简析(一)
阅读源码有利于陶冶情操,此文承接前文SpringMVC源码情操陶冶-DispatcherServlet父类简析 注意:springmvc初始化其他内容,其对应的配置文件已被加载至beanFactory ...
- SpringMVC源码情操陶冶-DispatcherServlet简析(二)
承接前文SpringMVC源码情操陶冶-DispatcherServlet类简析(一),主要讲述初始化的操作,本文将简单介绍springmvc如何处理请求 DispatcherServlet#doDi ...
- SpringMVC源码剖析1——执行流程
SpringMVC源码剖析1——执行流程 00.SpringMVC执行流程file:///C:/Users/WANGGA~1/AppData/Local/Temp/enhtmlclip/Image.p ...
- SpringMVC源码情操陶冶-DispatcherServlet
本文对springmvc核心类DispatcherServlet作下简单的向导,方便博主与读者查阅 DispatcherServlet-继承关系 分析DispatcherServlet的继承关系以及主 ...
- jdk源码剖析三:锁Synchronized
一.Synchronized作用 (1)确保线程互斥的访问同步代码 (2)保证共享变量的修改能够及时可见 (3)有效解决重排序问题.(Synchronized同步中的代码JVM不会轻易优化重排序) 二 ...
- Django Rest Framework源码剖析(三)-----频率控制
一.简介 承接上篇文章Django Rest Framework源码剖析(二)-----权限,当服务的接口被频繁调用,导致资源紧张怎么办呢?当然或许有很多解决办法,比如:负载均衡.提高服务器配置.通过 ...
随机推荐
- 【YashanDB知识库】开源调度框架Quartz写入Boolean值到YashanDB报错
问题现象 Quartz 是一个广泛应用于企业级应用中的开源作业调度框架,它主要用于在Java环境中管理和执行任务. 为了任务调度,Quartz的数据模型中使用了大量的布尔值记录任务.流程的各种状态,如 ...
- 机器学习--决策树算法(CART)
CART分类树算法 特征选择 我们知道,在ID3算法中我们使用了信息增益来选择特征,信息增益大的优先选择.在C4.5算法中,采用了信息增益比来选择特征,以减少信息增益容易选择特征值多的特征的问题. ...
- 音视频FAQ(三):音画不同步
摘要 本文介绍了音画不同步问题的五个因素:编码和封装阶段.网络传输阶段.播放器中的处理阶段.源内容产生的问题以及转码和编辑.针对这些因素,提出了相应的解决方案,如使用标准化工具.选择强大的传输协议.自 ...
- 小tips:xml文件转为html表格展示示例
books.xml文件格式如下: <?xml version="1.0" encoding="UTF-8"?> <xbrl xmlns=&qu ...
- Angular Material 18+ 高级教程 – 大杂烩
前言 本篇记入一些 Angular Material 的小东西. Override Material Icon Button Size 参考:Stack Overflow – Change size ...
- Angular 18+ 高级教程 – Signals
前言 首先,我必须先说明清楚.Signal 目前不是 Angular 的必备知识. 你的项目不使用 Signal 也不会少了条腿,断了胳膊. Angular 官方维护的 UI 组件库 Angular ...
- GIS转码的秋招历程与踩坑经历
本文介绍地理信息科学(GIS)专业的2024届应届生,在研三上学期期间,寻找后端研发.软件开发等IT方向工作的非科班转码秋招情况. 首先,这篇文章一开始写于2023年年底,当时为了参加一个征文 ...
- Nuxt Kit 实用工具的使用示例
title: Nuxt Kit 实用工具的使用示例 date: 2024/9/25 updated: 2024/9/25 author: cmdragon excerpt: 摘要:本文介绍了Nuxt ...
- [namespace hdk] 向量 direct_vector
我忏悔我有罪我心情又不好了不知道干什么所以又不小心封了个东西啊啊啊啊啊啊啊啊 功能 已重载 [] 运算符(左值) 已重载 = 运算符(可使用向量或 std:::vector) 已重载 + += - - ...
- 关于AutoCAD反复弹窗Nonvalid Software Detected的解决办法
事件起因: 客户安装的 CAD 2020 频繁弹窗Nonvalid Software Detected,报错内容:YOUR ACCESS IS NOW BLOCKED 解决办法: 在文件资源管理器 ...