电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

用 One-JAR 简化应用程序交付


发布日期:2020/9/23
 

如果您曾经试图把 Java 应用程序交付为单一的 Java 档案文件(JAR 文件)那么您很有可能遇到过这样的需求在构建最终档案文件之前要展开支持 JAR 文件(supporting JAR file)这不但是一个开发的难点还有可能让您违反许可协议在本文中Tuffs 向您介绍了 OneJAR 这个工具它使用定制的类装入器动态地从可执行 JAR 文件内部的 JAR 文件中装入类

有人曾经说过历史总是在不断地重复自身首先是悲剧然后是闹剧 最近我第一次对此有了亲身体会我不得不向客户交付一个可以运行的 Java 应用程序但是我已经交付了许多次它总是充满了复杂性在搜集应用程序的所有 JAR 文件为 DOS 和 Unix(以及 Cygwin)编写启动脚本确保客户端环境变量都指向正确位置的时候总是有许多容易出错的地方如果每件事都能做好那么应用程序能够按它预期的方式运行但是在出现麻烦时(而这又是常见的情况)结果就是大量时间耗费在客户端支持上

最近与一个被大量 ClassNotFound 异常弄得晕头转向的客户交谈之后我决定自己再也不能忍受下去了所以我转而寻找一个方法可以把我的应用程序打包到单一 JAR 文件中给我的客户提供一个简单的机制(比如 java jar)来运行程序

努力的结果就是 OneJAR一个非常简单的软件打包解决方案它利用 Java 的定制类装入器动态地从单一档案文件中装入应用程序所有的类同时保留支持 JAR 文件的结构在本文中我将介绍我开发 OneJAR 的过程然后告诉您如何利用它在一个自包含的文件中交付您自己的可以运行的应用程序

OneJAR 概述

在介绍 OneJAR 的细节之前请让我首先讨论一下我构建它的目的我确定一个 OneJAR 档案文件应该是

可以用 java jar 机制执行

能够包含应用程序需要的 所有 文件 —— 也就是说包括原始形式(未展开)的类和资源

拥有简单的内部结构仅仅用 jar 工具就可以被装配起来

对原来的应用程序不可见 —— 也就是说无需修改原来的应用程序就可以把它打包在 OneJAR 档案文件内部

问题和解决方案

在开发 OneJAR 的过程中我解决的最大问题就是如何装入包含在另外一个 JAR 文件中的 JAR 文件 Java 类装入器 sunmiscLauncher$AppClassLoader(在 java jar 开始的时候出现)只知道如何做两件事

装入在 JAR 文件的根出现的类和资源

装入 METAINF/MANIFESTMF 中的 ClassPath 属性指向的代码基中的类和资源

而且它还故意忽略针对 CLASSPATH 的全部环境变量设置还忽略您提供的命令行参数 cp 所以它不知道如何从一个包含在其他 JAR 文件中的 JAR 文件装入类或资源

显然我需要克服这个问题才能实现 OneJAR 的目标

解决方案 展开支持 JAR 文件

我为了创建单一可执行 JAR 文件所做的第一个尝试显然就是在可交付的 JAR 文件内展开支持 JAR 文件我们把可交付的文件称为 mainjar假设有一个应用程序的类叫做 commainMain而且它依赖两个类 —— comaA (在 ajar 中) 和 combB(在 bjar 中)那么 OneJAR 文件看起来应该像这样

mainjar

|com/main/Mainclass

|com/a/Aclass

|com/b/Bclass

这样最初来源于 ajar 文件的 Aclass 丢失了Bclass 也是如此虽然这看起来只是个小问题但却会真正带来问题我很快就会解释为什么

OneJAR 和 FJEP

最近发布的一个叫做 FJEP (FatJar Eclipse Plugin) 的工具支持在 Eclipse 内部直接构建扁平 JAR 文件 OneJAR 已经与 FatJar 集成在一起以支持在不展开 JAR 文件的情况下嵌入 JAR 文件请参阅 参考资料 了解有关详细内容

把 JAR 文件展开到文件系统以创建一个扁平结构这可能非常耗时还需要使用 Ant 这样的构建工具来展开和重新归档支持类

除了这个小麻烦之外我很快又遇到了两个与展开支持 JAR 文件有关的严重问题

如果 ajar 和 bjar 包含的资源的路径名相同 (比如说都是 logjproperties )那么您该选哪个?

如果 bjar 的许可明确要求您在重新发布它的时候不能修改它那您怎么办?您无法在不破坏许可条款的前提下像这样展开它

我觉得这些限制为另外一种方法提供了线索

解决方案 : MANIFEST ClassPath

