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

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


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

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

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

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

1. 概述

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

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

  • 在本文的「2. 项目打包」小节中,我们会学习如何将 Spring Cloud 项目打包成 jar 包。这样,我们后续就可以使用 Jenkins 进行部署。
  • 在本文的「3. 优雅下线」小节中,我们会分享在 Spring Cloud 项目关闭时,如何优雅下线,避免服务消费者继续请求当前项目。

2. 项目打包

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

友情提示:因为 Spring Cloud 项目也是基于 Spring Boot 的,因此一样可以使用 spring-boot-maven-plugin 插件来打包。

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

下面,我们新建 labx-16-demo-01 项目,作为本小节的示例,最终项目如下图:项目结构

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>
<artifactId>labx-16</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>labx-16-demo-01</artifactId>
<!-- <1> 配置打成 jar 包 -->
<packaging>jar</packaging>

<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
<spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
</properties>

<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

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

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

<!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>

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

</project>

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

2.2 配置文件

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

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

server:
port: 8079

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

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

spring:
application:
name: demo-service

cloud:
nacos:
# Nacos 作为注册中心的配置项,对应 NacosDiscoveryProperties 配置类
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址

  • server.port 配置项,设置 SpringMVC 的服务器端口。

  • management.server.port 配置项,设置 Spring Boot Actuator 的服务器端口。

  • management.endpoint.web.exposure.include 配置项,设置 Spring Boot Actuator 的开放的端点。

  • spring.application.name 配置项,应用(服务)名。

  • spring.cloud.nacos.discovery.server-addr 配置项,设置 Nacos 注册中心的地址。

    友情提示:如果对使用 Nacos 作为 Spring Cloud 注册中心的胖友,可以后续阅读《芋道 Spring Cloud Alibaba 注册中心 Nacos 入门》文章。

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

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

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

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

2.3 DemoController

创建 DemoController 类,提供示例 API 接口。代码如下:

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

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

}

2.4 Application

创建 Application 类,应用启动类。代码如下:

@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 labx-16/labx-16-demo-01 -Dmaven.test.skip=true 命令,构建 labx-16/labx-16-demo-01 子 Maven 模块

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

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

# 进入 jar 包所在目录,即指定项目的 target 目录下
$ cd labx-16/labx-16-demo-01/target

# 启动 Java 服务。这里,我们使用 local 环境
$ java -jar labx-16-demo-01.jar --spring.profiles.active=local
... 一堆 Spring Cloud 项目的启动日志。

后续,我们就可以参考《芋道 Jenkins 极简入门》「2.4 Jenkins 部署任务配置」小节,部署 Spring Cloud 项目。

3. 优雅下线

在 Spring Cloud 项目的微服务实例关闭时,需要首先从注册中心设置为下线,避免该服务的消费者继续请求该服务实例,导致请求失败。

虽然说,在 Spring 容器关闭时,Spring Cloud 内置的 AbstractAutoServiceRegistration 会将当前实例从注册中心取消注册,但是并不能保证这个步骤最先执行,在所有 Bean 的销毁之前。同时,虽然当前服务实例已经从注册中心取消注册,但是服务消费者从注册中心中拉取到最新的注册信息,是存在延迟的,只是长短的差别。例如说:

  • Eureka 注册中心贼慢,最长可达 3 分钟:

    【可忽略,服务异常宕机】90s(微服务在Eureka Server上租约到期)+

    30s(Eureka Server服务列表刷新到只读缓存ReadOnlyMap的时间,Eureka Client默认读此缓存)+
    30s(Zuul作为Eureka Client默认每30秒拉取一次服务列表) +
    30s(Ribbon默认动态刷新其ServerList的时间间隔)

    = 180s,即 3分钟

  • Nacos 注册中心贼快,略微有秒级的延迟。

如果我们在服务实例从注册中心取消注册后,立即销毁其它 Spring Bean 的话,会导致当前在处理的请求,又或者服务消费者因为注册中心延迟期间继续打进来的请求,产生处理失败的情况。

面对这样的问题,我们需要实现两件事情

  • 在任何 Spring Bean 的销毁之前,先将当前服务实例从注册中心取消注册。
  • 取消注册之后,sleep 一段时间,保证服务消费者能够从注册中心拉取到最新实例列表。

具体的解决方案,可以参考《Spring Cloud 微服务如何优雅停机及源码分析》文章。这里,艿艿倾向采用方式四:service-registry 端点

3.1 service-registry 端点

我们先来简单了解下 service-registry 端点,我们可以设置当前服务在注册中心的状态

一起来简单测试一波,直接使用「2. 项目打包」labx-16-demo-01 项目:

① 因为我们已经设置 management.endpoints.web.exposure.include 配置项为 *,所以 service-registry 端点已经开放。如下图所示:配置文件

② 启动 Spring Cloud 项目示例,操作命令如下:

# 进入 jar 包所在目录,即指定项目的 target 目录下
$ cd labx-16/labx-16-demo-01/target

# 启动 Java 服务。这里,我们使用 local 环境
$ java -jar labx-16-demo-01.jar --spring.profiles.active=prod
... 一堆 Spring Cloud 项目的启动日志。

启动完成后,我们可以在 Nacos 控制台看到该服务实例处于上线状态。如下图所示:服务实例 - 上线

③ 使用 Postman 请求 service-registry 端点,设置服务实例为下线。如下图所示:Postman 请求 `service-registry 端点

请完成后,我们可以在 Nacos 控制台看到该服务实例处于下线状态。如下图所示:服务实例 - 下线

3.2 集成到部署脚本

复制出新的 depoly.sh 脚本,将 service-registry 端点集成进来。最终如下图所示:脚本

  • 重点红框部分,记得看下说明哟。

一起来简单测试一波,整体步骤如下:

① 先将部署目录初始化如下图所示:部署目录

② 执行 deploy 脚本,进行部署。打印日志如下:

$ sh deploy.sh 
[backup] 开始备份 labx-16-demo-01 ...
[backup] 备份 labx-16-demo-01 完成
[stop] 开始停止 /work/projects/labx-16-demo-01/labx-16-demo-01
[stop] 从注册中心下线当前实例,并 sleep 20
[stop] /work/projects/labx-16-demo-01/labx-16-demo-01 运行中,开始 kill [43671]
-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .-e .[stop] 停止 $BASE_PATH/$SERVER_NAME 成功
[transfer] 开始转移 labx-16-demo-01.jar
[transfer] 移除 /work/projects/labx-16-demo-01/labx-16-demo-01.jar 完成
[transfer] 从 /work/projects/labx-16-demo-01/build 中获取 labx-16-demo-01.jar 并迁移至 /work/projects/labx-16-demo-01 ....
[transfer] 转移 labx-16-demo-01.jar 完成
[start] 开始启动 /work/projects/labx-16-demo-01/labx-16-demo-01
[start] JAVA_OPS: -Xms1024m -Xmx1024m -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/work/projects/labx-16-demo-01/heapError
[start] JAVA_AGENT:
[start] PROFILES: prod
[start] 启动 /work/projects/labx-16-demo-01/labx-16-demo-01 完成
[healthCheck] 开始通过 http://127.0.0.1:8078/actuator/health/ 地址,进行健康检查
appending output to nohup.out
-e .-e .-e .-e .[healthCheck] 健康检查通过
2020-03-30 20:48:02.790 INFO 46564 --- [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
2020-03-30 20:48:02.790 INFO 46564 --- [ main] org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.30]
2020-03-30 20:48:02.793 INFO 46564 --- [ main] o.a.c.c.C.[Tomcat-1].[localhost].[/] : Initializing Spring embedded WebApplicationContext
2020-03-30 20:48:02.793 INFO 46564 --- [ main] o.s.web.context.ContextLoader : Root WebApplicationContext: initialization completed in 84 ms
2020-03-30 20:48:02.804 INFO 46564 --- [ main] o.s.b.a.e.web.EndpointLinksResolver : Exposing 18 endpoint(s) beneath base path '/actuator'
2020-03-30 20:48:02.856 INFO 46564 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8078 (http) with context path ''
2020-03-30 20:48:02.860 INFO 46564 --- [ main] c.i.s.lab16.jenkinsdemo.Application : Started Application in 3.157 seconds (JVM running for 3.525)
2020-03-30 20:48:03.480 INFO 46564 --- [nio-8078-exec-1] o.a.c.c.C.[Tomcat-1].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2020-03-30 20:48:03.480 INFO 46564 --- [nio-8078-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2020-03-30 20:48:03.484 INFO 46564 --- [nio-8078-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms

后续,胖友可以再额外启动一个该 Spring Cloud 项目的服务实例,并再自己搭建一个 Spring Cloud Gateway 网关,转发请求到该服务的两个实例。同时,不断高频请求网关,不断逐个使用 deploy.sh 脚本重启服务实例,看看是否会出现请求失败的情况!

666. 彩蛋

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

如果想要使用 Jenkins 部署 Spring Boot 项目,可以参考《芋道 Spring Boot 持续交付 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 service-registry 端点
    2. 3.2. 3.2 集成到部署脚本
  4. 4. 666. 彩蛋