从Servet到SpringBoot零配置启动原理解析

117 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

        在工作中我们往往会用到了Spring框架做后端开发,从SpringBoot框架诞生后,开发的敏捷性得到了大幅度提升,但作为一个技术工程师,想寻求技术的突破,光学会用是远远不够的,例如Spring的核心容器IOC是如何实现的,Aop原理,Web应用启动原理,如何与其他框架整合,SpringBoot的零配置启动原理。

        学习的道路曲折且漫长,加油ヾ(◍°∇°◍)ノ゙!  

        首先我们先看一个Spring框架的UML类图, 主要是展现DispatcherServlet、FramworkServlet、HttpServletBean和ApplicationContextAware的关系,DispatcherServlet最终实现了Servlet接口。

        接着看另外一个核心接口WebApplicationContext的UML类图

        由UML类图可以发现WebApplicationContext有一个父接口ApplicationContext和一个子接口ConfigurableWebApplicationContext子接口, 其中ConfigurableWebApplicationContext有4个实现GenericWebApplicationContext、GroovyWebApplicationContext、StaticWebApplicationContext和XmlWebApplicationContext。

        此处实现用到了策略模式,Spring容器默认使用的XmlWebApplicationContext,可以在FrameworkServlet里找到一个静态属性DEFAULT_CONTEXT_CLASS, 在没有找到web容器的情况下,那么就使用XmlWebApplicationContext容器。

	public static final Class<?> DEFAULT_CONTEXT_CLASS = XmlWebApplicationContext.class;

         上述的类和接口基本都是Spring框架的核心类,为了掌握启动原理,不光要熟悉这些类,我们可以从Servlet开始追踪溯源,其中ContextLoaderListener是Spring框架提供的一个web应用的实现类, 实现了ServletContextListener接口。

一、web应用启动原理分析

1.  Web应用的启动监听器ContextLoaderListener

        早期的应用开发者在利用Spring框架开发web应用时,我们需要在web.xml文件里添加一个web监听器的配置 org.springframework.web.context.ContextLoaderListener, 为什么要添加ContextLoaderListener这个配置呢? 

        我们可以从Spring源码中找到这个ContextLoaderListener类,查看源码: 

        由上图可以发现ContextLoaderListener类继承了ContextLoader类并实现了ServletContextListener接口,如果对servlet比较熟悉的话,该接口也是sevlet的监听器,主要的作用在于监听容器的启动,初始化容器,tomcat就是一个很好的例子,可以从javax.servlet-api-3.1.0.jar里找到源代码,ServletContextListener接口包含2个方法 ContextInitialized(ServletContextEvent sce)和contextDestroyed(ServletContextEvent sce),主要的功能是初始化ServletContext 容器和销毁ServletContext容器。

public interface ServletContextListener extends EventListener {

    /**
     * Receives notification that the web application initialization
     * process is starting.
     *
     * <p>All ServletContextListeners are notified of context
     * initialization before any filters or servlets in the web
     * application are initialized.
     *
     * @param sce the ServletContextEvent containing the ServletContext
     * that is being initialized
     */
    public void contextInitialized(ServletContextEvent sce);

    /**
     * Receives notification that the ServletContext is about to be
     * shut down.
     *
     * <p>All servlets and filters will have been destroyed before any
     * ServletContextListeners are notified of context
     * destruction.
     *
     * @param sce the ServletContextEvent containing the ServletContext
     * that is being destroyed
     */
    public void contextDestroyed(ServletContextEvent sce);
}

       contextInitialized放发在Servlet容器启动时,就会执行我们可以在ContextLoaderListener类的源代码里找到实现,找到了initWebApplicationContext(event.getServletContext())方法:

	@Override
	public void contextInitialized(ServletContextEvent event) {
		initWebApplicationContext(event.getServletContext());
	}

         包含了Spring容器的初始化逻辑:

2. Web应用初始化入口init()

        servlet接口里的init()方法是servlet容器初始化的入口,也就是说我们可以通过实现Servlet接口在init()方法里定义初始化逻辑。 接着看Spring框架里的org.springframework.web.servlet.HttpServletBean, HttpServletBean是一个抽象类,实现了servlet接口里的Init()方法, 源码如下:

public abstract class HttpServletBean extends HttpServlet implements EnvironmentCapable, EnvironmentAware {


@Override
	public final void init() throws ServletException {
		if (logger.isDebugEnabled()) {
			logger.debug("Initializing servlet '" + getServletName() + "'");
		}

		// Set bean properties from init parameters.
		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) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to set bean properties on servlet '" + getServletName() + "'", ex);
				}
				throw ex;
			}
		}

		// Let subclasses do whatever initialization they like.
        // web 应用的启动入口 initServletBean()
		initServletBean();

		if (logger.isDebugEnabled()) {
			logger.debug("Servlet '" + getServletName() + "' configured successfully");
		}
	}

    ...

}

        该方法的作用是容器初始化的入口,从代码里可以找到一个方法initServletBean(),该方法在FrameWorkServlet类里实现,代码如下:

@SuppressWarnings("serial")
public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware {
	/**
	 * Overridden method of {@link HttpServletBean}, invoked after any bean properties
	 * have been set. Creates this servlet's WebApplicationContext.
	 *  web 容器启动入口
	 */
	@Override
	protected final void initServletBean() throws ServletException {
		getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
		if (this.logger.isInfoEnabled()) {
			this.logger.info("FrameworkServlet '" + getServletName() + "': initialization started");
		}
		long startTime = System.currentTimeMillis();

		try {
			// 初始化web容器
			this.webApplicationContext = initWebApplicationContext();
			initFrameworkServlet();
		}
		catch (ServletException ex) {
			this.logger.error("Context initialization failed", ex);
			throw ex;
		}
		catch (RuntimeException ex) {
			this.logger.error("Context initialization failed", ex);
			throw ex;
		}

		if (this.logger.isInfoEnabled()) {
			long elapsedTime = System.currentTimeMillis() - startTime;
			this.logger.info("FrameworkServlet '" + getServletName() + "': initialization completed in " +
					elapsedTime + " ms");
		}
	}
// 其他代码用...表示
...
}

       我们发现FrameworkServlet类实现了一个ApplicationContextAware接口,该接口里只包含一个方法setApplicationContext(ApplicationContext applicationContext),在Spring中,我们知道如果有类实现了ApplicationContextAware接口,那么我们可以通过setApplicationContext(AppicationContext applicationContext)方法拿到applicationContext容器, WebApplicationContext接口是ApplicationContext的子接口,他们俩是一个父子容器的关系。

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) {
		if (this.webApplicationContext == null && applicationContext instanceof WebApplicationContext) {
			this.webApplicationContext = (WebApplicationContext) applicationContext;
			this.webApplicationContextInjected = true;
		}
	}

        从上述代码,我们可以发现webApplicationContext容器先通过setApplicationContext方法初始化赋值。

        为什么在FramworkServlet的initServletBean方法里又做了一次初始webApplicationContext

的动作呢?

        接着看initWebApplicationContext()方法,里面判断了一下wac能否转为ConfigurableWebApplciationContext, 如果能的话,那就继续判断是否需要设置父容器,如果需要的父容器那么设置父容器WebApplicationContext。

	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()) {
					// 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);
				}
			}
		}

        从上述代码中我们可以找到问题的答案,Web应用需要通过parent属性将各容器之间关联起来,如果出现找不到容器的情况下,那么会去创建一个默认的web容器, 该默认的容器是XmlWebApplicationContext。

if (wac == null) {
			// No context instance is defined for this servlet -> create a local one
			wac = createWebApplicationContext(rootContext);
		}

         一切初始化好后,那么就开始初始化mvc所有相关的策略,前提是没有收到refresh事件

	if (!this.refreshEventReceived) {
			// Either the context is not a ConfigurableApplicationContext with refresh
			// support or the context injected at construction time had already been
			// refreshed -> trigger initial onRefresh manually here.
			onRefresh(wac);
		}

        接着看DispatcherServlet! 

3. Spring-webmvc核心实现DispatcherServlet

        MVC是现在非常流行一种web应用架构,Spring框架在启动IOC容器时在DispatcherSevlet类里的Onrefresh()方法实现了MVC的九大组件的初始化策略:

public class DispatcherServlet extends FrameworkServlet {	

@Override
	protected void onRefresh(ApplicationContext context) {
		initStrategies(context);
	}
....

}

        初始化策略如下: 多文件上传、国际化、主题、处理器映射、处理器适配器、处理器异常解决方案、请求转换为视图转换器、视图解析器、Flashmap管理器。

	protected void initStrategies(ApplicationContext context) {
		initMultipartResolver(context);
		initLocaleResolver(context);
		initThemeResolver(context);
		initHandlerMappings(context);
		initHandlerAdapters(context);
		initHandlerExceptionResolvers(context);
		initRequestToViewNameTranslator(context);
		initViewResolvers(context);
		initFlashMapManager(context);
	}

        上述流程执行完毕后,Web应用的启动流程就基本结束了,最后把WebApplicationContext放入到ServletContext上下文里使用。

	if (this.publishContext) {
			// Publish the context as a servlet context attribute.
			String attrName = getServletContextAttributeName();
			getServletContext().setAttribute(attrName, wac);
			if (this.logger.isDebugEnabled()) {
				this.logger.debug("Published WebApplicationContext of servlet '" + getServletName() +
						"' as ServletContext attribute with name [" + attrName + "]");
			}
		}

        也就是在初始化的时候,先去找从ServletContext里去拿WebApplicationContext,如果没有拿到,那么再去createWebApplicationContext。

