⭐⭐⭐ Spring Boot 项目实战 ⭐⭐⭐ Spring Cloud 项目实战
《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Jenkins/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labslab-41 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

《芋道 Jenkins 极简入门》中,我们已经学习了如何使用 Jenkins 部署一个 Java 项目,通过启动 jar 包的形式。如果没有看过的胖友,需要先把该文的「2. 快速入门」小节给看啦。因为该小节,已经介绍如何使用 Jenkins 部署 Spring Boot 打成的 jar 包,部署到服务器上。

本文,我们会在《芋道 Jenkins 极简入门》的基础之上,额外学习两块内容:

  • 在本文的「2. 项目打包」小节中,我们会学习如何将 Spring Boot 项目打包成 jar 包。这样,我们后续就可以使用 Jenkins 进行部署。
  • 在本文的「3. 优雅上下线」小节中,我们会分享在 Spring Boot 项目部署时,如何避免启动和关闭的过程中,避免 Nginx 请求到该服务器。

2. 项目打包

示例代码对应仓库:lab-41-demo01

Spring Boot 提供了 Maven 插件 spring-boot-maven-plugin,可以方便的将 Spring Boot 项目打成 jar 包或者 war 包。

考虑到部署的便利性,我们绝大多数 99.99% 的场景下,我们会选择打成 jar 包。这样,我们就无需在部署项目的服务器上,配置相应的 Tomcat、Jetty 等 Servlet 容器。所以本小节,我们也是将 Spring Boot 项目,打成 jar 包。

下面,让我们开始搭建示例。该示例,就是我们在《芋道 Jenkins 极简入门》「2. 快速入门」小节中,部署的 Spring Boot 项目。

2.1 引入依赖

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">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>lab-41-demo01</artifactId>
<!-- <1> 配置打成 jar 包 -->
<packaging>jar</packaging>

<dependencies>
<!-- <2> 实现对 SpringMVC 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- <3> 实现对 Actuator 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<build>
<!-- <4> 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<!-- 使用 spring-boot-maven-plugin 插件打包 -->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

  • <1> 处,通过 <packaging /> 设置为 jar,表示该项目构建打包成 jar 包。😈 如果胖友想要打成 war 包,可以将此处修改成 war
  • <2> 处,引入 spring-boot-starter-web 依赖,实现对 SpringMVC 的自动化配置。
  • <3> 处,引入 spring-boot-starter-actuator 依赖,实现对 Spring Boot Actuator 的自动化配置。这样,我们就可以使用 Actuator 提供的 health 端点,获得健康检查所需的 HTTP API。
  • <4> 处,设置构建的 jar 包的名字。因为我们在《芋道 Jenkins 极简入门》「2. 快速入门」小节中,提供的 deploy.sh 部署脚本,暂时不支持 jar 包的名字带有版本号,所以这里我们需要进行下设置。
  • <5> 处,引入 spring-boot-maven-plugin 插件,因为我们要使用它构建打包 Spring Boot 项目。

2.2 配置文件

resources 目录下,创建 5 个配置文件,对应不同的环境。如下:

嘿嘿,现在暂时偷懒,所以每个配置文件的内容基本是一样的。如下:

server:
port: 8079

management:
server:
port: 8078 # 自定义端口,避免 Nginx 暴露出去

endpoints:
web:
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。

  • server.port 配置项,设置 SpringMVC 的服务器端口。
  • management.server.port 配置项,设置 Spring Boot Actuator 的服务器端口。
  • management.endpoints.web.exposure.include 配置项,设置 Spring Boot Actuator 的开放的端点。

通过多个不同的配置文件,搭配上我们在《芋道 Jenkins 极简入门》「2. 快速入门」小节中,提供的 deploy.sh 部署脚本,设置使用 jar 包中不同的环境配置。

