2010年8月2日 星期一

JSP & Servlet Expression Language 使用介紹(1)

基於以下考量,對開發者而言Scriptlet並不是很好的開發型態:
1.不應該預期網頁設計及美工人員懂Java
2.頁面維護考量
   混雜的HTML語法與JSP程式碼會讓頁面雜亂無章,難以做出修改及維護

JSP提供了Java Bean規格的標準動作及EL(Expression Language)來處理這些問題
它們能讓程式撰寫在別處,減少頁面的Java程式碼

當然,以效能角度而言,EL會增加解析變數名稱的負擔
對於效能有較大要求的頁面則應該多使用Scriptlet

先提到標準動作的一些注意事項
對於命名,Java Bean規格有著比較嚴格的限制
我們將參數給予 bean,經由它內部的處理後,取得我們所要的結果

bean類別的建構式絕不能接受參數
對於其內部成員變數的處理只由getter與setter負責
getter是從外部取出成員變數的值的方法
方法名稱是get後面接成員變數的名稱
每個詞的第一個字為大寫,如 getMemberID()
setter會從外部設定成員變數的值
它的寫法與getter一致,例如 setMemberID()
getter的回傳型別必須與setter的參數型別一致
若要有回傳boolean值的函式寫法會是在名稱前面加上 is,例如 isRight()
注意,整個Bean就是屬性,而不是它的成員變數
之後的範例可以看到實際的bean寫法

以之前提過的作法來說
使用Scriptlet操作的範例如下:
<% Test t = (Test) request.getAttribute("test"); %>
t value is :<%= t.getVal() %>

以標準動作改寫後:
<jsp:useBean id="test" class="Test" scope="request" />
t value is :<jsp:getProperty name="test" property="val" />

用來取得結果的是<jsp:getProperty>的這個標籤
而在那之前,必須先用<jsp:useBean>標籤來宣告及初始化所要使用的bean

id 給予了這個bean在Servlet中的辨識名稱
class是它的物件類別
scope為bean指定屬性作用域
<jsp:getProperty>裡的name指定實際使用的bean物件,對應<jsp:useBean>裡的id
property指定要讀取的特性名稱,也就是bean類別裡具有getter/setter  method的成員變數

當<jsp:useBean>被建立時,它會根據標籤裡的 id 及 scope 查詢是否有既存的 bean
如果找不到的話,它就根據class指定的類別建立一個 bean 物件
並將物件指派給 id 所指定的變數,再將這個變數當作屬性放到scope指定的作用域裡
將<jsp:useBean>與<jsp:setProperty>並用的效果,可確保一定有物件被建立,且會被指派值
但是如果想做到的效果是,僅在bean是新建立時才要指定值,該如何寫呢?
這時候的作法是如下範例:
<jsp:useBean id="test" class="Test" scope="request" />
    <jsp:setProperty name="test" property="val" value="1"  />
</jsp:useBean >

這樣一來,特定值只會在新的bean備建立時才執行
這跟Servlet轉譯之後的程式碼有關,我們不需要去了解

bean物件也可以有多型的應用
以下的程式碼是抽象類別 "Test" 與其子類別 "subTest" 的演出
<jsp:useBean id="test" type="Test" class="subTest" scope="request" />
抽象類別Test現在是沒有辦法單獨建立的
因為 class="Test" 這句程式碼轉成Servlet的實作方式會是 new Test();
實作上還是必須遵守Java的規則 
範例轉換成Servlet後的程式碼就會是  Test  test = new subTest();
這樣就可以作出多型了

假如使用了 type 卻不使用 class
Servlet會去檢查該 id 的物件是否存在在指定的作用域裡
存在的情況下可以正常運作;但不存在時,則無法運作

標準動作的標籤的 scope 預設值是 page
標籤裡的 param 屬性可以將請求參數的值指定給 bean,只要有適當的請求參數名稱即可
例如表單以POST方式送來名為 s 的參數( tag的name屬性是 s1 ):
<jsp:useBean id="test" type="Test" class="subTest" scope="request" />
<jsp:setProperty name="test" property="val" param="s1" />

這樣就能夠將參數 "s" 的值指定給bean的特性了
如果表單中欄位名稱(name屬性)等同於bean物件的 property 的值
連 param 屬性也不用設定,container會從名稱相符的請求參數中取值
如下範例:
Test.java==========

package tw.vencs;

public abstract class Test {
    private String s1;
    private String s2;
   
    public void setS1(String s1){
        this.s1 = s1;
    }
    public void setS2(String s2){
        this.s2 = s2;
    }
    public String getS1(){
        return s1;
    }
    public String getS2(){
        return s2;
    }
    public boolean isOrigin(){
       return true;
    }
}

subTest.java=======

package tw.vencs;

public class subTest extends Test {
    private int val1;
   
    public void setVal1(int val1){
        this.val1 = val1;
    }
    public int getVal1(){
        return val1;
    }
    public boolean isOrigin(){
       return false;
    }
}

test.html=======

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Test</title>
</head>
<body>
<form action="bean.jsp" method="post">
  <p>test column 1</p><input type="text" name="s1" />
  <p>test column 2</p><input type="text" name="s2" />
  <br />
  <input type="submit" value="Send" />
</form>
</body>
</html>

bean.jsp======

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Bean Test</title>
</head>
<body>
<jsp:useBean id="test" type="tw.vencs.Test" class="tw.vencs.subTest" scope="page" >
    <jsp:setProperty name="test" property="s1" />
    <jsp:setProperty name="test" property="s2" />
