服务器端 Java 与 Jakarta-Tomcat

作者:Reuven M. Lerner

当我开始编写服务器端 Web 应用程序时,主要有两种选择:如果您希望程序快速执行,则选择 C 语言;如果您希望快速编写程序,则使用 Perl 语言。众所周知,当二进制文件需要小巧、快速且高效时,C 语言非常棒。但是 C 语言缺乏自动内存管理和像样的字符串处理,以及程序员需要格外小心才能使用它,这使得它与 Perl 相比成为较差的第二选择。

但在过去几年中,许多编程语言开始挑战 Perl 在服务器端 Web 编程领域的地位。特别是,Python 取得了显著进展,这在很大程度上归功于 Zope Web 开发环境的增长。

但也许服务器端编程的最大潮流来自 Java 社区。正如我们许多人可能记得的那样,Java 最初是一种客户端编程语言。在很大程度上,Applet 是尝试混合两种客户端范例的不愉快回忆——Flash 日益普及的使用似乎忽略了这个教训。

服务器端 Java 的基本单元是 servlet,这是一个小型程序,它响应 HTTP 请求而执行,并生成合法的 HTTP 响应。由于 servlet 是用 Java 编写的,因此它们被编写为对象类,继承自 servlet 祖先,并且可以利用 Java 的线程和异常处理。此外,servlet(像所有 Java 程序一样)在 Java 虚拟机 (JVM) 中运行,JVM 是一个抽象层,可以运行在任何操作系统平台上。这意味着相同的 servlet 几乎可以在任何操作系统上运行,提供比 CGI 程序更大的可移植性。

到目前为止,我在少数几个项目中使用过 servlet,但这个数字正在迅速上升。Java 现在是“流行”的语言。这部分是因为 Sun 公司在其营销方面投入了大量资金,部分是因为它提供了优于竞争对手的一些技术和基础设施优势,还因为它对 Windows 构成了严重的平台挑战。此外,服务器端 Java 是越来越多的基于 Java 的应用程序服务器的基石。

本月,我们将开始探索 Java 作为一种服务器端编程语言。作为第一步,我们将安装 Jakarta-Tomcat 环境来运行 servlet,以及相关的 Jasper 环境来创建 Java Server Pages (JSP)。在接下来的几个月中,我们将研究如何将我们的 servlet 和 JSP 连接到关系数据库,以及如何使用 Enterprise JavaBeans 和 Enhydra 应用程序服务器来获得更强大的环境。

安装 Java

几年前,当我第一次开始在 Linux 上使用 Java 时,情况似乎相当糟糕:虽然 Linux 是最著名的开源操作系统,而 Sun 公司正在推广 Java 作为一种通用编程语言,但很难甚至不可能为 Linux 获得一个好的 Java 版本。一些志愿移植工作,特别是 Blackdown porters 完成的工作,令人印象深刻,但安装容易出现问题,并且远不如开发人员可能希望的那样稳定。

当我于 2001 年 1 月撰写本文时,情况发生了巨大变化:您现在可以直接从 Sun 公司的网站下载最新 Java 开发工具包 (JDK) 的 Linux 版本。此外,Tomcat servlet/JSP 系统在 Linux 上运行良好。随着 Linux 越来越受欢迎,它正成为一个越来越有吸引力的 Java 编程平台。

由于我的主要 Linux 机器运行 Red Hat 6.2,我从 Sun 公司的网站 http://java.sun.com/ 下载了 JDK 1.3 RPM。为了下载 JDK,我必须注册成为“Java Developer Connection”的成员;虽然我对必须注册才能下载软件的想法并不感到兴奋,但这似乎不是一个可怕的代价。RPM 不能直接安装;首先,您必须同意 Sun 公司的 Java 许可协议。

一旦您接受了该协议,RPM 就会被解压缩并可用于安装。然后,您可以以 root 用户身份登录并安装 JDK,它将被放置在 /usr/java 目录中。通过将 /usr/java/jdk1.3/bin/ 放入您的 PATH 环境变量中,您可以执行 javac 编译器和 java 运行时环境,而无需指定显式路径。

安装 JDK 后,您应该至少运行一个简单的测试以确保它工作正常。清单 1 包含一个简单的程序,可以不带任何参数调用,并将“hello, world”打印到 STDOUT。如果程序传递了任何参数,它将打印这些参数,并用管道字符 (|) 分隔。