不过可能胖友会吐槽,一个 jar 包包含所有环境的配置,会不会不安全的问题?!确实存在,所以我们一般会做几个事情:

  • 1、不同环境处于不同的网络环境中。例如说,我们测试环境使用一个[阿里云 VPC](专有网络 VPC),正式环境使用另一个阿里云 VPC。这样,在测试环境下,即使使用 application-prod.yaml 配置文件,因为不同网络环境,也是无法连接正式环境的服务。
  • 2、将 application-pre.yamlapplication-prod.yaml 配置文件,放在对应环境中,避免泄露。又或者考虑采用配置中心。
  • 3、敏感配置,进行加密处理。

😈 上述三个方案,可以一起采用,目前我们就是这么干的。

2.3 DemoController

cn.iocoder.springboot.lab40.jenkinsdemo.controller 包路径下,创建 DemoController 类,提供示例 API 接口。代码如下:

@RestController
@RequestMapping("/demo")
public class DemoController {

@GetMapping("/echo")
public String echo() {
return "echo";
}

}

2.4 Application

创建 Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

@SpringBootApplication
public class Application {

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

@Component
public class Listener implements ApplicationListener<ApplicationEvent> {

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ContextClosedEvent) {
this.sleep(10);
}
}

private void sleep(int seconds) {
try {
Thread.sleep(seconds * 1000L);
} catch (InterruptedException ignore) {
}
}

}

}

这里创建的 Listener 监听器,主要是为了监听 ContextClosedEvent 事件,在 Spring 容器关闭之前,sleep 10 秒。这样,我们就能模拟 Spring Boot 应用,关闭时需要一段时间的过程。😈 毕竟,咱这个示例基本是一个“空”项目,关闭速度非常快,无法完整测试 deploy.sh 部署脚本的关闭流程。

2.5 简单测试

如果我们使用 IDEA,可以直接通过界面,使用 spring-boot-maven-plugin 插件打包。如下图所示:IDEA 打包

当然,我们也可以通过执行 mvn clean package -pl lab-41/lab-41-demo01 -Dmaven.test.skip=true 命令,构建 lab-41/lab-41-demo01 子 Maven 模块

友情提示:如果胖友要构建整个项目,可以考虑使用 mvn clean package -Dmaven.test.skip=true 命令。

打包完成后,我们可以使用 java -jar 命令,进行启动。操作命令如下:

# 进入 jar 包所在目录,即指定项目的 target 目录下
$ cd lab-41/lab-41-demo01/target

# 启动 Java 服务。这里,我们使用 local 环境
$ java -jar lab-41-demo01.jar --spring.profiles.active=local
... 一堆 Spring Boot 项目的启动日志。

3. 优雅上下线

示例代码对应仓库:lab-41-demo02

在生产环境下,我们会部署相同 Spring Boot 项目的,启动多个 Java 服务。而后,通过 Nginx 对 Java 服务进行负载均衡,实现高可用。如下图所示:负载均衡

友情提示:如果对 Nginx 不了解的胖友,可以后续看看《超实用的 Nginx 极简教程,覆盖了常用场景》文章,进行快速入门哈。

一般情况下,我们是通过静态配置 Nginx Upstream,负载均衡到多个 Java 服务。示例如下:

upstream bck_testing_01 {
server 192.168.250.220:8080
server 192.168.250.221:8080
server 192.168.250.222:8080
}

这样,就会存在两个问题?!

  • 问题一,如果某个 Java 服务宕机后,如果在 Nginx Upstream 还配置着该 Java 服务,Nginx 还是会负载均衡到该节点,导致返回错误给用户。
  • 问题二,在《芋道 Jenkins 极简入门》中我们提到过,Java 服务的启动和关闭是一个过程,需要一段时间,所以在此期间,Nginx 转发请求到该 Java 服务时,可能会响应较慢或者返回错误给用户。那么,每次我们在使用 Jenkins 部署 Spring Boot 项目时,必然会触发该问题。

在不考虑使用网关或者注册中心的情况下,我们需要动态化 Nginx Upstream 配置,实现如下效果:

  • 在 Java 服务宕机时,又或者 Java 服务正在关闭时,Nginx 将该 Java 服务从 Nginx Upstream 配置中移除,避免转发请求到其上。
  • 在 Java 服务完成启动时,Nginx 将该 Java 服务在 Nginx Upstream 配置中添加,开始转发请求到其上。

