在這份教程中,我們將會介紹 Tapestry 其中的一個核心組件:DirectLink。它是觸發服務端行為最常用的方法之一。接下來,我們開始探討一些其它使用 Tapestry 開發 web 應用的共通點。

HTML Template

這個應用只是計數我們點擊鏈接的次數:

directlink1

在這個例子裡,僅 HTML 模板就不夠了;我們還需要提供 Java 類。這個 Java 類包含了存儲點擊次數的屬性以及用於增加點擊次數的業務邏輯。

我們仍然從 Home.html 模板開始:

<html>
<head>
<title>Tutorial: DirectLink</title>
</head>
<body>
<h1>DirectLink Tutorial</h1>

<p>
The current value is:
<span style="font-size:xx-large"><span jwcid="@Insert" value="ognl:counter">37</span></span>
</p>

<p>
<a href="#" jwcid="@DirectLink" listener="listener:doClick">increment counter</a>
</p>

<p>
<a href="#" jwcid="@PageLink" page="Home">refresh</a>
</p>

</body>
</html>

這大部分看起來都應該很熟悉。我們再次用到了 Insert 組件以及 OGNL。我們用一種更簡單的方法使用 OGNL 而不是創建一個新的實例;它會讀取點擊次數然後在它的屬性值參數內將其傳遞給 Insert 組件。

這很好,但是點擊次數這個屬性放在哪呢?在 Home.html 的 Java 類裡。我們馬上會看到如何來創建一個類。

僅顯示當前的屬性值是不夠的,我們還需要改變它的方法。這也是使用 DirectLink 的原因;它會調用我們的 Java 類裡的函數。這個組件與函數間的連接由 listener 的參數提供。“listener:”前綴讓 Tapestry 調用同名的函數(譯注:即 doClick)。我們在頁面的 Java 類中提供了一個 doClick() 函數,當用戶在他們的瀏覽器裡點擊鏈接時,DirectLink 組件就會為我們調用這個函數。

Page classes

我們已經有了 HTML 模板,但還需要包含這個屬性(譯注:指點擊次數)和(被 DirectLink 調用的)函數的 Java 類。

首先,我們需要一個 Java package 來存儲這個類。在本教程中,我們會使用 tutorials.directlink.pages。這意味著我們會在 src/java/tutorials/directlink/pages 目錄下創建 Home.java 這個源代碼文件:

package tutorials.directlink.pages;

import org.apache.tapestry.annotations.Persist;
import org.apache.tapestry.html.BasePage;

public abstract class Home extends BasePage {
    @Persist
    public abstract int getCounter();
    public abstract void setCounter(int counter);

    public void doClick() {
        int counter = getCounter();
        counter++;
        setCounter(counter);
    }
}

你編寫的這些 Java 類繼承自 Tapestry 的 BasePage 類。我們為這個基類加入了一個屬性(譯注:指點擊次數)和一個名為 doClick() 的監聽函數。

Abstract?怎麼回事?這是第一次看到 Tapestry 頁面類的非常典型的反應;為什麼它是抽象類而不是一個普通的 JavaBean?

答案有些離題。Tapestry 的頁面存放在服務器上,而創建它們的開銷是頗大的...至少你不會希望頻繁的創建、銷毀。在這方面,它們很像數據庫連接...你希望可以將它們置於緩存池內好在下次的 request 中再次使用。

因為頁面被置於緩存池中被不同的用戶所共享,所以在它們返回緩存池之前清除其對象中用戶或 request 指定的數據是非常重要的。你可以在自己的代碼裡這麼做(需另行實現接口及編寫代碼),但讓 Tapestry 為你代勞會更好。

通過定義一個抽象的存取器函數,你就隱式的指示了 Tapestry 去填充它的細節;它會在運行時創建一個你提供的 Java 類的子類並給出那些所有煩人的細節。稍後可以看到,這還不僅限於屬性;所有有用的功能都可以被捆綁在不同的抽象函數上(常與不同的 annotation 結合)。

本例這個計數器的屬性有些特別。它需要記住 requests 間的值。@Persist 這個 annotation 附加在 getCounter() 上,讓 Tapestry 將其設置成為一個持久化的頁面屬性。儘管有個這樣的名稱,但這仍和數據庫持久化沒有關係,它存儲的是 request 期間 HttpSession 的屬性值,然後在下次同樣的用戶、session 訪問頁面時進行恢復。

這是 Tapestry 中另一個重要的功能;獨立的頁面屬性(或是頁面中使用的組件屬性)可以自動在 HttpSession 中存儲它們的值。我們可以從任何的頁面實例中析出它的持久化狀態。這使得必須存儲在 HttpSession 中的信息量減少到了最小;而不再需要存儲整個頁面對象(以及所有的模板和嵌套的組件),我們只需存儲極少量下次請求時會需要用到的屬性。

這種與頁面實例緩存池結合使用的 session 管理方式,是 Tapestry 基本原則的另一個體現:效率。Tapestry 應用會優化是因為它們管理服務端狀態的方式。其開銷是這些抽象的類和函數,許多函數的實現 Tapestry 只會在運行時動態的提供。

