# 部署相关问题

返回:springBoot

# 启动问题

  • DynamicTp logging env init failed,if collectType is not logging,this error can be ignored
    • 轻量级线程池管理开源项目

# Spring Boot随机端口你都不会,怎么动态扩容

back

server.port=${random.int(2000,8000)}
1

上面的方法虽然暂时达到了想要的效果,但是有个问题:如果生成的这个随机端口已经被使用了,那么项目启动就会出现端口冲突

# 通过System.setProperty设置有效随机端口

# server.port=0随机端口 (推荐)

通过设置server.port=0,在spring boot项目启动时,会自动去寻找一个空闲的端口,避免端口冲突。

# 分离第三方依赖独立打包pom配置

back | 类似

<build>
    <!-- 打包输出的根目录 -->
    <!-- <directory>target/${project.version}</directory> -->
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <!-- 剔除spring-boot打包的org和BOOT-INF文件夹(用于子模块打包) -->
<!--                    <skip>true</skip>-->
                <!-- 指定该jar包启动时的主类[建议] -->
                <mainClass>com.chlm.mysession.MysessionApplication</mainClass>
                <layout>ZIP</layout>
                <includes>
                    <include>
                        <!--                             排除第三方依赖jar(只保留本项目的jar) -->
                        <groupId>${project.groupId}</groupId>
                        <artifactId>${project.artifactId}</artifactId>
                        <!--                             排除所有jar -->
                        <!--                            <groupId>nothing</groupId>-->
                        <!--                            <artifactId>nothing</artifactId>-->
                    </include>
                </includes>
            </configuration>
            <!--                <executions>-->
            <!--                    <execution>-->
            <!--                        <goals>-->
            <!--                            <goal>repackage</goal>-->
            <!--                        </goals>-->
            <!--                    </execution>-->
            <!--                </executions>-->
        </plugin>
        <!-- 打源码包 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <configuration>
                <attach>true</attach>
            </configuration>
            <executions>
                <execution>
                    <id>attach-sources</id>
<!--                        意思是在mvn生命周期为compile时将源文件打包,即只要执行的mvn命令包含compile阶段,就会将源代码打包。-->
<!--                        phase还可以指定为verify、package、install等-->
                    <phase>compile</phase>
                    <goals>
                        <goal>jar-no-fork</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- 把项目依赖的第三方包打包在target/lib下 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-dependency-plugin</artifactId>
            <executions>
                <execution>
                    <id>copy-dependencies</id>
                    <phase>package</phase>
                    <goals>
                        <goal>copy-dependencies</goal>
                    </goals>
                    <configuration>
                        <outputDirectory>${project.build.directory}/lib</outputDirectory>
                        <excludeTransitive>false</excludeTransitive>
                        <stripVersion>false</stripVersion>
                        <includeScope>runtime</includeScope>
                    </configuration>
                </execution>
            </executions>
        </plugin>
        <!-- 配置maven install 跳过test,相当于命令:$mvn install -Dmaven.test.skip = true-->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <skip>true</skip>
            </configuration>
        </plugin>
    </plugins>
    <resources>
        <!-- 打包src/main/java下的xml文件 -->
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
        </resource>
        <!-- 排除resources下的配置文件 -->
        <resource>
            <filtering>true</filtering>
            <directory>src/main/resources</directory>
            <excludes>
                <exclude>static/**</exclude>
                <exclude>templates/**</exclude>
                <exclude>*.yml</exclude>
                <exclude>*.properties</exclude>
                <exclude>*.xml</exclude>
                <exclude>*.txt</exclude>
            </excludes>
            <targetPath>BOOT-INF/classes/</targetPath>
        </resource>
        <!-- 打包lib下的jar包 -->
        <resource>
            <directory>lib</directory>
            <targetPath>BOOT-INF/lib/</targetPath>
            <includes>
                <include>**/*.jar</include>
            </includes>
        </resource>
    </resources>

</build>
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113

# 我把SpringBoot项目从18.18M瘦身到0.18M,部署起来真省事

