Spring容器以及SpringMvc是怎么起作用的?
1.问题的提出
学习了一段时间的SSM框架的使用,但是对原理却一窍不通,最近在工作学习之余阅读了郝佳老师的《Spring源码深度解析 第2版》这本书,笔者头脑有些愚钝,书中仍有部分不太明白,但终究还是了解到了Spring与SpringMVC的大体上的原理,于是便考虑把自己的收获‘趁热’总结一下,记录出来。以下陈述均属于个人观点,但是难免因为我的个人认知能力问题导致所阐述信息与实际不符,如果您发现下面内容有哪里不对的地方,希望积极指出,谢谢!
在学习ssm框架的初期,我的大脑中常常会冒出来几个疑问,springMvc到底是怎么就到了我们的web环境中呢?为什么导入jar包以后写几个配置文件就能够直接使用Spring与SpringMvc了呢?
如果看官您也有着和我一样的疑问,那么请跟着我的思路,站在源码的角度一起来分析这几个疑问。
2.Spring容器的初始化
我们在JavaWeb项目中搭建SpringMVC环境时候首先做的就是在web.xml文件中添加一个ContextLoaderListener监听器和一对context-param。
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:application.xml</param-value>
</context-param>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
正是这个ContextLoaderListener监听器帮我们在web环境中实例化了一个Spring容器,并且配置到了我们的web环境中。下面我们点开源码看一下这个监听器都做了什么。
首先使用IDEA的Ctrl + H查看一下该类的继承结构。如下图
ContextLoaderListener继承ContextLoader类,实现ServletContextListener接口。
ServletContextListener接口中有一个contextInitialized(ServletContextEvent event)方法。
/**
* 接收正在启动web应用程序初始化过程的通知。
* 在初始化web应用程序中的任何过滤器或servlet之前,所有servletcontextlistener都会收到上下文初始化
* 的通知。
* @param event 包含正在初始化的ServletContext的ServletContextEvent
*/
public void contextInitialized(ServletContextEvent event){
initWebApplicationContext(event.getServletContext());
}
正是上面的initWebApplicationContex(ServletContextEvent event)t方法帮我们在web应用程序启动的时候初始化了一个Spring容器。而这个initWebApplicationContext方法是父类ContextLoader中的方法,我们进去看一下,下面代码已经把部分无关代码去除。关键代码上面加了注释。
public class ContextLoader {
private WebApplicationContext context;
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
try {
if (this.context == null) {
// 在这里生成的一个context
this.context = createWebApplicationContext(servletContext);
}
if (this.context instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
if (!cwac.isActive()) {
// 在这里面将web.xml中param-name为contextConfigLocation的值取出来,配置到Spring容器中
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 把我们的spring容器,添加到全局的servletContext内。没这一步,后面我们就根本找不到spring容器
// key = org.springframework.web.context.WebApplicationContext.ROOT
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
return this.context;
}
catch (RuntimeException | Error ex) {...}
}
public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation";
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
...
// 获取到web.xml中param-name为contextConfigLocation的值取出来,配置到Spring容器
String configLocationParam = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocationParam != null) {
wac.setConfigLocation(configLocationParam);
}
...
}
}
上面代码大概做了三个事情:
- 在initWebApplicationContext方法中初始化了一个Spring容器,然后。
- 调用configureAndRefreshWebApplicationContext方法将我们在web.xml中配置的contextConfigLocation添加到了容器的属性中.
- 将初始化好的spring容器添加到了全局的servletContext的Attribute中,key为org.springframework.web.context.WebApplicationContext.ROOT
综上,Spring容器在JavaWeb中的初始化其实就是由我们自己配置的ContextLoaderListener监听器来完成,而ContextLoaderListener实现了ServletContextListener接口,在web程序初始化的时候会调用监听器的初始化方法,从而完成Spring容器的初始化操作。
3.SpringMVC容器的初始化
同上配置spring一样,我们在web环境配置mvc时候,首先也是去web.xml中做些‘手脚’,通常我们都会配置一个servlet到web环境,并且将‘/’所代表的全部请求路径交给这个servlet处理。在实例化servlet时候,还给出了一个init-param,key为contextConfigLocation,value就是配置文件的路径。
<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:springMVC.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
接下来让我们去看一下这个特殊的servlet中到底做了什么。
第一步还是先看DispatcherServlet的继承结构。
从上图可以看出DispatcherServlet继承自FrameworkServlet,间接继承了HttpServletBean和GenericServlet。那我们就从GenericServlet开始说起。
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
/**
* 由servlet容器(例如tomcat内的servlet容器)调用,以指示servlet正在将servlet放入服务中
*/
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
// NOOP by default
}
}
在GenericServlet 中的init()方法是重点,在servlet容器(可以暂时理解成tomcat)初始化servlet的时候,会调用这个init(ServletConfig config)方法,而这个有参方法会调用其重载的无参init()方法,在GenericServlet 类中,这个方法内是空方法体,以供子类去重写该方法来实现更多内容。我们就顺藤摸瓜看看是谁Override了这个init方法。
public abstract class HttpServletBean extends HttpServlet
implements EnvironmentCapable, EnvironmentAware {
@Override
public final void init() throws ServletException {
...
// Let subclasses do whatever initialization they like.
initServletBean();
}
}
果不其然,在HttpServletBean类中Override了init()方法,并且在最后又调用了initServletBean()方法,在该类中initServletBean()方法也是个空方法体的方法,仍然是留给子类去重写的,我们继续看是谁Override了这个initServletBean方法。
public abstract class FrameworkServlet extends HttpServletBean implements
ApplicationContextAware {
/**
* Overridden method of {@link HttpServletBean}, invoked after any bean properties
* have been set. Creates this servlet's WebApplicationContext.
*/
@Override
protected final void initServletBean() throws ServletException {
...
try {
// 这里初始化了MVC容器
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
...
}
protected WebApplicationContext initWebApplicationContext() {
// 这里就是从当前的servletContext中获取Spring容器。
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(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) {
// 将Spring容器设置为当前web容器的父容器
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
...
return wac;
}
}
public abstract class WebApplicationContextUtils {
/**
* 从ServletContext 中获取key为org.springframework.web.context.WebApplicationContext.ROOT的对象
*/
public static WebApplicationContext getWebApplicationContext(ServletContext sc) {
return getWebApplicationContext(sc, WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
}
}
从上面代码中我们可以看到,FrameworkServlet 类的initServletBean()方法又调用了initWebApplicationContext()方法,在initWebApplicationContext方法中,通过WebApplicationContextUtils.getWebApplicationContext(getServletContext());获取到了我们之前配置到servletContext中的Spring容器,并且把他设置为了我们当前MVC容器的父容器。
3.总结
综合上面两个部分的内容,我们可以了解到,其实SpringMvc的核心就是一个特殊的Servlet,在其初始化的时候,经过层层调用,最终在其父类FrameworkServlet的 initWebApplicationContext()方法中实将配置好的Spring容器取了出来作为SpringMvc容器(本文并没有讲解mvc容器的细节)的父容器。而这一操作对于Spring容器来说是无感知的,他并不知道我们把它作为了自己的父容器,但是mvc容器却能够知道父容器的存在,于是,便有了‘子容器可以访问父容器的bean,父容器不能访问子容器的bean’的这一说法。
经过了上面的这几个步骤,我们的web环境中,就已经搭建好了Spring与SpringMvc环境,并且SpringMvc已经接管了所有的url请求。至于SpringMvc是如何处理请求的,以及它的原理,我将会在以后更新在我的博客。
对于本文如果您发现我哪里与实际情况有所出入,欢迎在下面评论区指出,我会仔细斟酌并加以改正,谢谢。