作者:蔡煥麟
日期:Jan-22-2003
更新:Feb-4-2003
上次的討論中,最後有一個範例是判斷質數的 JSP 程式,該程式在 JSP 中嵌入許多 Java code,我們也說過這是不好的設計方式,這次就來看看怎麼樣把這些 Java code 從 JSP 中抽離出來,成為獨立的類別(稱為 JavaBeans),並且示範如何在 JSP 裡面呼叫這些 JavaBeans 。另外,也會一併介紹由 Servlet 呼叫 JSP 的方式,之前看的範例程式,其流程、邏輯、和資料展現都放在 JSP,這種設計方式稱為 page-centric 架構,或 Model-1 架構(圖 1),現在開始撰寫的範例會將控制權交給 servlet,以 servlet 為控制中心,掌控程式的流程以及 HTML/JSP 網頁的分派,這是一種 servlet-centric 的架構,也稱為 Model-2 架構(圖 2),其實也就是 MVC(Model-View-Controller)架構的基礎。
圖 1. page-centric 架構 |
圖 2. servlet-centric 架構 |
這次的學習重點:
這裡所說的 JavaBeans 只是一般的 Java 類別,跟 EJB(Enterprise JavaBeans)是兩種不同的東西,請勿混淆了。那麼,servlet 也是 Java 類別,它跟 JavaBeans 又有什麼不同呢?
Servlet 的 Java 類別是繼承自 javax.servlet.HttpServlet,因此具有接收 HTTP request 和送出 HTTP response 等網站應用程式的基本功能,而 JavaBeans 則只是單純的類別,它可以繼承自任何類別,但無法處理 HTTP 訊息,它在網站應用程式中的角色通常是作為參數物件(在 JSP 和 servlet 之間傳遞,以共享資訊)或工具類別,作為參數物件時,通常代表種資料,因此被稱為 value bean,作為工具類別時,則稱為 utility bean。
只要你遵守 JavaBeans 規範中所建議的命名和設計慣例,而且你以 bean 的方式使用它,那麼它就可以稱為一個 bean。[1]
類別通常以 "動詞+Bean" 的方式命名,例如:UserInfoBean, CheckStockBean....等。這是一種慣例,雖然沒有強制非這樣命名不可,但是它有好處:清楚,別的程式設計師一眼就可以看出這是個 bean。
要讓 JSP 能夠使用你的 bean,你的 bean 必須提供一組屬性,JSP 便可以透過特殊的標籤來存取這個 bean 的屬性。所謂的屬性,其實是一組 getter 和 setter methods,兩者統稱為 access methods(存取方法),透過這組存取方法來間接地存取類別的私有成員,當然,這組存取方法必須宣告為 public。例如,有個 bean 類別 EmployeeBean,它要提供一個年齡的屬性給外界(JSP)存取,此類別的定義如下:
public class EmployeeBean { private int age; public int getAge() { return age; } public void setAge(int age) { self.age = age; } }
在 JSP 裡面使用時,是這麼個寫法:
<jsp:useBean id="emp" class="com.huanlin.EmployeeBean" scope="request"/> <jsp:setProperty name="emp" property="age" value="25" /> 員工的年齡是: <jsp:getProperty name="emp" property="age" />
其中
請特別注意兩點:
基本知識介紹到此,接下來是實作,如果有未詳盡之處,請自行參閱相關書籍。
我們把上次的教學文件的最後一個範例,也就是判斷質數的 JSP 程式拿來修改,其中的 isPrimeNumber 函式很明顯可以獨立出來(以便重複使用),放到一個類別裡面,我把這個類別命名為 CheckPrimeBean。程式碼如表 1 所示。
表 1. PrimeValidator.java
// 檔名:CheckPrimeBean.java // 編譯:javac -d ..\classes CheckPrimeBean.java //=============================================== package com.huanlin.util; public class CheckPrimeBean { private int number; public String getNumber() { return Integer.toString(number); // 整數轉成字串 } public void setNumber(String s) { try { number = Integer.parseInt(s); // 字串轉成整數 } catch (NumberFormatException e) { number = -1; } } public boolean isValidNumber() { // 檢查輸入的數字是否合法 if ((number < 2) || (number > 10000)) return false; return true; } public boolean isPrimeNumber() { // 判斷是否為質數 for (int i = 2; i <= number/2; i++) { if (number % 2 == 0) return false; } return true; } } |
幾點說明:
sources\CheckPrimeBean.java classes\com\huanlin\util\CheckPrimeBean.class |
也就是這個範例的目錄下會有兩個目錄:sources 和 classes,分別存放原始碼和編譯過的檔案。因為這個緣故,在編譯時必須特別指定輸出的檔案目錄,這部分請參考表 1 的第 2 行註解。
關於 package 你也許會發現,即使不寫 package 那行,程式也可以通過編譯,但由於這個 bean 是要用在 JSP 裡面的,如果你不為 package 命名的話,在 JSP 裡面使用這個 bean 時,Web container 會找不到這個 bean。請到相關書籍中找尋 package 的相關說明。 |
原本在 JSP 裡面的一些 Java 程式碼被抽離成獨立的 CheckPrimeBean 類別之後,程式碼就清爽些了,修改後的 JSP 檔名取做 CalcPrime2.jsp,參考表 2。
表 2. CalcPrime2.jsp
<%-- 檢查某個數字是否為質數的 JSP 程式 --%> <%@ page language="java" contentType="text/html;charset=big5" %> <% request.setCharacterEncoding("big5"); String num = request.getParameter("number"); // 取得 HTTP request 的參數 %> <html> <body> <jsp:useBean id="checker" class="com.huanlin.util.CheckPrimeBean" scope="request"/> <jsp:setProperty name="checker" property="number" value="<%= num %>" /> <% if (!checker.isValidNumber()) { %> <% response.setHeader("Refresh", "5; URL=prime2.htm"); %> 請輸入 2~10000 之間的整數。<p> 五秒後將自動回到 prime2.htm。 <% return; } %> <%-- 顯示錯誤訊息後結束,亦即後續的指令不會被處理 --%> <% if (checker.isPrimeNumber()) { %> <%= num %> 是質數 <% } else { %> <%= num %> 不是質數 <% } %> </body> </html> |
關於在 JSP 使用 bean 的方法,之前都有提過了,只有一點值得特別說明,就是 <jsp:setProperty> 這行的 value 屬性(attribute),請注意它使用了 <%= .. %> 標籤來將一個變數的值傳入 value 屬性。其實它還可以這樣寫:
<jsp:setProperty name="checker" property="number" param="number" />
也就是不明白指定 value,而改用 param 這個屬性,讓 Web container 在處理 JSP 指令時自動幫我們帶入 "number" 這個 HTML 表單傳入的參數。由於我們的 HTML 表單的參數名稱和 bean 的屬性名稱都叫做 "number",JSP 也允許我們將 param 省略不寫,像這樣:
<jsp:setProperty name="checker" property="number" />
這樣就更簡潔了。如果你覺得這樣寫語意不明,或者考慮到某些程式設計師不知道有這種寫法,那就還是把 param 寫上去好了。
在瀏覽器的網址列輸入 "http://127.0.0.1:8080/myapp/prime2.htm"。
Servlet 要呼叫(說得精確一點應該是:分派 JSP 頁面)JSP,跟在 JSP 中使用 JavaBeans 比起來要簡單多了,主要只是網頁轉送的技巧而已,此技巧在設計 圖 2 的架構時會用得著。
Servlet 程式碼列於表 3。
表 3. HelloWorldServlet.jsp
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class HelloWorldServlet extends HttpServlet { public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html; charset=big5"); request.setCharacterEncoding("big5"); String theMessage = "Hello, World!"; String targetURL = "/HelloFromServlet.jsp"; request.setAttribute("message", theMessage); RequestDispatcher rd; rd = getServletContext().getRequestDispatcher(targetURL); rd.forward(request, response); } } |
程式碼有幾個地方值得特別注意:
請試著從你手邊的書籍或網路資源中尋找相關的說明,以了解程式運作的原理。
表 3. HelloFromServlet.jsp
<%@ page language="java" contentType="text/html;charset=big5" %> <% String msg = (String)request.getAttribute("message"); %> <html> <body> 從 servlet 傳來的訊息: <%= msg %> </body> </html> |
之前的 servlet 程式中有用到 request.setAttribute(),這裡則使用了 request.getAttribute(),從這裡可以看得出來,servlet 和 JSP 之間是透過 request 物件來儲存及傳遞給對方的參數。
程式的運作過程如下:
我把整個過程畫成一個 UML 循序圖(圖 3),你可以搭配上面的文字描述來了解程式的運作過程。
圖 3. Servlet 分派 JSP 網頁的過程(sequence diagram)
<?xml version="1.0" encoding="ISO-8859-1"?> <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd"> <web-app> <servlet> <servlet-name> HelloWorldServlet </servlet-name> <servlet-class> HelloWorldServlet </servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloWorldServlet</servlet-name> <url-pattern>/HelloWorldServlet</url-pattern> </servlet-mapping> </web-app> |
程式的目錄結構如圖 4 所示:
圖 4. 目錄結構
編譯後的 Java class 檔案都輸出至 classes 目錄下,而由於 HelloServlet2.java 需要參考 UserInfoBean.java,所以在編譯時要必須使用 -class 參數,否則會找不到類別,為了方便起見,我們用一個批次檔 Make.bat 幫我們編譯所有的 Java 類別。
我們打算用一個 UserInfoBean 類別來儲存一個使用者的相關資訊,並且在 servlet 和 JSP 之間傳遞這個物件,以達到溝通和資訊共享的目的。為了示範方便,這個類別只提供了一個屬性:userName,程式碼列在表 5。
表 5. UserInfoBean.java
// 檔名:UserInfoBean.java // 編譯:javac -d ..\classes UserInfoBean.java package com.huanlin; public class UserInfoBean { private String userName; public void setUserName(String userName) { this.userName = userName; } public String getUserName() { return this.userName; } } |
表 6. HelloServlet2.jsp
// 檔案:HelloServlet2.java // 編譯:參考 Make.bat import java.io.*; import javax.servlet.*; import javax.servlet.http.*; import com.huanlin.UserInfoBean; public class HelloServlet2 extends HttpServlet { public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 下面兩行讓中文字能正確顯示 response.setContentType("text/html; charset=big5"); request.setCharacterEncoding("big5"); // 建立 userInfo 物件,並指定一個 session 的 attribute 與之繫結 UserInfoBean userInfo = new UserInfoBean(); userInfo.setUserName("令狐沖"); HttpSession session = request.getSession(); session.setAttribute("userInfo", userInfo); // 前往指定的網頁 RequestDispatcher rd; rd = getServletContext().getRequestDispatcher("/HelloFromServlet2.jsp"); rd.forward(request, response); } } |
session.setAttribute() 會將 UserInfoBean 物件的參考存入 session 裡面。
<%@ page contentType="text/html;charset=big5" %> <jsp:useBean id="userInfo" class="com.huanlin.UserInfoBean" scope="session"/> <html> <body> <p>從 servlet 傳入的 UserInfoBean.userName 是: <b> <jsp:getProperty name="userInfo" property="userName"/> </b> </body> </html> |
有個地方要特別注意,如果在 servlet 儲存參數時是呼叫 session.setAttribute() 方法,也就是將參數存入 session 中,那麼在 JSP 裡面的 <jsp:useBean> 標籤的 scope 就必須指明為 "session",否則會發生取不到參數的情形。
由於使用者登入之後,其帳號等相關資訊必須一直存在,直到這名使用者登出或將瀏覽器關閉之後才清除,因此我們把 UserInfoBean 物件存放在 session 中。一般來說,為了節省記憶體資源,非必要時不要將變數存在 session 中,如果 bean 傳送到 JSP 中用完即丟,可以將它存放在 request 裡面。
<web-app> <servlet> <servlet-name> HelloServlet2 </servlet-name> <servlet-class> HelloServlet2 </servlet-class> </servlet> <servlet-mapping> <servlet-name>HelloServlet2</servlet-name> <url-pattern>/HelloServlet2</url-pattern> </servlet-mapping> </web-app> |
[1] | Web Developement with JavaServer Pages. Duane K. Fields, Mark A Kolb, Shawn Bayern. Manning, 2002. |
[2] | UML 精華第二版,Martin Fowler 著,趙光正、薛琇文 譯,基峰,2000。 |