要理解 J2EE 应用程序体系结构的基本概念,第一步是确定如何将现有的 ASP.NET 应用程序移植到一个基于 J2EE 的模型,或者确定如何从头编写一个 J2EE 应用程序。我们将考察几个 ASP.NET 模型,以及可能如何将它们转换为根据 J2EE 组件构建的模型。您将看到简单的“意大利面条式的代码”如何演进为一个更优雅、可复用和可扩展的环境。在研究一些代码例子之前,让我们首先进一步考察一下 Web 应用程序中使用得最多的两个 J2EE 组件:Java Servlets 和 JavaServer Pages 技术。
Java Servlets 编程:基础
编写 Java Servlets 是为了以编程方式控制来自浏览器的 URL 请求。典型的 servlet 调用类似如下:
客户端向 Web 服务器发出请求,同时指定一个 servlet 作为 URL 的一部分——例如:
<FORM action="/myWebApp/LoginServlet" method="POST"> 。
Web 服务器将该请求转发给应用服务器,后者查找 servlet 类实例的位置。
然后 Web 容器调用该 servlet。(该 servlet 的单个实例将加载到内存中,每个请求在不同的线程中调用该单个实例。)
注意 HTML 表单中 servlet 的 URL包括该 servlet 的名称和一个称为 上下文根(context root) 的前缀(在上面的例子中是 /myWebApp )。上下文根大致等价于一个 IIS 虚拟目录。
图 4 描述了这些步骤。
图 4. Servlet 调用
Servlet 扩展了 HttpServlet 类。您可以根据需要重载 HttpServlet 的以下方法:
init() :在应用服务器加载某个 servlet 时调用
destroy() :在应用服务器卸载某个 servlet 时调用
doGet() :在 servlet 通过 HTTP GET 被调用时调用
doPost() :在 servlet 通过 HTTP POST 被调用时调用
Servlet 的编写涉及编写代码来处理 HTTP 请求、处理任何参数,以及直接返回 HTML 内容或委托其他资源(比如 JSP 页面)来处理响应。不推荐从 servlet 直接返回 HTML 内容,因为在 Java 类中管理 HTML 代码很麻烦。
当应用服务器调用 doGet() 或 doPost() 方法时,将有两个对象作为参数被传递:
HttpServletRequest 允许访问任何请求参数和导致产生该 servlet 调用的 HTTP 请求中的其他信息。
HttpServletResponse 充当返回客户端的通信渠道,允许 servlet 直接返回内容或返回其他 HTTP 返回代码(错误、重定向,等等)。
清单 20 和 21 显示了两个 Hello World servlet。清单 20 直接返回内容,而清单 21 使用 JSP 页面来返回内容。
清单 20. HelloWorld servlet: 直接返回内容
2
3 /**
4
5 * Handles all HTTP POST requests
6
7 *
8
9 * @param request Object that encapsulates the request to the servlet
10
11 * @param response Object that encapsulates the response from the servlet
12
13 */
14
15 public void doPost(
16
17 javax.servlet.http.HttpServletRequest request,
18
19 javax.servlet.http.HttpServletResponse response)
20
21 throws ServletException, IOException {
22
23 try {
24
25 PrintWriter out = response.getWriter();
26
27 out.println("");
28
29 out.println("Hello World");
30
31 out.println("");
32
33 } catch (Throwable t) {
34
35 ...
36
37 }
38
39 }
40
41 }
42
43
注意您从响应对象中得到了一个 PrintWriter ,并将 HTML 一次一行地输出到客户端。
清单 21. HelloWorld servlet: 使用 JSP 页面来返回内容
2
3 /**
4
5 * Handles all HTTP POST requests
6
7 *
8
9 * @param request Object that encapsulates the request to the servlet
10
11 * @param response Object that encapsulates the response from the servlet
12
13 */
14
15 public void doPost(
16
17 javax.servlet.http.HttpServletRequest request,
18
19 javax.servlet.http.HttpServletResponse response)
20
21 throws ServletException, IOException {
22
23 try {
24
25 RequestDispatcher rd = getServletContext().getRequestDispatcher("helloworld.jsp");
26
27 rd.forward(request, response);
28
29 } catch (Throwable t) {
30
31 ...
32
33 }
34
35 }
36
37 }
38
39
RequestDispatcher 是您想要向其转发请求的资源的包装器。注意要同时包括原始请求和响应对象,以便目标资源能够访问它们。从 getServletContext() 返回的 ServletContext 允许 servlet 与底层应用服务器通信,以获得一个 RequestDispatcher 。 注意所有 servlet 都能够通过 getServletContext() 方法访问它们的 ServletContext 。
使用 JavaServer Pages 技术来编程:基础
JSP 技术使您能够使用 Java 语言进行服务器端编程。JSP 页面是包含 HTML 和 Java 代码的复合页面,其中的 Java 代码由应用服务器在将页面返回客户端之前处理。应用服务器处理该嵌入代码,以便在将页面返回客户端之前生成静态内容。像 .aspx 文件一样,JSP 文件通常看起来像具有一些附加标签和 Java 代码片断的 HTML。
J2EE 应用服务器在 JSP 页面第一次被请求时,将每个JSP 页面转换为一个特殊的 servlet。该 servlet 将编译并加载到内存中。只要该 JSP 源代码没有修改,它就一直为针对该页面的请求提供服务。当源代码修改时,该过程又重来一次,这样就产生了一个新版本的 servlet。
可以在 JSP 页面中使用几个特殊的 JSP 标签,用户还可以定义他们自己开发的标签的行为。这些自定义的标签大致等价于 ASP.NET 中的自定义组件。还可以向 JSP 页面的不同部分添加一些 Java 代码块。J2EE 运行时环境使得许多变量(称为 隐含变量)对您的这些 Java 代码片断可用。隐含变量的例子包括:
request:与页面的特定调用相关联的 HttpServletRequest
response:与页面的特定调用相关联的 HttpServletResponse
out:与 HttpServletResponse 相关联的 PrintWriter
清单 22 显示了 JSP 页面的一个例子,其中包含 HTML 和 Java 代码的混合。 标签之间的 Java 代码部分称为 scriptlet。
清单 22. HelloWorld servlet:使用 JSP 页面来返回内容
2 <title>JSP page example</title>
3 The date is:
4 <%
5 Date date = new Date();
6 out.println(date);
7 %>
8 Some more HTML
9 The value of <em>e</em> is:
10 <%
11 double e = Math.exp(1.0);
12 out.println(e);
13 %>
14 Yet even more HTML
15 The value of PI is:
16 <%
17 double pi = 22.0/7.0;
18 out.println(pi);
19 %>
20 </html>
21
注意其中使用了 JSP 隐含变量 out 来将内容写回客户端,还要注意 HTML 和 Java 代码的交织。
一个例子场景
为了说明各种体系结构选项,我们将使用一个简单的用户登录场景,它包含:
一个具有 HTML 表单的登录页面,它从用户那里收集用户名和密码
验证用户凭据(可能使用数据库)并重定向站点主页的应用逻辑
这个简单的例子允许我们描述各种各样的应用体系结构。
“意大利面条式”代码
在 ASP.NET 方,“意大利面条式”代码方法使用单个 .aspx 文件来同时包含应用逻辑和 HTML 表单。不存在代码分离(code-behind)。(不推荐对现实中的例子采用这种方法,因为这样所有表示逻辑和应用逻辑都将是单个文件,从而阻止了您复用用于验证用户凭据的代码。)该代码的轮廓看起来类似清单 23 中所示的代码。
清单 23. ASP.NET 中的“意大利面条式”代码
2 <head>
3 <title>Login example</title>
4 <script language="C#" runat=server>
5 ...
6 private void btnLogin_Click(object sender, System.EventArgs e)
7 {
8 // Get the form field values
9 ...
10 // Validate the username and password
11 if (ValidateUser(username, password) {
12 Response.Redirect("mainpage.aspx");
13 }
14 }
15 private bool ValidateUser(string userName, string password) {
16 ...
17 }
18 ...
19 </script>
20 </head>
21 <body>
22 <form runat="server">
23 <!-- login form fields go here -->
24 </form>
25 </body>
26 </html>
27
正如清单 24 所示,您可以在 J2EE 中采用相同的方法,使用单个 JSP 页面来同时包含表单和应用逻辑。
清单 24. J2EE 中的“意大利面条式”代码
2 <head>
3 <title>Login example</title>
4 <%!
5 private boolean validateUser(String userName, String password) {
6 ...
7 }
8 %>
9 ...
10 </head>
11 <body>
12 <%
13 if (request.getMethod().equals("GET") ) { %>
14 <form method="POST" target="login.jsp">
15 <!-- login form fields go here -->
16 </form> <% }
17 else {
18 String userName = request.getParameter("username");
19 String password = request.getParameter("password");
20 if (validateUser(userName, password)) {
21 response.sendRedirect("mainpage.jsp");
22 }
23 ...
24 } %>
25 </body>
26 </html>
27
JSP 模型不是事件驱动的,因此您需要检查表单是否被发送回去了,方法是检查该请求,并在它不是 POST 请求时包括表单的 HTML。如果它是 POST 请求,您将使用 JSP 中声明的一个方法来验证登录。注意 )对方法无效。还要注意如何使用 if/then/else 编程结构来容易地包括或排斥较大的 HTML 块。与在 ASP.NET 例子中一样,不推荐将此方法用于 J2EE 开发。表示代码(HTML)和应用逻辑的混合仅允许很少的复用,并且使得代码难于维护。
改进的“意大利面条式”代码
在 ASP.NET 方,一种更好的方法建立在前一个例子基础上,不过除了 .aspx 文件外,它还使用了代码分离文件。事件处理代码和用户验证代码转移到了一个代码分离文件中,原先的 .aspx 文件只剩下 HTML 表单和其他 HTML 元素。这相对于前一种方法来说当然是一种进步;表示代码更清楚地分离了,这样可以让一个 HTML 设计师负责表示,让一个程序员负责代码分离文件。
如果使用标准 J2EE 组件,您就无法使用 ASP.NET 的事件驱动的代码分离文件方法。J2EE 端的一种更好方法是将应用逻辑转移到一个 Java Servlet,从而使 JSP 页面仅限于使用 HTML 组件。就像 ASP.NET 中的代码分离文件方法相对于“意大利面条式”代码是一种改进一样,这同样也是一种改进。清单 25 显示了如何通过把应用逻辑放到 servlet 中来简化 JSP 页面。
清单 25. J2EE 中改进的“意大利面条式”代码 JSP page
2 <head>
3 <title>Login example</title>
4 ...
5 </head>
6 <body>
7 <form method="POST" target="LoginServlet">
8 <!-- login form fields go here -->
9 </form>
10 </body>
11 </html>
12
清单 26 显示了 servlet 中的验证代码和导航逻辑。
清单 26. J2EE 中改进的“意大利面条式”代码 Java Servlet
2
3 /**
4
5 * Handles all HTTP POST requests
6
7 *
8
9 * @param request Object that encapsulates the request to the servlet
10
11 * @param response Object that encapsulates the response from the servlet
12
13 */
14
15 public void doPost(
16
17 javax.servlet.http.HttpServletRequest request,
18
19 javax.servlet.http.HttpServletResponse response)
20
21 throws ServletException, IOException {
22
23 try {
24
25 String userName = request.getParameter("username");
26
27 String password = request.getParameter("password");
28
29 if (validateUser(userName, password)) {
30
31 response.sendRedirect("mainpage.jsp");
32
33 }
34
35 ...
36
37 } catch (Throwable t) {
38
39 ...
40
41 }
42
43 }
44
45 private boolean validateUser(String userName, String password) {
46
47 ...
48
49 }
50
51 }
52
53
清单 26 中的 servlet 是表单提交的目标,并且充当一个控制器――处理用户输入并基于该输入调出适当的页面。注意 HttpServlet 父类允许您通过提供可重载的方法( doGet() 和 doPost() ),同时处理 GET 和 POST 请求。
这种方法的主要缺点在于,凭据验证代码(它很可能要访问数据库)对 ASP.NET 例子来说是代码分离文件的一部分,对 J2EE 例子来说是 servlet 的一部分。如果不同的页面需要使用这个逻辑,您就必须重复它。重复的代码更难于维护和更易于出错,因为您必须跟踪多个副本。
模型-视图-控制器(MVC)方法
下面我们将展示如何对这个例子使用更纯的 MVC 方法。在 ASP.NET 端,这体现为将凭据验证逻辑转移到一个单独的类库中,然后您可以在代码分离文件中访问该类库。在 J2EE 端,凭据验证代码将转移到一个单独的类中,然后从 servlet 访问该类。清单 27 中的代码片断显示了该 servlet 看起来的样子。
清单 27. J2EE 中的 MVC Java Servlet
2 2
3 3 public class LoginServlet extends HttpServlet {
4 4
5 5 /**
6 6
7 7 * Handles all HTTP POST requests
8 8
9 9 *
10 10
11 11 * @param request Object that encapsulates the request to the servlet
12 12
13 13 * @param response Object that encapsulates the response from the servlet
14 14
15 15 */
16 16
17 17 public void doPost(
18 18
19 19 javax.servlet.http.HttpServletRequest request,
20 20
21 21 javax.servlet.http.HttpServletResponse response)
22 22
23 23 throws ServletException, IOException {
24 24
25 25 try {
26 26
27 27 String userName = request.getParameter("username");
28 28
29 29 String password = request.getParameter("password");
30 30
31 31 UserValidation user = new UserValidation();
32 32
33 33 if (user.validate(userName, password)) {
34 34
35 35 response.sendRedirect("mainpage.jsp");
36 36
37 37 }
38 38
39 39 ...
40 40
41 41 } catch (Throwable t) {
42 42
43 43 ...
44 44
45 45 }
46 46
47 47 }
48 48
49 49 }
50 50
51 51
清单 28 显示了在一个单独的类中的凭据验证代码。
清单 28. J2EE 中的 MVC User-validation class
2
3 public class UserValidation {
4
5 public boolean validate(String username, String password) {
6
7 ...
8
9 }
10
11 }
12
13
凭据验证 servlet 所使用的类中进行。现在该 servlet 不必知道关于验证如何进行的信息。只要您保持相同的公开接口不变,就可以随意更改 UserValidation 类,不再需要改变 servlet 代码。
其他 J2EE 组件
前面的例子展示了如何将业务逻辑分离到一个或一组单独的类中。这样允许您以各种不同的方式实现业务逻辑,而不会影响 servlet 和 JSP 页面。与使用一个需要访问数据库(使用 JDBC)的业务对象不同,您可以通过各种不同的方式实现这个逻辑。例如:
使用 JCA 来访问诸如大型机等遗留系统:JCA 是一个 J2EE API,它允许 J2EE 应用程序使用标准接口来与遗留系统(比如 CICS 或者 IMS)交互。
使用 Web 服务客户端 API 来访问 Web 服务:这种方法涉及使用 Apache Axis 或其他某种 Java Web 服务客户端 API 来访问 Web 服务。
使用 JMS 将消息作为验证请求来发送:这种方法涉及使用标准 API 来访问 JMS 相容的消息中间件(比如 WebSphere MQ)。
使用 EJB 技术来实现验证逻辑:这种方法涉及使用 EJB 组件(而不是简单的 Java 类)来实现业务逻辑。EJB 组件能够使用应用服务器中的服务来管理(数据库或其他)事务,管理方法级的安全,以及提供群集和高速缓存。将此类任务委托给应用服务器,您可以把精力集中在业务逻辑上,而不必担心系统级的问题。