清单 1. Test.java

要将我们的测试类 (Test.java) 编译成字节码 (Test.class),请使用 Java 编译器 javac

javac Test.java

要运行该程序,我们必须调用 Java 运行时环境 (java),并为其提供我们的类名(不带 .class 后缀)

java Test
如果我们不传递任何参数,我们将获得以下输出
Hello, world
但是,我们可以将参数传递给我们的程序
java Test a b "q r s" 123
在这种情况下,我们得到以下输出
a|b|q r s|123
除了正确设置您的 PATH 之外,您还应该设置环境变量 JAVA_HOME 以指向 JDK 的位置。如果您使用 bash,您可以简单地将以下行放在您的启动文件之一中
export JAVA_HOME=/usr/java/jdk1.3
安装 Jakarta-Tomcat

现在我们已经安装了 JDK,我们可以安装 Jakarta-Tomcat。Jakarta 是 Apache 软件基金会赞助的 Java 相关项目的总称,而 Tomcat 是 ASF 的 servlet 和 JSP 项目。(JSP,我们将在下个月看到,只是创建 servlet 的一种简单方法。)Tomcat 旨在成为各种平台上 servlet 和 JSP 的参考标准,使其具有可移植性,并易于在服务器端 Web 应用程序中使用 Java。

与 CGI 程序(在其自己的 UNIX 进程中执行)不同,也与 mod_perl 处理程序(作为 Apache 中的子例程执行)不同,servlet 在 Java 虚拟机中执行。此 JVM 被称为“servlet 容器”,它可以是服务器本身(如果服务器是用 Java 编写的)、嵌入在服务器内部或外部于服务器。

本文假设您将使用 Apache,在这种情况下,servlet 容器位于 HTTP 服务器外部。但是,Tomcat 本身就是一个功能齐全的 HTTP 服务器,这意味着我们可以进行一些初步测试,而无需配置 Apache。

您可以从 Jakarta 网站 http://jakarta.apache.org/ 下载并安装最新版本的 Tomcat。Jakarta 项目为各种平台和多个计划分发软件,包括源代码和二进制格式。可能需要查找一下,但您应该能够找到适用于 Linux 的最新稳定 Tomcat 版本的可下载二进制文件。截至撰写本文时,Tomcat 的最新稳定版本是 3.2.1,我下载的文件是 jakarta-tomcat-3.2.1.tar.gz。

下载到您的计算机后,切换到您要安装 Tomcat 的目录,然后打开它

cd /usr/java
tar -zxvf jakarta-tomcat-3.2.1.tar.gz

您的 /usr/java 目录现在将包含两个子目录,一个名为 jdk1.3,另一个名为 jakarta-tomcat-3.2.1。

正如您必须设置 JAVA_HOME 以指示您的 Java 发行版所在位置一样,您还必须设置 TOMCAT_HOME 变量以指示 Tomcat 的安装位置。使用 bash 的用户可以将以下行添加到他们的启动文件之一中

export TOMCAT_HOME=/usr/java/jakarta-tomcat-3.2.1

如果您计划编写自己的 servlet,您还需要告诉 Java 在哪里查找 servlet 相关类。这些类位于 Java 存档 (.jar) 文件 $TOMCAT_HOME/lib/servlet.jar 中。如果您使用 bash 并且没有以其他方式设置您的 CLASSPATH,您可以按如下方式设置它

export CLASSPATH=$TOMCAT_HOME/lib/servlet.jar:.
如果您只计划运行其他人编写的 servlet,则无需以这种方式修改 CLASSPATH。运行时 Java servlet 引擎知道在哪里查找适当的 .jar 文件,并且其 CLASSPATH 在您安装 Tomcat 时已正确设置。

完成所有这些步骤后,Tomcat 就可以使用了。您可以使用 $TOMCAT_HOME/bin 下的 shell 脚本启动它

$TOMCAT_HOME/bin/startup.sh

屏幕上将出现许多诊断消息。但是,主要的 servlet.log 日志文件通常位于 $TOMCAT_HOME/logs 中。

