One-Jar之旅

One-Jar之旅

    
1             问题的提出
作为一个经常使用Java编程的程序员,当我在发布我的Java程序的时候,我习惯于这样组织所有的程序和资源:主程序放到JVM系统变量“user.dir”所指向的目录中(假设是MyAppDir目录),程序所用到的工具类(通常是打好包的jar文件)放到MyAppDir/lib/目录,其他资源(如图片等)放到MyAppDir/res/目录,然后写一个批处理文件,在里面设置好环境变量和各种路径,最后将所有的这些东西交给用户。但是如果这个程序不是很大的话,是不是心里觉得这么多目录和文件有点不够简洁?会不会觉得运行bat文件出来的那个Dos窗口有点碍眼?会不会羡慕Windows编程人员可以把所有资源和程序都放到一个exe文件中?下面我们就来着手解决这些问题。
2             解决方案
我们先提出一个实际的例子来说明这个问题: 我的应用程序的主类为 cn.edu.hlju.ica.app.Main ,它位于 mainApp.jar 。Main类需要用到位于tools.jar中的com.util.Tool类。通常的做法是将tools.jar放到MyAppDir/lib/目录下,然后执行
java  –classpath  .;./lib/tools.jar  –jar  mainApp.jar
但是如果我想把tools.jar和mainApp.jar打包到一起,把它们都放到OneJarApp.jar中,然后只需要执行
java –jar OneJarApp.jar
这样该多好啊,尤其是当你需要的jar文件比较多的时候,无论是这些jar包的组织,还是bat文件的书写都是挺麻烦的事。下面我们就分析一下解决方法。
n        第一种:也是最容易想到的方法,就上例来说,我们可以将tools.jar和mainApp.jar都展开,然后将com.util.Tool(本来位于tools.jar中)和cn.edu.hlju.ica.app.Main(本来位于mainApp.jar中)一起打包成OneJarApp.jar。有很多开发环境如JBuilder或干脆用jar命令都可以很容易做到,但是这样做会产生一些很严重的问题:如果Main类不只需要一个而是需要两个分别位于toolsOne.jar和toolsTwo.jar中的类com/a/A.class和com/b/B.class,而A和B都需要引用在各自的jar文件中com/目录下的一个资源,比如说是com/log4J.property,这时如果将它们展开,并入到一个jar文件的时候,你该选择哪一个资源呢?再有,如果toolsTwo.jar的许可明确要求你再重新发布之前不能修改它,你该怎么办?这种方法显然不够通用,而且在某些情况下回出现问题。
n        第二种:在回过头来看我们的实例,既然不能把现有的tools.jar文件展开,那我们干脆就把它和mainApp.jar一起打包成OneJarApp.jar中行吗?即下面的组织形式:
OneJarApp.jar
              |-- META-INF/MANIFEST.MF
              |--mainApp.jar
                     |--cn/edu/hlju/ica/app/Main.class
              |--tools.jar
                     |--com.util.Tool
然后我们在 MANIFEST.MF 中制定Class-Path属性,希望类装载器在启动程序的同时也可以正确
的装载其他jar文件。很不幸,这样做是行不通的。因为系统类装载器并不能从嵌入的jar
(tools.jar就是嵌入到了OneJarApp.jar中 )文件中读取所需的类。所以我们需要使用自定义类
装载器来完成这项工作,不过不用担心,开源工程One-JAR已经为我们做好了相关工作,我们只需
很少几步操作就可以达成目的。
3             One-Jar简介
One-Jar是SourceForge上的一个开源工程,它使用自定义的类装载器从嵌入的jar文件中载入类和资源,从而可以使原来的程序中所有用到的jar文件(包括主类所在的jar文件)都打包到一个jar文件中,且原来的程序不需要做任何修改,然后我们就可以简单的通过java –jar OneJarApp.jar来运行该应用程序,是不是很理想?
该工程短小精悍,只有三个类,分别是:
com.simontuffs.onejar.JarClassLoader
com.simontuffs.onejar.Boot
com.simontuffs.onejar.Handler
它的实现原理是:程序运行之初首先由One-Jar接手,它先通过其自定义的类装载器装载嵌入的各个jar文件,然后利用reflectionAPI调用你的应用程序的主函数,使你的应用程序得以执行。但是它对各个嵌入的jar文件在宿主jar文件中的位置有一定的要求,即含有主类的jar文件必须放在宿主jar文件的main/目录下,其他的jar文件放到宿主jar文件的lib/目录下。还是上面的例子,主类cn.edu.hlju.ica.app.Main位于mainApp.jar中,Main所用到的com.util.Tool位于tools.jar中,最终要将它们都打包成OneJarApp.jar,其目录结构应如下
OneJarApp.jar
              |-- META-INF/MANIFEST.MF
              |--main
|--mainApp.jar
                            |-- META-INF/MANIFEST.MF
                            |--cn/edu/hlju/ica/app/Main.class
              |--bin
|--tools.jar
                            |--com.util.Tool
              |--com/simontuffs/onejar
                     |-- Boot.class
                     |-- Handler.class
                     |-- JarClassLoader.class
注意,这里的(红色) MANIFEST.MF One-JAR 工程自带的
,而不是你的应用程序的(绿色) MANIFEST.MF (你应用程序的 MANIFEST.MF mainApp.jar中
);而 One—Jar 工程的三个类也必须包含在OneJarApp.jar中。
下面是各个类的代码
Main.java
package cn.edu.hlju.ica.app;
import com.util.*;
public class Main {
                     public static void main(String[] args) {
                            new Tool();
                     }
              }
              Tool.java
              package com.util;
public class Tool {
                     static {
                            System.out.println("创建了一个Win32版的Tool实例");
                     }    
}

目录结构如下
One-Jar之旅
One-Jar之旅
最外层的 MANIFEST.MF 文件内容如下:
Manifest-Version: 1.0
Main-Class: com.simontuffs.onejar.Boot
One-Jar-Expand: expand,doc
执行结果
One-Jar之旅
4             扩展应用
仅仅做到以上功能是不够的,我们在解决实际问题时还会遇到一些更为复杂的情况。比如说如果我们要做串口通信的程序,需要用到SUN的串口包comm.jar,但是Windows平台和Linux平台的串口包虽然API名字相同但是内部实现却不同。所以如果我的程序需要跨平台使用的话,就需要在执行期动态选择相应的串口包。但是按照上面的方法,我们的两个串口包都要放宿主jar文件的lib/目录下,这时会发生什么情况呢?会不会发生混淆呢?答案是会,并且One-Jar也作了一些比较初级的处理操作。还是拿我们上面那个例子。我的程序要同时支持Windows平台和Linux平台,所以我的com.util.Tool有两个版本,分别打包到tools_windows.jar和tools_linux.jar中,然后我将它们都放到OneJarApp.jar的lib/目录下,执行-Done-jar.verbose来查看执行的详细信息:
One-Jar之旅
我们看到,2的位置One-Jar的ClassLoader载入了Linux版的Tool;3的位置上,类装载器又试图载入Tool类的windows版,但是从下面画线部分我们可以看到,Tool的windows版并没有载入成功,因为类装载器不能两次载入同一个类,虽然它们实现不同,但是它们的名字一样,都是com.util.Tool,所以windows版就被之前载入的Linux版屏蔽了。从4看到程序可以正常运行。但是One-Jar并没有给我们控制到底选哪一个的权力,所以我们需要对One-Jar修改和扩充。
我们的目标是One-Jar的自定义的类装载器,我们只需要控制其类的装载过程,即JarClassLoader的load(String,String)方法,就可以达到我们的目的。我们首先看一下load()方法的主要部分:
public String load(String mainClass, String jarName) {
………省略
       if (jarName == null) {
                            jarName = System.getProperty(JAVA_CLASS_PATH);
                     }
                     //获得jarName所代表的jar文件中所包含的的全部项目
                     JarFile jarFile = new JarFile(jarName);
                     Enumeration myEnum = jarFile.entries();
                     Manifest manifest = jarFile.getManifest();
                     while (myEnum.hasMoreElements()) {
                            JarEntry entry = (JarEntry)myEnum.nextElement();
                            if (entry.isDirectory())
                                   continue;
                            ………省略
                            //打印提示信息
                            INFO("caching " + jar);
                                VERBOSE("using jarFile.getInputStream(" + entry + ")");
                                   {
// Note: loadByteCode consumes the input stream, so make sure //its scope
                                          // does not extend beyond here.
// 注意以下部分
                                          InputStream is = jarFile.getInputStream(entry);
                                          if (is == null)
throw new IOException("Unable to load resource /" + jar + " using " + this);
                                          loadByteCode(is, jar);      
                                   }
                            ………省略
}
请注意看红颜色的部分,One-Jar在这里直接用 JarEntry entry生成输入流,再由loadByteCode()载入其字节
码,请注意,这里的entry就是要嵌入的jar文件对象,即tools_windows.jar或tools_linux.jar,如果我们在语句
InputStream is = jarFile.getInputStream(entry);
之前判断一下本地操作系统的类型,再结合jar文件的后缀”_windows”或”_linux”就可以选择性的载入与
本地操作系统相配的jar文件了。为此,我生成了JarClassLoaderEx类,它是JarClassLoader的子类,并且只
覆些了JarClassLoader的load(String,String)方法。我们需要通过JVM属性(我自己定义的属:loader.type)
进行控制,如果loader.type=exLoader,则选择采用JarClassLoaderEx,否则保持不变,仍然采用
JarClassLoader。为达到这一目的,我们需要在com.simontuffs.onejar.Boot类中判loader.type属性的值如果
是ExLoader就生成JarClassLoaderEx实例,否则就生成JarClassLoader实例。重要代码片断如下:
One-Jar之旅
这里我定义了一个osType,用来判定本地操作系统的类型。接下来
One-Jar之旅
我们通过jar文件的后缀,就可以判断它是否是适合本地平台了,如果不适合就跳过此jar文件的装载过程。上
面已经有了Windows版的Tool类,这里再给出Linux版的Tool类的实现:
Tool.java--------Linux
              package com.util;
public class Tool {
                     static {
                            System.out.println("创建了一个Linux版的Tool实例");
                     }    
}
下面我们来看一下结果,首先我们设置JVM系统属性”os.name”为Windows系统时:
当我们设定”os.name”为Linux系统时:
5             后记
通过研究Boot类的代码我们可以看到,One-Jar有一个默认的设置JVM系统变量的的机制,即将一个one-jar.property的文件放到最终的jar文件的根目录下:
One-Jar之旅
你可以在其中设置一些你自己的属性对,由One-Jar负责帮你将其添加加到JVM中。
6             结束语
至此,我们的 One-Jar 之旅也告一段落了,相信你看完这篇短文后也能自己动手定制属于你自己的 One-Jar 来实现更丰富的功能,充分享受开源给我们带来的美妙世界。我是用 Eclipse 来编写全部代码的,如果有需要源码的话,请和我联系。

你可能感兴趣的:(java)