前言
由于笔者之前也一直在使用 IDEA, Maven 等成熟工具|框架的打包方式, 也没有仔细研究过这个 JAR 包打包之中的细节, 网上公开的视频也没有找到, 但文章倒挺多的, 那周六日就简单看一下吧, 将这些打包方式都整理整理.
本篇文章彻底理解 Maven & IDEA & 原生的打包方式, 妈妈再也不用担心我不懂 jar 了.
原生命令行打包
基本介绍
安装完毕的JDK_HOME/bin目录下, 存在jar.exe文件, 如下:
当我们通过命令行进行调用时, 会给出提示信息:
C:\Users\Administrator>jar
用法: jar {ctxui}[vfmn0PMe] [jar-file] [manifest-file] [entry-point] [-C dir] files ...
选项:-c 创建新档案-t 列出档案目录-x 从档案中提取指定的 (或所有) 文件-u 更新现有档案-v 在标准输出中生成详细输出-f 指定档案文件名-m 包含指定清单文件中的清单信息-n 创建新档案后执行 Pack200 规范化-e 为捆绑到可执行 jar 文件的独立应用程序指定应用程序入口点-0 仅存储; 不使用任何 ZIP 压缩-P 保留文件名中的前导 '/' (绝对路径) 和 ".." (父目录) 组件-M 不创建条目的清单文件-i 为指定的 jar 文件生成索引信息-C 更改为指定的目录并包含以下文件
如果任何文件为目录, 则对其进行递归处理。
清单文件名, 档案文件名和入口点名称的指定顺序
与 'm', 'f' 和 'e' 标记的指定顺序相同。
示例 1
: 将两个类文件归档到一个名为 classes.jar 的档案中:
jar cvf classes.jar Foo.class Bar.class
示例 2
: 使用现有的清单文件 'mymanifest' 并将 foo/ 目录中的所有文件归档到 'classes.jar' 中:
jar cvfm classes.jar mymanifest -C foo/ .
最后两行提示的示例1 & 示例2, 是两种不同的打包方式。
下面用土白话说明一下.
示例1
: 不使用自定义的 MANIFEST 文件进行打包, 这种方式生成的jar包不能使用java -jar XXX.jar命令执行, 因为不是自定义的 MANIFEST 文件, 所以不存在程序入口. (程序入口需要自定义配置 MANIFEST 文件, 并在其进行指明)
示例2
: 比较常用的打包方式, 使用自定义的 MANIFEST 文件参与打包, 这样能够实现往包中添加依赖, 并且可以指定程序入口, 实现java -jar XXX.jar 直接运行jar包.
示例1打包方式
我们准备如下项目结构:
PS C:\Users\Administrator\Desktop\MyJavaProject> tree /F ./
C:\USERS\ADMINISTRATOR\DESKTOP\MYJAVAPROJECTProject1.javaProject2.java
上述准备了两个简单的Java文件, 内容如下:
public class Project1 {public static void main(String[] args){System.out.println("Project1 Hello~");}
}
// Project2 与 Project1 程序逻辑相同, 简单输出内容即可
随后我们将其编译:
进行简单编译完成后, 我们可以使用jar.exe - 示例1
进行打包, 如下:
PS C:\Users\Administrator\Desktop\MyJavaProject> jar cvf classes.jar Project1.class Project2.class
已添加清单
正在添加: Project1.class(输入 = 425) (输出 = 289)(压缩了 32%)
正在添加: Project2.class(输入 = 425) (输出 = 289)(压缩了 32%)
将其进行打包后, 我们进行运行其中的Project1 & Project2, 以及看一下java -jar classes.jar
的运行结果:
PS C:\Users\Administrator\Desktop\MyJavaProject> java -cp .\classes.jar Project1
Project1 Hello~
PS C:\Users\Administrator\Desktop\MyJavaProject> java -cp .\classes.jar Project2
Project2 Hello~
PS C:\Users\Administrator\Desktop\MyJavaProject> java -jar classes.jar
classes.jar中没有主清单属性
这里java -jar classes.jar说没有主清单属性
, 其原因则是因为META-INF/MANIFEST.MF中没有指明程序入口.
但java -cp classes.jar 类名
可运行成功, 本质上是执行的该jar
包下的具体类::main方法
.
我们也可以直接通过修改classes.jar!/META-INF/MANIFEST.MF
文件, 通过配置Main-Class
来配置我们的程序入口:
Manifest-Version: 1.0
Created-By: 1.8.0_131 (Oracle Corporation)
Main-Class: Project1
结果如下:
示例2打包方式
批量编译小脚本
同样还是test1.java
与test2.java
以及Main.java
但是各自有自己的包名, 下面看一下即将创建的目录结构:
Project1.java
:
package cn.heihu577;public class Project1 {public static void main(String[] args){System.out.println("Heihu577~");}public void sayHello(){System.out.println("[Project1] sayHello");}
}
Project2.java
:
package cn.helen;public class Project2 {public static void main(String[] args){System.out.println("Helen~");}public void sayHello(){System.out.println("[Project2] sayHello");}
}
mymain.java
:
package cn;import cn.heihu577.Project1;
import cn.helen.Project2;public class mymain {public static void main(String[] args){System.out.println("[mymain] main");Project1 project1 = new Project1();Project2 project2 = new Project2();project1.sayHello();project2.sayHello();}
}
随后创建out
目录, 创建完毕后创建批量编译脚本:
:: 运行本脚本, 一定要放在项目根目录, 比如当前目录下有cn包, 一定放到cn包同级, 因为使用了 %cd%
:: 这里需要指明要编译的包名
setlocal
set PACKAGE_NAME=cn
set WRITE_PATH=outfor /r %cd%/%PACKAGE_NAME%/ %%i in (*.java) do javac %%i -d %cd%/%WRITE_PATH% -encoding utf-8 -cp %cd%;lib/*
endlocal
随后在项目根目录进行运行脚本, 将会批量编译java:
最终将编译好的所有class
文件都存放至out
目录中.
将编译好的 class 进行打包
这里我们需要创建out/META-INF/MANIFEST.MF
文件, 用来指明我们程序入口:
Manifest-Version: 1.0
Main-Class: cn.mymain
目录结构如下:
最终我们进行执行示例2的打包方式:
PS C:\Users\Administrator\Desktop\MyJavaProject> jar cvfm test.jar out/META-INF/MANIFEST.MF -C out/ .
已添加清单
正在添加: cn/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: cn/heihu577/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: cn/heihu577/Project1.class(输入 = 518) (输出 = 329)(压缩了 36%)
正在添加: cn/helen/(输入 = 0) (输出 = 0)(存储了 0%)
正在添加: cn/helen/Project2.class(输入 = 512) (输出 = 324)(压缩了 36%)
正在添加: cn/mymain.class(输入 = 547) (输出 = 371)(压缩了 32%)
正在忽略条目META-INF/
正在忽略条目META-INF/MANIFEST.MF
最终我们可以看到打包的结果, 并可以通过-jar
进行运行刚刚生成的jar
:
IDEA 编辑器打包
我们知道的是, IDEA 编辑器提供了打包方式, 下面我们来看看这里打包方式是如何使用的.
而上面由于我们知道了, 整个打包是围绕MANIFEST.MF
这个文件进行打包的, 上面的案例是我们自己创建的MANIFEST.MF文件, 而IDEA中打包会帮助我们生成MANIFEST.MF文件.
基本环境搭建
这里我们在IDEA中创建一个基本环境, 用于主要测试。
首先pom.xml
文件如下:
<dependencies><dependency><groupId>commons-beanutils</groupId><artifactId>commons-beanutils</artifactId><version>1.9.4</version></dependency>
</dependencies>
引入beanutils
包则是方便我们后续对打包带依赖 | 不带依赖
所生成的MANIFEST.MF
文件进行理解.
随后创建两个基本的测试类:
package com.heihu577;import org.apache.commons.beanutils.BeanUtils;public class sayHello {public static void main(String[] args) throws Exception {Person person = new Person();BeanUtils.setProperty(person, "name", "heihu577");System.out.println(BeanUtils.getProperty(person, "name"));}
}
而Person类
代码如下:
package com.heihu577;public class Person {private String name;public String getName() {return name;}public void setName(String name) {this.name = name;}
}
而我们知道的是, IDEA打jar包中是这样配置的:
而这里有两个选项, 分别是提取到目标 JAR && 复制到输出目录并通过清单链接
, 这两种方式有什么区别呢?
注意: 在实际通过 IDEA 打 JAR 包时, 一定要指明主类.
通过清单链接
这里所说的通过清单链接
其实是在说: 将库中的BeanUtils.jar, 以及当前项目的jar, 分别导出出来, 然后当前项目的jar通过MANIFEST.MF进行指明ClassPath来进行链接, 说这么多意义也不大, 直接上图片.
这里可以看到的是, 导出的是四个jar包, 而由于我们的PackageStudy01.jar依赖于BeanUtils.jar, 所以它的/META-INF/MANIFEST.MF文件中的内容是这样的:
可以从中看到的是, PackageStudy01.jar是可以成功运行的. 而如果我们将生成的这四个jar包中, 将beanutils.jar以及它相关的jar都删掉, 只留下PackageStudy01.jar, 运行是否会爆出类加载不上异常?
可以看到的是, jar 与 jar 之间的依赖也是通过Class-Path
进行指明的.
提取到目标 JAR
而提取到目标JAR则比较暴力, 将当前项目以及所需要的依赖, 都打到一个文件里面, 直接放上效果图:
这里我们也可以看到MANIFEST.MF中, 是不需要通过Class-Path进行指明的. 因为都在同一个包下.
Maven 打包
关于Maven打包, 都是使用提供好的插件进行打包, 而Maven打包的插件也是比较多的, 这里笔者主要来介绍一下它们之间的选择, 与区别。
maven-jar-plugin [不带依赖打包]
在pom.xml
文件中进行引入:
<build><finalName>${project.artifactId}</finalName><!--修改编译出来的jar包名,仅为{artifactId}.jar--><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.2.0</version><configuration><archive><manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile>
<!-- <manifest>-->
<!-- <addClasspath>true</addClasspath>-->
<!-- <mainClass>com.leon.Main</mainClass>-->
<!-- </manifest>--></archive></configuration></plugin></plugins>
</build>
这里archive标签, 我们可以进行定义manifest的参数, 也可以通过引入本地MANIFEST.MF文件, 在这里我们就使用本地引入文件的方式来举例吧。
这里MANIFEST.MF
文件内容定义如下:
Manifest-Version: 1.0
Main-Class: com.heihu577.sayHello
如果爆红, 我们只需要使用dependency引入一下即可:
<dependencies><dependency><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>3.1.2</version></dependency>
</dependencies>
随后打包过程如下:
随后可以看到的是, 并没有将依赖包打入进来, 当然, META-INF/MANIFEST.MF文件中是不存在Class-Path的, 使用该插件只能进行打包无依赖的jar.
maven-dependency-plugin [通过清单链接]
但是maven-jar-plugin可以通过配合maven-dependency-plugin达到IDEA中通过清单链接的效果, 它的配置如下:
<build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-dependency-plugin</artifactId><executions><execution><id>copy-dependencies</id><phase>prepare-package</phase><goals><goal>copy-dependencies</goal></goals><configuration><outputDirectory>${project.build.directory}/lib</outputDirectory><!-- 将带有依赖的 jar 放入到 lib 目录中 --><overWriteReleases>false</overWriteReleases><overWriteSnapshots>false</overWriteSnapshots><overWriteIfNewer>true</overWriteIfNewer></configuration></execution></executions></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><manifest><addClasspath>true</addClasspath><classpathPrefix>lib/</classpathPrefix><!-- 配置 Class-Path, 目录与上面 outputDirectory 保持一致即可 --><mainClass>com.heihu577.sayHello</mainClass> <!-- 放置自己的入口程序 --></manifest></archive></configuration></plugin></plugins>
</build>
其操作步骤与maven-jar-plugin相同, 最终打包结果如下:
其效果与IDEA中打包的通过清单链接的功能效果是一致的.
maven-assembly-plugin [带依赖打包]
使用该插件进行打包将带有依赖, 配置如下:
<build><finalName>${project.artifactId}</finalName><plugins><plugin><artifactId>maven-assembly-plugin</artifactId><configuration><archive><manifestFile>${project.basedir}/src/main/resources/META-INF/MANIFEST.MF</manifestFile><!-- 需本地创建 /META-INF/MANIFEST.MF 文件 --></archive><descriptorRefs><descriptorRef>jar-with-dependencies</descriptorRef></descriptorRefs></configuration><executions><execution><id>make-assembly</id> <!-- this is used for inheritance merges --><phase>package</phase> <!-- bind to the packaging phase --><goals><goal>single</goal></goals></execution></executions></plugin></plugins>
</build>
其操作步骤与maven-jar-plugin相同, 最终打包结果如下:
SpringBoot 打包
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins>
</build>
梭哈即可.
Ending...
原创 Heihu577 Heihu Share