ClassLoader解决方案只需要投入一次成本,它提供了一个解决类版本冲突的方法
最近,我不断听到同事和熟人抱怨J2EE应用服务器中出现的软件版本冲突。这个基础问题由来已久,但是,随着应用程序与应用服务器之间共享的Java库日益增多,这个问题似乎也越来越严重。当应用服务器使用一个Java包的A版本,而位于这台服务器上的应用程序却使用这个包的B版本时,如果这两个版本不兼容,那么就会产生版本冲突。当应用程序试图使用这个包,系统加载的是版本A中的类,而不是B版本中的类。如果这两种类的行为不同,就会出现问题。
这种情况相当普遍,部分原因是因为如此多的应用服务器都在某种程度上依靠于开源软件。商业性软件的发布周期通常没有开源软件的发布周期那么短。因此,新发布的应用服务器经常不包含一些库的最新版本。另外,企业软件升级周期又落后于供应商的发布周期,而且为了保持稳定性,有时候也会跳过发布周期。结果,开发人员想当然地使用最新最好的Java库,一头扎进组合更旧版本的一些库的J2EE服务器中。
这个问题也会出现在其他方向上。在升级应用服务器时,多年来一直使用的企业应用软件可能会遇见兼容问题。如果程序依靠于与应用服务器打包在一起的旧版本的库,那么在程序试图访问一个不存在的API元素时,新的库会引发运行时异常,比如NoSuchMethodException。
很多时候,程序员没有注意到正在使用的是不同版本的库(这里,我认为类是Java包的集合),因为他们没有使用已经变化了的那一部分API,或者没有引起在后来版本中得到修正的缺陷。问题一旦发生,开发人员就不得不想办法解决它。替换应用服务器库会使应用服务器或者该服务器上的其他应用程序中断。如果开发人员没有对应用服务器的管理控制权,那么这个解决方案根本不可行。
共享和类共享
这个问题的症结在于大多数J2EE供应商为他们的产品设计了一个类加载层次结构,这个层次结构最终会把类加载委托给应用服务器的类加载程序。即使给每个Web应用程序指派单独的类加载程序,防止Web应用程序彼此干涉,服务器本身使用的库仍然可以被所有Web应用程序共享。我想肯定一些产品没有这个问题,但是我听到的有关报道说,这个问题几乎出现于大多数主要的开放源代码产品和封闭源代码产品上。
编程人员用来解决版本冲突的常用方法是:将库的源代码中的包的名称改成只由其自己代码使用的惟一名称,重新编译库,并将所有导入语句及其引用更新为其源代码中的包的名称。这个解决方案只在访问库的源代码时起作用,然而并不是总是如此。尽管源代码修正和重新编译是一个短期可靠的解决方案,从长远角度来看,实际上它花费的时间更长。编写shell脚本或使用IDE插件能够使包自动重新命名。
查找并替换源代码中出现的所有包的名称,这样将捕获包名称在包声明和导入语句之外的地方的使用情况,比如说,在使用完全限定类名称时,或有在用于反射的字符串中插入名称时。配置文件和其他支持文件都可以包含名称,并且反射中使用的一些名称可以自动生成。您不必总是仔细地检查代码来重新命名某一个名称的所有实例。更重要的是,当升级到新版本时,必须重复一遍整个过程。基于以前的修改的补丁文件不会重新命名出现在新版本中的所有包的名称,并且shell脚本可能要求进行更新来应对代码更改。
最后,源代码修正为维护带来了一个难题。您要花费人手和时间来维护一个原本不需要维护的东西,它实际上是源代码树的一个独立分支。
源代码修正的两个主要缺陷是:必须访问源代码,而维护源代码需要做相当多的工作。维护一个单独的库的分支看起来似乎不是十分困难,但是那些想解决这个问题的人必须解决与多个库的冲突。
源代码包重名命名的一个替换解决方案是重写二进制类文件。重写类文件有一个好处:不需要维护一个单独的源代码分支,也不需要维护源代码。惟一需要的是JAR文件。专门重新命名JAR文件中的包的工具很少,但是大多数代码混淆工具(code obfuscation tool)都有重新命名包和JAR文件中包含的类的能力。用这个方法可以使库的升级变得容易一些。您所要做的就是用重写工具处理JAR文件,然后就大功告成。
想做便做!
尽管类文件重写看上去似乎很有效,但实际上这还不是一个完美的解决方案。当库使用反射以及在字符串或配置文件中嵌入包的名称时,这种方法不起作用。它还不能把您从更改应用程序源代码中包名称的使用方式的不懈努力中解脱出来。
一个更全面的解决方案是做一些应用服务器供应商应该首先作的事情,然后通过使用一个自定义类加载器把您的库的副本与服务器的库的副本分离开。要做到这一点,必须编写一些额外的代码,但是不必改变现有源文件使用包名称的方式。库升级变得简单是因为您只需使用新的JAR文件取代旧的文件即可。如何做到这一点的呢?
版本冲突的根源是应用服务器的类加载设计。Web应用程序类加载器在试图自己定位一个类之前,把类的加载委托给了这个类的父类加载器。因此,如果应用服务器的类加载器能够在系统位置上找到这个类,那么它会加载那个版本,而不是加载和Web应用程序一起打包的那个版本。如果使用您自己的没有父类的类加载器来引导应用程序,那么您就可以绕过应用服务器使用的库。
作为这项技巧的一个例子,我定义了一个叫做Printer的接口和一个叫做VersionPrinter的实现类,这个类表示一个应用程序。 VersionPrinter依靠于Version类,但是需要特定的5.0.0版本。然而,应用服务器用的是1.0.2版本。因此,在调用VersionPrinter.print时,就会输出字符串“version: 1.0.2”。
哪一个版本?
清单1. VersionPrinter使用一个新的5.0.0版本的Version,但是应用服务器装载的是老的1.0.2版本。
public interface Printer {
public void print();
}
public class VersionPrinter implements Printer {
public void print() {
Version v = new Version();
System.out.println("version: " + v.getVersion());
}
}
public class Version {
public String getVersion() {
return "1.0.2";
}
}
public class Version {
public String getVersion() {
return "5.0.0";
}
}
自定义类加载
清单2. 使用自定义类加载器和一个动态代理,您可以绕过应用服务器的类路径。
package example;
import java.io.*;
import java.net.URL;
import java.net.URLClassLoader;
import java.lang.reflect.*;
public final class Main {
static class PrinterInvoker implements
InvocationHandler {
Object adaptee;
public PrinterInvoker(Object adaptee) {
this.adaptee = adaptee;
}
public Object invoke(
Object proxy, Method method, Object[] args)
throws Throwable
{
Method adapteeMethod =
adaptee.getClass().getMethod(
method.getName(),
method.getParameterTypes());
if(!adapteeMethod.isAccessible())
adapteeMethod.setAccessible(true);
return adapteeMethod.invoke(adaptee, args);
}
}
public static final void main(String[] args)
throws Exception
{
VersionPrinter vp = new VersionPrinter();
vp.print();
// hardcoded demo paths from build.xml
File path1 =
new File(System.getProperty("user.dir"),
"build.src2");
File path2 =
new File(System.getProperty("user.dir") ,
"build.src");
URL[] classpath = new URL[] {
path1.toURL(), path2.toURL()
};
URLClassLoader cl = new URLClassLoader(
classpath, null);
Object obj =
cl.loadClass(
"example.VersionPrinter").newInstance();
Printer p =
(Printer)Proxy.newProxyInstance(
Printer.class.getClassLoader(),
new Class[] { Printer.class },
new PrinterInvoker(obj));
p.print();
}
}
通过定义一个类加载器,可以绕过应用服务器的库,这个类加载器在查看服务器库目录之前,会首先查看自己的库目录。通过把两个Version类放进两个不同的构建目录中,并先在类加载路径中放置了包含Version类的5.0.0版本的目录(参见清单2),我模拟了这个类加载器。接着,我创建了一个URLClassLoade实例,并用自定义路径和一个空的父类对其进行初始化。空的父类可以确保类的加载不会委托给父类。然后,我加载了这个类,并使用一个动态代理把它映射到一个已知接口。在运行这个示例程序时,直接调用VersionPrinter.print将输出“version: 1.0.2”,而动态代理调用将输出“version: 5.0.0”,这些输出结果显示了想要使用的类版本,而不是默认版本。
使用例子中的技巧,您根本不必更改应用程序代码。有时,编程人员会自己加载一些类似Version的特殊类,但也许您不想那样做。如果打算这样,您将不得不更改VersionPrinter。这样,就必须通过反射访问每一个冲突类。那会使代码变得一团糟。您想做是:建立一个由接口定义的应用程序入口点(比如,Printer),并自定义加载那个应用程序。然后,自定义类加载器将加载这个应用程序使用的所有更深层的类。
一次性购买
实现一个能够用自定义类路径和委派servlet(delegate servlet)配置的包装器servlet是有可能的。包装器 servlet将使用这个自定义类路径来加载委派的servlet,并将所有调用委派给那个委派servlet。不幸的是,一些应用服务器中的servlet方法需要访问由应用服务器类加载器加载的资源。因此,包装器servlet技术不能保证在所有情况下都有效。您仍然可以使用实现一个选择性地将类加载委派给父类的类加载器的技巧。在从特定包中加载类时,可以将类加载器配置成不将类加载委派给父类。
类加载器解决方案所需的额外努力是一个缺点,但是该解决方案的花费是一次性的。动态代理的使用应该不会降低性能,只要您在离主应用入口点尽可能近的地方使用它即可,这样会最大程度地减少反射性方法调用的数量。加载的类将消耗额外的内存,但那是为在相同JVM中使用同一个类的不同版本付出的代价。一些新版的J2EE服务器可能提供了他们自己的版本冲突解决方案。至少我想起来有一台服务器重新命名了它所使用的包,因此,您不必重新命名这些表包。不过,即使现在遇见类版本冲突,您也已经有了一个摆脱困境的方法。
关于作者
Daniel F. Savarese是一名独立软件开发人员和技术顾问。他曾是ORO公司的创始人,Caltech高级计算处理中心的高级科学家和WebOS软件开发的副总裁。Daniel是Jakarta ORO 文本处理包和Jakarta Commons NET网络协议库的原始作者.他还是《How to Build a Beowulf》(MIT Press, 1999)一书的合著者之一。
原文出处
http://www.ftponline.com/channels/java/javapro/2005_03/magazine/columns/proshop/
<!--文章其他信息-->
分享到:
相关推荐
克服磁粉制动器低速爬行的自动控制方法pdf,克服磁粉制动器低速爬行的自动控制方法
如何克服厌训心理.docx
针对用于风扰模拟的力加载器系统,设计以DSP为核心的数字控制器。在传统的位置伺服控制的...试验结果表明,该力加载器能够进行有效地进行各种类型的风扰模拟,具有较高的控制精度和响应速度,能够良好的克服多余力干扰。
如何克服团队协作的五种障碍.pptx
针对控制对象存在模型不确定性及非线性的特点,提出了基于带有权重的广义预测控制GPC和PID控制并联控制的控制器设计方法。该方法不仅能够充分利用控制对象的模型信息,而且能在一定程度上克服模型中存在的结构不确定...
自制了一套古建木构架水平低周往复荷载试验加载装置,通过改变平面摇摆柱的双刀口支座的两个"刀口"距离和耳板孔到平面摇摆柱中心轴的距离,实现了古建木构架大位移低周往复荷载试验加载同步装置,克服了加载设备作用在...
班会教案--克服粗心大意.doc
十美 JVM的通用测试运行程序。 本机支持并行运行测试。 从UI到类加载的完整堆栈。 克服了JUnit的测试运行器,IDE和构建工具中的诸多限制。 有关更多信息,请参见
小学主题班会感恩父母克服任性PPT教案.pptx
如何帮助学生克服厌学情绪备课讲稿.pdf
行业分类-设备装置-地基光测设备克服恒星穿越探测窗口干扰的卫星跟踪方法.zip
数字化转型手册-拨云见日 克服数字化转型挑战
克服C语言浮点数的汇编语言处理数据,是原创的
江苏省苏州市八年级政治 5.2克服逆反心理教案 人教新课标版.doc
如何克服拖延症
客服软件试试吧绝对好
为了检测采煤机整机的性能并对其工作时的状况进行全面的测试,现构想一种固定式整机加载试验装置,来模拟实际工况时截割部及牵引部同时工作的情景,对采煤机各部同步进行加载、检测,能克服部件分别加载的缺点,通过科学...
【精--如何克服演讲紧张恐惧】如何克服紧张演讲.docx
工业4.0悖论:克服数字化转型道路上的脱节.pdf
【精--如何克服演讲紧张心理】 如何克服演讲紧张心理.docx