您的位置:首页 > 运维架构 > Tomcat

spring boot 源码解析16-spring boot外置tomcat部署揭秘

2018-01-05 15:09 801 查看

前言

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启动,后续的故事就和我们之前的分析一样了.这里就不在赘述了.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  spring 源码 tomcat