跳轉到內容

使用 Click 框架/頁面進行 Java Web 應用程式開發

來自華夏公益教科書

頁面是 Web 應用程式的核心。在 Click 中,頁面封裝了對 HTML 請求的處理和對 HTML 響應的渲染。本節討論 Click 頁面並涵蓋以下主題

在 Click 中,邏輯頁面由一個 Java 類和一個 Velocity 模板組成,這些元件在 click.xml 檔案的頁面元素中定義。

<page path="search.htm" classname="com.mycorp.page.Search"/>

path 屬性指定頁面 Velocity 模板的位置,classname 屬性指定頁面 Java 類名。

或者,您也可以將 Click 配置為使用 JSP 頁面進行渲染。

<page path="search.jsp" classname="com.mycorp.page.Search"/>

所有自定義 Click 頁面都必須是 [click-api/net/sf/click/Page.html 頁面] 基類的子類。頁面類及其關聯的輔助類 Context 和 Control 在下圖 1 中描述。

圖 1. 頁面類圖 - 由 Enterprise Architect 建立,由 Sparx Systems 提供

頁面類提供了一個模型屬性,該屬性用於儲存頁面 Velocity 模板中渲染的所有物件。模型還可以包含 Control 物件,這些物件在頁面上提供使用者介面控制元件。

頁面還具有關聯的 Context 物件,該物件引用與請求關聯的所有 javax.servlet 物件。在使用 Click 進行程式設計時,您可以使用 Context 物件訪問 HttpServletRequest 屬性、引數和 HttpSession 物件。

頁面類提供了一些空處理方法,子類可以覆蓋這些方法以提供功能

  • onSecurityCheck()
  • onInit()
  • onGet()
  • onPost()
  • onRender()
  • onDestroy()

ClickServlet 依賴於使用公共無引數建構函式例項化頁面,因此在建立頁面子類時,您必須確保不新增不相容的建構函式。

執行

[edit | edit source]

頁面 GET 請求的執行順序總結如下,如圖 2 所示。

圖 2. GET 請求順序圖 - 由 Enterprise Architect 建立,由 Sparx Systems 提供。

逐步執行此 GET 請求順序,將建立一個新的頁面例項,並設定頁面的屬性(上下文、格式、標頭、路徑)。接下來,將請求引數值繫結到任何匹配的公共頁面欄位。

然後onSecurityCheck()處理程式。此方法可用於確保使用者有權訪問頁面,並在必要時可以中止任何進一步的處理。

接下來呼叫的方法是onInit(),這是您放置任何建構函式後初始化程式碼的地方。

下一步是處理頁面的控制元件。ClickSerlvet 從頁面獲取控制元件列表,然後遍歷列表,呼叫onProcess()。如果任何控制元件的onProcess()方法返回 false,則中止後續控制元件的處理和頁面的onGet()方法。

如果一切正常執行,則頁面的onGet()方法現在被呼叫。

下一步是渲染頁面模板以生成顯示的 HTML。ClickServlet 從頁面獲取模型(地圖),然後將以下物件新增到模型中

  • 使用欄位的名稱進行任何公共頁面欄位
  • context - Servlet 上下文路徑,例如 /mycorp
  • cssImports - 頁面標題中要包含的 CSS 匯入和樣式塊。
  • format - 用於格式化物件顯示的 Format 物件。
  • imports - 頁面標題中要包含的 CSS 和 JavaScript 匯入。
  • jsImports - 頁面頁尾中要包含的 JavaScript 匯入和指令碼塊。
  • messages - 頁面 getMessage() 方法的訊息對映介面卡
  • path - 要渲染的頁面模板的路徑
  • request - 頁面的 HttpServletRequest 物件
  • response - 頁面的 HttpServletResponse 物件
  • session - 使用者 HttpSession 的 SessionMap 介面卡

然後,它將模板與頁面模型合併,並將結果寫入 HttpServletResponse。當模型與模板合併時,模型中的任何控制元件都可以使用其toString()方法進行渲染。

此順序中的最後一步是呼叫頁面的onDestroy()方法。此方法可用於在頁面被垃圾回收之前清理與頁面關聯的資源。即使在前面的步驟中發生異常,也保證會呼叫onDestroy()方法。