back

  • 1、配置pom文件
    进入项目根目录,执行命令:mvn clean install
    将编译后的Jar包解压,拷贝 BOOT-INF 目录下的lib文件夹 到目标路径;
<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <configuration>
        <mainClass>com.chlm.mysession.MysessionApplication</mainClass>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>repackage</goal>
            </goals>
        </execution>
    </executions>
</plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 2、修改pom.xml配置,编译出不带 lib 文件夹的Jar包
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <mainClass>com.chlm.mysession.MysessionApplication</mainClass>
                <layout>ZIP</layout>
                <includes>
                    <include>
                        <groupId>nothing</groupId>
                        <artifactId>nothing</artifactId>
                    </include>
                </includes>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>repackage</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- 配置maven install 跳过test,相当于命令:$mvn install -Dmaven.test.skip = true-->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-surefire-plugin</artifactId>
            <configuration>
                <skip>true</skip>
            </configuration>
        </plugin>
    </plugins>
</build>
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
  • 3、运行编译后的Jar包
    • 将 步骤1 解压出来的lib文件夹、步骤2编译的jar包放在同一个目录, 运行下面命令:
java -Dloader.path=lib -jar myJar.jar
1

根据实际情况写lib目录

# 让工程支持热部署

back

添加jar包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <optional>true</optional> <!-- 这个需要为 true 热部署才有效 -->
</dependency>
1
2
3
4
5

# 部署tomcat问题汇总

back

# java连接mysql报错

back

java.sql.SQLException: Unknown system variable 'query_cache_size'
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:545)
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:513)
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:115)
at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1983)
at com.mysql.cj.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:1936)
at com.mysql.cj.jdbc.StatementImpl.executeQuery(StatementImpl.java:1422)
at com.mysql.cj.jdbc.ConnectionImpl.loadServerVariables(ConnectionImpl.java:2831)
at com.mysql.cj.jdbc.ConnectionImpl.initializePropsFromServer(ConnectionImpl.java:2381)
at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:1739)
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:1596)
at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:633)
at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:347)
at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:219)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:270)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

原因是mysql-connector-java的版本还是6.0.6,需要升级版本到8.0.11 ,这个报错就不存在了

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
1
2
3
4
5

此方法同样适用于gradle

# invalid_LOC_header(bad_signature)

back

找到错误的jar包,确保正确下载

# 调整打包方式为war

back

  • 修改打包方式
<packaging>jar</packaging>
1

如下:

<packaging>war</packaging>
1
  • 移除内置tomcat
<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>
1
2
3
4
5
6
7
8
9
10

或者:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>
1
2
3
4
5
  • 其他处置
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>

<!-- servlet 依赖 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12

@Mapper

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.3.0</version>
</dependency>
1
2
3
4
5

直接在启动类中:

<!-- @MapperScan(value= {"com.ffCamera.mapper"}) -->
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>1.2.2</version>
</dependency>
1
2
3
4
5
6
  • 罪魁祸首

导致springboot工程放在eclipse的tomcat下运行不起来,需要注释掉。 运行中会报错如下文件所示:

<dependency>
    <groupId>org.apache.ibatis</groupId>
    <artifactId>ibatis-core</artifactId>
    <version>3.0</version>
</dependency>
1
2
3
4
5

这个错误关键在最后一个cause by异常信息这个错误在网上查了很多资料,有的说和问题1解决方案一样。其实不是的。正确解决方案: 错误发生由于porm.xml中多了

<dependency>
    <groupId>org.apache.ibatis</groupId>
    <artifactId>ibatis-core</artifactId>
    <version>3.0</version>
</dependency>
1
2
3
4
5

在output中多了这个jar包导致使用了iBatissqlSessionFactory这个bean,而这个bean没有setVfsImpl方法。
删除该段xml后使用正确的mybatis-spring-boot-starter中的sqlSessionFactoryBean这个bean,成功调用方法。 修改后记得调整打包的Artifacts。

# JSP支持

back

  • 添加jar包