您可以通过将浏览器指向已启动 Tomcat 的计算机上的端口 8080(默认端口)来检查 Tomcat 是否工作。换句话说,http://localhost:8080/ 应该为您提供一条欢迎消息,指示“这是 Tomcat 默认主页”,并提供一些指向系统上安装的 servlet 和 JSP 示例的附加链接。示例 servlet 应该正确执行,为您演示一些我们可以使用 Tomcat 执行的简单任务。

Servlet 类通常安装在名为 WEB-INF 的目录下,该目录位于 URL 中命名的目录之下;也就是说,Tomcat 附带的示例 servlet RequestInfoExample 在 http://localhost:8080/examples/servlet/RequestInfoExample 上可用。

实际的 Java .class 文件(以及该类的 .java 源文件)存储在 $TOMCAT_HOME/webapps/examples/WEB-INF/classes/RequestInfoExample.class 中。

我们很快将看到如何为 servlet 配置其他目录。但是,我们始终必须将我们的类安装在目录 WEB-INF/classes 下,并且 WEB-INF 层次结构将对公众隐藏。

清单 2. HelloWorld.java,一个简单的 applet,用于处理 GET 请求方法。

一个简单的 Servlet

我们可以通过将一个简单的 servlet HelloWorld.java(参见清单 2)放在上面提到的目录 $TOMCAT_HOME/webapps/examples/WEB-INF/classes/ 中来测试我们的 Tomcat 安装。

请记住,Java 要求文件名与类名匹配。如果您想将文件名更改为 ABC.java,则必须将源代码内部的类声明更改为相同的名称。否则,Java 编译器将报错并提示致命错误。

要将 HelloWorld.java 编译成可执行的 servlet,请使用 Java 编译器,就像我们通常做的那样

javac HelloWorld.java

如果您的 CLASSPATH 环境变量未正确设置,javac 将报错,提示它无法解析符号 HttpServletResponse、ServletException 和许多其他类。通过将您的 CLASSPATH 设置为包含 servlet.jar 来纠正此问题,如上所示。

一旦 servlet 被编译,您应该能够使用 http://localhost:8080/examples/servlet/RequestInfoExample 调用它。

如果您将 firstname 参数附加到 URL,servlet 也应该打印您的名字:http://localhost:8080/examples/servlet/RequestInfoExample?firstname=Reuven。

如您所见,这个 servlet 非常简单。它导入了许多其他有用的 Java 包,包括最重要的 javax.servlet.* 和 javax.servlet.http.* 层次结构。然后,我们将我们的 servlet 定义为 HttpServlet 的子类。这样做,我们继承了 HttpServlet 的所有逻辑。

我们的 HelloWorld servlet 特别简单,并且包含一个方法 doGet。每当使用 GET 方法调用 servlet 时,都会调用 doGet。HTTP 支持多种方法,但最常见的是 GET 和 POST;GET 通常在用户直接请求 URL 或单击超链接时使用,而 POST 在有人单击 HTML 表单底部的“提交”按钮时使用。因为我们的 servlet 定义了一个 doGet 方法,但没有定义 doPost 方法,所以它只能处理 GET 请求。

doGet 的两个参数描述了 HTTP 请求和响应。如果我们想从 HTTP 请求中检索信息,我们使用请求对象上的一个方法。例如,我们可以使用 getParameter 方法检索与 firstname 参数关联的值

String firstname = request.getParameter("firstname");

如果请求中未传递 firstname 参数,则变量“firstname”将被赋值为 null 值。(这与空字符串不同,空字符串表示参数已在 HTTP 请求中传递,但不包含任何值。)

我们可以通过调用响应对象上的方法来类似地影响 HTTP 响应。例如,我们可以使用 setContentType 方法设置 HTTP 响应的 MIME 类型

response.setContentType("text/html");

要将信息发送到用户的浏览器,我们使用 response.getWriter(),它返回一个 PrintWriter

PrintWriter out = response.getWriter();
假设我们发送的内容类型为 text/html,我们现在可以使用 out.println 将 HTML 发送到用户的浏览器
out.println("<HTML>");
out.println("<Head><Title>Hello, world</Title></Head>");
Apache

我们可以继续使用 Tomcat 作为我们的主要 HTTP 服务器。但是,它既不如 Apache 或大多数其他服务器快,也不如它们可配置。因此,通常的做法是使用 Apache 处理大多数 HTTP 请求,并将 servlet 或 JSP 相关请求转发到 Tomcat。

