深入 <jsp:useBean>、<jsp:setProperty> 與 <jsp:getProperty>


JSP 網頁最終將轉換為 Servlet,所謂的 JavaBean,實際上也是 Servlet 中的一個物件實例,當你使用 <jsp: useBean> 時,實際上就是在宣告一個 JavaBean 的物件,id 屬性即是用以指定參考名稱,而 class 屬性則是型態名稱。例如若你在 JSP 的頁面中撰寫以下的內容:

<jsp:useBean id="user" class="cc.openhome.User" />

實際在轉譯為 Servlet 之後,會產生以下的程式碼片段:

cc.openhome.User user = null; // id="user" 就是產生這邊的user參考名稱
synchronized (request) {
    user = (cc.openhome.User) _jspx_page_context.getAttribute(
                  "user", PageContext.PAGE_SCOPE);
    if (user == null){
        user = new cc.openhome.User();
        _jspx_page_context.setAttribute(
                    "user", user, PageContext.PAGE_SCOPE);
    }
}

其中 _jspx_page_context 參考至 PageContext 物件,也就是說,使用 <jsp:useBean> 標籤時,會在屬性範圍(預設是 page 範圍)中尋找有無 id 名稱所指定的屬性,如果找到就直接使用,如果沒有找到就建立新的物件。

你可以在使用 <jsp:useBean> 標籤時,使用 scope 屬性來指定其儲存的屬性範圍,可以指定的值有 page(預設)、requestsessionapplication。例如:

<jsp:useBean id="user" class="cc.openhome.User" scope="session"/>

則轉譯後的 Servlet 中將會有以對的程式碼片段,也就是改從會話範圍中尋找指定的屬性:

cc.openhome.User user = null;
synchronized (request) {
    user = (cc.openhome.User) _jspx_page_context.getAttribute(
                  "user", PageContext.SESSION_SCOPE);
    if (user == null){
        user = new cc.openhome.User();
        _jspx_page_context.setAttribute(
                    "user", user, PageContext.SESSION_SCOPE);
    }
}

注意!如果你使用 <jsp:useBean> 標籤時沒有指定 scope,則預設「只」在 page 範圍中尋找 JavaBean,找不到就建立新的 JavaBean 物件(不會再到 requestsessionapplication 中尋找)。

在轉譯後的 Servlet 程式碼中,如果想指定宣告 JavaBean 時的型態,則可以使用 type 屬性。例如:

<jsp:useBean id="user" 
             type="cc.openhome.BaseUser" class="cc.openhome.User"
             scope="session"/>

如此產生的 Servlet 程式碼中,將會有以下的片段:

cc.openhome.BaseUser user = null;
synchronized (request) {
    user = (cc.openhome.BaseUser) _jspx_page_context.getAttribute(
                  "user", PageContext.SESSION_SCOPE);
    if (user == null){
        user = new cc.openhome.User();
        _jspx_page_context.setAttribute(
                    "user", user, PageContext.SESSION_SCOPE);
    }
}

type 屬性的設定可以是一個抽象類別,也可以是一個介面。如果你只設定 type 而沒有設定 class 屬性,則必須確定在某個屬性範圍中已經存在所要的物件,否則會發生 InstantiationException 例外。

標籤的目的是減少 JSP 中 Script 的使用,所以反過來說,如果你發現 JSP 中有 Scriptlet,撰寫的是從某個屬性範圍中取得物件,則可以思考一下,是否可以用 <jsp:useBean> 來消除 Scriptlet 的使用。

在使用 <jsp:useBean> 標籤取得或建立 JavaBean 實例之後,若要設值給 JavaBean,則可以使用 <jsp:setProperty> 標籤,你可以使用幾個方式來進行設定。例如:

<jsp:setProperty name="user" property="password" value="123456" />

這會在產生的 Servlet 程式碼中,使用 PageContextfindAttribute(),從 pagerequestsessionapplication 依序找看看有無 name 所指定的屬性名稱,找到的話,再透過反射(Reflection)機制找出 JavaBean 上的 setPassword() 方法,呼叫並將 value 的指定值設定給JavaBean。