我决定研究 java jar 装入器中的另外一种机制装入的类是在档案文件中一个叫做 METAINF/MANIFESTMF 的特殊文件中指定的通过指定称为 ClassPath 的属性我希望能够向启动时的类装入器添加其他档案文件下面就是这样的一个 OneJAR 文件看起来的样子

mainjar

|METAINF/MANIFESTMF

|+ClassPath: lib/ajar lib/bjar

|com/main/Mainclass

|lib/ajar

|lib/bjar

说明与线索

URLClassloader 是 sunmiscLauncher$AppClassLoader 的基类它支持一个相当神秘的 URL 语法让您能够引用 JAR 文件内部的资源这个语法用起来像这样 jar:file:/fullpath/mainjar!/aresource

从理论上讲要获得一个在 JAR 文件 内部 的 JAR 文件中的项您必须使用像 jar:file:/fullpath/mainjar!/lib/ajar!/aresource 这样的方式但是很不幸这么做没有用JAR 文件协议处理器在找 JAR 文件时只认识最后一个 !/ 分隔符

但是这个语法确实为我最终的 OneJAR 解决方案提供了线索……

这能工作么? 当我把 mainjar 移动到另外一个地方并试着运行它时好像是可以了为了装配 mainjar 我创建了一个名为 lib 的子目录并把 ajar 和 bjar 放在里面不幸的是应用程序的类装入器只从文件系统提取支持 JAR 文件而不能从嵌入的 JAR 文件中装入类

为了克服这一问题我试着用神秘的 jar:!/ 语法的几种变体来使用 ClassPath(请参阅 说明和线索但是没有一次成功我能 做的就只有分别交付 ajar 和 bjar 并把它们与 mainjar 一起放在文件系统中了但是这正是我想避免的那类事情

进入 JarClassLoader

此时我感到备受挫折我如何才能让应用程序从它自己的 JAR 文件中的 lib 目录装入它自己的类呢?我决定应当创建定制类装入器来承担这个重任编写定制类装入器不是一件容易的事情但是实际上这个工作并没有那么复杂类装入器对它所控制的应用程序有非常深刻的影响所以在发生故障的时候很难诊断和解释故障虽然对于类装入的完整处理超出了本文的范围(请参阅 参考资料)我还是要介绍一些基本概念好保证您能从后面的讨论中得到最大收获

装入类

当 JVM 遇到一个对象的类未知的时候就会调用类装入器类装入器的工作是找到类的字节码(基于类的名称)然后把这些字节传递给 JVMJVM 再把这些字节码链接到系统的其余部分使得正在运行的代码可以使用新装入的类JDK 中关键的类是 javalangClassloader 以及 loadClass 方法摘要如下

public abstract class ClassLoader {

protected synchronized Class loadClass(String name boolean resolve)

throws ClassNotFoundException {}

}

ClassLoader 类的主要入口点是 loadClass() 方法您会注意到 ClassLoader 是一个抽象类但是它没有声明任何抽象方法这样关于 loadClass() 方法是不是要关注的方法一点线索也没留下实际上它不是 要关注的主方法回到过去的好时光看看 JDK 的类装入器可以看到 loadClass() 是您可以有效扩展类装入器的惟一地方但是从 JDK 最好让类装入器单独做它所做的工作即以下工作

检查类是否已经装入

检查上级类装入器能否装入类

调用 findClass(String name) 方法让派生的类装入器装入类

ClassLoaderfindClass() 的实现是抛出一个新的 ClassNotFoundException 异常并且是我们实现定制类装入器时要考虑的第一个方法

JAR 文件何时不是 JAR 文件?

为了能够装入在 JAR 文件内部 的 JAR 文件中的类(这是关键问题您可以回想起来)我首先必须能够打开并读取顶层的 JAR 文件(上面的 mainjar 文件)现在因为我使用的是 java jar 机制所以 javaclasspath 系统属性中的第一个(也是惟一一个)元素是 OneJAR 文件的完整路径名!用下面的代码您可以得到它

jarName = SystemgetProperty(javaclasspath);

我接下来的一步是遍历应用程序的所有 JAR 文件项并把它们装入内存如清单 所示

清单 遍历查找嵌入的 JAR 文件

JarFile jarFile = new JarFile(jarName);

Enumeration enum = jarFileentries();

while (enumhasMoreElements()) {

JarEntry entry = (JarEntry)enumnextElement();

if (entryisDirectory()) continue;

String jar = entrygetName();

if (jarstartsWith(LIB_PREFIX) || jarstartsWith(MAIN_PREFIX)) {

// Load it!

InputStream is = jarFilegetInputStream(entry);

if (is == null)

throw new IOException(Unable to load resource / + jar + using + this);

loadByteCode

上一篇:软件过程的发展的思考

下一篇:特效:在网页中显示可拖动的月历