SpringBoot可执行Jar很方便,但相关的背景知识你了解吗

731 阅读4分钟

在 Java 里,如果要运行一个 Java 程序,那么需要一个包含 main 方法 Class 的 Jar 包或者 Class 文件,然后执行命令

# 运行一个class文件
java $class

# 运行一个Java包(Manifest中指定了Main-Class)
java -jar $jarfile

# 运行一个Class的main方法,并且将多个jar包添加到运行时的classpath
java -cp(-classpath) $path(目录/jar文件/zip文件) #zip文件需要符合jar格式的规范

这种方式会有一些弊端,因为大多数的 Java 程序里,会包含很多依赖,如果要启动这个程序就必须要通过-classpath来指定这些依赖包文件,而这些 classpath 的指定还必须得在服务器上去指定,不是很方便

尤其是在 DevOps / 微服务的盛行下,这种指定 classpath 的方式显得太不灵活了,能不能做到直接构建一个聚合的 jar 包,执行发布或者运行呢?

Executable Jar

Executable Jar(可执行的 jar 包),一般是指将所有依赖的 jar 包都放在一个 Jar 包内,这个 Jar 包包含了所有运行时需要依赖的 Jar 包代码,不过并没有规定这个 “放” 的方式,可以是将依赖的 jar 包直接放在入口 jar 包内,就像这样

├─executable jar
│  ├─META-INF
│  │  ├─MANIFEST.MF
│  ├─com...
│  ├─lib
│  │  ├─io.netty....jar
│  │  ├─com.google....jar
│  │  ├─com.github....jar
│  │  ├─org.apache.....jar

也可以是将依赖的 jar 包内的文件给拷贝到入口的 Jar 包内,就像这样:

├─executable jar
│  ├─META-INF
│  │  ├─MANIFEST.MF
│  ├─com...
│  ├─io.netty.... classes
│  ├─com.google.. classes
│  ├─com.github.. classes
│  ├─org.apache.. classes

Spring Boot 算是把 Executable Jar 传到了千家万户,Spring Boot 中提供了一个 Maven 插件spring-boot-maven-plugin,这个插件可以将你所有的 Maven 依赖 Jar 包在构建时打包到一个 jar 文件内,并且通过 Spring Boot 的 ClassLoader 和启动类,可以加载到这些 Executable Jar 包中的 Jar 包,就是上面介绍的第一种方式:将依赖的 jar 包直接放在入口 jar 包内

Uber Jar

第一次看到这个词的时候,一头雾水,不知道这个单词是个什么鬼意思,Uber 打车?

在查阅资料后才知道,Uber Jar 的原单词是Über Jar,是德语单词,可以解释为 "Over",结束的意思,不过在实际的上下文中,翻译为 “一切” 可能更合适。

这个术语最初是由开发人员创造的,他们认为将所有依赖项和自己的代码放入一个 jar 文件中可以解决很多冲突问题。但是大多数输入法上德语Ü很难打出来,所以就成了 "Uber"。

Fat jar

Fat 翻译为肥的、胖的、大的,Fat jar 就很好解释了:一个 “肥胖的” jar,和 Uber jar 表达的含义,指的是包含所有依赖包的 Jar 包

Shade Jar/Shadow Jar

shade机翻为阴影、遮蔽,shade jar 是指将 jar 包及其依赖包打包到一个 jar 文件内,同时提供 shade“遮蔽 / 重命名” 某些依赖包的功能

比如一个 Maven 工程,依赖了很多三方包,但实际打包时你想重命名部分包,这个重命名的过程在这里可以叫 shade

这里详细解释下 “shade”,shade 在这个过程里的目的时重命名某些包,那么为什么要重命名呢?

比如我们基于 JAVA Instrumentation API 去开发一个 Agent,我们在开发这个 Agent 时也需要依赖一些三方包,比如 Netty,那么在实际运行时需要通过 - javaagent 或者动态 attach agent jar 的形式去加载我们这个 Agent jar 包;

这里加载的 Agent 只能是一个独立的 jar 包,所以首先要将我们的 agent 和依赖的包都打到一个 jar 包内,构建一个 "Uber jar";然后需要考虑 Class 包冲突的问题,因为 Agent 内的依赖包的 Class,和目标 JVM 进程中的 Class 很可能是冲突的,比如 Agent 中依赖了 netty 4.1.58.Final ,而目标 JVM 进程中依赖了 netty 4.0.14.Final,而我们的 Agent 中使用了 4.0.14 中不存在的 API(比如某个类的方法,是新版本新增的);此时程序就会出错了,NoSuchMethodError,因为目标进程已经加载了那个 Class,Agent 包中的同名 Class 不会被重复加载

这里解决这种冲突问题的话,有一个简单的方案,在构建 "Uber jar" 时,将依赖的包的包名修改,重定位(relocaiton),比如将io.netty修改为com.github.kongwu.io.netty,同时将 Java 代码中所有的引用都修改为重定位之后的包名。这样,通过修改包名,就完美避开了依赖包 Class 冲突的问题

而上面这个 “重定位” 的行为,就叫 Shade,或者叫 Shadow

Maven 中的 Shade 插件为 maven.apache.org/plugins/mav…,可以将程序打包成一个独立的 Jar 包,包含依赖包

另一款类似的 Maven 插件为 maven.apache.org/plugins/mav…,也可以实现同样的效果

Gradle 中也有类似的插件 imperceptiblethoughts.com/shadow/,功能也很强大,也支持 shade 功能

参考