Java项目打包方式分析

[TOC]

概述

在项目实践过程中,有个需求需要做一个引擎能执行指定jar包的指定main方法。

起初我们以一个简单的spring-boot项目进行测试,使用spring-boot-maven-plugin进行打包,使用java -cp demo.jar <package>.<MainClass>执行,结果报错找不到对应的类。

我分析了spring-boot-maven-plugin打包的结构,又回头复习了java原生jar命令打包的结果,以及其他Maven打包插件打包的结果,然后写成这边文章。

这篇文章里会简单介绍java原生的打包方式,maven原生的打包方式,使用maven shade插件将项目打成一个大一统的jar包的方式,使用spring-boot-maven-plugin将项目打成一个大一统的jar包的方式,并比较它们的差异,给出使用建议。

Java原生打包

为了简单起见,假设我们的项目只有一个HelloWorld.java,不使用Maven。假设当前目录为.,初始目录下没有任何内容。

首先,我们在当前目录新建文件HelloWorld.java。为了演示如何让编译的class文件自动放置到与package对应的目录结构中,特地添加package命令。

package com.hikvision.demo;

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}

在当前目录新建target子目录,此时,目录结构如下:

 ./
  ├─ HelloWorld.java
  ├─ target/

编译

命令:javac HelloWorld.java -d target。目录结构变为:

 ./
  ├─ HelloWorld.java
  ├─ target/
        ├─ com/hikvision/demo/
              ├─ HelloWorld.class

打包

命令:jar cvf demo-algorithm.jar -C target/ .。目录结构变为:

 ./
  ├─ 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时),可以通过配置改变这一点。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350