联系博主


你的名字:
Email:
建议:

SSM之Spring和SpringMVC整合原理(父子容器启动流程)

编辑时间:2019-07-21      赞:1       踩:0

导言:

    在Spring-SpringMVC-MyBatis框架中,Spring是整个框架的核心,把SpringMVC和MyBatis糅合在一起。我之前写过一篇关于Spring和MyBatis整合原理的文章。因此,本文主要讲述Spring和SpringMVC的整合原理。网上虽然有很多讲述Spring启动原理、Spring IOC容器启动原理、SSM 父子容器加载流程的文章,但是讲得可能比较混乱,我会尝试把比较关键的节点解析清楚,尽力地把其中门门道道给梳理清除。友情提示,文中有大量的代码,如果看不习惯可以打开Eclipse或IDEA查看SSM的工程代码。

正文:

    对于Java Web项目而言,web.xml往往是整个项目的入口。因此,要解析Spring和SpringMVC的整合原理,web.xml这个配置文件是一个很好的切入口。

<!-- 初始化spring容器 -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:spring/applicationContext-*.xml</param-value>
	</context-param>
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
<!-- 前端控制器 -->
	<servlet>
		<servlet-name>kshop-sso-web</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<!-- contextConfigLocation不是必须的, 如果不配置contextConfigLocation, springmvc的配置文件默认在:WEB-INF/servlet的name+"-servlet.xml" -->
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:spring/springmvc*.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>
	<servlet-mapping>
		<servlet-name>kshop-sso-web</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>

     在web容器启动时,会触发容器初始化事件,此时contextLoaderListener会监听到这个事件,其contextInitialized方法会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,在缺乏相应配置的情况下,其默认的实现类是XmlWebApplicationContext。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的context-param标签指定。在这个IoC容器初始化完毕后,spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE为属性Key,将其存储到ServletContext中,便于获取。关键代码解析如下:

public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
	@Override
	public void contextInitialized(ServletContextEvent event) {
 	   initWebApplicationContext(event.getServletContext());
	}
	
	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!");
		}

		·····

		try {
			// Store context in local instance variable, to guarantee that
			// it is available on ServletContext shutdown.
			if (this.context == null) {
			// 创建一个容器,并将servletContext上下文作为参数传递进去,
				this.context = createWebApplicationContext(servletContext);
			}
			if (this.context instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) this.context;
				if (!cwac.isActive()) {
					// The context has not yet been refreshed -> provide services such as
					// setting the parent context, setting the application context id, etc
					if (cwac.getParent() == null) {
						// The context instance was injected without an explicit parent ->
						// determine parent for root web application context, if any.
						ApplicationContext parent = loadParentContext(servletContext);
						cwac.setParent(parent);
					}
				
					//IOC容器的启动是通过AbstractApplicationContext的refresh()方法进行启动的,这个方法标准IoC容器的正式启动.
						//该函数会调用AbstractApplicationContext.refresh()
					configureAndRefreshWebApplicationContext(cwac, servletContext);
				}
			}
			//把新建的WebApplicationContext作为根上下文存放在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);
			}

			return this.context;
		}
		catch (RuntimeException ex) {
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
			throw ex;
		}
		catch (Error err) {			 
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, err);
			throw err;
		}
	}
}	


    至此,Spring根容器就初始化完成,而这便是是在SSM框架中常提到的父子容器中的父容器的真面目。那么父子容器中的子容器又是什么呢?它又是何时初始化的呢?我们回到web.xml中再次探究其奥秘。
    在web.xml中,我们除了配置了ContextLoaderListener外,还配置了DispatcherServlet。因此,我们可以合理地猜测,父子容器中的子容器和DispatcherServlet息息相关。接下来,我将为你讲述DispatcherServlet的初始化流程。
    在contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以最常见的DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是XmlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为Key,而是通过一些转换,具体可自行查看源码)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文定义的那些bean。
    上面是整个流程的文字描述,下面我会结合代码去讲解DispatcherServlet是如何初始化的。在看代码前,我们必须先了解DispatcherServlet的继承体系。DispatcherServlet继承体系如下图:

    众所周知,Servlet容器在创建Servlet的时候会调用Servlet的init方法。DispatcherServlet的init方法的是实现不在DispatcherServlet中,而是在其继承的HttpServletBean中,代码如下:

public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {
	/**
	 * Map config parameters onto bean properties of this servlet, and
	 * invoke subclass initialization.
	 * @throws ServletException if bean properties are invalid (or required
	 * properties are missing), or if subclass initialization fails.
	 */
	@Override
	public final void init() throws ServletException {
		// Set bean properties from init parameters.
		PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
        //把当前Servlet封装成一个BeanWrapper在把它交给Spring管理
		if (!pvs.isEmpty()) {
			try {
				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()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}

		// 重点是这句,子类可以根据自己的需要,在初始化的的时候可以复写这个方法,而不再是init方法了
		initServletBean();
	}
}

         通过上面的代码,我们可以发现,Servlet的初始化方法由init变为initServletBean了,而且该方法的实现在其子类。因此我们只需要再看看initServletBean()方法的实现即可,它是由FrameworkServlet去实现的:

@SuppressWarnings("serial")
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
	@Override
	protected final void initServletBean() throws ServletException {
		//忽略部分无关紧要的代码
		·········
		
		try {
			// 初始化容器,这也是父子容器中的子容器,重点
			this.webApplicationContext = initWebApplicationContext();
            //与initServletBean同理,给子类去复写初始化所需要的操作 。一般都为空实现即可,除非自己要复写DispatcherServlet,做自己需要做的事
			initFrameworkServlet();
		}
		catch (ServletException | RuntimeException ex) {
			logger.error("Context initialization failed", ex);
			throw ex;
		}
		//忽略部分无关紧要的代码
		·········
	}
    protected WebApplicationContext initWebApplicationContext() {
        //通过ServletContext获取根容器,也即父子容器中的父容器
		WebApplicationContext rootContext =
				WebApplicationContextUtils.getWebApplicationContext(getServletContext());
		WebApplicationContext wac = null;
		//如果Servlet的容器已创建完毕,基于注解驱动的SpringMVC配置会进去该分支
        //该分支里面所进行的操作与Spring中类似,都是完成容器的初始化、刷新工作
		if (this.webApplicationContext != null) {
			// A context instance was injected at construction time -> use it
			wac = this.webApplicationContext;
			if (wac instanceof ConfigurableWebApplicationContext) {
				ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
				if (!cwac.isActive()) {
					// The context has not yet been refreshed -> provide services such as
					// setting the parent context, setting the application context id, etc
					if (cwac.getParent() == null) {
						// The context instance was injected without an explicit parent -> set
						// the root application context (if any; may be null) as the parent
                         //重要,这里便将Spring的容器注册为自己的父容器,父子关系在此确立,也是父子容器名称的由来
						cwac.setParent(rootContext);
					}
                    //IOC容器的入口,要了解SpringMVC容器的IOC容器初始化流程,可以在这里下个断点
					configureAndRefreshWebApplicationContext(cwac);
				}
			}
		}
        //若是web.xml方式,会执行以下分支
		if (wac == null) {
			// No context instance was injected at construction time -> see if one
			// has been registered in the servlet context. If one exists, it is assumed
			// that the parent context (if any) has already been set and that the
			// user has performed any initialization such as setting the context id
   			//该函数会通过WebApplicationContextUtils工具类去寻找是否已经有创建好的容器
			wac = findWebApplicationContext();
		}
		if (wac == null) {
            //如果上面没有找到创建好的容器,那么会新建一个容器,会把根容器传进去
			// No context instance is defined for this servlet -> create a local one
			wac = createWebApplicationContext(rootContext);
		}

		if (!this.refreshEventReceived) {
			// Either the context is not a ConfigurableApplicationContext with refresh
			// support or the context injected at construction time had already been
			// refreshed -> trigger initial onRefresh manually here.
			synchronized (this.onRefreshMonitor) {
				onRefresh(wac);
			}
		}

		if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
		}

		return wac;
	}

	/**
	 * Retrieve a {@code WebApplicationContext} from the {@code ServletContext}
	 * attribute with the {@link #setContextAttribute configured name}. The
	 * {@code WebApplicationContext} must have already been loaded and stored in the
	 * {@code ServletContext} before this servlet gets initialized (or invoked).
	 * <p>Subclasses may override this method to provide a different
	 * {@code WebApplicationContext} retrieval strategy.
	 * @return the WebApplicationContext for this servlet, or {@code null} if not found
	 * @see #getContextAttribute()
	 */
	@Nullable
	protected WebApplicationContext findWebApplicationContext() {
		String attrName = getContextAttribute();
		if (attrName == null) {
			return null;
		}
		WebApplicationContext wac =
				WebApplicationContextUtils.getWebApplicationContext(getServletContext(), attrName);
		if (wac == null) {
			throw new IllegalStateException("No WebApplicationContext found: initializer not registered?");
		}
		return wac;
	}
