近来收到一些反馈:使用 maven 编译 Java 工程,如何保留本地 repository 缓存,避免每次构建都重新下载所有依赖包,毕竟这很耗时。
实际上,构建工具(docker/buildkit 等)在构建过程中是没办法直接挂载本地目录到系统的,所以构建系统也没办法通过为用户创建缓存来复用依赖包。但是,可以利用容器镜像构建缓存机制来复用 Java 依赖包缓存。
原始 Dockerfile
以一个 Java Hello World 工程为例,Dockerfile 内定义了一个两阶段构建,首次构建耗时 110s,且后续构建也无法利用前次已经下载依赖包缓存。
# First stage: complete build environment
FROM maven:3.5.0-jdk-8-alpine AS builder
# add pom.xml and source code
ADD ./pom.xml pom.xml
ADD ./src src/
# package jar
RUN mvn install -Dmaven.test.skip=true
From openjdk:8
# copy jar from the first stage
COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar
EXPOSE 8080
CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"]
优化和遇到的问题
优化思路是将项目包下载、打包过程划分开,先拷贝工程 pom.xml 并下载所有的依赖包,再拷贝工程源代码并打包项目,下文给了一个改写方案。
使用此 Dockerfile 首次构建耗时在 240s,且惊奇的发现两次 mvn install 过程中,第二次依然需要下载所有依赖包,无法复用第一次的结果。更改项目代码,再次构建镜像也没办法利用到前次构建的缓存。
# First stage: complete build environment
FROM maven:3.5.0-jdk-8-alpine AS builder
# download dependencies (no re-download when the source code changes)
ADD ./pom.xml pom.xml
RUN mvn install
ADD ./src src/
# package jar
RUN mvn install -Dmaven.test.skip=true
From openjdk:8
# copy jar from the first stage
COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar
EXPOSE 8080
CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"]
使用 mvn 下载依赖包的命令有很多,例如:mvn install、dependency:go-offline 等。
起初怀疑是 maven 的问题,但是直接在本地运行并进入基础镜像 maven:3.5.0-jdk-8-alpine,手动执行 Dockerfile 内的所有命令,发现第二次执行 mvn install 是可以利用到第一次的依赖包缓存的。
mvn install 命令默认将依赖包下载到 ~/.m2 目录(即镜像内的 /root/.m2)下,而对于 Dockerfile 内的每个 RUN ,构建工具都会启动新容器来执行命令,生成新的镜像层。猜测是启动容器时 /root/.m2 目录被清理了,所以才导致缓存失效,这应该与基础镜像 maven:3.5.0-jdk-8-alpine 有关。
查看 maven:3.5.0-jdk-8-alpine 的镜像配置,发现 /root/.m2 目录被定义成 Volume 了。
查看官方文档中对 Volume 的说明可以知道在构建过程中,所有被写入卷目录的内容在后续构建过程中都会被清理,这也就是缓存无法被利用到的原因。
最终版本
为了避开默认 /root/.m2 目录,使用 -Dmaven.repo.local 来显示指定本地 maven 仓库目录。首次构建耗时 115s,后续构建耗时在 10s 左右,复用了依赖包缓存,耗时降低了 91%。
# First stage: complete build environment
FROM maven:3.5.0-jdk-8-alpine AS builder
# To resolve dependencies in a safe way (no re-download when the source code changes)
ADD ./pom.xml pom.xml
RUN mvn install -Dmaven.repo.local=./.m2
ADD ./src src/
# package jar
RUN mvn -Dmaven.repo.local=./.m2 install -Dmaven.test.skip=true
From openjdk:8
# copy jar from the first stage
COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar
EXPOSE 8080
CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"]