one-jar 本地文件_通过One-JAR简化您的应用交付

本文介绍了One-JAR,一个解决Java应用程序打包难题的解决方案,允许将所有依赖项打包到单个可执行JAR文件中。文章详细讨论了在创建One-JAR时遇到的问题,如加载嵌套JAR文件的挑战,以及如何通过定制类加载器JarClassLoader实现这一目标。One-JAR通过使用特殊的协议前缀和类加载机制,使得应用程序可以在单个文件中运行,简化了部署和执行过程。
摘要由CSDN通过智能技术生成

人们注意到,历史重演,首先是悲剧,然后是闹剧。 最近,当我不得不将可运行的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文件(我们称为main.jar)中扩展支持的JAR文件。 给定一个名为com.main.Main的应用程序类,并假设它依赖于两个类com.aA (在a.jar内)和com.bB (在b.jar内)– One-JAR文件如下所示:

main.jar
    |  com/main/Main.class
    |  com/a/A.class
    |  com/b/B.class

A.class最初来自a.jar的事实以及A.class的原始位置B.class 。 尽管这似乎只是个小问题,但可能会引起实际的问题,正如我稍后会解释的那样。

将支持的JAR文件扩展到文件系统以创建平面结构可能会非常耗时。 它还需要使用诸如Ant之类的构建工具来扩展和重新归档支持类。

除了这种小麻烦之外,在扩展支持的JAR文件时,我很快遇到了两个严重的问题:

  • 如果a.jar和b.jar包含一个具有相同路径名的资源(例如log4j.properties ),那么您会选择哪一个?
  • 如果b.jar许可证明确要求您以未经修改的形式重新分发它,该怎么办。 在不违反该许可条款的前提下,您不能像这样扩展它。

我认为这些限制值得另一种方法。

解决方案2:清单类路径

我决定研究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

这个工作了吗? 好吧,直到我将main.jar文件移动到其他地方并尝试运行它为止。 为了组装main.jar,我创建了一个名为lib的子目录,并将a.jar和b.jar压入其中。 不幸的是,应用程序类加载器只是从文件系统中获取支持的JAR文件。 它不是从嵌入式JAR文件中加载类。

为了解决这个问题,我尝试使用Class-Path ,并在相当神秘的jar:!/语法上使用了几种变体(请参阅“ 注释和线索 ”),但是我什么都无法工作。 我可以做的是分别交付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文件条目。

主目录

我已经讨论过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类加载器构建以下名为JarClassLoader.byteCode Map结构, JarClassLoader.byteCode具有用于类的单个键值对和用于资源的两个键。

图1. One-JAR的内存结构
图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. getResourceAsStream()的一个JAR实现
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()方法的新实现似乎可以解决问题,直到我尝试对使用URL url = object.getClass().getClassLoader().getResource()模式加载资源的应用程序进行一次JAR操作之前,该方法一直存在。 在这一点上,事情崩溃了。 为什么? 由于由ClassLoader的默认实现返回的URL为null,因此破坏了调用者代码。

在这一点上,事情开始变得非常混乱。 我必须弄清楚应该使用什么URL来引用lib /目录中JAR文件中的资源。 会像jar:file:main.jar!lib/a.jar!com.aAresource吗?

我尝试了所有可能想到的组合,但没有一个起作用。 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条目)。
  • 调用com.main.Main.main(String[])或任何的名称Main-Class是在main.jar/MANIFEST.MF文件)通过装载类和使用反射来调用main() 在One-JAR命令行上传递的参数将直接传递给应用程序main方法,而无需进行修改。

结论

如果这一切使您烦恼,请不要担心:使用One-JAR比尝试了解其工作原理要简单得多。 随着FatJar Eclipse插件的出现(见FJEP的相关信息 ),Eclipse用户现在可以选择在向导复选框创建一个-JAR应用程序。 从属库放置在lib /目录中,主程序和类放置在main / main.jar中,并且META-INF / MANIFEST.MF文件是自动编写的。 如果你使用JarPlug(再次参见相关主题 ),您可以查看您构建的JAR文件中,并从IDE中启动它。

总体而言,One-JAR是一个简单但功能强大的解决方案,用于解决包装应用程序的交付问题。 但是,它并不能提供所有可能的应用方案。 例如,如果您的应用程序使用未委托其父级的较旧样式的JDK 1.1类加载器,则该类加载器将无法从嵌套的JAR文件中定位类。 您可以通过构建和部署“包装”类加载器来修改顽强的类加载器来克服此问题,尽管这需要使用字节码操纵技术和Javassist或字节码工程库(BCEL)之类的工具。

您可能还会遇到嵌入式应用程序和Web服务器使用的某些特定类型的类加载器的问题。 具体来说,您可能会遇到以下问题:没有首先委派给父类加载器的类加载器,或那些在文件系统中查找代码库的类加载器。 One-JAR包含一种可以将JAR文件条目扩展到文件系统中的机制应该会有所帮助。 该机制由META-INF / MANIFEST.MF文件中的One-JAR-Expand属性控制。 或者,您可以尝试使用字节码操作即时修改类加载器,而不会破坏支持的JAR文件的完整性。 如果走这条路,每种情况可能都需要定制的包装类加载器。

请参阅相关主题下载FatJar Eclipse插件和JarPlug,并更多地了解一个-JAR。


翻译自: https://www.ibm.com/developerworks/java/library/j-onejar/index.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值