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 架構

這次的學習重點:

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 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" />

其中

請特別注意兩點:

  1. 屬性的大小寫。在 JSP 裡面,屬性的名稱是完全小寫的 "age",但是 getter 和 setter methods 的名稱卻是 getAge() 和 setAge(),這種名稱的轉換對應規則是固定的,照這個規則來命名,Web container 就能夠找到正確的存取方法。
  2. 對於 OOP 觀念不熟的人來說,可能會誤以為在 JSP 裡面存取的 "age" 屬性,就是類別定義裡面的那個宣告為 private 成員(age),其實兩者只有字面上相同而已,實際運作是可以毫無關聯的,因為 JSP 完全是透過 getter 和 setter 方法來存取屬性,況且外界本來就無法存取類別的私有成員。

基本知識介紹到此,接下來是實作,如果有未詳盡之處,請自行參閱相關書籍。

2.2 撰寫 JavaBean

我們把上次的教學文件的最後一個範例,也就是判斷質數的 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;
    }
}

幾點說明:

  1. 這裡使用了具名的套件(named package),套件名稱是 com.huanlin.util,表示你將來佈署的 .class 檔案也要有相同的路徑結構,也就是編譯出來的檔案及路徑名稱會是 "com\huanlin\util\CheckPrimeBean.class"。在編譯這個檔案時,編譯器會根據你的 package 名稱幫你自動建立好對應的目錄。(
  2. 在安排檔案目錄的結構時,我把原始碼和編譯過的類別檔分開目錄存放,檔案目錄結構像是這個樣子:

    sources\CheckPrimeBean.java
    classes\com\huanlin\util\CheckPrimeBean.class
    

    也就是這個範例的目錄下會有兩個目錄:sources 和 classes,分別存放原始碼和編譯過的檔案。因為這個緣故,在編譯時必須特別指定輸出的檔案目錄,這部分請參考表 1 的第 2 行註解。  

  3. 這個類別的使用方式,是先設定 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");  // 取得 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 寫上去好了。

2.4 佈署與執行

  1. 把 sources 目錄下的 prime2.htm 和 CalcPrime.jsp 複製到 Tomcat 的 webapps\myapp\ 目錄下。
  2. 把 classes 目錄整個複製到 Tomcat 的 webapps\myapp\WEB-INF\ 目錄下。
  3. 在瀏覽器的網址列輸入 "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 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);
    }
}

程式碼有幾個地方值得特別注意:

  1. 之前的 servlet 範例程式都是用 doGet 來處理用戶端的 HTTP request,這裡則改用 service。
  2. request.setAttribute()。
  3. RequestDisatcher 類別。
  4. getServletContext()。
  5. ServletContext 類別的 getRequestDispatcher() 方法。
  6. RequestDispatcher 類別的 forward() 方法。

請試著從你手邊的書籍或網路資源中尋找相關的說明,以了解程式運作的原理。

3.2 撰寫 JSP

表 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 物件來儲存及傳遞給對方的參數。

程式的運作過程如下:

  1. 用戶端送出 HTTP request,請求的網址為 "http://127.0.0.1:8080/myapp/HelloWorldServlet"。
  2. HelloWorldServlet 收到請求之後,透過 request.setAttribute() 把要傳遞給 JSP 的參數字串 "Hello, World!" 儲存在 request 物件裡。
  3. HelloWorldServlet 透過 ServletContext 建立 RequestDispatcher 物件,並指定欲分派的網址。
  4. HelloWorldServlet 呼叫  RequestDispatcher 的 forward() 方法,把這次的 HTTP request 轉送至另一個網頁,也就是 HelloFromServlet.jsp。
  5. JSP 先透過 request.getAttribute() 取得 request 物件中的屬性值,該屬性值是在步驟 2 中,由 servlet 指定的。最後再該屬性值搭配 HTML 標籤顯示出來。

我把整個過程畫成一個 UML 循序圖(圖 3),你可以搭配上面的文字描述來了解程式的運作過程。

圖 3. Servlet 分派 JSP 網頁的過程(sequence diagram)

3.3 佈署與執行

  1. 在 Tomcat 的 webapps 目錄下建立目錄結構:myapp\WEB-INF\classes\。請注意大小寫是有區別的。
  2. 將 HelloFromServlet.jsp 複製到 myapp 目錄下。
  3. 編譯 HelloWorldServlet.java,並將編譯後產生的 HelloWorldServlet.class 複製到 myapp\WEB-INF\classes\ 目錄下。
  4. 編輯 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>
  5. 開啟瀏覽器,在網址列輸入 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" 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 裡面。

4.4 佈署與執行

  1. 執行 Make.bat 產生所有的 .class 檔。
  2. 將 Make.bat 產生的 .class 檔案,也就是整個 classes 目錄複製到 myapp\WEN-INF\classes\ 目錄下。
  3. 將 HelloFromServlet2.jsp 複製到 myapp 目錄下。
  4. 編輯 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>
  5. 開啟瀏覽器,在網址列輸入 URL "http://127.0.0.1:8080/myapp/HelloServlet2"。

5.0 學習評量

  1. 解釋何謂 page-centric 和 servlet-centric 架構,並比較兩者的優缺點。
  2. 什麼是 JavaBeans?
  3. JSP 提供哪些標籤可以讓我們存取 JavaBeans 的屬性?
  4. JavaBeans 如何提供屬性讓 JSP 存取?
  5. 為什麼要使用具名的 package?有什麼好處?
  6. 表 3 的 HelloWorldServlet 類別改寫(override)了 service() 方法,這和之前改寫 doGet() 方法有什麼差別?
  7. 說明 HttpServletRequest.setAttribute() 的用途。
  8. 說明 RequestDispatcher.forward() 的用途。
  9. <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。

如欲下載範例程式,請按這裡