Wetts's blog

Stay Hungry, Stay Foolish.

0%

Java-项目打包方式分析

参考:https://www.cnblogs.com/ywjy/p/7771042.html

前置条件

代码如下:

1
2
3
4
5
6
7
package com.hikvision.demo;

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

目录结构如下:

1
2
3
./
├─ HelloWorld.java
├─ target/

Java 原生打包

通过 jar 命令打包。JAR 包是 Java 中所特有一种压缩文档,其实大家就可以把它理解为 .zip 包。当然也是有区别的,JAR 包中有一个 META-INF\MANIFEST.MF 文件,当你打成 JAR 包时,它会自动生成。

jar 命令

jar 命令格式:jar {c t x u f }[ v m e 0 M i ][-C 目录]文件名...

  • -c 创建一个 jar 包
  • -t 显示 jar 中的内容列表
  • -x 解压 jar 包
  • -u 添加文件到 jar 包中
  • -f 指定 jar 包的文件名
  • -v 生成详细的报造,并输出至标准设备
  • -m 指定 manifest.mf 文件.(manifest.mf 文件中可以对 jar 包及其中的内容作一些一设置)
  • -0 产生 jar 包时不对其中的内容进行压缩处理
  • -M 不产生所有文件的清单文件(Manifest.mf)。这个参数与忽略掉-m参数的设置
  • -i 为指定的 jar 文件创建索引文件
  • -C 表示转到相应的目录下执行 jar 命令,相当于 cd 到那个目录,然后不带 -C 执行 jar 命令

jar 使用范例

  • 创建 jar 包
    • jar cf hello.jar hello
      • 利用 test 目录生成 hello.jar 包,如 hello.jar 存在,则覆盖
  • 创建并显示打包过程
    • jar cvf hello.jar hello
      • 利用 hello 目录创建 hello.jar 包,并显示创建过程
  • 显示 jar 包
    • jar tvf hello.jar
      • 查看 hello.jar 包的内容。
      • 指定的 jar 包必须真实存在,否则会发生 FileNoutFoundException。
  • 解压 jar 包
    • jar xvf hello.jar
      • 解压 hello.jar 至当前目录
  • jar 中添加文件
    • jar uf hello.jar HelloWorld.java
      • 将 HelloWorld.java 添加到 hello.jar 包中
  • 创建不压缩内容 jar 包
    • jar cvf0 hello.jar *.class
      • 利用当前目录中所有的 .class 文件生成一个不压缩 jar 包
  • 创建带 manifest.mf 文件的 jar 包
    • jar cvfm hello.jar manifest.mf hello
      • 创建的 jar 包多了一个 META-INF 目录,META-INF 子目录下多了一个 manifest.mf 文件
  • 忽略 manifest.mf 文件
    • jar cvfM hello.jar hello
      • 生成的 jar 包中不包括 META-INF 目录及 manifest.mf 文件
  • 加 -C 应用
    • jar cvfm hello.jar mymanifest.mf -C hello/
      • 表示在切换到 hello 目录下然后再执行 jar 命令
  • -i 为 jar 文件生成索引列表
    • jar i hello.jar
      • 执行完这条命令后,它会在 hello.jar 包的 META-INF 文件夹下生成一个名为 INDEX.LIST 的索引文件,它会生成一个列表,最上边为 jar 包名。
  • 导出解压列表
    • jar tvf hello.jar > hello.txt
      • 如果你想查看解压一个 jar 的详细过程,而这个 jar 包又很大,屏幕信息会一闪而过,这时你可以把列表输出到一个文件中

