Skip to content

Embedded Web Server

03 - 嵌入式Web容器

嵌入式Servlet Web容器

从之前项目启动的日志中,总能看到一行:

[           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''

说明项目使用的Web容器是Tomcat,而在pom.xml中没有直接引入相关依赖,而在WAR包的解压目录下,可以看到WEB-INF/lib/spring-boot-starter-tomcat-2.3.2.RELEASE.jar的存在,说明该JAR文件应该由spring-boot-starter-web间接引入,使用Maven的dependency插件可以看到依赖关系:

$ mvn dependency:tree -Dincludes=*:spring-boot-starter-tomcat:jar:2.3.2.RELEASE
[INFO] Scanning for projects...
[INFO] 
[INFO] ---------< deep-in-spring-boot:first-spring-boot-application >----------
[INFO] Building first-spring-boot-application 1.0.0-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ first-spring-boot-application ---
[INFO] deep-in-spring-boot:first-spring-boot-application:war:1.0.0-SNAPSHOT
[INFO] \- org.springframework.boot:spring-boot-starter-web:jar:2.3.2.RELEASE:compile
[INFO]    \- org.springframework.boot:spring-boot-starter-tomcat:jar:2.3.2.RELEASE:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.107 s
[INFO] Finished at: 2020-08-04T14:47:52+08:00
[INFO] ------------------------------------------------------------------------

Spring Boot官方网站,介绍Spring Boot的特性时,有如下内容

Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)

示例项目都是Servlet Web程序,Spring Boot准备了3种嵌入式Web容器,分别是TomcatJettyUndertow

在官方文档中,对3种Servlet容器的相关介绍:

Spring Boot supports the following embedded servlet containers:

Name Servlet Version
Tomcat 9.0 4.0
Jetty 9.4 3.1
Undertow 2.0 4.0

You can also deploy Spring Boot applications to any Servlet 3.1+ compatible container.

1. Tomcat作为嵌入式Servlet Web容器

嵌入式Tomcat作为Web应用的一部分,结合其API实现Servlet容器的引导。同样,Tomcat也提供了Maven插件,不需要编码,也不需要外置Tomcat容器,将当前应用直接打包为可运行的JAR或WAR文件,通过java -jar命令启动。

新建项目servlet-sample,结构如下:

$ tree .
.
├── pom.xml
└── src
    ├── main
    │   ├── java
    │   │   └── deep
    │   │       └── in
    │   │           └── spring
    │   │               └── boot
    │   │                   └── servlet
    │   │                       └── HelloServlet.java
    │   ├── resources
    │   └── webapp
    │       └── WEB-INF
    │           └── web.xml
    └── test
        └── java

13 directories, 3 files

传统的Java Web项目,需要在web根路径下有WEB-INF/web.xml文件存在,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0">

    <servlet>
        <!-- Servlet 声明 -->
        <servlet-name>HelloServlet</servlet-name>
        <servlet-class>deep.in.spring.boot.servlet.HelloServlet</servlet-class>
        <init-param>
            <param-name>init-param1</param-name>
            <param-value>param1</param-value>
        </init-param>
    </servlet>

    <!-- 声明 Servlet 映射 -->
    <servlet-mapping>
        <!-- 关联 Servlet-->
        <servlet-name>HelloServlet</servlet-name>
        <url-pattern>/hello</url-pattern>
    </servlet-mapping>
</web-app>

声明了一个Servlet,并配置一对初始化参数,同时声明该Servlet的映射地址,编写Servlet类:

public class HelloServlet extends HttpServlet {
    @Override
    public void init(ServletConfig servletConfig) {
        Collections.list(servletConfig.getInitParameterNames())
                .forEach(name -> {
                    System.out.println("Init param name : " + name
                            + " , value : " + servletConfig.getInitParameter(name));
                });
    }

    /**
     * 输出 HTTP 请求参数 "messsage" 的内容(支持任意 HTTP 方法)
     *
     * @param request  {@link HttpServletRequest}
     * @param response {@link HttpServletResponse}
     * @throws IOException
     * @throws ServletException
     */
    @Override
    public void service(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        // 获取请求参数 "message" 内容
        String message = request.getParameter("message");
        System.out.println("message : " + message);
        PrintWriter writer = response.getWriter();
        // 输出 "message" 参数内容
        writer.println(message);
        writer.flush();
    }
}

