前言
spring boot 内嵌了一个servlet 容器,但是有的时候,可以还是希望将spring boot 应用部署到tomcat 中,通过war包的方式,那么该如何实现呢? 原理是什么呢? 我们从以下2点来说明:
- spring boot外置tomcat实现
- spring boot外置tomcat分析
spring boot外置tomcat实现
项目结构如下:
pom 文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.jihegupiao.demo</groupId> <artifactId>spring-boot-war-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>spring-boot-war-demo</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <version>2.2</version> <configuration> <url>http://localhost:8080/manager/text</url> <server>Tomcat7</server> <username>admin</username> <password>admin</password> <port>8082</port> <uriEncoding>UTF-8</uriEncoding> <path>/</path> <warFile>${basedir}/target/${project.build.finalName}.war</warFile> </configuration> </plugin> </plugins> </build> </project>
将原先的启动类修改为如下:
package com.example.demo; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.web.support.SpringBootServletInitializer; @SpringBootApplication public class ServletInitializer extends SpringBootServletInitializer { @Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(ServletInitializer.class); } public static void main(String[] args) { SpringApplication.run(ServletInitializer.class, args); } }
其中configure方法 指定了启动类
测试controller如下:
package com.example.demo.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; @Controller public class TestController { @RequestMapping(value = "/test", method = RequestMethod.GET) @ResponseBody public String test() { return "hi"; } }
- 测试一下吧,执行 mvn:clean install tomcat7:run, 访问http://127.0.0.1:8082/test,如果正常的话,返回 hi.
spring boot外置tomcat分析
上篇文章有提到,spring 4 通过 servlet3.0 规范 实现了 spring mvc 零配置,其关键的核心是SpringServletContainerInitializer,其为加载类路径下所有WebApplicationInitializer的实现,此时有如下实现:
- JerseyWebApplicationInitializer
- ServletInitializer(我们的启动类继承了SpringBootServletInitializer,其实现了WebApplicationInitializer,因此,该类会自动被加载)
JerseyWebApplicationInitializer#onStartup 代码如下:
public void onStartup(ServletContext servletContext) throws ServletException { // We need to switch *off* the Jersey WebApplicationInitializer because it // will try and register a ContextLoaderListener which we don't need servletContext.setInitParameter("contextConfigLocation", "<NONE>"); }
向ServletContext 添加了一个初始化参数–>key:contextConfigLocation,value:
SpringBootServletInitializer#onStartup,其代码如下:
public void onStartup(ServletContext servletContext) throws ServletException { // Logger initialization is deferred in case a ordered // LogServletContextInitializer is being used // 1. 初始化log this.logger = LogFactory.getLog(getClass()); // 2.创建WebApplicationContext WebApplicationContext rootAppContext = createRootApplicationContext( servletContext); if (rootAppContext != null) { // 3. 添加ContextLoaderListener,ContextLoaderListener 初始化时没有做任何事, 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"); } }
2件事:
- 初始化logger
- 调用createRootApplicationContext,创建WebApplicationContext,如果创建成功,则添加一个ContextLoaderListener,该Listener在contextInitialized中没有做任何事,因为ApplicationContext在创建的过程中已经初始化了.否则,打印日志. createRootApplicationContext代码如下:
protected WebApplicationContext createRootApplicationContext( ServletContext servletContext) { // 1. 初始化SpringApplicationBuilder SpringApplicationBuilder builder = createSpringApplicationBuilder(); // 2. 初始化StandardServletEnvironment StandardServletEnvironment environment = new StandardServletEnvironment(); environment.initPropertySources(servletContext, null); builder.environment(environment); // 3. 设置启动类为当前类 builder.main(getClass()); // 4. 如果存在父容器,则添加一个ParentContextApplicationContextInitializer 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)); } // 5. 添加ServletContextApplicationContextInitializer builder.initializers( new ServletContextApplicationContextInitializer(servletContext)); // 6. 设置contextClass 为 AnnotationConfigEmbeddedWebApplicationContext builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class); // 7. 个性化配置 builder = configure(builder); SpringApplication application = builder.build(); // 如果sources 为空,并且启动类有@Configuration 注解,则添加当前类到sources中 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) { // 8. 如果registerErrorPageFilter 为true,默认为true,则向sources中添加ErrorPageFilterConfiguration application.getSources().add(ErrorPageFilterConfiguration.class); } // 9. 启动 return run(application); }
10件事:
创建SpringApplicationBuilder.代码如下:
protected SpringApplicationBuilder createSpringApplicationBuilder() { return new SpringApplicationBuilder(); }
实例化StandardServletEnvironment. StandardServletEnvironment初始化的过程我们之前的文章有分析过,其构造器会向其内部持有的propertySources 添加如下Source:
- 名为servletConfigInitParams 的StubPropertySource
- 名为servletContextInitParams 的StubPropertySource
- 如果jndi存在的话,则添加名为jndiProperties 的StubPropertySource,这个默认是会添加的
- 名为systemProperties,值为System#getProperties的返回值 的MapPropertySource
- 名为systemEnvironment,值为System#getenv的返回值 的SystemEnvironmentPropertySource
接下来调用StandardServletEnvironment#initPropertySources进行初始化servletConfigInitParams, servletContextInitParams 所对应的Source.代码如下:
@Override public void initPropertySources(ServletContext servletContext, ServletConfig servletConfig) { WebApplicationContextUtils.initServletPropertySources(getPropertySources(), servletContext, servletConfig); }
调用
public static void initServletPropertySources( MutablePropertySources propertySources, ServletContext servletContext, ServletConfig servletConfig) { Assert.notNull(propertySources, "'propertySources' must not be null"); if (servletContext != null && propertySources.contains(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME) && propertySources.get(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME) instanceof StubPropertySource) { propertySources.replace(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME, new ServletContextPropertySource(StandardServletEnvironment.SERVLET_CONTEXT_PROPERTY_SOURCE_NAME, servletContext)); } if (servletConfig != null && propertySources.contains(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME) && propertySources.get(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME) instanceof StubPropertySource) { propertySources.replace(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME, new ServletConfigPropertySource(StandardServletEnvironment.SERVLET_CONFIG_PROPERTY_SOURCE_NAME, servletConfig)); } }
注意,这里由于ServletConfig等于null,因此最终StandardServletEnvironment持有了servletContext.
设置启动类为当前类,也就是我们项目中的ServletInitializer.class
调用getExistingRootWebApplicationContext,获得父容器,如果存在,则添加一个ParentContextApplicationContextInitializer.代码如下:
private ApplicationContext getExistingRootWebApplicationContext( ServletContext servletContext) { Object context = servletContext.getAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE); if (context instanceof ApplicationContext) { return (ApplicationContext) context; } return null; }
这里是获取不到的
添加ServletContextApplicationContextInitializer,代码如下:
builder.initializers( new ServletContextApplicationContextInitializer(servletContext));
其在SpringApplication#run中最终会调用其initialize方法,代码如下:
public void initialize(ConfigurableWebApplicationContext applicationContext) { applicationContext.setServletContext(this.servletContext); if (this.addApplicationContextAttribute) { this.servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, applicationContext); } }
- 为ConfigurableWebApplicationContext也就是SpringApplication所持有的设置ServletContext
- 如果addApplicationContextAttribute(是否向servletContext中保存applicationContext)为true,则进行添加,由于我们在实例化ServletContextApplicationContextInitializer时传入的false,因此这步是不会执行的.
问题: 我们知道,在spring mvc 中, applicationContext 是需要保存在servletContext中的,此时我们就可以调用WebApplicationContextUtils#getWebApplicationContext,从而在service层获得WebApplicationContext的实例,那么在外置tomcat中,是何时设置的呢?
在SpringApplication的启动过程中,最终会调用 AbstractApplicationContext#refresh,在该方法中,调用了EmbeddedWebApplicationContext#onRefresh,最终调用了createEmbeddedServletContainer,代码如下:
private void createEmbeddedServletContainer() { EmbeddedServletContainer localContainer = this.embeddedServletContainer; // 1. 获得ServletContext ServletContext localServletContext = getServletContext(); if (localContainer == null && localServletContext == null) { // 2 内置Servlet容器和ServletContext都还没初始化的时候执行 // 2.1 获取自动加载的工厂 EmbeddedServletContainerFactory containerFactory = getEmbeddedServletContainerFactory(); // 2.2 获取Servlet初始化器并创建Servlet容器,依次调用Servlet初始化器中的onStartup方法 this.embeddedServletContainer = containerFactory .getEmbeddedServletContainer(getSelfInitializer()); } else if (localServletContext != null) { // 3. 内置Servlet容器已经初始化但是ServletContext还没初始化,则进行初始化.一般不会到这里 try { getSelfInitializer().onStartup(localServletContext); } catch (ServletException ex) { throw new ApplicationContextException("Cannot initialize servlet context", ex); } } // 4. 初始化PropertySources initPropertySources(); }
- 获得ServletContext,
如果localContainer等于null并且ServletContext等于null,则意味着是内置容器的情况,这时只需获得嵌入容器就行了,在调用EmbeddedServletContainerFactory#getEmbeddedServletContainer时将ServletContextInitializer传入了进去,其onStartup方法调用了EmbeddedWebApplicationContext#selfInitialize,一般情况下,此时调用的是TomcatEmbeddedServletContainerFactory#getEmbeddedServletContainer,经过层层调用,最终实例化了TomcatStarter,其实现了ServletContainerInitializer接口,当容器初始化的时候,会调用其onStartup方法,而在TomcatStarter的实现中,会依次调用其内部持有的ServletContextInitializer的onStartup进行处理,代码如下:
for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); }
因此,也就会调用到之前在EmbeddedServletContainerFactory#getEmbeddedServletContainer时实例化的ServletContextInitializer,也就会调用到EmbeddedWebApplicationContext#selfInitialize,代码如下:
private void selfInitialize(ServletContext servletContext) throws ServletException { prepareEmbeddedWebApplicationContext(servletContext); ConfigurableListableBeanFactory beanFactory = getBeanFactory(); ExistingWebApplicationScopes existingScopes = new ExistingWebApplicationScopes( beanFactory); // 注册了各种属于web的scope WebApplicationContextUtils.registerWebApplicationScopes(beanFactory, getServletContext()); existingScopes.restore(); // 注册了web特定的contextParameters,contextAttributes等 WebApplicationContextUtils.registerEnvironmentBeans(beanFactory, getServletContext()); for (ServletContextInitializer beans : getServletContextInitializerBeans()) { beans.onStartup(servletContext); // servlet、filter和listener都会注册到ServletContext上 } }
其中 prepareEmbeddedWebApplicationContext方法中有
servletContext.setAttribute( WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);
从而向servletContext中保存了自己.
否则,就是外置tomcat的情况(对于当前情况,localContainer是等于Null的,因为要进行创建,而ServletContext是在ServletContextApplicationContextInitializer#initialize中赋值的).此时会最终调用selfInitialize方法.接下来同样也会调用prepareEmbeddedWebApplicationContext方法,在servletContext中保存了自己(同第2步)
设置contextClass 为 AnnotationConfigEmbeddedWebApplicationContext
个性化配置,这里我们复写了该方法,如下:
@Override protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { return application.sources(ServletInitializer.class); }
- 构建出SpringApplication,如果SpringApplication 中的sources 为空,并且启动类有@Configuration 注解,则添加当前类到sources中,对于当前,由于我们在第7步已经加入了ServletInitializer.class,因此这步是不会执行的.
如果registerErrorPageFilter,默认为true,则向sources中添加ErrorPageFilterConfiguration. 在该类中声明了ErrorPageFilter.代码如下:
@Bean public ErrorPageFilter errorPageFilter() { return new ErrorPageFilter(); }
是一个Filter,关于这个的作用我们在后续的文章进行分析
- 调用SpringApplication#run启动,后续的故事就和我们之前的分析一样了.这里就不在赘述了.