<!-- https://mvnrepository.com/artifact/javax.servlet.jsp.jstl/jstl -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jstl</artifactId>
</dependency>
<!-- servlet 依赖 -->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-jasper</artifactId>
    <scope>provided</scope>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 配置文件修改

先在src/main/下新建文件夹webapp/WEB-INF/jsp/ 然后修改application.properties

spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp
1
2

# SpringBoot的几种部署方式

back

在Java Archive(JAR)中作为独立应用程序进行部署

# WAR

back

可以将Spring Boot应用程序打包到WAR文件中,以部署到现有的servlet容器(例如Tomcat,Jetty等)中。这可以按如下方式完成:

通过pom.xml文件指定WAR包<packaging>war</packaging>。这会将应用程序打包成WAR文件(而不是JAR)。
在第二步,将Tomcat(servlet容器)依赖关系的范围设置为provided(以便它不会部署到WAR文件中):

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-tomcat</artifactId
 <scope>provided</scope>
</dependency>
1
2
3
4
5

通过扩展SpringBootServletInitializer覆盖configure方法来初始化Tomcat所需的Servlet上下文,如下所示:

@SpringBootApplication
public class DemoApp extends SpringBootServletInitializer {
 @Override
 protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
 return builder.sources(DemoApp.class);
 }
 public static void main(String[] args) {
 SpringApplication.run(DemoApp.class, args);
 }
}
1
2
3
4
5
6
7
8
9
10

要将应用程序打包到war文件中,请mvn clean package在项目目录下运行标准maven命令。这将生成可以部署到servlet容器中的WAR包。

# DockerContainer

back

在将应用程序部署到Docker容器之前,我们首先将应用程序打包在(胖)JAR文件中。
在第一步,我们需要构建一个容器镜像。为此,我们首先在项目根目录中创建一个Dockerfile,如下所示:

# latest oracle openjdk is the basis
FROM openjdk:oracle
# copy jar file into container image under app directory
COPY target/demoApp.jar app/demoApp.jar
# expose server port accept connections
EXPOSE 8080
# start application
CMD ["java", "-jar", "app/demoApp.jar"]
1
2
3
4
5
6
7
8

请注意,在上面的代码片段中,我们假设应用程序JAR文件“ demoApp.jar”位于项目的目标目录下。我们还假设嵌入式servlet端口是8080(这是Tomcat的默认情况)。

我们现在可以使用以下命令构建Docker镜像(Dockerfile所在的位置):

docker image build -t demo-app:latest .
1

-t是要构建的镜像的名称和标记。构建镜像后,我们可以通过以下方式创建和运行容器:

docker container run -p 8080:8080 -d --name app-container demo-app
1

-p是发布(映射)主机端口到容器端口(在这种情况下,两个都是8080)。选项-d(detach)指定在后台运行容器,并用--name指定容器的名称

# NGINX

back

在直接设置中,我们直接在localhost上运行Nginx Web服务器和Spring Boot应用程序(当然在不同的端口上)。我们让Ngnix代理REST请求到Spring Boot应用程序:

在Linux上安装Nginx Web服务器sudo apt-get install nginx,
/etc/ngnix/sites-available/default使用文本编辑器打开文件,
比如说,我们有两个Spring Boot应用程序需要代理。然后用两个Spring Boot应用程序的以下块替换文件中的“location”块。

location /app1 {
 proxy_pass http://localhost:8080;
}
location /app2 {
 proxy_pass http://localhost:9000;
}
1
2
3
4
5
6

在此基础上对将来的请求http://localhost/app1/将被定向到/http://localhost:8080/,和将来的请求http://localhost/app2/将被引导到/http://localhost:9000/

# 负载均衡

back

如果您正在运行Spring Boot应用程序的多个实例,则可以启用Nginx以应用负载平衡。例如,如果我们在端口8080,8081和8082上运行3个app1实例。我们可以在这些服务器之间进行负载平衡,如下所示:

打开文件/etc/ngnix/sites-available/default并在文件顶部添加以下块(在服务器块之前):

#configure load-balancing
upstream backend {
 server localhost:8080;
 server localhost:8081;
 server localhost:8082;
}
1
2
3
4
5
6

