5

在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 插件为https://maven.apache.org/plugins/maven-shade-plugin/,可以将程序打包成一个独立的Jar包,包含依赖包

另一款类似的Maven插件为https://maven.apache.org/plugins/maven-assembly-plugin/usage.html,也可以实现同样的效果

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

参考


空无
3.3k 声望4.3k 粉丝

坚持原创,专注分享 JAVA、网络、IO、JVM、GC 等技术干货