SpringMVC是一款Web MVC框架。 它跟Struts框架类似,是目前主流的Web MVC框架之一
一、SpringMVC的由来
早期 Java Web 的开发中,把显示层、控制层、数据层的操作全部交给 JSP 或者 JavaBean 来进行处理,我们称之为 Model1:
然而出现了很多的弊端,比如:
- JSP 和 Java Bean 之间严重耦合,Java 代码和 HTML 代码也耦合在了一起
- 要求开发者不仅要掌握 Java ,还要有高超的前端水平
- 前端和后端相互依赖,前端需要等待后端完成,后端也依赖前端完成,才能进行有效的测试
随后出现servlet,就有了早期的MVC模式
首先用户请求到servlet,然后根据请求调用响应的JavaBean,并把所有的显示交给Jsp去处理,这样就称之为mvc模式:
- M代表模型(Model):数据、bean
- V代表视图(View):网页,jsp....展示模型中的数据
- C代表控制器(Controller):把不同的数据(Model),显示在不同的视图(View)上,Servlet 扮演的就是这样的角色
为解决持久层中一直未处理好的数据库事务的编程,又为了迎合 NoSQL 的强势崛起,Spring MVC 给出了方案
二、初始化过程
当一个web应用部署tomcat时,在接收用户请求之前,会进行以下初始化过程
- 部署在web.xml文件里由
<listener>
元素标记事件的监听器会被创建和初始化 - 对于所有事件监听器,如果实现了
ServletContextListener
接口,将会执行其实现的contextInitialized()
方法 - 部署描述文件由
<filter>
元素标记的过滤器会被创建和初始化,并调用其init()
方法 - 部署在描述文件由
<servlet>
元素标记的servlet会根据<load-on-start>
的权值按顺序创建并初始化,并调用其init()
方法
三、启动流程
以下为一个常见的web.xml
配置进行SpringMVC启动流程分析
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd" >
<web-app>
<display-name>spring-demo</display-name>
<!--全局变量-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!--监听器-->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!--乱码filter-->
<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>utf-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/**</url-pattern>
</filter-mapping>
<!--dispatcherServlet-->
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
1、Listener初始化过程
首先定义了一个<context-param>
, 用于配置一个全局的变量,<context-param>
标签内容读取后会放进application中,作为web应用的全局变量使用,接下来创建listener
时会使用这个变量,因此,web应用在启动时,会先读取这个变量,之后才进行下一步,接着定义了一个ContextLoaderListener
的listener
,这个listener的源码为
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
public ContextLoaderListener() {
}
public ContextLoaderListener(WebApplicationContext context) {
super(context);
}
public void contextInitialized(ServletContextEvent event) {
this.initWebApplicationContext(event.getServletContext());
}
public void contextDestroyed(ServletContextEvent event) {
this.closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
ContextLoaderListener类继承了ContextLoader类并实现了ServletContextListener接口,首先看一下前面讲述的ServletContextListener接口源码:
public interface ServletContextListener extends EventListener {
void contextInitialized(ServletContextEvent var1);
void contextDestroyed(ServletContextEvent var1);
}
该接口只有两个方法contextInitialized
和contextDestroyed
,这里采用的是观察者模式,也称为为订阅-发布模式,实现了该接口的listener会向发布者进行订阅,当Web应用初始化或销毁时会分别调用上述两个方法, ContextLoaderListener的contextInitialized()
方法直接调用了initWebApplicationContext()
方法,这个方法是继承自ContextLoader类,通过函数名可以知道,该方法是用于初始化Web应用上下文,即IOC容器,这里使用的是代理模式,继续查看ContextLoader类的initWebApplicationContext()方法的源码如下:
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
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!");
} else {
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
if (logger.isInfoEnabled()) {
logger.info("Root WebApplicationContext: initialization started");
}
long startTime = System.currentTimeMillis();
try {
if (this.context == null) {
this.context = this.createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)this.context;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
ApplicationContext parent = this.loadParentContext(servletContext);
cwac.setParent(parent);
}
this.configureAndRefreshWebApplicationContext(cwac, 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.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
} catch (Error | RuntimeException var8) {
logger.error("Context initialization failed", var8);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, var8);
throw var8;
}
}
}
initWebApplicationContext()方法如上讲述,主要目的就是创建root WebApplicationContext对象即IOC容器
,其中比较重要的就是,整个Web应用如果存在IOC容器则有且只能有一个,根IOC容器
作为全局变量存储在ServletContext
即application
对象中。将根IOC容器
放入到application对象之前进行了IOC容器
的配置和刷新操作,调用了configureAndRefreshWebApplicationContext()
方法,该方法源码如下:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
String configLocationParam;
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
configLocationParam = sc.getInitParameter("contextId");
if (configLocationParam != null) {
wac.setId(configLocationParam);
} else {
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX + ObjectUtils.getDisplayString(sc.getContextPath()));
}
}
wac.setServletContext(sc);
configLocationParam = sc.getInitParameter("contextConfigLocation");
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment)env).initPropertySources(sc, (ServletConfig)null);
}
this.customizeContext(sc, wac);
wac.refresh();
}
比较重要的就是获取到了web.xml中的<context-param>
标签配置的全局变量contextConfigLocation
,并最后一行调用了refresh()
方法,ConfigurableWebApplicationContext
是一个接口,通过对常用实现类ClassPathXmlApplicationContext
逐层查找后可以找到一个抽象类AbstractApplicationContext实现了refresh()
方法,其源码如下
public void refresh() throws BeansException, IllegalStateException {
synchronized(this.startupShutdownMonitor) {
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
this.initMessageSource();
this.initApplicationEventMulticaster();
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var9) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var9);
}
this.destroyBeans();
this.cancelRefresh(var9);
throw var9;
} finally {
this.resetCommonCaches();
}
}
}
该方法主要用于创建并初始化contextConfigLocation
类配置的xml文件中的Bean
,因此,如果我们在配置Bean时出错,在Web应用启动时就会抛出异常
,而不是等到运行时才抛出异常
整个ContextLoaderListener
类的启动过程到此就结束了,可以发现,创建ContextLoaderListener
是比较核心的一个步骤,主要工作就是为了创建根IoC容器
并使用特定的key将其放入到application对象中,供整个Web应用使用,由于在ContextLoaderListener类中构造的IOC容器
配置的Bean是全局共享的,因此,在标识的contextConfigLocation
的xml配置文件一般包括: 数据库DataSource、DAO层、Service层、事务等相关Bean
2、Filter的初始化
在监听器listener
初始化完成后,接下来会进行filter的初始化操作,GenericFilterBean
是任何类型的过滤器的一个比较方便的超类,这个类主要实现的就是从web.xml
文件中取得init-param中
设定的值,然后对Filter进行初始化(当然,其子类可以覆盖init()
方法)。
Filter的生命周期如下:
- void init(FilterConfig config): 用于完成Filter 的初始化
- void destroy(): 用于Filter 销毁前,完成某些资源的回收
- void doFilter(ServletRequest request, ServletResponse response,FilterChain chain): 实现过滤功能,该方法就是对每个请求及响应增加的额外处理。 过滤器Filter也具有生命周期:init()->doFilter()->destroy(),由部署文件中的filter元素驱动
3、Servlet的初始化
Web应用启动的最后一个步骤就是创建和初始化相关Servlet
,在开发中常用的Servlet就是DispatcherServlet
类前端控制器,前端控制器作为中央控制器是整个Web应用的核心,用于获取分发用户请求并返回响应DispatcherServlet
类的间接父类实现了Servlet
接口,因此其本质上依旧是一个Servlet
。DispatcherServlet
设计很巧妙,上层父类不同程度的实现了相关接口的部分方法,并留出了相关方法用于子类覆盖,将不变的部分统一实现,将变化的部分预留方法用于子类实现
具体顺序图DispatcherServelt
类的本质是Servlet
,通过文章开始的讲解可知,在Web应用部署到容器后进行Servlet
初始化时会调用相关的init(ServletConfig)
方法,因此,DispatchServlet
类的初始化过程也由该方法开始。上述调用逻辑中比较重要的就是FrameworkServlet
抽象类中的initServletBean()
方法、initWebApplicationContext()
方法以及DispatcherServlet
类中的onRefresh()
方法
a、initServletBean()
protected final void initServletBean() throws ServletException {
this.getServletContext().log("Initializing Spring " + this.getClass().getSimpleName() + " '" + this.getServletName() + "'");
if (this.logger.isInfoEnabled()) {
this.logger.info("Initializing Servlet '" + this.getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {
this.webApplicationContext = this.initWebApplicationContext();
this.initFrameworkServlet();
} catch (RuntimeException | ServletException var4) {
this.logger.error("Context initialization failed", var4);
throw var4;
}
if (this.logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ? "shown which may lead to unsafe logging of potentially sensitive data" : "masked to prevent unsafe logging of potentially sensitive data";
this.logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails + "': request parameters and headers will be " + value);
}
if (this.logger.isInfoEnabled()) {
this.logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
该方法是重写了父类HttpServletBean
抽象类的initServletBean()
方法,HttpServletBean
抽象类在执行init()
方法时会调用initServletBean()
方法,由于多态的特性,最终会调用其子类FrameworkServlet
抽象类的initServletBean()
方法。该方法由final标识,子类就不可再次重写了。该方法中比较重要的就是initWebApplicationContext()
方法的调用,该方法仍由FrameworkServlet
抽象类实现
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(this.getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext)wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
this.configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = this.findWebApplicationContext();
}
if (wac == null) {
wac = this.createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
synchronized(this.onRefreshMonitor) {
this.onRefresh(wac);
}
}
if (this.publishContext) {
String attrName = this.getServletContextAttributeName();
this.getServletContext().setAttribute(attrName, wac);
}
return wac;
}
该方法的主要作用同样是创建一个WebApplicationContext
对象,即IOC容器
,不过前文讲过每个Web应用最多只能存在一个IOC容器
,这里创建的则是特定Servlet
拥有的子IOC容器
。
父子IOC容器的访问特性
父子容器类似于类的继承关系,子类可以访问父类中的成员变量,而父类不可访问子类的成员变量,同样的,子容器可以访问父容器中定义的Bean,但父容器无法访问子容器定义的Bean,根IOC容器
做为全局共享的IOC容器
放入Web应用需要共享的Bean,而子IOC容器
根据需求的不同,放入不同的Bean,这样能够做到隔离
,保证系统的安全性
DispatcherServlet
类的子IOC容器
创建过程,如果当前Servlet存在一个IOC容器
则为其设置根IOC容器
作为其父类,并配置刷新该容器,用于构造其定义的Bean,这里的方法与前文讲述的根IOC容器
类似,同样会读取用户在web.xml
中配置的<servlet>
中的<init-param>
值,用于查找相关的xml配置文件用于构造定义的Bean。如果当前Servlet不存在一个子IoC容器就去查找一个,如果仍然没有查找到则调用
createWebApplicationContext()方法去创建
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = this.getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Fatal initialization error in servlet with name '" + this.getServletName() + "': custom WebApplicationContext class [" + contextClass.getName() + "] is not of type ConfigurableWebApplicationContext");
} else {
ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(this.getEnvironment());
wac.setParent(parent);
String configLocation = this.getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
this.configureAndRefreshWebApplicationContext(wac);
return wac;
}
}
该方法用于创建一个子IOC容器
并将根IOC容器
做为其父容器,接着进行配置和刷新操作用于构造相关的Bean。至此,根IOC容器
以及相关Servlet
的子IOC容器
已经配置完成,子容器中管理的Bean一般只被该Servlet使用,因此,其中管理的Bean一般是局部
的,如SpringMVC中需要的各种重要组件,包括Controller、Interceptor、Converter、ExceptionResolver等
当IOC子容器
构造完成后调用了onRefresh()
方法,该方法的调用与initServletBean()
方法的调用相同,由父类调用但具体实现由子类覆盖,调用onRefresh()
方法时将前文创建的IOC子容器作为参数传入
protected void onRefresh(ApplicationContext context) {
this.initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
this.initMultipartResolver(context);
this.initLocaleResolver(context);
this.initThemeResolver(context);
this.initHandlerMappings(context);
this.initHandlerAdapters(context);
this.initHandlerExceptionResolvers(context);
this.initRequestToViewNameTranslator(context);
this.initViewResolvers(context);
this.initFlashMapManager(context);
}
onRefresh()
方法直接调用了initStrategies()
方法,源码如上,通过函数名可以判断,该方法用于初始化创建multipartResovle
来支持图片等文件的上传、本地化解析器、主题解析器、HandlerMapping处理器映射器、HandlerAdapter
处理器适配器、异常解析器、视图解析器、flashMap管理器等,这些组件都是SpringMVC开发中的重要组件,相关组件的初始化创建过程均在此完成。
到此,初始化就全部结束了
四、总结
在Spring的web容器启动时会去读取web.xml
文件,相关启动顺序为:<context-param> --> <listener> --> <filter> --> <servlet>
,具体为:
- 1、解析
<context-param>
键值对 - 2、创建一个application对象即ServletContext,servlet上下文,用于全局共享
- 3、将
<context-param>
键值对放入ServletContext中,web应用全局共享 - 4、读取
<listener>
标签,创建监听器,一般使用ContextLoaderListener
,如果使用了ContextLoaderListener
,Spring就会创建一个WebApplicationContext对象,这个就是IOC容器
,ContextLoaderListener
创建的IOC容器
是全局共享的,并将其放在ServletContext
中, 键名为WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
, 读取web.xml
文件里的contextConfigLocation
配置中的xml文件来创建bean - 5、listener创建完毕后如果有Filter会去创建Filter
- 6、初始化Servlet,一般使用DispatchServlet类
- 7、DispatchServlet的父类FrameworkServlet会重写其父类的initServletBean方法,并调用
initWebApplicationContext()
以及onRefresh()
方法 - 8、
initWebApplicationContext()
方法会创建一个当前servlet的一个IOC子容器,如果存在上述的全局WebApplicationContext
则将其设置为父容器,如果不存在上述全局的则父容器为null。 - 9、读取
<servlet>
标签的<init-param>
配置的xml文件并加载相关Bean - 10、
onRefresh()
方法创建Web应用相关组件
好了,本文到此就结束了,篇幅过长,如果其中有误,可以在下方评论留言,也希望可以关注我哦