JavaServer Pages
上个月,我们初步了解了服务器端 Java,通过编写一些 servlet 程序来试水。Servlet 程序是生成动态 Web 内容的 Java 程序。CGI 程序是 Web 服务器外部的可执行程序,每次调用时都从头开始执行。相比之下,Java servlet 程序存在于 servlet 容器(Java 虚拟机 (JVM))中,该容器与 HTTP 服务器紧密相连。每当 Web 服务器需要动态内容时,它都会向 servlet 容器发出请求。
在许多方面,编写 Java servlet 程序就像编写 mod_perl 处理程序:它为您提供了强大的功能,但也需要相当多的规范。编写 90% 是静态 HTML 和 10% 是 Java 的 servlet 程序也可能令人沮丧,而且您调用 out.println() 的次数可能会变得令人恼火。
JavaServer Pages 或 JSP 是解决此问题的一种日益流行的方案。JSP 在精神上类似于 Microsoft 的 ASP,以及开源的 PHP 语言和 Mason 组件系统。JSP 允许您以多种不同的方式将 Java 与 HTML 混合使用。JSP 与大多数 Java 程序一样,具有卓越的平台独立性,这意味着您可以在 Windows 机器上编写 JSP,在开发 Linux 服务器上运行它们,然后在 Solaris 上部署它们。
本月,我们将快速了解 JSP,JSP 是学习 Java 的一种好方法,也是 Java 程序员轻松创建 servlet 程序而无需过于努力的一种简便方法。
JSP 背后的想法非常简单:它们是伪装的 servlet 程序。当首次调用 JSP 时,它会自动转换为 servlet 程序。然后,此 servlet 程序被编译成 Java .class 文件,然后在 servlet 容器内部执行。首次调用 JSP 时,由于幕后发生的所有操作,将需要更长的时间才能将数据返回给用户。
在 JSP 中,除非放置在特殊的大括号 <% 和 %> 内,否则一切都被假定为静态内容。这些标记在 JSP 术语中称为“scriptlet”。以下 HTML 文件(我们将其命名为 main.jsp)也是一个完全合法的 JSP
<HTML> <Head> <Title>Static JSP Title</Title> </Head> <Body> <P>Static JSP Content</P> </Body> </HTML>
上面的 JSP 相当枯燥,因为它完全由静态内容组成。但是 JSP 引擎并不关心 JSP 包含多少动态内容;无论其复杂程度如何,它都会将整个内容转换为 servlet 程序。对于上面的 JSP,生成的 servlet 程序将只不过是在 doGet() 方法内部的一长串 out.println() 语句。
在我的系统上,我将上面的 HTML 保存到 Tomcat 附带的 examples 目录中的 main.jsp 中 /usr/java/jakarta-tomcat-3.2.1/webapps/examples/jsp/。诚然,这不是安装它的最佳位置,但却是最简单的位置。
安装 JSP 后,我不需要执行任何其他操作;系统会自动将其转换为 servlet 源代码 (.java) 文件,然后将其编译为 Java .class 文件。
我们可以通过 Tomcat 服务器执行和查看我们的 JSP,该服务器默认在端口 8080 上运行,https://:8080/examples/jsp/main.jsp/。
如果您已配置 Apache 和 mod_jk 将 servlet 和 JSP 查询转发到 Tomcat,那么您也应该能够使用此 URL 查看 main.jsp:https:///examples/jsp/main.jsp/。
在我的系统上,JSP 系统为 main.jsp 生成的 .java 和 .class 文件位于目录:/usr/java/jakarta-tomcat-3.2.1/work/localhost_8080%2Fexamples 中。如果我列出此目录的内容,我看到以下内容
_0002fjsp_0002fmain_0002ejspmain.class _0002fjsp_0002fmain_0002ejspmain_jsp_0.java _0002fjsp_0002fmain_0002ejspmain_jsp_1.java _0002fjsp_0002fmain_0002ejspmain_jsp_2.java _0002fjsp_0002fmain_0002ejspmain_jsp_3.java _0002fjsp_0002fmain_0002ejspmain_jsp_4.java _0002fjsp_0002fmain_0002ejspmain_jsp_5.java _0002fjsp_0002fmain_0002ejspmain_jsp_6.java
如您所见,有七个不同的 .java 文件,每个文件都对应于原始 JSP 的不同版本。每次我修改 JSP 时,系统都必须创建一个新的 .java 文件。Tomcat 默认保留以前版本的基于 JSP 的 servlet 程序;但是,在给定时间只能有一个 .class 文件,这在此目录中显然是这种情况。
.java 和 .class 文件的名称很长,不适合直接输入到 Web 浏览器中。JSP 的部分魔力在于 Tomcat 可以智能且自动地找到与给定 URL 关联的 servlet 程序,并在必要时创建 Java 源代码文件。
您应该查看 JSP 转换器创建的 Java 源代码,以便了解幕后完成的艰苦工作。我们简单的静态 JSP 已被转换为一个超过 100 行 Java 源代码的 servlet 程序。万一我们需要从转换后的 servlet 源代码调试我们的 JSP(这是一项艰巨的任务,任何使用过 Perl 的 HTML::Mason 的人都知道),servlet 程序都包含注释,这些注释提供了从原始 JSP 中的行号到生成的 servlet 程序中的行号的基本映射。
我们可以通过向 main.jsp 添加一个特殊的 JSP 标记来使事情变得更有趣。第一个标记将一些 Java 代码的结果插入到发送给用户的输出中
<HTML> <Head> <Title>Mini-dynamic JSP Title</Title> </Head> <Body> <P>You are connecting from <%= request.getRemoteHost() %>.</P> </Body> </HTML>
调用 <%= %> 内部的表达式,并将其返回值放置在输出流中。由于 JSP 是伪装的 servlet 程序,因此它可以访问通常可用于 servlet 程序的对象,例如“request”和“response”。请注意,<%= %> 内的 Java 代码末尾没有分号;根据个人经验,我可以告诉您,很难打破在那里插入分号的习惯,但是如果您坚持这样做,您的 JSP 将会崩溃。
要执行一个或多个 Java 计算,而无需将结果发送到用户的浏览器,请使用基本的 <% %> 标记。这些标记可以与 HTML 交错使用,从而可以在响应中显示条件文本(请参阅列表 1)。
如果用户的计算机的主机名可用,我们将打印其名称。否则,我们将打印主机的 IP 地址。请注意 if/then/else 块是如何与静态 HTML 交错的。仅当 request.getRemoteHost() 返回空字符串 (“”) 时,才会调用 request.getRemoteAddr() 调用。
许多 JSP 指令都使用 <%@ %> 标记调用。所有这些指令都在 JSP 到 servlet 程序的转换时生效。指令关键字紧跟在 @ 符号之后,后跟零个或多个属性。
例如,假设我们在名为 menubar.jsp 的 JSP 中有一个标准的站点范围菜单栏
<table> <tr> <td><a href="one">Option 1</a></td> </tr> <tr> <td><a href="two">Option 2</a></td> </tr> </table>
我们可以使用“include”指令将其合并到我们的文档中(请参阅列表 2)。
重要的是要记住,指令在 JSP 转换为 servlet 程序时生效,而不是在运行时生效。因此,在您更改 menubar.jsp 之前,上面的示例将可以正常工作。由于 menubar.jsp 的内容在 main.jsp 转换为 servlet 程序之前已合并到 main.jsp 中,因此 <%@ include %> 标记不再存在,因此不会以我们可能期望的方式更新内容。解决方案是使用运行时 JSP 操作,如下所述。
还有两个额外的特殊 JSP 标记。其中之一 <%-- --%> 充当注释。虽然在 HTML 注释已经存在的情况下使用 JSP 注释似乎很奇怪,但重要的是要记住区别:JSP 注释在创建 servlet 程序时由 JSP 引擎删除。相比之下,HTML 注释会原封不动地传递,并且任何选择浏览器上的“查看源代码”选项的最终用户都可见。给定选择,我倾向于将大多数注释放在 JSP 注释标记内,除了那些可以帮助我使用生成的 HTML 源代码调试 JSP 输出的注释之外。
最后一个 JSP 标记 <%! %> 允许您为 JSP 生成的 servlet 程序声明实例变量(也称为字段)。虽然使用声明标记来声明您将在 JSP 的其余部分中使用的变量似乎很诱人,但请记住,使用字段意味着您必须处理线程安全问题。考虑到与线程相关的麻烦,如果可以避免它们,这可能是一个好主意。您也可以使用声明标记来定义 JSP 本地的新方法,尽管我不相信这是一个好主意。
如果我们想影响 servlet 程序的构建方式,指令很有用。但是,如果我们想在运行时影响 servlet 程序的行为呢?
当然,我们可以包含 Java 代码来执行这些操作。但是 JSP 包含许多特殊标记,这些标记在 servlet 程序中被转换为 Java 代码,从而使您无需了解任何 Java 即可编写代码。
JSP 操作标记实际上是 XML,并在称为标记库的特殊 XML 文档中定义。因此,尽管它们可能看起来像 HTML,但它们不是,这通常意味着您必须特别注意诸如闭合标记和斜杠之类的项目。
内置的 JSP 标记库包含许多功能,其中一个功能看起来与我们在列表 2 中看到的 include 指令非常相似。列表 3 显示了 main.html 的一个版本,该版本使用 <jsp:include> 操作而不是指令来引入 menubar.jsp。
此版本与其前身之间的区别是微妙但重要的:include 指令在 JSP 转换为 servlet 程序时合并命名页面,而 include 操作在运行时工作。如果您在 main.jsp 的调用之间修改 menubar.jsp,则指令版本将忽略新的菜单栏,而操作版本将显示最新版本。当然,这是有代价的;<jsp:include> 操作执行运行时请求,使其比指令慢且效率低。
由于 <jsp:include> 请求的页面可以访问来自顶层 JSP 的所有请求信息,因此可以使用 <jsp:include> 创建动态更改的菜单栏、个性化系统和数据库访问库。
还有其他 JSP 操作。其中之一是 <jsp:forward>,它将请求传递给另一个 JSP。与 <jsp:include> 一样,这发生在 servlet 引擎内部,这意味着 HTTP 请求和用户会话信息仍然可用。例如,列表 4 显示了 JSP 的一个版本,如果用户的 hostname 无法识别,则该版本会将用户转发到另一个页面。
如果您的服务器不包含名为 no-reverse.jsp 的页面,则用户将在其 Web 浏览器中收到 404(文件未找到)错误。但是,他们的浏览器将继续显示原始请求页面 main.jsp 的 URL。这是因为 JSP 转发是在内部执行的,无需外部 HTTP 转发。
上个月,我们编写了一些简单的 servlet 程序,使我们能够创建和查看 Web 日志,有时也称为博客。由于 JSP 被转换为 servlet 程序,因此我们没有理由不能创建一个 JSP 来完成与该 servlet 程序相同的事情。它显然看起来会有些不同,但效果应该是相同的。
列表 5 包含一个 JSP (showblog.jsp),它执行与上个月的 ShowBlog servlet 程序相同的任务。换句话说,此 JSP 打印我的 Web 日志的内容,该内容存储在 PostgreSQL 数据库表中,并从最新条目到最旧条目排序。
我现在应该注意到,showblog.jsp 是如何编写 JSP 的一个糟糕示例;它仅仅是为了演示什么是可能的,而不是什么是优雅或最好的。(在接下来的两个月中,当我们讨论 JavaBeans 和自定义标记库时,我们将讨论此类问题。)
让我们浏览一下此 JSP,以便您可以准确了解它的工作原理。
我们从两个“page”指令开始。这些指令允许我们为 JSP 设置基本配置,从页面将返回的 MIME ContentType 标头开始,甚至允许我们指定要与非编程文本交错的编程语言。虽然这种功能实际上并不存在,但在理论上可以编写使用 Perl 生成 XML 或使用 Python 生成 PNG 图像的 JSP。
请注意,我们如何在 page 指令中命名一个或多个属性。showblog.jsp 的第一行同时设置了 language 和 ContentType 属性。第二行指示生成的 servlet 程序应导入 java.sql 中的包,这将使我们能够使用 JDBC 连接到我们的关系数据库服务器(在本例中为 PostgreSQL)。
在简短的介绍性 HTML 之后,我们深入研究 Java。我们创建一个 SQL 连接对象并使用它连接到我们的 PostgreSQL 服务器。我们从数据库中检索数据,然后逐行迭代 ResultSet。
正如我上面指出的,这是一种编写 JSP 的糟糕方法。不仅由于创建和销毁的数据库连接过多而导致性能会很差,而且我们还创建了代码和 HTML 的可怕混杂。实际上,我们似乎在这里节省的唯一东西是编写 out.println() 来生成 HTML 输出所付出的一些努力。
此外,JSP 背后的许多意图是从 HTML 页面中删除代码,从而使非程序员能够以最小的努力创建动态页面。如果我们必须插入这么多代码才能创建动态页面,那么非程序员想要尝试 Web 开发的可能性就很小。
此外,我们将 ShowBlog servlet 程序转换为 JSP 导致删除了几个异常处理例程。我们的 servlet 程序足够智能,可以处理 PostgreSQL 服务器的消失,并且可以生成合理的错误消息。相比之下,我们的 JSP 生成了一个包含错误消息的回溯。此回溯对开发人员很有用,但对于将访问我们网站的最终用户来说,既不友好也不实用(公平地说,我们可以在 page 指令中设置 errorPage 属性,以便将错误转发到不同的 JSP)。
一个好的解决方案是从 JSP 中删除尽可能多的代码,允许非程序员以标准方式使用该代码,并将编程内容和非编程内容分开。实际上,JSP 附带了对 JavaBeans 的支持,其中每个“bean”实际上是一个对象,该对象具有我们可以从 JSP 内部使用,使用特殊的 <jsp: > 操作的各种方法。成功部署 JSP 的诀窍在很大程度上取决于 JavaBeans 的智能使用。此外,JSP 允许我们创建自己的标记库来定义自定义操作,以便我们可以用类似于各种 <jsp: > 操作的标记替换更多代码。
JavaServer Pages 或 JSP 为 servlet 程序提供了一层语法糖,servlet 程序有时对于非程序员来说可能难以学习。但是,本月最复杂的 JSP 示例表明,它们很容易失控,包含的代码几乎与典型的 servlet 程序一样多。虽然总体而言使用 JSP 更容易,但随着 JSP 变得更加复杂,将代码与 HTML 分开的所谓好处也随之消失。
下个月,我们将研究 JavaBeans,它允许我们将相当多的编程推送到 JSP 外部定义和维护的类中。在此之后,我们将研究 JSP 的自定义标记库,这使得我们可以创建自己的小语言以在我们的 JSP 内部使用。
