在本系列的第 1 部分簡要回顧了 JSR 168 Portlet,並對 JSR 286 Portlet 的新增特性做了詳細介紹,第 2 部分和第 3 部分將透過在 Apache Pluto 2.0 平台上開發和部署 Portlet 應用程式,向讀者介紹 JSR 286 Portlet 新特性的使用方法。本文將介紹 JSR 286 Portlet 的 Portlet 過濾器和 Portlet 視窗應用程式開發。
本系列專門針對具有 JSR 168 Portlet 開發基礎,並且想了解 JSR 286 Portlet 新特性和開發流程的開發人員。在學完本系列後,您將了解到相對於 JSR 168 Portlet,JSR 286 Portlet 究竟提供了哪些增強功能,以及這些新增特性在實際開發中的應用。
本系列的第 1 部分簡單回顧了 JSR 168 Portlet,並列出了 JSR 286 Portlet 的新增內容。第 2 部分和第 3 部分將透過在 Apache Pluto 2.0 平台上開發和部署 Portlet 應用程式,向讀者介紹 JSR 286 Portlet 新特性的使用方法。
本文承接第 2 部分,繼續介紹 JSR 286 Portlet 的 Portlet 過濾器和 Portlet 視窗應用程式開發。閱讀本文之前,您應當對 JSR 168 Portlet 有所了解,並閱讀了本系列的第 1 部分和第 2 部分。
透過第 1 部分的介紹,我們知道 Portlet 過濾器分為:
我們將首先對這四種 Portlet 過濾器的開發使用流程分別單獨進行介紹,然後將這四種 Portlet 過濾器綜合起來進行更進一步的開發,最後透過和 Servlet 過濾器的結合使用,使讀者明白 Portlet 過濾器和 Servlet 過濾器的關係和區別。
新建 Java 類 TestActionFilter:
package com.ibm.samples.jsr286.filters;
import ...
public class TestActionFilter implements ActionFilter {
private static Log log = LogFactory.getLog(TestActionFilter.class);
private FilterConfig filterConfig;
public void init(FilterConfig filterConfig) throws PortletException {
log.info("action filter [" + filterConfig.getFilterName() + "] is initialized.");
this.filterConfig = filterConfig;
}
public void destroy() {
log.info("action filter [" + filterConfig.getFilterName() + "] is destroyed.");
}
public void doFilter(ActionRequest actionRequest, ActionResponse actionResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("action filter [" + filterConfig.getFilterName() + "] is called.");
filterChain.doFilter(actionRequest, actionResponse);
}
}
這個程式的主要作用就是在 Action Filter 初始化、過濾器呼叫、銷毀的時候分別列印相應的資訊。程式碼片段 1 中 filterChain.doFilter(actionRequest, actionResponse) 需要讀者特別注意,這行程式碼保證了過濾器鏈的傳遞,刪去這行程式碼,則過濾器鏈將在該過濾器執行結束後終結。
編輯 portlet.xml 檔案,加入如下片段:
<portlet-app ...>
...
<filter>
<display-name>TestActionFilter</display-name>
<filter-name>TestActionFilter</filter-name>
<filter-class>com.ibm.samples.jsr286.filters.TestActionFilter</filter-class>
<lifecycle>ACTION_PHASE</lifecycle>
</filter>
...
</portlet-app>
定義 Action 過濾器映射,可以映射到具體某個 Portlet,或者根據模式匹配到一組 Portlet:
...
<filter-mapping>
<filter-name>TestActionFilter</filter-name>
<portlet-name>*</portlet-name>
</filter-mapping>
...
在上面的定義中,我們宣告 TestActionFilter 對所有 Portlet 的 processAction 呼叫進行攔截。
重啟 Web 應用程式並將 TestPortlet 部署到 "Test JSR 286 Portlet Page" 頁面,並將該頁面其他所有 Portlet 移除,輸入資料,點擊 Submit 按鈕觸發 processAction 呼叫,Eclipse Console 出現如下輸出:
...
2008-3-16 22:35:20 com.ibm.samples.jsr286.filters.TestActionFilter init
資訊: action filter [TestActionFilter] is initialized.
2008-3-16 22:35:24 com.ibm.samples.jsr286.filters.TestActionFilter doFilter
資訊: action filter [TestActionFilter] is called.
2008-3-16 22:35:24 com.ibm.samples.jsr286.filters.TestActionFilter destroy
資訊: action filter [TestActionFilter] is destroyed.
...
從上面的資訊可以看出,對於 Portlet 的每次 processAction 呼叫,Action Filter 都要經歷一個初始化、過濾方法 doFilter 呼叫、銷毀的全過程。讀者可以多次實驗證實這一點。
新建 Java 類 TestRenderFilter:
package com.ibm.samples.jsr286.filters;
import ...
public class TestRenderFilter implements RenderFilter {
private static Log log = LogFactory.getLog(TestRenderFilter.class);
private FilterConfig filterConfig;
public void init(FilterConfig filterConfig) throws PortletException {
log.info("render filter [" + filterConfig.getFilterName() + "] is initialized.");
this.filterConfig = filterConfig;
}
public void destroy() {
log.info("render filter [" + filterConfig.getFilterName() + "] is destroyed.");
}
public void doFilter(RenderRequest renderRequest, RenderResponse renderResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("render filter [" + filterConfig.getFilterName() + "] is called.");
filterChain.doFilter(renderRequest, renderResponse);
}
}
編輯 portlet.xml 檔案,加入如下片段:
<portlet-app ...>
...
<filter>
<display-name>TestRenderFilter</display-name>
<filter-name>TestRenderFilter</filter-name>
<filter-class>com.ibm.samples.jsr286.filters.TestRenderFilter</filter-class>
<lifecycle>RENDER_PHASE</lifecycle>
</filter>
...
</portlet-app>
定義 Render 過濾器映射:
...
<filter-mapping>
<filter-name>TestRenderFilter</filter-name>
<portlet-name>*</portlet-name>
</filter-mapping>
...
重啟 Web 應用程式並將多個 Portlet 部署到 "Test JSR 286 Portlet Page" 頁面,造訪該頁面,Eclipse Console 出現多個如下輸出:
...
2008-3-16 22:53:11 com.ibm.samples.jsr286.filters.TestRenderFilter init
資訊: render filter [TestRenderFilter] is initialized.
2008-3-16 22:53:11 com.ibm.samples.jsr286.filters.TestRenderFilter doFilter
資訊: render filter [TestRenderFilter] is called.
2008-3-16 22:53:11 com.ibm.samples.jsr286.filters.TestRenderFilter destroy
資訊: render filter [TestRenderFilter] is destroyed.
...
從上面的資訊可以看出,對於 Portlet 的每次 render 呼叫,Render Filter 都要經歷一個初始化、過濾方法 doFilter 呼叫、銷毀的全過程。以上資訊出現的次數與部署到頁面上的 Portlet 個數相同,意味著 Portlet 過濾器的攔截是分別針對每個 Portlet 進行的。
新建 Java 類 TestResourceFilter:
package com.ibm.samples.jsr286.filters;
import ...
public class TestResourceFilter implements ResourceFilter {
private static Log log = LogFactory.getLog(TestResourceFilter.class);
private FilterConfig filterConfig;
public void init(FilterConfig filterConfig) throws PortletException {
log.info("resource filter [" + filterConfig.getFilterName() + "] is initialized.");
this.filterConfig = filterConfig;
}
public void destroy() {
log.info("resource filter [" + filterConfig.getFilterName() + "] is destroyed.")
}
public void doFilter(ResourceRequest resourceRequest, ResourceResponse resourceResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("resource filter [" + filterConfig.getFilterName() + "] is called.")
filterChain.doFilter(resourceRequest, resourceResponse);
}
}
編輯 portlet.xml 檔案,加入如下片段:
<portlet-app ...>
...
<filter>
<display-name>TestResourceFilter</display-name>
<filter-name>TestResourceFilter</filter-name>
<filter-class>com.ibm.samples.jsr286.filters.TestResourceFilter</filter-class>
<lifecycle>RESOURCE_PHASE</lifecycle>
</filter>
...
</portlet-app>
定義 Resource 過濾器映射:
...
<filter-mapping>
<filter-name>TestResourceFilter</filter-name>
<portlet-name>*</portlet-name>
</filter-mapping>
...
重啟 Web 應用程式並將 TestPortlet 部署到 "Test JSR 286 Portlet Page" 頁面,造訪該頁面,點擊超連結 "Click me to request Resource URL" 請求資源,Eclipse Console 出現如下輸出:
...
2008-3-17 13:21:03 com.ibm.samples.jsr286.filters.TestResourceFilter init
資訊: resource filter [TestResourceFilter] is initialized.
2008-3-17 13:21:03 com.ibm.samples.jsr286.filters.TestResourceFilter doFilter
資訊: resource filter [TestResourceFilter] is called.
2008-3-17 13:21:05 com.ibm.samples.jsr286.filters.TestResourceFilter destroy
資訊: resource filter [TestResourceFilter] is destroyed.
...
從上面的資訊可以看出,對於 Portlet 的每次 serveResource 呼叫,Resource Filter 都要經歷一個初始化、過濾方法 doFilter 呼叫、銷毀的全過程。
新建 Java 類 TestEventFilter:
package com.ibm.samples.jsr286.filters;
import ...
public class TestEventFilter implements EventFilter {
private static Log log = LogFactory.getLog(TestEventFilter.class);
private FilterConfig filterConfig;
public void init(FilterConfig filterConfig) throws PortletException {
log.info("event filter [" + filterConfig.getFilterName() + "] is initialized.")
this.filterConfig = filterConfig;
}
public void destroy() {
log.info("event filter [" + filterConfig.getFilterName() + "] is destroyed.")
}
public void doFilter(EventRequest eventRequest, EventResponse eventResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("event filter [" + filterConfig.getFilterName() + "] is called.")
Event event = eventRequest.getEvent();
log.info("event name: " + event.getName());
log.info("event qname: " + event.getQName());
log.info("event value: " + event.getValue().toString());
filterChain.doFilter(eventRequest, eventResponse);
}
}
編輯 portlet.xml 檔案,加入如下片段:
<portlet-app ...>
...
<filter>
<display-name>TestEventFilter</display-name>
<filter-name>TestEventFilter</filter-name>
<filter-class>com.ibm.samples.jsr286.filters.TestEventFilter</filter-class>
<lifecycle>EVENT_PHASE</lifecycle>
</filter>
...
</portlet-app>
定義 Event 過濾器映射:
...
<filter-mapping>
<filter-name>TestEventFilter</filter-name>
<portlet-name>*</portlet-name>
</filter-mapping>
...
重啟 Web 應用程式並將 TestSimpleEventSenderPortlet、TestSimpleEventReceiverPortlet、TestComplexEventSenderPortlet、TestComplexEventReceiverPortlet 部署到 "Test JSR 286 Portlet Page" 頁面,造訪該頁面,點擊 TestSimpleEventSenderPortlet 按鈕,Eclipse Console 出現如下輸出:
...
2008-3-17 19:51:35 com.ibm.samples.jsr286.filters.TestEventFilter init
資訊: event filter [TestEventFilter] is initialized.
2008-3-17 19:51:35 com.ibm.samples.jsr286.filters.TestEventFilter doFilter
資訊: event filter [TestEventFilter] is called.
2008-3-17 19:51:35 com.ibm.samples.jsr286.filters.TestEventFilter doFilter
資訊: event name: simple-event
2008-3-17 19:51:35 com.ibm.samples.jsr286.filters.TestEventFilter doFilter
資訊: event qname: {https://cn.ibm.com/}simple-event
2008-3-17 19:51:35 com.ibm.samples.jsr286.filters.TestEventFilter doFilter
資訊: event value: simple-event is sent by TestSimpleEventSenderPortlet
2008-3-17 19:51:35 com.ibm.samples.jsr286.filters.TestEventFilter destroy
資訊: event filter [TestEventFilter] is destroyed.
...
從上面的資訊可以看出,對於 Portlet 的每次發送事件行為,Event Filter 都要經歷一個初始化、過濾方法 doFilter 呼叫、銷毀的全過程。從程式碼片段 16 也可以看到過濾器捕獲到的事件資訊。發送複雜事件的過濾器結果捕獲讀者可以自行測試。
Portlet 的四種過濾器可以整合到一個類中去實作,只要該類實作了上述四個介面即可。以下為類 TestAllPhaseFilter 的類別圖:

TestAllPhaseFilter 實作程式碼如程式碼片段 17 所示:
package com.ibm.samples.jsr286.filters;
import ...
public class TestAllPhaseFilter implements ActionFilter, RenderFilter, ResourceFilter, EventFilter {
private static Log log = LogFactory.getLog(TestAllPhaseFilter.class);
private FilterConfig filterConfig;
public void init(FilterConfig filterConfig) throws PortletException {
log.info("filter [" + filterConfig.getFilterName() + "] is initialized.")
this.filterConfig = filterConfig;
}
public void destroy() {
log.info("filter [" + filterConfig.getFilterName() + "] is destroyed.")
}
public void doFilter(ActionRequest actionRequest, ActionResponse actionResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("filter [" + filterConfig.getFilterName() + "] for action is called.")
filterChain.doFilter(actionRequest, actionResponse);
}
public void doFilter(RenderRequest renderRequest, RenderResponse renderResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("filter [" + filterConfig.getFilterName() + "] for render is called.")
PrintWriter out = renderResponse.getWriter();
out.println("<font color=red>Portlet filter test for writing to output stream.</font>");
filterChain.doFilter(renderRequest, renderResponse);
}
public void doFilter(ResourceRequest resourceRequest, ResourceResponse resourceResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("filter [" + filterConfig.getFilterName() + "] for resouce is called.")
log.info("Resource ID:" + resourceRequest.getResourceID());
PrintWriter out = resourceResponse.getWriter();
out.println("<font color=red>Portlet filter test for writing to output stream.</font>");
filterChain.doFilter(resourceRequest, resourceResponse);
}
public void doFilter(EventRequest eventRequest, EventResponse eventResponse, FilterChain filterChain)
throws IOException, PortletException {
log.info("filter [" + filterConfig.getFilterName() + "] for event is called.")
Event event = eventRequest.getEvent();
log.info("event name: " + event.getName());
log.info("event qname: " + event.getQName());
log.info("event value: " + event.getValue().toString());
filterChain.doFilter(eventRequest, eventResponse);
}
}
編輯 portlet.xml 檔案,加入如下片段:
...
<filter>
<display-name>TestAllPhaseFilter</display-name>
<filter-name>TestAllPhaseFilter</filter-name>
<filter-class>com.ibm.samples.jsr286.filters.TestAllPhaseFilter</filter-class>
<lifecycle>ACTION_PHASE</lifecycle>
<lifecycle>RENDER_PHASE</lifecycle>
<lifecycle>RESOURCE_PHASE</lifecycle>
<lifecycle>EVENT_PHASE</lifecycle>
</filter>
...
<filter-mapping>
<filter-name>TestAllPhaseFilter</filter-name>
<portlet-name>*</portlet-name>
</filter-mapping>
...
這部分宣告代表該過濾器支援 Portlet 四個階段 action、render、resource、event 的過濾攔截,且對所有 Portlet 均起作用。具體執行結果請讀者自行驗證。讀者可以從中體會到 Portlet 過濾器的攔截和輸出流中寫入資料的功能。
同 Servlet 過濾器一樣,Portlet 過濾器同樣存在過濾器鏈,但這個過濾器鏈是特定於 Portlet 的某個階段的,如圖 2 所示:

其中圖 2 中的兩個 Portlet 過濾器組成了過濾器鏈 (Portlet Filter Chain),Portlet 請求先流經 Portlet 過濾器鏈,最後到達 Portlet,Portlet 回應同樣也流經 Portlet 過濾器鏈到達 Portal 頁面聚集內容顯示。
TestAllPhaseFilter 支援 Portlet 四個階段的回應,在 Portlet 特定的階段,可以分別和前四節中的 Portlet 過濾器形成過濾器鏈,讀者可以從日誌輸出的 Portlet 過濾器呼叫次序證明這一點。
從本系列第 1 部分的圖 3 所示,我們已經得知,Servlet 過濾器是一個入口級過濾器,它是接收和修改客戶端請求的第一個元件,同時也是修改對客戶端回應的最後一個元件。Servlet 過濾器比 Portlet 過濾器的優先級別要高,容器將首先進行 Servlet 過濾,其次是 Portlet 過濾。
為了在 Portal 容器中測試 Servlet 過濾器,我們最好能自己將 Portal 容器建構起來。幸運的是,Apache Pluto 使用 Servlet 實作了輕量級的 Portal 容器,我們很容易就可以做到這一點。
將 ${TOMCAT_HOME}\PlutoDomain\ 下的 pluto-portal-2.0.0-SNAPSHOT.war 解壓縮。目錄結構如圖 3 所示:

在 Eclipse 新建動態 Web 專案 myportal,執行環境選擇 Tomcat 6,Servlet 版本選擇 2.3。
將 jsr286portlets 專案中 WebContent\META-INF\ 目錄下的 context.xml 檔案複製到 myportal 專案的 WebContent\META-INF\ 目錄下。
將 pluto-portal-2.0.0-SNAPSHOT\ 目錄下的以下目錄和檔案複製到 myportal 專案的 WebContent\ 目錄下:
將 pluto-portal-2.0.0-SNAPSHOT\WEB-INF\ 目錄下的以下目錄和檔案複製到 myportal 專案的 WebContent\WEB-INF 目錄下:
將 pluto-portal-2.0.0-SNAPSHOT\WEB-INF\classes\ToolTips.properties 檔案複製到 myportal 專案的 src 目錄下。
編輯 myportal 專案的 web.xml 檔案,內容如程式碼片段 19 所示:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "https://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>myportal</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/pluto-portal-driver-services-config.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<listener>
<listener-class>org.apache.pluto.driver.PortalStartupListener</listener-class>
</listener>
<servlet>
<servlet-name>plutoPortalDriver</servlet-name>
<servlet-class>org.apache.pluto.driver.PortalDriverServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>plutoPortalDriver</servlet-name>
<url-pattern>/portal/*</url-pattern>
</servlet-mapping>
<taglib>
<taglib-uri>https://portals.apache.org/pluto</taglib-uri>
<taglib-location>/WEB-INF/tld/pluto.tld</taglib-location>
</taglib>
</web-app>
在 myportal 專案的 WEB-INF 目錄下新建 pluto-portal-driver-config.xml 檔案,該檔案為 pluto portal 容器的入口網站配置檔案。內容如程式碼片段 20 所示:
<?xml version="1.0" encoding="UTF-8"?>
<pluto-portal-driver>
<portal-name>pluto-portal-driver</portal-name>
<portal-version>2.0.0-SNAPSHOT</portal-version>
<container-name>Pluto Portal Driver</container-name>
<supports>
<portlet-mode>view</portlet-mode>
<portlet-mode>edit</portlet-mode>
<portlet-mode>help</portlet-mode>
<portlet-mode>config</portlet-mode>
<window-state>normal</window-state>
<window-state>maximized</window-state>
<window-state>minimized</window-state>
</supports>
<render-config default="JSR 286 test page">
<page name="JSR 286 test page" uri="/WEB-INF/themes/pluto-default-theme.jsp">
<portlet context="/jsr286portlets" name="TestPortlet" />
<portlet context="/jsr286portlets" name="TestSimpleEventSenderPortlet"/>
<portlet context="/jsr286portlets" name="TestSimpleEventReceiverPortlet"/>
<portlet context="/jsr286portlets" name="TestComplexEventSenderPortlet"/>
<portlet context="/jsr286portlets" name="TestComplexEventReceiverPortlet"/>
<portlet context="/jsr286portlets" name="TestPublicRenderParameterPortlet"/>
</page>
</render-config>
</pluto-portal-driver>
在 Eclipse 的 Servers 視圖中將 myportal 專案新增到 tomcat 伺服器中,啟動 tomcat。使用瀏覽器造訪 https://localhost:8080/myportal/portal/,呈現一個包含我們之前產生的所有 portlet 的一個 portal 頁面,如圖 4 所示:

在 myportal 專案中新建 Java 類 TestServletFilter,如程式碼片段 21 所示:
package com.ibm.samples.servlet.filters;
import ...
public class TestServletFilter implements Filter {
private static Log log = LogFactory.getLog(TestServletFilter.class);
private FilterConfig filterConfig;
public void init(FilterConfig filterConfig) throws ServletException {
log.info("servlet filter [" + filterConfig.getFilterName() + "] is initialized.")
this.filterConfig = filterConfig;
}
public void destroy() {
log.info("servlet filter [" + filterConfig.getFilterName() + "] is destroyed.")
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
log.info("serlvet filter [" + filterConfig.getFilterName() + "] is called.")
filterChain.doFilter(servletRequest, servletResponse);
}
}
編輯 myportal 專案的 web.xml 檔案,增加如程式碼片段 22 的配置內容:
...
<filter>
<filter-name>TestServletFilter1</filter-name>
<filter-class>com.ibm.samples.servlet.filters.TestServletFilter</filter-class>
</filter>
<filter>
<filter-name>TestServletFilter2</filter-name>
<filter-class>com.ibm.samples.servlet.filters.TestServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TestServletFilter1</filter-name>
<url-pattern>/portal/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>TestServletFilter2</filter-name>
<url-pattern>/portal/*</url-pattern>
</filter-mapping>
...
可以看到,程式碼片段 22 中使用 Java 類 TestServletFilter 共配置了兩個 Servlet 過濾器,均對 portal 進行過濾。
透過日誌分析,我們可以得到如圖 5 所示的過濾器鏈,該圖體現了一個 HTTP 請求所流經的 Servlet 過濾器鏈和 Portlet 過濾器鏈。請注意圖中用不同顏色的線條表示 Portlet 過濾器針對的 Portlet 的四個不同階段,這四個階段的過濾器操作是分別獨立發生的,而不是並行發生的。

讀者可以從日誌輸出注意到 Servlet 過濾器和 Portlet 過濾器的以下區別和聯繫:
PortletRequest 新增了一個方法 getWindowID(),可以獲得 Portlet 的視窗 ID,這個 ID 是由容器產生的。在 Portal 容器中佈局同一個 Portlet 多次的情況下,windowID 可以用來區分同一個 Portlet 的不同視窗,從而可以使這些 Portlet 視窗快取並呈現不同的資料。
在本節中,將提供一個範例,在同一個 Portlet 的不同視窗中,根據 Portlet 視窗 ID 的不同,呈現不同的資料。
本例中我們使用 XML 檔案提供兩個聯絡人資料,在專案 jsr286portlets 的 src 目錄下,新建 address.xml 檔案,內容如程式碼片段 23 所示:
<?xml version="1.0" encoding="UTF-8"?>
<addressbooks>
<addressbook>
<name>Yan Zhi Dong</name>
<address>Beijing</address>
<telphone>010-12345678</telphone>
<mobile>13512345678</mobile>
<email>yanzhid@cn.ibm.com</email>
</addressbook>
<addressbook>
<name>Liu Xu Jin</name>
<address>Tianjin</address>
<telphone>010-87654321</telphone>
<mobile>13887654321</mobile>
<email>liuxujin@cn.ibm.com</email>
</addressbook>
</addressbooks>
本例使用 Apache Commons 庫的 Digester 套件解析 XML 內容並且封裝到 Java Bean 中。從 https://commons.apache.org/ 查找並下載 commons-digester、commons-collections、commons-beanutils 套件,並將 jar 檔案複製到 WEB-INF\lib 目錄下。
按照 Digester 套件的要求,在 src 目錄下新建一個規則檔案 address-rule.xml,定義 XML 檔案到 Java Bean 的對應規則。如程式碼片段 24 所示:
<?xml version="1.0" encoding="UTF-8"?>
<digester-rules>
<pattern value="addressbooks/addressbook">
<object-create-rule classname="com.ibm.samples.bean.AddressBook" />
<set-next-rule methodname="add" paramtype="java.lang.Object" />
<set-properties-rule />
<bean-property-setter-rule pattern="name" />
<bean-property-setter-rule pattern="address" />
<bean-property-setter-rule pattern="telphone" />
<bean-property-setter-rule pattern="mobile" />
<bean-property-setter-rule pattern="email" />
</pattern>
</digester-rules>
新建 Java 類 AddressBook,該類為所對應的 Java Bean,如程式碼片段 25 所示:
package com.ibm.samples.bean;
import java.io.Serializable;
public class AddressBook implements Serializable {
private static final long serialVersionUID = 5490046055431517141L;
private String name;
private String address;
private String telphone;
private String mobile;
private String email;
// ... setters and getters
}
新建 Portlet 類 TestWindowPortlet,該類繼承了 GenericPortlet,實作了 VIEW 和 EDIT 模式。如程式碼片段 26 所示:
package com.ibm.samples.jsr286.portlets;
import ...
public class TestWindowPortlet extends GenericPortlet {
private static Log log = LogFactory.getLog(TestWindowPortlet.class);
@Override
protected void doEdit(RenderRequest request, RenderResponse response)
throws PortletException, IOException {
PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher("/WEB-INF/jsp/TestWindowPortletEdit.jsp");
rd.include(request, response);
}
@Override
protected void doView(RenderRequest request, RenderResponse response)
throws PortletException, IOException {
String windowID = request.getWindowID();
request.setAttribute("windowID", windowID);
AddressBook addressbook = (AddressBook) getPortletContext().getAttribute(request.getWindowID());
request.setAttribute("addressbook", addressbook);
PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher("/WEB-INF/jsp/TestWindowPortletView.jsp");
rd.include(request, response);
}
@Override
public void processAction(ActionRequest request, ActionResponse response)
throws PortletException, IOException {
List<AddressBook> addressbooks = (List<AddressBook>) getPortletContext().getAttribute("addressbooks");
if (addressbooks == null) {
addressbooks = new ArrayList<AddressBook>();
URL rule = getClass().getResource("/address-rule.xml");
Digester digester = DigesterLoader.createDigester(rule);
InputStream inputStream = getClass().getResourceAsStream("/address.xml");
try {
digester.parse(inputStream);
} catch (SAXException e) {
log.error(this, e);
}
getPortletContext().setAttribute("addressbooks", addressbooks);
}
String strIndex = request.getParameter("index");
int index = Integer.parseInt(strIndex);
AddressBook addressbook = addressbooks.get(index);
getPortletContext().setAttribute(request.getWindowID(), addressbook);
response.setPortletMode(PortletMode.VIEW);
}
}
注意 processAction 方法,根據 Portlet 的視窗 ID,在方法的末尾將特定的 AddressBook 物件和特定的 Portlet 視窗關聯起來,並將這種關聯放在 Portlet 上下文中 (也可以視作快取)。
相應的 JSP 檔案,如程式碼片段 27 和 28:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="portlet" uri="https://java.sun.com/portlet_2_0"%>
<portlet:defineObjects />
<form action="<portlet:actionURL/>">
<table>
<tr><td>Please select one item of the following:</td></tr>
<tr><td><input type="radio" name="index" value="0" />Yan Zhi Dong</td></tr>
<tr><td><input type="radio" name="index" value="1" />Liu Xu Jin</td></tr>
<tr><td><input type="submit" value="Submit" /></td></tr>
</table>
</form>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@taglib prefix="portlet" uri="https://java.sun.com/portlet_2_0"%>
<portlet:defineObjects />
<table>
<tr><td>WindowID:</td><td>${windowID}</td></tr>
<tr><td>Name:</td><td>${addressbook.name}</td></tr>
<tr><td>Address:</td><td>${addressbook.address}</td></tr>
<tr><td>Telphone:</td><td>${addressbook.telphone}</td></tr>
<tr><td>Mobile:</td><td>${addressbook.mobile}</td></tr>
<tr><td>Email:</td><td>${addressbook.email}</td></tr>
</table>
在 portlet.xml 和 web.xml 檔案中增加相應的 Portlet 定義 TestWindowPortlet,並設定該 Portlet 支援 VIEW 和 EDIT 模式。
新建 test 頁面,並將 TestWindowPortlet 新增到 test 頁面兩次,如圖 6 所示:

在 EDIT 模式下,分別選擇不同的選項,然後提交,如圖 7:

最後兩個視窗可以顯示不同的資料,如圖 8 所示:

在該範例中,我們使用 XML 檔案儲存資料。在實際的生產環境中,資料來源可能是一個關聯式資料庫,甚至是一個遠端呼叫 (比如 Web Services)。在這種情況下,如果資料的更新週期比較長,就可以透過資料的快取提高效能。在同一個 Portal 頁面中,如果存在同一個 Portlet 的不同視窗副本,就可以根據 Portlet 的視窗 ID,把從資料庫呼叫或者遠端呼叫獲得的資料快取到 Portal Server 本地,並且和 Portlet 的特定視窗關聯起來,以減少資料庫存取或者遠端呼叫的開銷,提高系統的效能。本例中使用 Portlet 上下文快取資料,實際生產過程中可以使用某些快取產品,如 Ehcache 等。
本部分透過範例程式碼介紹了 JSR 286 Portlet 過濾器和 Portlet 視窗的實際開發使用步驟。至此,JSR 286 Portlet 的新特性及開發範例已經全部介紹完畢。