项目的pom.xml中引入相关依赖和插件配置:

<?xml version="1.0" encoding="UTF-8"?>
<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>deep-in-spring-boot</groupId>
    <artifactId>servlet-sample</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>war</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>
    <dependencies>
        <!-- 使用 Servlet 3.1 API -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <!-- Tomcat 8 Maven 插件用于构建可执行 war -->
            <!-- https://mvnrepository.com/artifact/org.apache.tomcat.maven/tomcat8-maven-plugin -->
            <plugin>
                <groupId>org.apache.tomcat.maven</groupId>
                <artifactId>tomcat8-maven-plugin</artifactId>
                <version>3.0-r1655215</version>
                <executions>
                    <execution>
                        <id>tomcat-run</id>
                        <goals>
                            <!-- 最终打包成可执行的jar包 -->
                            <goal>exec-war-only</goal>
                        </goals>
                        <phase>package</phase>
                        <configuration>
                            <!-- ServletContext 路径 -->
                            <path>/</path>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>8</source>
                    <target>8</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <pluginRepositories>
        <pluginRepository>
            <!-- tomcat8-maven-plugin 所在仓库 -->
            <id>Alfresco</id>
            <name>Alfresco Repository</name>
            <url>https://artifacts.alfresco.com/nexus/content/repositories/public/</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>
</project>

准备好所需内容后直接执行打包:

$ mvn clean package
[INFO] Scanning for projects...
[INFO] 
[INFO] -----------------< deep-in-spring-boot:servlet-sample >-----------------
[INFO] Building servlet-sample 1.0.0-SNAPSHOT
[INFO] --------------------------------[ war ]---------------------------------
[INFO] 
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ servlet-sample ---
[INFO] Deleting /Users/nanlei/Dev/Codebase/deep-in-spring-boot/servlet-sample/target
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ servlet-sample ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ servlet-sample ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to /Users/nanlei/Dev/Codebase/deep-in-spring-boot/servlet-sample/target/classes
[INFO] 
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ servlet-sample ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory /Users/nanlei/Dev/Codebase/deep-in-spring-boot/servlet-sample/src/test/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ servlet-sample ---
[INFO] Changes detected - recompiling the module!
[INFO] 
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ servlet-sample ---
[INFO] 
[INFO] --- maven-war-plugin:2.2:war (default-war) @ servlet-sample ---
[INFO] Packaging webapp
[INFO] Assembling webapp [servlet-sample] in [/Users/nanlei/Dev/Codebase/deep-in-spring-boot/servlet-sample/target/servlet-sample-1.0.0-SNAPSHOT]
[INFO] Processing war project
[INFO] Copying webapp resources [/Users/nanlei/Dev/Codebase/deep-in-spring-boot/servlet-sample/src/main/webapp]
[INFO] Webapp assembled in [26 msecs]
[INFO] Building war: /Users/nanlei/Dev/Codebase/deep-in-spring-boot/servlet-sample/target/servlet-sample-1.0.0-SNAPSHOT.war
[INFO] WEB-INF/web.xml already added, skipping
[INFO] 
[INFO] --- tomcat8-maven-plugin:3.0-r1655215:exec-war-only (tomcat-run) @ servlet-sample ---
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.817 s
[INFO] Finished at: 2020-08-06T14:47:16+08:00
[INFO] ------------------------------------------------------------------------

打包完成,使用java -jar命令运行:

$ cd target
$ java -jar servlet-sample-1.0.0-SNAPSHOT-war-exec.jar
Aug 06, 2020 2:50:17 PM org.apache.coyote.http11.Http11NioProtocol init
INFO: Initializing ProtocolHandler ["http-nio-8080"]
Aug 06, 2020 2:50:17 PM org.apache.tomcat.util.net.NioSelectorPool getSharedSelector
INFO: Using a shared selector for servlet write/read
Aug 06, 2020 2:50:17 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service Tomcat
Aug 06, 2020 2:50:17 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet Engine: Apache Tomcat/8.0.14
Aug 06, 2020 2:50:18 PM org.apache.jasper.servlet.TldScanner scanJars
INFO: At least one JAR was scanned for TLDs yet contained no TLDs. Enable debug logging for this logger for a complete list of JARs that were scanned but no TLDs were found in them. Skipping unneeded JARs during scanning can improve startup time and JSP compilation time.
Aug 06, 2020 2:50:18 PM org.apache.coyote.http11.Http11NioProtocol start
INFO: Starting ProtocolHandler ["http-nio-8080"]

