DispatcherServlet 在 Tomcat 中的启动过程

1,751 阅读6分钟

通常在一个 Web 应用中,会通过将 Web 程序部署到类似于 Tomcat/Jetty 这类 Web 容器中去。Tomcat 在整个系统扮演的角色主要有两个,一个是面向 HTTP 请求的 HTTP 服务器,另一个是后面用于处理后端逻辑的 Servlet 容器。SpringMVC 简单的来说就是一个 Servlet。运行在 Servlet 容器中。

这篇文章将简单概述 SpringMVC 的初始化过程。

在 Tomcat 启动应用程序

Tomcat 会扫描 webapps 目录下的所有的应用,并且依次进行加载。 打包的应用程序会被放到 webapps 目录下。随后,Tomcat 会读取应用内部的 WEB-INF 目录下的 web.xml 文件,对该文件进行解析,包括解析 servlet,该 servlet 的 load_on_startup 的值等属性。这里插一句。在 Tomcat 中,一个 web.xml 文件中读取出来的所有的 servlet 会存储在 TreeMap 的数据结构中,key 为 load_on_startup 的值,value 为一个 List,List 中存的是 Wrapper(servlet)。只有 load_on_startup 的值大于等于 0 时才会被放入到 TreeMap 中。并且值越低,启动的优先级越高。servlet 的加载就是通过 load 方法进行加载的。所以,这里的 wrapper.load() 就是 servlet 加载的入口。下面的代码就是 Tomcat 中加载 Wrapper 的入口。

public boolean loadOnStartup(Container children[]) {
    // Collect "load on startup" servlets that need to be initialized
    TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();$$
    for (Container child : children) {
        Wrapper wrapper = (Wrapper) child;
        int loadOnStartup = wrapper.getLoadOnStartup();
        if (loadOnStartup < 0) {
            continue;
        }
        Integer key = Integer.valueOf(loadOnStartup);
        ArrayList<Wrapper> list = map.get(key);
        if (list == null) {
            list = new ArrayList<>();
            map.put(key, list);
        }
        list.add(wrapper);
    }
    // Load the collected "load on startup" servlets
    for (ArrayList<Wrapper> list : map.values()) {
        for (Wrapper wrapper : list) {
            try {
                wrapper.load();
            } catch (ServletException e) {
                getLogger().error(sm.getString("standardContext.loadOnStartup.loadException",
                      getName(), wrapper.getName()), StandardWrapper.getRootCause(e));
                // NOTE: load errors (including a servlet that throws
                // UnavailableException from the init() method) are NOT
                // fatal to application startup
                // unless failCtxIfServletStartFails="true" is specified
                if(getComputedFailCtxIfServletStartFails()) {
                    return false;
                }
            }
        }
    }
    return true;
}

Servlet 的加载流程

StandardWrapper#load() 方法对 Servlet 进行加载。load 方法的主要逻辑在 loadServlet 方法中。

public synchronized Servlet loadServlet() throws ServletException {

    // ...

    Servlet servlet;
    try {
        long t1=System.currentTimeMillis();
        // Complain if no servlet class has been specified
        if (servletClass == null) {
            unavailable(null);
            throw new ServletException
                (sm.getString("standardWrapper.notClass", getName()));
        }
        InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
        try {
            servlet = (Servlet) instanceManager.newInstance(servletClass);
        } catch (ClassCastException e) {
            unavailable(null);
            // Restore the context ClassLoader
            throw new ServletException
                (sm.getString("standardWrapper.notServlet", servletClass), e);
        } 

        // ...
        
        initServlet(servlet);
        fireContainerEvent("load", this);

        // ...

    return servlet;
}

上述方法中删去了部分不重要的代码,仅留下了比较重要的代码。上述代码中可以看到调用了 InstanceManager 的 newInstance 的方法,创建了一个 servlet。虽然编译类型为 Servlet 类型,但是实际上的运行时类型为 DispatcherServlet 类型。

随后调用了 initServlet 方法,对 servlet 进行初始化。

private synchronized void initServlet(Servlet servlet)
        throws ServletException {

        // ...

        //protected final StandardWrapperFacade facade = new StandardWrapperFacade(this);
        // StandardWrapperFacade 实现了 ServletConfig 接口
        servlet.init(facade);
        instanceInitialized = true;
        
        // ...
}

初始化方法中调用了 servlet 的 init 方法。最终调到了 HttpServletBean#init() 方法。 init 方法的主要功能就是将属性设置到 Dispatcher 中。比如把 web.xml 中的 ContextConfigLocation 属性 设置给 DispatcherServlet。设置之后在初始化 Spring 容器的时候就能够直接获取容器的配置文件。

@Override
public final void init() throws ServletException {
	// Set bean properties from init parameters.
	// 将 servlet 中配置的参数封装进 PropertyValues
	PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
	if (!pvs.isEmpty()) {
		try {
			// 将当前对象 servlet 封装成 BeanWrapper
			BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
			// 创建 ResourceLoader
			ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
			// 注册自定义的 PropertyEditor
			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;
		}
	}
	// Let subclasses do whatever initialization they like.
	// 初始化 Servlet Bean
	// 模板方法
	initServletBean();
	}

PropertyValues 中保存了 Spring xml 文件路径(ContextConfigLocation)。 属性设置之后,后续就是 Spring 那一套了。随后调用 initServletBean 方法。

protected final void initServletBean() throws ServletException {

    //...

		// 初始化 webApplicationContext
		this.webApplicationContext = initWebApplicationContext();
		// WebApplicationContext 已经启动
		// 初始化 FrameworkServlet
		// 模板方法
		initFrameworkServlet();

    //...
}
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
	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");
	}
	// 默认创建一个 XMLWebApplicationContext 类型的应用上下文
	ConfigurableWebApplicationContext wac =
			(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
	wac.setEnvironment(getEnvironment());
	wac.setParent(parent);
	String configLocation = getContextConfigLocation();
	if (configLocation != null) {
		// 将配置的 xml 文件设置进去
		// 一般为 [servletName]-Servlet.xml
		wac.setConfigLocation(configLocation);
	}
	// 启动容器
	configureAndRefreshWebApplicationContext(wac);
	return wac;
}

