Web 应用程序与 Java/JSP
所有酷炫的新编程语言,如 Ruby,总是拥有适用于 Linux 的编译器/解释器和工具,而当您需要时,像 Tcl/Tk 这样的旧 UNIX 支柱仍然存在。那么,为什么 Java 在 Linux 领域不是一个普遍存在的参与者呢?
Linux 和 Java 确实有很多可以相互提供的东西。两者都是坚如磐石且可扩展的服务器级软件系统,并且大多数拥有软件相关学位的大学毕业生都熟悉它们,从而形成了强大的组合。在本文中,我将通过 Java Servlet 规范、Java 编程语言本身和 Java Server Pages 向您介绍 Java Web 应用程序。这三个工具可以帮助您在比您想象的更短的时间内运行 Web 应用程序。
Java Servlet 规范定义了 Servlet 容器、Web 应用程序和 Servlet API,Servlet API 是将这些部分粘合在一起的胶水。
Servlet 容器类似于 Web 服务器,但它也知道如何部署和管理 Web 应用程序,因此通常被称为应用程序服务器。Servlet 容器提供支持 Servlet API 的服务,Web 应用程序使用 Servlet API 与 HTTP 请求和响应进行交互。
Java Web 应用程序是一个自包含的配置文件、静态和动态资源、编译类和支持库的集合,所有这些都被 Servlet 容器视为一个有凝聚力的单元。它们与标准的 LAMP 风格的 Web 应用程序有些不同,后者更像是关联程序或脚本的集合,而不是正式定义的、自包含的单元。为了演示 Java Web 应用程序,我开发了一个简单的“时间表”,其中包含一些标准的 Java 库,这些库帮助我编写了它。
通常,Web 应用程序打包在 WAR(Web 存档)文件中,WAR 文件只是一个具有特殊目录结构和配置文件的 ZIP 文件。Web 应用程序的目录结构在逻辑上和物理上分隔了这些类型的文件。WEB-INF 目录包含所有配置文件,lib 目录包含所有库(打包在 JAR 或 Java 存档文件中),classes 目录包含应用程序的编译代码。清单 1 显示了 Web 应用程序的文件布局以供参考。
清单 1. timesheet.war 的内容
index.jsp tasks.jsp WEB-INF/web.xml WEB-INF/lib/jstl-impl-1.2.jar WEB-INF/lib/jstl-api-1.2.jar WEB-INF/classes/lj/timesheet/Task.class WEB-INF/classes/lj/timesheet/GetTasksServlet.class WEB-INF/classes/lj/timesheet/BaseServlet.class WEB-INF/classes/lj/timesheet/Client.class WEB-INF/classes/lj/timesheet/SaveTaskServlet.class WEB-INF/classes/ApplicationResources_en.properties WEB-INF/classes/ApplicationResources_de.properties WEB-INF/classes/ApplicationResources.properties WEB-INF/classes/ApplicationResources_es.properties WEB-INF/classes/ApplicationResources_fr.properties META-INF/context.xml META-INF/MANIFEST.MF
WEB-INF 目录还包含一个特殊文件 web.xml,它被称为 Web 应用程序的部署描述符。它定义了 Web 应用程序的所有行为,包括 URI 映射、身份验证和授权。让我们看一下此 Web 应用程序的部署描述符。
清单 2. web.xml
<?xml version="1.0" encoding="ISO-8859-1" ?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <servlet> <servlet-name>get-tasks</servlet-name> <servlet-class>lj.timesheet.GetTasksServlet</servlet-class> </servlet> <servlet> <servlet-name>save-task</servlet-name> <servlet-class>lj.timesheet.SaveTaskServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>get-tasks</servlet-name> <url-pattern>/tasks</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>save-task</servlet-name> <url-pattern>/save-task</url-pattern> </servlet-mapping> <security-constraint> <web-resource-collection> <web-resource-name>Protected Pages</web-resource-name> <url-pattern>/tasks</url-pattern> <url-pattern>/save-task</url-pattern> </web-resource-collection> <auth-constraint> <role-name>*</role-name> </auth-constraint> </security-constraint> <login-config> <auth-method>BASIC</auth-method> <realm-name>Timesheets</realm-name> </login-config> <security-role> <description>Users of the timesheet application</description> <role-name>user</role-name> </security-role> </web-app>
您可以看到,每个 servlet 都在 <servlet> 元素中定义,该元素定义了包含代码的 Java 类,以及 servlet 的名称(稍后使用)。定义 servlet 后,然后使用 <servlet-mapping> 元素将它们(按名称)映射到传入的 URI。这种 servlet 映射可能看起来很繁琐和冗长,但出于以下几个原因,它可能非常强大
您可以将一个 servlet 映射到多个 URI。
您可以使用通配符映射 (/foo/bar/*)。
您可能不想向远程访问者透露任何代码结构。
您可能有根本不想映射的 servlet。
在 servlet 映射之后是容器管理的身份验证和授权。Servlet 规范要求 Servlet 容器提供身份验证和授权机制,并且 Web 应用程序中的配置是声明性的:web.xml 只是指定哪些资源受到保护以及谁被允许访问它们,使用基于角色的授权约束。设置非常简单明了,并且 Web 应用程序无需在应用程序内部实现该功能,从而变得更简单。在此应用程序中,我选择使用 HTTP BASIC 身份验证来简化应用程序。DIGEST、FORM 和 (SSL) CLIENT-CERT 是 Servlet 规范允许的其他选项。
容器配置
您可能想知道 Servlet 容器如何了解数据库的任何信息。答案在另一个配置文件中找到,该文件特定于每个 Servlet 容器,其中包含此信息。您可以查看示例 Web 应用程序文件附带的 conf/context.xml 文件(请参阅资源),但您必须参考 Apache Tomcat 网站以了解有关此 Tomcat 特定配置文件格式的详细信息。如果您想在不同的应用程序服务器上部署示例应用程序,则需要编写自己的容器特定配置文件,其中包含您的数据库配置。
现在您已经了解了 Web 应用程序是如何打包和部署的,接下来让我们关注 Web 应用程序中的实际操作:代码。
Java 既是一种编程语言,也是一种运行时环境,很像 Perl 和 PHP。在这些情况下,编译器通常在脚本执行时调用,而 Java 始终是预先编译的。Java 编程语言本身是面向对象的、过程式的、块结构的,并且对于任何用类似 C 的语言编写过代码的人来说都完全熟悉。它有许多明确定义的原始数据类型以及引用类型。您编写的所有 Java 代码都存在于类的定义中,包括 servlet 代码。
清单 3. GetTasksServlet.java
package lj.timesheet; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class GetTasksServlet extends BaseServlet { public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getUserPrincipal().getName(); try { List<Client> clients = getClients(); // Convert client list to lookup table Map<Integer,Client> clientMap = new HashMap<Integer,Client>(clients.size()); for(Client client : clients) clientMap.put(client.getId(), client); request.setAttribute("clients", clients); request.setAttribute("clientMap", clientMap); request.setAttribute("tasks", getTasks(username)); getServletContext().getRequestDispatcher("/tasks.jsp") .forward(request, response); } catch (SQLException sqle) { throw new ServletException("Database error", sqle); } } ... }
文件的第一行声明了定义类的“包”。包有助于保持代码井井有条,并对变量、方法和类范围和可见性产生影响。接下来的几行是“imports”,它们向编译器指示此类将引用哪些类。那些以java.开头的类是标准 Java 类,而那些以javax.servlet开头的类是由 Java Servlet 规范提供的类。然后,我们定义一个名为 GetTasksServlet 的类,它扩展了一个名为 HttpServlet 的现有类,HttpServlet 是所有面向 HTTP 的 servlet 的基础。HttpServlet 类定义了许多 doXXX 方法,其中 XXX 是 HTTP 方法之一,例如 GET (doGet)、POST (doPost)、PUT (doPut) 等。我覆盖了 doGet 方法,以便响应来自客户端的 HTTP GET 请求。
doGet 方法接受两个参数:请求和响应,它们提供了 Servlet 容器提供的资源以及客户端为特定 HTTP 请求提供的信息的钩子。我使用两个实用程序方法(稍后在类中定义)来获取客户端列表和任务列表,并将它们存储在请求对象的“属性”中,这是一个可以放置数据以便在请求处理阶段之间传递数据的位置。当我介绍用于生成内容的 JSP 文件时,您将看到如何访问此信息。最后,我调用“请求调度程序”的 forward 方法,该方法告诉容器将请求转发到另一个资源:tasks.jsp。
Java Server Pages (JSP) 是一种用于动态内容生成的技术,用于诸如网页之类的内容。JSP 类似于 PHP 页面,静态文本可以与 Java 代码混合,结果将发送到客户端。从技术上讲,JSP 由特殊的 servlet(由 Servlet 容器提供)动态转换为它们自己的 servlet 并编译为字节码,然后像“普通”servlet 一样运行。清单 4 显示了 tasks.jsp 的代码,tasks.jsp 是 GetTasksServlet 的 doGet() 方法中引用的页面。
清单 4. tasks.jsp
<%@ page pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <fmt:setBundle basename="ApplicationResources" /> <html> <head> <title><fmt:message key="tasks.title" /></title> </head> <body> <h1><fmt:message key="tasks.title" /></h1> <table> <tr> <th><fmt:message key="tasks.header.date" /></th> <th><fmt:message key="tasks.header.client" /></th> <th><fmt:message key="tasks.header.description" /></th> <th><fmt:message key="tasks.header.duration" /></th> </tr> <c:forEach var="item" items="${tasks}"> <tr> <td> <fmt:formatDate dateStyle="short" value="${item.date}" /> </td> <td><c:out value="${clientMap[item.clientId].name}" /></td> <td><c:out value="${item.description}" /></td> <td><c:out value="${item.duration}" /></td> </tr> </c:forEach> </table> <form method="POST" action="<c:url value="/save-task" />"> <fieldset> <legend>Add New Task</legend> <div class="form-field"> <label for="clientId"> <fmt:message key="tasks.header.client" /> </label> <select name="clientId" id="clientId"> <option value="">Please choose…</option> <c:forEach var="client" items="${clients}"> <option value="${client.id}"> <c:out value="${client.name}" /> </option> </c:forEach> </select> </div> <div class="form-field"> <label for="description"> <fmt:message key="tasks.header.description" /> </label> <input type="text" name="description" id="description" size="50" /> </div> <div class="form-field"> <label for="duration"> <fmt:message key="tasks.header.duration" /> </label> <input type="text" name="duration" id="duration" size="4" /> </div> <div class="buttons"> <input type="submit" value="<fmt:message key="task.save" />" /> </div> </fieldset> </form> </body> </html>
该页面以页面声明开始,其中包括一些关于页面的元数据,包括输出字符编码,然后是一些“taglib”标签,这些标签告诉 JSP 编译器我想使用一些“标签库”。标签库是帮助程序库,允许 JSP 脚本使用非常简单的语法来运用强大的工具。在 DOCTYPE 之后,有一个 <fmt:setBundle> 元素,在页面的 <title> 中,有一个 <fmt:message> 元素。这两个标签由“fmt”标签库定义,它们协同工作,为此页面提供国际化功能。<fmt:setBundle> 标签定义了页面要使用的字符串资源束,<fmt:message> 标签使用该束从相应的文件中提取本地化文本以显示在页面中。结果是,当我使用设置为 en_US 语言环境的 Web 浏览器访问此页面时,我得到英文文本,但如果我将语言环境切换为 fr_BE 并重新加载页面,则页面将切换为法语,而无需任何进一步的编程。
标准的 Java API 实际上提供了所有这些开箱即用的功能,但 JSTL(Java 标准模板库)“fmt”标签库使我们能够访问 Java 的国际化 API,而无需编写任何 Java 代码。通过为我要支持的每种语言环境提供一个 Java 属性文件(一个具有简单键=值语法的文本文件),我几乎可以免费获得文本本地化。在 JSP 文件中更靠下的位置,您可以看到另一个“fmt”标签 <fmt:formatDate> 的使用。此标签使用用户的语言环境和格式的简单名称(在本例中为“simple”)格式化日期对象。这在美国的结果为 MM/dd/yy,在比利时的结果为 dd/MM/yy。
下一个 JSTL 标签是 <c:forEach>。此标签实际上包含一个主体,该主体被多次评估:对于在“items”属性中找到的每个项目评估一次。“items”值的${items}意味着该值不仅仅是一个简单的文字值,而是一个应该评估的表达式。“items”对象在请求对象的“属性”中找到(请记住,我将其放在 servlet 代码中),并在此处用作循环的数据。在 <c:forEach> 的主体中,定义了“item”对象,并且可以被任何 JSTL 标签使用。
关于作用域的说明
Java Servlet 规范定义了三个数据作用域:应用程序、会话和请求。JSP 规范添加了第四个作用域:页面。这些作用域中的每一个都是 Web 应用程序可以存储数据以便随时使用的地方。当在表达式中使用对象标识符时,它们会在每个作用域级别中查找,直到找到对象为止。首先搜索页面作用域,然后是请求,然后是会话,然后是应用程序。这就是 <c:forEach> 等标签如何定义新的对象名称,以及主体中的标签如何访问它们。
下一个标签 <c:out> 以 Web 安全的方式输出值。如果该值包含任何 < 字符,它们将被转义以避免恶心的 XSS 攻击。${clientMap[item.clientId].name}的值再次是一个表达式,它告诉 <c:out> 从 item 对象中获取客户端 ID,使用它在“clientMap”中查找值,然后获取其名称。“item”和“clientMap”对象都从请求属性中检索,<c:out> 标签为我们处理表达式评估和输出转义。
此页面包含一个允许我们输入新任务的表单。<form> 最重要的属性之一是“action”,当然,它告诉表单数据应该发送到哪里。我在这里使用 <c:url> 标签为我们生成 URL。当我可以直接使用/timesheet/save-task作为 action 属性的值时,使用标签似乎很傻,但是这里有一些微妙的问题在起作用,必须加以考虑。首先,Web 应用程序可以部署到任何“上下文路径”中,这意味着 servlet 的路径实际上可能是/my-timesheet/save-task。<c:url> 标签知道 Web 应用程序已部署在何处(由 Servlet API 定义的请求对象提供),并且可以为 URL 提供适当的路径前缀。其次,<c:url> 可以使用会话标识符对 URL 进行编码,这对于为许多 Web 应用程序提供良好的用户体验至关重要。如果客户端使用 cookie 将会话身份传达给服务器,则 <c:url> 标签足够智能,可以从 URL 中省略会话标识符,但在 cookie 不可用时将其包含在 URL 中作为后备。会话是 Servlet 规范定义的另一个方便的功能,由 Servlet 容器提供,并通过 Servlet API 访问。
现在我已经介绍了时间表的显示以及可用于提交新任务的表单,让我们看一下接受此表单提交的代码:SaveTaskServlet.java(清单 5),它实现了“save-task”servlet,该 servlet 映射到 URL /save-task。
清单 5. SaveTasksServlet.java
package lj.timesheet; import java.io.IOException; import java.util.Date; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Timestamp; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class SaveTaskServlet extends BaseServlet { public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Integer taskId; if(null == request.getParameter("id") || "".equals(request.getParameter("id").trim())) taskId = null; else taskId = new Integer(Integer.parseInt( request.getParameter("id"))); int clientId = Integer.parseInt( request.getParameter("clientId")); Date date = new Date(); String description = request.getParameter("description"); int duration = Integer.parseInt( request.getParameter("duration")); String username = request.getUserPrincipal().getName(); Task task = new Task(taskId, username, date, clientId, description, duration); try { save(task); response.sendRedirect(response.encodeRedirectURL( request.getContextPath() + "/tasks")); } catch (SQLException sqle) { throw new ServletException("Database error", sqle); } } // see below }
SaveTaskServlet 覆盖了 HttpServlet 的 doPost 方法,以便我们可以处理 HTTP POST 消息。它从请求中收集数据,通过请求对象的 getParameter 方法提供,然后创建一个 Task 对象并调用一个名为“save”的帮助程序方法(稍后在类中定义)。保存新任务后,用户将被重定向到“tasks”servlet 以查看更新的任务列表。您是否注意到执行重定向的代码行调用了 response.encodeRedirectURL 并将上下文路径添加到目标 URI?这正是通过使用 <c:url> 标签在 JSP 文件中避免的繁琐之处。
SaveTaskServlet 还定义了一个与数据库交互的“save”方法。虽然这些代码都不是面向 servlet 的,但了解 Java 的一些标准 API 的强大功能是有益的。在本例中,正是 JDBC API 使我们能够访问关系数据库(清单 6)。
清单 6. SaveTaskServlet.java
public Task save(Task task) throws SQLException { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = getConnection(); if(null == task.getId()) { // A new task ps = conn.prepareStatement( "INSERT INTO task (owner, date, client_id, description, duration) VALUES (?,?,?,?,?)", PreparedStatement.RETURN_GENERATED_KEYS); ps.setString(1, task.getOwner()); ps.setTimestamp(2, new Timestamp( task.getDate().getTime())); ps.setInt(3, task.getClientId()); ps.setString(4, task.getDescription()); ps.setInt(5, task.getDuration()); ps.executeUpdate(); rs = ps.getGeneratedKeys(); if(!rs.next()) throw new SQLException( "Expected auto-generated key, got none"); int taskId = rs.getInt(1); if(rs.wasNull()) throw new SQLException( "Got bogus auto-generated key"); task = new Task(taskId, task.getOwner(), task.getDate(), task.getClientId(), task.getDescription(), task.getDuration()); } else { ps = conn.prepareStatement( "UPDATE task SET date=?, client_id=?, description=?, duration=? WHERE id=? AND owner=?"); ps.setTimestamp(1, new Timestamp( task.getDate().getTime())); ps.setInt(2, task.getClientId()); ps.setString(3, task.getDescription()); ps.setInt(4, task.getDuration()); ps.setInt(5, task.getId()); ps.setString(6, task.getOwner()); ps.executeUpdate(); } return task; } finally { close(conn, ps, rs); } }
首先,此方法从数据库连接池获取连接,然后确定 Task 是从头创建还是更新(尽管我们的 UI 尚未提供“更新”方法,但此类已设计为允许更新)。在每种情况下,都会准备一个参数化的 SQL 语句,然后用从调用代码传入的数据填充。然后,执行该语句以写入数据库,并将一个新对象传递回调用方。对于新任务,在执行后从语句中获取数据库生成的主键,以便将其传递回调用方。
在正常情况下,诸如“save”之类的方法将被拆分到单独的类中,以便于组织、测试和架构分离,但为了简单起见,我将它们保留在 servlet 类中。
示例的完整源代码和预构建的 WAR 文件可从 Linux Journal FTP 服务器(请参阅资源)获得,我鼓励您下载并试用。我还包括了 Java 和 Apache Tomcat servlet 容器的快速安装说明,这是运行示例应用程序所必需的。
通常,基于 Perl 和 PHP 的 Web 应用程序由执行一项任务的自包含脚本组成:例如,加载和显示任务。仅使用 JSP 完全可以实现这种事情。有一些标签库可以执行 SQL 查询,您甚至可以直接在 JSP 中编写 Java 代码,尽管我在这里没有介绍它,因为对于 JSTL 提供的丰富工具来说,它不是必需的。另一方面,有一些哲学和实践原因不将所有内容都塞进单个 JSP。大多数 (Java) 程序员都订阅“模型-视图-控制器”架构,其中代码被分成逻辑单元,这些单元对您的问题域进行建模(在我们的示例中,这将是 Task 和 Client 对象),提供数据的视图(这就是我们的 JSP)并控制程序流程(servlet)。这种架构分离实际上带来了很多实际好处,包括
更易于代码维护:分离促进了代码重用并简化了自动化测试。
错误处理:如果控制器是唯一可能发生故障的组件(由于错误的输入、数据库连接故障等),您不必担心视图组件在呈现期间发生故障,从而破坏您的输出。
大多数 Java 项目都将以这种方式拆分,因此我编写了我的示例来说明这种架构,并且我希望您也在您的 Java 项目中考虑使用这种架构。
将 Java 添加到您用于构建 Web 应用程序的技能库中,使您可以访问 Servlet 规范保证的内置服务以及大量高质量的第三方库。Servlet 容器通过简单的配置和/或 API 提供许多对您的 Web 应用程序有用的服务。Java Server Pages 可用于快速构建复杂的网页,同时避免业务逻辑。您编写的用于实现业务逻辑的 Servlet 可以完全访问许多 API,以满足您能想到的任何需求。Java Web 应用程序的强大功能以及 Linux 的稳定性和可扩展性可以结合到一个平台上,许多高质量的在线服务都建立在该平台上,包括我的服务。我希望我已经让您体验到使用 Java Servlet 规范提供的工具创建健壮且有用的 Java Web 应用程序是多么容易,并且您考虑在您的下一个 Web 应用程序中使用 Java。
资源
本文的示例 Web 应用程序:ftp.linuxjournal.com/pub/lj/listings/issue197/10810.tgz
Java Servlet 规范(版本 2.5):jcp.org/aboutJava/communityprocess/mrel/jsr154/index2.html
JavaServer Pages 标准标签库:https://jstl.dev.java.net
Apache Tomcat 网站:tomcat.apache.org
Christopher Schultz 是 Total Child Health, Inc. 的首席技术官,这是一家总部位于马里兰州巴尔的摩的医疗保健软件公司。自从这些词可以合理地放在同一个句子中以来,他一直在用 Java 开发 Web 应用程序。他是 Apache Tomcat 用户邮件列表的活跃成员,并且是 Apache Velocity 项目的提交者。他与妻子 Katrina、儿子 Maxwell 和狗 Paddy 一起住在弗吉尼亚州阿灵顿。