为了让 Apache 与 Tomcat 通信,我们必须将一个模块编译到我们的 Apache 服务器中。传统的方式是使用 mod_jserv,它基于一个名为 JServ 的项目。一个新的模块 mod_jk,其编译方式与 mod_jserv 类似,但使用更高效、更灵活的协议与 Tomcat 通信。

安装 mod_jk 最简单的方法是从 Jakarta 网站下载 Tomcat 的源代码。即使您已经下载了 Tomcat 的二进制版本以供一般使用,您也需要检索源代码才能编译和安装 mod_jk。解压缩源代码发行版后,切换到 src/native/apache1.3。如果您使用的是早期版本的 Apache 2.0,则应转到 src/native/apache2.0 以获取 mod_jk 源代码。

以下说明假设您已构建 Apache 服务器,使其能够处理 DSO,即最初未编译到服务器中的动态加载模块。DSO 是一种非常灵活的机制,用于添加新模块;并非所有模块都能始终处理这种灵活性,您可能会发现自己静态地编译了部分或全部 Apache 服务器。虽然 mod_jk 当然可以静态编译,但在线文档鼓励用户将其作为 DSO 安装,这既是因为这样做更容易,也是因为它意味着您可以更新 mod_jk,而无需重新编译 Apache。

将模块编译为 DSO 意味着它必须使用在服务器编译时指定的名称和地址链接到 Apache 服务器。为了让我们使用与服务器相同的环境和信息来编译模块,Apache 提供了 apxs,这是一个 Perl 程序,可确保我们的模块被正确编译。apxs 接受与 cc 相同的参数,以及它自己的几个参数,这些参数允许我们自动安装模块。

在 apache1.3 目录中,我们可以使用以下命令在 httpd.conf 中编译 mod_jk