前面提到还没有关联 IOC 容器,initServletBean 方法中就通过调用 initWebApplicationContext() 方法对 IOC 容器进行了初始化。初始化是通过调用 FrameworkServlet#createWebApplicationContext(ApplicationContext) 方法来进行的。通过创建一个 ConfigurableWebApplicationContext 类型的容器(具体的类型为 XMLWebApplicationContext),设置其配置文件路径,随后就是 Spring 容器的启动了。

容器启动的过程中回去加载 Spring 配置文件中的 BeanDefinition。随后在 AbstractApplicationContext#finishBeanFactoryInitialization 方法中对 Bean 进行初始化。包括一些 Controller 或者一些注解驱动等标签解析后相关的 Bean。

在 Spring 容器启动的最后,会调用到 finishRefresh() 方法,该方法中会发布一个 ContextRefreshedEvent 事件。所有监听了该事件的监听器会收到该事件的通知。因为 FrameworkServlet#configureAndRefreshWebApplicationContext 方法中在启动容器之前注册了一个 FrameworkServlet 内部的 ContextRefreshListener 监听器,可以监听 ContextRefreshedEvent 事件。因此当发布了 ContextRefreshedEvent 事件的时候会回调 onApplicationEvent 方法。

public void onApplicationEvent(ContextRefreshedEvent event) {
	// 置为 true,表示已经启动
	this.refreshEventReceived = true;
	synchronized (this.onRefreshMonitor) {
		// 调到子类,这里会调到 DispatcherServlet 的 onRefresh 方法
		// 将当前的 webApplicationContext 传进去
		onRefresh(event.getApplicationContext());
	}
}
protected void onRefresh(ApplicationContext context) {
	initStrategies(context);
}

回调方法中最后执行到了 initStrategies 方法,该方法会初始化 DispatcherServlet 中用到的九大组件。

DispatcherServlet 的 initStrategies

initStrategies 中会初始化九大组件。默认需要初始化的组件定义在 DispatcherServlet.properties 文件中。如果已经初始化,已经初始化是指前面在 Spring 容器启动过程中已经注册成为一个 Bean,则不再进行初始化。若自定义了指定名称的 Bean,则不会初始化 DispatcherServlet.properties 文件中定义的 Bean,而是初始化自定义的 Bean。当然不同的组件根据需要也分别会选择进行初始化或者不初始化。下面代码注释中标明了是否需要初始化的情况。

	protected void initStrategies(ApplicationContext context) {
		// MultipartResolver 可以不需要
		initMultipartResolver(context);
		// 如果没有指定会创建一个默认的 LocaleResolver AcceptHeaderLocaleResolver
		initLocaleResolver(context);
		// 如果没有指定会创建一个默认的 ThemeResolver FixedThemeResolver
		initThemeResolver(context);
		// 如果没有指定会创建默认的 HandlerMapping:
		// FixedThemeResolverBeanNameUrlHandlerMapping
		// RequestMappingHandlerMapping
		// RouterFunctionMapping
		initHandlerMappings(context);
		// 如果没有指定会创建默认的 HandlerAdapter:
		// HttpRequestHandlerAdapter
		// SimpleControllerHandlerAdapter
		// RequestMappingHandlerAdapter
		// HandlerFunctionAdapter
		initHandlerAdapters(context);
		// 如果没有指定会创建默认的 HandlerExceptionResolver:
		// ExceptionHandlerExceptionResolver
		// ResponseStatusExceptionResolver
		// DefaultHandlerExceptionResolver
		initHandlerExceptionResolvers(context);
		// 如果没有指定会创建一个默认的 RequestToViewNameTranslator DefaultRequestToViewNameTranslator
		initRequestToViewNameTranslator(context);
		// 如果没有指定会创建默认的 ViewResolvers:
		// 可以有多个
		// InternalResourceViewResolver
		initViewResolvers(context);
		// 如果没有指定会创建默认的 FlashMapManager:SessionFlashMapManager
		initFlashMapManager(context);
	}

初始化之后的实例因为是 prototype 类型,因为 IOC 容器并不会维护这些实例。仅仅通过 Spring 来创建这些对象。这些对象都会被赋给 DispatcherServlet 的属性中。

private MultipartResolver multipartResolver;
private LocaleResolver localeResolver;
private ThemeResolver themeResolver;
private List<HandlerMapping> handlerMappings;
private List<HandlerAdapter> handlerAdapters;
private List<HandlerExceptionResolver> handlerExceptionResolvers;
private RequestToViewNameTranslator viewNameTranslator;
private FlashMapManager flashMapManager;
private List<ViewResolver> viewResolvers;

至此,DispatcherServlet 的加载过程就全部完成了,后续就是等待请求的到来,以及对请求的处理了。

推荐阅读