Java 扩展机制在Java教程中被描述为一种“通过标准可扩展的方式来让Java平台上所有应用使用自定义API”。正如在理解扩展机制进行类加载中描述的,“扩展框架充分使用了类加载代理机制”。这种机制会在rt.jar引导(boot)类加载之后,标准classpath中的类加载之前,加载扩展类。
扩展目录的工作机制在类的加载上与classpath有点类似。对Java应用程序来说,所有扩展目录下JAR文件包含的类都可以访问。然而,会有一些关键的不同点。这些区别会在下面的文字中高亮显示。
特征 | Classpath | 扩展机制(可选包) |
---|---|---|
作用域 | 典型的应用相关
主机上所有可能的JRE
|
所有运行在特定JRE上的JVM
各种主机上的JRE
|
如何指定 | .jar文件
.class Files
|
所有在指定目录下的JAR文件都会被加载(即使扩展名不是.jar或者没有扩展名) |
类加载顺序 | 引导和扩展类加载之后 | 引导类加载之后,classpath上的类加载之前 |
一个最重要且值得重视的问题是,扩展机制会找出所有jar格式的文件,即使文件后缀名不是.jar。这意味着,改变classpath中的jar文件后缀名以此逃过通配符的筛选,这种方法在扩展目录中行不通。
我会用一些简单的例子来展示一些上面提到的区别。接下来的两段代码是一个简单的HelloWorld类和一个main应用程序中的Main类。Main通过调用main方法来使用HelloWorld类。
HelloWorld.java
- public class HelloWorld
- {
- @Override
- public String toString()
- {
- return "Hello, World!";
- }
- }
Main.java
- import static java.lang.System.out;
- public class Main
- {
- public static void main(final String[] arguments)
- {
- out.println(new HelloWorld());
- }
- }
为了展示classpath和扩展机制的主要区别,我将会把编译过的HelloWorld.class文件归档到一个jar包里,命名为HelloWorld.jar。并把它放在一个跟编译过的Main.class不同的目录下。
为了展示传统的classpath的使用,我把HelloWorld.jar放在一个叫做C:\hello的目录下并且会用通配符访问JAR来给Main使用。下面的两个截图对此进行了展示。
以上两个截图说明,尽管我删掉了当前目录下的HelloWorld.class,Java 主应用仍然能加载它。这是因为Java launcher被告知(通过-classpath这个可选参数)去C:\hello目录下寻找。使用扩展机制,不需要把类放到当前目录或者指定到 classpath下就可以加载。接下来的截图展示了这一点。
上面的截图说明,当某个类是在扩展目录下的某个JAR里,Java launcher甚至不需要把HelloWorld.class放到同一个目录下或者在classpath中指定。这常常被用来说明使用扩展机制的优点。因为所有在这个JRE(或者可能是主机上的所有应用)上运行的程序都可以不用在classpath上指定就能看到扩展目录下的类。
使用传统classpath方式——指导应用去加载JAR中的类,包含.class文件的JAR文件必须以.jar结尾。接下来的截图展示了当把在 classpath引用的目录下的HelloWorld.jar重命名为HelloWorld.backup之后所发生的事情。
上面这张图展示了当classpath引用的目录下JAR文件没有以.jar结尾时发生的NoClassDefFoundError异常。可能有点令人惊讶,扩展机制不是这样工作的。所有在扩展目录下的JAR文件,不管后缀名是什么甚至没有后缀名都会被加载。接下来的截图展示了这一点。
这张图展示了,给在扩展目录中的JAR文件重命名为没有后缀的文件并不妨碍类加载器加载JAR文件中的类。换句话说,类加载机制是通过文件类型而不是文件名或后缀来加载所有在扩展目录中的JAR文件的。正如可选包(Optional Package)概览所总结的,“JAR文件本身没有什么特别的地方,其中包含的class文件也没有让JAR成为已安装过的可选包。只有位于jre/lib/ext下,才可能让JAR成为已安装的可选包。”
在扩展目录中放包含太多类定义的JAR会有一些风险和负面效果。例如,当我们看到classpath中所指定的类方法存在,还报出NoSuchMethodErrors异常,会令人非常恼火。这是我以前写过众多可以导致NoSuchMethodError问题的其中一个。但是忘记扩展目录下JAR文件中的过时(outdated)和废弃的(obsolete)类是另一个潜在的原因。接下来会展示这一点。
接下来的两段代码展示了Main.java和HelloWorld.java修改后的版本。特别要注意的是,HelloWorld有一个全新的方法,这个 方法会被新版本的Main调用。在这个例子中,我会把新编译的HelloWorld.class文件和Main放在同一个目录下。这样当我运行Main 的时候,就能证明扩展目录下的JAR中过时的类会比当前目录下的新类优先加载。
修改后的Hello World.java(新方法)
- public class HelloWorld
- {
- @Override
- public String toString()
- {
- return "Hello, World!";
- }
- public String directedHello(final String name)
- {
- return "Hello, " + name;
- }
- }
修改后的Main.java
- import static java.lang.System.out;
- public class Main
- {
- public static void main(final String[] arguments)
- {
- final HelloWorld helloWorld = new HelloWorld();
- out.println(helloWorld);
- out.println(helloWorld.directedHello("Dustin"));
- }
- }
***一张截图展示了,扩展目录下过时的HelloWorld类优先于同一目录下的新定义的HelloWorld类加载。甚至当我把当前目录写进 classpath中,扩展目录下的旧版本的类仍然优先。接下来的图也同样展示了扩展目录下的JAR文件“隐藏”了更新的JAR以及其中类的新方法。这些扩展目录下的JAR文件甚至都不是以.jar结尾的。
刚刚展示的这个例子,在扩展目录下JAR导致的众多问题来说不算很复杂。例子中,至少有一个NoSuchMethodError来提醒这个问 题。一个潜在的更加复杂的情况是,旧的类有和新类一样的方法签名但实现的方式已经过时。在这种情况下,可能没有错误、异常或者throwable中任何一种,但是应用的逻辑不会像预期那样工作。旧的方法可能会一直存在代码的底层直到被发现。当缺乏单元测试或其他测试时尤其如此。
使用扩展目录会让开发人员变得轻松。因为扩展目录下JAR文件中的类,可以被所有运行在与此扩展目录(如果在操作系统上在主机范围内启用扩展目录,那么所有主机上的JRE都可以访问)关联JRE上的应用访问。然而,随意使用扩展目录会有一定的风险。你会非常容易忘记扩展目录下过时的类。这会妨碍类加载器选择明显应当被加载的版本。这种情况下,本来应该让开发者感觉轻松的扩展机制会让他们非常痛苦。
Elliotte Rusty Harold提对扩展机制有一个警告:“尽管这些看上去很方便,从长远来看也是引入了一个隐患,迟早你会从一个你根本没想过的地方载入一个错误的类版本。这会浪费你不少时间调试”。Java教程同样提出警告(我在这里也着重强调):“尽管这个机制扩展了平台的核心API,但是应该审慎使用。大部分情况,它是用于像JCP这样标准化比较好的接口,同时也适用于整个站点的接口”。
尽管扩展(可选包)机制与classpath机制很像,并且它们都用于部分的类加载,两者之间的区别也是非常值得注意的。特别的,记住所有的在扩展目录下的JAR文件(即使它们没有以.jar结尾)都会被加载是很重要的。给那些JARs重命名甚至改变他们的文件后缀名都不足以让类加载器忽略它们。另一方面,使用classpath的时候,重命名classpath中指定的JAR文件会使该JAR无法加载,改变后缀名后,即使在classpath中使用通配符也无法加载所有目录中的JAR。
一些情况下,扩展机制是比较好的选择,但是这种情况相当少。当处理预期以外的NoSuchMethodErrors问题时,记住扩展机制使很重要的。这样就会去检查看看是否问题就出在扩展的目录中。
原文链接: marxsoftware 翻译: ImportNew.com - 孟 冰川