SpringMVC之DispatcherServlet初始化過程
DispatcherServlet的類繼承圖。
DispatcherServlet是一個Servlet,那麼它就遵循Servlet的生命週期。如上圖所示,DispatcherServlet還實現了Spring IOC的Aware介面,瞭解Aware介面的人都知道,Spring在建立物件的時候,會自動注入Aware介面方法裡的物件。比如上圖,會自動給DispatcherServlet注入Environment和ApplicationContext物件,如果你這麼認為,那就大錯特錯了,只能說明你Spring學的不錯。
DispatcherServlet物件由Web容器(Tomcat)來管理,並不由Spring IOC管理,因此,根本就不可能自動注入Environment和ApplicationContext物件。
這裡的ApplicationContextAware和EnvironmentAware實際是作為普通介面使用,需要手動程式設計呼叫介面方法。
在瞭解DispatcherServlet的init()初始化方法之前,先了解它的static靜態程式碼塊。
private static final String DEFAULT_STRATEGIES_PATH = "DispatcherServlet.properties"; private static final Properties defaultStrategies; static { try { ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class); defaultStrategies = PropertiesLoaderUtils.loadProperties(resource); } catch (IOException ex) { //... } }
靜態程式碼塊會讀取DispatcherServlet.properties
配置檔案,該配置檔案配置了預設的SpringMVC需要使用的一系列元件,當沒有配置<mvc:annotation-driven />標籤時,這些預設配置才會生效,很顯然,我們已經配置了<mvc:annotation-driven />標籤。
DispatcherServlet.properties檔案內容:
org.springframework.web.servlet.LocaleResolver=org.springframework.web.servlet.i18n.AcceptHeaderLocaleResolver org.springframework.web.servlet.ThemeResolver=org.springframework.web.servlet.theme.FixedThemeResolver org.springframework.web.servlet.HandlerMapping=org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\ org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping org.springframework.web.servlet.HandlerAdapter=org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\ org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\ org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerExceptionResolver,\ org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,\ org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver org.springframework.web.servlet.RequestToViewNameTranslator=org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator org.springframework.web.servlet.ViewResolver=org.springframework.web.servlet.view.InternalResourceViewResolver org.springframework.web.servlet.FlashMapManager=org.springframework.web.servlet.support.SessionFlashMapManager
再次強調,SpringMVC通常不會使用這些預設的甚至過時的配置,新增<mvc:annotation-driven />標籤,該標籤會為我們註冊當前最優秀的MVC元件,後面我們會分析到。
Servlet建立時,會執行init()初始化方法,看HttpServletBean.init()。
@Override
public final void init() throws ServletException {
//...
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
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) {
throw ex;
}
}
// 初始化入口方法
initServletBean();
}
上面的原始碼,完成了從servletConfig取值
、給當前HttpServletBean物件屬性賦值
、呼叫初始化入口方法
三個功能。
我們寫一個簡單列子,演示一下,讀者便可立馬明白上面的程式碼邏輯。
User user = new User();
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(user);
PropertyValue pv = new PropertyValue("name", "張三");
bw.setPropertyValue(pv);
System.out.println(user.getName());
output:張三
由此可見,DispatcherServlet的contextConfigLocation屬性就有值了,該屬性定義在FrameworkServlet內。
繼續看FrameworkServlet.initServletBean()方法:
protected final void initServletBean() throws ServletException {
//...
try {
this.webApplicationContext = initWebApplicationContext();
// 空方法
initFrameworkServlet();
}
//...
}
其實就是建立了一個Spring的WebApplicationContext物件,稱之為web應用上下文,儲存在DispatcherServlet中。
FrameworkServlet#initWebApplicationContext()方法原始碼:
protected WebApplicationContext initWebApplicationContext() {
// ①、使用ContextLoaderListener所載入的Web應用上下文
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// ②、使用Servlet建構函式註冊的Web應用上下文,Servlet3.0+API使用
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// ③、使用contextAttribute值指定的ServletContext上下文中的Web應用上下文
wac = findWebApplicationContext();
}
if (wac == null) {
// ④、DispatcherServlet中init-param指定的contextConfigLocation所載入的Web應用上下文
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// ⑤、初始化SpringMVC的基礎元件
onRefresh(wac);
}
if (this.publishContext) {
// ⑥、將Web應用上下文,儲存在ServletContext上下文中
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
我們再對上述6條註釋進行詳細說明。
注:這裡所謂的Web應用上下文,指的是Spring的WebApplicationContext物件。
①、使用ContextLoaderListener所載入的Web應用上下文,並不陌生,web.xml中構造父子容器的常見方案。
<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>
②、使用Servlet建構函式註冊的Web應用上下文,好像沒聽說過,其實呢是Servlet3.0+程式設計式建立Servlet時使用。
/**
* Create a new {@code DispatcherServlet} with the given web application context. This
* constructor is useful in Servlet 3.0+ environments where instance-based registration
* of servlets is possible through the {@link ServletContext#addServlet} API.
**/
public DispatcherServlet(WebApplicationContext webApplicationContext) {
super(webApplicationContext);
setDispatchOptionsRequest(true);
}
③、使用contextAttribute值指定的ServletContext上下文中的Web應用上下文。
例如:從ServletContext上下文中去找一個key為myWebApplicationContext的Web應用上下文,來作為DispatcherServlet物件中的webApplicationContext屬性的值,當然,前提是ServletContext上下文中放置過該Web應用上下文物件。
<servlet>
<servlet-name>dispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextAttribute</param-name>
<param-value>myWebApplicationContext</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
④、DispatcherServlet中init-param指定的contextConfigLocation所載入的Web應用上下文,也就是我們入門例子中配置的唯一Web應用上下文,重點關注。
⑤、初始化SpringMVC的基礎元件。
如果配置了<mvc:annotation-driven />標籤,則使用<mvc:annotation-driven />標籤所繫結的SpringMVC基礎元件。如果沒有配置<mvc:annotation-driven />標籤,則使用DispatcherServlet.properties配置檔案內預設的SpringMVC基礎元件 。我們的入門例子配置了<mvc:annotation-driven />標籤。
⑥、將Web應用上下文,儲存在ServletContext上下文中
本例key=org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcherServlet
動作:getServletContext().setAttribute(key, wac);
至此,webApplicationContext
物件,存在於DispatcherServlet物件中,也存在於ServletContext上下文中,ServletContext物件,就是傳說中的global session,也就是jsp中的application物件。
webApplicationContext上下文物件中,也儲存了servletContext和servletConfig物件。
FrameworkServlet#createWebApplicationContext(org.springframework.context.ApplicationContext)
protected WebApplicationContext createWebApplicationContext(ApplicationContext parent) {
Class<?> contextClass = getContextClass();
//...
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
wac.setParent(parent);
wac.setConfigLocation(getContextConfigLocation());
configureAndRefreshWebApplicationContext(wac);
return wac;
}
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
//...
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
// 空方法
postProcessWebApplicationContext(wac);
// 執行web.xml中init-param指定的ApplicationContextInitializer的實現類初始化方法
applyInitializers(wac);
// Spring容器的重新整理方法
wac.refresh();
}
wac.refresh()是Spring容器的啟動重新整理方法,它會掃描<context:component-scan base-package="com.spring"/>所指定目錄下的@Component,@Service,@Controller,@Repository,@Configuration所標註的類,大多數博文都遺漏了@Configuration,其實,@Service,@Controller,@Repository,@Configuration,都是@Component,它們自身都被@Component所標註。
至此,webApplicationContext就建立完畢了。
回過頭來,我們再看看DispatcherServlet#onRefresh(wac)收尾方法。
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
從方法名上,可以看到,後面帶s的代表有多個,不帶s的代表單個,我們以initHandlerMappings()為例:
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// 找所有的HandlerMappings,包含祖先容器上下文.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<HandlerMapping>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
// 找名字為handlerMapping的HandlerMapping物件,包含祖先容器上下文.
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
catch (NoSuchBeanDefinitionException ex) {
}
}
if (this.handlerMappings == null) {
// 如果沒有配置,取DispatcherServlet.properties中的預設配置
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
}
}
getBean()是返回第一個HandlerMapping物件,包含祖先容器。
而BeanFactoryUtils.beansOfTypeIncludingAncestors()方法是返回所有HandlerMapping物件,包含祖先容器,是一個集合。
如果沒有配置,取DispatcherServlet.properties中的預設配置。本例中,沒有祖先容器,也不會取DispatcherServlet.properties的預設配置,因為我們配置了<mvc:annotation-driven />標籤。
至此,DispatcherServlet的初始化過程就完成了,初始化過程,主要完成了兩個功能:
1、建立並完成啟動重新整理webApplicationContext上下文物件。
2、註冊SpringMVC的八大元件,並從Controllor中解析出每個HandlerMethod,由<mvc:annotation-driven />標籤解析器完成。
Servlet 容器與 Spring 容器有什麼關係?
Tomcat&Jetty在啟動時給每個Web應用建立一個全域性的上下文環境,這個上下文就是ServletContext,其為後面的Spring容器提供宿主環境。
Tomcat&Jetty在啟動過程中觸發容器初始化事件,Spring的ContextLoaderListener會監聽到這個事件,它的contextInitialized方法會被呼叫,在這個方法中,Spring會初始化全域性的Spring根容器,這個就是Spring的IoC容器,IoC容器初始化完畢後,Spring將其儲存到ServletContext中,便於以後來獲取。
Tomcat&Jetty在啟動過程中還會掃描Servlet,一個Web應用中的Servlet可以有多個,以SpringMVC中的DispatcherServlet為例,這個Servlet實際上是一個標準的前端控制器,用以轉發、匹配、處理每個Servlet請求。
Servlet一般會延遲載入,當第一個請求達到時,Tomcat&Jetty發現DispatcherServlet還沒有被例項化,就呼叫DispatcherServlet的init方法,DispatcherServlet在初始化的時候會建立自己的容器,叫做SpringMVC 容器,用來持有Spring MVC相關的Bean。同時,Spring MVC還會通過ServletContext拿到Spring根容器,並將Spring根容器設為SpringMVC容器的父容器,請注意,Spring MVC容器可以訪問父容器中的Bean,但是父容器不能訪問子容器的Bean, 也就是說Spring根容器不能訪問SpringMVC容器裡的Bean。說的通俗點就是,在Controller裡可以訪問Service物件,但是在Service裡不可以訪問Controller物件。