假如您曾经试图把 java 应用程序交付为单一的 Java 档案文件(JAR 文件),那么您很有可能碰到过这样的需求:在构建最终档案文件之前,要展开支持 JAR 文件(supporting JAR file)。这不但是一个开发的难点,还有可能让您违反许可协议。在本文中,Tuffs 向您介绍了 One-JAR 这个工具,它使用定制的类装入器,动态地从可执行 JAR 文件内部的 JAR 文件中装入类。 有人曾经说过,历史总是在不断地重复自身,首先是悲剧,然后是闹剧。 最近,我第一次对此有了亲身体会。我不得不向客户交付一个可以运行的 Java 应用程序,但是我已经交付了许多次,它总是布满了复杂性。在搜集应用程序的所有 JAR 文件、为 DOS 和 Unix(以及 Cygwin)编写启动脚本、确保客户端环境变量都指向正确位置的时候,总是有许多轻易出错的地方。假如每件事都能做好,那么应用程序能够按它预期的方式运行。但是在出现麻烦时(而这又是常见的情况),结果就是大量时间耗费在客户端支持上。
最近与一个被大量 ClassNotFound 异常弄得晕头转向的客户交谈之后,我决定自己再也不能忍受下去了。所以,我转而寻找一个方法,可以把我的应用程序打包到单一 JAR 文件中,给我的客户提供一个简单的机制(比如 java -jar)来运行程序。
努力的结果就是 One-JAR,一个非常简单的软件打包解决方案,它利用 Java 的定制类装入器,动态地从单一档案文件中装入应用程序所有的类,同时保留支持 JAR 文件的结构。在本文中,我将介绍我开发 One-JAR 的过程,然后告诉您如何利用它在一个自包含的文件中交付您自己的可以运行的应用程序。
说明与线索 URLClassloader 是 sun.misc.Launcher$AppClassLoader 的基类,它支持一个相当神秘的 URL 语法,让您能够引用 JAR 文件内部的资源。这个语法用起来像这样: jar:file:/fullpath/main.jar!/a.resource。
从理论上讲,要获得一个在 JAR 文件 内部 的 JAR 文件中的项,您必须使用像 jar:file:/fullpath/main.jar!/lib/a.jar!/a.resource 这样的方式,但是很不幸,这么做没有用。JAR 文件协议处理器在找 JAR 文件时,只熟悉最后一个 “!/” 分隔符。
但是,这个语法确实为我最终的 One-JAR 解决方案提供了线索……
这能工作么? 当我把 main.jar 移动到另外一个地方,并试着运行它时,似乎是可以了。为了装配 main.jar ,我创建了一个名为 lib 的子目录,并把 a.jar 和 b.jar 放在里面。不幸的是,应用程序的类装入器只从文件系统提取支持 JAR 文件,而不能从嵌入的 JAR 文件中装入类。
JAR 文件何时不是 JAR 文件? 为了能够装入在 JAR 文件内部 的 JAR 文件中的类(这是要害问题,您可以回想起来),我首先必须能够打开并读取顶层的 JAR 文件(上面的 main.jar 文件)。现在,因为我使用的是 java -jar 机制,所以, java.class.path 系统属性中的第一个(也是惟一一个)元素是 One-JAR 文件的完整路径名!用下面的代码您可以得到它:
jarName = System.getProperty("java.class.path");
我接下来的一步是遍历应用程序的所有 JAR 文件项,并把它们装入内存,如清单 1 所示:
清单 1. 遍历查找嵌入的 JAR 文件
JarFile jarFile = new JarFile(jarName); Enumeration enum = jarFile.entries(); while (enum.hasMoreElements()) { JarEntry entry = (JarEntry)enum.nextElement(); if (entry.isDirectory()) continue; String jar = entry.getName(); if (jar.startsWith(LIB_PREFIX) jar.startsWith(MAIN_PREFIX)) { // Load it! InputStream is = jarFile.getInputStream(entry); if (is == null) throw new IOException("Unable to load resource /" + jar + " using " + this); loadByteCode