如果我们要实现该效果,需要给 Nginx 增加健康检查功能,对 Nginx Upstream 配置的 Java 服务,进行不断的健康检查。在健康检查通过时,才真正将该 Java 服务添加到 Nginx Upstream 配置中,才会转发请求到其上。

😈 咳咳咳,感觉自己有点啰嗦。

在 Nginx 上,有多种健康检查插件,在[《Nginx 负载均衡中后端节点服务器健康检查 - 运维笔记》](TODO https://www.cnblogs.com/kevingrace/p/6685698.html)中,有详细的介绍。不过真正可用的,只有阿里开源的 Tengine 的 Upstream check module 插件。因为只有该插件,支持主动向 Nginx Upstream 配置的 Java 服务,进行健康检查。

下面,我们开始本小节的示例:

  • 首先,我们会搭建一个 Spring Boot 项目。整体来说,会和「2. 项目打包」类似。不过为了更好的和 Nginx 的健康检查集成,我们会自定义一个 HealthIndicator 实现类,嘿嘿。
  • 然后,我们会搭建一个 Tengine 服务,配置对搭建的 Spring Boot 项目的负载均衡与健康检查。
  • 最后,我们会进行简单的测试。

一共三个步骤,走起~

3.1 Spring Boot 项目搭建

「2. 项目打包」基本相似的,主要是三个部分:

下面,我们来看看不同的部分。

3.1.1 ServerHealthIndicator

cn.iocoder.springboot.lab40.jenkinsdemo.actuate 包下,创建 ServerHealthIndicator 类,自定义服务器状态的 HealthIndicator 实现类。代码如下:

@Component
public class ServerHealthIndicator extends AbstractHealthIndicator implements ApplicationListener<ApplicationEvent> {

private Logger logger = LoggerFactory.getLogger(getClass());

/**
* 是否在服务中
*/
private volatile boolean inService = false;

@Override
protected void doHealthCheck(Health.Builder builder) {
if (inService) {
builder.up();
} else {
builder.down();
}
}

@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationReadyEvent) {
this.handleApplicationReadyEvent((ApplicationReadyEvent) event);
} else if (event instanceof ApplicationFailedEvent) {
this.handleApplicationFailedEvent((ApplicationFailedEvent) event);
} else if (event instanceof ContextClosedEvent) {
this.handleContextClosedEvent((ContextClosedEvent) event);
}
}

@SuppressWarnings("unused")
private void handleApplicationReadyEvent(ApplicationReadyEvent event) {
this.inService = true;
}

@SuppressWarnings("unused")
private void handleApplicationFailedEvent(ApplicationFailedEvent event) {
this.inService = false;
}

@SuppressWarnings("unused")
private void handleContextClosedEvent(ContextClosedEvent event) {
// 标记不提供服务
this.inService = false;

// sleep 等待负载均衡完成健康检查
for (int i = 0; i < 20; i++) { // TODO 20 需要配置
logger.info("[handleContextClosedEvent][优雅关闭,第 {} sleep 等待负载均衡完成健康检查]", i);
try {
Thread.sleep(1000L);
} catch (InterruptedException ignore) {
}
}
}

}

① 继承 AbstractHealthIndicator 抽象类,所以实现 #doHealthCheck(Health.Builder builder) 方法,根据 inService 的状态,返回服务器是 UP 还是 DOWN 状态。

② 实现 ApplicationListener 接口,所以实现 #onApplicationEvent(ApplicationEvent event) 方法,监听 ApplicationEvent 事件。Spring Boot 在启动的过程中,会发布不同的 ApplicationEvent 事件,因此我们可以根据这些事件,标记服务器状态 inService,是否处于服务中。不同的 ApplicationEvent 时间处理如下:

  • 如果是 ApplicationReadyEvent 事件,说明 Spring Boot 应用启动完毕,相应的 Bean 等都初始化完成,可以正常提供服务。因而,我们标记 inService = true
  • 如果是 ApplicationFailedEvent 事件,说明 Spring Boot 应用启动失败,无法正常提供服务。因此,我们标记 inService = false
  • 如果是 ContextClosedEvent 事件,说明 Spring 容器要开始关闭了,一旦开始关闭,无法正常提供服务。因此,我们标记 inService = false。😈 同时,这里非常关键,这里我们额外 sleep 了 20 秒,这样 Tengine 能够完成对该 Spring Boot 应用的健康检查,将其从 Upstream 配置移除,嘿嘿。

