简介

Spring Boot 2.3以前,我们创建Spring Boot - docker image最通用的办法就是将Spring boot的应用程序打包成一个fat jar,然后写一个Dockerfile,将这个fat jar制作成为一个docker image然后运行。

Spring Boot 2.3发布后,附带了快速创建docker image的功能

传统做法和它的缺点

首先创建一个非常简单的Spring Boot程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
@SpringBootApplication
@RestController
public class Application {

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

@GetMapping("/getInfo")
public String getInfo() {
return "www.flydean.com";
}
}

默认情况下,我们build出来的是一个fat jar:springboot-with-docker-0.0.1-SNAPSHOT.jar

以下是这个fat jar解压后的内容:

Spring boot的fat jar分为三个部分:

  • 第一部分就是BOOT-INF, 里面的class目录放的是我们自己编写的class文件。而lib目录存放的是项目依赖的其他jar包。

  • 第二部分是META-INF,里面定义了jar包的属性信息。

  • 第三部分是Spring Boot的类加载器,fat jar包的启动是通过Spring Boot的jarLauncher来创建LaunchedURLClassLoader,通过它来加载lib下面的jar包,最后以一个新线程启动应用的Main函数。

如果想要用这个fat jar来创建docker image,可以使用如下Dockerfile

1
2
3
4
5
FROM openjdk:8-jdk-alpine
EXPOSE 8080
ARG JAR_FILE=target/springboot-with-docker-0.0.1-SNAPSHOT.jar
ADD ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

缺点

这样写有两个问题:

  • 第一个问题:我们是用的far jar,在使用far jar的过程中会有一定的性能问题,肯定要比解压过后的性能要低,尤其是在容器环境中运行的情况下,可能会更加突出。

  • 第二个问题:我们知道docker的image是按layer来构建的,按layer构建的好处就是可以减少image构建的时间和重用之前的layer。

但是如果使用的是fat jar包,即使我们只修改了我们自己的代码,也会导致整个fat jar重新更新,从而影响docker image的构建速度。

使用Buildpacks

Spring Boot在2.3.0之后,引入了Cloud Native 的buildpacks,通过这个工具,我们可以非常非常方便的创建docker image。

在Maven和Gradle中,Spring Boot引入了新的phase:spring-boot:build-image

只需要引入此plugin:spring-boot-maven-plugin,就可以直接运行以下指令来构建镜像:

在早些版本时,会默认从gcr.io中拉取镜像,但目前3.x则是从docker.io拉取

1
mvn spring-boot:build-image

构建过程中会看到类似如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[INFO] --- spring-boot-maven-plugin:3.1.2:build-image (default-cli) @ behappy-common-core ---
[INFO] Building image 'docker.io/wangxiaowu950330/behappy-gulimall/behappy-member:latest'
[INFO]
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 16%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 28%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 30%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 30%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 35%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 47%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 56%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 68%
[INFO] > Pulling builder image 'docker.io/paketobuildpacks/builder:tiny' 100%
[INFO] > Pulled builder image 'paketobuildpacks/builder@sha256:1a59354925fcb7ba54744b8017630c97c2b035e1a9e19309330557b9c66bfc2c'
[INFO] > Pulling run image 'docker.io/paketobuildpacks/run:tiny-cnb' 100%
[INFO] > Pulled run image 'paketobuildpacks/run@sha256:adf913cf28031f2090aeaedac65edb36f2987d81a23a8dffab5ea18ca216c94c'

Layered Jars

如果你不想使用Cloud Native Buildpacks,还是想使用传统的Dockerfile的话,SpringBoot也为我们提供了独特的分层jar包系统。

我们需要在POM文件中加上下面的配置,即可开启:

1
2
3
4
5
6
7
8
9
10
11
12
13
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>
</plugins>
</build>

再次打包,可以看到jar包内的内容和之前的jar包大致相同,但多了一个layers.idx 这个index文件:

layers分层

1
2
3
4
5
6
7
8
9
10
- "dependencies":
- "BOOT-INF/lib/"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"

index文件主要分为4个部分:

  • dependencies - 非SNAPSHOT的依赖jar包
  • snapshot-dependencies - SNAPSHOT的依赖jar包
  • spring-boot-loader - Spring boot的class loader文件
  • application - 应用程序的class和resources文件

注意,这里的index文件是有顺序的,它和我们将要添加到docker image中的layer顺序是一致的。

最少变化的将会最先添加到layer中,变动最大的放在最后面的layer。

我们可以使用layertools jarmode来对生成的fat jar进行校验或者解压缩:

1
2
3
4
5
6
7
8
java -Djarmode=layertools -jar springboot-with-docker-0.0.1-SNAPSHOT.jar 
Usage:
java -Djarmode=layertools -jar springboot-with-docker-0.0.1-SNAPSHOT.jar

Available commands:
list List layers from the jar that can be extracted
extract Extracts layers from the jar for image creation
help Help about any command

使用list命令,可列出jar包中的layer信息。使用extract我们可以解压出不同的layer。

我们执行下extract命令,看下结果:

可以看到,我们根据layers.idx解压出了不同的文件夹。

所以开启了layer的DockerFile可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM adoptopenjdk:11-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

这样我们的一个分层的DockerImage就创建完成了。

自定义Layer

如果我们需要自定义Layer,可以创建一个独立的layers.xml文件:

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
<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="company-dependencies">
<include>com.flydean:*</include>
</into>
<into layer="dependencies"/>
</dependencies>
<layerOrder>
<layer>dependencies</layer>
<layer>spring-boot-loader</layer>
<layer>snapshot-dependencies</layer>
<layer>company-dependencies</layer>
<layer>application</layer>
</layerOrder>
</layers>

然后添加到build plugin中就可以了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
<configuration>${project.basedir}/src/main/resources/layers.xml</configuration>
</layers>
</configuration>
</plugin>
</plugins>
</build>