在項目實踐過程當中,有個需求須要作一個引擎能執行指定jar包的指定main方法。node
起初咱們以一個簡單的spring-boot項目進行測試,使用spring-boot-maven-plugin
進行打包,使用java -cp demo.jar <package>.<MainClass>
執行,結果報錯找不到對應的類。web
我分析了spring-boot-maven-plugin
打包的結構,又回頭複習了java原生jar
命令打包的結果,以及其餘Maven打包插件打包的結果,而後寫成這邊文章。spring
這篇文章裏會簡單介紹java原生的打包方式,maven原生的打包方式,使用maven shade插件將項目打成一個大一統的jar包的方式,使用spring-boot-maven-plugin
將項目打成一個大一統的jar包的方式,並比較它們的差別,給出使用建議。apache
爲了簡單起見,假設咱們的項目只有一個HelloWorld.java
,不使用Maven。假設當前目錄爲.
,初始目錄下沒有任何內容。api
首先,咱們在當前目錄新建文件HelloWorld.java
。爲了演示如何讓編譯的class文件自動放置到與package
對應的目錄結構中,特意添加package
命令。app
package com.hikvision.demo; public class HelloWorld { public static void main(String[] args) { System.out.println("Hello World"); } }
在當前目錄新建target
子目錄,此時,目錄結構以下:maven
./ ├─ HelloWorld.java ├─ target/
命令:javac HelloWorld.java -d target
。目錄結構變爲:ide
./ ├─ HelloWorld.java ├─ target/ ├─ com/hikvision/demo/ ├─ HelloWorld.class
命令:jar cvf demo-algorithm.jar -C target/ .
。目錄結構變爲:spring-boot
./ ├─ HelloWorld.java ├─ target/ │ └─ com/hikvision/demo/ │ └─ HelloWorld.class ├─ demo-algorithm.jar
打包的結果demo-algorithm.jar
,其內部結構爲:
demo-algorithm.jar ├─ com │ └─ hikvision │ └─ demo │ └─ HelloWorld.class └─ META-INF └─ MANIFEST.MF
其中,MANIFEST.MF的內容爲:
Manifest-Version: 1.0 Created-By: 1.8.0_144 (Oracle Corporation)
命令:java -cp demo-algorithm.jar com.hikvision.demo.HelloWorld
。
留意上面的jar包的結構,若是咱們但願以java -cp
的方式運行jar包中的某一個類的main方法,class的package必須對應jar包內部的一級目錄。
這種結構咱們稱之爲java標準jar包結構。
我通常使用mvn clean package
命令打包。
maven打包的結果,jar包名稱是根據artifactId和version來生成的,好比對於com.hikvision.algorithm:demo-algorithm:0.0.1-SNAPSHOT
的打包結果是:demo-algorithm-0.0.1-SNAPSHOT.jar
。
分析這個jar包的結構:
. ├─com │ └─hikvision │ └─algorithm │ └─HelloWorld.class ├─META-INF │ ├─maven │ │ └─com.hikvision.algorithm │ │ └─demo-algorithm │ │ ├─pom.properties │ │ └─pom.xml │ └─MANIFEST.MF └─application.properties
除META-INF目錄以外,其餘的都是class path,這一點符合java標準jar結構。不一樣的是META-INF有一級子目錄maven,放置項目的maven信息。
對於maven原生的打包結果,可使用java -cp
的方式執行其中某個主類。可是須要注意它並無包含因此來的jar包,這須要另外提供。
Maven打包插件應該不止一種,這裏使用的是maven-shade-plugin
。
在pom文件中添加插件配置:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> </execution> </executions> </plugin>
根據上面的配置,在package階段,會自動執行插件的shade
目標,這個目標負責將項目的class文件,以及項目依賴的class文件都會統一打到一個jar包裏。
咱們能夠執行mvn clean package
來自動觸發shade
,或者直接執行mvn shade:shade
。
target目錄會生成2個jar包,一個是maven原生的jar包,一個是插件的jar包:
target/ ├─ original-demo-algorithm-0.0.1-SNAPSHOT.jar (4KB) └─ demo-algorithm-0.0.1-SNAPSHOT.jar (6.24MB)
original-demo-algorithm-0.0.1-SNAPSHOT.jar
是原生的jar包,不包含任何依賴,只有4KB。demo-algorithm-0.0.1-SNAPSHOT.jar
是包含依賴的jar包,有6.24MB。
對照上文能夠猜想shade插件對maven原生打包結果進行重命名以後,使用這個名字又打出一個集成了依賴的jar包。
注意,這表示若是執行了mvn install
,最終被安裝到本地倉庫的是插件打出的jar包,而不是maven原生的打包結果。能夠配置插件,修改打包結果的名稱:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.3</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <finalName>demo-algorithm-0.0.1-SNAPSHOT-assembly</finalName> </configuration> </execution> </executions> </plugin>
使用這個配置,最終的打包結果:
target/ ├─ demo-algorithm-0.0.1-SNAPSHOT.jar (4KB) └─ demo-algorithm-0.0.1-SNAPSHOT-assembly.jar (6.24MB)
此時,demo-algorithm-0.0.1-SNAPSHOT.jar是maven原生的打包結果,demo-algorithm-0.0.1-SNAPSHOT-assembly.jar是插件的打包結果。
插件打包結果的內部結構以下:
├─ch │ └─qos │ └─logback │ ├─classic │ │ ├─boolex │ │ ├─db │ │ │ ├─names │ │ │ └─script │ │ ├─encoder │ │ └─util │ └─core │ ├─boolex │ ├─db │ │ └─dialect │ ├─encoder │ ├─joran │ │ ├─action │ │ ├─conditional │ │ ├─event │ │ │ └─stax │ │ ├─node │ │ ├─spi │ │ └─util │ │ └─beans │ ├─subst │ └─util ├─com │ └─hikvision │ └─algorithm ├─META-INF │ ├─maven │ │ ├─ch.qos.logback │ │ │ ├─logback-classic │ │ │ └─logback-core │ │ ├─com.hikvision.algorithm │ │ │ └─demo-algorithm │ │ ├─org.slf4j │ │ │ ├─jcl-over-slf4j │ │ │ ├─jul-to-slf4j │ │ │ ├─log4j-over-slf4j │ │ │ └─slf4j-api │ │ ├─org.springframework.boot │ │ │ ├─spring-boot │ │ │ ├─spring-boot-autoconfigure │ │ │ ├─spring-boot-starter │ │ │ └─spring-boot-starter-logging │ │ └─org.yaml │ │ └─snakeyaml │ ├─org │ │ └─apache │ │ └─logging │ │ └─log4j │ │ └─core │ │ └─config │ │ └─plugins │ └─services └─org ├─apache │ ├─commons │ │ └─logging │ │ └─impl │ └─log4j │ ├─helpers │ ├─spi │ └─xml ├─slf4j │ ├─bridge │ ├─event │ ├─helpers │ ├─impl │ └─spi ├─springframework │ ├─boot │ │ ├─admin │ │ ├─ansi │ │ ├─web │ │ │ ├─client │ │ │ ├─filter │ │ │ ├─servlet │ │ │ └─support │ │ └─yaml │ └─validation │ ├─annotation │ ├─beanvalidation │ └─support └─yaml └─snakeyaml ├─error ├─tokens └─util
這裏省略了全部的文件,以及大部分的子目錄。
除META-INF
目錄外的其餘全部目錄,都是classpath,結構和Maven原生的打包結構相同。不一樣的是shade插件將全部的依賴jar解壓縮以後,和項目的class文件一塊兒從新打成jar包;而且在META-INF/maven
下包含了項目自己及所依賴的項目的pom信息。
若是在pom文件中,聲明某個依賴是provided
的,它就不會被集成到jar包裏。
總的來講,使用maven-shade-plugin
打出的jar包的結構依然符合java標準jar包結構,因此咱們能夠經過java -cp
的方式運行jar包中的某一個類的main方法。
spring-boot-maven-plugin
插件打包項目首先必須是spring-boot項目,即項目直接或間接繼承了org.springframework.boot:spring-boot-starter-parent
。
在pom文件中配置spring-boot-maven-plugin
插件:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin>
這個插件默認將打包綁定在了maven生命週期的package
階段,即執行package
命令會自動觸發插件打包。
插件會將Maven原生的打包結果重命名,而後將本身的打包結果使用以前那個名字。好比:
target/ ├─ ... ├─ demo-algorithm-0.0.1-SNAPSHOT.jar.original └─ demo-algorithm-0.0.1-SNAPSHOT.jar
如上,demo-algorithm-0.0.1-SNAPSHOT.jar.original
是Maven原生的打包結果,被重命名以後追加了.original
後綴。demo-algorithm-0.0.1-SNAPSHOT.jar
是插件的打包結果。
這裏須要注意,若是運行了mvn install
,會將這個大一統的jar包安裝到本地倉庫。這一點能夠配置,使用下面的插件配置,能夠確保安裝到本地倉庫的是原生的打包結果:
<plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <!--將原始的包做爲install和deploy的對象,而不是包含了依賴的包--> <attach>false</attach> </configuration> </plugin>
spring-boot-maven-plugin打包的結構以下:
. ├─BOOT-INF │ ├─classes │ │ └─com │ │ └─hikvision │ │ └─algorithm │ └─lib ├─META-INF │ └─maven │ └─com.hikvision.algorithm │ └─demo-algorithm └─org └─springframework └─boot └─loader ├─archive ├─data ├─jar └─util
這裏忽略了全部的文件。
分析這個結構,spring-boot插件將項目自己的class放到了目錄BOOT-INF/classes
下,將全部依賴的jar放到了BOOT-INF/lib
下。在jar包的頂層有一個子目錄org
,是spring-boot loader相關的classes。
因此,這個與java標準jar包結構是不一樣的,和maven原生的打包結構也是不一樣的。
另外,須要注意的是,即便設置爲provided的依賴,依然會被集成到jar包裏,這一點與上文的shade插件不一樣。
分析META-INF/MANIFEST.MF
文件內容:
Manifest-Version: 1.0 Implementation-Title: demo-algorithm Implementation-Version: 0.0.1-SNAPSHOT Archiver-Version: Plexus Archiver Built-By: lijinlong9 Implementation-Vendor-Id: com.hikvision.algorithm Spring-Boot-Version: 1.5.8.RELEASE Implementation-Vendor: Pivotal Software, Inc. Main-Class: org.springframework.boot.loader.JarLauncher Start-Class: com.hikvision.algorithm.HelloWorld Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Created-By: Apache Maven 3.3.9 Build-Jdk: 1.8.0_144 Implementation-URL: http://projects.spring.io/spring-boot/demo-algorithm/
注意,這裏配置了Main-Class
,這表示咱們能夠以java -jar
的方式執行這個jar包。Main-Class
對應的值爲org.springframework.boot.loader.JarLauncher
,這表示具體的加載過程是由spring-boot定義的。
這裏有一篇文章分析spring boot
jar的啓動過程。我簡單看了下這篇文章,並無細讀,我大概猜想到spring-boot實現了一套本身的加載機制,與這個機制相對應的,spring-boot也自定義了一套本身的jar包結構。對我這說,目前瞭解到這個程度就夠了。
由於不符合Java標準jar包結構,因此沒法經過java -cp <package>.<MainClass>
的方式運行jar包裏的某個類,由於按照標準的jar包結構是找不到這個類的。
從這一點來看,咱們須要從新思考什麼樣的項目或者module應該作成spring-boot項目?到目前爲止,我認爲只有完整、可運行的項目或module才須要作成spring-boot項目,好比對外提供rest服務的module。而像common類的module,對外提供公共類庫,其自己沒法獨立運行,則不該該做爲spring-boot項目。
更況且對於多module的項目,將最頂層的module定義爲spring-boot項目,而讓全部的子module都經過繼承頂層module來間接繼承spring-boot-starter-parent
的作法,應該是大謬的吧。
java -cp
的方式執行,通常沒法直接使用java -jar
的方式執行。java -cp
的方式執行,可使用java -jar
的方式執行。mvn install
時),能夠經過配置改變這一點。