web容器初始化完毕后,WebApplicationContext也具有了Servlet相关的特性。 

二、 Spring IOC容器的启动原理分析

        从上述代码中可以发现,在启动web应用时,会伴随着IOC容器的启动,我们都知道Spring框架的核心是IOC,IOC简单的讲就是将对象的管理和依赖交给Spring去管理,其实Spring的IOC容器真正启动的时候是在调用AbstractApplicationContext.refresh()方法结束后,refresh()方法包含了Spring IOC容器启动的所有流程。

​        refresh()方法在AbstractApplicationContext类里有具体的实现:

​        refresh()方法源代码: 

public void refresh() throws BeansException, IllegalStateException {
		synchronized (this.startupShutdownMonitor) {
			// Prepare this context for refreshing.
			prepareRefresh();

			// Tell the subclass to refresh the internal bean factory.
			ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

			// Prepare the bean factory for use in this context.
			prepareBeanFactory(beanFactory);

			try {
				// Allows post-processing of the bean factory in context subclasses.
				postProcessBeanFactory(beanFactory);

				// Invoke factory processors registered as beans in the context.
				invokeBeanFactoryPostProcessors(beanFactory);

				// Register bean processors that intercept bean creation.
				registerBeanPostProcessors(beanFactory);

				// Initialize message source for this context.
				initMessageSource();

				// Initialize event multicaster for this context.
				initApplicationEventMulticaster();

				// Initialize other special beans in specific context subclasses.
				onRefresh();

				// Check for listener beans and register them.
				registerListeners();

				// Instantiate all remaining (non-lazy-init) singletons.
				finishBeanFactoryInitialization(beanFactory);

				// Last step: publish corresponding event.
				finishRefresh();
			}

			catch (BeansException ex) {
				if (logger.isWarnEnabled()) {
					logger.warn("Exception encountered during context initialization - " +
							"cancelling refresh attempt: " + ex);
				}

				// Destroy already created singletons to avoid dangling resources.
				destroyBeans();

				// Reset 'active' flag.
				cancelRefresh(ex);

				// Propagate exception to caller.
				throw ex;
			}

			finally {
				// Reset common introspection caches in Spring's core, since we
				// might not ever need metadata for singleton beans anymore...
				resetCommonCaches();
			}
		}
	}

        IOC容器的启动是也就意味着Spring应用的启动成功,我们才能从IOC容器中拿到Bean。 

三、Spring Boot 应用启动原理分析

        Spring本身是一个低侵入、可扩展的轻量级框架,现在最流行的Springboot 框架也是基于Spring实现的。

        可以从main()方法找到boot的启动路线以及原理, 进入到SpringApplication.run(String... args)方法:

        我们可以发现最终调用到了AbstractApplicationContext.refresh()方法, 如下:

         由此可见,Springboot一方面帮我们的开发简化了很多配置上的工作,另一方面具备Spring 框架所有的功能。

        在SpringBoot中,DispatcherServlet是有新的请求过来的时候会初始化, 是在Tomcat容器初始化完成后:

         访问localhost:8080, 可以发现控制台多了三条打印,其实FrameWorkServlet在初始化DispatcherServlet。

        再次证明了initServletBean() 是初始化Servlet相关web容器的入口。

四、SpringBoot零配置启动原理 

        我们在使用SpringBoot框架时,不需要配置额外的spring、springmvc等相关的配置,由SpringBoot帮我们去实现自动化配置,我们不需要配置任何xml信息就可以启动一个最简单的SpringBoot应用。

        因为在spring-boot-starter-web包里依赖了一个org.springframework.boot:spring-boot-autoconfigure包,autoconfigure包里实现了自动装配原理。

Spring.factories

        spring.factories是Spring框架对外提供的一个注册式的SPI,可以扫描到除了@ComponenetScan以为的类,也就是说我们可以在resource目录下新建META-INF/spring.factories文件,然后只需要将接口的实现写入到该文件里,Spring 框架会扫描到所有的接口实现,从而管理这些类。

        Spring-core包里有扫描Spring.factoires文件的实现SpringFactoriesLoader,所在路径为org.springframework.core.io.support.SpringFactoriesLoader.java, 可以发现此类里会扫描Spring应用的META-INF/spring.factories文件里的所有配置到一个ConcurrentHashMap里,

    static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap();

        接着看org.springframework.boot:spring-boot-autoconfigure2.5.12 包里的spring.factoires,找到org.springframework.boot.autoconfigure.EnableAutoConfiguration 这一栏,可以发现配置了一个List列表数据。

        这里面配置了Spring应用所有相关的配置类:

        举个栗子,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration, DataSourceAutoConfiguration类实现了mysql数据源的属性配置注入:

         我们只需要在application.properties文件里配置数据源即可:

spring.datasource.url=jdbc:mysql://${server.host}:${mysql.port}/db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver