如果您曾经试图把 Java 应用程序交付为单一的 Java 档案文件(JAR
文件),那么您很有可能遇到过这样的需求:在构建最终档案文件之前,要展开支持 JAR 文件(supporting JAR
file)。这不但是一个开发的难点,还有可能让您违反许可协议。在本文中,Tuffs 向您介绍了 One-JAR
这个工具,它使用定制的类装入器,动态地从可执行 JAR 文件内部的 JAR 文件中装入类。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters -->
<!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
有人曾经说过,历
史总是在不断地重复自身,首先是悲剧,然后是闹剧。 最近,我第一次对此有了亲身体会。我不得不向客户交付一个可以运行的 Java
应用程序,但是我已经交付了许多次,它总是充满了复杂性。在搜集应用程序的所有 JAR 文件、为 DOS 和 Unix(以及
Cygwin)编写启动脚本、确保客户端环境变量都指向正确位置的时候,总是有许多容易出错的地方。如果每件事都能做好,那么应用程序能够按它预期的方式
运行。但是在出现麻烦时(而这又是常见的情况),结果就是大量时间耗费在客户端支持上。
最近与一个被大量
ClassNotFound
异常弄得晕头转向的客户交谈之后,我决定自己再也不能忍受下去了。所以,我转而寻找一个方法,可以把我的应用程序打包到单一 JAR 文件中,给我的客户提供一个简单的机制(比如
java
-jar
)来运行程序。
努力的结果就是 One-JAR,一个非常简单的软件打包解决方案,它利用
Java 的定制类装入器,动态地从单一档案文件中装入应用程序所有的类,
同时保留支持 JAR 文件的结构。在本文中,我将介绍我开发 One-JAR
的过程,然后告诉您如何利用它在一个自包含的文件中交付您自己的可以运行的应用程序。
One-JAR 概述
在介绍 One-JAR 的细节之前,请让我首先讨论一下我构建它的目的。我确定一个 One-JAR 档案文件应该是:
- 可以用
java -jar
机制执行。
- 能够包含应用程序需要的
所有 文件 —— 也就是说,
包括原始形式(未展开)的类和资源。
- 拥有简单的内部结构,仅仅用
jar
工具就可以被装配起来。
- 对原来的应用程序不可见 —— 也就是说,无需修改原来的应用程序,就可以把它打包在 One-JAR 档案文件内部。
问题和解决方案
在开发 One-JAR 的过程中,我解决的最大问题,就是如何装入包含在另外一个 JAR 文件中的 JAR 文件。 Java 类装入器
sun.misc.Launcher$AppClassLoader
(在
java -jar
开始的时候出现)只知道如何做两件事:
- 装入在 JAR 文件的根出现的类和资源。
- 装入
META-INF/MANIFEST.MF 中的 Class-Path
属性指向的代码基中的类和资源。
而且,它还故意忽略针对
CLASSPATH
的全部环境变量设置,还忽略您提供的命令行参数
-cp
。所以它不知道如何从一个包含在其他 JAR 文件中的 JAR 文件装入类或资源。
显然,我需要克服这个问题,才能实现 One-JAR 的目标。
解决方案 1:展开支持 JAR 文件
我为了创建单一可执行 JAR 文件所做的第一个尝试,显然就是在可交付的 JAR 文件内展开支持 JAR 文件,我们把可交付的文件称为 main.jar。假设有一个应用程序的类叫做
com.main.Main
,而且它依赖两个类 ——
com.a.A
(在 a.jar 中) 和
com.b.B
(在 b.jar 中),那么 One-JAR 文件看起来应该像这样:
main.jar | com/main/Main.class | com/a/A.class | com/b/B.class
|
这样,最初来源于 a.jar 文件的
A.class
丢失了,
B.class
也是如此。虽然这看起来只是个小问题,但却会真正带来问题,我很快就会解释为什么。
|
One-JAR 和 FJEP
最近发布的一个叫做 FJEP (FatJar Eclipse Plugin) 的工具支持在 Eclipse 内部直接构建扁平 JAR 文件。
One-JAR 已经与 FatJar 集成在一起,以支持在不展开 JAR 文件的情况下嵌入 JAR 文件。请参阅
参考资料 了解有关详细内容。
|
|
把 JAR 文件展开到文件系统以创建一个扁平结构,这可能非常耗时。还需要使用 Ant 这样的构建工具来展开和重新归档支持类。
除了这个小麻烦之外,我很快又遇到了两个与展开支持 JAR 文件有关的严重问题:
- 如果 a.jar 和 b.jar 包含的资源的路径名相同
(比如说,都是
log4j.properties
),那么您该选哪个?
- 如果 b.jar 的许可明确要求您在重新发布它的时候不能修改它,那您怎么办?您无法在不破坏许可条款的前提下像这样展开它。
我觉得这些限制为另外一种方法提供了线索。
解决方案 2: MANIFEST Class-Path
我决定研究
java -jar
装入器中的另外一种机制:装入的类是在档案文件中一个叫做 META-INF/MANIFEST.MF 的特殊文件中指定的。通过指定称为
Class-Path
的属性,我希望能够向启动时的类装入器添加其他档案文件。下面就是这样的一个
One-JAR 文件看起来的样子:
main.jar | META-INF/MANIFEST.MF | + Class-Path: lib/a.jar lib/b.jar | com/main/Main.class | lib/a.jar | lib/b.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:!/
语法的几种变体来使用
Class-Path
(请参阅 “
说明和线索”),但是没有一次成功。我
能 做的,就只有分别交付 a.jar 和 b.jar ,并把它们与 main.jar 一起放在文件系统中了;但是这正是我想避免的那类事情。
进入 JarClassLoader
此时,我感到备受挫折。我如何才能让应用程序从它自己的 JAR 文件中的
lib
目录装入它自己的类呢?我决定应当创建定制类装入器来承担这个重任。编写定制类装入器不是一件容易的事情。但是实际上这个工作并没有那么复杂,类装入器对
它所控制的应用程序有非常深刻的影响,所以在发生故障的时候,很难诊断和解释故障。虽然对于类装入的完整处理超出了本文的范围(请参阅 参考资料),我还是要介绍一些基本概念,好保证您能从后面的讨论中得到最大收获。
装入类
当 JVM 遇到一个对象的类未知的时候,就会调用类装入器。类装入器的工作是找到类的字节码(基于类的名称),然后把这些字节传递给 JVM,JVM 再把这些字节码链接到系统的其余部分,使得正在运行的代码可以使用新装入的类。JDK 中关键的类是
java.lang.Classloader
以及
loadClass
方法,摘要如下:
public abstract class ClassLoader { ... protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException {...} }
|
ClassLoader
类的主要入口点是
loadClass()
方法。您会注意到,
ClassLoader
是一个抽象类,但是它没有声明任何抽象方法,这样,关于
loadClass()
方法是不是要关注的方法,一点线索也没留下。实际上,它
不是 要关注的主方法:回到过去的好时光,看看 JDK 1.1 的类装入器,可以看到
loadClass()
是您可以有效扩展类装入器的惟一地方,但是从 JDK 1.2 起,最好让类装入器单独做它所做的工作,即以下工作:
- 检查类是否已经装入。
- 检查上级类装入器能否装入类。
- 调用
findClass(String name)
方法,让派生的类装入器装入类。
ClassLoader.findClass()
的实现是抛出一个新的
ClassNotFoundException
异常,并且是我们实现定制类装入器时要考虑的第一个方法。
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(is, jar); ...
|
注意,
LIB_PREFIX
生成字符串
lib/,
MAIN_PREFIX
生成字符串
main/。我想把任何以
lib/ 或
main/ 开始的东西的字节码装入内存,供类装入器使用,并在循环中忽略任何其他 JAR 文件项。
main 目录
前面我已经谈到过 lib/ 子目录的角色,那么 main/ 目录是干什么的呢?
简要来说,类装入器的代理模式要求我把主要类
com.main.Main
放在它自己的 JAR 文件中,
这样它才能找到库类(它依赖的库类)。新的 JAR 文件看起来像这样:
one-jar.jar | META-INF/MANIFEST.MF | main/main.jar | lib/a.jar | lib/b.jar
|
在上面的清单 1 中,
loadByteCode()
方法接受来自 JAR 文件项的流和一个项名称,把项的字节装入内存,并根据项代表的是
类 还是
资源,给它分配最多两个名称。演示这个技术的最好方法是通过示例。假设 a.jar 包含一个类
A.class
和一个资源
A.resource
。One-JAR 类装入器构造以下
Map
结构,名为
JarClassLoader.byteCode
,它对于类只有一对关键字/值组合,而对于资源则有两个关键字。
图 1. One-JAR 在内存中的结构 如
果您多看图 1
一会,您可以看到类项是按照类名称设置关键字的,而资源关键字的设置则根据一对名称:全局名称和局部名称。用来解析资源名称冲突的机制是:如果两个库
JAR 文件都用相同的全局名称定义一个资源,那么则根据调用程序的堆栈帧来采用局部名称。更多细节请参阅 参考资料。
找到类
回忆一下,我在概述类装入的时候,最后介绍的是
findClass()
方法。方法
findClass()
以类的名称作为
String
参数,而且必须找到并定义该名称所代表的字节码。由于
loadByteCode
很好地构建了类名和字节码之间的
Map
,所以实现这个方法现在非常简单:只要根据类名查找字节码,然后调用
defineClass()
,如清单 2 所示:
清单 2. findClass() 摘要 protected Class findClass(String name) throws ClassNotFoundException { ByteCode bytecode = (ByteCode)JarClassLoader.byteCode.get(name); if (bytecode != null) { ... byte bytes[] = bytecode.bytes; return defineClass(name, bytes, pd); } throw new ClassNotFoundException(name); }
|
装入资源
在 One-JAR 开发期间,
findClass
是我把自己的想法付诸实施的第一件事。 但是,当我开始部署更复杂的应用程序时,我发现除了要装入类之外,还必须要处理资源的装入问题。这一次,事情有点棘手。为了查找资源,需要在
ClassLoader
中找到一个合适的方法去覆盖,我选了我最熟悉的一个,如清单 3 所示:
清单 3. getResourceAsStream() 方法 public InputStream getResourceAsStream(String name) { URL url = getResource(name); try { return url != null ? url.openStream() : null; } catch (IOException e) { return null; } }
|
这个时候应当响起警钟:我就是无法理解为什么用 URL 来定位资源。所以我不用这个实现,而是插入我自己的实现,如清单 4 所示:
清单 4. One-JAR 中的 getResourceAsStream() 实现 public InputStream getResourceAsStream(String resource) { byte bytes[] = null; ByteCode bytecode = (ByteCode)byteCode.get(resource); if (bytecode != null) { bytes = bytecode.bytes; } ... if (bytes != null) { return new ByteArrayInputStream(bytes); } ... return null; }
|
最后一个障碍
我对
getResourceAsStream()
方法的新实现看起来解决了问题,但是直到我试着用 One-JAR 来处理一个用
URL url =
object.getClass().getClassLoader().getResource()
模式装入资源的应用程序时,才发现实际情况与想像的不一样。为什么?因为
ClassLoader
的默认实现返回的 URL 是 null,这个结果破坏了调用程序的代码。
这时,事情变得真的是说不清了。我必须弄清应当用什么 URL 来引用 lib/ 目录中的 JAR 文件内部的资源。是不是应该像
jar:file:main.jar!lib/a.jar!com.a.A.resource
这样才好?
我试尽所有我能想到的组合,但是没有任何一个起作用。
jar:
语法就是不支持嵌套 JAR 文件,这使得我的整个 One-JAR 方法好像面临着死路一条。虽然大多数应用程序好像都不使用
ClassLoader.getResource
方法,但是确实有些使用了这个方法,所以我实在不愿意有需要排除的情况,让我说“如果您的应用程序使用
ClassLoader.getResource()
,您就不能用 One-JAR。”
最后的解决方案……
当我试图弄清楚
jar:
语法的时候,我意外地了解到了 Java 运行时环境把 URL 前缀映射到处理器的机制。这成为我修复
findResource
问题所需要的线索:我只要发明自己的协议前缀,称为
onejar:
。
这样,我就能把新的前缀映射到协议处理器,处理器就会返回资源的字节流,如清单 5 所示。注意,清单 5 表示的是两个文件中的代码,这两个文件是 JarClassLoader 和一个叫做
com/simontuffs/onejar/Handler.java 的新文件。
清单 5. findResource 和 onejar: 协议 com/simontuffs/onejar/JarClassLoader.java protected URL findResource(String $resource) { try { // resolve($resource) returns the name of a resource in the // byteCode Map if it is known to this classloader. String resource = resolve($resource); if (resource != null) { // We know how to handle it. return new URL(Handler.PROTOCOL + ":" + resource); } return null; } catch (MalformedURLException mux) { WARNING("unable to locate " + $resource + " due to " + mux); } return null; } com/simontuffs/onejar/Handler.java package com.simontuffs.onejar; ... public class Handler extends URLStreamHandler { /** * This protocol name must match the name of the package in which this class * lives. */ public static String PROTOCOL = "onejar"; protected int len = PROTOCOL.length()+1;
protected URLConnection openConnection(URL u) throws IOException { final String resource = u.toString().substring(len); return new URLConnection(u) { public void connect() { } public InputStream getInputStream() { // Use the Boot classloader to get the resource. There // is only one per one-jar. JarClassLoader cl = Boot.getClassLoader(); return cl.getByteStream(resource); } }; } }
|
|
|
启动 JarClassLoader
到现在,您可能只剩下一个问题了:我是怎样把
JarClassLoader
插入启动顺序,让它首先开始从 One-JAR 文件装入类的?具体的细节超出了本文的范围;但是,基本上说,我没有用主类
com.main.Main
作为
META-INF/MANIFEST.MF/Main-Class
属性,而是创建了一个新的启动主类
com.simontuffs.onejar.Boot
,它被指定作为
Main-Class
属性。新类要做以下工作:
- 创建新的
JarClassLoader
。
- 用新的装入器从 main/main.jar 装入
com.main.Main
(基于 main.jar 中的
META-INF/MANIFEST.MF Main-Class
项)。
- 装入类,用反射调用
main()
,从而调用
com.main.Main.main(String[])
(或者诸如
main.jar/MANIFEST.MF
文件中的
Main-Class
的名称)。在 One-JAR 命令行上传递的参数,被不加修改地传递到应用程序的主方法。
结束语
如果前面这些让您头痛,不要担心:使用 One-JAR 要比理解它的工作方式容易得多。随着
FatJar Eclipse 插件(请参阅
参考资料 中的 FJEP)的推出,
Eclipse 的用户现在只要在向导中选中一个复选框,就可以创建 One-JAR 应用程序。依赖的库被放进
lib/ 目录,主程序和类被放进
main/main.jar,并自动写好 META-INF/MANIFEST.MF 文件。如果您使用 JarPlug(还是请参阅
参考资料),您可以查看您构建的 JAR 文件的内部结构,并从 IDE 中启动它。
总之,One-JAR
是一个简单而强大的解决方案,解决了应用程序打包交付的问题。但是,它没有解决所有的应用程序场景。例如,如果您的应用程序使用老式的 JDK 1.1
的类装入器,不把装入委托给上一层,那么类装入器就无法在嵌套 JAR
文件中找到类。您可以构建和部署一个“包装”类装入器来修改顽固的类装入器,从而克服这个问题,不过这可能需要与 Javassist
或者字节码工程库(Byte Code Engineering Library,BCEL)这样的工具一起使用字节码操纵技术。
对于嵌入式应用程序和 Web
服务器使用的特定类型的类装入器,您还可能遇到问题。特别是对于那些不把装入工作先委托给上一级的类装入器,以及那些在文件系统中查找代码基的装入器,您
可能会碰到问题。不过,One-JAR 中包含了一个机制,可以在文件系统中展开 JAR 文件项,这应当有帮助。这个机制由
META-INF/MANIFEST.MF 文件中的 One-JAR-Expand
属性控制。另外,您可以试着用字节码操纵技术动态地修改类装入器,这样可以不破坏支持 JAR 文件的完整性。如果您采用这种方法,那么每种个别情况可能都需要一个定制的包装类装入器。
请参阅
参阅资料 以下载 FatJar
Eclipse 插件和 JarPlug,并了解更多关于 One-JAR 的内容。
参考资料
关于作者
|
|
|
P. Simon Tuffs 博士是一位独立顾问,目前的研究领域是 Java Web 服务的可伸缩性。在业余时间里,他创建并发布一些开源项目,比如 One-JAR。 |
分享到:
相关推荐
one-jar-boot-0.95.jar
开发人员能够利用它在 Java 应用程序中运行大数据查询、获取分析结果,并将其集成到企业数据流程中。此外,它具备良好的兼容性和扩展性,能够与常见的 Java 数据库工具和框架无缝对接,支持高效的并发查询和数据处理...
webscarab-one-20110329-1330(最新版) webscarab-one-20110329-1330.jar 1、安装JDK 2、鼠标双击webscarab-one-20110329-1330.jar即可
是将Java应用程序及其依赖项jar打包到单个jar文件中的工具。 什么是一罐(JAR)靴子? one-jar-boot是底层的JarClassLoader和其他One-JAR引导机制,其中包含One-JAR引导类的源代码()。 那么这个仓库是做什么用的...
ksoap2-android-assembly-2.6.5-jar-with-dependencies.jar 要是需要最新的,下载地址: http://code.google.com/p/ksoap2-android/
MySQL Connector/J 8.0.30 是 MySQL 官方提供的用于 Java 应用程序的数据库驱动程序,它实现了 JDBC(Java Database Connectivity)规范,使得 Java 开发人员能够方便地在 MySQL 数据库上进行数据操作。这篇内容将...
One-JAR(TM) 是 Java 中一个棘手问题的简单解决方案:当应用程序依赖于多个其他 jar 文件时,如何将其作为单个 jar 文件分发。 One-JAR使用自定义的类加载器来发现主jar中的库jar文件。
hamcrest-library-1.3.rc2.jar ,单元测试JUnit必须引用的jar包,否则会报错无法启动测试程序
这个版本号表明它是RESTClient的3.2.2迭代,且“jar-with-dependencies”意味着这是一个Java Archive (JAR) 文件,集成了项目的所有依赖,便于用户一次性下载和使用。 在进行API开发、测试或调试时,RESTClient提供...
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,被广泛用于Web应用程序之间的数据传输,尤其是AJAX(Asynchronous JavaScript and XML)技术。JSON-Lib是Java平台上的一个库,它提供了处理JSON数据...
总结来说,fat jar技术简化了Java应用程序的分发和运行,通过将所有依赖整合在一个jar文件中。打包工具如Maven的maven-shade-plugin和Gradle的shadow插件是实现这一目标的关键。而JavaLauncher可能是一个自定义的...
总结来说,"mybatis-3.4.1-jar包"是MyBatis框架3.4.1版本的核心组件,它包含所有必要的类和资源,用于简化Java应用程序与数据库之间的交互,提高开发效率,同时保持代码的清晰和可维护性。在Java Web开发中,它是...
"Fat-Jar"打包工具是Java开发者常用的工具之一,它主要用于将Java应用程序的所有依赖库(JAR文件)合并到一个单一的JAR文件中,因此被称为"fat"或"uber"JAR。这个过程解决了运行Java应用时需要管理多个外部库的问题...
这两个库在Web应用程序中尤其常见,因为它们提供了处理HTTP请求中的多部分数据(如表单上传的文件)的便捷方法。 `commons-fileupload-1.2.1.jar`是Apache Commons FileUpload项目的版本1.2.1的实现,它专注于处理...
API JAR文件包含JSF的公共接口和类,允许开发者在应用程序中引用和使用JSF的功能,如创建可重用的UI组件、处理用户事件、数据绑定等。 3. **jsf-impl.jar**:与jsf-api.jar相对应,这个文件包含了JSF的实现代码。在...
Java-WebSocket jar包,封装WebSocket实现。
hibernate-jar包是Java开发中用于对象关系映射(ORM)的工具,它将数据库操作转换为对Java对象的操作,简化了数据库驱动的应用程序的编写。在给定的压缩包文件中,我们看到一系列与Java开发相关的库,这些库可能与...
SAP Java JCo(JCo,全称Java Connector)是SAP公司提供的一个软件开发工具包,用于在Java应用程序和...正确理解和使用SAP Java JCo-2.1.8-jar文件,将极大地提升开发效率,确保Java应用程序能够高效地与SAP系统交互。
Java -jar 命令是Java开发中一个非常实用的功能,它允许开发者将Java应用程序打包成JAR(Java Archive)文件,便于分发和...这不仅简化了部署流程,还增强了JAR文件的可用性,使得Java应用程序的分发和执行更加便捷。
Servlet-api.jar和jsp-api.jar是Java Web开发中两个非常重要的库文件,它们包含了Servlet和JSP(JavaServer Pages)的相关API,使得开发者可以构建动态Web应用程序。这两个文件通常由Java EE(Enterprise Edition)...