概要
实现Java 众多API中的一个是一件困难的工作。你必须经常实现许多相互依赖的接口。对新特征的需求促进了最新Java API的创建,投资商们必须不断的更新他们的实现以跟上时代的需要。当复杂性和持续修改可以理解甚至被看好时, API 版本的不兼容性迫使你为更新的版本保留独立的代码库,从而N次方增加你的受挫等级。本文演示了如何克服接口版本的不兼容性,制作可为多个API版本编译的代码库的进程。
Java 已经添加了无数个API,像Java 数据库连接(JDBC),到它的标准库系列上。这可以帮助更广泛的用户使用API,因为可选软件包不必捆绑到配置上。对于编写这些流行的API的工作组来说,使用的越多他们的付出就越有价值。但是,当更新的Java版本运载最新的API,而最新的API依赖的类和方法是以前的Java版本所不能实现的时候,这些工作组希望他们的API仍然是可选的(与包括在Java的标准库系列内相反)。这样你忽然必须要保留实现的两个版本——一个编译老的API,另一个编译新的API。这就是发生在Java2平台标准版(J2SE)1.4中的JDBC API身上的事情。因为修改了JDBC API, java.sql.Connection 的一个实现不能在J2SE 1.3 和 1.4版本下编译。
你可能发现你自己也处于与我一样的困境中:我需要实现JDBC 接口如 java.sql.Connection,但是我的代码需要在J2SE 1.3 和1.4下编译。我不想为J2SE 1.3 和1.4保留不同的源文件,所以我寻找更好的解决方法。
不幸的是,当你需要Java编译器来完成编译时著名的WORA (一次编写,各处运行)不包括WOCA (一次编写,各处编译)。幸运的是,使用Reflection API对代码做一些修改并且使用Ant 对编译做一些修改可以帮你走出困境。我能有一组.java 源文件和Ant来帮助我在J2SE 1.3 和 1.4两者之上编译代码。Ant允许我运行时修改.java文件, 正确修改以便与编译时使用的Java版本兼容。但是在我阐述整个解决方案之前,我必须得说明整个问题。
穷人的连接池
两年前,我的公司需要一个JDBC连接池但是又买不起。那个时候,我们找不到好的免费的备选的东西,所以我们编写了一个内部的连接池。为了更好的追溯连接在整个我们的应用程序上是如何使用的,我们创建了com.icentris.sql.ConnectionWrapper,它实现java.sql.Connection和一些其他的可实现其他java.sql接口的包装者类。 包装者类只追踪我们应用程序内的数据库的使用并且传递方法调用到真正的JDBC资源处。
当 J2SE 1.4 出来的时候,我们自然想要将一些我们的客户端程序移到它上面,这样他们就能从J2SE 1.4的许多改进中获益。但是,当然,我们仍然需要支持J2SE 1.3,因为那些客户端看不出有什么理由需要升级。我们气愤的是, ConnectionWrapper和我们其他的JDBC包装者类不修改就不能在J2SE 1.4上编译。为了保持文章的简洁,我使用ConnectionWrapper来演示我应用到所有不能同时在J2SE 1.3和 1.4下编译的类的技巧。 为了遵循最新的JDBC API,我不得不添加几个方法到ConnectionWrapper上,这造成了两个大问题:
1.因为我的包装者类需要通过方法调用,我不得不调用J2SE 1.3 sql 类中没有的方法。
2.因为一些新方法依赖于新的类,我不得不依赖于J2SE 1.3 中没有的类。
Reflection 来拯救
一些代码样品最适合解释第一个问题。因为我的ConnectionWrapper包裹了java.sql.Connection,所有我的样品依赖于构造器中的realConnection实际变量 (粗体) 组:
private java.sql.Connection realConnection = null;
public ConnectionWrapper(java.sql.Connection connection) {
realConnection = connection;
}
|
为了看清楚我在没有不兼容问题的情况下做过什么,我们看看setHoldability(int) (到J2SE 1.4中的java.sql.Connection 的新方法):
public void setHoldability(int holdability) throws SQLException {
realConnection.setHoldability( holdability );
}
|
不幸的是,该代码在J2SE 1.3下不能编译因为 java.sql.Connection 没有我在J2SE 1.3才能调用的setHoldability()方法。但是要在J2SE 1.4下编译,我必须有一个setHoldability()方法可正确的实现该API. 要解决这个catch-22,我假设我的setHoldability() 方法只能在J2SE 1.4下编译,所以我可以使用Reflection API来调用该方法:
public void setHoldability(int holdability) throws SQLException {
Class[] argTypes = new Class[] { Integer.TYPE };
Object[] args = new Object[] {new Integer(holdability)};
callJava14Method("setHoldability", realConnection, argTypes, args);
}
public static Object callJava14Method(String methodName, Object instance,
Class[] argTypes, Object[] args)
throws SQLException
{
try {
Method method = instance.getClass().getMethod(methodName, argTypes);
return method.invoke(instance, args );
} catch (NoSuchMethodException e) {
e.printStackTrace();
throw new SQLException("Error Invoking method (" + methodName + "): "
+ e);
} catch (IllegalAccessException e) {
e.printStackTrace();
throw new SQLException("Error Invoking method (" + methodName + "): "
+ e);
} catch (InvocationTargetException e) {
e.printStackTrace();
throw new SQLException("Error Invoking method (" + methodName + "): "
+ e);
}
}
|
现在我有了setHoldability()方法,所以我可以在 J2SE 1.4下编译。我不直接调用我的java.sql.Connection 上以前不存在的方法,所以我能在J2SE 1.3下编译。我的 callJava14Method()方法使用 Reflection API调用该方法,然后包裹任何错误在SQLException里面,因为SQLException就是我给出的异常的全部。我将该策略用于所有新的J2SE 1.4方法,调用包裹了的方法,然后在 1.3下编译。现在我只需要解决第二个问题并且找出一条方法来依赖J2SE 1.3中不存在的类。
Ant就是答案
在J2SE 1.4中,java.sql.Connection依赖于新类java.sql.Savepoint。因为这个新类设置在 java.sql软件包内,你不可能将它添加到J2SE 1.3上。Java不允许添加任何第三组织的东西到java.* 或者 javax.*软件包中的核心类系列上。所以这也是对我的挑战:使用新的java.sql.Savepoint类来编写我的代码这样它就可以在J2SE 1.4下运行, 然后确保代码可在J2SE 1.3下编译,实际上,这个类在J2SE 1.3中不存在。简单,对吧?说“YES”的人都品行良好。好,至少,它简单得现在我已经找到答案了。
首先,我将下列条件输入包括进来:
// Comment_next_line_to_compile_with_Java_1.3
import java.sql.Savepoint;
|
然后为 Ant找到一条方法:当在J2SE 1.3下编译时可注释该输入。 简化之后, Ant 脚本核心部分为:
<replace>
<replacetoken>Comment_next_line_for_Java_1.3
</replacetoken>
<replacevalue>Comment_next_line_for_Java_1.3
//</replacevalue>
</replace>
|
该 Ant 标记有几个选项——在下面的我的完整范例中你会看到更多——但是最重要的部分就是我搜索 然后用取代它。“
” 是用于"newline"的XML 实体。当在J2SE 1.4下编译时,Ant对源文件不做任何修改;并且在 J2SE 1.3下,重要语句被注释:
// Comment_next_line_to_compile_with_Java_1.3
//import java.sql.Savepoint;
|
但是在必须依赖Savepoint 的类的体中仍有代码:
public Savepoint setSavepoint(String name) throws SQLException { . . .
|
再次,我只能期望可在J2SE 1.4下使用这些新方法,这样他们不必在J2SE 1.3下运行; 他们只需要编译。我发现:如果我的软件包中有一个Savepoint 类,我的代码不需要输入语句就可以编译。但是当输入语句没有注释的话(在J2SE 1.4编辑下),我的Savepoint 类会被忽略因为存在更特殊的输入。所以我创建了我自己的哑元类com.icentris.sql.Savepoint,它(java文章排斥它)可能是最短的有效类 :
package com.icentris.sql;
/** Dummy class to allow ConnectionWrapper to implement java.sql.Connection
* and still compile under J2SE 1.3 and J2SE 1.4. When compiled
* under J2SE 1.3, this class compiles as a placeholder instead of the
* missing java.sql.Savepoint (not in J2SE 1.3). When compiled
* under J2SE 1.4, this class is ignored and ConnectionWrapper uses the
* java.sql.Savepoint that is new in J2SE 1.4.
*/
public class Savepoint {}
|
在J2SE 1.4下,我现在可以正确的输入java.sql.Savepoint。在J2SE 1.3下, Ant 注释输入行,这样我的代码中引用的 Savepoint正好在同一个软件包处于哑元类的位置。 现在我可以添加所有引用Savepoint 的方法并且仍然可以使用前面解释过的Reflection 技巧:
// Comment_next_line_to_compile_with_Java_1.3
import java.sql.Savepoint;
. . .
public Savepoint setSavepoint() throws SQLException {
Class[] argTypes = new Class[0];
Object[] args = new Object[0];
return (Savepoint) callJava14Method("setSavepoint", realConnection,
argTypes, args);
}
public Savepoint setSavepoint(String name) throws SQLException {
Class[] argTypes = new Class[] { String.class };
Object[] args = new Object[] { name };
return (Savepoint) callJava14Method("setSavepoint", realConnection,
argTypes, args);
}
public void rollback(Savepoint savepoint) throws SQLException {
Class[] argTypes = new Class[] { Savepoint.class };
Object[] args = new Object[] { savepoint };
callJava14Method("rollback", realConnection, argTypes, args);
}
public void releaseSavepoint(Savepoint savepoint) throws SQLException {
Class[] argTypes = new Class[] { Savepoint.class };
Object[] args = new Object[] { savepoint };
callJava14Method("releaseSavepoint", realConnection, argTypes, args);
}
|
现在我需要的就是:用来探测J2SE 1.3 的我所有的Ant compile目标,以及当有人试图使用J2SE 1.3编译ConnectionWrapper时在运行过程中对输入行的注释:
<target name="compile">
<antcall target="undoJava13Tweaks" />
<antcall target="doJava13Tweaks" />
<javac srcdir="src" destdir="WEB-INF/classes" debug="on">
<classpath>
<fileset dir="WEB-INF/lib">
<include name="*.jar"/>
</fileset>
</classpath>
</javac>
<antcall target="undoJava13Tweaks" />
</target>
<target description="Find out if we're being compiled on Java 1.3"
name="isJava13">
<echo message="java.specification.version=[${java.specification.version}]"/>
<condition property="isJava13">
<equals arg1="${java.specification.version}" arg2="1.3" />
</condition>
</target>
<target description="There are a couple tweaks I have to do to compile under Java 1.3"
name="doJava13Tweaks" depends="isJava13" if="isJava13">
<echo message="This is Java 1.3, doing Tweaks!" />
<replace dir="src/com/icentris/" summary="true">
<include name="sql/ConnectionWrapper.java" />
<replacetoken>Comment_next_line_for_Java_1.3
</replacetoken>
<replacevalue>Comment_next_line_for_Java_1.3
//</replacevalue>
</replace>
</target>
<target description="Let's undo Java 1.3 tweaks" name="undoJava13Tweaks">
<replace dir="src/com/icentris/" summary="true">
<include name="sql/ConnectionWrapper.java" />
<replacetoken>Comment_next_line_for_Java_1.3
//</replacetoken>
<replacevalue>Comment_next_line_for_Java_1.3
</replacevalue>
</replace>
</target>
|
注意compile目标在doJava13Tweaks之前之后都调用undoJava13Tweaks。 这是为了防止javac编辑目标时失败,并且我们拥有没有清除掉的剩余替代品。
不需要保留两个实现
对于一个新的API版本来说将新方法和新的类(或者接口)包括在内是很普遍的。如果实现器能够实现新的API并且仍然提供对更老版本的后台兼容,那么这对实现器来说也不是一个重要的障碍。但是,当API是核心Java类的一部分时,API的改动会引起大得多的困难,因为 Java不允许任何外在的修改或者任何java.*软件包的添加物。通常,这会导致需要支持修改了的API的各个版本的不同源代码树。
但是,就像上文演示的一样,你可以在两个Java 版本上都编译一个源代码树。Reflection API 允许调用不存在的方法,并且Ant允许输入以适时修改,以便支持对在编译 Java 版本下不存在的类的依赖。尽管上述范例为了演示方便简化了,但我成功地使用这些或者类似的技巧来解决与同时支持J2SE 1.3 和1.4有关的不同问题。既然这么多的Java APIs都不断的更新,那么你可以使用这些技巧来帮助避免维护两个代码库的非想望的需求。
关于作者
作为iCentris的总设计师,Sam Mefford 将兼容性摆在第一位。他领导的团队使得一个代码库为许多公司所采用;一个应用服务器包括Tomcat, WebLogic, Resin, Orion,和WebSphere;一个数据库包括Oracle, PostgreSQL, MySQL, 和 Informix;一个代码库可用在多个Java运营环境内。
本文译自:http://www.javaworld.com/javaworld/jw-09-2003/jw-0926-overcome.html
(责任编辑:赵纪雷)
|
| 赛迪网推出“IT博客”,花不到一分钟就完成注册 |
|
【评论】
【推荐】
【大 中 小】
【打印】
【关闭】
|