测试HTTP服务:

$ curl http://127.0.0.1:8080/hello?message=helloworld
helloworld

同时可以看到命令行输出:

Init param name : init-param1 , value : param1
message : helloworld

此时在target目录下出现解压的.extract目录,观察结构:

$ tree .extract/
.extract/
├── conf
│   └── web.xml
├── logs
│   └── access_log.2020-08-06
├── temp
├── webapps
│   ├── ROOT
│   │   ├── META-INF
│   │   │   ├── MANIFEST.MF
│   │   │   └── maven
│   │   │       └── deep-in-spring-boot
│   │   │           └── servlet-sample
│   │   │               ├── pom.properties
│   │   │               └── pom.xml
│   │   └── WEB-INF
│   │       ├── classes
│   │       │   └── deep
│   │       │       └── in
│   │       │           └── spring
│   │       │               └── boot
│   │       │                   └── servlet
│   │       │                       └── HelloServlet.class
│   │       └── web.xml
│   └── ROOT.war
└── work
    └── Tomcat
        └── localhost
            └── ROOT

20 directories, 8 files

由此可见,Tomcat Maven插件并非嵌入式Tomcat,仍为传统Tomcat容器部署方式,将应用打包为ROOT.war,然后在Tomcat启动过程中将ROOT.war部署到webapps目录,但该插件支持指定ServletContext路径。

Spring Boot使用嵌入式Tomcat构建为TomcatWebServer Bean,由Spring上下文将其引导,嵌入式组件的运行,ClassLoader的装载均由Spring Boot框架完成。

Tomcat Maven插件打包后的JAR或WAR属于非FAT模式,归档文件会被压缩,而Spring Boot Maven插件spring-boot-maven-plugin使用零压缩模式,将应用归档到JAR或WAR包中,在jar命令帮助中有介绍:

$ jar
Usage: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
Options:
    -c  create new archive
    -t  list table of contents for archive
    -x  extract named (or all) files from archive
    -u  update existing archive
    -v  generate verbose output on standard output
    -f  specify archive file name
    -m  include manifest information from specified manifest file
    -n  perform Pack200 normalization after creating a new archive
    -e  specify application entry point for stand-alone application 
        bundled into an executable jar file
    -0  store only; use no ZIP compression
    -P  preserve leading '/' (absolute path) and ".." (parent directory) components from file names
    -M  do not create a manifest file for the entries
    -i  generate index information for the specified jar files
    -C  change to the specified directory and include the following file
If any file is a directory then it is processed recursively.
The manifest file name, the archive file name and the entry point name are
specified in the same order as the 'm', 'f' and 'e' flags.

Example 1: to archive two class files into an archive called classes.jar: 
       jar cvf classes.jar Foo.class Bar.class 
Example 2: use an existing manifest file 'mymanifest' and archive all the
           files in the foo/ directory into 'classes.jar': 
       jar cvfm classes.jar mymanifest -C foo/ .

传统Servlet容器将压缩的WAR文件解压到对应目录,再加载该目录中的资源。而Spring Boot的可执行WAR文件需要在不解压的前提下读取其中资源,也就是spring-boot-loader需要覆盖内建JAR协议的URLStreamHandler的原因所在。

2. Jetty作为嵌入式Servlet Web容器

将默认的嵌入式容器Tomact切换至Jetty的步骤非常简单,官方文档对此有详细说明:

Use Another Web Server
Many Spring Boot starters include default embedded containers.

For servlet stack applications, the spring-boot-starter-web includes Tomcat by including spring-boot-starter-tomcat, but you can use spring-boot-starter-jetty or spring-boot-starter-undertow instead.

For reactive stack applications, the spring-boot-starter-webflux includes Reactor Netty by including spring-boot-starter-reactor-netty, but you can use spring-boot-starter-tomcat, spring-boot-starter-jetty, or spring-boot-starter-undertow instead.