/**
	 * Instantiate the WebApplicationContext for this servlet, either a default
	 * {@link org.springframework.web.context.support.XmlWebApplicationContext}
	 * or a {@link #setContextClass custom context class}, if set.
	 * <p>This implementation expects custom contexts to implement the
	 * {@link org.springframework.web.context.ConfigurableWebApplicationContext}
	 * interface. Can be overridden in subclasses.
	 * <p>Do not forget to register this servlet instance as application listener on the
	 * created context (for triggering its {@link #onRefresh callback}, and to call
	 * {@link org.springframework.context.ConfigurableApplicationContext#refresh()}
	 * before returning the context instance.
	 * @param parent the parent ApplicationContext to use, or {@code null} if none
	 * @return the WebApplicationContext for this servlet
	 * @see org.springframework.web.context.support.XmlWebApplicationContext
	 */
    //创建一个容器,和Spring的创建方法类似
	protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
        //默认返回XmlWebApplicationContext.class,和Spring的默认容器是同一个类
		Class<?> contextClass = getContextClass();
		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());
        //把传进来的Spring容器设置为父容器,确立父子关系
		wac.setParent(parent);
		String configLocation = getContextConfigLocation();
		if (configLocation != null) {
			wac.setConfigLocation(configLocation);
		}
        //配置并刷新容器
		configureAndRefreshWebApplicationContext(wac);

		return wac;
	}
}
    至此SSM框架的父子容器便创建完毕了。值得一提的是,FrameworkServlet在configureAndRefreshWebApplicationContext()方法中注册了一个ContextRefreshListener,该listener会监听ContextRefreshedEvent。ContextRefreshedEvent 事件会在容器初始化完成时触发(ps:对于web应用会出现父子容器,这样就会触发两次)。ContextRefreshListener在收到事件是会最终调用到FrameworkServlet的onRefresh()方法,并把应用上下文传进去。FrameworkServlet的OnRefresh()是一个空实现,这个其实是预留给子类覆盖的一个方法。子类如果要在容器初始化后进行某些操作可以覆盖该方法,而DispatcherServlet刚好就覆盖了该方法。


