SpringBoot胖jar转瘦jar

1 需求的提出

原来的项目中的jar包大约50MB,但是jar包是分散在不同客户方使用的。每次版本升级jar都需要下载50MB,导致每次升级下载都很慢,所以体验很差。

所以呢,既然大多数依赖包都不会被修改,那么能否缩减一下jar的容量,使得每次下载只下载修改的那部分jar呢?这样升级下载速度会有很大提升。

2 解决

胖jar:fat jar,指spring boot打包出的可执行jar,包含很多依赖,所以包比较胖大

瘦jar:thin jar,指spring boot打包时,可以考虑不打包依赖,把依赖从外部加载,这样打出的jar就比较小,称为瘦jar

2.1 解决思路

  • 将外部依赖排除掉,只需要打包需要的依赖(比如容易改动的公司内部模块)
  • 将需要外部引用的依赖提取出来
  • 调整启动逻辑,使得启动的时候能够读取外部依赖,也能读取jar包中的依赖

2.2 解决步骤

2.2.1 调整spring-boot-maven-plugin

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <!-- 指定SpringBoot主类 -->
        <mainClass>com.xxx.XXXApplication</mainClass>
    <!-- 设置布局 -->
    <layout>ZIP</layout>
    <!-- 
            指定需要包含的依赖,其它的依赖都不会包含。
            下面的都是项目内部模块的依赖,所以全部进行引入 。
        -->
    <includes>
      <include>
        <groupId>com.xxx.xxx</groupId>
        <artifactId>gsp-xxx1</artifactId>
      </include>
      <include>
        <groupId>com.xxx.xxx</groupId>
        <artifactId>gsp-xxx2</artifactId>
      </include>
      <include>
        <groupId>com.xxx.xxx</groupId>
        <artifactId>gsp-xxx2</artifactId>
      </include>
    </includes>
  </configuration>
  <executions>
    <execution>
      <goals>
        <goal>repackage</goal>
      </goals>
    </execution>
  </executions>
</plugin>

LAYOUT的可选值如下:

  • JAR: 可执行jar
  • WAR: 可执行war, provided依赖放在WEB-INF/lib-provided目录中,以防止war部署在servlet容器会崩溃
  • ZIP (alias to DIR): 和JAR布局相似,不同的是可以使用外部配置,而不是JAR布局中的默认配置。
  • NONE: 打包所有依赖和项目资源,不打包启动加载器(指spring boot loader代码)

通过这种配置方式,原有的jar大小58MB,修改后的jar仅为579KB。

SpringBoot胖jar转瘦jar

打包后包含的依赖包:

SpringBoot胖jar转瘦jar

如需排除所有依赖jar,则使用:

<groupId>nothing</groupId>
<artifactId>nothing</artifactId>

2.2.2 提取外部依赖

现在把外部依赖都提取出来,放到与当前执行jar包相同目录的lib文件夹下。那么如何提取依赖呢?使用maven依赖插件的copy-dependencies目标来实现。

# outputDirectory:表示拷贝的依赖放在当前目录的lib文件夹中
# includeScope:需要拷贝的依赖的范围是runtime
# excludeGroupIds:需要排除的依赖,其实就上面引入的那些项目内部依赖
mvn dependency:copy-dependencies -DoutputDirectory=lib -DincludeScope=runtime -DexcludeGroupIds=com.xxx.xxx

2.2.3 添加loader.properties

既然现在已经打的小jar包有了,外部依赖也有了。那么就剩下如何加载外部依赖包了。

SpringBoot默认的依赖包加载加载路径是当前jar包中的BOOT-INF/lib/目录,而我们现在还需要从lib文件夹中读取。所以只需要在loader.properties(放在classpath下,比如src/main/resources)中配置加载路径即可:

# 表示优先从当前jar目录的lib文件夹中读取,然后再从BOOT-INF/lib/中读取
# 注意:loader.path也可以在命令行启动的时候设置,即添加-Dloader.path=lib/,BOOT-INF/lib/
loader.path=lib/,BOOT-INF/lib/

打包后的loader.properties文件位置如下:

SpringBoot胖jar转瘦jar

注意:逗号分隔的顺序好像有问题,如果把lib/放在后面,启动就会报错,报错看到貌似没有正常读取到默认的yml配置。

2.2.4 项目运行

直接按照以前启动的jar的方式启动即可:

java -jar gsp-client-service.jar

3 扩展学习

3.1 可执行jar是靠什么支撑实现的?

