Java应用上云接入K8S的第一步便是构建镜像,其最简单的方法就是直接将整个jar包打入Docker镜像。除此之外,SpringBoot还提供了一种分层镜像的方式,能够将基本不变的外部依赖包与经常发生变动的应用文件隔离成了不同的Docker层,在二次构建过程中,不变的外部依赖包的Docker层会默认使用缓存,以带来更快的构建速度。

下面将分别介绍Jar包的整体构建分层构建这两种构建Java云原生镜像的方式。

一、准备一个SpringBoot项目

新建Maven工程,在pom.xml中引入web依赖:

<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">
  <modelVersion>4.0.0</modelVersion>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
  </parent>

  <groupId>org.example</groupId>
  <artifactId>app</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

创建入口类:

@SpringBootApplication
@RestController
public class HelloApplication {

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

    @GetMapping("/")
    public String hello() {
        return "Hello World!";
    }

}

运行项目,并验证应用:

mvn spring-boot:run
curl http://localhost:8080

二、Jar包整体打入Docker镜像

新建Dockerfile文件:

FROM eclipse-temurin:17-jre
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
ENTRYPOINT ["java", "-jar" , "application.jar"]

基于Dockerfile构建Docker镜像:

mvn clean package
docker build -t app .

运行容器,并验证应用:

docker run --rm -p 8080:8080 app
curl http://localhost:8080

三、Jar包分层打入Docker镜像

修改Dockerfile文件:

FROM eclipse-temurin:17-jre as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM eclipse-temurin:17-jre
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.launch.JarLauncher"]

这里的java -Djarmode=layertools -jar application.jar extract命令会将application.jar解压成四个目录,并在后续分别COPY到镜像中,以支持分层缓存。

基于新的Dockerfile构建Docker镜像:

mvn clean package
docker build -t app .

在第一次构建时,这种分层方式与前面的方式所需的构建时间其实并没有多大差异,分层镜像的优势在于其在后续重新构建的速度会更快。修改HelloController中的任意内容,重新打包并执行docker的build命令,会发现大部分Docker层都使用了缓存,构建速度也比第一次要快得多。

除了默认的分层方式外,spring-boot-maven-plugin插件也支持自定义的分层,详见Custom Layers Configuration

运行容器,并验证应用:

docker run --rm -p 8080:8080 app
curl http://localhost:8080

四、总结

Jar包整体构建是指将Jar包作为一个整体打入Docker镜像的过程,而Jar包分层构则是指将Jar包的多个部分拆分成不同的层级分别打入Docker镜像的过程,这两种方式各有优劣,最后采取哪种方式,需视情况而定。

在构建Java云原生镜像中,较之Jar包的整体构建,Jar包分层构建在二次构建的速度上会有明显提升,其原理是通过Spring的layertools工具,将基本不变的外部依赖包与经常发生变动的应用文件隔离,并在构建镜像的时候生成不同的Docker层,由于外部依赖等Docker层一般不会发生文件变动,因此能够有效利用缓存,以加快构建速度。

在实际运维开发过程中,Jar包的整体构建也能够快速验证原型实现功能,同时,对Docker运维不太熟悉的开发人员来说,Jar包的整体构建理解起来会相对简单一些,如果应用的外部依赖并不多,或对构建速度不太敏感,采用Jar包整体构建方式也是可以的。

同时,除了本文介绍的方式之外,构建Java云原生镜像还有其它多种方案,如:基于SpringBoot的GraalVM镜像基于Quarkus的镜像等等,具体详见后文。

参考文档

  1. spring-boot container-images
  2. spring-boot maven-plugin packaging-layers