在函數計算(Aliyun FC)中發布一個 Java 函數,往往需要將函數打包成一個 all-in-one 的 zip 包或者 jar 包。Java 中這種打包 all-in-one 的技術常稱之為 Fatjar 技術。本文小結一下 Java 里打包 FatJar 的若干種方法。
什么是 FatJar
FatJar 又稱作 uber-Jar,是包含所有依賴的 Jar 包。Jar 包中嵌入了除 java 虛擬機以外的所有依賴。我們知道 Java 的依賴分為兩種, 零散的 .class 文件和把多個 .class 文件以 zip 格式打包而成 jar 文件。FatJar 是一個 all-in-one Jar 包。FatJar 技術可以讓那些用于最終發布的 Jar 便于部署和運行。
三種打包方法
我們知道 .java 源碼文件會被編譯器編譯成字節碼.class 文件。Java 虛擬機執行的是 .class 文件。一個 java 程序可以有很多個 .class文件。這些 .class 文件可以由 java 虛擬機的類裝載器運行期裝載到內存里。java 虛擬機可以從某個目錄裝載所有的 .class 文件,但是這些零散的.class 文件并不便于分發。所有 java 支持把零散的.class 文件打包成 zip 格式的 .jar 文件,并且虛擬機的類裝載器支持直接裝載 .jar 文件。
一個正常的 java 程序會有若干個.class 文件和所依賴的第三方庫的 jar 文件組成。
1. 非遮蔽方法(Unshaded)
非遮蔽是相對于遮蔽而說的,可以理解為一種樸素的辦法。解壓所有 jar 文件,再重新打包成一個新的單獨的 jar 文件。
借助 Maven Assembly Plugin 都可以輕松實現非遮蔽方法的打包。
Maven Assembly Plugin
Maven Assembly Plugin 是一個打包聚合插件,其主要功能是把項目的編譯輸出協同依賴,模塊,文檔和其他文件打包成一個獨立的發布包。使用描述符(descriptor)來配置需要打包的物料組合。并預定義了常用的描述符,可供直接使用。
預定義描述符如下
- bin 只打包編譯結果,并包含 README, LICENSE 和 NOTICE 文件,輸出文件格式為 tar.gz, tar.bz2 和 zip。
- jar-with-dependencies 打包編譯結果,并帶上所有的依賴,如果依賴的是 jar 包,jar 包會被解壓開,平鋪到最終的 uber-jar 里去。輸出格式為 jar。
- src 打包源碼文件。輸出格式為 tar.gz, tar.bz2 和 zip。
- project 打包整個項目,除了部署輸出目錄 target 以外的所有文件和目錄都會被打包。輸出格式為 tar.gz, tar.bz2 和 zip。
除了預定義的描述符,用戶也可以指定描述符,以滿足不同的打包需求。
打包成 uber-jar,需要使用預定義的 jar-with-dependencies 描述符:
在 pom.xml 中加入如下配置
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-assembly-plugin</artifactId><version>CHOOSE LATEST VERSION HERE</version><configuration><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs></configuration><executions><execution><id>assemble-all</id><phase>package</phase><goals><goal>single</goal></goals></execution></executions>
</plugin>
Gradle Java plugin
gradle 下打包一個非遮蔽的 jar 包,有不少插件可以用,但是由于 gradle 自身的靈活性,可以直接用 groove 的 dsl 實現。
apply plugin: 'java'jar {from {(configurations.runtime).collect {it.isDirectory() ? it : zipTree(it)}}
}
非遮蔽方法會把所有的 jar 包里的文件都解壓到一個目錄里,然后在打包同一個 fatjar 中。對于復雜應用很可能會碰到同名類相互覆蓋問題。
2. 遮蔽方法(Shaded)
遮蔽方法會把依賴包里的類路徑進行修改到某個子路徑下,這樣可以一定程度上避免同名類相互覆蓋的問題。最終發布的 jar 也不會帶入傳遞依賴沖突問題給下游。
Maven Shade Plugin
在 pom.xml 中加入如下配置
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-shade-plugin</artifactId><version>3.1.1</version><configuration><!-- put your configurations here --></configuration><executions><execution><phase>package</phase><goals><goal>shade</goal></goals></execution></executions></plugin>
Gradle Shadow plugin
Gradle shadow plugin 使用非常簡單,簡單聲明插件后就可以生效。
plugins {id 'com.github.johnrengelman.shadow' version '2.0.4'id 'java'
}shadowJar {include '*.jar'include '*.properties'exclude 'a2.properties'
}
遮蔽方法依賴修改 class 的字節碼,更新依賴文件的包路徑達到規避同名同包類沖突的問題,但是改名也會帶來其他問題,比如代碼中使用 Class.forName 或 ClassLoader.loadClass 裝載的類,Shade Plugin 是感知不到的。同名文件覆蓋問題也沒法杜絕,比如
META-INF/services/javax.script.ScriptEngineFactory
不屬于類文件,但是被覆蓋后會出現問題。
3. 嵌套方法(Jar of Jars)
還是一種辦法就是在 jar 包里嵌套其他 jar,這個方法可以徹底避免解壓同名覆蓋的問題,但是這個方法不被 JVM 原生支持,因為 JDK 提供的 ClassLoader 僅支持裝載嵌套 jar 包的 class 文件。所以這種方法需要自定義 ClassLoader 以支持嵌套 jar。
Onejar Maven Plugin
One-JAR 就是一個基于上面嵌套 jar 實現的工具。onejar-maven-plugin 是社區基于 onejar 實現的 maven 插件。
<plugin><groupId>com.jolira</groupId><artifactId>onejar-maven-plugin</artifactId><version>1.4.4</version><executions><execution><goals><goal>one-jar</goal></goals></execution></executions>
</plugin>
Spring boot plugin
One-JAR 有點年久失修,好久沒有維護了,Spring Boot 提供的 Maven Plugin 也可以打包 Fatjar,支持非遮蔽和嵌套的混合模式,并且支持 maven 和 gradle 。
<plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><layout>ZIP</layout><requiresUnpack><dependency><groupId>org.jruby</groupId><artifactId>jruby-complete</artifactId></dependency></requiresUnpack></configuration>
</plugin>
plugins {id 'org.springframework.boot' version '2.0.4.RELEASE'
}bootJar {requiresUnpack '**/jruby-complete-*.jar'
}
requiresUnpack 參數可以定制那些 jar 不希望被解壓,采用嵌套的方式打包到 Fatjar 內部。
其打包后的內部結構為
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
應用的類文件被防止到 BOOT-INF/classes 目錄,依賴包被放置到 BOOT-INF/lib 目錄。
查看 META-INF/MANIFEST.MF 文件,其內容為
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.mycompany.project.MyApplication
啟動類是固定的 org.springframework.boot.loader.JarLauncher,應用程序的入口類需要配置成 Start-Class。這樣做的目的主要是為了支持嵌套 jar 包的類裝載,替換掉默認的 ClassLoader。
但是函數計算需要的 jar 包是一種打包結構,在服務端運行時會解壓開,不會調用 Main-Class。所以自定義 ClassLoader 是不生效的,所以不要使用嵌套 jar 結構,除非在入口函數指定重新定義 ClassLoader 或者 Classpath 以支持 BOOT-INF/classes 和 BOOT-INF/lib 這樣的定制化的類路徑。
小結
插件 | 構建平臺 | 工作機制 |
---|---|---|
maven-assembly-plugin | maven | Unshaded |
Gradle Java plugin | gradle | Unshaded |
maven-shade-plugin | maven | Shaded |
com.github.johnrengelman.shadow | gradle | Shaded |
Onejar | ant, maven | Jar of Jars |
Spring boot plugin | maven, gradle | Unshaded, Jar of Jars |
單從 Fatjar 的角度看, Spring boot maven/gradle 做得最精致。但是 jar 包內部的自定義路徑解壓開以后和函數計算是不兼容的。所以如果用于函數計算打包,建議使用 Unshaded 或者 Shared 的打包方式,但是需要自己注意文件覆蓋問題。
參考閱讀
- https://imagej.net/Uber-JAR
- https://softwareengineering.stackexchange.com/questions/297276/what-is-a-shaded-java-dependency
- https://docs.spring.io/spring-boot/docs/current/reference/html/executable-jar.html