外置容器创建及启动ApplicationContext过程

Posted 恒奇恒毅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了外置容器创建及启动ApplicationContext过程相关的知识,希望对你有一定的参考价值。

一、外置容器使用步骤

SpringBoot Web在外置容器中也可以运行,使用步骤是

  1. 须创建一个war项目;
  2. 将嵌入式的Tomcat指定为provided;
<dependency> 
	<groupId>org.springframework.boot</groupId> 
	<artifactId>spring‐boot‐starter‐tomcat</artifactId> 
	<scope>provided</scope> 
</dependency>
  1. 编写一个SpringBootServletInitializer的子类,并调用configure方法,传入主配置类(标注@SpringBootApplication
public class ServletInitializer extends SpringBootServletInitializer  
	@Override 
	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder)  
		//传入SpringBoot应用的主程序 
		return 	builder.sources(SpringBootApplicationMain.class); 
	 

当然我们还可以利用builder对应用做更多的配置
4. 将应用打成war包部署到web容器,比如tomcat

二、Servlet3.0规范ServletContainerInitializer

在说明启动原理之前我们先回顾一下Servlet3.0规范关于Shared libraries / runtimes pluggability的说明。

  1. 服务器启动(web应用启动)会创建当前web应用里面每一个jar包里面ServletContainerInitializer实例
  2. ServletContainerInitializer的实现放在jar包的META-INF/services文件夹下,有一个名为 javax.servlet.ServletContainerInitializer的文件,内容就是ServletContainerInitializer的实现类的全类名
  3. ServletContainerInitializer实现类上使用@HandlesTypes,在应用启动的时候传入我们感兴趣的类;
/**
 * ServletContainerInitializers (SCIs) are registered via an entry in the
 * file META-INF/services/javax.servlet.ServletContainerInitializer that must be
 * included in the JAR file that contains the SCI implementation.
 * <p>
 * SCI processing is performed regardless of the setting of metadata-complete.
 * SCI processing can be controlled per JAR file via fragment ordering. If an
 * absolute ordering is defined, the only those JARs included in the ordering
 * will be processed for SCIs. To disable SCI processing completely, an empty
 * absolute ordering may be defined.
 * <p>
 * SCIs register an interest in annotations (class, method or field) and/or
 * types via the @link javax.servlet.annotation.HandlesTypes annotation which
 * is added to the class.
 *
 * @since Servlet 3.0
 */
public interface ServletContainerInitializer 

    /**
     * Receives notification during startup of a web application of the classes
     * within the web application that matched the criteria defined via the
     * @link javax.servlet.annotation.HandlesTypes annotation.
     *
     * @param c     The (possibly null) set of classes that met the specified
     *              criteria
     * @param ctx   The ServletContext of the web application in which the
     *              classes were discovered
     *
     * @throws ServletException If an error occurs
     */
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;

标注在该类实现类上的类,会通过onStartup的第一个参数传递进来。

三、启动原理

  1. 我们发现在spring-web模块下,jar包的META-INF/services下的确存在一个文件javax.servlet.ServletContainerInitializer,其内容就是org.springframework.web.SpringServletContainerInitializer
  2. SpringServletContainerInitializer的类定义如下,类上标注了@HandlesTypes(WebApplicationInitializer.class),传入了WebApplicationInitializer,于是在加载的时候onStartup方法的第一个参数会传递所有类路径下的WebApplicationInitializer的子类。在onStartup方法中,实例化了所有WebApplicationInitializer的可实例化子类,排序后分别调用了其onStartup方法,传入ServletContext
