Skip to content

Jar中的Manifest文件

写了个简单的Java代码,要怎么把它打包为jar包供jre去运行?

项目结构如下:

bash
fuming@fumingdeiMac-Pro test % tree
.
├── src
│   └── main
│       └── Main.java
└── test.iml

2 directories, 2 files

其中,src/Main.java的内容如下:

java
package main;

public class Main {
    public static void main(String[] args) {
       System.out.println(1);
    }
}

先不放Manifest,直接打包,会怎样?

打包

无Manifest打包

直接编译java文件为class文件并打包:

bash
fuming@MacBook-Pro src javac main/Main.java
fuming@MacBook-Pro src jar cf MyJar.jar main/*.class
fuming@MacBook-Pro src tree
.
├── MyJar.jar
└── main
    ├── Main.class
    └── Main.java

1 directory, 3 files
fuming@MacBook-Pro src java -jar MyJar.jar
MyJar.jar中没有主清单属性

可以看到能打包成功,但是无法运行,因为MyJar.jar中未指定程序入口是哪个函数,也就是“没有主清单属性”。

给无Manifest的jar包添加程序入口

下面命令更新了jar包,为其加入了程序入口:

bash
fuming@MacBook-Pro src jar ufe MyJar.jar main.Main # 这里表示main.Main这个class,符号'.'可以用'/'替换

更新之后,解压MyJar.jar ,可以看到:

bash
fuming@MacBook-Pro MyJar tree
.
├── META-INF
│   └── MANIFEST.MF
└── main
    └── Main.class

2 directories, 2 files
fuming@MacBook-Pro MyJar cat META-INF/MANIFEST.MF
Manifest-Version: 1.0
Created-By: 18.0.2.1 (Oracle Corporation)
Main-Class: main.Main

多了META-INF/MANIFEST.MF,且其中的内容是jar工具自己写入的。

有Manifest打包

src/下放置文件META-INF/MANIFEST.MF,内容如下(注意文件后一定要有一个空行,不要使用Windows风格的换行符)。

ini
Manifest-Version: 1.0
Created-By: 18.0.2.1 (Oracle Corporation)
Main-Class: main.Main

然后即可开始打包。

先将java文件编译为class文件:

bash
fuming@MacBook-Pro src javac main/Main.java
fuming@MacBook-Pro src tree
.
├── META-INF
│   └── MANIFEST.MF
└── main
    ├── Main.class
    └── Main.java

2 directories, 3 files
fuming@MacBook-Pro src jar cvfm MyJar.jar META-INF/MANIFEST.MF main/*.class
已添加清单
正在添加: main/Main.class(输入 = 383) (输出 = 273)(压缩了 28%)
fuming@MacBook-Pro src java -jar MyJar.jar
1

运行没有问题,解压后其内容和上文给无Manifest的jar包添加程序入口部分解压结果相同。

Manifest的使用

ClassPath

刚才打包的是test项目,如果test想调用其他人提供的jar包中的方法,应该配置ClassPath。

现在我们新建一个test2项目,其中只有一个主类如下:

java
package org.flxdu.example;

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

    public static int add(int a, int b) {
        return a + b;
    }
}

按照上文的方法进行打包得到Test2.jar,确保jar包运行可以输出Hello world!

首先说一下在IntelliJ IDEA中如何使代码可以调用Test2.jar中的org.flxdu.example.Main.add(int a, int b)方法。

在IntelliJ IDEA中添加ClassPath

进入下图“Project Structure”位置:

image-20230722213608782

在“SDKs”部分找到项目使用的JDK,新增ClassPath:

image-20230722213738939

然后便可以import Test2.jar中的类和静态方法了。

如下图:

image-20230722213910283

在Manifest中使用ClassPath

上文我们在test项目中调用了Test2.jar。接下来我们打包test项目。

首先在Manifest中加入ClassPath:

ini
Manifest-Version: 1.0
Created-By: 18.0.2.1 (Oracle Corporation)
Main-Class: main.Main
Class-Path: Test2.jar # 这个相对路径是打为jar包后,和jar包同级的目录;可以写成绝对路径。

重新编译main/Main.java

bash
fuming@MacBook-Pro src javac main/Main.java 
main/Main.java:6: 错误: 程序包org.flxdu.example不存在
        System.out.println(org.flxdu.example.Main.add(11, 2));
                                            ^
1 个错误

这里会报错,是因为我们上面的配置告诉了IDEA去哪里寻找org.flxdu.example,但是终端中的javac并没有被配置。

我们需要使用--class-path <path>, -classpath <path>, -cp <path>来指定Test2.jar的路径。

bash
fuming@MacBook-Pro src ls
META-INF  Test2.jar main
fuming@MacBook-Pro src javac -cp Test2.jar main/Main.java
fuming@MacBook-Pro src jar cvfm MyJar.jar META-INF/MANIFEST.MF main/*.class
已添加清单
正在添加: main/Main.class(输入 = 440) (输出 = 315)(压缩了 28%)
fuming@MacBook-Pro src java -jar MyJar.jar 
13

可以看到没有问题。

如果把Test2.jar移走呢:

bash
fuming@MacBook-Pro src ls
META-INF  MyJar.jar Test2.jar main
fuming@MacBook-Pro src mv Test2.jar main 
fuming@MacBook-Pro src java -jar MyJar.jar
Exception in thread "main" java.lang.NoClassDefFoundError: org/flxdu/example/Main
        at main.Main.main(Main.java:6)
Caused by: java.lang.ClassNotFoundException: org.flxdu.example.Main
        at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641)
        at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188)
        at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:521)
        ... 1 more

可以看到MyJar.jar找不到Test2.jar,也就找不到org.flxdu.example.Main,然后报错。

Setting Package Version Information

回到test项目,其中,src/Main.java的内容如下:

java
package main;

public class Main {
    public static void main(String[] args) {
       System.out.println(1);
    }
}

将其编译为class文件。

Manifest.txt中写入信息,这些信息不会影响项目打包运行,只是向jar包使用者展示信息。

可以写入的内容有很多,详见Java™ Product Versioning

META-INF/MANIFRST.MF中写入一些基本的信息:

ini
Main-Class: main.Main
Name: main
Specification-Title: Test
Specification-Version: 1.2
Specification-Vendor: Example Tech, Inc.
Implementation-Title: main
Implementation-Version: 0.0.1
Implementation-Vendor: Example Tech, Inc.

目录结构:

bash
fuming@fumingdeiMac-Pro test % tree            
.
├── src
│   ├── META-INF
│   │   └── MANIFEST.MF
│   └── main
│       ├── Main.class
│       └── Main.java
└── test.iml

3 directories, 4 files

打包:

bash
fuming@fumingdeiMac-Pro src % jar cvfm MyJar.jar META-INF/MANIFEST.MF main/*.class
已添加清单
正在添加: main/Main.class(输入 = 383) (输出 = 273)(压缩了 28%)

运行jar包,可以看到运行输出没有问题。

打包完毕后,需要使用JDK提供的类java.lang.Package来读取这些内容,具体使用见Java™ Product Versioning

Sealing Packages within a JAR File

包密封是指在生成JAR文件的时候,可以选择对JAR文件中的一个包进行密封。对JAR文件中的一个包进行密封是指这个包中定义的所有类都必须出自同一个JAR文件,否则JVM会抛出SecurityException。

https://docs.oracle.com/javase/tutorial/deployment/jar/sealman.html

Enhancing Security with Manifest Attributes

https://docs.oracle.com/javase/tutorial/deployment/jar/secman.html

Spring Boot项目Jar包中的Manifest文件

解压一个Spring Boot项目的jar包,可以看到如下目录结构:

bash
fuming@MacBook-Pro app-0.0.1-SNAPSHOT tree -L 2
.
├── BOOT-INF
│   ├── classes							# 项目源码
│   ├── classpath.idx
│   ├── layers.idx
│   └── lib									# 依赖库,都是jar包
├── META-INF								# 重点!!!!!
│   ├── MANIFEST.MF
│   └── maven
└── org
    └── springframework     # spring框架的一些class文件

7 directories, 3 files

查看META-INF/MANIFEST.MF,内容如下:

ini
Manifest-Version: 1.0
Created-By: Maven Jar Plugin 3.2.0
Build-Jdk-Spec: 13
Implementation-Title: app
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.JarLauncher			# JAR包的主类,也就是程序的入口类
Start-Class: com.example.app.Application										# Spring Boot应用的主类,也就是Spring Boot应用的入口类
Spring-Boot-Version: 2.4.2
Spring-Boot-Classes: BOOT-INF/classes/											# Spring Boot应用的类文件存放的路径
Spring-Boot-Lib: BOOT-INF/lib/															# Spring Boot应用的依赖库存放的路径
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx					# Spring Boot应用的类路径索引文件
Spring-Boot-Layers-Index: BOOT-INF/layers.idx								# Spring Boot应用的分层索引文件

运行jar包时,会先启动Spring Boot的JarLauncher,然后启动项目的主类。

启动流程和细节可以查阅Spring Boot文档。