2006 年 9 月 04 日
許多開發人員都期待著利用 Ajax 技術來提高基於 Web 的應用程序的用戶體驗,但是 Ajax 編程可能是一項麻煩的任務。開放源碼的 Direct Web Remoting (DWR) 庫通過自動把 Java 類轉換成 JavaScript 類,可以為 Java™ 開發人員簡化 Ajax 開發。在這篇文章中,將學習如何用 DWR 和符合 JSR-168 規范的 portlet 迅速而容易地構建 Ajax 應用程序。
Portlet 是基於 Java 平台的 Web 門戶應用程序。JSR-168 是開發 portlet 應用程序的 Java Community Process 標准,它描述了 portlet 生命周期管理、portlet 容器合約、打包、部署以及與門戶有關的其他方面。
異步 JavaScript + XML(或者叫做 Ajax)是一項用於開發豐富、交互的 Web 應用程序的技術。Ajax 組合了 XML、HTML、DHTML、JavaScript 和 DOM。
Portlet 和 Ajax 看起來彼此之間是完美搭配,因為它們都側重於用 Web 浏覽器作為向用戶呈現用戶界面的工具。把這兩者與 Java 技術組合在一起的簡易方式就是使用 DWR 庫。DWR 是 Apache 許可下的開放源碼 Java 庫,用於構建基於 Ajax 的 Web 應用程序。DWR 的基本目的是向開發人員隱藏 Ajax 的細節。您在服務器端使用普通 Java 對象(POJO),而 DWR 動態地生成 JavaScript 代理函數,所以使用 JavaScript 的客戶端開發感覺起來就像直接調用 JavaBean。DWR 的主要組件是一個 Java servlet,處理從浏覽器到服務器的調用。
本文使用 DWR、基於三個 portlet 來構建一個示例 Ajax 應用程序。我將介紹如何把 DWR 與 porlet 應用程序集成,但是我不想深入 DWR 的幕後工作細節;在這個項目的 Web 站點和 developerWorks 的頁面上(請參閱 參考資料 獲得細節)可以找到關於這個庫的更多信息。要構建我描述的應用程序,需要 1.3 或以後版本的 Java 平台和符合 JSR-168 規范的的門戶環境。我用來開發和測試這個代碼的環境包含 IBM Rational Application Developer V6.0、Apache Jetspeed 2.0 portal 和 Java 5.0。
在開始之前,還應當熟悉 portlet 和 Ajax 開發。如果願意學習關於這些主題的更多內容,請參閱下面的 參考資料 小節。可以從 下載 小節下載示例應用程序的完整代碼,包括 DWR。
構建示例的 portlet 間通信應用程序
我們的示例應用程序有三個 portlet:Orders、Order Details 和 Customer Details;圖 1 顯示了示例應用程序:
Orders portlet 有一個訂單列表。當用戶點擊訂單號時,該 portlet 把訂單號發送給 Order Details 和 Customer Details portlet,這兩者然後顯示適當的訂單和客戶細節。
設置開發環境
在可以開發 portlet 之前,需要設置開發環境和 DWR。我使用 IBM Rational Application Developer V6.0,它對 Java portlet 開發具有內置的支持,但是其他開發環境也可以。按以下步驟開始:
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<display-name>DWR Servlet</display-name>
<servlet-class>uk.ltd.getahead.dwr.DWRServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
<!DOCTYPE dwr PUBLIC
"-//GetAhead Limited//DTD Direct Web Remoting 1.0//EN"
"http://www.getahead.ltd.uk/dwr/dwr10.dtd">
<dwr>
<allow>
<create creator="new" javascript="MessagingBean">
<param name="class" value="msg.MessagingBean"/>
</create>
</allow>
</dwr>
現在可以在 portlet 中使用 DWR 了。但是在創建示例 portlet 之前,需要創建一個消息 bean 和一個偽裝數據庫 bean(充當示例應用程序的後端)。
創建 MockupDB 和 MessagingBean
清單 3 所示的 MockupDB
是個單體類,模擬客戶訂單的數據庫。所有訂單都硬編碼在這個類中。真實應用程序可能使用關系數據庫系統,但這個示例對我們的目的來說足夠了。
package db;
import java.util.Hashtable;
import java.util.Map;
public class MockupDB {
private static MockupDB instance=new MockupDB();
private String[] orders=new String[4];
private Map orderDetails=new Hashtable();
private Map customerDetails=new Hashtable();
private MockupDB()
{
String ordStart="ORD";
orders[0]=ordStart+"000408015";
orders[1]=ordStart+"001600023";
orders[2]=ordStart+"000042000";
orders[3]=ordStart+"011235813";
orderDetails.put(orders[0],"1. WebSphere Everyplace Connection Manager<br/>"+
"2. WebSphere Portal");
orderDetails.put(orders[1],"1. DB2 Universal Database<br/>2. DB2 Everyplace");
orderDetails.put(orders[2],"1. Tivoli Access Manager for e-business <br/>2."+
"Tivoli Directory Integrator");
orderDetails.put(orders[3],"1. IBM System z9<br/>2. IBM System p5 550 Express");
customerDetails.put(orders[0],"<b>Systems and Technology Group</b><br/>"+
"Some Road<br/>Finland");
customerDetails.put(orders[1],"<b>Global Financing</b><br/>Another Street"+
"<br/>Finland");
customerDetails.put(orders[2],"<b>Software</b><br/>Yet Another Road"+
"<br/>Finland");
customerDetails.put(orders[3],"<b>Global Services</b><br/>Still Another "+
"Street<br/>Finland");
}
public static MockupDB getInstance()
{
return instance;
}
public String[] getOrders()
{
return orders;
}
public String getOrderDetails(String orderNro)
{
return (String)orderDetails.get(orderNro);
}
public String getCustomerDetails(String orderNro)
{
return (String)customerDetails.get(orderNro);
}
}
清單 4 所示的 MessagingBean
是個簡單的 POJO,有兩個方法,都接受訂單號,但是分別返回訂單細節和客戶細節。MessagingBean
從 MockupDB
得到細節。
package msg;
import javax.servlet.http.HttpSession;
import db.MockupDB;
public class MessagingBean {
public MessagingBean()
{
}
public String getOrderDetails(String orderNumber,HttpSession httpSession)
{
String orderDetails=MockupDB.getInstance().getOrderDetails(orderNumber)
httpSession.setAttribute("orderDetailsOrderNumber",orderNumber);
httpSession.setAttribute("orderDetails",orderDetails);
return orderDetails;
}
public String getCustomerDetails(String orderNumber,HttpSession httpSession)
{
String customerDetails=MockupDB.getInstance().getCustomerDetails(orderNumber);
httpSession.setAttribute("customerDetailsOrderNumber",orderNumber);
httpSession.setAttribute("customerDetails",customerDetails);
return customerDetails;
}
}
MessagingBean
還把訂單細節和客戶細節添加到 HttpSession
。
javaScriptFunctions.jsp
javaScriptFunctions.jsp 導入了來自 DWR 的 JavaScript 庫(engine.js)並動態地創建庫 MessagingBean.js。注意,MessagingBean.js 使用的名稱與 dwr.xml(清單 2)中的 JavaBean 的名稱相同;實際上,DWR 生成 MessagingBean.js。DWR 框架使用 engine.js 庫;作為開發人員,通常不需要考慮直接使用它。
如清單 5 所示,sendOrderNr() 函數調用 清單 4 中定義的 MessagingBean 函數。DWR 自動把 HttpSession 添加到方法調用。JavaScript 函數中的最後一個參數是 callback 函數。在稍後創建的 portlet JSP 中,包含這個 JSP。
<%@ page contentType="text/html"
import="java.util.*,javax.portlet.*,interportletmessagingusingajax.*" %>
<%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<portlet:defineObjects/>
<SCRIPT type="text/javascript"
src='<%= renderResponse.encodeURL(renderRequest.getContextPath() +
"/dwr/interface/MessagingBean.js") %>'>
</SCRIPT>
<SCRIPT type="text/javascript"
src='<%= renderResponse.encodeURL(renderRequest.getContextPath() +
"/dwr/engine.js") %>'>
</SCRIPT>
<SCRIPT type="text/javascript">
function <portlet:namespace />sendOrderNr(orderNr)
{
document.getElementById("orderDetailsOrderNumber").innerHTML=orderNr;
document.getElementById("customerDetailsOrderNumber").innerHTML=orderNr;
MessagingBean.getOrderDetails(orderNr,<portlet:namespace />showOrderDetails);
MessagingBean.getCustomerDetails(orderNr,<portlet:namespace />showCustomerDetails);
return false;
}
function <portlet:namespace />showOrderDetails(orderDetails)
{
document.getElementById("orderDetails").innerHTML=orderDetails;
return false;
}
function <portlet:namespace />showCustomerDetails(customerDetails)
{
document.getElementById("customerDetails").innerHTML=customerDetails;
return false;
}
</SCRIPT>
創建 portlet
現在有了後端和代理函數,可以開發 portlet 本身了。所有三個 portlet 都使用相同的代碼基;惟一的區別是每個 portlet 使用的 JSP 的名稱。
package interportletmessagingusingajax;
import java.io.*;
import javax.portlet.*;
public class Orders extends GenericPortlet {
// JSP folder name
public static final String JSP_FOLDER = "/interportletmessagingusingajax/jsp/";
// JSP file name to be rendered on the view mode
public static final String VIEW_JSP = "OrdersView";
public void init(PortletConfig config) throws PortletException{
super.init(config);
}
public void doView(RenderRequest request, RenderResponse response)
throws PortletException, IOException {
// Set the MIME type for the render response
response.setContentType(request.getResponseContentType());
// Invoke the JSP to render
PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(
getJspFilePath(request, VIEW_JSP));
rd.include(request,response);
//this is workaround for portletsession sharing between
//servlets and portlets
//see http://weblogs.java.net/blog/wholder/archive/2005/02/session_session.html
//and http://mail-archives.apache.org/mod_mbox/portals-pluto-dev/200502.mbox/%3Ca
//2519328f3ba1d1eddfc33c924b6805d@umich.edu%3E
//
PortletRequestDispatcher rd2 = getPortletContext().getRequestDispatcher("/dwr/");
rd2.include(request, response);
}
private static String getJspFilePath(RenderRequest request, String jspFile) {
String markup = request.getProperty("wps.markup");
if( markup == null )
markup = getMarkup(request.getResponseContentType());
return JSP_FOLDER+markup+"/"+jspFile+"."+getJspExtension(markup);
}
private static String getMarkup(String contentType) {
if( "text/vnd.wap.wml".equals(contentType) )
return "wml";
return "html";
}
private static String getJspExtension(String markupName) {
return "jsp";
}
}
<%@ page contentType="text/html"
import="java.util.*,javax.portlet.*,interportletmessagingusingajax.*" %>
<%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<portlet:defineObjects/>
<jsp:include page="javascriptFunctions.jsp" />
<DIV style="margin: 6px">
<H4 style="margin-bottom: 3px">Orders</H4>
<table cellspacing="0" cellpadding="5" border="1">
<% db.MockupDB database= db.MockupDB.getInstance();
String[] orders=database.getOrders();
for(int i=0;i<orders.length;i++)
{
%>
<tr>
<td><%="000000000"+String.valueOf(i+1) %></td>
<td><a href="" onclick="return <portlet:namespace />sendOrderNr('<%=
orders[i]%>');"><%=orders[i]%></a></td>
</tr>
<%
}
%>
</table>
</DIV>
VIEW_JSP
變量的值改成 OrdersDetailsPortletView.jsp
。這個 JSP 的代碼如清單 8 所示: <%@ page contentType="text/html"
import="java.util.*,javax.portlet.*,interportletmessagingusingajax.*" %>
<%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<portlet:defineObjects/>
<DIV style="margin: 6px">
<H4 style="margin-bottom: 3px">Order details</H4>
<table cellspacing="0" cellpadding="5" border="1">
<tr>
<th>Order number</th>
<th>Order details</th>
</tr>
<tr>
<%
String orderDetailsOrderNumber=(String)renderRequest.getPortletSession().getAttribute(
"orderDetailsOrderNumber",PortletSession.APPLICATION_SCOPE);
String orderDetails=(String)renderRequest.getPortletSession().getAttribute(
"orderDetails",PortletSession.APPLICATION_SCOPE);
if(orderDetailsOrderNumber==null)
{
orderDetailsOrderNumber="";
}
if(orderDetails==null)
{
orderDetails="";
}
%>
<td><div id="orderDetailsOrderNumber"><%=orderDetailsOrderNumber%>
</div></td>
<td><div id="orderDetails"><%=orderDetails%></div></td>
</tr>
</table>
</DIV>
VIEW_JSP
變量的值改成 CustomerDetailsPortletView.jsp
。這個 JSP 的代碼如清單 9 所示: <%@ page contentType="text/html"
import="java.util.*,javax.portlet.*,interportletmessagingusingajax.*" %>
<%@taglib uri="http://java.sun.com/portlet" prefix="portlet" %>
<portlet:defineObjects/>
<%
%>
<DIV style="margin: 6px">
<H4 style="margin-bottom: 3px">Customer details</H4>
<table cellspacing="0" cellpadding="5" border="1">
<tr>
<th>Order number</th>
<th>Customer details</th>
</tr>
<tr>
<%
String customerDetailsOrderNumber=
(String)renderRequest.getPortletSession().getAttribute(
"customerDetailsOrderNumber",PortletSession.APPLICATION_SCOPE);
String customerDetails=(String)renderRequest.getPortletSession().getAttribute(
"customerDetails", PortletSession.APPLICATION_SCOPE);
if(customerDetailsOrderNumber==null)
{
customerDetailsOrderNumber="";
}
if(customerDetails==null)
{
customerDetails="";
}
%>
<td><div id="customerDetailsOrderNumber"><%=customerDetailsOrderNumber%>
</div></td>
<td><div id="customerDetails"><%=customerDetails%></div></td>
</tr>
</table>
</DIV>
示例應用程序現在准備好了。下一步是把 portlet 打包成 WAR 文件並在 Apache Jetspeed 門戶中測試它。
測試示例應用程序
在這一節,將看到示例應用程序的作用。首先,創建 portlet WAR 並把它安裝到 Jetspeed 門戶。然後,把三個 portlet 添加到門戶,看它們是如何工作的。將把它們全都構建到一個頁面,但如果需要也可以把它們放到多個頁面;幕後的機制仍然起作用。
把 portlet 應用程序安裝到 Jetspeed
把 portlet WAR 文件安裝到 Jetspeed 的方法是把 WAR 文件拷貝到 <Jetspeed install dir>/webapps/jetspeed/WEB-INF/deploy 目錄。然後 Jetspeed 會自動安裝 portlet,portlet 即可使用了。
使用以下步驟把新頁面添加到 Jetspeed 門戶:
Portlet
Orders portlet 如圖 4 所示,列出訂單:
圖 4. Orders portlet
在點擊訂單號時,其他 portlet 顯示這個訂單的細節。Customer Details portlet 顯示客戶信息,如圖 5 所示。信息檢索自 MockupDB
。
圖 5. Customer Details portlet
Order Details portlet 也顯示檢索自 MockupDB
的信息,如圖 6 所示:
圖 6. Order Details portlet
如果喜歡,可以回過去,向不同的頁面添加一個或多個 portlet。將會看到,portlet 不需要在單個頁面上,因為 portlet 內容保存在用戶會話中。
結束語
這篇文章介紹了用 Ajax 實現 portlet 間通信的一種方式。Ajax 是開發交互式 Web 頁面的一種非常強大的技術,而支持 Ajax 的 portlet 通過消除門戶中典型存在的請求-響應延遲,極大地改善了用戶體驗。
可以用本文中的代碼作為開發您自己的應用程序的起點;文中的代碼還顯示了 DWR 如何把 Java 編程模型擴展到 Web 浏覽器。使用 DWR,JavaBean 幾乎就像是在浏覽器中可用一樣。DWR 幾乎隱藏了 Ajax 的全部細節,讓您可以專注於手頭的工作,而不必考慮 Ajax 開發的具體細節,從而簡化了工作。
下載
參考資料
學習關於作者
Sami Salkosuo 從 1999 年起一直在 IBM 工作。他的主要興趣領域是 Java 編程,是 Sun 認證 Java 程序員,IBM 認證的 XML 和相關技術解決方案開發人員,IBM 認證的 IBM WebSphere Portal 解決方案開發人員。除了 Java 技術,他還有 Python、Fortran、LabVIEW、Visual Basic、LISP、Perl 和 PHP 的經驗。