java web中各种context的关系
我举得这篇文章解决了我的很多疑惑,理清了我以前不太清楚的Context关系,读懂这篇文章很有助于理解源码,
原文链接在这里:https://www.jianshu.com/p/2537e2fec546
我把它转载在自己博客里,害怕以后找不到,原文如下
网上博客中看到一句话,很形容的描绘了web程序和上下文的关系,这里引用一下来说明:如果对“上下文”不太了解的,我这边说下,程序里面所谓的“上下文”就是程序的执行环境,打个比方:你有家吧?如果家都没有就别学编程了,租的也行啊!你就相当于web程序,家就相当于web程序的上下文,你可以在家里放东西,也可以取东西,你的衣食住行都依赖这个家,这个家就是你生活的上下文环境。
该博客地址: Spring和SpringMVC配置中父子WebApplicationContext的关系
Spring启动过程
第一步:
首先,对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext,其为后面的spring IoC容器提供宿主环境;
第二步:
其次,在web.xml中会提供有contextLoaderListener。在web容器启动时,会触发容器初始化事件,此时contextLoaderListener会监听到这个事件,其contextInitialized方法会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是XmlWebApplicationContext。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的context-param标签指定。在这个IoC容器初始化完毕后,spring以【WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE】为属性Key,将其存储到ServletContext中,便于获取;
第三步:
再次,contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以最常见的DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用【WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE】先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文(有个parent属性作为对Spring的ApplicationContext的引用)。有了这个parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是XmlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为Key,而是通过一些转换,具体可自行查看源码)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定义的那些bean。
通过上面这个Spring的启动过程,我们可以清楚的了解ServletContext、WebApplicationContext、XmlWebApplicationContext、以及DispatcherServlet上下文之间的关系,并且会将WebApplicationContext放在ServletContext中。
1. ServletContext:
首先说说ServletContext这个web应用级的上下文。web容器(比如tomcat、jboss、weblogic等)启动的时候,它会为每个web应用程序创建一个ServletContext对象 它代表当前web应用的上下文(注意:是每个web应用有且仅创建一个ServletContext,一个web应用,就是你一个web工程)。一个web中的所有servlet共享一个ServletContext对象,所以可以通过ServletContext对象来实现Servlet之间的通讯。在一个继承自HttpServlet对象的类中,可以通过this.getServletContext来获取。
2. WebApplicationContext:
通过源码详细说明一下 第二步 的过程,web.xml(上图)中我们配置了ContextLoaderListener,该listener实现了ServletContextListener的contextInitialized方法用来监听Servlet初始化事件:
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/SpringApplicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener>
由下面的源码可以发现初始化的是WebApplicationContext的IoC容器,它是一个接口类,其默认实现是XmlWebApplicationContext。
public class ContextLoaderListener extends ContextLoader implements ServletContextListener { public ContextLoaderListener() { } public ContextLoaderListener(WebApplicationContext context) { super(context); } @Override public void contextInitialized(ServletContextEvent event) { initWebApplicationContext(event.getServletContext()); } @Override public void contextDestroyed(ServletContextEvent event) { closeWebApplicationContext(event.getServletContext()); ContextCleanupListener.cleanupAttributes(event.getServletContext()); } }
这是ContextLoaderListener中的contextInitialized()方法,这里主要是用initWebApplicationContext()方法来初始化WebApplicationContext。这里涉及到一个常用类WebApplicationContext:它继承自ApplicationContext,在ApplicationContext的基础上又追加了一些特定于Web的操作及属性。
initWebApplicationContext(event.getServletContext()),进行了创建根上下文,并将该上下文以key-value的方式存储到ServletContext中。
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!"); } Log logger = LogFactory.getLog(ContextLoader.class); servletContext.log("Initializing Spring root WebApplicationContext"); if (logger.isInfoEnabled()) { logger.info("Root WebApplicationContext: initialization started"); } long startTime = System.currentTimeMillis(); try { // Store context in local instance variable, to guarantee that // it is available on ServletContext shutdown. if (this.context == null) { 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); } 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.isDebugEnabled()) { logger.debug("Published root WebApplicationContext as ServletContext attribute with name [" + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE + "]"); } if (logger.isInfoEnabled()) { long elapsedTime = System.currentTimeMillis() - startTime; logger.info("Root WebApplicationContext: initialization completed in " + elapsedTime + " ms"); } 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; } }
在initWebApplicationContext()方法中主要体现了WebApplicationContext实例的创建过程。首先,验证WebApplicationContext的存在性,通过查看ServletContext实例中是否有对应key的属性验证WebApplicationContext是否已经创建过实例。如果没有通过,createWebApplicationContext()方法来创建实例,并存放至ServletContext中。
protected WebApplicationContext createWebApplicationContext(ServletContext sc) { 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); }
在createWebApplicationContext()方法中,通过BeanUtils.instanceClass()方法创建实例,而WebApplicationContext的实现类名称则通过determineContextClass()方法获得。
protected Class<?> determineContextClass(ServletContext servletContext) { String contextClassName = servletContext.getInitParameter(CONTEXT_CLASS_PARAM); if (contextClassName != null) { try { return ClassUtils.forName(contextClassName, ClassUtils.getDefaultClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load custom context class [" + contextClassName + "]", ex); } } else { contextClassName = defaultStrategies.getProperty(WebApplicationContext.class.getName()); try { return ClassUtils.forName(contextClassName, ContextLoader.class.getClassLoader()); } catch (ClassNotFoundException ex) { throw new ApplicationContextException( "Failed to load default context class [" + contextClassName + "]", ex); } } }
determineContextClass()方法,通过defaultStrategies.getProperty()方法获得实现类的名称,而defaultStrategies是在ContextLoader类的静态代码块中赋值的。具体的途径,则是读取ContextLoader类的同目录下的ContextLoader.properties属性文件来确定的。
也就是说,在初始化的过程中,程序会首先读取ContextLoader类的同目录下的属性文件ContextLoader.properties,并根据其中的配置提取将要实现WebApplicationContext接口的实现类,并根据这个类通过反射进行实例的创建。
综上所述:
LoaderListener监听器的作用就是启动Web容器时,自动装配ApplicationContext的配置信息。因为它实现了ServletContextListener这个接口,在web.xml配置了这个监听器,启动容器时,就会默认执行它实现的contextInitialized()方法初始化WebApplicationContext实例,并放入到ServletContext中。由于在ContextLoaderListener中关联了ContextLoader这个类,所以整个加载配置过程由ContextLoader来完成。
3. DispatcherServlet
详解第三步,contextLoaderListener监听器初始化完毕后,开始初始化DispatcherServlet,下面为初始化方法的源码:
protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }
需要做的八件事情如下所述:
- initMultipartResolver:初始化MultipartResolver,用于处理文件上传服务,如果有文件上传,那么就会将当前的HttpServletRequest包装成DefaultMultipartHttpServletRequest,并且将每个上传的内容封装成CommonsMultipartFile对象。需要在dispatcherServlet-servlet.xml中配置文件上传解析器。
- initLocaleResolver:用于处理应用的国际化问题,本地化解析策略。
- initThemeResolver:用于定义一个主题。
- initHandlerMapping:用于定义请求映射关系。
- initHandlerAdapters:用于根据Handler的类型定义不同的处理规则。
- initHandlerExceptionResolvers:当Handler处理出错后,会通过此将错误日志记录在log文件中,默认实现类是SimpleMappingExceptionResolver。
- initRequestToViewNameTranslators:将指定的ViewName按照定义的RequestToViewNameTranslators替换成想要的格式。
- initViewResolvers:用于将View解析成页面。
- initFlashMapManager:用于生成FlashMap管理器。
通过查看DispatcherServlet(源码内容太多就不往上放了),DispatcherServlet继承自FrameworkServlet,而FrameworkServlet是继承自HttpServletBean的,HttpServletBean又继承了HttpServlet。这是因为DispatcherServlet本身就得是一个Servlet,且含有doGet()和doPost()方法,Web容器才可以调用它,所以它的顶级父类为含有这俩方法的HttpServlet。具体的Web请求,会经过FrameServlet的processRequest方法简单处理后,紧接着调用DispatcherServlet的doService方法,而在这个方法中封装了最终调用处理器的方法doDispatch。这也意味着,DispatcherServlet的最主要的核心功能由doService和doDispatch实现的。
DispatcherServlet类的方法大致可分为三种:
- 初始化相关处理类的方法。
- 响应Http请求的方法。
- 执行处理请求逻辑的方法。
核心方法 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 { try { ModelAndView mv = null; Object dispatchException = null; try { processedRequest = this.checkMultipart(request); multipartRequestParsed = processedRequest != request; mappedHandler = this.getHandler(processedRequest); if (mappedHandler == null || mappedHandler.getHandler() == null) { this.noHandlerFound(processedRequest, response); return; } HandlerAdapter ha = this.getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = "GET".equals(method); if (isGet || "HEAD".equals(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (this.logger.isDebugEnabled()) { this.logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified); } if ((new ServletWebRequest(request, response)).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } this.applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception var20) { dispatchException = var20; } catch (Throwable var21) { dispatchException = new NestedServletException("Handler dispatch failed", var21); } this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); } catch (Exception var22) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, var22); } catch (Throwable var23) { this.triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", var23)); } } finally { if (asyncManager.isConcurrentHandlingStarted()) { if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else if (multipartRequestParsed) { this.cleanupMultipart(processedRequest); } } }
DispatcherServlet处理该类请求的步骤:
- 在接收到请求后,通过几级Servlet类型的父类的处理,先调用doService在request中设置一些必需的参数。最终会调用DispatcherServlet的doDispatch方法。
- 在doDispatch方法中,首先检测request是否包含多媒体类型(如File文件上传),然后将检测后的request转换为processedRequest对象。之后检测processedRequest对象是否为原始request(如果是,即原来的request不包含多媒体信息),然后将boolean结果赋给multipartRequestParsed变量(若multipartRequestParsed为true,在最后会清除processedRequest对象中的多媒体信息)。
- 十分重要的一步,就是通过调用处理器映射器查找Handler。调用getHandler来获取相关的处理器对象。在getHandler方法中,利用处理器映射器HandlerMapping通过request来获取一个包含Handler处理器本身和其前后拦截器interceptor的处理器执行链HandlerExecutionChain对象。
- 通过HandlerExecutionChain对象获取具体的Handler处理器对象,此时使用getHandlerAdapter方法获取可以处理类型的处理器适配器HandlerAdapter对象。
- 调用HandlerAdapter对象的handle方法,将可能带有多媒体信息的processRequest对象,原始request对象,以及Handler处理器本身作为参数传入,handle方法会根据这些参数去执行开发者自己开发的Handler的相关
请求处理逻辑,并返回含有反馈信息和结果视图信息的ModelAndView对象。 - 获得ModelAndView对象后,会进行视图渲染,将model数据填充到request域。在processDispatchResult方法中会对ModelAndView对象进行处理。而在processDispatchResult方法中包含一个render方法,其参数为ModelAndView对象以及request和response对象。在render方法中,通过resolveViewName会获取到实际需要使用的视图View对象,这个对象的具体类型是由XXX决定的。然后就会执行具体的View对象的render方法来完成数据的显示过程。这里举一个视图类型的例子,他在render方法中具体执行了以下逻辑来绑定结果数据和视图:
protected void exposeModelAsRequestAttributes(Map<String, Object> model, HttpServletRequest request) throws Exception{ //遍历model里面的数据,填充到request域 for (Map.Entry<String, Object> entry : model.entrySet()){ String modeName = entry.getKey(); Object modelValue = entry.getValue(); if(modelValue != null){ request.setAttribute(modelName, modelValue); if(logger.isDebugEnabled()){ logger.debug("Added model object '" + modelName + "' of type [" + modelValue.getClass.getName() +"] to request in view with name '" + getBeanName() + "'"); } else{ request.removeAttribute(modelName); if(logger.isDebugEnabled()){ logger.debug("Remove modek object '" +modelName + "' from request in view with name '" +getBeanName() + "'"); } } } } }
可以看到,在这里会把ModelAndView中model的数据遍历出来,分为key和value,并且将数据设置在request的attribute域中。之后加载页面时就可以使用标签在request域中获取返回参数了。