回到我們的主題來。我們現在有了計數器屬性,知道了它是怎樣在 request 期間存儲在 HttpSession 中的。這使得 doClick() 監聽者函數的實現變得如此的簡單:獲取當前的屬性值,增值,然後將其存儲回屬性中。

再次重申,我們只演示涉及到的 Tapestry 部分:我們談到的對象、函數和屬性。Tapestry 為 DirectLink 生成的 URL,存儲在 HttpSession 中的屬性不作詳談。

Locating the page class

我們已經提供了 Home.html 和 Java 類,但仍沒有將它們完全的連接起來,程序還不能運行。如果我們嘗試運行這個應用(用瀏覽器打開 http://localhost:8080/directlink/app),我們會看到這個 Tapestry 的異常頁面:

directlink2

這裡面有非常多的信息。導致這個異常的根本原因在於 Tapestry 查找不到我們創建的 Home 類,因此它使用了 BasePage 替代。BasePage 並沒有計數器這個屬性,所以 OGNL 表達式 counter 無法被驗證(你可以在最底層的異常中看到這個情況)。你可以在 ognl.NoSuchPropertyException 的 target 屬性中看到,$BasePage_0@cec78d[Home] 是頁面類的 toString() 函數輸出的值;第一個部分是類名(記住,這是 Tapestry 在運行時生成的子類),方括號內是頁面的名稱。

這個異常在 Tapestry 的最高層處擲出,Tapestry 就卡在了 Home.html 這裡,所以生成這個異常報告。

正如你所見,異常報告非常的詳細;它顯示了整個棧的異常信息,包括它們的屬性。它識別到了出錯的文件以及錯誤所在的行,甚至顯示了文件中出錯的部分。頁面的下半部分是關於 Servlet API 對象的詳細信息...總之你需要的應用出錯的所有信息都已經給出了而無須重新啟用一個調試器。這是 Tapestry 另一個在實踐中的原則:反饋。當應用出錯的時候,Tapestry 應當幫助你修復而不是成為阻礙。

因此,我們問題的根本是 Tapestry 不能找到我們的 Home.html,所以我們要告訴它去哪找。這需要我們向 Tapestry 提供一些應用的配置信息。

我們將會為應用程序創建一份應用程序說明,將配置信息存儲在里面。應用程序說明是一份向 Tapestry 提供應用程序額外信息的 XML 文檔。它是可選的;在之前的範例中我們並沒有使用。

說明文檔的名稱由 Servlet 的名稱(在本例中是“app”)加上擴展名“.application& rdquo;組成。說明文檔自身存儲在 web 應用程序的 WEB-INF 目錄下。在我們的項目中,它是 src/context/WEB-INF/app.application:

<?xml version="1.0"?>
<!DOCTYPE application PUBLIC
    "-//Apache Software Foundation//Tapestry Specification 4.0//EN"
    "http://jakarta.apache.org/tapestry/dtd/Tapestry_4_0.dtd">

<application>
    <meta key="org.apache.tapestry.page-class-packages" value="tutorials.directlink.pages" />
</application>

應用程序說明文檔由指定的 DTD 進行校驗。<meta> 元素用於指定元數據及配置信息。在這裡我們用它來通知 Tapestry 在哪個 Java package 中查找頁面。如果需要查找的 package 超過一個的話,value 甚至還可以是一個由逗號分隔的 package 列表。

有了這個文檔,Tapestry 就有了運行我們應用的所需了:Home.html,它的 Java 類以及兩者的鏈接。現在,讓我們對範例稍加改進。

Understanding DirectLink URLs

由 DirectLink 生成的 URL 遠較 PageLink 生成的複雜。我們來看一看:

 http://localhost:8080/directlink/app?component=%24DirectLink&page=Home&service=direct&session=T

第一個查詢參數是 component,它確定頁面內的組件。%24 是 URL 中的 dollar 符號。在 Tapestry 中,每個組件都由它頁面中唯一的 id 來結束。如果你沒有提供 id ,Tapestry 會根據組件的類型創建一個,前綴是 dollar 號。在這裡,我們匿名的 DirectLink 組件分配到的 id 是 $DirectLink。如果你在一個頁面中有許多不同的 DireciLink 組件,你會看到它們的 id 是 $DireckLink_0、$DireckLink_1,以此類推。

你可以為組件設定一個更加簡短易記的名稱,在“@”前加上希望設定的名稱即可:

    <a href="#" jwcid="inc@DirectLink" listener="listener:doClick">increment counter</a>

更改模板後,DirectLink 組件的 URL 可讀性更好了。

 http://localhost:8080/directlink/app?component=inc&page=Home&service=direct&session=T

與之前範例不同的還有 service 和 session 這兩個查詢參數。service 參數表明它的 request 過程與由 PageLink 生成的鏈接的 request 過程是不同的。在這我們需要獲取一個頁面,並在該頁面中查找組件然後在響應之前調用 listener 函數。而 PageLink 只是獲取頁面然後輸出。

最後,session 查詢參數表明鏈接輸出的時候 HttpSession 是否存在。Tapestry 用它來檢測 HttpSession 何時過期...可能因為用戶在點擊鏈接之前離開了計算機一段時間。如果當這個鏈接生成的時候應用程序是無狀態的(即無 HttpSession),那麼 session 參數將不會出現在 URL 中。

有一點需要注意的是,函數名稱並不是 URL 中的一部分,組件的 id 才是。這很棒...為什麼非得暴露程序的結構呢?這很重要,它有助於防止惡意的用戶破壞你的程序;不能輕易的調用任意一個 listener 函數,除了作為開發人員(綁定在指定的組件上)的你。

這就是被 Tapestry 開發人員所稱的“ugly URLs”。它的討厭之處在於用查詢參數在 URL 中表達信息,而不是路徑。Ugly URLs 會引發一些問題;由於整個應用都在 /app 下,所以很難實施 J2EE 所宣稱的安全措施。同樣的,查詢參數的使用意味著大多數的搜索引擎將不會偵查到這個網站。解決的方法是使用“friendly URLs”,稍後的教程中會說到。

Adding more links

這個應用很好,但我們應該有個讓計數器歸零的方法。我們打算為頁面加入一個讓計數器歸零的方法。最終的結果如下:

directlink3

為了完成這個我們要在 Home.html 上加入另一個鏈接,然後將它連接到 Home class 新的函數上。首先是模板:

<p>
    <a href="#" jwcid="clear@DirectLink" listener="listener:doClear">clear counter</a>
</p>

這只是同一頁面上的另一個 DirectLink 組件,但有著不同的組件 id 以及不同的配置。這裡我們稱它為“clear”,然後將它連接到 doClear() listener 函數上。

這個函數也非常的簡單:

public void doClear(){
    setCounter(0);
}

這就是要做的全部。我們已經在頁面中加入了新的操作,清空計數器,四行的 Java (如果你用 Sun 建議的格式就是三行)和幾行 HTML 代碼。無須外部的設置。這遵循了 Tapestry 的另一個原則:一致性。加入更多的操作與加入首個操作沒有不同。要加多少隨你喜歡,Tapestry 都會搞定。

與之對比,使用傳統的 servlets,我們還需要:

  • 確定 URL
  • 在 HTML 文件中添加 URL
  • 為一個操作編寫一個全新的 servlet 類
  • 在 web.xml 中添加 servlet 以及 servlet mapping(URL)

Passing data in the links

僅調用一個操作有一定的限制;我們應該可以增加不只是 1:

directlink4

在這種情況下,我們要不只一個的 DirectLink 來調用同樣的 listener。然後我們要搞清楚計數器的增量是 1、5 還是 10。

這要發動兩處。首先,我們必須將舊的增加鏈接改為三個:

<p>
  <a href="#" jwcid="by1@DirectLink" listener="listener:doClick" parameters="ognl:1">
    increment counter by 1
  </a>
</p>

<p>
  <a href="#" jwcid="by5@DirectLink" listener="listener:doClick" parameters="ognl:5">
    increment counter by 5
  </a>
</p>

<p>
  <a href="#" jwcid="by10@DirectLink" listener="listener:doClick" parameters="ognl:10">
    increment counter by 10
  </a>
</p>

我們給出了三個容易記憶的 id(“by1”,“by5”,“ by10”)。另外,我們在 URL 中傳遞參數(使用 parameter 參數)。我們可以看到經 URL 編碼的鏈接:

 http://localhost:8080/directlink/app?component=by10&page=Home&service=direct&session=T&sp=10

sp 查詢參數持有增量值。“sp”是“service parameter”的簡寫,沿自 Tapestry 3.0。在 Tapestry 4.0 中,這些叫做“listener parameters”,這是因為它們僅對 listener 函數有意義。同樣的,我們只顯示了一個參數,但該機制支持多參數。

這就是信息在 URL 編碼的過程,但 listener 函數怎樣找到它呢?在 doClick() listener 函數中加入一個參數就可以了:

public void doClick(int increment){
    int counter = getCounter();
    counter += increment;
    setCounter(counter);
}

Tapestry 將 sp 的值映射到 listener 函數的參數中。同樣要注意,值的類型已被保留。它從一個數字開始,然後它然後會是一個數字。listener 的參數幾乎可以是任何的類型,然後會在 URL 編碼以及隨後的 request 解碼中保留它們的類型。你甚至可以將它傳遞給任意一個對象...只要它們實現了 java.io.Serializable 接口(但如果這麼做的話你將會看到超長的 URL)。

再次重申,我們談的是一致性。我們使用相同的機制在 URL 中傳遞信息:DirectLink、listener 函數...我們只是加入了一些額外的小功能以從點 A(輸出的頁面)獲取所需的信息傳遞給點 B(當鏈接被點擊時的 listener 函數)。