Spring Boot 2.3 已经发布一个月了,这两天才想起来尝一尝鲜儿。除了常规的升级外,很大部分的升级是针对 Docker 的,让你不得不相信,Docker 容器化微服务已然大势所趋。还没有用过的同学,再不下手就晚了。
此次升级主要包括如下几个方面,接下来就跟着我一起来尝一尝吧。
为了说明 Spring Boot 2.3 的新特性,必须创建一个项目,以便试验。
1、创建一个 Spring Boot 项目,可以到 https://start.spring.io/ 上创建,也可以使用 IDEA 自带的功能创建。选择版本 2.3.1,JDK 还是选择亲爱的 Java 8,引入 Web 和 Actuator 两个依赖包。
有一点要注意一下,在我写本文的时候,Spring Boot 2.3.1 还不能从中央仓库下载,需要添加 Spring Boot 官方的里程碑仓库。
<repositories>
<repository>
<id>spring-milestone</id>
<name>Spring Milestone Repository</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
2、在 pom 文件中引入 Maven 插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.1.RELEASE</version>
</plugin>
</plugins>
</build>
3、添加一个 Controller,做测试用。
@RestController
public class PlayController {
@GetMapping(value = "play")
public String play(){
return "hey, play with me!";
}
}
4、启动项目
mvn spring-boot:run
5、访问 http://localhost:8080/play,说明项目启动成功
如果不使用 Docker 呢,那就直接打成 jar
包,使用如下命令
mvn package spring-boot:repackage
然后就可以把这个 Jar
包部署到服务器了,当然这个过程可能是用自动化部署工具实现的,不如 jenkins 或者自研系统。
抛开公司(尤其是大厂)里成熟的自动化部署流程不谈,我这里说的是一般性小厂或者是个人项目。
如果你在之前的版本就已经用 Docker 方式,那基本上都是自己写 Dockerfile ,然后自己写脚本使用 Dockerfile 打镜像包,或者使用 Maven
插件,比如 dockerfile-maven-plugin
,我之前写过一篇 Spring Boot 和 Docker 实现微服务部署,就是用的这种方式,可以对比着看一下。
如果你了解 Dockerfiles 的话,那你肯定了解用 Dockerfiles 构建镜像的过程,需要你创建一个 Dockerfile 文件然后在里面写上构建镜像所需的一系列动作,而 Cloud Native Buildpacks 则无需配置类似的过程文件,很大程度上减轻了开发者的工作,提高了效率。这还不是最重要的,最重要的是它提供了更高层次的抽象能力,使镜像的分层更加清晰,并且合理有效的利用层缓存,这样一来,当我们对应用程序进行修改之后,再次构建镜像时的速度飞快,比如我们的应用只改了几行代码,那当我们使用 Buildpacks 构建镜像时,只需要在应用程序层进行重新构建,其他层使用缓存就可以,也就是只对变化了的层重新构建。
首先要确保你本地已经正常启动了 Docker 服务。
Spring Boot 2.3 官方的 Docker Maven 插件,从此不用再借助第三方了。我们前面创建项目的时候已经引入了这个 Maven 插件。
此插件不仅提供了打镜像包的功能,还有其他的常用功能,比如 run、repackage 等。
为什么前面要说 Cloud Native Buildpacks 呢,不是跑题啊,是因为 Spring Boot 2.3 生成 Docker 镜像包的方式就是集成了 Cloud Native Buildpacks。
那我们就打个镜像包试一下吧
mvn spring-boot:build-image
你以为马上就能看到成果了吗,还是太年轻。
大中华区开发者怎么了
对于中国的开发者来说,打包这一步不会太顺利,原因大家都很清楚。不出意外的话,应该会出现这样的错误,不出错可能才是意外。
[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.3.1.RELEASE:build-image (default-cli) on project play: Execution default-cli of goal org.springframework.boot:spring-boot-maven-plugin:2.3.1.RELEASE:build-image failed: Docker API call to 'localhost/v1.24/images/create?fromImage=gcr.io%2Fpaketo-buildpacks%2Fbuilder%3Abase-platform-api-0.3' failed with status code 500 "Internal Server Error" and message "Get https://gcr.io/v2/: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)" -> [Help 1]
出现这个问题的原因是因为 Buildpacks 调用 Docker API 创建镜像的方法,要访问 https://gcr.io ,从上面 pull 一些基础镜像下来,这是 Google 的 Google Cloud ,是 Google 的容器仓库,然而对于中国的开发者来说,这个地址是 404 的。
所以我们要加个系统级别代理,或者专门为 Docker 配置代理。我是在 Docker 中配置的代理,系统代理的影响太大。我本机安装的是 Docker Desktop,直接打开设置,在里面加上代理就可以了(别问我代理怎么搞,问我就是没有代理)。
好了,通过上面一顿猛如虎的操作,再次运行命令
mvn spring-boot:build-image
根据你的网速,等上一段时间,就会出现下面的结果,说明镜像创建成功了。
之后你可以使用 docker images
命令查看。这时间也是醉了,40 years ago。
使用此镜像启动容器
使用命令直接启动容器。
docker run -it -p8080:8080 play:0.0.1-SNAPSHOT
然后访问 8080 端口,得到正确的返回结果,说明启动成功了。
Docker Image 的一个特点是,每个层都是前一层变化的增量。有一个工具叫做 dive,可以清楚的查看分层结构里面包含的内容。具体安装和使用请自行搜索。
使用 dive 查看的一个小技巧,因为镜像层包含的指令很多,所以我们选择只查看相对于上一层的增量内容,使用 Ctrl+L
组合键。
然后按 Tab
进入视图,然后按 Ctrl+U
,去掉没有更改的选项,也就是只看变化的部分。
然后上下箭头可以切换层查看,比如下面这个图展示了一个 18 M 的层相对于上一层的变化内容,可以看出来这个层实际上就是应用程序层,包含了很多当前应用程序的类和第三方依赖包等。
分层打包配置很方便,最简单的方式就是在 pom 文件中加上如下配置:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.1.RELEASE</version>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
加上分层配置之后,仍然使用常规的命令打包
mvn package spring-boot:repackage
分层打包其实和以前的打包方式没有什么不同,打出来的包几乎和之前是完全一样的,分层其实只是逻辑上的抽象而已。打出的 jar 包结构如下(jar包其实就是个压缩包,可以解压缩查看目录结构)
在 jar 包的 BOOT-INF 目录下可以看到 classpath.idx
和layers.idx
两个文件,这两个就是为了分层 jar 的关键。
默认情况下会分成如下四个层。
dependencies
对版本没有要求的依赖包,也就是你的应用程序无论怎么改,都几乎不会影响的依赖包。spring-boot-loader
Spring Boot 加载类。snapshot-dependencies
对应用版本有要求的依赖包,比如应用升级后,可能同时需要升级的依赖包。application
应用程序编译类和配置文件等。在 layers.idx
可以看出这个分层结构,用普通的文本编辑器就可以打开,比如 sublime。打开之后看到这样一个类似于 yaml 的结构,四个层以及他们所指的目录都清晰的列出来了。
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"
classpath.idx
文件列出了依赖的 jar 包列表,到时候会按照这个顺序载入。
- "spring-boot-starter-actuator-2.3.1.RELEASE.jar"
- "spring-boot-starter-2.3.1.RELEASE.jar"
- "spring-boot-2.3.1.RELEASE.jar"
- "spring-boot-autoconfigure-2.3.1.RELEASE.jar"
- "spring-boot-starter-logging-2.3.1.RELEASE.jar"
- "logback-classic-1.2.3.jar"
- "logback-core-1.2.3.jar"
- "log4j-to-slf4j-2.13.3.jar"
如果我们想要在默认的 4 层上增加新的分层,Spring Boot 2.3 也提供了定制分层的功能。配置也很简单,在 plugin
配置如下,指定了 layers.xml
作为自定义分层配置
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/layers.xml</configuration>
</layers>
</configuration>
layers.xml
的配置像下面这样
<layers xmlns="http://www.springframework.org/schema/boot/layers"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
https://www.springframework.org/schema/boot/layers/layers-2.3.xsd">
<application>
<into layer="spring-boot-loader">
<include>org/springframework/boot/loader/**</include>
</into>
<into layer="application" />
</application>
<dependencies>
<into layer="snapshot-dependencies">
<include>*:*:*SNAPSHOT</include>
</into>
<into layer="dependencies" />
</dependencies>
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>snapshot-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>
当你开启分层功能后,可以使用 -Djarmode
查看分层情况。
java -Djarmode=layertools -jar target/play-0.0.1-SNAPSHOT.jar list
显示的结果就是分层情况,比如默认情况下就是这样,列出了 4 个默认分层。
dependencies
spring-boot-loader
snapshot-dependencies
application
题外话:
Djarmode
其实就是个 Java-Agent
,关于Java-Agent
,可以看我之前写的一篇文章,Java 调试工具、热部署、JVM 监控工具都用到了它,挺有意思的。
说了半天分层包了,那分层包到底有啥用呢?
这么说吧,它其实是为了和 Docker 配合使用的,如果你不用 Docker 方式部署,还是用原始 jar 包的方式,可以说没什么用,如果非得说有什么用,那就是让你更加清楚项目的依赖情况。
前面介绍 Docker 镜像包的时候说了 Buildpacks 可以让你的镜像分层清晰,而 Spring Boot 2.3 提供的分层 jar 功能可以在镜像分层的基础上更上一层楼,使分层更加清晰。
那我们开启分层配置,然后重新打个 Docker 镜像出来看一看。
mvn spring-boot:build-image
然后再使用 dive 工具看一下启用分层 jar 功能后的 Docker 镜像分层情况,是不是变得更好了。前面的层都是一样的,都是一些集成镜像和配置,从 18 MB 的这个层开始的 4 个层就是启用分层后的4个层,分别对应 dependencies、spring-boot-loader、snapshot-dependencies、application
比如这个 5.4K 的 application 层。
那这样做有什么好处呢,前面不是说了吗,Buildpacks 打镜像包会使用缓存的,如果这一层没变那就不用重新打这一层,只需要重新打包修改过的层,这样一来,如果你只修改了 application 中的内容,比如新加了 Controller 或者配置文件等,那么只需要重新打包这一层,也就是几 K,几十K 不会很大,这样一来打包速度就很快了,要不然一个上百兆的镜像包也得需要一段时间。
什么叫优雅停机呢,假设这是一个分布式服务,其中一台服务所在的实体机需要打安全补丁,需要关机重启,那实体机关机之前要先把这个服务停掉。
关掉服务的方式,比如:
额,还行吧,但是有点儿问题,比如当前服务实例正在处理请求,还没处理完,你咔嚓一下就给它结束了,谁受得了,不要太刺激。
我们把前面的那个 Controller 中的 play
方法改一下,加一个延时,等待 6 秒才返回,模拟一个比较慢的请求。
@GetMapping(value = "play")
public String play() throws InterruptedException{
Thread.sleep(6000);
return "hey, play with me!";
}
效果就是你访问这个地址,然后等了 6 秒之后才显示出 hey, play with me!
。
如果在这 6 秒钟之内我杀掉了进程,将会在浏览器中出现下面这个讨厌的界面。
只需要在配置文件中增加 server.shutdown
的配置,一种是 immediate
,也就是立即停止,另一种就是所谓的优雅关机 graceful
。
server:
port: 8081
shutdown: graceful
# 缓冲10s,上面定义的那个方法延时 6秒,所以10秒肯定够了
spring:
lifecycle:
timeout-per-shutdown-phase: 10s
之后,再启动服务,然后访问这个页面,这个过程中结束进程。然后会看到控制台有输出,提示优雅关机的过程,并提示说会等待活动状态的请求处理完成。
请求也变得正常了。
之前版本的 spring-boot-starter-actuator
就已经有健康状态检测了,不开启活性状态检测,当我们访问 health 的时候,会看到下面的信息,说明服务是可用的。
通过在配置文件中配置如下信息,可开启活动状态检测。
management:
health:
probes:
enabled: true
endpoint:
health:
show-details: always
开启上述配置之后,重启服务,在访问 health 页面,看到的内容如下
除了状态标示外,还多了一个 groups
节点。
Liveness:应用程序是否处于可用状态
可通过 /actuator/health/liveness
路径查看
Readiness:应用程序是否准备好接受客户端请求了。
可通过 /actuator/health/readiness
路径查看
这个功能其实是针对部署在 Kubernetes 上的服务做的支持。Kubernetes 提供了 LivenessProbe 和 cProbe 两类探针,活动状态检查便是对这两类探针提供无缝支持。
在配置文件中增加配置即可,与 kubernetes 做无缝对接。
spring:
main:
cloud-platform: kubernetes
拿 Readiness 来说吧,假设我们要对外宣布次服务暂时不接受请求,那就改变 readiness 的状态,当探针过来的时候发现不接受请求,那就去请求其他实例了。
具体怎么做呢,我在 Controller 中加了两个方法,一个开启接受请求,一个停止接收请求。
@RestController
public class PlayController {
private final ApplicationEventPublisher publisher;
public PlayController(ApplicationEventPublisher publisher) {
this.publisher = publisher;
}
@GetMapping(value = "play")
public String play() throws InterruptedException{
Thread.sleep(6000);
return "hey, play with me!";
}
@GetMapping(value = "up")
public String up(){
AvailabilityChangeEvent.publish(publisher,this, ReadinessState.ACCEPTING_TRAFFIC);
return "up";
}
@GetMapping(value = "down")
public String down(){
AvailabilityChangeEvent.publish(publisher,this, ReadinessState.REFUSING_TRAFFIC);
return "down";
}
}
通过 AvailabilityChangeEvent
这个类的 publish 方法,更改自身服务状态。当我们访问 down 接口之后,再次查看 health/readiness
的状态情况,会显示如下内容:OUT_OF_SERVICE
表示离线,不接受请求。
而当我们请求 up 接口后,服务状态又变成了 up,这也就实现了服务下线和上线的功能。
Spring Boot 2.3 支持 JDK 14了,但跟我有啥关系吗,没有。我依然用我的 Java 8。真香。
Spring Boot 2.3发布了 Spring Data Neumann,其中包含许多主要版本和驱动程序升级。此版本还增加了对 R2DBC(Reactive Relational Database Connectivity) 的稳定版本支持。R2DBC 提供了异步编程方式访问数据库的 API,主要是配合开发异步非阻塞式的应用程序使用的。
从中可以看出很大部分内容都是与 Docker 容器技术有关的,比如分层打镜像包、无缝支持 kubernetes 等,可见 docker 微服务已然成为很多开发者的选择。但是仍然有待改进,比如默认的 docker hub 是 Google Cloud,就不能灵活配置,支持国内的镜像仓库不好吗。
你们用的 Spring Boot 哪个版本,会来尝个鲜儿吗?
参考文档:
https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/maven-plugin/reference/html/index.html#goals
https://medium.com/@TimvanBaarsen/whats-new-in-spring-boot-2-3-22d01d036f11