POST 請求的執行順序幾乎相同,只是onPost()方法在onGet()上被呼叫。請參閱 [../images/post-sequence-diagram.png POST 請求順序圖]。

下面的活動圖說明了頁面的另一種執行流程檢視。

圖 3. 頁面執行活動圖 - 由 Enterprise Architect 建立,由 Sparx Systems 提供。

請求引數自動繫結

[edit | edit source]

Click 會自動將任何請求引數值繫結到具有相同名稱的公共頁面欄位。在繫結這些值時,它還會嘗試將它們轉換為正確的型別。

理解這一點的最佳方法是逐步瀏覽一個示例。我們的應用程式接收一個 GET 請求

https://:8080/mycorp/customer-details.htm?customerId=7203

此請求會由我們的CustomerDetails頁面自動處理

package com.mycorp.page;

public class CustomerDetails extends Page 
{
    public Integer customerId;
}

建立 CustomerDetails 頁面後,"customerId" 請求引數值 "7023" 將被轉換為 Integer 並分配給公共頁面欄位customerId.

Click 的另一個特性是,任何公共頁面欄位都會在頁面渲染之前自動新增到頁面的模型中。這將使這些值在頁面模板中可用以顯示。在我們的示例中,公共customerId欄位將被新增到頁面模型中,並且將在頁面模板中可用以進行渲染

我們的 customer-details.htm 頁面模板包含

<html>
<body>

  Customer ID: $customerId

</body>
</html>

處理完請求後,我們的頁面將渲染為

客戶 ID:7203

自定義自動繫結

[edit | edit source]

自動繫結支援將請求字串引數轉換為 Java 類:Integer、Double、Boolean、Byte、Character、Short、Long、Float、BigInteger、BigDecimal、String 和各種 Date 類。