When switching to a different HTTP server, you need to exclude the default dependencies in addition to including the one you need. To help with this process, Spring Boot provides a separate starter for each of the supported HTTP servers.

The following Maven example shows how to exclude Tomcat and include Jetty for Spring MVC:

<properties>
    <servlet-api.version>3.1.0</servlet-api.version>
</properties>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <!-- Exclude the Tomcat dependency -->
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

The version of the Servlet API has been overridden as, unlike Tomcat 9 and Undertow 2.0, Jetty 9.4 does not support Servlet 4.0.

根据官方文档的说明,新建项目embedded-web-server。前面分析过spring-boot-starter-tomcatspring-boot-starter-web间接引入,所以在依赖中需要排除,再添加新的Jetty依赖spring-boot-starter-jetty即可。

运行项目:

$ mvn spring-boot:run
[INFO] Scanning for projects...
(省略部分内容...)
[INFO] <<< spring-boot-maven-plugin:2.3.2.RELEASE:run (default-cli) < test-compile @ first-spring-boot-application <<<
[INFO] 
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.3.2.RELEASE:run (default-cli) @ first-spring-boot-application ---
[INFO] Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.2.RELEASE)

2020-08-06 17:50:22.470  INFO 8185 --- [           main] deep.in.spring.boot.App                  : Starting App on nanleis-MacBook-Pro.local with PID 8185 (/Users/nanlei/Dev/Codebase/deep-in-spring-boot/first-spring-boot-application/target/classes started by nanlei in /Users/nanlei/Dev/Codebase/deep-in-spring-boot/first-spring-boot-application)
2020-08-06 17:50:22.472  INFO 8185 --- [           main] deep.in.spring.boot.App                  : No active profile set, falling back to default profiles: default
2020-08-06 17:50:22.916  INFO 8185 --- [           main] org.eclipse.jetty.util.log               : Logging initialized @947ms to org.eclipse.jetty.util.log.Slf4jLog
2020-08-06 17:50:23.023  INFO 8185 --- [           main] o.s.b.w.e.j.JettyServletWebServerFactory : Server initialized with port: 8080
2020-08-06 17:50:23.024  INFO 8185 --- [           main] org.eclipse.jetty.server.Server          : jetty-9.4.30.v20200611; built: 2020-06-11T12:34:51.929Z; git: 271836e4c1f4612f12b7bb13ef5a92a927634b0d; jvm 1.8.0_251-b08
2020-08-06 17:50:23.043  INFO 8185 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring embedded WebApplicationContext
2020-08-06 17:50:23.043  INFO 8185 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 531 ms
2020-08-06 17:50:23.111  INFO 8185 --- [           main] org.eclipse.jetty.server.session         : DefaultSessionIdManager workerName=node0
2020-08-06 17:50:23.111  INFO 8185 --- [           main] org.eclipse.jetty.server.session         : No SessionScavenger set, using defaults
2020-08-06 17:50:23.112  INFO 8185 --- [           main] org.eclipse.jetty.server.session         : node0 Scavenging every 660000ms
2020-08-06 17:50:23.118  INFO 8185 --- [           main] o.e.jetty.server.handler.ContextHandler  : Started o.s.b.w.e.j.JettyEmbeddedWebAppContext@6c67e137{application,/,[file:///private/var/folders/dy/g2n42jw12y3g6th2pgtz59_c0000gn/T/jetty-docbase.8002049805771173289.8080/],AVAILABLE}
2020-08-06 17:50:23.118  INFO 8185 --- [           main] org.eclipse.jetty.server.Server          : Started @1150ms
2020-08-06 17:50:23.214  INFO 8185 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-08-06 17:50:23.303  INFO 8185 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-08-06 17:50:23.303  INFO 8185 --- [           main] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2020-08-06 17:50:23.307  INFO 8185 --- [           main] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
2020-08-06 17:50:23.334  INFO 8185 --- [           main] o.e.jetty.server.AbstractConnector       : Started ServerConnector@b6b1987{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
2020-08-06 17:50:23.336  INFO 8185 --- [           main] o.s.b.web.embedded.jetty.JettyWebServer  : Jetty started on port(s) 8080 (http/1.1) with context path '/'
2020-08-06 17:50:23.343  INFO 8185 --- [           main] deep.in.spring.boot.App                  : Started App in 1.098 seconds (JVM running for 1.374)

项目正常启动,不同的是,运行容器切换到了Jetty,其中org.springframework.boot.web.embedded.jetty.JettyWebServer就是Spring Boot结合Jetty API实现的org.springframework.boot.web.server.WebServer Bean。

3. Undertow作为嵌入式Servlet Web容器

若将Servlet容器切换到Undertow,移除spring-boot-starter-tomcat依赖后再添加spring-boot-starter-undertow依赖即可,再次运行项目:

$ mvn spring-boot:run
[INFO] Scanning for projects...
(省略部分内容...)
[INFO] <<< spring-boot-maven-plugin:2.3.2.RELEASE:run (default-cli) < test-compile @ first-spring-boot-application <<<
[INFO] 
[INFO] 
[INFO] --- spring-boot-maven-plugin:2.3.2.RELEASE:run (default-cli) @ first-spring-boot-application ---
[INFO] Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.2.RELEASE)

2020-08-06 18:06:05.804  INFO 8332 --- [           main] deep.in.spring.boot.App                  : Starting App on nanleis-MacBook-Pro.local with PID 8332 (/Users/nanlei/Dev/Codebase/deep-in-spring-boot/first-spring-boot-application/target/classes started by nanlei in /Users/nanlei/Dev/Codebase/deep-in-spring-boot/first-spring-boot-application)
2020-08-06 18:06:05.806  INFO 8332 --- [           main] deep.in.spring.boot.App                  : No active profile set, falling back to default profiles: default
2020-08-06 18:06:06.317  WARN 8332 --- [           main] io.undertow.websockets.jsr               : UT026010: Buffer pool was not set on WebSocketDeploymentInfo, the default pool will be used
2020-08-06 18:06:06.334  INFO 8332 --- [           main] io.undertow.servlet                      : Initializing Spring embedded WebApplicationContext
2020-08-06 18:06:06.334  INFO 8332 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 488 ms
2020-08-06 18:06:06.449  INFO 8332 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2020-08-06 18:06:06.531  INFO 8332 --- [           main] io.undertow                              : starting server: Undertow - 2.1.3.Final
2020-08-06 18:06:06.534  INFO 8332 --- [           main] org.xnio                                 : XNIO version 3.8.0.Final
2020-08-06 18:06:06.542  INFO 8332 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.0.Final
2020-08-06 18:06:06.601  INFO 8332 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final
2020-08-06 18:06:06.649  INFO 8332 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
2020-08-06 18:06:06.656  INFO 8332 --- [           main] deep.in.spring.boot.App                  : Started App in 1.068 seconds (JVM running for 1.323)

从日志中可以看到,Undertow Web容器已经成功启动,但这里的输出日志是UndertowWebServer,而实际上此处Undertow的实现是org.springframework.boot.web.embedded.undertow.UndertowServletWebServer,它继承自UndertowWebServer,由于当前版本的子类UndertowServletWebServer中没有定义日志输出,而是在父类UndertowWebServer进行中输出的,所以在日志上没有明确说明。

这里也可以进行断点跟踪来确定,在ServletWebServerApplicationContext中有createWebServer()方法,在Spring Boot项目启动过程中会执行到此,进行断点跟踪,就能明确实际创建的WebServer的具体实例是什么。

UndertowServletWebServer

嵌入式Reactive Web容器

嵌入式Reactive Web容器通常处于被动激活状态,需要增加spring-boot-starter-webflux依赖,而它和spring-boot-starter-web同时存在时,spring-boot-starter-webflux会被忽略,这是SpringApplication中对Web应用类型的推断决定的:

    /**
     * Create a new {@link SpringApplication} instance. The application context will load
     * beans from the specified primary sources (see {@link SpringApplication class-level}
     * documentation for details. The instance can be customized before calling
     * {@link #run(String...)}.
     * @param resourceLoader the resource loader to use
     * @param primarySources the primary bean sources
     * @see #run(Class, String[])
     * @see #setSources(Set)
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }

创建SpringApplication实例时,this.webApplicationType = WebApplicationType.deduceFromClasspath();就是对Web应用的类型进行推断,其具体逻辑在枚举类WebApplicationType中:

    private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

    private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

    private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

    private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

    static WebApplicationType deduceFromClasspath() {
        if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
                && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
            return WebApplicationType.REACTIVE;
        }
        for (String className : SERVLET_INDICATOR_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return WebApplicationType.NONE;
            }
        }
        return WebApplicationType.SERVLET;
    }

可以很容易看出,只有当spring-boot-starter-webflux单独存在是,WebApplicationType才是REACTIVE类型。

4. Undertow作为嵌入式Reactive Web容器

修改pom.xml,添加相关依赖:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-webflux</artifactId>
            <version>2.3.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-undertow</artifactId>
        </dependency>
    </dependencies>

编写一些WebFlux代码,并将maven-compiler-plugin的编译级别调整到1.8:

@RestController
@SpringBootApplication
public class App {

    @RequestMapping("/")
    public String index() {
        return "Welcome to SpringBoot";
    }

    @Bean
    public RouterFunction<ServerResponse> helloworld() {
        return route(GET("/helloworld"),
                request -> ok().body(Mono.just("Hello World"), String.class)
        );
    }

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

启动项目:

$ mvn spring-boot:run
[INFO] Scanning for projects...
(省略部分内容...)
[INFO] --- spring-boot-maven-plugin:2.3.2.RELEASE:run (default-cli) @ first-spring-boot-application ---
[INFO] Attaching agents: []

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.3.2.RELEASE)

2020-08-07 14:03:53.126  INFO 3738 --- [           main] deep.in.spring.boot.App                  : Starting App on nanleis-MacBook-Pro.local with PID 3738 (/Users/nanlei/Dev/Codebase/deep-in-spring-boot/first-spring-boot-application/target/classes started by nanlei in /Users/nanlei/Dev/Codebase/deep-in-spring-boot/first-spring-boot-application)
2020-08-07 14:03:53.130  INFO 3738 --- [           main] deep.in.spring.boot.App                  : No active profile set, falling back to default profiles: default
2020-08-07 14:03:53.717  INFO 3738 --- [           main] io.undertow                              : starting server: Undertow - 2.1.3.Final
2020-08-07 14:03:53.721  INFO 3738 --- [           main] org.xnio                                 : XNIO version 3.8.0.Final
2020-08-07 14:03:53.727  INFO 3738 --- [           main] org.xnio.nio                             : XNIO NIO Implementation Version 3.8.0.Final
2020-08-07 14:03:53.793  INFO 3738 --- [           main] org.jboss.threads                        : JBoss Threads version 3.1.0.Final
2020-08-07 14:03:53.853  INFO 3738 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
2020-08-07 14:03:53.862  INFO 3738 --- [           main] deep.in.spring.boot.App                  : Started App in 0.947 seconds (JVM running for 1.206)

此时仍然启动的是UndertowWebServer实例,但实际情况还是通过断点跟踪来一探究竟:

UndertowReactiveWebServer

要注意的是,运行在REACTIVE类型下,Spring的上下文就是ReactiveWebServerApplicationContext了,其中的createWebServer()方法和ServletWebServerApplicationContext的略有不同:

    private void createWebServer() {
        WebServerManager serverManager = this.serverManager;
        if (serverManager == null) {
            String webServerFactoryBeanName = getWebServerFactoryBeanName();
            ReactiveWebServerFactory webServerFactory = getWebServerFactory(webServerFactoryBeanName);
            boolean lazyInit = getBeanFactory().getBeanDefinition(webServerFactoryBeanName).isLazyInit();
            this.serverManager = new WebServerManager(this, webServerFactory, this::getHttpHandler, lazyInit);
            getBeanFactory().registerSingleton("webServerGracefulShutdown",
                new WebServerGracefulShutdownLifecycle(this.serverManager));
            getBeanFactory().registerSingleton("webServerStartStop",
                new WebServerStartStopLifecycle(this.serverManager));
        }
        initPropertySources();
    }

二者都是WebServerApplicationContext接口的子类,其中的getWebServer()方法就可以获取到运行的WebServer实例,在App.java中添加相关代码:

    @Bean
    public ApplicationRunner runner(WebServerApplicationContext context) {
        return args -> {
            System.out.println("WebServer type: " + context.getWebServer().getClass().getName());
        };
    }

利用了ApplicationRunner接口的特性,在SpringApplication启动后回调。再次启动项目可以看到控制台输出:

2020-08-07 14:19:58.687  INFO 3867 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
2020-08-07 14:19:58.722  INFO 3867 --- [           main] deep.in.spring.boot.App                  : Started App in 2.19 seconds (JVM running for 2.659)
WebServer type: org.springframework.boot.web.embedded.undertow.UndertowWebServer

继续测试HTTP服务:

$ curl http://localhost:8080
Welcome to Spring
$ curl http://localhost:8080/helloworld
Hello World

但在上面使用ApplicationRunner注入WebServerApplicationContext时,代码存在一个潜在的问题:没有考虑非Web应用的情况。可以利用Spring的事件机制来进行完善。

Spring Boot官方文档对相关事件的介绍:

the following events are also published after ApplicationPreparedEvent and before ApplicationStartedEvent:
A WebServerInitializedEvent is sent after the WebServer is ready. ServletWebServerInitializedEvent and ReactiveWebServerInitializedEvent are the servlet and reactive variants respectively.

Servlet和Reactive有各自的初始化事件,使用它们的父类WebServerInitializedEvent则覆盖的范围更广,修改App.java代码:

//    @Bean
//    public ApplicationRunner runner(WebServerApplicationContext context) {
//        return args -> {
//            System.out.println("WebServer type: " + context.getWebServer().getClass().getName());
//        };
//    }

    @EventListener(WebServerInitializedEvent.class)
    public void onWebServerReady(WebServerInitializedEvent event) {
        System.out.println("WebServer Type: " + event.getWebServer().getClass().getName());
    }

启动项目,依然可以得到结果,但程序健壮性更佳,即便不在Web应用中运行,也不会注入WebServerApplicationContext失败:

2020-08-07 15:24:05.760  INFO 4413 --- [           main] o.s.b.w.e.undertow.UndertowWebServer     : Undertow started on port(s) 8080 (http)
WebServer Type: org.springframework.boot.web.embedded.undertow.UndertowWebServer
2020-08-07 15:24:05.772  INFO 4413 --- [           main] deep.in.spring.boot.App                  : Started App in 1.87 seconds (JVM running for 2.319)

注意输出的顺序,理解事件机制的时序。

5. Jetty作为嵌入式Reactive Web容器

和上面类似,若要使用Jetty作为Web容器,只需修改pom.xml中的依赖即可:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jetty</artifactId>
        </dependency>

启动项目得到类似的输出:

2020-08-07 15:32:04.568  INFO 4478 --- [           main] o.s.b.web.embedded.jetty.JettyWebServer  : Jetty started on port(s) 8080 (http/1.1) with context path '/'
WebServer Type: org.springframework.boot.web.embedded.jetty.JettyWebServer
2020-08-07 15:32:04.579  INFO 4478 --- [           main] deep.in.spring.boot.App                  : Started App in 1.448 seconds (JVM running for 1.877)

6. Tomcat作为嵌入式Reactive Web容器

要注意的是,Tomcat是Servlet Web的默认容器,但不是Reactive Web的默认容器。

同样要使用Tomcat,修改pom.xml:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>

运行结果为:

2020-08-07 15:35:03.966  INFO 4497 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
WebServer Type: org.springframework.boot.web.embedded.tomcat.TomcatWebServer
2020-08-07 15:35:03.977  INFO 4497 --- [           main] deep.in.spring.boot.App                  : Started App in 1.677 seconds (JVM running for 2.116)

7. 默认的嵌入式Reactive Web容器

Netty作为默认的Reactive Web容器,若要使用,去掉容器依赖即可:

2020-08-07 15:36:58.625  INFO 4510 --- [           main] o.s.b.web.embedded.netty.NettyWebServer  : Netty started on port(s): 8080
WebServer Type: org.springframework.boot.web.embedded.netty.NettyWebServer
2020-08-07 15:36:58.638  INFO 4510 --- [           main] deep.in.spring.boot.App                  : Started App in 1.393 seconds (JVM running for 1.811)