</jsp:useBean>

<p>The word is <jsp:getProperty name="test" property="s1" /> </p>

<%-- 下面紅字的部分使用了EL,EL在文章最後會介紹 --%>
<%-- test是上面宣告的bean物件名稱,origin 會回傳 isOrigin() 的結果,在這裡回傳false --%>
<p>The class is origin: ${test.origin} </p>
</body>
</html>


上面的範例示範僅有使用到 "s1" 請求參數
將所有的請求參數名稱完全與bean的 property 的值相等的話,還能做到更方便的操作
<jsp:useBean id="test" type="tw.vencs.Test" class="tw.vencs.subTest" scope="page" >
    <jsp:setProperty name="test" property="*" />
</jsp:useBean>

上面的程式碼中的 property 使用了萬用自元 *
它能夠指示container檢視bean裡的 getter 與 setter
而 getter 與 setter 裡不必如範例這般簡單,可以增加或修改要回傳的處理之類的程式碼
並依此判斷出bean所有的特性,並將他們對應到請求參數的名稱
一口氣設定好所有的特性

JavaBean 的特性可以是任何型別
但如果是String或是基本型別(primitive type),container能做到資料的強制轉型
其餘的類別會由container試著呼叫他們的 toString() method
當然如果輸入的請求參數無法被轉換時,會回傳錯誤
所以要先確認請求參數是否可以被順利轉型
另外,使用Scriptlet的話會沒法讓型別自動轉換運作
即使將Scriptlet與標準動作標籤混用也沒辦法

標準動作標籤的使用也不是萬能的
它並不支援巢狀特性的處理
如果Bean裡還有非字串與非基本型別的類別時
我們沒辦法這樣寫: <jsp:setProperty name="test" property="OtherType.id" />
所以接下來要說明的是EL(Expression Language)

EL表達式的型態是 ${applicationScope.mail} 這樣
效果等同於 <%= application.getAttribute("mail") %>
將表達式包在大括號中,並在前面加個 $
表達式里的第一個具名變數都是隱含物件
先聲明很重要的一點,表達式與Java並不是等號
EL提供不使用Java來存取Java物件的機制,所以兩者個語法規則有相異之處

EL的隱含物件共有:
pageScope、requestScope、sessionScope、applicationScope
param、paramValues、header、headerValues、cookie、initParam、pageContext
最剛開始提到的四個隱含物件能指定作用域
除了最後的pageContext以外的隱含物件都是Map型別,也就是鍵值對的群集
pageContext 是指向 pageContext物件的實際參考
從這邊也可以看出EL的隱含物件與JSP隱含物件並不相同

EL裡使用 . 表達隱含物件或是屬性的特性
[] 也可以做到同樣的效果,例如 ${ test["id"] } 等同 ${ test.id }
那,這兩種運算子的差別在哪邊呢?
差別在於 [] 還能表達陣列或 List,這是點號做不到的
如在Servlet中存了一筆陣列資料:
String[] SA = { "one", "two", "three"}
request.setAttribute("test", SA);

JSP中用EL的得到的值:
${test}         -> 呼叫陣列的toString() method
${test[0]}     -> one
${test["1"]}  -> two

可以看到範例的最後一個並不符合一般程式的寫法
對陣列或List來說,字串型別的索引值會被強制轉型成 int
當然這個字串也要能被轉型才不會收到錯誤訊息
這就是EL與Java不相同的其中一個地方

如果 [] 裡沒有雙引號,container會根據 [] 裡的內容搜尋該名稱的屬性進行轉換
能做為搜尋Map類別的使用,找不到時則會回傳 null
範例如下:

request.setAttribute("Convertion", "test");
${ demo[Convertion] }   <%-- 會被轉換成 ${ demo["test"] } --%>

另外,雖然 ${ test["id"] } 等同 ${ test.id } 的寫法可以使用
但是不能出現這樣的寫法 ->  ${ test["1"] } O    ${ test.1 } X
命名必須遵守Java規則,不能出現以數字開頭命名的變數或屬性

接著回頭說明EL的隱含物件
param、paramValues是很相似的隱含物件
${param.xxx} 等同於 <%=request.getParameter("xxx") %>
paramValues使用方式範例:  ${ paramValues.demo[0] }
header隱含物件會將所有的header放置在Map裡,使用法類似paramValues
它可以讓我們得到更多的資訊,如: ${ header["host"] }
想在EL裡取得Request 請求物件的方式與requestScope沒有關係
requestScope只是一個包含了Request作用域的屬性的Map物件
透過 pageContext 才能夠取得請求物件:${ pageContext.request.id }
其餘的作用域也是如此

作用域隱含物件其實並不一定需要使用
即使只知道物件的名稱,而不知道所存在的作用域
也能夠使用 ${ Object.Attribute }  這樣的寫法來呼叫
作用域提供的功能除了避免發生物件名稱衝突,呼叫正確的物件外
另一個作用是接受讓屬性名稱可以不以遵守Java命名規則來命名
例如 ${ com.Object.Attribute } 需改成 ${ requestScope["com.Object"].Attribute } 才能執行

EL的 cookie 隱含物件提供了方便的使用方法
使用Scriptlet時所用的Cookie隱含物件的讀取方式,必須繞行整個Cookie陣列才行
EL的則很輕鬆 ${ cookie.userName.value }

initParam則可以讓我們使用Context初始參數,當然必須先在DD中設定好
使用 ${ initParam.Attribute } 這樣的呼叫方式
這樣就講完了EL的隱含物件

沒有留言:

張貼留言