JSP、Servlet 與 JavaBean 的組合應用
作者:蔡煥麟
日期:Jan-22-2003
更新:Feb-4-2003
1.0 簡介
上次的討論中,最後有一個範例是判斷質數的 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。
- 了解如何在 JSP 中使用 JavaBeans(存取 JavaBeans 的屬性和方法)。
- 了解 servlet 如何分派 JSP 網頁。
2.0 範例:JSP 呼叫 JavaBeans
2.1 基礎知識
這裡所說的 JavaBeans 只是一般的 Java 類別,跟 EJB(Enterprise JavaBeans)是兩種不同的東西,請勿混淆了。那麼,servlet 也是 Java 類別,它跟 JavaBeans 又有什麼不同呢?
JavaBeans 只是普通的類別
Servlet 的 Java 類別是繼承自 javax.servlet.HttpServlet,因此具有接收 HTTP request 和送出 HTTP response 等網站應用程式的基本功能,而 JavaBeans 則只是單純的類別,它可以繼承自任何類別,但無法處理 HTTP 訊息,它在網站應用程式中的角色通常是作為參數物件(在 JSP 和 servlet 之間傳遞,以共享資訊)或工具類別,作為參數物件時,通常代表種資料,因此被稱為 value bean,作為工具類別時,則稱為 utility bean。
怎樣的類別可以稱為 JavaBeans?
只要你遵守 JavaBeans 規範中所建議的命名和設計慣例,而且你以 bean 的方式使用它,那麼它就可以稱為一個 bean。[1]
類別通常以 "動詞+Bean" 的方式命名,例如:UserInfoBean, CheckStockBean....等。這是一種慣例,雖然沒有強制非這樣命名不可,但是它有好處:清楚,別的程式設計師一眼就可以看出這是個 bean。
JSP 如何使用 JavaBeans?
要讓 JSP 能夠使用你的 bean,你的 bean 必須提供一組屬性,JSP 便可以透過特殊的標籤來存取這個 bean 的屬性。所謂的屬性,其實是一組 getter 和 setter methods,兩者統稱為 access methods(存取方法),透過這組存取方法來間接地存取類別的私有成員,當然,這組存取方法必須宣告為 public。例如,有個 bean 類別 EmployeeBean,它要提供一個年齡的屬性給外界(JSP)存取,此類別的定義如下:
public class EmployeeBean { private int age; public int () { return age; } public void (int age) { self.age = age; } }
在 JSP 裡面使用時,是這麼個寫法:
< id="emp" class="com.huanlin.EmployeeBean" scope="request"/> < name="emp" property="" value="25" /> 員工的年齡是: < name="emp" property="" />
其中
- <jsp:useBean> 標籤就是指明要使用一個 bean 物件,id 代表該物件的名稱,class 指明了要使用什麼類別的 bean,scope 則代表物件的生命週期。
- <jsp:setProperty> 標籤用來設定 bean 的屬性值,property 指明了要設定哪個屬性,value 就是數值,要別注意的是,HTML 表單傳入的資料一定是字串,但我們的 age 屬性卻是整數,這個部分的轉換會由 Web container 幫我們處理掉。
- <jsp:getProperty> 標籤是用來取得 bean 的屬性值。
請特別注意兩點:
- 屬性的大小寫。在 JSP 裡面,屬性的名稱是完全小寫的 "age",但是 getter 和 setter methods 的名稱卻是 getAge() 和 setAge(),這種名稱的轉換對應規則是固定的,照這個規則來命名,Web container 就能夠找到正確的存取方法。
- 對於 OOP 觀念不熟的人來說,可能會誤以為在 JSP 裡面存取的 "age" 屬性,就是類別定義裡面的那個宣告為 private 成員(age),其實兩者只有字面上相同而已,實際運作是可以毫無關聯的,因為 JSP 完全是透過 getter 和 setter 方法來存取屬性,況且外界本來就無法存取類別的私有成員。
基本知識介紹到此,接下來是實作,如果有未詳盡之處,請自行參閱相關書籍。
2.2 撰寫 JavaBean
我們把上次的教學文件的最後一個範例,也就是判斷質數的 JSP 程式拿來修改,其中的 isPrimeNumber 函式很明顯可以獨立出來(以便重複使用),放到一個類別裡面,我把這個類別命名為 CheckPrimeBean。程式碼如表 1 所示。
表 1. PrimeValidator.java
package ; 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; } } |
幾點說明:
- 這裡使用了具名的套件(named package),套件名稱是 com.huanlin.util,表示你將來佈署的 .class 檔案也要有相同的路徑結構,也就是編譯出來的檔案及路徑名稱會是 "com\huanlin\util\CheckPrimeBean.class"。在編譯這個檔案時,編譯器會根據你的 package 名稱幫你自動建立好對應的目錄。(
- 在安排檔案目錄的結構時,我把原始碼和編譯過的類別檔分開目錄存放,檔案目錄結構像是這個樣子:
sources\CheckPrimeBean.java classes\com\huanlin\util\CheckPrimeBean.class
也就是這個範例的目錄下會有兩個目錄:sources 和 classes,分別存放原始碼和編譯過的檔案。因為這個緣故,在編譯時必須特別指定輸出的檔案目錄,這部分請參考表 1 的第 2 行註解。
- 這個類別的使用方式,是先設定 number 這個屬性,然後呼叫 isValidNumber() 檢查輸入的數字是否為有效的整數,最後才由 isPrimeNumber() 判斷是否為質數。
關於 package 你也許會發現,即使不寫 package 那行,程式也可以通過編譯,但由於這個 bean 是要用在 JSP 裡面的,如果你不為 package 命名的話,在 JSP 裡面使用這個 bean 時,Web container 會找不到這個 bean。請到相關書籍中找尋 package 的相關說明。 |
2.3 在 JSP 中使用 JavaBeans
原本在 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"); %> <html> <body> < id="checker" class="com.huanlin.util.CheckPrimeBean" scope="request"/> < name="checker" property="number" value=" num " /> <% if (!checker.()) { %> <% response.setHeader("Refresh", "5; URL=prime2.htm"); %> 請輸入 2~10000 之間的整數。<p> 五秒後將自動回到 prime2.htm。 <% return; } %> <%-- 顯示錯誤訊息後結束,亦即後續的指令不會被處理 --%> <% if (checker.()) { %> <%= num %> 是質數 <% } else { %> <%= num %> 不是質數 <% } %> </body> </html> |
關於在 JSP 使用 bean 的方法,之前都有提過了,只有一點值得特別說明,就是 <jsp:setProperty> 這行的 value 屬性(attribute),請注意它使用了 <%= .. %> 標籤來將一個變數的值傳入 value 屬性。其實它還可以這樣寫:
<jsp:setProperty name="checker" property="number" ="number" />
也就是不明白指定 value,而改用 param 這個屬性,讓 Web container 在處理 JSP 指令時自動幫我們帶入 "number" 這個 HTML 表單傳入的參數。由於我們的 HTML 表單的參數名稱和 bean 的屬性名稱都叫做 "number",JSP 也允許我們將 param 省略不寫,像這樣:
<jsp:setProperty name="checker" property="number" />
這樣就更簡潔了。如果你覺得這樣寫語意不明,或者考慮到某些程式設計師不知道有這種寫法,那就還是把 param 寫上去好了。
2.4 佈署與執行
- 把 sources 目錄下的 prime2.htm 和 CalcPrime.jsp 複製到 Tomcat 的 webapps\myapp\ 目錄下。
- 把 classes 目錄整個複製到 Tomcat 的 webapps\myapp\WEB-INF\ 目錄下。
-
在瀏覽器的網址列輸入 "http://127.0.0.1:8080/myapp/prime2.htm"。
3.0 範例:Servlet 呼叫 JSP
Servlet 要呼叫(說得精確一點應該是:分派 JSP 頁面)JSP,跟在 JSP 中使用 JavaBeans 比起來要簡單多了,主要只是網頁轉送的技巧而已,此技巧在設計 圖 2 的架構時會用得著。
3.1 撰寫 servlet
Servlet 程式碼列於表 3。
表 3. HelloWorldServlet.jsp
import java.io.*; import javax.servlet.*; import javax.servlet.http.*; public class HelloWorldServlet extends HttpServlet { public void (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.("message", theMessage); rd; rd = (targetURL); rd.(request, response); } } |
程式碼有幾個地方值得特別注意:
- 之前的 servlet 範例程式都是用 doGet 來處理用戶端的 HTTP request,這裡則改用 service。
- request.setAttribute()。
- RequestDisatcher 類別。
- getServletContext()。
- ServletContext 類別的 getRequestDispatcher() 方法。
- RequestDispatcher 類別的 forward() 方法。
請試著從你手邊的書籍或網路資源中尋找相關的說明,以了解程式運作的原理。
3.2 撰寫 JSP
表 3. HelloFromServlet.jsp
<%@ page language="java" contentType="text/html;charset=big5" %> <% String msg = (String)request.("message"); %> <html> <body> 從 servlet 傳來的訊息: <%= msg %> </body> </html> |
之前的 servlet 程式中有用到 request.setAttribute(),這裡則使用了 request.getAttribute(),從這裡可以看得出來,servlet 和 JSP 之間是透過 request 物件來儲存及傳遞給對方的參數。
程式的運作過程如下:
- 用戶端送出 HTTP request,請求的網址為 "http://127.0.0.1:8080/myapp/HelloWorldServlet"。
- HelloWorldServlet 收到請求之後,透過 request.setAttribute() 把要傳遞給 JSP 的參數字串 "Hello, World!" 儲存在 request 物件裡。
- HelloWorldServlet 透過 ServletContext 建立 RequestDispatcher 物件,並指定欲分派的網址。
- HelloWorldServlet 呼叫 RequestDispatcher 的 forward() 方法,把這次的 HTTP request 轉送至另一個網頁,也就是 HelloFromServlet.jsp。
- JSP 先透過 request.getAttribute() 取得 request 物件中的屬性值,該屬性值是在步驟 2 中,由 servlet 指定的。最後再該屬性值搭配 HTML 標籤顯示出來。
我把整個過程畫成一個 UML 循序圖(圖 3),你可以搭配上面的文字描述來了解程式的運作過程。
圖 3. Servlet 分派 JSP 網頁的過程(sequence diagram)
3.3 佈署與執行
- 在 Tomcat 的 webapps 目錄下建立目錄結構:myapp\WEB-INF\classes\。請注意大小寫是有區別的。
- 將 HelloFromServlet.jsp 複製到 myapp 目錄下。
- 編譯 HelloWorldServlet.java,並將編譯後產生的 HelloWorldServlet.class 複製到 myapp\WEB-INF\classes\ 目錄下。
- 編輯 myapp\WEB-INF\web.xml 檔案,內容如下:
表 4. web.xml
<?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>
- 開啟瀏覽器,在網址列輸入 URL "http://127.0.0.1:8080/myapp/HelloWorldServlet"。
4.0 範例:Servlet、JavaBeans、與 JSP 的組合
此範例跟 3.0 的範例其實很像,都是由 Servlet 傳遞參數給 JSP,再由 JSP 取出參數並顯示出來,只是 3.0 的範例的參數是字串,而這裡要示範的是以 JavaBeans 物件當作參數來傳遞共享資訊。程式的目錄結構如圖 4 所示:
圖 4. 目錄結構
編譯後的 Java class 檔案都輸出至 classes 目錄下,而由於 HelloServlet2.java 需要參考 UserInfoBean.java,所以在編譯時要必須使用 -class 參數,否則會找不到類別,為了方便起見,我們用一個批次檔 Make.bat 幫我們編譯所有的 Java 類別。
4.1 撰寫 JavaBean
我們打算用一個 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; } } |
4.2 撰寫 Servlet
表 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 裡面。
4.3 撰寫 JSP
<%@ page contentType="text/html;charset=big5" %> <jsp:useBean id="userInfo" class="com.huanlin.UserInfoBean" /> <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 裡面。
4.4 佈署與執行
- 執行 Make.bat 產生所有的 .class 檔。
- 將 Make.bat 產生的 .class 檔案,也就是整個 classes 目錄複製到 myapp\WEN-INF\classes\ 目錄下。
- 將 HelloFromServlet2.jsp 複製到 myapp 目錄下。
- 編輯 myapp\WEB-INF\web.xml 檔案,加入下列內容如下:
表 4. web.xml
<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>
- 開啟瀏覽器,在網址列輸入 URL "http://127.0.0.1:8080/myapp/HelloServlet2"。
5.0 學習評量
- 解釋何謂 page-centric 和 servlet-centric 架構,並比較兩者的優缺點。
- 什麼是 JavaBeans?
- JSP 提供哪些標籤可以讓我們存取 JavaBeans 的屬性?
- JavaBeans 如何提供屬性讓 JSP 存取?
- 為什麼要使用具名的 package?有什麼好處?
- 表 3 的 HelloWorldServlet 類別改寫(override)了 service() 方法,這和之前改寫 doGet() 方法有什麼差別?
- 說明 HttpServletRequest.setAttribute() 的用途。
- 說明 RequestDispatcher.forward() 的用途。
- <jsp:useBean> 標籤的 scope 作用是什麼?其值除了可設定為 "session" 之外,還有哪些值可以設定?各有何不同?
參考文獻
[1] | Web Developement with JavaServer Pages. Duane K. Fields, Mark A Kolb, Shawn Bayern. Manning, 2002. |
[2] | UML 精華第二版,Martin Fowler 著,趙光正、薛琇文 譯,基峰,2000。 |
如欲下載範例程式,請按這裡。