/usr/local/apache/bin/apxs -i -o mod_jk.so -I../jk \
   -I$JAVA_HOME/include \
   -I$JAVA_HOME/include/linux -c *.c ../jk/*.c

如果您在一行而不是三行中输入上述命令,请记住在每行前两行的末尾包含反斜杠 (\)。另请注意,我们没有包含 -a 选项,该选项会在 Apache 配置文件中激活模块,因为(正如我们很快将看到的)这是从另一个自动生成的配置文件中完成的。

现在 mod_jk 已安装,我们必须让 Tomcat 和 mod_jk 相互通信。通常,Tomcat 希望接收来自 Apache 的使用 Ajpv12 协议的请求。但是,mod_jk 和 Tomcat 都理解 Ajpv13 协议,该协议在许多方面都更高级。因此,我们需要修改我们的 Tomcat 配置,使其支持 Ajpv13,然后配置 Apache 以使用该协议与 Tomcat 通信。

Tomcat 有两个配置文件,一个用于 HTTP 服务器 (web.xml),另一个用于 Java servlet 容器 (server.xml) 文件。即使您从未使用过 XML,也不必太担心;XML 不仅易于学习,而且 Tomcat 配置文件也带有大量注释。这两个配置文件都位于 $TOMCAT_HOME/conf 目录中。

为了告诉 Tomcat 使用 Ajpv13,我们必须找到 server.xml 中定义 Ajp12 连接器的部分。当您安装 Tomcat 时,该部分通常如下所示

<ConnectorclassName="org.apache.tomcat.service.

    <Parameter name="handler"
     value="org.apache.tomcat.service.connector.

    <Parameter name="port" value="8007"/>
</Connector>

如您所见,这定义了 TCP/IP 连接器处理程序,并指示 Tomcat 应使用 org.apache.tomcat.service.connector 包中的 Ajp12ConnectionHandler 对象。我们将在上面显示的块之后立即添加一个类似的块

<Connector className="org.apache.tomcat.service.

   <Parameter name="handler"
    value="org.apache.tomcat.service.connector.

   <Parameter name="port" value="8009"/>
</Connector>
除了更改处理程序对象的名称外,您可以看到我们将端口号修改为 8009。

mod_jk 说明明确指出,我们应该将新的 Ajp13 处理程序添加到 server.xml,同时保留 Ajp12 处理程序。否则,当您尝试关闭 Tomcat 时可能会出现问题。

使用 $TOMCAT_HOME/bin/shutdown.sh 关闭 Tomcat,然后使用 $TOMCAT_HOME/bin/startup.sh 再次启动它,以确保配置更改没有破坏任何东西。如果一切正常,您应该看到消息指示 HttpConnectionHandler 在端口 8080 上运行,Ajp12ConnectionHandler 在端口 8007 上运行,Ajp13ConnectionHandler 在端口 8009 上运行。

告诉 Apache 如何连接到 Tomcat 的最简单、最快的方法是使用 mod_jk.conf-auto,Tomcat 每次重启时都会生成一个文件。此文件位于 $TOMCAT_HOME/conf 中,包含加载和使用 Tomcat 所需的所有 Apache 指令。您只需从您的 Apache 配置中包含这组定义

Include /usr/java/jakarta-tomcat-3.2.1/conf/mod_jk.conf-auto

mod_jk.conf-auto 不仅有用且自动化,它还提供了如何配置 mod_jk 以及如何在 Apache 和 Tomcat 之间创建复杂交互的良好思路。使用 Apache 和 Tomcat 时要记住的一件事是,Apache 必须始终在 Tomcat 运行后启动,以便它可以连接到适当的套接字。

一组简单的 Servlet

为了演示编写 servlet 有多容易,我们将创建一个简单的 Web 应用程序——一个博客创建工具。博客,或“Web 日志”,是越来越流行的 Web 日记,其中最新的条目传统上出现在顶部。第一个 Web 日志是 Dave Winer 的 Scripting News (http://www.scripting.com/),但有成千上万个 Web 日志提供有关各种主题的有用新闻和评论。

我们将使用 servlet 创建一个非常简单的 Web 日志。实际的日志条目将存储在 PostgreSQL 数据库中,我们可以按如下方式定义它

CREATE TABLE BlogEntries (
  entry_id       SERIAL    NOT NULL  PRIMARY KEY,
  entry_date     DATETIME  NOT NULL  CHECK

  entry_headline TEXT      NOT NULL  CHECK

  entry_text     TEXT      NOT NULL  CHECK

  UNIQUE(entry_date, entry_headline)
);

由于我们将按日期和标题检索数据,我们在两列上各创建一个索引

CREATE INDEX headline_date_index ON BlogEntries

CREATE INDEX entry_headline_index ON BlogEntries (entry_headline);
现在我们已经创建了数据库表和索引,我们将需要创建两个 servlet:一个 servlet 将接收来自 HTML 表单的输入,并使用该输入将新行插入到 BlogEntries 表中。(据推测,此 servlet 将仅对站点所有者可用,站点所有者是 Web 日志的编辑。)第二个 servlet 将检索过去三天内的所有 Web 日志条目,并以传统的后进先出的顺序显示它们。

用于添加新 Web 日志条目的 servlet AddBlogEntry [参见清单 3,网址为 ftp://ftp.linuxjournal.com/pub/lj/listings/issue84/],期望从 HTML 表单接收两个参数。第一个参数 (entry_headline) 包含标题,而第二个参数 (entry_text) 包含与之关联的文本。

清单 3 中的 servlet 定义了一个实例变量 con,其中包含 JDBC 数据库连接。该 servlet 还定义了三个方法

  • init,在 servlet 首次执行之前执行。在 init 中,我们与数据库建立初始连接,并将连接保留以供将来使用。

  • doGet,打印一条错误消息,指示此 servlet 仅接受 POST 请求。

  • doPost,它使用 init 建立的数据库连接将新行 INSERT 到 BlogEntries 表中。

修改 servlet 与修改 CGI 程序不同,servlet 容器必须从磁盘重新加载 servlet。Apache 和 mod_perl 默认情况下不重新加载 Perl 模块;Tomcat 也默认忽略修改后的 servlet。您可以通过将“reloadble”属性设置为“true”来更改此行为;如果您未能这样做,您将需要在每次修改和重新编译 servlet 时重新启动 Tomcat。当然,当 servlet 可重新加载时,会存在性能损失,这就是为什么 Tomcat 文档建议在生产系统中保持它们不可重新加载的原因。

我们的 doPost 方法是此 servlet 中的真正主力,它从用户的 HTML 表单获取输入,并将它们插入到 PostgreSQL 中的表中。

首先,我们确保我们已收到来自用户的 entry_headline 和 entry_text 参数,并且这些参数不为空。如果一个或多个为空,那么我们创建一个消息,指示缺少什么。否则,我们将继续创建一个 PreparedStatement,用于将新行插入到数据库中。

Perl 程序员将看到 JDBC 和 Perl 的 DBI 之间有很多相似之处。JDBC 要求我们基于数据库连接创建一个语句

PreparedStatement statement =
   con.prepareStatement(
      "INSERT INTO BlogEntries " +
      "  (entry_date, entry_headline, entry_text) " +
      "  VALUES " +
      "  (NOW(), ?, ?)"
      );

由于我们使用的是 PreparedStatement 而不是简单语句,因此我们可以使用问号 (?) 代替变量值。某些数据库(例如 Oracle)的驱动程序利用这些占位符并使用它们来提高速度。但即使是低端数据库的用户也可以从使用占位符中受益,因为它们确保字符串将被正确引用,即使它们包含引号或撇号

statement.setString(1, entry_headline);
statement.setString(2, entry_text);
请注意,第一个占位符编号为 1,而不是 0。请记住,这两个值都是字符串;如果它们是整数或浮点数,我们将不得不使用语句上的不同方法。

接下来,我们执行实际的插入

int updateCount = statement.executeUpdate();

updateCount 被分配了受 executeUpdate() 方法影响的行数。在本例中,我们试图插入单行,因此我们将 updateCount 与 1 进行比较。如果我们使用 executeUpdate() 来执行 SQL “UPDATE”,updateCount 可能包含不同的数字。

最后,我们捕获在我们使用 SQL 期间可能发生的异常。然后,我们打印一条错误消息,包括异常的文本。虽然在生产网站上向最终用户打印如此明确的消息可能不是一个好主意,但在开发过程中这是一个绝妙的主意。

显示 Web 日志

现在我们已经了解了如何使用 servlet 将信息输入到我们的 Web 日志中,我们将编写另一个 servlet 来显示最新内容。此 servlet 将相对简单;它将不带任何参数,并将显示 Web 日志的最新内容(参见清单 4,网址为 ftp://ftp.linuxjournal.com/pub/lj/listings/issue84/)。

我们的 ShowBlog servlet 将只有两个方法,init(与 AddBlogEntry 中的“init”方法相同)和 doGet。doGet 将检索 Web 日志中的所有条目,从最新的到最旧的。它将每个条目显示为 HTML 表格中的三列行,显示添加条目的日期和时间、标题以及与该标题关联的文本。

当然,真正的 Web 日志会以稍微更智能的方式做事,限制评论的数量,并以更好的设计感来安排它们。但是,一旦我们以正确的顺序从数据库中检索到信息,这很容易做到。

我们创建我们的查询(在“synchronized”块内)并将其包装到 Statement 中。请注意,我们不需要使用 PreparedStatement,因为我们不打算将任何变量值实例化到语句中。

我们将查询的结果检索到 ResultSet 中

ResultSet rs = statement.executeQuery(query);

ResultSet 允许我们一次从数据库中拉取一行结果。我们可以使用 rs.next() 方法在 while 循环中迭代每一行。在每次迭代中,我们可以使用 rs.getString() 方法检索列作为 String 值,并将列名作为参数传递。

在编译此 servlet 并将其放在我的系统上之后,我能够在几分钟内添加一些新的 Web 日志条目并显示它们。

结论

Servlet 是 Java 世界中与 Perl 世界中用于 mod_perl 的模块等效的东西。在许多方面,它们实际上更好,因为它们提供了强大的功能,而不会因潜在的风险程序而危及 Web 服务器。本月,我们看到了一些使用 servlet 和我们可以从 Web 下载的开源工具构建 Web 应用程序的简单方法。下个月,我们将继续探索服务器端 Java,方法是查看 Java Server Pages(也称为 JSP)的一些简单用法。

资源

Server-Side Java with Jakarta-Tomcat
Reuven M. Lerner 拥有一家小型咨询公司,专门从事 Web 和互联网技术,并对其进行管理。当您阅读本文时,他应该(终于!)完成 Core Perl 的编写,该书将于今年晚些时候由 Prentice-Hall 出版。您可以通过 reuven@lerner.co.il 或 ATF 主页 http://www.lerner.co.il/atf/ 与他联系。
加载 Disqus 评论