Manifest.mf 文件编写规则

  1. 一般属性

    1. Manifest-Version
      • 用来定义manifest文件的版本,例如:Manifest-Version: 1.0
    2. Created-By
      • 声明该文件的生成者,一般该属性是由jar命令行工具生成的,例如:Created-By: Apache Ant 1.5.1
    3. Signature-Version
      • 定义jar文件的签名版本
    4. Class-Path
      • 应用程序或者类装载器使用该值来构建内部的类搜索路径
  2. 应用程序相关属性

    1. Main-Class
      • 定义jar文件的入口类,该类必须是一个可执行的类,一旦定义了该属性即可通过 java -jar x.jar来运行该jar文件。
  3. 小程序(Applet)相关属性

    1. Extendsion-List
      • 该属性指定了小程序需要的扩展信息列表,列表中的每个名字对应以下的属性
    2. -Extension-Name
    3. -Specification-Version
    4. -Implementation-Version
    5. -Implementation-Vendor-Id
    6. -Implementation-URL
  4. 扩展标识属性

    1. Extension-Name
      • 该属性定义了jar文件的标识,例如Extension-Name: Struts Framework
  5. 包扩展属性

    1. Implementation-Title 定义了扩展实现的标题
    2. Implementation-Version 定义扩展实现的版本
    3. Implementation-Vendor 定义扩展实现的组织
    4. Implementation-Vendor-Id 定义扩展实现的组织的标识
    5. Implementation-URL : 定义该扩展包的下载地址(URL)
    6. Specification-Title 定义扩展规范的标题
    7. Specification-Version 定义扩展规范的版本
    8. Specification-Vendor 声明了维护该规范的组织
    9. Sealed 定义jar文件是否封存,值可以是true或者false (这点我还不是很理解)
  6. 签名相关属性

    • 签名方面的属性我们可以来参照 JavaMail 所提供的 mail.jar 中的一段

      • ```
        Name: javax/mail/Address.class

      Digest-Algorithms: SHA MD5

      SHA-Digest: AjR7RqnN//cdYGouxbd06mSVfI4=

      MD5-Digest: ZnTIQ2aQAtSNIOWXI1pQpw==

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
         - 这段内容定义类签名的类名、计算摘要的算法名以及对应的摘要内容(使用 BASE64 方法进行编码)
      7. 自定义属性
      - 除了前面提到的一些属性外,你也可以在 MANIFEST.MF 中增加自己的属性以及响应的值,例如 J2ME 程序 jar 包中就可能包含着如下信息
      - ```
      MicroEdition-Configuration: CLDC-1.0

      MIDlet-Name: J2ME_MOBBER Midlet Suite

      MIDlet-Info-URL: http://www.javayou.com/

      MIDlet-Icon: /icon.png

      MIDlet-Vendor: Midlet Suite Vendor

      MIDlet-1: mobber,/icon.png,mobber

      MIDlet-Version: 1.0.0

      MicroEdition-Profile: MIDP-1.0

      MIDlet-Description: Communicator

打包

编译

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

1
2
3
4
5
./
├─ HelloWorld.java
├─ target/
├─ com/hikvision/demo/
├─ HelloWorld.class

打包

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

1
2
3
4
5
6
7
./
├─ HelloWorld.java
├─ target/
│ └─ com/hikvision/demo/
│ └─ HelloWorld.class
├─ demo-algorithm.jar

打包的结果 demo-algorithm.jar,其内部结构为:

1
2
3
4
5
6
7
demo-algorithm.jar
├─ com
│ └─ hikvision
│ └─ demo
│ └─ HelloWorld.class
└─ META-INF
└─ MANIFEST.MF

其中,MANIFEST.MF 的内容为:

1
2
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 -cp 和 -classpath 一样,是指定类运行所依赖其他类的路径,通常是类库,jar 包之类,需要全路径到 jar 包,window 上分号“;” 分隔,linux 上是分号“:”分隔。不支持通配符,需要列出所有 jar 包,用一点“.”代表当前路径。
格式:
java -cp .;myClass.jar packname.mainclassname
表达式支持通配符,例如:
java -cp .;c:\classes01\myClass.jar;c:\classes02\*.jar packname.mainclassname

相关阅读
java -jar myClass.jar
执行该命令时,会用到目录 META-INF\MANIFEST.MF 文件,在该文件中,有一个叫 Main-Class 的参数,它说明了 java -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 包的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
.
├─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 文件中添加插件配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<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 包:

1
2
3
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 原生的打包结果。可以配置插件,修改打包结果的名称:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<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>

使用这个配置,最终的打包结果:

1
2
3
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 是插件的打包结果。

插件打包结果的内部结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
├─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 插件:

1
2
3
4
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

这个插件默认将打包绑定在了 maven 生命周期的 package 阶段,即执行 package 命令会自动触发插件打包。

插件会将 Maven 原生的打包结果重命名,然后将自己的打包结果使用之前那个名字。比如:

1
2
3
4
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 包安装到本地仓库。这一点可以配置,使用下面的插件配置,可以确保安装到本地仓库的是原生的打包结果:

1
2
3
4
5
6
7
8
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--将原始的包作为install和deploy的对象,而不是包含了依赖的包-->
<attach>false</attach>
</configuration>
</plugin>

spring-boot-maven-plugin 打包的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.
├─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 文件内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 定义的。

因为不符合 Java 标准 jar 包结构,所以无法通过 java -cp <package>.<MainClass> 的方式运行 jar 包里的某个类,因为按照标准的 jar 包结构是找不到这个类的。

总结

  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 时),可以通过配置改变这一点。