initWebApplicationContext
方法如下:创建一个web子容器,并且和上面Spring已经创建好了的父容器关联上
protected WebApplicationContext initWebApplicationContext() { // 从ServletContext中把上面已经创建好的根容器拿到手 WebApplicationContext rootContext = WebApplicationContextUtils.getWebApplicationContext(getServletContext()); WebApplicationContext wac = null; //但是,但是,但是此处需要注意了,因为本处我们是注解驱动的,在上面已经看到了,我们new DispatcherServlet出来的时候,已经传入了根据配置文件创建好的子容器web容器,因此此处肯定是不为null的,因此此处会进来,和上面一样,完成容器的初始化、刷新工作,因此就不再解释了~ 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()) { if (cwac.getParent() == null) { //此处吧根容器,设置为自己的父容器 cwac.setParent(rootContext); } //根据绑定的配置,初始化、刷新容器 configureAndRefreshWebApplicationContext(cwac); } } } //若是web.xml方式,会走这里,进而走findWebApplicationContext(),因此此方法,我会在下面详细去说明,这里占时略过 if (wac == null) { wac = findWebApplicationContext(); } if (wac == null) { wac = createWebApplicationContext(rootContext); } // 此处需要注意了:下面有解释,refreshEventReceived和onRefresh方法,不会重复执行~ if (!this.refreshEventReceived) { onRefresh(wac); } //我们是否需要吧我们的容器发布出去,作为ServletContext的一个属性值呢?默认值为true哦,一般情况下我们就让我true就好 if (this.publishContext) { // Publish the context as a servlet context attribute. // 这个attr的key的默认值,就是FrameworkServlet.SERVLET_CONTEXT_PREFIX,保证了全局唯一性 // 这么一来,我们的根容器、web子容器其实就都放进ServletContext上下文里了,拿取都非常的方便了。 只是我们一般拿这个容器的情况较少,一般都是拿跟容器,比如那个工具类就是获取根容器的~~~~~~ String attrName = getServletContextAttributeName(); getServletContext().setAttribute(attrName, wac); } return wac; }
备注,在DispatcherServlet的doService
方法里都有这样的一段代码,方便我们非常方便获取到一些参数,比如web子容器等等
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
FrameworkServlet策略式的实现了监听方法,监听应用的刷新事件。当我们刷新应用的时候(比如上面执行refresh()方法,这里就会执行,并且打上标记说已经执行过了),然而onRefresh()是一个模版方法,具体实现交给子类,这样子DispatcherServlet就可以做做初始化web组件的一些事情了~ 这种设计模式可谓非常优秀,
这就是为何会抽象出FrameworkServlet的原因,因为它设计的初衷不仅仅只想支持到Servlet
所以此处就不得不说一下,子类自己实现的onRefresh()方法:
@Override protected void onRefresh(ApplicationContext context) { initStrategies(context); } //初始化Spring MVC的9大组件(至此,才算全部初始化完成了~不容器啊) protected void initStrategies(ApplicationContext context) { initMultipartResolver(context); initLocaleResolver(context); initThemeResolver(context); initHandlerMappings(context); initHandlerAdapters(context); initHandlerExceptionResolvers(context); initRequestToViewNameTranslator(context); initViewResolvers(context); initFlashMapManager(context); }
至于初始化Spring MVC9大组件的详细工作问题,此处也不展开了,参见此篇博文:~~~~~~~~~~~~~~~~~~
当看到这句日志,就能证明整个Spring父子容器全部初始化、启动完成了~
14:42:47.354 [RMI TCP Connection(2)-127.0.0.1] INFO o.s.web.servlet.DispatcherServlet - FrameworkServlet 'dispatcher': initialization completed in 1674475 ms
这里顺便解释一下SpringMVC中的Servlet的三个层次:
1.HttpServletBean直接继承自java的HttpServlet,其作用是将Servlet中配置的参数设置到相应的Bean属性上
2.FrameworkServlet初始化了WebApplicationContext
3.DispatcherServlet初始化了自身的9个组件(本文重点)
为了协助理解,画了一个时序图如下:
配置多个DispatcherServletServlet
从本文中我们看到Spring的容器存在父子容器的。因此我们可以很容器的配置多个web子容器,然后父容器都是Root容器,这是被允许的。
当然一般情况下,定义多个 dispatcherservlect 实际上是没有什么特别的用处的。但能够解决可能存在的jar包内的访问路径冲突问题。
比较常用的一个应用场景为:web请求和rest请求分离处理。
比如/rest/api/v1/全部为rest风格的请求,返回json数据不返回页面,交给一个。
/page/api/v1前缀的就返回渲染的页面,交给另外一个
当然静态资源的请求,也可以用对应的处理方式~~~~~~
Spring MVC是有提供配置多个web子容器的能力的,但是使用的时候,路径方面要谨慎处理
ContextLoader类
在继续讲解web.xml方式启动之前,我觉得有必要深入讲解一些ContextLoader这个类。因为从上面我们发现,初始化Spring的根容器,ContextLoaderListener所有事情都是委派给它来完成的,因此我们来讲解一下。
常量解释:
// init-param定义root根容器id的key public static final String CONTEXT_ID_PARAM = "contextId"; // init-param 定义root根容器的配置文件的路径地址的key public static final String CONFIG_LOCATION_PARAM = "contextConfigLocation"; // init-param 自己可以指定一个WebApplicationContext的实现类(一般都不需要~) public static final String CONTEXT_CLASS_PARAM = "contextClass"; // init-param 可以伴随着容器初始化的时候,我们自己做一些工作的类们。注意需要实现对应接口哦~ key public static final String CONTEXT_INITIALIZER_CLASSES_PARAM = "contextInitializerClasses"; // 基本同上 public static final String GLOBAL_INITIALIZER_CLASSES_PARAM = "globalInitializerClasses"; // 多值分隔符号 private static final String INIT_PARAM_DELIMITERS = ",; \t\n"; // 默认的配置文件 里面内容只有一句话: org.springframework.web.context.WebApplicationContext=org.springframework.web.context.support.XmlWebApplicationContext // 由此课件,它默认是采用XmlWebApplicationContext来初始化上下文的 private static final String DEFAULT_STRATEGIES_PATH = "ContextLoader.properties"; // ContextLoader在被实例化的时候,会执行下面的这个静态代码块。做了一件事:把默认的配置文件加载进来而已 private static final Properties defaultStrategies; static { try { ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, ContextLoader.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } } // 这两个属性主要拿到当前的容器上下文。其中static的工具方法ContextLoader.getCurrentWebApplicationContext是基于此的 private static final Map<ClassLoader, WebApplicationContext> currentContextPerThread = new ConcurrentHashMap<>(1); @Nullable private static volatile WebApplicationContext currentContext;
有了上面的 一些配置:这句
if (this.context == null) { this.context = createWebApplicationContext(servletContext); }
这时一般就会创建前点所述的XmlWebApplicationContext
,至于我们怎么替换,下文会有介绍
最后介绍这个类的一个静态方法:
public static WebApplicationContext getCurrentWebApplicationContext() { ClassLoader ccl = Thread.currentThread().getContextClassLoader(); if (ccl != null) { WebApplicationContext ccpt = currentContextPerThread.get(ccl); if (ccpt != null) { return ccpt; } } return currentContext; }
此静态方法,可以在任何地方都获取到Spring容器(根容器),非常好用。
我们知道还有一种方法如下:
ApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext());
这个毕竟要请求与request对象,相对来说麻烦点,比如在Service层呢,就不太好获取了嘛,所以我们就可以用这个啦
@Autowired private HttpServletRequest request; @Override public Object hello() { ApplicationContext ctx1 = WebApplicationContextUtils.getWebApplicationContext(request.getSession().getServletContext()); WebApplicationContext ctx2 = ContextLoader.getCurrentWebApplicationContext(); System.out.println(ctx1); //Root WebApplicationContext: startup date ... System.out.println(ctx1 == ctx2); //true return "service hello"; }
由此看出,我们以后可以用这种方式来获取容器
FrameworkServlet
DispatcherServlet创建自己的WebApplicationContext并管理这个WebApplicationContext里面的 handlers/controllers/view-resolvers
简单理解,功能有点想ContextLoader。FrameworkServlet实现了ApplicationContextAware接口的setApplicationContext()方法,可知DispatcherServlet的applicationContext来自FrameworkServlet。
@Override public void setApplicationContext(ApplicationContext applicationContext) { if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) { this.webApplicationContext = (WebApplicationContext) applicationContext; this.webApplicationContextInjected = true; } }
注意,注意,注意一句doc:Primarily added to support use in embedded servlet containers.,它是Spring4.0之后有的,只用于嵌入式的Servlet环境。war同期环境这里是不会执行的,因为它并不会以Bean的形式存在于Spring容器(说白了,就是init方法里的这段代码)
生效了,就能注入了~
一些常量介绍:
//getNameSpace会返回你在web.xml中配置的servlet-name加上"-servlet",这个namespace会在之后application context加载spring MVC配置文件时候用到 //比如你给servlet取名叫 SpringMVCServlet,那么当Spring MVC初始化的时候,会去寻找名为/WEB-INF/SpringMVCServlet-servlet.xml的配置文件。 // 不过个人建议:还是配置上配置文件比较好 不要用默认的 public static final String DEFAULT_NAMESPACE_SUFFIX = "-servlet"; public String getNamespace() { return (this.namespace != null ? this.namespace : getServletName() + DEFAULT_NAMESPACE_SUFFIX); } // 默认的容器类型。若没有配置contextClass就用它 public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class; // 这些值,都可以通过将Servlet初始化参数(init-param)设置到该组件上(如contextAttribute、contextClass、namespace、contextConfigLocation) @Nullable private String contextAttribute; private Class<?> contextClass = DEFAULT_CONTEXT_CLASS; @Nullable private String contextId; @Nullable private String namespace; @Nullable private String contextConfigLocation;