Java項目打包方式分析

概述

在項目實踐過程當中,有個需求須要作一個引擎能執行指定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

Java原生打包

爲了簡單起見,假設咱們的項目只有一個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包結構。

Maven原生打包

我通常使用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 shade插件打包

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的作法,應該是大謬的吧。

總結

  1. Java原生打包、Maven原生打包、shade插件打包的結果,其結構都是一致的。可使用java -cp的方式執行,通常沒法直接使用java -jar的方式執行。
  2. 使用spring-boot插件打包,其結構和上述的結構不一樣。不能使用java -cp的方式執行,可使用java -jar的方式執行。
  3. shade插件會忽略provided的依賴,不集成到jar包裏;spring-boot插件會將全部的依賴都集成到jar包裏。
  4. 默認的狀況下,shade插件和spring-boot插件的打包結果,會代替Maven原生打包結果被安裝到本地倉庫(執行mvn install時),能夠經過配置改變這一點。
相關文章
相關標籤/搜索