修改app1 的proxy_pass参数,如下所示:

location / app1 {
 proxy_pass http:// backend;
}
1
2
3

基于此请求http://localhost/app1/将被发送到/http://localhost:8080/,/http://localhost:8081/或/http://localhost:8082/

# tomcat配置

back

# 服务器地址和端口

server.port = 80
server.address = my_custom_ip
1
2

# 错误处理

Spring Boot提供标准错误网页。此页面称为Whitelabel

#禁用
server.error.whitelabel.enabled = false
1
2

Whitelabel的默认路径是*/error*。可以通过设置server.error.path参数来自定义它:

server.error.path = /user-error

1
2

还可以设置属性,以确定显示有关错误的信息。例如,我们可以包含错误消息和堆栈跟踪:

server.error.include-exception= true
server.error.include-stacktrace= always
1
2

# 服务器连接

在Spring Boot中,我们可以定义Tomcat工作线程的最大数量:
server.tomcat.max-threads= 200

配置Web服务器时,设置服务器连接超时也可能很有用。这表示服务器在连接关闭之前等待客户端发出请求的最长时间:
server.connection-timeout= 5s

我们还可以定义请求头的最大大小:
server.max-http-header-size= 8KB

请求正文的最大大小:
server.tomcat.max-swallow-size= 2MB

或者整个POST请求的最大大小:
server.tomcat.max-http-post-size= 2MB

# SSL

back

要在我们的Spring Boot应用程序中启用SSL支持,我们需要将server.ssl.enabled属性设置为true,并定义SSL协议:
server.ssl.enabled = true
server.ssl.protocol = TLS

我们要配置保存证书密钥库的密码,类型和路径:

server.ssl.key-store-password=my_password  
server.ssl.key-store-type=keystore_type  
server.ssl.key-store=keystore-path  
1
2
3

我们还必须定义标识密钥库中密钥的别名:
server.ssl.key-alias=tomcat

有关SSL配置的更多信息,请访问:HTTPS using self-signed certificate in Spring Boot。

# Tomcat服务器访问日志

在尝试统计页面命中数,用户会话活动等时,Tomcat访问日志非常有用。

要启用访问日志,只需设置:
server.tomcat.accesslog.enabled = true

我们还应该配置其他参数,例如附加到日志文件的目录名,前缀,后缀和日期格式:

server.tomcat.accesslog.directory=logs
server.tomcat.accesslog.file-date-format=yyyy-MM-dd
server.tomcat.accesslog.prefix=access_log
server.tomcat.accesslog.suffix=.log
1
2
3
4

# springboot发布程序的原则

back

如果使用 SpringBoot 多模块发布到外部 Tomcat,可能会遇到各种各样的问题

# 8大原则

  • 在发布模块打包,而不是父模块上打包
  • 公共调用模块,打包类型设置为 jar 格式
  • 发布模块打包类型设置为 war 格式
<packaging>war</packaging>
1
  • 排除内置tomcat
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-tomcat</artifactId>
 <scope>provided</scope>
</dependency>
1
2
3
4
5

当设置 scope=provided 时,此 jar 包不会出现在发布的项目中,从而就排除了内置的 tomcat。

  • 设置启动类
@SpringBootApplication
public class ApiApplication extends SpringBootServletInitializer {
 @Override
 protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
 return application.sources(ApiApplication.class);
 }
 public static void main(String[] args) {
 SpringApplication.run(ApiApplication.class, args);
 }
}
1
2
3
4
5
6
7
8
9
10
  • 如果使用拦截器一定要排除静态文件
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
 @Override
 protected void addResourceHandlers(ResourceHandlerRegistry registry) {
 // 排除静态文件
 registry.addResourceHandler("swagger-ui.html")
 .addResourceLocations("classpath:/META-INF/resources/");
 registry.addResourceHandler("/webjars/**")
 .addResourceLocations("classpath:/META-INF/resources/webjars/");
 }
 // do something
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 先装载公共模块,再发布项目