/**
 * Servlet 3.0 @link ServletContainerInitializer designed to support code-based
 * configuration of the servlet container using Spring's @link WebApplicationInitializer
 * SPI as opposed to (or possibly in combination with) the traditional
 * @code web.xml-based approach.
 *
 * <h2>Mechanism of Operation</h2>
 * This class will be loaded and instantiated and have its @link #onStartup
 * method invoked by any Servlet 3.0-compliant container during container startup assuming
 * that the @code spring-web module JAR is present on the classpath. This occurs through
 * the JAR Services API @link ServiceLoader#load(Class) method detecting the
 * @code spring-web module's @code META-INF/services/javax.servlet.ServletContainerInitializer
 * service provider configuration file. See the
 * <a href="http://download.oracle.com/javase/6/docs/technotes/guides/jar/jar.html#Service%20Provider">
 * JAR Services API documentation</a> as well as section <em>8.2.4</em> of the Servlet 3.0
 * Final Draft specification for complete details.
 *
 * <h3>In combination with @code web.xml</h3>
 * A web application can choose to limit the amount of classpath scanning the Servlet
 * container does at startup either through the @code metadata-complete attribute in
 * @code web.xml, which controls scanning for Servlet annotations or through an
 * @code <absolute-ordering> element also in @code web.xml, which controls which
 * web fragments (i.e. jars) are allowed to perform a @code ServletContainerInitializer
 * scan. When using this feature, the @link SpringServletContainerInitializer
 * can be enabled by adding "spring_web" to the list of named web fragments in
 * @code web.xml as follows:
 *
 * <pre class="code">
 * @code
 * <absolute-ordering>
 *   <name>some_web_fragment</name>
 *   <name>spring_web</name>
 * </absolute-ordering>
 * </pre>
 *
 * <h2>Relationship to Spring's @code WebApplicationInitializer</h2>
 * Spring's @code WebApplicationInitializer SPI consists of just one method:
 * @link WebApplicationInitializer#onStartup(ServletContext). The signature is intentionally
 * quite similar to @link ServletContainerInitializer#onStartup(Set, ServletContext):
 * simply put, @code SpringServletContainerInitializer is responsible for instantiating
 * and delegating the @code ServletContext to any user-defined
 * @code WebApplicationInitializer implementations. It is then the responsibility of
 * each @code WebApplicationInitializer to do the actual work of initializing the
 * @code ServletContext. The exact process of delegation is described in detail in the
 * @link #onStartup onStartup documentation below.
 *
 * <h2>General Notes</h2>
 * In general, this class should be viewed as <em>supporting infrastructure</em> for
 * the more important and user-facing @code WebApplicationInitializer SPI. Taking
 * advantage of this container initializer is also completely <em>optional</em>: while
 * it is true that this initializer will be loaded and invoked under all Servlet 3.0+
 * runtimes, it remains the user's choice whether to make any
 * @code WebApplicationInitializer implementations available on the classpath. If no
 * @code WebApplicationInitializer types are detected, this container initializer will
 * have no effect.
 *
 * <p>Note that use of this container initializer and of @code WebApplicationInitializer
 * is not in any way "tied" to Spring MVC other than the fact that the types are shipped
 * in the @code spring-web module JAR. Rather, they can be considered general-purpose
 * in their ability to facilitate convenient code-based configuration of the
 * @code ServletContext. In other words, any servlet, listener, or filter may be
 * registered within a @code WebApplicationInitializer, not just Spring MVC-specific
 * components.
 *
 * <p>This class is neither designed for extension nor intended to be extended.
 * It should be considered an internal type, with @code WebApplicationInitializer
 * being the public-facing SPI.
 *
 * <h2>See Also</h2>
 * See @link WebApplicationInitializer Javadoc for examples and detailed usage
 * recommendations.<p>
 *
 * @author Chris Beams
 * @author Juergen Hoeller
 * @author Rossen Stoyanchev
 * @since 3.1
 * @see #onStartup(Set, ServletContext)
 * @see WebApplicationInitializer
 */
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer 

	/**
	 * Delegate the @code ServletContext to any @link WebApplicationInitializer
	 * implementations present on the application classpath.
	 * <p>Because this class declares @@code HandlesTypes(WebApplicationInitializer.class),
	 * Servlet 3.0+ containers will automatically scan the classpath for implementations
	 * of Spring's @code WebApplicationInitializer interface and provide the set of all
	 * such types to the @code webAppInitializerClasses parameter of this method.
	 * <p>If no @code WebApplicationInitializer implementations are found on the classpath,
	 * this method is effectively a no-op. An INFO-level log message will be issued notifying
	 * the user that the @code ServletContainerInitializer has indeed been invoked but that
	 * no @code WebApplicationInitializer implementations were found.
	 * <p>Assuming that one or more @code WebApplicationInitializer types are detected,
	 * they will be instantiated (and <em>sorted</em> if the @@link
	 * org.springframework.core.annotation.Order @Order annotation is present or
	 * the @link org.springframework.core.Ordered Ordered interface has been
	 * implemented). Then the @link WebApplicationInitializer#onStartup(ServletContext)
	 * method will be invoked on each instance, delegating the @code ServletContext such
	 * that each instance may register and configure servlets such as Spring's
	 * @code DispatcherServlet, listeners such as Spring's @code ContextLoaderListener,
	 * or any other Servlet API componentry such as filters.
	 * @param webAppInitializerClasses all implementations of
	 * @link WebApplicationInitializer found on the application classpath
	 * @param servletContext the servlet context to be initialized
	 * @see WebApplicationInitializer#onStartup(ServletContext)
	 * @see AnnotationAwareOrderComparator
	 */
	@Override
	public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException 

		List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();

		if (webAppInitializerClasses != null) 
			for (Class<?> waiClass : webAppInitializerClasses) 
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) 
					try 
						initializers.add((WebApplicationInitializer) 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;
		

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) 
			initializer.onStartup(servletContext);
		
	


WebApplicationInitializer类定义如下:

/**
 * Interface to be implemented in Servlet 3.0+ environments in order to configure the
 * @link ServletContext programmatically -- as opposed to (or possibly in conjunction
 * with) the traditional @code web.xml-based approach.
 *
 * <p>Implementations of this SPI will be detected automatically by @link
 * SpringServletContainerInitializer, which itself is bootstrapped automatically
 * by any Servlet 3.0 container. See @linkplain SpringServletContainerInitializer its
 * Javadoc for details on this bootstrapping mechanism.
 *
 * <h2>Example</h2>
 * <h3>The traditional, XML-based approach</h3>
 * Most Spring users building a web application will need to register Spring's @code
 * DispatcherServlet. For reference, in WEB-INF/web.xml, this would typically be done as
 * follows:
 * <pre class="code">
 * @code
 * <servlet>
 *   <servlet-name>dispatcher</servlet-name>
 *   <servlet-class>
 *     org.springframework.web.servlet.DispatcherServlet
 *   </servlet-class>
 *   <init-param>
 *     <param-name>contextConfigLocation</param-name>
 *     <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
 *   </init-param>
 *   <load-on-startup>1</load-on-startup>
 * </servlet>
 *
 * <servlet-mapping>
 *   <servlet-name>dispatcher</servlet-name>
 *   <url-pattern>/</url-pattern>
 * </servlet-mapping></pre>
 *
 * <h3>The code-based approach with @code WebApplicationInitializer</h3>
 * Here is the equivalent @code DispatcherServlet registration logic,
 * @code WebApplicationInitializer-style:
 * <pre class="code">
 * public class MyWebAppInitializer implements WebApplicationInitializer 
 *
 *    &#064;Override
 *    public void onStartup(ServletContext container) 
 *      XmlWebApplicationContext appContext = new XmlWebApplicationContext();
 *      appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
 *
 *      ServletRegistration.Dynamic dispatcher =
 *        container.addServlet("dispatcher", new DispatcherServlet(appContext));
 *      dispatcher.setLoadOnStartup(1);
 *      dispatcher.addMapping("/");
 *    
 *
 * </pre>
 *
 * As an alternative to the above, you can also extend from @link
 * org.springframework.web.servlet.support.AbstractDispatcherServletInitializer.
 *
 * As you can see, thanks to Servlet 3.0's new @link ServletContext#addServlet method
 * we're actually registering an <em>instance</em> of the @code DispatcherServlet, and
 * this means that the @code DispatcherServlet can now be treated like any other object
 * -- receiving constructor injection of its application context in this case.
 *
 * <p>This style is both simpler and more concise. There is no concern for dealing with
 * init-params, etc, just normal JavaBean-style properties and constructor arguments. You
 * are free to create and work with your Spring application contexts as necessary before
 * injecting them into the @code DispatcherServlet.
 *
 * <p>Most major Spring Web components have been updated to support this style of
 * registration.  You'll find that @code DispatcherServlet, @code FrameworkServlet,
 * @code ContextLoaderListener and @code DelegatingFilterProxy all now support
 * constructor arguments. Even if a component (e.g. non-Spring, other third party) has not
 * been specifically updated for use within @code WebApplicationInitializers, they still
 * may be used in any case. The Servlet 3.0 @code ServletContext API allows for setting
 * init-params, context-params, etc programmatically.
 *
 * <h2>A 100% code-based approach to configuration</h2>
 * In the example above, @code WEB-INF/web.xml was successfully replaced with code in
 * the form of a @code WebApplicationInitializer, but the actual
 * @code dispatcher-config.xml Spring configuration remained XML-based.
 * @code WebApplicationInitializer is a perfect fit for use with Spring's code-based
 * @code @Configuration classes. See @@link
 * org.springframework.context.annotation.Configuration Configuration Javadoc for
 * complete details, but the following example demonstrates refactoring to use Spring's
 * @link org.springframework.web.context.support.AnnotationConfigWebApplicationContext
 * AnnotationConfigWebApplicationContext in lieu of @code XmlWebApplicationContext, and
 * user-defined @code @Configuration classes @code AppConfig and
 * @code DispatcherConfig instead of Spring XML files. This example also goes a bit
 * beyond those above to demonstrate typical configuration of the 'root' application
 * context and registration of the @code ContextLoaderListener:
 * <pre class="code">
 * public class MyWebAppInitializer implements WebApplicationInitializer 
 *
 *    &#064;Override
 *    public void onStartup(ServletContext container) 
 *      // Create the 'root' Spring application context
 *      AnnotationConfigWebApplicationContext rootContext =
 *        new AnnotationConfigWebApplicationContext();
 *      rootContext.register(AppConfig.class);
 *
 *      // Manage the lifecycle of the root application context
 *      container.addListener(new ContextLoaderListener(rootContext));
 *
 *      // Create the dispatcher servlet's Spring application context
 *      AnnotationConfigWebApplicationContext dispatcherContext =
 *        new AnnotationConfigWebApplicationContext();
 *      dispatcherContext.register(DispatcherConfig.class);
 *
 *      // Register and map the dispatcher servlet
 *      ServletRegistration.Dynamic dispatcher =
 *        container.addServlet("dispatcher", new DispatcherServlet(dispatcherContext));
 *      dispatcher.setLoadOnStartup(1);
 *      dispatcher.addMapping("/");
 *    
 *
 * </pre>
 *
 * As an alternative to the above, you can also extend from @link
 * org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer.
 *
 * Remember that @code WebApplicationInitializer implementations are <em>detected
 * automatically</em> -- so you are free to package them within your application as you
 * see fit.
 *
 * <h2>Ordering @code WebApplicationInitializer execution</h2>
 * @code WebApplicationInitializer implementations may optionally be annotated at the
 * class level with Spring's @@link org.springframework.core.annotation.Order Order
 * annotation or may implement Spring's @link org.springframework.core.Ordered Ordered
 * interface. If so, the initializers will be ordered prior to invocation. This provides
 * a mechanism for users to ensure the order in which servlet container initialization
 * occurs. Use of this feature is expected to be rare, as typical applications will likely
 * centralize all container initialization within a single @code WebApplicationInitializer.
 *
 * <h2>Caveats</h2>
 *
 * <h3>web.xml versioning</h3>
 * <p>@code WEB-INF/web.xml and @code WebApplicationInitializer use are not mutually
 * exclusive; for example, web.xml can register one servlet, and a @code
 * WebApplicationInitializer can register another. An initializer can even
 * <em>modify</em> registrations performed in @code web.xml through methods such as
 * @link ServletContext#getServletRegistration(String). <strong>However, if
 * @code WEB-INF/web.xml is present in the application, its @code version attribute
 * must be set to "3.0" or greater, otherwise @code ServletContainerInitializer
 * bootstrapping will be ignored by the servlet container.</strong>
 *
 * <h3>Mapping to '/' under Tomcat</h3>
 * <p>Apache Tomcat maps its internal @code DefaultServlet to "/", and on Tomcat versions
 * &lt;= 7.0.14, this servlet mapping <em>cannot be overridden programmatically</em>.
 * 7.0.15 fixes this issue. Overriding the "/" servlet mapping has also been tested
 * successfully under GlassFish 3.1.<p>
 *
 * @author Chris Beams
 * @since 3.1
 * @see SpringServletContainerInitializer
 * @see org.springframework.web.context.AbstractContextLoaderInitializer
 * @see org.springframework.web.servlet.support.AbstractDispatcherServletInitializer
 * @see org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer
 */
public interface WebApplicationInitializer 

	/**
	 * Configure the given @link ServletContext with any servlets, filters, listeners
	 * context-params and attributes necessary for initializing this web application. See
	 * examples @linkplain WebApplicationInitializer above.
	 * @param servletContext the @code ServletContext to initialize
	 * @throws ServletException if any call against the given @code ServletContext
	 * throws a @code ServletException
	 */
	void onStartup(ServletContext servletContext) throws ServletException;


  1. 上面的机制中,我们实现的WebApplicationInitializer就会被实例化并调用onStartup。一般我们不会直接实现WebApplicationInitializer,而是继承SpringBootServletInitializer,它完成了容器的创建。我们看到除开SpringBootServletInitializer还有其他类也实现了WebApplicationInitializer,后面有机会再表。
  2. SpringBootServletInitializer,该类完成了容器的创建。
public abstract class SpringBootServletInitializer implements WebApplicationInitializer 

	protected Log logger; // Don't initialize early

	private boolean registerErrorPageFilter = true;

	/**
	 * Set if the @link ErrorPageFilter should be registered. Set to @code false if
	 * error page mappings should be handled via the Servlet container and not Spring
	 * Boot.
	 * @param registerErrorPageFilter if the @link ErrorPageFilter should be registered.
	 */
	protected final void setRegisterErrorPageFilter(boolean registerErrorPageFilter) 
		this.registerErrorPageFilter = registerErrorPageFilter;
	

	@Override
	public void onStartup(ServletContext servletContext) throws ServletException 
		// Logger initialization is deferred in case a ordered
		// LogServletContextInitializer is being used
		this.logger = LogFactory.getLog(getClass());
		WebApplicationContext rootAppContext = createRootApplicationContext(
				servletContext);
		if (rootAppContext != null) 
			servletContext.addListener(new ContextLoaderListener(rootAppContext) 
				@Override
				public void contextInitialized(ServletContextEvent event) 
					// no-op because the application context is already initialized
				
			);
		
		else 
			this.logger.debug("No ContextLoaderListener registered, as "
					+ "createRootApplicationContext() did not "
					+ "return an application context");
		
	

	protected WebApplicationContext createRootApplicationContext(
			ServletContext servletContext) 
		SpringApplicationBuilder builder = createSpringApplicationBuilder();
		builder.main(getClass());
		ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
		if (parent != null) 
			this.logger.info("Root context already created (using as parent).");
			servletContext.setAttribute(
					WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
			builder.initializers(new ParentContextApplicationContextInitializer(parent));
		
		builder.initializers(
				new ServletContextApplicationContextInitializer(servletContext));
		builder.listeners(new ServletContextApplicationListener(servletContext));
		builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
		builder = configure(builder);
		SpringApplication application = builder.build();
		if (application.getSources().isEmpty() && AnnotationUtils
				.findAnnotation(getClass(), Configuration.class) != null) 
			application.getSources().add(getClass());
		
		Assert.state(!application.getSources().isEmpty(),
				"No SpringApplication sources have been defined. Either override the "
						+ "configure method or add an @Configuration annotation");
		// Ensure error pages are registered
		if (this.registerErrorPageFilter) 
			application.getSources().add(ErrorPageFilterConfiguration.class);
		
		return run(application);
	

	/**
	 * Returns the @code SpringApplicationBuilder that is used to configure and create
	 * the @link SpringApplication. The default implementation returns a new
	 * @code SpringApplicationBuilder in its default state.
	 * @return the @code SpringApplicationBuilder.
	 * @since 1.3.0
	 */
	protected SpringApplicationBuilder createSpringApplicationBuilder() 
		return new SpringApplicationBuilder();
	

	/**
	 * Called to run a fully configured @link SpringApplication.
	 * @param application the application to run
	 * @return the @link WebApplicationContext
	 */
	protected WebApplicationContext run(SpringApplication application) 
		return (WebApplicationContext) application.run();
	

	private ApplicationContext getExistingRootWebApplicationContext(
			ServletContext servletContext) 
		Object context = servletContext.getAttribute(
				WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
		if (context instanceof ApplicationContext) 
			return (ApplicationContext) context;
		
		return null;
	

	/**
	 * Configure the application. Normally all you would need to do is to add sources
	 * (e.g. config classes) because other settings have sensible defaults. You might
	 * choose (for instance) to add default command line arguments, or set an active
	 * Spring profile.
	 * @param builder a builder for the application context
	 * @return the application builder
	 * @see SpringApplicationBuilder
	 */
	protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) 
		return builder;
	



createRootApplicationContext方法中,首先创建一个SpringApplicationBuilder,然后配置一系列的属性(最关键的设置了ApplicationContext为AnnotationConfigEmbeddedWebApplicationContext和配置注解源),再据此创建了SpringApplication,最后调用其run方法完成容器启动。run方法后面再表。
6. 提供了一个我们可以定制builder属性的方法configure。利用该方法,我们可以设置注解源、环境配置等。

protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) 
		return builder;
	

以上是关于外置容器创建及启动ApplicationContext过程的主要内容,如果未能解决你的问题,请参考以下文章

使用外置的Servlet容器

『中级篇』docker之java容器运行外置springboot-jar(番外篇)(79)

SpringBoot配置外部Tomcat项目启动流程源码分析(上)

SpringBoot配置外部Tomcat项目启动流程源码分析(上)

docker容器自动启动及修改配置文件

面试阿里,字节,美团必看的Spring的Bean管理详解