springboot发布到tomcat

前言

最近在学习springboot相关的东西,其中有一个点就是项目开发完成后的部署。我们都知道springboot项目可以打包为只执行jar的方式和war包的方式。可执行jar使用的是内嵌的servlet容器,war包的方式可以部署到外部的容器中。通常我们都使用外部容器的方式来部署应用。这篇文章就将我学习到的如何将springboot应用部署到tomcat中的方法记录下来。

tomcat版本选择

springboot内嵌的tomcat版本是tomcat7或更高的版本(因为springboot依赖servlet3.0规范),所以我们外部的tomcat最低版本应该是tomcat7。如果必须使用tomcat6,则可以参考这篇文章https://github.com/dsyer/spring-boot-legacy

项目配置

第一步:修改项目打包方式

1
<packaging>war</packaging>

第二步:修改pom.xml配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<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-tomcat</artifactId>
<scope>provided</scope>
</dependency>
```

第三步:修改启动类

```java
@SpringBootApplication
public class Application extends SpringBootServletInitializer {

@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder builder) {
return application.sources(Application.class);
}

public static void main(String[] args) {
SpringApplication.run(ReadingListApplication.class, args);
}
}

到此就可以完成将项目发布到外部tomcat中所需的改造操作。

原理分析

javax.servlet.ServletContainerInitializer

该类是servlet3.0规范中新增的类,该类的说明文档如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 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
*/

其实就是servlet3.0规范定义了支持该规范的容器在启动加载项目时,需要通过ServiceLoader加载实现了接口ServletContainerInitializer的类,实例化并调用实例的void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException方法。

需要注意的是:该方法的第一个参数是一个Set, 它的值的类型是由实现了接口ServletContainerInitializer的类上的注解javax.servlet.annotation.HandlesTypes指定的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* This annotation is used to declare an array of application classes which are passed to a {@link javax.servlet.ServletContainerInitializer}.
*
* @since Servlet 3.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface HandlesTypes {
/**
* @return array of classes
*/
Class<?>[] value();
}

spring的支持

spring从3.1版本开始中提供了实现上述接口的类SpringServletContainerInitializer

1
2
3
4
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
//....
}

Servlet容器在启动时就会调用SpringServletContainerInitializeronStartup方法,并将实现了接口WebApplicationInitializer的所有类收集起来作为参数传递给该方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {

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

trueif (webAppInitializerClasses != null) {
truetruefor (Class<?> waiClass : webAppInitializerClasses) {
truetruetrue// Be defensive: Some servlet containers provide us with invalid classes,
truetruetrue// no matter what @HandlesTypes says...
truetruetrueif (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
truetruetruetruetrueWebApplicationInitializer.class.isAssignableFrom(waiClass)) {
truetruetruetruetry {
truetruetruetruetrueinitializers.add((WebApplicationInitializer) waiClass.newInstance());
truetruetruetrue}
truetruetruetruecatch (Throwable ex) {
truetruetruetruetruethrow new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
truetruetruetrue}
truetruetrue}
truetrue}
true}

trueif (initializers.isEmpty()) {
truetrueservletContext.log("No Spring WebApplicationInitializer types detected on classpath");
truetruereturn;
true}

trueservletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
trueAnnotationAwareOrderComparator.sort(initializers);
truefor (WebApplicationInitializer initializer : initializers) {
truetrueinitializer.onStartup(servletContext);
true}
}

在上面的方法中会调用每个实现了接口WebApplicationInitializeronStartup并将ServletContext对象作为参数传入到该方法中,每个实例根据自己的策略来对ServletContext对象进行操作,例如添加Filer,添加Servlet和Listener。

抽象类SpringBootServletInitializer实现了接口WebApplicationInitializer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
@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);
}
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
truetruereturn builder;
true}
trueprotected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
truetruereturn builder;
true}
}

上面的代码很清晰,就是为了创建Spring容器。在创建前通过Builder模式创建了SpringApplication对象,并通过SpringApplication对象的run方法来创建和启动整个spring应用。

在上面的代码中定义并调用了一个protected方法configure(),这其实就是spring提供的一个扩展点,让我们有机会来控制容器的启动。

我们应用中的类ReadingListApplication继承了类SpringBootServletInitializer并重写了方法configure

1
2
3
4
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(ReadingListApplication.class);
}

在该方法中我们调用了SpringApplicationBuilder类的sources方法。该方法的代码如下:

/**
* Add more sources (configuration classes and components) to this application.
* @param sources the sources to add
* @return the current builder
*/
public SpringApplicationBuilder sources(Class<?>... sources){ this.sources.addAll(new LinkedHashSet<Object>(Arrays.asList(sources)));
        return this;
}

功能就是提供Spring容器所需的配置类和组件类。

我们的类ReadingListApplication就是一个集配置和组件为一体的类。这是因为我们的类加了注解@SpringBootApplication。该注解的功能相当于下面的三个注解:

@Configuration
@EnableAutoConfiguration
@ComponentScan

后记

关于SpringApplication如何创建Spring容器,需要在深入代码进行了解。

文章目录
  1. 1. 前言
  2. 2. tomcat版本选择
  3. 3. 项目配置
  4. 4. 原理分析
    1. 4.1. javax.servlet.ServletContainerInitializer
    2. 4.2. spring的支持
  5. 5. 后记
|