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/
<!--文章其他信息-->
分享到:
相关推荐
标题中的“hello3:在three.js中有些混乱”暗示了我们将在three.js这个JavaScript库的使用上探讨一些可能遇到的问题和困惑。...通过实践项目和深入研究代码,可以逐渐克服初学阶段的混乱,提升在3D图形编程领域的技能。
下面,我们将深入探讨这些主题,并提供有关如何克服混乱的指导。 首先,我们要理解神经网络的基本结构。神经网络是由一系列节点(称为神经元)和连接这些神经元的权重构成的。它们模拟大脑的工作原理,通过学习数据...
Struts是由Apache软件基金会开发的一个开源框架,它的出现是为了克服早期Web开发中的CGI(Common Gateway Interface)和Servlet技术的局限性。CGI虽然易于实现,但在处理大量请求时效率低下。Servlet虽然功能强大,...
理解Hibernate的工作原理,尤其是其对关联管理和延迟加载的策略,对于排查这类问题至关重要。同时,利用IDE的调试器查看对象状态,以及使用日志输出来追踪执行流程也是必不可少的。 总之,理解和处理Hibernate的...
当前,OSChina面临的问题主要包括JavaScript代码的混乱、Bean类的逻辑复杂、分散式缓存改造的挑战,以及自增长字段对数据库分布式结构的阻碍。 如果对OSChina的技术还有更多兴趣,可以探讨其如何优化JS、简化Bean类...
策划书还对竞争对手的网站进行了细致分析,如电器专卖店类和超市类网站,借鉴其优点,如清晰的导航、丰富的多媒体元素、高效的购物流程等,以提升自身网站的竞争力。 #### 改版任务与规划 改版任务围绕解决现有...
本文将深入探讨这一问题,并提供一系列详细的解决步骤,帮助用户有效地克服这一障碍。 ### 问题解析 在Office Word 2007中,当用户从其他应用程序或桌面切换回Word窗口时,可能会发现鼠标不再响应编辑操作,如无法...
解决这个问题的方法是在调用html2canvas之前确保所有图片已经加载完毕,可以使用`window.onload`或`img.onload`事件监听器。 3. **透明度和混合模式**:HTML2Canvas可能无法完全处理CSS的透明度和混合模式,这可能...
同时,根据实际需求考虑是否使用分页加载数据,避免一次性加载过多数据导致内存消耗过大。 4. **限制ListView的高度**:给ListView设置一个合适的高度,以防止其占用整个ScrollView的空间,可以通过计算数据数量和...
描述中提到的“垃圾网站”可能是指使用`DIV+CSS`时出现的技术问题,比如加载速度慢、页面布局混乱、兼容性差等。开发者可能对网站评分制度感到不满,认为这阻碍了资源的自由交流。理想的交流平台应该是开放的,用户...
总结来说,本文主要介绍了如何利用LPC2294微控制器来设计一个支持多路CAN总线并具备PCI接口的智能通信卡,以克服现有产品在接口数量和跨网段通信方面的局限性,提升了工业控制领域的通信效率和可靠性。
第四章 数 据 类 型 .28 4.1 值 类 型 .28 4.2 引 用 类 型 .33 4.3 装箱和拆箱 .39 4.4 小 结 .42 第五章 变量和常量 .44 5.1 变 量 .44 5.2 常 量 .46 5.3 小 结 .47 第六章 类 型 转 换 .48 ...
3. **未知编码类型**:处理来源不明或格式混乱的文本数据时无法确定其编码方式。 **解决方案**: 1. **指定正确编码**:使用`open()`函数打开文件时,明确指定`encoding`参数。 2. **统一字符串编码**:在处理字符...
由于Nandflash的编程阶段需要较长的时间,通过流水线技术,可以在一片Nandflash进行编程的同时加载下一片的数据,极大地提高了整体写入效率,有效克服了Nandflash写入速度慢的瓶颈。 总的来说,这个基于FPGA的高速...
DOM0级事件绑定简单直观,但存在一些限制,比如只能为同一元素的同一事件绑定一个处理函数,而DOM2级事件则克服了这些限制。 首先,DOM2级事件引入了`addEventListener`和`removeEventListener`方法,允许开发者为...