使用spring-boot-loader模块使得Spring Boot支持可执行jar和war。

如果在项目中使用Maven(spring-boot-maven-plugin)或者Gradle插件,那么可执行jar将会自动生成。

怎么知道spring-boot-loader会影响可执行jar?在可执行jar中可以发现spring-boot-loader的源码

SpringBoot胖jar转瘦jar

3.2 什么是嵌套jar(nested jar)?

嵌套jar,指的是jar中包含jar。Java本身并没有提供一种标准方式来加载内嵌的jar文件。所以就有这样的问题,很难直接通过命令行直接运行一个自给的应用。

为了解决这个问题,很多开发者使用shaded jar,这种jar会把所有jar中的class都打包到一个uber jar中。这样也会有问题,很难知道哪些代码是自己写的代码,哪些是依赖代码。当然还有可能会文件名冲突(多个jar中的文件名称一样的情况)。Spring Boot则采取了另外一种方式,可以让我们直接内嵌jar。

3.3 可执行jar的工作原理

3.3.1 核心启动类Launcher

可执行jar的核心类是org.springframework.boot.loader.Launcher,这是一个特别的启动类,作为可执行jar的主入口。其有三个子类:

  • JarLauncher:启动可执行jar,从BOOT-INF/lib/读取外部依赖
  • WarLauncher:启动可执行war,从WEB-INF/lib/和WEB-INF/lib-provided/读取外部依赖
  • PropertiesLauncher:和JarLauncher类似,从配置中读取主类和classpath。默认从BOOT-INF/lib/读取外部依赖,也可以指定配置路径来加载外部依赖(使用环境变量LOADER_PATH 、命令行指定loader.path、loader.properties(放在classpath下)中指定loader.path方式均可指定外部依赖位置)。

SpringBoot胖jar转瘦jar

3.3.2 可执行jar/war文件结构

上面就知道要从哪些地方加载依赖,现在看下可执行jar/war的文件结构:

# 可执行jar
# 自定义代码:BOOT-INF/classes
# 第三方依赖:BOOT-INF/lib
example.jar
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-BOOT-INF
    +-classes
    |  +-mycompany
    |     +-project
    |        +-YourClasses.class
    +-lib
       +-dependency1.jar
       +-dependency2.jar
   
# 可执行war
# 第三方依赖:WEB-INF/lib
# 运行时依赖:WEB-INF/lib-provider,比如servlet-api.jar。
example.war
 |
 +-META-INF
 |  +-MANIFEST.MF
 +-org
 |  +-springframework
 |     +-boot
 |        +-loader
 |           +-<spring boot loader classes>
 +-WEB-INF
    +-classes
    |  +-com
    |     +-mycompany
    |        +-project
    |           +-YourClasses.class
    +-lib
    |  +-dependency1.jar
    |  +-dependency2.jar
    +-lib-provided
       +-servlet-api.jar
       +-dependency3.jar

3.3.3 启动原理

java -jar XXX.jar启动原理:通过启动XXX.jar中META-INF/MANIFEST.MF文件中的Main-Class指定的包含main()方法的类来实现的。

Spring Boot Loader也做了对应的处理,其Main-Class会指定为JarLauncher、WarLauncher和PropertiesLauncher,jar中实际要执行的主类使用Start-Class来指定。典型的MANIFEST.MF文件如下:

# 可执行jar
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.mycompany.project.MyApplication

# 可执行war
Main-Class: org.springframework.boot.loader.WarLauncher
Start-Class: com.mycompany.project.MyApplication

# PropertiesLauncher
Main-Class: org.springframework.boot.loader.PropertiesLauncher
Start-Class: com.example.demo.DemoApplication
# PropertiesLauncher真实案例
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.5.0
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.PropertiesLauncher

3.3.4 内嵌jar中的class是如何加载的?通过Spring Boot Loader的JarFile类

支持加载内嵌jar的核心类是org.springframework.boot.loader.jar.JarFile。当第一次加载完后,每一个JarEntry的位置都会映射到外部jar物理文件的偏移量上,就像下面这样:

myapp.jar
+-------------------+-------------------------+
| /BOOT-INF/classes | /BOOT-INF/lib/mylib.jar |
|+-----------------+||+-----------+----------+|
||     A.class      |||  B.class  |  C.class ||
|+-----------------+||+-----------+----------+|
+-------------------+-------------------------+
 ^                    ^           ^
 0063                 3452        3980