如果发布的模块引用了本项目的其他公共模块,需要先把本项目的公共模块装载到本地仓库。
操作方式,双击父模块的 install 即可, install 成功之后,点击发布模块的 package 生成 war 包,就完成了项目的打包,

  • 部署项目

# 可能出现的问题

back

  • SpringBoot 配置了端口号影不影响程序发布?

不影响,配置的 server.port 会被覆盖,以 tomcat 本身的端口号为准,tomcat 端口号在 tomcat/config/server.xml 文件中配置。

  • 发布报错,不能找到其他模块或项目中的公共模块,怎么办?

因为没有执行父节点 maven 的 install 操作,install 就是把公共模块放入本地仓库,提供给其它项目使用。

  • 不能找到 SpringBoot 运行的 main 类,怎么办?

因为没有设置启动类导致的,设置方式:

pom.xml 配置启动类,配置<configuration><mainClass>com.bi.api.ApiApplication</mainClass></configuration>
启动类继承 SpringBootServletInitializer 实现 SpringApplicationBuilder 方法,具体代码参考文中第五部分。

  • 把 SpringBoot 项目部署到 Tomcat 7 一直提示找不到 xxx.jar 包?

这是因为 SpringBoot 版本太高,tomcat 版本太低的原因。如果你使用的是最新版的 SpringBoot,可以考虑把 tomcat 也升级为 tomcat 8.x+ 最新的版本,就可以解决这个问题。

# 优雅关闭springboot应用

back

随着线上应用逐步采用 SpringBoot 构建,SpringBoot应用实例越来多,当线上某个应用需要升级部署时,常常简单粗暴地使用 kill 命令,这种停止应用的方式会让应用将所有处理中的请求丢弃,响应失败。这样的响应失败尤其是在处理重要业务逻辑时需要极力避免的

# 定制Tomcat_Connector行为

back

要平滑关闭 Spring Boot 应用的前提就是首先要关闭其内置的 Web 容器,不再处理外部新进入的请求。为了能让应用接受关闭事件通知的时候,保证当前 Tomcat 处理所有已经进入的请求,我们需要实现 TomcatConnectorCustomizer 接口.
Connector 属于 Tomcat 抽象组件,功能就是用来接受外部请求,以及内部传递,并返回响应内容,是Tomcat 中请求处理和响应的重要组件,具体实现有 HTTP ConnectorAJP Connector

package org.springframework.boot.web.embedded.tomcat;

import org.apache.catalina.connector.Connector;

/**
 * 回调接口,可用于自定义Tomcat {@link Connector}.
 *
 * @author Dave Syer
 * @see ConfigurableTomcatWebServerFactory
 * @since 2.0.0
 */
@FunctionalInterface
public interface TomcatConnectorCustomizer {

/**
    * Customize the connector.
    * @param connector the connector to customize
    */
void customize(Connector connector);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

以上在spring-boot-2.1.7.RELEASE.jar

package com.ffCamera.service;

import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.catalina.connector.Connector;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextClosedEvent;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * <p>Title: CustomShutDownApp</p>
 * <p>Description:定制tomcat行为 </p>
 *
 * @author huting
 * @date 2019/8/20 10:09
 */
@Slf4j
@Setter
public class CustomShutDownApp implements TomcatConnectorCustomizer, ApplicationListener<ContextClosedEvent> {
    /**
     * 超市时间(单位:秒)
     */
    @Value(value = "${tomcat.close.timeOut}")
    private Integer timeOut;
    private volatile Connector connector;

    @Override
    public void customize(Connector connector) {
        this.connector = connector;
    }

    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        //暂停接收所有外部请求
        this.connector.pause();
        //获取connector对应的线程池
        Executor executor = this.connector.getProtocolHandler().getExecutor();
        if (executor instanceof ThreadPoolExecutor){
            try {
                log.warn("<<<<<应用即将关闭");
                ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;
                threadPoolExecutor.shutdown();
                if (!threadPoolExecutor.awaitTermination(this.timeOut, TimeUnit.SECONDS)){
                    log.warn("应用等待关闭时间超过最大时长[{}]秒,将强行关闭",this.timeOut);
                    threadPoolExecutor.shutdownNow();
                    if (!threadPoolExecutor.awaitTermination(this.timeOut,TimeUnit.SECONDS)){
                        log.error("应用关闭失败>>>>>");
                    }
                }
            } catch (InterruptedException e){
                log.error(e.getMessage());
                Thread.currentThread().interrupt();
            }
        }
    }
}
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
61

