SpringMVC的启动流程分析
说在前面
SpringMVC的项目是基于外部Tomcat的启动来启动的,当Tomcat启动好了,SpringMVC的项目也就启动好了,也就是说SpringMVC是借助于Tomcat提供的扩展点来完成启动的,其实SpringMVC的启动主要利用Tomcat的两个扩展点:
- ServletContainerInitializer的回调
- ServletContextListener事件回调
下面我们就这两个Tomcat的扩展点来展开分析SpringMVC的启动流程:
从Servlet的规范说起
ServletContainerInitializer的回调
Tomcat的回调逻辑
在Tomcat的启动过程中会触发ServletContainerInitializer的回调,即在StandardContext中的startInternal方法中会遍历所有的ServletContainerInitializer实现并触发其onStartup方法,代码如下:
// Call ServletContainerInitializers
// 回调实现了ServletContainerInitializer的实现类的onStartup方法
// SpringMVC正式基于这一回调实现的,丢进去DispatcherServlet
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
initializers.entrySet()) {
try {
// SpringMVC在此注入了ContextLoaderListener监听器【创建了RootApplicationContext,并将其丢到ContextLoaderListener中】,
// 并创建了WebApplicationContext
// 同时创建了DispatcherServlet对象并且赋值到Context中
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
Tomcat是通过SPI机制来加载所有的ServletContainerInitializer的对象的,这就很方便第三方来利用这个扩展点,SpringMVC就是利用了这一点,注入了自己的SpringServletContainerInitializer,我们来看下SpringMVC中SPI的配置:
image当Tomcat启动时回调到ServletContainerInitializers的这个点时,就会触发SpringServletContainerInitializer#onStartup方法的执行,并且Servlet3.0的规范是会扫描ServletContainerInitializer实现类上标注@HandlesTypes注解中的类做为onStartup方法的第一个参数。那么下面我们就重点分析下SpringServletContainerInitializer这个类:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
if (webAppInitializerClasses != null) {
initializers = new ArrayList<>(webAppInitializerClasses.size());
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says... 所有非接口非抽象的WebApplicationInitializer实现类
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)//集合负责保存满足上面条件的类
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
//下面会调用所有的满足要求的WebApplicationInitializer,调用他们的onStartup方法
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);//调用所有的WebApplicationInitializer的onStartup方法
}
}
}
通过看代码,我们知道Tomcat启动时会将项目中所有的WebApplicationInitializer类对象作为集合参数传给onStartup方法的第一个参数,然后在onStartup方法中会循环遍历调用所有的WebApplicationInitializer对象的onStartup方法完成启动。
基于SpringMVC我们如何让Tomcat启动我们的项目
所以当我们想Tomcat启动能够成功启动我们的SpringMVC项目,我们就需要实现WebApplicationInitializer接口来完成启动流程,当然WebApplicationInitializer只是一个接口,为了方便快速开发,SpringMVC通过模板模式为我们提供了一些实现类:
image当我们是基于注解开发时,我们只需要继承AbstractAnnotationConfigDispatcherServletInitializer方法即可,如下:
public class QuickAppStarter extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override //根容器的配置(Spring的配置文件===配置类)
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[]{SpringConfig.class};
}
@Override //web容器的配置(SpringMVC的配置文件===配置类)
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{SpringMVCConfig.class};
}
@Override //Servlet的映射DispatcherServlet的映射路径
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
其中,我们需要重写几个方法:
- getRootConfigClasses:指定根容器的配置类,父子容器中的父容器
- getServletConfigClasses:指定web容器的配置类,父子容器中的子容器
- getServletMappings:指定DispatcherServlet的映射路径
具体的启动流程
启动起来还是十分简单的,我们来看下这个的启动流程:
因为我们继承的是AbstractAnnotationConfigDispatcherServletInitializer,所以首先会调用AbstractDispatcherServletInitializer的onStartup方法,在这个方法中也干了两件事:
- 调用父类AbstractContextLoaderInitializer的onStart方法
具体调用的AbstractContextLoaderInitializer#registerContextLoaderListener方法如下:
protected void registerContextLoaderListener(ServletContext servletContext) {
WebApplicationContext rootAppContext = createRootApplicationContext();//创建一个根容器
if (rootAppContext != null) { // 将创建好的Spring容器放入到ContextLoaderListener监听器中,后面Tomcat启动完应用是回调这个监听器的contextInitialized方法
ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
listener.setContextInitializers(getRootApplicationContextInitializers());
// 将监听器加入到上下文
servletContext.addListener(listener);
}
else {
logger.debug("No ContextLoaderListener registered, as " +
"createRootApplicationContext() did not return an application context");
}
}
主要就是往ServletContext中加入了一个监听器ContextLoaderListener,并且这个监听器中组合了通过我们指定的配置类而创建的根容器。这个监听器会在ServletContainerInitializer回调完之后被回调到,使我们后面要讲的第二个扩展点。
- 往ServletContext中注册DispatcherServlet
具体调用的AbstractDispatcherServletInitializer#registerDispatcherServlet方法代码逻辑如下:
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() must not return null or empty");
WebApplicationContext servletAppContext = createServletApplicationContext();// 创建Web容器
Assert.notNull(servletAppContext, "createServletApplicationContext() must not return null");
FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);//new 出来了一个DispatcherServlet,并保存了web-ioc容器
Assert.notNull(dispatcherServlet, "createDispatcherServlet(WebApplicationContext) must not return null");
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
if (registration == null) {
throw new IllegalStateException("Failed to register servlet with name '" + servletName + "'. " +
"Check if there is another servlet registered under the same name.");
}
registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());//根据我指定的DispatcherServlet的路径进行注册
registration.setAsyncSupported(isAsyncSupported());
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
这个方法主要也就干了一件事:将创建的DispatcherServlet添加到ServletContext中,其中DispatcherServlet中组合了根据我们指定的配置创建的Web容器。
总结
也就是说:整个ServletContainerInitializer的回调主要就干了两件事:
- 创建了ContextLoaderListener监听器【组合了创建的根容器】,并加入到ServletContext
- 创建了DispatcherServlet【组合了创建的web容器】,并加入到ServletContext
ServletContextListener事件回调
ServletContainerInitializer的回调结束之后,Tomcat会回调ServletContextListener的contextInitialized方法,即StandardContext中listenerStart方法,代码如下:
for (Object instance : instances) {
if (!(instance instanceof ServletContextListener)) {
continue;
}
ServletContextListener listener = (ServletContextListener) instance;
try {
fireContainerEvent("beforeContextInitialized", listener);
if (noPluggabilityListeners.contains(listener)) {
listener.contextInitialized(tldEvent);
} else {
// 回调了监听器:Spring的Web容器在此初始化
listener.contextInitialized(event);
}
fireContainerEvent("afterContextInitialized", listener);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
fireContainerEvent("afterContextInitialized", listener);
getLogger().error(sm.getString("standardContext.listenerStart",
instance.getClass().getName()), t);
ok = false;
}
}
所以就会触发我们在ServletContainerInitializer回调时加入的ContextLoaderListener的contextInitialized方法:
@Override // 监听器回调【会在web启动完成的时候tomcat会触发监听器钩子】
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());// 初始化ioc容器(根容器)
}
对应的具体逻辑在initWebApplicationContext方法中:
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!");
}
servletContext.log("Initializing Spring root WebApplicationContext");
Log logger = LogFactory.getLog(ContextLoader.class);
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);
}
// 刷新容器,触发Spring的流程
configureAndRefreshWebApplicationContext(cwac, servletContext);
}
}
// 将根容器添加到SevletContext中的application域中,后面DispatherServlet启动的时候回来拿,
// 从而构建父子容器
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);// 将创建的容器放到tomcat的application域中
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
if (logger.isInfoEnabled()) {
long elapsedTime = System.currentTimeMillis() - startTime;
logger.info("Root WebApplicationContext initialized in " + elapsedTime + " ms");
}
return this.context;
}
catch (RuntimeException | Error ex) {
logger.error("Context initialization failed", ex);
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, ex);
throw ex;
}
}
在这个方法中,主要是将我们前面创建的根容器进行了刷新【Spring的刷新逻辑】,然后将根容器对象添加到了SevletContext的属性中,即应用的Application域中,为何要放进去呢?这就是和下面构建父子容器相关了。
SpringMVC父子容器启动的过程
SpringMVC是可以通过构造父子容器来进行,隔离基本组件和web组件配置的,就像我们上面的配置就是构建父子容器的。那他究竟是何时将父子容器关联起来了呢?这个就需要我们来看下DispatcherServlet初始化的过程了,关键的逻辑在FrameworkServlet#initServletBean方法中,代码如下:
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
if (logger.isInfoEnabled()) {
logger.info("Initializing Servlet '" + getServletName() + "'");
}
long startTime = System.currentTimeMillis();
try {//初始化web ioc容器
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
logger.error("Context initialization failed", ex);
throw ex;
}
if (logger.isDebugEnabled()) {
String value = this.enableLoggingRequestDetails ?
"shown which may lead to unsafe logging of potentially sensitive data" :
"masked to prevent unsafe logging of potentially sensitive data";
logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
"': request parameters and headers will be " + value);
}
if (logger.isInfoEnabled()) {
logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
}
}
通过调用initWebApplicationContext方法会构建好父子容器关系,并启动父容器;因为我们在构造DispatcherServlet的时候传入了创建的Web容器,然后在通过应用的Application域获取当我们前面存放的根容器,然后构建父子容器关系,即cwac.setParent(rootContext);
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =//先会获取之前的WebApplicationContext,这是用来构建父子容器的
WebApplicationContextUtils.getWebApplicationContext(getServletContext());//父容器【S】
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;//当前的web-ioc容器
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
cwac.setParent(rootContext);//父子容器的体现
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
//.......省略相关代码
最后通过configureAndRefreshWebApplicationContext(cwac);来启动web容器。并且注册了监听ContextRefreshedEvent事件的监听器,当web容器刷新完成的时候会触发构建SpringMVC的九大组件的创建。
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
if (ObjectUtils.identityToString(wac).equals(wac.getId())) {
// The application context id is still set to its original default value
// -> assign a more useful id based on available information
if (this.contextId != null) {
wac.setId(this.contextId);
}
else {
// Generate default id...
wac.setId(ConfigurableWebApplicationContext.APPLICATION_CONTEXT_ID_PREFIX +
ObjectUtils.getDisplayString(getServletContext().getContextPath()) + '/' + getServletName());
}
}
wac.setServletContext(getServletContext());
wac.setServletConfig(getServletConfig());
wac.setNamespace(getNamespace());
// 注册了一个监听器,当容器完全刷新结束的时候,会回调这个监听器,完成SpringMVC的八大组件的初始化
wac.addApplicationListener(new SourceFilteringListener(wac, new ContextRefreshListener()));// 用来监听初始化全部结束,容器会发送Spring的事件
// The wac environment's #initPropertySources will be called in any case when the context
// is refreshed; do it eagerly here to ensure servlet property sources are in place for
// use in any post-processing or initialization that occurs below prior to #refresh
ConfigurableEnvironment env = wac.getEnvironment();
if (env instanceof ConfigurableWebEnvironment) {
((ConfigurableWebEnvironment) env).initPropertySources(getServletContext(), getServletConfig());
}
postProcessWebApplicationContext(wac);
applyInitializers(wac);
wac.refresh();//刷新容器
}
最后
至此,根容器和Web容器就刷新完成了,也就是说相关的组件都加载到我们指定的容器中了,当然也包括SpringMVC的九大组件,这样整个SpringMVC项目就启动完成了,后面当请求进来的时候就会通过DispatcherServlet来进行分发请求,然后通过九大组件来完成请求的处理以及响应。