Java 和 Postgres95
Java 的本地方法是用 C 语言(或其他编译型语言)编写的函数,并在运行时由 Java 解释器动态加载。它们提供了访问尚未移植到 Java 的库的途径,并且允许在系统的关键点插入快速编译的代码。
在本文中,我们将完整地介绍编写本地代码的过程。我们将通过围绕 libpq 库编写包装类来创建一个 Java 到 Postgres95 的接口。Postgres95 是一个免费的数据库系统(在 GPL 许可下),可以在包括 Linux 在内的大多数 Unix 变体上运行。
虽然本文仅在 Linux 上编写(和测试),但本文的原理应适用于任何版本的 Unix,并且(除了如何构建共享库之外)代码应易于移植。为了充分利用本文,您应该有一些 Java 经验,或者非常熟悉 C++ 和 OO 原则。
最近,Java 作为一种出色的 WWW 工具受到了极大的关注(以及相当多的炒作)。带有动画和交互界面的“Java Powered”页面已在 Web 上涌现,包括 Microsoft (gasp!) 在内的每个人都在吵着要将 Java 功能添加到他们的浏览器中。许多人没有意识到的是,Java 不仅仅如此:它是一种完整的编程语言,适用于独立应用程序,尤其是客户端-服务器应用程序。
Java 提供了几个使其成为理想应用程序语言的功能。其中首要的显然是可移植性。使用 Java,无需编写 Windows95、Mac 和多个 Unix 版本的应用程序。由于代码由 Java 虚拟机 (VM) 运行,因此只需要将 VM(以及您想要使用的任何本地库)移植到该平台即可。
编写 Java 的另一个令人信服的理由是其库(在 Java 中称为“包”)的深度:网络、I/O、容器和一个完整的窗口系统都已集成。当运行 Java Applet 时,许多这些功能被“削弱”,但应用程序可以自由地充分利用所有这些功能。Java 是一个多线程环境,允许在当前不支持本地线程的平台上安全使用线程。Java 具有垃圾回收系统,消除了显式释放内存的需要。异常处理是内置的(并且它的使用实际上是许多库所要求的,包括我们将要编写的库),并且其真正的 OO 特性简化了继承和重用。
即使有这么多优点,使用 Java 开发应用程序仍然存在一个主要缺点:许多系统还没有 Java 接口,从头开始编写接口通常很困难,甚至是不可能的。
这就是当我想要从 Java 访问 Postgres95 数据库时面临的问题。Postgres95 附带了一个出色的(且简单的)C 库 (libpq),但完全不支持 Java。由于源代码(在这种情况下)是可用的,我考虑在 Java 下重新创建 libpq,但这被证明是一项艰巨的任务,并且需要深入了解 Postgres 内部结构。(事实上,截至本文撰写之时,Blackdown 组织的 John Kelly 正在编写这样一个程序。它被称为 Java-Postgres95 项目,您可以在 ftp://java.blackdown.org/pub/Java 找到一个 alpha 版本。
然后我决定简单地为 libpq 编写包装类。这种方法有几个缺点:首先,它不能在 Applet 中使用。浏览器明确禁止任何访问本地代码(浏览器提供的代码除外),因此这些类根本无法工作。其次(也是更重要的),这种解决方案不如用纯 Java 编写的解决方案那样具有可移植性。虽然 libpq 可以移植到所有主要的 Unix 版本,并且我们将要编写的代码也可以移植,但目前没有适用于 Windows95/NT 或 Mac 的 libpq。
除了更简单之外,用本地代码编写它还有另一个优点:当 Postgres95 项目发布错误修复或更改其通信协议时,我们的代码几乎不需要更改。
我们将分三个步骤进行,并在过程中提供如何使用每个部分的示例。
首先,我们将为 libpq 的 PGconn 和 PGResult 创建包装器。这将允许我们连接到数据库、发出查询并处理结果。
然后,我们将使用 Java 的 Stream 类为 Postgres95 的 Large Objects(或其他数据库中的 blobs)编写一个新的接口。
最后,我们将使用 Java 的线程为 Postgres95 的异步通知系统提供一个简单、幕后的接口。
已声明为“native”的 Java 方法(类函数)允许程序员访问共享库中的代码。从理论上讲,此代码可以用任何“与 C 链接”的语言编写(但一般来说,您可能希望坚持使用 C,或者可能是 C++)。
当加载 Java 类时,它可以显式地告诉 Java 系统将任何共享库(Linux 中的 .sos)加载到系统中。Java 使用环境变量 LD_LIBRARY_PATH(和 ldconfig)来搜索库,然后将使用该库来解析任何已声明为“native”的方法。
编写本地代码的一般过程如下
编写 .java 文件,将所有本地方法声明为“native”(.java 文件此时必须干净地编译,因此如果需要,请插入虚拟方法)
将 loadLibrary() 命令添加到您的 .java 文件中,以告诉 Java 加载共享库
编译类
javac [-g] classname.java
生成头文件和存根文件
javah classname (no extension)
javah -stubs classname
使用 classname.h 文件中的声明来编写您的 C 代码(我使用文件 classnameNative.c,因为它似乎很流行,并且存根文件使用 classname.c)
使用 -fPIC(位置无关)标志编译 .c 文件
gcc -c -fPIC -I/usr/local/java/include filename.c
生成共享库(这些标志适用于 gcc 2.7.0)
gcc -shared -Wl,-soname,libFOO.so.1 -o libFOO.so.1.0 *.o -lotherlib
将 .so 文件放在 LD_LIBRARY_PATH 中的某个位置(或将其添加到 /etc/ld.so.conf)。
PGConnection 类是 libpq 的 PGconn 的包装器。PGconn 表示与后端 Postgres95 进程的连接,并且对数据库的所有操作都通过该连接进行。每个 PGConnection 将创建一个 PGconn 并保留一个指向它的指针以供将来使用。
让我们逐步完成上述步骤
首先,我们编写我们的 PGConnection.java 文件(列表 1)。请记住,它必须干净地编译才能生成我们的头文件和存根文件,因此如果您引用任何您尚未编写的 Java 方法,请为它们创建虚拟方法。我们将需要一个构造函数、一个终结器以及 libpq 允许在 PGconn 上执行的所有操作。我们将大多数这些操作声明为本地方法(参见列表 1—exec() 和 getline() 是特殊情况,我们稍后会考虑)。
为了获得 PGconn,libpq 提供了函数
PGConn *setDB(char *host, char *port, char *options, char *tty, char *dbName)
由于这实际上“构造”了与数据库的连接,我们将以此作为我们构造函数的模型(参见列表 1,第 18 行)。构造函数只是调用 connectDB() (列表 1,第 21 行;一个调用 setdb() 的本地方法——我们稍后会定义它),并且如果未建立连接,则抛出异常。在构造函数中进行错误检查可确保如果调用 setdb () 失败,则不会返回任何连接。
现在让我们看一下我们的第一个本地方法 connectDB()。我们在 列表 1 的第 70 行将其声明为 native。请注意,没有提供 Java 代码。
关于此声明,有几点需要注意。“private”关键字使此方法只能从 PGConnection 类本身访问(我们只希望我们的构造函数调用它)。“native”关键字告诉 Java,应该在运行时为此方法加载共享库中的代码。由于 libpq 不是“线程安全的”,我们希望防止两个线程同时调用 libpq。使我们所有的本地方法“synchronized”在很大程度上实现了这个目标(当我们处理异步通知系统时,我们将回到这一点)。最后(列表 1,第 70-73 行),声明指出 connectDB() 接受五个 Java 字符串作为参数,并且不返回任何内容。
图 1. 类型在 Java 和 C 之间以及从 Java 和 C 之间的转换方式
其余的本地调用都遵循相同的模式,但 exec() 和 getline() 除外。同样,我们将稍后讨论这些。
在我们继续之前,让我们添加 loadLibrary 调用。我们将其放在类的末尾,在一个标记为“static”的块中(列表 1,第 92 行),没有方法名称。任何这样的块都在类加载时执行(仅执行一次),并且已加载的库不会重复加载。在我们的示例中,我们将库命名为 libJgres.so.1.0,因此我们需要使用 loadLibrary (“Jgres”)(参见列表 1,第 94 行)。
完成 .java 文件后,我们就可以编写 C 代码了。首先,我们使用以下命令编译 .java 文件
javac PGConnection.java
然后,我们使用以下命令创建“存根”文件和 .h 文件
javah PGConnection javah -stubs PGConnection
此时,您应该在当前目录中拥有 PGConnection.h 和 PGConnection.c。PGConnection.c 是“存根”文件,不应修改。对于我们的目的,您必须对存根文件做的唯一事情是编译它并将其链接到您的共享库中。
PGConnection.h 是一个头文件,必须包含在任何访问 PGConnection 对象的 C 文件中。在第 14 行(参见列表 2),您将找到与我们对象的数据对应的结构声明。在该声明下方,您将找到我们声明的所有本地方法的原型。在编写本地方法的 C 代码时,您必须完全匹配这些签名。列表 2. PGConnectionNative.c(包含 PGConnection.h)
现在,让我们(终于)编写 C 代码。
connectDB 的代码非常简单明了,并演示了编写本地代码所涉及的大部分问题。请注意,connectDB 的第一个参数未在 PGConnection.java 文件中列出。Java 自动传递一个“句柄”(一个花哨的指针)给您正在处理的对象,作为每个本地方法的第一个参数。在我们的例子中,这是一个指向 struct HPGConnection 的指针(在 PGConnection.h 中定义),我们将其命名为“this”(列表 2,第 14 行。如果您在 C++ 中工作,您可能想要使用“self”,因为“this”是一个关键字)。对对象数据的任何访问都必须通过此句柄进行。
其余参数是我们传入的字符串(参见 PGConnection.java)。这些也作为句柄或指向 struct Hjava_lang_String 的指针传递(在 native.h 包含的 java_lang_string.h 中定义)。我们可以像访问任何其他句柄一样访问这些结构(见下文),但 Java 提供了几个方便的函数,可以更轻松地处理字符串。
这些函数中最有用的是 makeCString 和 makeJavaString。这些函数将 Java 的字符串转换为 char *s,反之亦然,它们使用 Java 的垃圾回收器来自动处理内存分配和回收。(
您必须将 makeCString 返回的值存储在一个变量中。如果您将返回值直接传递给一个函数,垃圾回收器可能会随时释放它。makeJavaString 则不然。)列表 2 中的第 30-34 行显示了 makeCString 的用法,我们首先在第 51 行使用了 makeJavaString。列表 2 中的第 41-42 行显示了我们对 libpq 库的调用。它像正常调用一样被调用,结果指针存储在变量 tmpConn 中。您可能会注意到我们在这里没有进行任何错误检查:我们在构造函数的 Java 代码中进行错误检查,在那里更容易抛出异常。
正如我上面提到的,PGConnection 需要保留 PGconn 指针,以便它可以在以后的调用中使用它——实际上是所有以后的调用。为了做到这一点,我们将 32 位指针存储在具有 Java 类型 int 的数据成员中,在将其强制转换为 C long 以避免警告后(有关类型转换列表,请参见表 1)。
要访问此成员,我们必须使用 Java 的“句柄”。句柄用于访问 Java 对象中的数据。当您想要访问数据成员时,您只需使用 unhand(ptr)->member 而不是 ptr->member (其中 ptr 是句柄)。我们在 PGConnectionNative.c 的第 42 行(列表 2)中这样做,以将 setDB 返回的指针保存在 Java int 中(注意:如果您忘记 unhand() 宏,您将收到关于不兼容指针类型的警告)。
此函数几乎涵盖了您需要了解的所有内容,以便从 Java 调用 C 函数(从 C 调用 Java 方法是可能的,但此时接口充其量是笨拙的,并且在可能的情况下,我建议避免使用它)。大多数其余方法(host、options、port 等)只是转换数据并进行 C 调用。我们只看一下其中之一,PGConnection.db()。
C 函数 PGConnection_db() 的唯一重要部分是它的第一行(列表 2,第 46 行)。它需要一个 PGconn 传递给 PQdb(),因此它必须从 PGConnection 成员 PGconnRep 中获取它。它使用 cw[unhand() 将指针作为 long 获取,然后将其强制转换为 (PGconn *)。由于这一行太混乱了(并且开始看起来像 lisp!),我创建了一个宏 thisPGconn,以稍微清理代码。它在文件的其余部分中使用,其定义位于文件的顶部(不要将其放在 PGConnection.h 中,因为它是机器生成的)。
Java 类 PGResult 中的所有本地方法都遵循相同的基本结构,没有理由再赘述它们。
exec() 方法(看,我告诉过你会讲到它)需要返回一个 PGResult 对象。这与 libpq 的结构以及 Java 的 OO 性质保持一致。但是,从本地方法返回对象可能会变得非常棘手。“官方”的方法是调用函数
HObject *execute_java_constructor(ExecEnv *, char *classname, ClassClass *cb, char *signature, ...);
并返回它返回的 HObject *。就个人而言,我发现这个接口非常笨拙,并且设法避免了它。但是,为了完整起见,在我们的例子中,实际的调用将是
return execute_java_constructor(EE(), "classPGResult", 0, "(I)LclassPGResult;", (long)tmpResult);
我发现创建一个 exec() 调用和 PQexec() 调用之间的缓冲区要容易得多,该缓冲区可以从 Java 调用构造函数。这就是 nativeExec() 方法的由来。exec() 只是将字符串传递给 nativeExec(),后者返回一个 int(PQexec() 返回的 PGresult 指针)。然后它使用该 int 调用 PGResult 的构造函数。
当我们添加异步通知系统时,额外的层也将派上用场。
PQgetline() 希望用户在填充静态缓冲区时不断调用它。这在 Java 中根本不需要。一个更好的接口是让 getline() 只返回一个 String。但是,构建字符串(附加 PQgetline() 的每个返回值)需要从 C 调用 Java 方法——正如我们在障碍 #1 中看到的那样,这非常混乱。通过使用 StringBuffer(可以增长的字符串)并在 Java 代码中完成工作,它更容易理解,如果稍微慢一点的话。
这样做的好处是返回值现在是 String,因此必须有另一种方法来判断是否发生了错误或是否已到达 EOF。一种解决方案(我正在寻找更好的解决方案),也是我们使用的解决方案,是设置一个数据成员标志。如果该标志已设置为 EOF,我们只需返回一个 Java null String。因此,再一次,额外的层使我们免于编写大量真正糟糕的代码!
我认为 JavaSoft 团队应该为我们解决这个障碍。根本无法从 FileStream 获取 FILE *(或文件描述符)。PQtrace() 需要 FILE *,因此我们只需根据用户传入的文件名打开一个 FILE *。我们检查它是否是“stdout”或“stderr”,并据此采取行动。
当我们尝试实现 Postgres95 的 printTuples(或 1.1 的 displayTuples)时,我们再次看到了这个问题。它也需要 FILE*,但这次的解决方案有点麻烦。在这里,我们希望输出在一个 String 中,因此我们打开一个临时文件,将其发送到 libpq 函数,重绕它,读取它,然后关闭它。这非常麻烦,但它确实有效,并且实际上速度相当快。如果我们想编写一个更简洁的版本,我们当然可以使用我们已经定义的 PGResult 的本地方法 fname() 和 getValue() 在 Java 代码中完全重写 displayTuples()。
:
在编写完所有 C 代码后,我们就可以生成我们的共享库了。
首先,我们必须编译 .c 文件
gcc -O2 -fPIC -I/usr/local/java/include/ \ -I/usr/local/java/include/solaris \ -c PGConnectionNative.c gcc ... (repeat for each .c file)
然后我们链接它们
gcc -shared -Wl,-soname,libJgres.so.1 -o libJgres.so.1.0 *.o -lpq
-lpq 告诉动态加载器在 Java 加载此库时加载 libpq.so。
最后,将它们放在动态加载器可以找到它们的位置(在您的 LD_LIBRARY_PATH 中,或在标准位置(即 /usr/local/lib)中,然后重新运行 /sbin/ldconfig -v)。
这就是全部内容。现在我们可以像使用任何其他 Java 类一样使用 PGConnection 和 PGResult。
为了完成本节,让我们使用我们的新类来实现一个简单的 SQL 客户端。客户端将连接到数据库“foo”并从标准输入接受查询字符串。PGConnection.exec() 将处理查询,并使用 formatTuples() 将结果打印到终端。与数据库的连接在 列表 3 (QueryTest.java) 的第 17 行建立。
对于我们不知道的任何参数,我们使用 libpq 约定发送 NULL(空 Java 字符串 "" 转换为 NULL char *)。请注意,对 PGConnection 构造函数的调用被“try”块包围。如果在此块中抛出异常,我们的连接出现问题并优雅地退出(第 54-58 行,列表 3)。
在 列表 3 的第 24 行,我们测试了一些简单的函数来打印出有关我们连接到什么的信息。然后我们读取一个查询字符串,如果它是“q”或“Q”,则退出。
我们通过调用 exec() 在 列表 3 的第 33 行处理查询。请注意,我们在这里嵌套了另一个“try”块,因为如果我们在 exec() 上收到 PostgresException,我们只想打印错误并继续(我们在第 43-46 行处理异常)。如果我们到达第 34 行,我们知道 PGResult 是有效的。我们检查它是否返回了任何元组,如果返回了,则使用 formatTuples() 打印它们。如果未返回,我们只需打印当前状态并继续。
Charles “Bill” Binko 于 1994 年毕业于佛罗里达大学,获得计算机工程学士学位。目前是佐治亚州亚特兰大的一名软件工程师,自 1993 年以来一直是 Linux 爱好者。他的主要计算机兴趣在于模拟、遗传算法和分布式编程,他认为 Java 是所有这些的绝佳平台。