A.class是通过位置0063找到的,B.class是通过3452,C.class位于3980。通过这个信息,就可以在内前加中找到相应的类,且不需要解压这个jar,也不需要把jar中的所有内容都读入内存。

org.springframework.boot.loader.jar.JarFile继承自java.util.jar.JarFile,保持了与已有的代码的兼容性。getURL()方法返回的URL打开的连接与java.net.JarURLConnection兼容并能被Java的URLClassLoader所使用。

3.3.5 PropertiesLauncher的相关配置属性

PropertiesLauncher 有一些特别的特性,可以通过设置外部属性(系统属性、环境变量、manifest entries或者loader.properties)来启用。相关属性如下:

Key

Manifest Entry

环境变量

作用

loader.path

Loader-Path

LOADER_PATH

逗号分隔的Classpath,比如lib,${HOME}/app/lib。前面的优先加载,这里lib就会先加载,${HOME}/app/lib其后加载。

loader.home

Loader-Home

LOADER_HOME

用于解析loader.path的home位置。比如loader.path=lib,则classpath路径就是${loader.home}/lib。

loader.args

Loader-Args

LOADER-ARGS

给main方法的默认参数,空格分隔

loader.main

Start-Class

LOADER_MAIN

要启动的main类,比如com.app.Application

loader.config.name

-

-

启动加载的配置文件名,默认是loader。所以加载的是loader.properties。

loader.config.location

Loader-Config-Location

LOADER_CONFIG_LOCATION

配置文件的路径位置,默认是loader.properties。

loader.system

Loader-System

LOADER_SYSTEM

一个Boolean标记位,标记是否所有的属性都会被添加到系统属性中去。默认false。

使用插件构建胖jar时会自动将Main-Class属性值移动到Start-Class上去。也就是说,Main-Class上原本是启动类,然后移动到了Start-Class,而Main-Class值被Spring Boot Loader中的启动器类所代替。

其它说明:

  • loader.properties的查找顺序:先loader.home里找,再classpath的根目录,再classpath:/BOOT-INF/classed
  • 仅当loader.config.location没有指定时,loader.home才会生效
  • loader.path的值:目录(会递归扫描jar和zip文件)、压缩文件路径(可以是相对于loader.home的路径或者以以jar:file:为前缀的文件系统路径)、jar包中的目录(比如xxx.jar!/lib)、通配符正则表达式(默认JVM的行为)
  • PropertiesLauncher和JarLauncher的区别:如果loader.path配置为空,那么就使用默认值BOOT-INF/lib。则此时PropertiesLauncher和JarLauncher是等价的
  • loader.path不能用于配置loader.properyties的位置
  • 所有placeholder的替换会在值使用前做好替换
  • 查找属性的顺序:环境变量、系统属性、loader.properties、解压后的manifest文件、和压缩包中的manifest文件

3.3.6 可执行jar有什么限制?

有如下限制:

  • Zip Entry压缩问题:对于内嵌jar必须使用ZipEntry.STORED方法保存,这样就能直接在内嵌jar中查询内容。
  • System ClassLoader:启动的应用去加载字节码时应该使用Thread.getContextClassLoader()(大多数包和框架默认用的都是这个)。如果使用ClassLoader.getSystemClassLoader()加载内嵌jar中的class文件,就会失败,java.util.Logging用的就是这个类加载器,所以你可能需要换一种日志实现。

考虑到SpringBoot可执行jar有这样的限制,还可以考虑使用如下替代方案:

3.3.7 解压后运行可执行程序

有的PaaS服务会先解压jar,然后再运行。比如:

unzip -q myapp.jar
java org.springframework.boot.loader.JarLauncher

参考

1.[spring boot可执行jar](https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#executable-jar)

2.[把SpringBoot项目从18.18M瘦身到0.18M,部署起来真省事!](https://zhuanlan.zhihu.com/p/136645768)

3.[给你的SpringBoot工程打的jar包瘦瘦身](https://blog.****.net/w1014074794/article/details/106445145/)

4.[maven依赖插件](http://maven.apache.org/plugins/maven-dependency-plugin/copy-dependencies-mojo.html)

5.[spring-boot-maven-plugin](https://docs.spring.io/spring-boot/docs/current/maven-plugin/reference/htmlsingle/)

6.[spring-boot-maven-plugin的用法](https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/maven-plugin/usage.html)

 

上一篇:Codeforces Round #726 (Div. 2) A. Arithmetic Array


下一篇:Floyd算法图解(内附核心代码)