胖友在一起结合 ① 和 ②,思考下整个过程。后续,我们可以通过 http://127.0.0.1:8078/actuator/health/server 地址,返回服务器是否提供服务。当然,实际上也是可以使用 http://127.0.0.1:8078/actuator/health/server 地址,看自己喜好吧。

不过,ServerHealthIndicator 针对 ContextClosedEvent 的 sleep 20 秒,是一定要做的,保证 Tengine 有足够的时间,在该 Spring Boot 应用关闭前,完成对其的健康检查,从而从 Upstream 配置移除。咳咳咳,😈 好像又啰嗦了一遍。

3.1.2 Demo02Application

创建 Demo02Application.java 类,配置 @SpringBootApplication 注解即可。代码如下:

@SpringBootApplication
public class Demo02Application {

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

}

3.2 Tengine 搭建

参考《芋道 Tengine 极简入门》文章,先搭建一个 Tengine 服务,并且记得安装 ngx_http_upstream_check_module 插件噢。

搭建完成后,我们配置 Tengine 配置文件 conf/nginx.conf 如下,增加对稍后启动的 Spring Boot 应用的健康检查。配置如下:

# // 省略其它配置内容...

http {
# // 省略其它配置内容...

upstream cluster1 {
# simple round-robin
#server 192.168.0.1:80;
server 10.8.8.18:8079;

check interval=3000 rise=2 fall=3 timeout=1000 type=http port=8078;
check_http_send "HEAD /actuator/health/server HTTP/1.0\r\n\r\n";
check_http_expect_alive http_2xx http_3xx;
}

server {
# // 省略其它配置内容...

location /springboot {
proxy_pass http://cluster1/;
}

location /status {
check_status;

access_log off;
#allow SOME.IP.ADD.RESS;
#deny all;
}
}

upstream cluster1 配置项,我们创建了一个名字为 cluster1 的 Upstream 配置。

  • server 10.8.8.18:8079 配置项,我们稍后启动的 Spring Boot 应用的服务地址。😈 注意,这里的 8079 端口,是我们配置的 Spring Boot 应用的 SpringMVC 的服务器端口。
  • check interval=3000 rise=2 fall=3 timeout=1000 type=http port=8078; 配置项,我们配置了健康检查功能。
    • interval:向后端发送的健康检查包的间隔。
    • fall:如果连续失败次数达到指定次数,服务器就被认为是 DOWN
    • rise:如果连续成功次数达到指定次数,服务器就被认为是 UP
    • timeout:后端健康请求的超时时间。
    • type:健康检查包的类型,现在支持以下多种类型 tcpssl_hellohttpmysqlajp
    • port:指定后端服务器的检查端口。你可以指定不同于真实服务的后端服务器的端口,比如后端提供的是 443 端口的应用,你可以去检查 80 端口的状态来判断后端健康状况。默认是 0,表示跟后端 server 提供真实服务的端口一样。😈 注意,这里的 8078 端口,是我们配置的 Spring Boot 应用的 Actuator 的服务器端口。同时,SpringMVC 和 Actuator 使用不同的端口,避免 Actuator 被 Nginx 暴露到外网去。
  • check_http_send "HEAD /actuator/health/server HTTP/1.0\r\n\r\n"; 配置项,该指令可以配置 http 健康检查包发送的请求内容。为了减少传输数据量,推荐采用 "HEAD" 方法。😈 注意,这里的 /actuator/health/server 地址,就是我们在「3.1.1 ServerHealthIndicator」 所提供的健康检查接口。
  • check_http_expect_alive http_2xx http_3xx; 配置项,该指令指定 HTTP 回复的成功状态,默认认为 2XX 和 3XX 的状态是健康的。😈 注意,在 Actuator 提供的 health 端点,在返回服务器是 UP 状态时的状态码为 200,在返回服务器是 DOWN 状态时的状态码为 503,满足check_http_expect_alive 配置项。

② location /springboot 配置项,我们创建了一个 Location,转发到我们配置的 Upstream。

③ location /status 配置项,我们创建了一个 Location,转发到 Tengine 提供的服务器的健康状态页。

  • check_status; 配置项:显示服务器的健康状态页面。
  • allow + deny 组合,配置允许访问的 IP,保证安全性。这里我们先注释掉,方便我们测试哈。

配置完成后,记得使用 sbin/nginx -s reload 命令,重新让 Tengine 加载下最新配置噢。

3.3 简单测试

友情提示:艿艿的 Tengine 是安装在 IP 为 172.16.48.180 服务器上。

先不启动 Spring Boot 应用。使用浏览器,打开 Tengine 提供的服务器的状态健康页 http://172.16.48.180/status,可以看到 Spring Boot 应用处于 DOWN 状态。如下图所示:Tengine 服务器的状态健康页

启动 Spring Boot 应用。😈 这里我们偷懒,直接使用 IDEA Debug 运行 Demo02Application 的 #main(...) 方法,进行启动,嘿嘿。使用浏览器,不断刷新 http://172.16.48.180/status 地址,会最终看到 Spring Boot 应用处于 UP 状态。如下图所示:启动 Spring Boot 应用

在变成 UP 状态之前,而 Spring Boot 应用启动完成之后,在这一段时间里,如果我们使用浏览器访问 http://172.16.48.180/springboot/demo/echo 地址,Tengine 并不会转发到该 Spring Boot 应用之上,因为健康检查还没通过。胖友可以自己测试一下。

关闭 Spring Boot 应用。😈 如果使用 IDEA,可以点击左下角的红色小方块 Stop Demo02Application。在此期间,我们会看到 Spring Boot 应用会 sleep 20 秒。而在此之前,因为我们配置的 Tengine 健康检查频率是 3 秒,并且 3 次健康检查不通过,则标记 Spring Boot 应用为 DOWN。如下图所示:关闭 Spring Boot 应用

在变成 DOWN 状态之前,而 Spring Boot 应用还在 sleep 的时候,在这一段时间里,如果我们使用浏览器访问 http://172.16.48.180/springboot/demo/echo 地址,Tengine 并不会转发到该 Spring Boot 应用之上,因为健康检查还没通过。胖友可以自己测试一下。

😎 至此,我们完成了 Spring Boot 优雅上下线的示例。因为测试过程有点“动态”,胖友可以自行操练操练,嘿嘿。

😈 当然,通过 Tengine 健康检查来实现 Spring Boot 优雅上下线的唯一方案,艿艿了解到,还有公司是通过部署脚本,步骤如下:

  • 首先,将 Upstream 移除准备重启的 Spring Boot 应用。
  • 然后,将 Spring Boot 应用进行重启。
  • 最后,将 Upstream 重新增加该 Spring Boot 应用。

艿艿偏向 Tengine 健康检查方案的原因是,整个方案更加简单干净,嘿嘿。

666. 彩蛋

好像暂时没有什么可写的彩蛋,用就完事了,妥妥的。

如果想要使用 Jenkins 部署 Spring Cloud 项目,可以参考《芋道 Spring Cloud 持续交付 Jenkins 入门》文章。

文章目录
  1. 1. 1. 概述
  2. 2. 2. 项目打包
    1. 2.1. 2.1 引入依赖
    2. 2.2. 2.2 配置文件
    3. 2.3. 2.3 DemoController
    4. 2.4. 2.4 Application
    5. 2.5. 2.5 简单测试
  3. 3. 3. 优雅上下线
    1. 3.1. 3.1 Spring Boot 项目搭建
      1. 3.1.1. 3.1.1 ServerHealthIndicator
      2. 3.1.2. 3.1.2 Demo02Application
    2. 3.2. 3.2 Tengine 搭建
    3. 3.3. 3.3 简单测试
  4. 4. 666. 彩蛋