另外需要注意的是我们的类 CustomShutdown 实现了 ApplicationListener 接口,意味着监听着 Spring 容器关闭的事件,即当前的 ApplicationContext 执行 close 方法。

# 内嵌Tomcat添加Connector回调

back

/**
    * 程序启动
    * @param args
    */
public static void main(String[] args) {
    ConfigurableApplicationContext run = SpringApplication.run(FfCameraApplication.class, args);
    Object webFactory = run.getBean("webServerFactory");
}

@Bean
public CustomShutDownApp customShutDownApp(){
    return new CustomShutDownApp();
}

public ConfigurableServletWebServerFactory webServerFactory(final CustomShutDownApp customShutDownApp){
    TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
    tomcatServletWebServerFactory.addConnectorCustomizers(customShutDownApp);
    return tomcatServletWebServerFactory;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

这里的 TomcatServletWebServerFactory 是 Spring Boot 实现内嵌 Tomcat 的工厂类,类似的其他 Web 容器,也有对应的工厂类如 JettyServletWebServerFactory,UndertowServletWebServerFactory。他们共同的特点就是继承同个抽象类 AbstractServletWebServerFactory,提供了 Web 容器默认的公共实现,如应用上下文设置,会话管理等。

如果我们需要定义Spring Boot 内嵌的 Tomcat 容器时,就可以使用 TomcatServletWebServerFactory 来进行个性化定义,例如下方为官方文档提供自定示例:

public ConfigurableServletWebServerFactory webServerFactory(final CustomShutDownApp customShutDownApp){
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
    factory.setPort(9999);
    return factory;
}
1
2
3
4
5

我们这里使用 addConnectorCustomizers 方法将自定义的 Connector 行为添加到内嵌的Tomcat 之上,为了查看加载效果,我们可以在 Spring Boot 程序启动后从容器中获取下webServerFactory 对象,然后观察,在它的 tomcatConnectorCustomizers 属性中可以看到已经有了 CustomeShutdownApp 对象。

# 开启Shutdown_Endpoint

back

我们可以利用 Spring Boot Actuator 来实现Spring 容器的远程关闭

Spring Boot Actuator 是 Spring Boot 的一大特性,它提供了丰富的功能来帮助我们监控和管理生产环境中运行的 Spring Boot 应用。我们可以通过 HTTP 或者 JMX 方式来对我们应用进行管理,除此之外,它为我们的应用提供了审计,健康状态和度量信息收集的功能,能帮助我们更全面地了解运行中的应用。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1
2
3
4

Spring Boot Actuator 采用向外部暴露 Endpoint (端点)的方式来让我们与应用进行监控和管理,引入 spring-boot-starter-actuator 之后,我们就需要启用我们需要的 Shutdown Endpoint,在配置文件 application.properties 中,设置如下

#actuator配置
management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=*
1
2
3

第一行表示启用 Shutdown Endpoint ,第二行表示向外部以 HTTP 方式暴露所有 Endpoint,默认情况下除了 Shutdown Endpoint 之外,其他 Endpoint 都是启用的。

除了 Shutdown Endpoint,Actuator Endpoint 还有十余种,有的是特定操作,比如 heapdump 转储内存日志;有的是信息展示,比如 health 显示应用健康状态。具体所有 Endpoint 信息可以参见官方文档-53. Endpoints 一节。

# 完成配置编码

back

到这里我们的前期配置工作就算完成了。当启动应用后,就可以通过POST 方式请求对应路径的 http://host:port/actuator/shutdown 来实现Spring Boot 应用远程关闭,是不是很简单呢。

 curl -X POST "http://localhost:45600/ffCamera/actuator/shutdown"
1

# 制作脚本

back

#!/bin/bash
# 平滑关闭和启动 Spring Boot 程序
#设置端口
SERVER_PORT="8081"
#设置应用名称
JAR_NAME="springboot-shutdown-0.0.1-SNAPSHOT"
#设置 JAVA 启动参数
JAVA_OPTIONS="-server -Xms1024M -Xmx1024M -Dserver.port=$SERVER_PORT"

#Actuator 方式远程关闭应用
curl -X POST "http://localhost:$SERVER_PORT/actuator/shutdown"
echo ""
#循环遍历应用端口是否被使用,作为应用运作状态的标志
echo "关闭旧应用开始"
UP_STATUS=1
while(( $UP_STATUS>0 ))
do
   UP_STATUS=$(lsof -i:"$SERVER_PORT" | wc -l)
done
echo "\n关闭旧应用结束"
echo "启动应用开始"
#非挂起方式启动应用,并且跟踪启动日志文件
nohup>"$SERVER_PORT".log java -jar "$JAVA_OPTIONS" "$JAR_NAME".jar 2>&1 &
echo "启动应用中" && tail -20f "$SERVER_PORT".log
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# jar部署配置文件外部化

back

# 访问命令行属性

back

在默认的情况下, SpringApplication 会将任何命令行选项参数(以 - 开头 --server.port=9000)转换为 property 并添加到Spring环境当中。
例如,启动项目的时候指定端口:

java -jar analysis-speech-tool-0.0.1-SNAPSHOT.jar --server.port=9000
1

SpringBoot 使用了一个非常特殊的 PropertySource 命令,目的是为了让属性值的重写按照一定的顺序来,而在这个顺序当中,命令行属性总是优先于其他属性源。
当然,如果不想将命令行属性添加到 Spring 环境当中,可以使用以下代码来禁用它们。

SpringApplication.setAddCommandLineProperties(false);
1

# 应用程序属性文件

back

SpringApplication 将从 application.properties 以下位置的文件中加载属性并且将其添加到 Spring 的环境当中:

当前目录下的 /config 子目录
当前目录classpath中的 /config 目录
classpath根目录

该列表按照优先级的顺序排列(在列表中较高的位置定义的属性将会覆盖在较低位置定义的属性)。

如果您不喜欢 application.properties 作为配置文件名,则可以通过指定 spring.config.name 环境属性来切换到另一个名称。还可以使用 spring.config.location 环境属性(以逗号分隔的目录位置列表或文件路径)引用显式位置。
比如:

java -jar myproject.jar --spring.config.name = myproject
java -jar myproject.jar --spring.config.location = classpath:/default.properties,classpath:/override.properties
java -jar -Dspring.config.location = D:\speech\default.properties nacos-config-0.0.1-SNAPSHOT.jar
1
2
3

# war部署

back

Spring Boot是支持发布jar包和war的,但它推荐的是使用jar形式发布。使用jar包比较方便,但如果是频繁修改更新的项目,需要打补丁包,那这么大的jar包上传都是问题

# 修改Spring Boot启动类

启动类继承 SpringBootServletInitializer类,并覆盖 configure方法。

@SpringBootApplication
@MapperScan(value= {"com.ffCamera.mapper"})
@EnableScheduling
@Slf4j
@AllArgsConstructor
public class FfCameraApplication extends SpringBootServletInitializer implements ApplicationRunner  {
private final CustomerProperties customerProperties;

/**
    * 外部tomcat启动方式变更
    */
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(FfCameraApplication.class);
}

/**
    * 程序启动
    * @param args 启动参数
    */
public static void main(String[] args) {
    SpringApplication.run(FfCameraApplication.class, args);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

请注意一个注解@AllArgsConstructor,这个导致启动类没有了默认的无参构造函数,从外部容器启动将会出现问题,解决办法2个:

  • 去掉这个注解@AllArgsConstructor
  • 手动添加一个无参的构造函数,保留注解@AllArgsConstructor

# 修改jar为war包形式

在pom文件中,添加war包配置。

<packaging>
war
</packaging>
1
2
3

# 去除Spring Boot内置Tomcat

修改自带tomcat依赖范围为provided,防止与外部tomcat发生冲突。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
</dependencies>
1
2
3
4
5
6
7

或者

<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>
1
2
3
4
5
6
7
8
9
10

# 添加war包打包插件

如果你用的是继承spring-boot-starter-parent的形式使用Spring Boot,那可以跳过,因为它已经帮你配置好了。如果你使用的依赖spring-boot-dependencies形式,你需要添加以下插件。

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-war-plugin</artifactId>
    <configuration>
        <failOnMissingWebXml>false</failOnMissingWebXml>
    </configuration>
</plugin>
1
2
3
4
5
6
7

failOnMissingWebXml需要开启为false,不然打包会报没有web.xml错误

# 其他需注意问题

  • 如果项目中有用到HttpServletRequest之类的类,解决办法:需添加依赖如下
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <scope>provided</scope>
</dependency>
1
2
3
4
5
  • springboot2.2.0.RELEASE版本对mybatis-plus的问题,出现其配置文件无法注入,解决办法如下:
Failed to bind properties under 'mybatis-plus.configuration.result-maps[0]' to org.apache.ibatis.mapping.ResultMap:
1

升级2.2.1.RELEASE或者退回到2.2.0.RELAESE之前的版本

  • Failed to instantiate WebApplicationInitializer class

手动添加无参构造函数,原因可能是添加了有参构造函数,导致不会自动添加无参构造函数

# jar包转war包有什么影响

  • 1、application配置文件中的server.xx等关于容器的配置就无效了,改配置需要在外部tomcat中进行。
  • 2、Spring Boot的升级是否需要Tomcat跟着升级?需要观察。
  • 3、打war包比打jar明显要变慢好多。。

# 常见几种方法关闭springboot

back

# 通过Springboot提供的actuator

back

<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-actuator</artifactId>
 </dependency>
1
2
3
4

然后将shutdown节点打开,也将/actuator/shutdown暴露web访问也设置上,除了shutdown之外还有health, info的web访问都打开的话将management.endpoints.web.exposure.include=*就可以。

@Slf4j
public class CustomShutDownApp{
    @PreDestroy
    public void preDestroy(){
        log.warn("App is closing>>>>>");
    }
1
2
3
4
5
6
public static void main(String[] args) {
    SpringApplication.run(FfCameraApplication.class, args);
}

@Bean
public CustomShutDownApp customShutDownApp(){
    return new CustomShutDownApp();
}
1
2
3
4
5
6
7
8

启动程序,执行curl -X POST "http://localhost:45600/ffCamera/actuator/shutdown",打印日志com.ffCamera.service.CustomShutDownApp : App is closing>>>>>

# 获取程序启动时候的context

back

这样程序在关闭的时候也会调用PreDestroy注解。

 /* method 2: use ctx.close to shutdown all application context */
 ConfigurableApplicationContext ctx = SpringApplication.run(ShutdowndemoApplication.class, args);
 try {
 TimeUnit.SECONDS.sleep(10);
 } catch (InterruptedException e) {
 e.printStackTrace();
 }
 ctx.close();
1
2
3
4
5
6
7
8

# 在springboot启动的时候将进程号写入一个app.pid文件

# 通过自己实现

back

@RestController
public class ShutDownController implements ApplicationContextAware {
 private ApplicationContext context;
 @PostMapping("/shutDownContext")
 public String shutDownContext() {
 ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) context;
 ctx.close();
 return "context is shutdown";
 }
 @GetMapping("/")
 public String getIndex() {
 return "OK";
 }
 @Override
 public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
 context = applicationContext;
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 通过调用一个SpringApplication.exit()

back

/- method 4: exit this application using static method */
 ConfigurableApplicationContext ctx = SpringApplication.run(ShutdowndemoApplication.class, args);
 exitApplication(ctx);

 public static void exitApplication(ConfigurableApplicationContext context) {
 int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0);
 System.exit(exitCode);
 }
1
2
3
4
5
6
7
8