public class DispatcherServlet extends FrameworkServlet {
	/**
	 * This implementation calls {@link #initStrategies}.
	 */
	@Override
	protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
	}

	/**
	 * Initialize the strategy objects that this servlet uses.
	 * <p>May be overridden in subclasses in order to initialize further strategy objects.
	 */
	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}
}
    如果你理解SpringMVC处理Http请求的流程后,相信你对DispatcherServlet的onRefresh()方法中初始化的一些注解并不陌生,比如HandlerAdapter、ViewResolver等等。至于这些组件是如何协同起来一起工作的,你可以去查看DispatcherServlet中的doService()方法,我在这里就不再赘述了。
    既然说起了SSM的父子容器,那么我就在聊一下SSM的一些相关知识吧。
    先说最重要的一点基础知识,在父子容器中,子容器可以获取父容器的bean对象,父容器不可获取子容器的bean。为什么呢?如果理解了我在上文中讲述的父子容器启动流程,你应该能发现父容器并没有进行保存其子容器引用的操作,而子容器则会保存其父容器的引用。因此,父容器无法获取注册在子容器中的Bean,反之则毫无问题。
    还有一点比较重要的是,在实际工程中,会包括很多配置,根据不同的业务模块来划分,所以我们一般思路是各负其责,明确边界,Spring根容器负责所有其他非Controller(比如DAO、Service)的Bean的注册,而SpringMVC只负责controller相关的Bean的注册。为什么要这样做呢?如果不这样做会导致什么后果吗?
    在SpringMVC处理Http请求的流程中,我们需要RequestMappingHandlerMapping加载@RequestMapping。RequestMappingHandlerMapping继承了AbstractHandlerMethodMapping这个抽象类。SpringMVC初始化时,会寻找所有当前容器中的所有@Controller注解的Bean,来确定其是否是一个handler,而这些操作都是在AbstractHandlerMethodMapping中的initHandlerMethods()进行的。注意看下面的代码,detectHandlerMethodsInAncestorContexts这个Switch,它主要控制从那里获取容器中的bean,是否包括父容器,默认是不包括的。也就是说,DispatcherServlet不会在父容器中寻找Controller。也就是说,如果把Controller注册在父容器,SpringMVC在处理Http请求时将无法找到Controller。当然这也是有解决方法的,大家可以看看下面这篇文章:https://www.jianshu.com/p/88bd9292cb7d
public abstract class AbstractHandlerMethodMapping<T> extends AbstractHandlerMapping implements InitializingBean {
	/**
	 * Scan beans in the ApplicationContext, detect and register handler methods.
	 * @see #isHandler(Class)
	 * @see #getMappingForMethod(Method, Class)
	 * @see #handlerMethodsInitialized(Map)
	 */
	protected void initHandlerMethods() {
		··········
		//
		String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
				BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
				getApplicationContext().getBeanNamesForType(Object.class));
				
		·········
	}
}

    如果这篇文章有哪里描述错误的话,可以通过联系作者的方式给我提出你宝贵的意见,O(∩_∩)O谢谢!如果觉得这篇文章写得不错的话,可以点个赞喔。你的每一个赞都是我写作的动力,O(∩_∩)O谢谢!

    最后,十分建议本文的读者可以阅读或Debug一下源码。

总结:
    当ContextLoaderListener和DispatcherServlet一起使用时, ContextLoaderListener 先创建一个根applicationContext,然后DispatcherSerlvet创建一个子applicationContext并且绑定到根applicationContext

    子上下文可以访问父上下文中的bean,但是父上下文不可以访问子上下文中的bean。

    在实际工程中,会包括很多配置,根据不同的业务模块来划分,所以我们一般思路是各负其责,明确边界,Spring根容器负责所有其他非Controller(比如DAO、Service)的Bean的注册,而SpringMVC只负责controller相关的Bean的注册。


参考资料:

RequestMappingHandlerMapping相关 :https://www.jianshu.com/p/8d60bf614200

SSM父子容器启动流程:https://blog.csdn.net/f641385712/article/details/87883205

SSM父子容器启动流程:https://blog.csdn.net/caomiao2006/article/details/51290494

DispatcherServlet相关:https://blog.csdn.net/qq924862077/article/details/52809312



转载请注明:
    本文转载自:www.kantblog.com/blog/WebDevelop/7

Kant©2016 All rights reserved 粤ICP备16014517号