如果想要將請求參數的值設定給 JavaBean 的某個屬性,則以下是個範例:

<jsp:setProperty name="user" param="password" property="password" />

如果請求參數中包括 password,則會透過 JavaBean 的 setPassword() 方法設定給 JavaBean 實例。你也可以不指定請求參數名稱,而由 JSP 的自省(Introspection)機制來判斷是否有相同的請求參數名稱,如果有的話就自動找出對應的設值方法並呼叫以設值給 JavaBean。例如以下會找看看有無 password 請求參數,有的話就設定給 JavaBean:

<jsp:setProperty name="user" property="password" />

<jsp:setProperty> 有個最有彈性的寫法,就是將請求參數名稱與 JavaBean 的屬性名稱交給自省機制來自動匹配。例如:

<jsp:setProperty name="user" property="*" />

如果你的 JavaBean 屬性是整數、浮點數之類的基本型態,自省機制可以自動轉換請求參數字串為對應屬性的基本資料型態。

你也可以在使用 <jsp:useBean> 時一併設定屬性值,例如:

<jsp:useBean id="user" class="cc.openhome.User" scope="session">
    <jsp:setProperty name="user" property="*" />
</jsp:useBean>

如此一來,如果屬性範圍中找不到 user 時,則會新建一個物件並設定其屬性值;如果可以找到物件的話就直接使用。也就是轉譯後產生以下的程式碼:

cc.openhome.User user = null;
synchronized (request) {
    user = (cc.openhome.User) _jspx_page_context.getAttribute(
                  "user", PageContext.SESSION_SCOPE);
    if (user == null){
        user = new cc.openhome.User();
        _jspx_page_context.setAttribute(
                    "user", user, PageContext.SESSION_SCOPE);
        org.apache.jasper.runtime.JspRuntimeLibrary.introspect(
            _jspx_page_context.findAttribute("user"), request);
    }
}

這與撰寫以下的內容是有點不同的:

<jsp:useBean id="user" class="cc.openhome.User" scope="session"/>
<jsp:setProperty name="user" property="*" />

如果使用以上的寫法,則無論是找到或新建 JavaBean 物件,都一定會使用內省機制來設值,也就是轉譯的 Servlet 程式碼中會有以下片段:

cc.openhome.User user = null;
synchronized (request) {
    user = (cc.openhome.User) _jspx_page_context.getAttribute(
                  "user", PageContext.SESSION_SCOPE);
    if (user == null){
        user = new cc.openhome.User();
        _jspx_page_context.setAttribute(
                    "user", user, PageContext.SESSION_SCOPE);
    }
}
org.apache.jasper.runtime.JspRuntimeLibrary.introspect(
        _jspx_page_context.findAttribute("user"), request);

標籤的目的是減少 JSP 中 Script 的使用,所以反過來說,如果你發現 JSP 中有 Scriptlet,有透過設值方法(Setter)對 JavaBean 作設值的動作,則可考慮使用 <jsp:setProperty> 來消除 Scriptlet 的使用。

<jsp:getProperty> 的使用比較單純,在使用 <jsp:useBean> 標籤取得或建立 JavaBean 實例之後,基本上就只有一種用法:

<jsp:getProperty name="user" property="name"/>

這會使用透過 PageContextfindAttribute() 找出 user 屬性,並透過 getName() 方法取得值以顯示在網頁上,也就是轉譯後的 Servlet 原始碼中會有以下片段:

out.write(org.apache.jasper.runtime.JspRuntimeLibrary.toString(((
    (cc.openhome.User)_jspx_page_context.findAttribute("user"))
                                        .getName()
)));

在使用 <jsp:useBean> 標籤取得或建立 JavaBean 實例之後,由於 <jsp:setProperty><jsp:getProperty> 轉譯後,都是使用 PageContextfindAttribute() 來尋找屬性,因此尋找的順序是頁面、請求、會話、應用程式範圍。

標籤的目的是減少 JSP 中 Script 的使用,所以反過來說,如果你發現 JSP 中有 Scriptlet,有透過取值方法(Getter)對 JavaBean 作取值的動作,則可考慮使用 <jsp:getProperty> 來消除 Scriptlet 的使用。