springboot项目docker瘦身
正常打包项目
给 Spring Boot 打包大家应该很熟了吧,只需要在 pom.xml 文件中配置 spring-boot-maven-plugin 打包插件:
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>然后在项目根目录执行 mvn clean pavkage 就可以完成打包了,如下是我本地的一个项目打包情况:
应用瘦身(分离lib和配置文件)
其实 jar 包大的原因在于所有的依赖包全部集成在 jar 包里面,如下是瘦身前的 jar 包内部结构:
其中 classes 就是我们项目的代码,仅仅1.3M,而 129MB 的 lib 目录是项目中所有的依赖(比如spinrg、Hibernate等依赖),如果我们能把这个 lib 目录提取出来,整个项目就会变得特别小了。说干就干。
我们知道 Spring Boot 的打包终究是依赖于 Maven ,所以想到更改打包信息,无非就是指定 Maven 的配置。
在 pom.xml 添加如下信息(后文解释):
<build>
<finalName>你想要的jar包名称</finalName>
<plugins>
<!-- 1、编译出不带 lib 文件夹的Jar包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--表示编译版本配置有效-->
<fork>true</fork>
<!--引入第三方jar包时,不添加则引入的第三方jar不会被打入jar包中-->
<includeSystemScope>true</includeSystemScope>
<!--排除第三方jar文件-->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 2、完成对Java代码的编译,可以指定项目源码的jdk版本,编译后的jdk版本,以及编码 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<!-- 源代码使用的JDK版本 -->
<source>${java.version}</source>
<!-- 需要生成的目标class文件的编译版本 -->
<target>${java.version}</target>
<!-- 字符集编码 -->
<encoding>UTF-8</encoding>
<!-- 用来传递编译器自身不包含但是却支持的参数选项 -->
<compilerArguments>
<verbose/>
<!-- windwos环境(二选一) -->
<bootclasspath>${java.home}/lib/rt.jar:${java.home}/lib/jce.jar</bootclasspath>
<!-- Linux环境(二选一) -->
<bootclasspath>${java.home}/lib/rt.jar:${java.home}/lib/jce.jar</bootclasspath>
</compilerArguments>
</configuration>
</plugin>
<!-- 3、将所有依赖的jar文件复制到target/lib目录 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-dependencies</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<!--复制到哪个路径,${project.build.directory} 缺醒为 target,其他内置参数见下面解释-->
<outputDirectory>${project.build.directory}/lib</outputDirectory>
<overWriteReleases>false</overWriteReleases>
<overWriteSnapshots>false</overWriteSnapshots>
<overWriteIfNewer>true</overWriteIfNewer>
</configuration>
</execution>
</executions>
</plugin>
<!-- 4、指定启动类,指定配置文件,将依赖打成外部jar包 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<!-- 是否要把第三方jar加入到类构建路径 -->
<addClasspath>true</addClasspath>
<!-- 外部依赖jar包的最终位置 -->
<classpathPrefix>lib/</classpathPrefix>
<!-- 项目启动类 -->
<mainClass>com.javam4.MyApplication</mainClass>
</manifest>
</archive>
<!--资源文件不打进jar包中,做到配置跟项目分离的效果-->
<excludes>
<!-- 业务jar中过滤application.properties/yml文件,在jar包外控制 -->
<exclude>*.properties</exclude>
<exclude>*.xml</exclude>
<exclude>*.yml</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>如下一一细拆如上配置:
1、spring-boot-maven-plugin
Springboot 默认使用 spring-boot-maven-plugin 来打包,这个插件会将项目所有的依赖打入项目 jar 包里面,正常打包时 spring-boot-maven-plugin 结构如下:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<mainClass>com.javam4.MyApplication</mainClass>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>如下是提取的修改项:
<configuration>
<!--表示编译版本配置有效-->
<fork>true</fork>
<!--引入第三方jar包时,不添加则引入的第三方jar不会被打入jar包中-->
<includeSystemScope>true</includeSystemScope>
<!--排除第三方jar文件-->
<includes>
<include>
<groupId>nothing</groupId>
<artifactId>nothing</artifactId>
</include>
</includes>
</configuration>修改的作用:
- includeSystemScope:jar包分两种,一种是spring、mybatis等这种项目依赖的,再就是我们外部手动引入的第三方 jar 依赖,如果该参数不设置为 true 的话是不能被打包进来的~
- includes:这个节点就是排除项目中所有的 jar,那还怎么打包?
其实我们需要将打包插件替换为 maven-jar-plugin,然后使用该插件拷贝依赖到 jar 到外面的 lib 目录。
2、maven-xxx-plugin
从 2、3、4 你会发现用到了 maven-xxx-plugin 格式的三种插件,简单说一下这三者的作用:
maven-compiler-plugin:
完成对Java代码的编译,可以指定项目源码的jdk版本、编译后的jdk版本,以及编码,如果不写这个插件也是没问题的,不写会使用默认的 jdk 版本来处理,只是这样容易出现版本不匹配的问题,比如本地maven环境用的3.3.9版本,默认会使用jdk1.5进行编译,而项目中用的jdk1.8的编译环境,那就会导致打包时编译不通过。
maven-dependency-plugin:
作用就是将所有依赖的jar文件复制到指定目录下,其中涉及到的
${project.xx}见下文补充。maven-jar-plugin:
主要作用就是将maven工程打包成jar包。
主要说一下 maven-jar-plugin 插件的如下配置:
<configuration>
<!--资源文件不打进jar包中,做到配置跟项目分离的效果-->
<excludes>
<!-- 业务jar中过滤application.properties/yml文件,在jar包外控制 -->
<exclude>*.properties</exclude>
<exclude>*.xml</exclude>
<exclude>*.yml</exclude>
</excludes>
</configuration>打包时排除资源配置文件,如果排除了配置文件那么项目启动是怎么读取呢?
配置文件有这么一个默认的优先级:
当前项目config目录下 > 当前项目根目录下 > 类路径config目录下 > 类路径根目录下
因此只需要将配置文件复制一份到与 jar 包平级目录下,或者与jar包平行config目录下,就能优先使用此配置文件,达到了伪分离目的。
最终的目录结构如下:
Maven 中的内置变量说明:
${basedir}项目根目录${project.build.directory}构建目录,缺省为target${project.build.outputDirectory}构建过程输出目录,缺省为target/classes${project.build.finalName}产出物名称,缺省为{project.artifactId}-$${project.packaging}打包类型,缺省为jar${project.packaging}打包类型,缺省为jar${project.xxx}当前pom文件的任意节点的内容
瘦身总结
Spring Boot 框架提供了一套自己的打包机制 — spring-boot-maven-plugin,Springboot 默认使用该插件来打包,打包时会将项目所有的依赖打入项目 jar 包里面,如果我们想要抽离依赖的 jar 仅仅使用该插件是不行的,就需要将打包插件替换为 maven-jar-plugin,并拷贝所有的依赖到 jar 外面的 lib 目录。
项目打包时,在分离依赖 jar 包基础上,我们又排除了配置文件,因为配置文件有一个默认的读取路径:
当前项目config目录下 > 当前项目根目录下 > 类路径config目录下 > 类路径根目录下
我们只需要在当前项目 jar 包同级目录创建一个 config 文件夹,然后将配置文件复制一份,这样就达到了伪分离目的。
之后再修改配置文件,比如端口号、数据库连接信息等,就不需要重新打包项目了,直接修改完配置文件重启项目就可以了。
而经过分离依赖后的 jar 包从原来的100多兆到现在的1兆,如果后面需要变更业务逻辑,只需要轻量的编译项目,快速的实现项目的上传替换,有效的减少了网络开销,提高项目部署的效率。
java -jar -Dloader.path="/lib" xxx-.jarDocker镜像优化
解决在生产环境下,网速带宽小,每次推拉镜像影响线上服务问题,按本文方式构建镜像,除了第一次拉取、推送、构建镜像慢,第二、三…次都是几百K大小传输,速度非常快,构建、打包、推送几秒内完成。 注意,SpringBoot 2.3.x 已经默认支持分层功能,请参考:java SpringBoot 2.3.x 分层构建 Docker 镜像
Spring Boot Layer工具:分离依赖
虽然多阶段构建减小了镜像体积,但Spring Boot的Layer工具通过将依赖与应用程序代码分离进一步优化了缓存,最小化开发过程中的重建。layertools模式将JAR分为:
- dependencies:已发布的库(很少变化)
- spring-boot-loader:Spring内部组件
- snapshot-dependencies:SNAPSHOT库(偶尔变化)
- application:您的代码(频繁变化)
开启分层打包
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<layers>
<enabled>true</enabled>
</layers>
</configuration>
</plugin>编写支持分层 Dockerfile
核心是通过 spring boot 提供的 layertools 工具,将 jar 进行拆分 然后通过 COPY 指令去分别加载
FROM adoptopenjdk:11-jre-hotspot as builder
WORKDIR application
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
#从打包好的jar包中解压Layer工具的分层输出
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"]默认分层虽然实用,但还不够精细。比如,所有依赖被打包进一个层,即使只是内部库有变更,也得重建整个依赖层
https://cloud.tencent.com/developer/article/2363863
https://www.cnblogs.com/niceyoo/p/14083706.html
https://zhuanlan.zhihu.com/p/158738762
https://www.cnblogs.com/hahaha111122222/p/13807767.html
https://spring.didispace.com/article/20250517-spring-boot-docker-layer-optimizations.html