預設情況下,型別轉換由 [click-api/net/sf/click/util/RequestTypeConverter.html RequestTypeConverter] 類執行,該類由 ClickServlet 方法 [click-api/net/sf/click/ClickServlet.html#getTypeConverter() getTypeConverter()] 使用。

如果您需要新增對其他型別的支援,則需要編寫自己的型別轉換器類並對 ClickSerlvet 進行子類化以使用自定義轉換器。

例如,如果我們想從資料庫中自動載入一個客戶物件,當指定客戶 ID 請求引數時,您可以編寫自己的型別轉換器

public class CustomTypeConverter extends RequestTypeConverter
{
    private CustomerService customerService = new CustomerService();

    /**
     * @see RequestTypeConverter#convertValue(Object, Class)
     */
    protected Object convertValue(Object value, Class toType)
    {
        if (toType == Customer.class) 
        {
            return customerService.getCustomerForId(value);

        } 
        else
        {
            return super.convertValue(value, toType);
        }
    }
}

此型別轉換器將處理以下請求

https://:8080/mycorp/customer-details.htm?customer=7203

使用給定的 "7203" 客戶 ID 值從資料庫中載入客戶物件。然後,ClickServlet 會將此客戶物件分配給匹配的頁面欄位

package com.mycorp.page;

public class CustomerDetails extends Page 
{
    public Customer customer;
}

要使自定義型別轉換器可用,您需要對 ClickServlet 進行子類化並覆蓋getTypeConverter()方法。例如

public class CustomClickServlet extends ClickServlet 
{
    /**
     * @see ClickServlet#getTypeConverter()
     */
    protected TypeConverter getTypeConverter()
    {
        if (typeConverter == null) 
        {
            typeConverter = new CustomTypeConverter();
        }
        return typeConverter;
    }
}
[edit | edit source]

使用轉發、重定向和設定頁面模板路徑來實現頁面之間的導航。

轉發

[edit | edit source]

要使用 servlet RequestDispatcher 轉發到另一個頁面,請設定頁面的轉發屬性。例如,要轉發到路徑為 index.htm 的頁面

/**
 * @see Page#onPost()
 */
public void onPost() 
{
   // Process form post
   ..

   setForward("index.htm");
}

這將呼叫對映到路徑 index.htm 的新頁面類例項。請注意,當請求轉發到另一個頁面時,第二個頁面上的控制元件將不會被處理。這可以防止混淆和錯誤,例如第二個頁面上的表單嘗試處理來自第一個頁面的 POST 請求。

轉發引數傳遞

[edit | edit source]

當您轉發到另一個頁面時,請求引數將被保留。這是一種透過請求傳遞狀態資訊的方法。例如,您可以新增一個客戶物件作為請求引數,該引數在轉發頁面的模板中顯示。

public boolean onViewClick() 
{
   Long id = viewLink.getValueLong();
   Customer customer = CustomerDAO.findByPK(id);

   getContext().setRequestAttribute("customer", customer);
   setForward("view-customer.htm");

   return false;
}

轉發到頁面模板 view-customer.htm

<html>
 <head>
   <title>Customer Details</title>
 </head>
 <body>
   <h1>Customer Details</h1>
   <pre>
     Full Name: $customer.fullName
     Email:     $customer.email
     Telephone: $customer.telephone
    </pre>
  </body>
</html>

請求屬性會自動新增到 Velocity Context 物件中,因此在頁面模板中可用。

頁面轉發

[edit | edit source]

頁面轉發是傳遞頁面之間資訊的另一種方式。在這種情況下,您使用 Context createPage() 方法建立要轉發的頁面,然後直接在頁面上設定屬性。最後,將此頁面設定為將請求轉發到的頁面。例如

public boolean onEditClick() 
{
   Long id = viewLink.getValueLong();
   Customer customer = CustomerDAO.findByPK(id);

   EditPage editPage = (EditPage) getContext().createPage("/edit-customer.htm");
   editPage.setCustomer(customer);
   setForward(editPage);

   return false;
}

使用createPage()方法建立頁面時,請確保在頁面路徑前新增"/"字元。

您還可以使用其類來指定目標頁面,只要頁面具有唯一的路徑。使用此技術,上面的程式碼將變為

public boolean onEditClick() 
{
   Long id = viewLink.getValueLong();
   Customer customer = CustomerDAO.findByPK(id);

   EditPage editPage = (EditPage) getContext().createPage(EditPage.class);
   editPage.setCustomer(customer);
   setForward(editPage);

   return false;
}

此頁面轉發技術是最佳實踐,因為它為您提供編譯時安全性,並讓您不必在程式碼中指定頁面路徑。

請始終使用 ContextcreatePage()方法以允許 Click 注入頁面依賴項。

模板路徑

[編輯 | 編輯原始碼]

轉發到新頁面的另一種方法是簡單地將路徑設定為要呈現的新頁面模板。使用這種方法,要呈現的頁面模板必須具有它需要的一切,而無需建立其關聯的頁面物件。我們修改後的示例將是

public boolean onViewClick() 
{
   Long id = viewLink.getValueLong();
   Customer customer = CustomerDAO.findByPK(id);

   addModel("customer", customer);
   setPath("view-customer.htm");

   return false;
}

請注意如何將customer物件透過頁面模型傳遞到模板中。當您轉發到另一個頁面時,這種使用頁面模型的方法不可用,因為第一個頁面物件會在建立第二個頁面物件之前被“銷燬”,任何模型值都會丟失。

重定向

[編輯 | 編輯原始碼]

重定向是導航頁面之間的一種非常有用的方法。

重定向的優點是,它們在使用者的瀏覽器中提供了與他們正在檢視的頁面匹配的乾淨 URL。這對於使用者想要將頁面加入書籤時非常重要。重定向的缺點是,它們涉及與使用者瀏覽器進行通訊往返,瀏覽器請求新頁面。這不僅需要時間,而且意味著所有頁面和請求資訊都會丟失。

下面提供了一個重定向到 logout.htm 頁面的示例

public boolean onLogoutClick()
{
   setRedirect("/logout.htm");
   return false;
}

如果重定向位置以“/”字元開頭,則重定向位置將以 Web 應用程式的上下文路徑為字首。

例如,如果應用程式部署到上下文“mycorp”,則呼叫setRedirect("/customer/details.htm")將請求重定向到:"/mycorp/customer/details.htm"

您還可以透過目標頁面的類獲取重定向路徑。例如

public boolean onLogoutClick() 
{
   String path = getContext().getPagePath(Logout.class);
   setRedirect(path);
   return false;
 }

請注意,使用此重定向方法時,目標頁面類必須具有唯一的路徑。

重定向的簡便方法是在重定向方法中簡單地指定目標頁面類。例如

public boolean onLogoutClick()
{
   setRedirect(Logout.class);
   return false;
 }

重定向引數傳遞

[編輯 | 編輯原始碼]

您可以使用 URL 請求引數在重定向的頁面之間傳遞資訊。ClickServlet 將使用 HttpServletResponse.encodeRedirectURL(url) 方法為您對 URL 進行編碼。

在下面的示例中,使用者將點選“確定”按鈕以確認付款。該onOkClick()按鈕處理程式處理付款,獲取付款交易 ID,然後將交易 ID 編碼到 URL 中並重定向到 trans-complete.htm 頁面。

public class Payment extends Page 
{
   ..

   public boolean onOkClick() 
   {
      if (form.isValid()) 
      {
         // Process payment
         ..

         // Get transaction id
         Long transId = OrderDAO.purchase(order);

         setRedirect("trans-complete.htm?transId="   transId);

         return false;
       }
 
       return true;
    }
}

trans-complete.htm 頁面的頁面類可以透過請求引數獲取交易 ID"transId":

public class TransComplete extends Page 
{
   /**
    * @see Page#onInit()
    */
   public void onInit() 
   {
      String transId = getContext().getRequest().getParameter("transId");

      if (transId != null) 
      {
         // Get order details
         Order order = OrderDAO.findOrderByPK(new Long(transId));
         if (order != null) 
         {
            addModel("order", order);
         }
      }
   }
}

Post 重定向

[編輯 | 編輯原始碼]

上面的引數傳遞示例也是 Post 重定向的示例。Post 重定向技術是防止使用者透過點選重新整理按鈕兩次提交表單的非常有用的方法。

頁面模板

[編輯 | 編輯原始碼]

Click 支援頁面模板(在 Struts 中也稱為Tiles),使您可以為您的 Web 應用程式建立標準的外觀,並大大減少您需要維護的 HTML 數量。

要實現模板,請定義一個邊界模板基本頁面,內容頁面應擴充套件該頁面。模板基本頁面類覆蓋 Page getTemplate() 方法,返回要呈現的邊界模板的路徑。例如

public class BorderedPage extends Page 
{
    /**
     * @see Page#getTemplate()
     */
    public String getTemplate() 
    {
        return "/border.htm";
    }
}

BorderedPage 模板 border.htm

 <html>
   <head>
     <title>$title</title>
     <link rel="stylesheet" type="text/css" href="style.css" title="Style"/>
   </head>
   <body>

     <h2 class="title">$title</h2>

     #parse($path)

   </body>
 </html>

其他頁面使用 Velocity #parse 指令將它們的內容插入到此模板中,並將它們的內容頁面的路徑傳遞給它。$path 值由 ClickServlet 自動新增到 VelocityContext 中。

下面提供了一個帶邊框的主頁示例

<page path="home.htm" classname="Home"/>
public class Home extends BorderedPage 
{
     public String title = "Home";
}

主頁的內容 home.htm

<b>Welcome</b> to Home page your starting point for the application.

當請求主頁(home.htm)時,Velocity 將合併 border.htm 頁面和 home.htm 頁面,返回

 <html>
   <head>
     <title>Home</title>
     <link rel="stylesheet" type="text/css" href="style.css" title="Style"/>
   </head>
   <body>

     <h2 class="title">Home</h2>

     <b>Welcome</b> to Home page your application starting point.

   </body>
 </html>

這可能會被渲染為

主頁

歡迎使用您的應用程式的起點主頁。

請注意,主頁類如何定義一個title模型值,該模型值在border.htm模板中被引用為$title。每個帶邊框的頁面都可以定義自己的標題,該標題在此模板中呈現。

使用相同模式還支援使用 JSP 頁面進行模板化。請參閱 Click 示例應用程式以瞭解演示。

安全性

[編輯 | 編輯原始碼]

頁面提供了一個 onSecurityCheck 事件處理程式,應用程式頁面可以覆蓋該處理程式以實現程式設計安全模型。

請注意,您通常不需要使用此功能,並且在可能的情況下,您應該使用宣告性 JEE 安全模型。

應用程式身份驗證

[編輯 | 編輯原始碼]

應用程式可以使用onSecurityCheck()方法來實現自己的安全模型。下面的示例類提供了一個基本的安全頁面類,其他頁面可以擴充套件該類以確保使用者已登入。在此示例中,登入頁面在使用者成功驗證後建立會話。然後,此安全頁面檢查使用者是否具有會話,否則請求將重定向到登入頁面。

public class Secure extends Page 
{
    /**
     * @see Page#onSecurityCheck()
     */
    public boolean onSecurityCheck() 
    {

        if (getContext().hasSession()) 
        {
            return true;
        } 
        else 
        {
            setRedirect(LoginPage.class);
            return false;
        }
    }
}

容器身份驗證

[編輯 | 編輯原始碼]

或者,您也可以使用 JEE Servlet Container 提供的安全服務。例如,為了確保使用者已透過 Serlvet Container 驗證,您可以使用一個安全頁面

public class Secure extends Page 
{
    /**
     * @see Page#onSecurityCheck()
     */
    public boolean onSecurityCheck() 
    {

        if (getContext().getRequest().getRemoteUser() != null) 
        {
            return true;
 
        } 
        else 
        {
            setRedirect(LoginPage.class);
            return false;
        }
    }
}

容器訪問控制

[編輯 | 編輯原始碼]

Servlet Container 還提供設施來強制實施基於角色的訪問控制(授權)。下面的示例是一個基本頁面,用於確保只有“admin”角色中的使用者才能訪問該頁面,否則使用者將被重定向到登入頁面。應用程式管理頁面將擴充套件此安全頁面以提供其功能。

public class AdminPage extends Page 
{
    /**
     * @see Page#onSecurityCheck()
     */
    public boolean onSecurityCheck() 
    {
        if (getContext().getRequest().isUserInRole("admin")) 
        {
            return true;
 
        }
        else 
        {
            setRedirect(LoginPage.class);
            return false;
        }
    }
}

要使用應用程式或基於容器的安全模型登出,您只需使會話失效即可。

public class Logout extends Page 
{
    /**
     * @see Page#onInit()
     */
    public void onInit() 
    {
        getContext().getSession().invalidate();
    }
}

有狀態頁面

[編輯 | 編輯原始碼]

Click 支援有狀態頁面,其中頁面的狀態在使用者請求之間儲存。有狀態頁面在許多場景中很有用,包括

  • 搜尋頁面和編輯頁面互動,在這種情況下,您從一個可能已應用篩選條件的有狀態搜尋頁面導航到一個物件編輯頁面。在編輯頁面上完成物件更新後,使用者將被重定向到搜尋頁面,並且有狀態篩選條件仍然適用。
  • 具有多個表單或表格的複雜頁面,需要在互動之間維護其狀態。

要使頁面有狀態,您只需將頁面的 [click-api/net/sf/click/Page.html#stateful stateful] 屬性設定為 true,並讓頁面實現Serializable介面。例如

package com.mycorp.page;

import java.io.Serializable;

import net.sf.click.Page;

public class SearchPage extends Page implements Serializable
{

    public SearchPage() 
    {
        setStateful(true);
        ..
    }
}

有狀態頁面例項儲存在使用者的 HttpSession 中,使用頁面的類名作為鍵。在上面的示例中,頁面將使用類名儲存在使用者的會話中。com.mycorp.page.SearchPage

頁面建立

[編輯 | 編輯原始碼]

對於有狀態頁面,它們只建立一次,之後從會話中檢索。但是,頁面事件處理程式會為每個請求呼叫,包括onInit()方法進行渲染。

當您建立有狀態頁面時,通常會將所有控制元件建立程式碼放在 Pages 建構函式中,以便只調用一次,並且不要將此程式碼放在onInit()方法中,該方法將隨每個請求呼叫。

如果您有動態控制元件建立程式碼,通常會將此程式碼放在onInit()方法中,但您需要確保頁面中不存在控制元件或模型。

頁面執行

[編輯 | 編輯原始碼]

預設的 Click 頁面執行模型是執行緒安全的,因為每個請求和執行緒都會建立一個新的 Page 例項。對於有狀態頁面,使用者將擁有單個頁面例項,該例項在多個請求和執行緒中被重用。為了確保頁面執行是執行緒安全的,使用者的頁面例項是同步的,因此一次只有一個請求執行緒可以執行頁面例項。

頁面銷燬

[編輯 | 編輯原始碼]

在正常頁面例項執行後,它們會解除引用並由 JVM 回收。但是,對於有狀態頁面,它們儲存在使用者的HttpSession中,因此需要注意不要在有狀態頁面例項中儲存太多物件,這可能會導致記憶體和效能問題。

當頁面完成執行後,所有 Page 的控制元件onDestroy()方法都會被呼叫,然後 Page 的onDestroy()方法會被呼叫。這是您解除引用任何大型集合或圖的機會。例如,Table 控制元件預設情況下會在其 onDestroy() 方法中解除引用其 rowList。

錯誤處理

[編輯 | 編輯原始碼]

如果在處理 Page 物件或渲染模板時發生異常,錯誤將委託給註冊的處理程式。預設的 Click 錯誤處理程式是 ErrorPage,它被自動配置為

<page path="click/error.htm" classname="net.sf.click.util.ErrorPage"/>

要註冊替代的錯誤處理程式,您必須子類化 ErrorPage 並使用路徑“click/error.htm”定義您的頁面。例如

<page path="click/error.htm" classname="com.mycorp.page.ErrorPage"/>

當 ClickSevlet 啟動時,它會檢查 click Web 子目錄中是否存在 error.htm 模板。如果找不到頁面,ClickServlet 將自動部署一個。您可以根據自己的喜好調整 click/error.htm 模板,ClickServlet 不會覆蓋它。

當應用程式處於開發除錯模式時,預設的錯誤模板將顯示大量除錯資訊。示例錯誤頁面顯示包括

  • NullPointerException - 在頁面方法中
  • ParseErrorException - 在頁面模板中

當應用程式處於生產模式時,只會顯示簡單的錯誤訊息。

頁面未找到

[編輯 | 編輯原始碼]

如果 ClickServlet 在click.xml配置檔案中找不到請求的頁面,它將使用註冊的 not-found.htm 頁面。

Click 找不到頁面被自動配置為

<page path="click/not-found.htm" classname="net.sf.click.Page"/>

您可以覆蓋預設配置並指定自己的類,但不能更改路徑。

當 ClickSevlet 啟動時,它會檢查 click Web 子目錄中是否存在 not-found.htm 模板。如果找不到頁面,ClickServlet 將自動部署一個。

您可以根據自己的需要調整 click/not-found.htm 模板。此頁面模板可以訪問通常的 Click 物件。

訊息屬性

[編輯 | 編輯原始碼]

Page 類提供了一個 messages 屬性,它是一個 MessagesMap,包含該頁面的本地化訊息。當頁面以鍵messages渲染時,這些訊息在 VelocityContext 中可用。例如,如果您有一個頁面標題訊息,您將在頁面模板中訪問它,如下所示。

<h1> $messages.title </h1>

此訊息對映從頁面類的屬性包載入。例如,如果您有一個頁面類com.mycorp.page.CustomerList,您可以擁有一個包含頁面本地化訊息的關聯屬性檔案

/com/mycorp/page/CustomerList.properties

您還可以定義一個應用程式全域性頁面訊息屬性檔案

/click-page.properties

在此檔案中定義的訊息將透過您的應用程式中的所有頁面提供。請注意,在頁面類屬性檔案中定義的訊息將覆蓋應用程式全域性頁面屬性檔案中定義的任何訊息。

頁面訊息也可用於覆蓋控制元件訊息。

華夏公益教科書