前言
springboot 自 2.3 版本起,引入了 layered jar 特性,使得在 docker 镜像构建中,将 jar包根据更新频率分为不同的层,节省 docker 构建时间和占用空间,下面我们将介绍如何使用 和 其中原理。
Docker 构建使用
springboot 应用镜像的构建分为两步,编译阶段和运行阶段,Dockerfile 如下:
# 编译 java代码为 jar # 这里的编译镜像是 事先做好的 FROM maven:3.8.1-adoptopenjdk-11 AS builder WORKDIR /usr/src/app COPY pom.xml . RUN mvn -B -s /usr/share/maven/ref/settings.xml dependency:go-offline COPY . . RUN mvn -B -s /usr/share/maven/ref/settings.xml package -DskipTests RUN mv target/*.jar fatjar.jar RUN java -Djarmode=layertools -jar fatjar.jar extract #copy jar 到运行的镜像并启动 FROM openjdk11:jre-11.0.11_9-alpine WORKDIR app COPY --from=builder /usr/src/app/dependencies/ ./ COPY --from=builder /usr/src/app/spring-boot-loader/ ./ COPY --from=builder /usr/src/app/snapshot-dependencies/ ./ COPY --from=builder /usr/src/app/application/ ./ ENV SPRING_PROFILE ${SPRING_PROFILE:-test} ENV JAVA_OPTS -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 ENTRYPOINT [ "/bin/sh", "-c", "java -Dspring.profiles.active=$SPRING_PROFILE $JAVA_OPTS org.springframework.boot.loader.JarLauncher" ]
原理分析
通常呢,我们一个打包后的 jar包,zip 解压后,都会是这样的目录结构:
─BOOT-INF
│ ├─classes
│ │ └─com
│ │ ——–xxxx
│ │ ————-xxxx
│ │ ——————xxxx
│ └─lib
├─META-INF
——–maven
└─org
——-─springframework
—————–boot
—————–loader
—————–archive
—————–data
—————–jar
—————–jarmode
—————–util
其中 META-INF/MANIFEST.MF
文件中 Main-Class: org.springframework.boot.loader.JarLauncher
指定了 启动类。
我们执行jar文件有两种方式:
1. java -jar xxx.jar
2. 解压jar包, java com.xxx.Main
(这里 springboot 应用中是 org.springframework.boot.loader.JarLauncher
)
解压jar 包的环节
java -Djarmode=layertools -jar fatjar.jar extract
这里呢,extract 命令调用了 jar包内的 BOOT-INF/lib/spring-boot-jarmode-layertools-2.6.3.jar
, 使用 spring 的规则 对jar包进行解压(原理请看下面),
这里的解压后的目录结构是:
─application
│ ├─BOOT-INF
│ │ └─classes
│ │——–com
│ │————xxx
│ │—————xxxx
│ └─META-INF
│———-maven
├─dependencies
——–BOOT-INF
————lib
–snapshot-dependencies
——–BOOT-INF
————lib
├─spring-boot-loader
——-org
———-springframework
————-boot
—————loader
—————archive
—————data
—————jar
—————jarmode
—————util
上面四条 COPY 命令 将 denpendency
, spring-boot-loader
, snapshot-dependencies
, application
内的文件分别拷贝到 运行镜像中,
拷贝后的文件 组成了 jar包 zip 解压后的形式,通过 java 启动类 的方式执行。
这四个目录变化频率也是依次变大的,每当应用程序修改,重新构建镜像,可有效利用 docker缓存,减少磁盘占用,减少构建时间。
spring-boot-jarmode-layertools.jar 源码分析
一个打包成jar的springboot 应用,jar 包内都会有两个独特的文件:
BOOT-INF/lib/spring-boot-jarmode-layertools-2.6.3.jar
BOOT-INF/layers.idx
看一下 layer.idx:
- "dependencies": - "BOOT-INF/lib/HdrHistogram-2.1.12.jar" - "BOOT-INF/lib/HikariCP-4.0.3.jar" - "BOOT-INF/lib/LatencyUtils-2.0.3.jar" - "BOOT-INF/lib/activation-1.1.1.jar" - "BOOT-INF/lib/aspectjweaver-1.9.7.jar" - "spring-boot-loader": - "org/" - "snapshot-dependencies": - "BOOT-INF/lib/xxxx-redis-spring-boot-starter-2.5.0-SNAPSHOT.jar" - "BOOT-INF/lib/xxxx-common-1.0.1-SNAPSHOT.jar" - "application": - "BOOT-INF/classes/" - "BOOT-INF/classpath.idx" - "BOOT-INF/layers.idx" - "META-INF/"
layer.idx 描述了 java -Djarmode=layertools -jar xxx.jar extract
后的目录结构,
spring-boot-jarmode-layertools-2.6.3.jar 的代码在 https://github.com/spring-projects/spring-boot/tree/main/spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools
我们大概看下其中几个类:
org.springframework.boot.jarmode.layertools.IndexedLayers
这个类对应了 layer.idx 文件,解析并放入一个 Map, layers 中,
class IndexedLayers implements Layers { private final Map<String, List<String>> layers = new LinkedHashMap<>(); // 解析layer.idx 文件,放入 layers 中备用 IndexedLayers(String indexFile) { String[] lines = Arrays.stream(indexFile.split("\n")).map((line) -> line.replace("\r", "")) .filter(StringUtils::hasText).toArray(String[]::new); List<String> contents = null; for (String line : lines) { if (line.startsWith("- ")) { contents = new ArrayList<>(); this.layers.put(line.substring(3, line.length() - 2), contents); } else if (line.startsWith(" - ")) { contents.add(line.substring(5, line.length() - 1)); } else { throw new IllegalStateException("Layer index file is malformed"); } } Assert.state(!this.layers.isEmpty(), "Empty layer index file loaded"); } .......... }
org.springframework.boot.jarmode.layertools.ExtractCommand
这个类执行解压的过程,读取 jar包遍历其中文件,根据 上面 IndexedLayers 中存的规则分别 保存在不同的目录中
class ExtractCommand extends Command { @Override protected void run(Map<Option, String> options, List<String> parameters) { try { File destination = options.containsKey(DESTINATION_OPTION) ? new File(options.get(DESTINATION_OPTION)) : this.context.getWorkingDir(); for (String layer : this.layers) { // 创建 dependency, application 等 目录 if (parameters.isEmpty() || parameters.contains(layer)) { mkDirs(new File(destination, layer)); } } try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getArchiveFile()))) { ZipEntry entry = zip.getNextEntry(); Assert.state(entry != null, "File '" + this.context.getArchiveFile().toString() + "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled"); while (entry != null) { if (!entry.isDirectory()) { // 找到文件应该保存的目录 String layer = this.layers.getLayer(entry); if (parameters.isEmpty() || parameters.contains(layer)) { // 保存文件 write(zip, entry, new File(destination, layer)); } } entry = zip.getNextEntry(); } } } catch (IOException ex) { throw new IllegalStateException(ex); } } }
其实作用就是对文件进行CV而已。