Stefan Hepper
架构师,Websphere Portal Server Development, IBM 2004 年 2 月
本文详细说明了 Java
TMJava
TMStandardization Request for the Java Portlet 规范(以下称为 JSR 168)和 IBM WebSphere Portal Version 5.0 支持的 IBM Portlet API之间的相似点和不同点。文中解释了 JSR 168 的一些基本概念和特征,并将它们与 IBM Portlet API 进行了比较,并且为两个 Portlet 编程接口中的每一个都提供了一个示例 Portlet。本文是为那些对 Java 编程、Servlet 或Portlet 编程有良好理解并具备 portal 方面的基本知识的 Portlet 开发人员和 Portal 架构师准备的。
引言 随 着越来越多的企业Portal 的出现,不同的厂商已经创建了各种用于 Portal 组件的 API(称为 Portlet)。存在多种不兼容的接口产生了应用程序提供者、Portal 消费者和Portal 服务器厂商之间的问题。Java Community Process(JCP)定义的Java Portlet 规范(JSR 168)提供了 Portlet 和 Portal之间的互操作性的标准。它将及时解决这些问题。 本文档的目的是比较 JSR 168 和 WebSpherePortal V5.0 Portlet API,并且给出了一些示例。 重要:为了给顾客和合作伙伴提供一个方便的移植窗口,WebSphere Portal的下一个版本仍将支持 IBM Portlet API。 JSR168 简介 Java Standardization Request 168(JSR 168)定义了一个Portlet规范,包括 Portlet 容器和 portlet 之间的合约。JSR 168 是由Java Community Process(JCP)定义的。JSR 168 是由 IBM 和 Sun共同领导的,并且有一个很大的 Expect Group以帮助创建目前可用的最终版本。这个专家组由 Apache Software Foundation、Art Technology Group Inc.(ATG)、BEA、Boeing、Borland、Citrix Systems、Fujitsu、Hitachi、IBM、Novell、Oracle、SAP、SAS Institute、Sun、Sybase、Tibco和 Vignette 组成。关于这个 JSR 的更多信息可以在 http://jcp.org/en/jsr/detail?id=168上找到。 重要:在本分析中提供的信息不承担任何性质的保证之责。本分析旨在为读者提供一个指示性的工具。 JSR 168定义 这部分提供了基本的 JSR 168定义,目的是帮助您理解 JSR 168 如何融入整个 Portal体系结构中。 Portal 和 Portlet 容器 portal是一个 Web应用程序,它通常提供不同来源的个性化、单点登录的内容集合,并且托管不同后端系统的表示层。 Portal的主要任务是将不同的应用程序集合到一个页面,这个页面的外观是Portal 用户共有的。Portal也可以有复杂的个性化特征,这些特征能够给用户提供自定义内容。Portal页可以有不同的 Portlet 集,以便为不同的用户创建内容。 图1展 示了 Portal 的体系结构,比如一个由 WebSphere Portal 提供服务的Portal。客户端请求由 PortalWeb 应用程序进行处理,它为当前用户检索当前页上的 Portlet。然后,Portal Web应用程序为每个 Portlet 调用 Portlet 容器来通过 Container Invoker API 检索它的内容。Portlet 容器通过 Portlet API 调用Portlet。Container Provider Service Provider Interface(SPI)允许 portlet 容器通过Portal 检索信息。 图 1. 基本的 Portal 体系结构 Portlet 容器运行 Portlet,给它们提供所需的运行时环境,并且管理它们的生命周期。它为Portlet 首选项项提供持久性存储,这使得能够为不同的用户生成自定义输出。 Portal 页和 Portlet 图2展 示了基本的 Portal 页组件。Portal 页表示一个完整的标记文档并且聚集若干 Portlet 窗口;也就是说,它将不同的应用程序用户界面组合到一个统一的表示中去。Portal 页使用户能够通过登录对话框向 Portal 验证自己的身份以便访问个性化的 Portal 视图。大部分 Portal 页包括一些导航机制以允许用户导航到其他的 Portal 页。 图 2:Portal 页 Portlet 窗口包括:
- 标题栏,带有 Portlet 的标题
- 修饰,包括用于更改 Portlet 的窗口状态的按钮(比如最大化或最小化 Portlet)和用于更改 Portlet 的模式的按钮(比如显示帮助或编辑预定义的 Portlet 设置)
- 由 Portlet 产生的内容(也称为一个标记段)。
图 3在不同的浏览器上展示了一个 Portlet 窗口。如您所见,该 portlet 产生的标记段并不局限于 HTML,而可以是任何标记。 图 3:在 HTML 浏览器(顶部)和 WML 浏览器(底部)中显示的 Portlet Portlet 生命周期 基本的 portlet 生命周期必须:
- 初始化,使用初始化类初始化 Portlet 并将其放入服务中。
- 处理请求,处理不同种类的动作并呈现内容。
- 完成,使用销毁类除去 Portlet。
Portlet 基于用户与 Portlet 或 Portal页的交互来接收请求。请求处理分为两个阶段:
- 动作处理
如果用户单击 Portlet上的链接,就会触发动作。动作处理必须在页面上的任何Portlet 呈现开始之前结束。在动作阶段,Portlet可以改变 Portlet 的状态。 - 呈现内容
在呈现阶段,Portlet 会产生要发送回客户端的标记。呈现不应该改变任何状态。它允许一个页面重新刷新而不需要改变Portlet 的状态。一个页面上的所有 Portlet的呈现都可以并行执行。 图 4描述从客户端到 Portlet的请求流,并且更详细地展示了动作和呈现阶段。在这个示例中,Portlet A接收了一个动作。在该动作执行之后,页面(A、B、C)上的所有Portlet 的呈现方法都被调用。 图 4. 从客户端到 Portlet 的请求流 Portlet 模式 Portlet 执行不同的任务并根据它们的当前函数来创建内容。Portlet模式指示 Portlet 何时将执行函数。Portlet模式指定 Portlet应该执行哪一类任务和应该生成什么样的内容。当调用 Portlet时,Portlet 容器提供当前对 Portlet 的请求的模式。在处理动作请求时,Portlet可以程序化地改变它们的 Portlet 模式。 JSR 168 定义了三类 Portlet 模式:
- 必须支持的模式(语义同上)
Edit
-
显示一个或多个视图以让用户自定义个性化的 Portletsettings.Help
Help
-
显示帮助视图。
View
-
显示 Portlet 输出。
- 可选择的客户模式
About
-
显示 Portlet 的目的、来源、版本和其他信息。
Config
-
显示一个或多个配置视图以让管理员配置对所有用户有效的Portlet 设置。
Edit_defaults
-
设置可修改的首选项的缺省值,这些首选项通常可以在EDIT 屏幕中进行更改。
Preview
-
呈现输出而不需要后端连接或特定于用户的可用数据。
Print
-
显示适于打印的视图。
- 特定于 Portal 厂商的模式
这些模式只适用于特定的厂商Portal。 窗口状态 窗 口状态是分配给 Portlet 生成的 Portal 页面空间的指示器。Portlet容器提供当前的窗口状态给 Portlet,而 Portlet 通过窗口状态来决定它应该呈现多少信息。然而,在处理动作请求时,Portlet也可以程序化地改变它们的窗口状态。 JSR 168 定义了如下窗口状态:
Normal
-
Portlet 与其他 Portlet共享空间,在产生它的输出时应该对考虑这种状态。
Maximized
-
与处于正常的窗口状态相比,窗口有更真实的状态来提供它的输出。
Minimized
-
Portlet 应该只呈现最小的输出或没有输出。
除了这些窗口状态之外,JSR 168 允许 Portal 定义自定义窗口状态。 数据模式 JSR 168为 Portlet定义了不同的机制以访问瞬态数据和持久性数据。 Portlet 可以设置和获取下列作用域内的瞬态数据:
请求 | 请求有附加的数据,比如请求参数和属性,与Servlet类似。请求可以包含一些特性,以允许进行扩展;也可以包含从Portal 传送到 Portlet 的客户端头字段(反之亦然)。 | 会话 | Portlet可以将数据存储在具有全局作用域的会话中,以让 Web应用程序中的其他组件访问这些数据;也可以将数据存储在Portlet作用域中,这个作用域是 Portlet 私有的。 | 上下文 | Portlet可以将数据存储在 Web 应用程序上下文中,与 Servlet类似。 | Portlet 可以访问这些作用域内的持久性数据:
每个 Portlet | Portlet可以将配置和个人数据存储在 Portlet引用中,以允许 Portlet 创建个性化的输出。Portlet可以定义允许用户在编辑模式下更改那些数据(例如股票报价)以及哪些数据是只能由管理员在配置模式下进行更改的配置设置(例如股票报价服务器)。 | 每个用户 | Portlet可以读取用户概要信息来针对用户调整它的输出(例如显示用户所在城市的天气情况)。 | Portlet 应用程序 所有资源、Portlet 和配置描述符一起打包成一个 Web应用程序档案文件(WAR)。有两个配置描述符:
- 所有不是 Portlet 的 Web 应用程序资源必须在 web.xml配置描述符中指定。
- 所有 Portlet 和 Portlet 相关的设置必须在 portlet.xmldeployment 描述符中指定。
比较 JSR 168 与 IBM Portlet API 这一部分大概地比较了 JSR 168 Portlet API 与 IBM Portlet API。首先讲解了一些相似的概念,接着说明了两者之间的不同之处。 相似点 下列概念在 JSR 168和 IBM Portlet API 中是非常相似的。
特征 | 相似点 | 不同点 | Portlet 模式 | 两者都支持基本的 Portlet 模式:Edit、Help 和 View。 | 这种配置模式在 JSR 168中是可选的。IBM Portlet API 不支持其他可选的 JSR 168模式(About、Edit_defaults、Preview、Print)。 | 窗口状态 | 支持如下窗口状态:Maximized、Normal和 Minimized。 | Solo 窗口状态只有 IBM Portlet API支持。 | Portlet 生命周期 | 生命周期是相同的:初始化、处理请求、毁坏。 | 没有 | 请求处理 | 请求处理分为处理用户动作的动作阶段和产生标记的呈现阶段。 | 没有 | URL 编码 | 两者都支持创建指向 Portlet或资源的 URL。 | 没有 | 包含 Servlet/JSP | Servlet 和 JSP 可以包括在 Portlet中。 | 没有 | Portlet 会话 | Portlet可以存储瞬态信息,这些信息应该都在会话中的请求内。 | 没有 | Portlet 应用程序打包 | 两者都通过附加的部署描述符(名为 portlet.xml )来将 Portlet 应用程序打包成 WAR文件。 | portlet.xml 格式有所不同。 | 基于到期时间的缓存 | Portlet可以支持基于到期时间的缓存。 | API使用不同的机制来实现这一功能。 IBM Portlet API使用轮询机制,在这种机制中, Portal 查询 Portlet以获取标记的有效时间,而在 JSR 168 中,Portlet可以将到期时间附加到创建的每个标记上。在用户之间共享缓存条目只有在IBM Portlet API 中才是可能的。 | 不同点 JSR168 和 IBM Portlet API 在以下几个方面有所不同。
特征 | IBM Portlet API | JSR 168 | Portlet 应用程序实体 | 使您可以通过部署描述符将抽象的Portlet 应用程序及其不同的实例定义为具体的 Portlet应用程序。这允许重用抽象的 Portlet应用程序的设置,而只重写对于每个具体的 Portlet应用程序惟一的部分。 | 该部署描述符紧跟在 web.xml 部署描述符之后,它定义一个应用程序及其 Portlet定义。 | Portlet 实体 | Web 部署描述符中的每个 Portlet配置都有一个 Portlet 对象实例。根据 Flyweight模式,可以有许多 PortletSettings 对象来参数化相同的 Portlet对象(基于每个请求)。 PortletSettings 中的改变会应用到这个具体的 Portlet 中的所有 Portlet实例。用户也可以具体 Portlet的个人视图,它是通过使用自定义输出的 PortletData 呈现的。 | PortletSettings 和 PortletData 合并成一个名为 PortletPreferences 的对象。 | 请求/响应对象 | Portlet在呈现调用中接收到的请求/响应对象与动作调用接收到的相同。 | 在 JSR 168中,有两个不同的对象。 | JSR 168 独有的特征 这些项只适用于 JSR 168。
特征 | 描述 | 呈现参数 | 呈现参数允许 Portlet存储它的导航状态。 呈现参数在随后的呈现请求中保持不变,而只是在Portlet接收到一个新的动作时才发生改变。这启用了书签功能,并且解决了浏览器后退按钮问题。 | 全局 HttpSession 作用域 | Portlets 不仅可以存储具有 Portlet可见性的数据,而且可以存储具有整个 Web 应用程序可见性的数据。 | 重定向 | Portlet可以重定向到动作阶段中的其他 Web 资源。 | IBM Portlet API 独有的特征 以下概念只适用于 IBM?Portlet API。
特征 | 描述 | 事件 | 事件可以在?Portlet之间发送。 | 附加的生命周期侦听器 | 除了动作和呈现之外,生命周期侦听器(比如首页)也不适用于 JSR168 的第一个版本。 | Portlet 菜单 | 让 Portlet可以提供内容到菜单栏,以便通过 Portal页使导航更加容易。 | 基于无效时间的缓存 | 让 Portlet可以显式使缓存的内容无效。 | 示例 Portlet 这一部分展示一个 HelloWorld Portlet,在两个 API的每一个中都有它的实现。其主要的不同之处突出显示在第二个Portlet 中。 这两个示例 Portlet 实现了相同的功能:
- 将 JSP 用于呈现输出
- 根据特定于用户的数据自定义 Portlet 输出
- 处理动作以允许用户改变这些动作
这个示例展示了一个个性化的 HelloWorld Portlet。它通过名称识别用户,并且允许用户进入编辑模式以设置一个新的名称。在 action方法(在 IBM Portlet API 中称为 actionPerformed ,而在 JSR 168中称为 processAction )中,Portlet持久化存储用户输入的新用户名。 清单 1. IBM Portlet API 示例 Portlet 首先看一看通过 IBM Portlet API 实现的 Portlet。
public class HelloWorld extends AbstractPortlet implements ActionListener { public void doView (PortletRequest request, PortletResponse response) throws PortletException, IOException { //Get the user's name to display from persistent storage PortletData portletData = request.getData(); String stringToDisplay=(String)portletData.getAttribute("userName");
if (stringToDisplay == null) { stringToDisplay = defaultString; // set default string }
// Add the display string to the portlet request to make it // accessible by the view JSP request.setAttribute("userName", stringToDisplay);
// Get a context for the current session for invoking the JSP PortletContext context = getPortletConfig().getContext(); context.include(viewJSP, request, response); }
public void doEdit(PortletRequest portletRequest, PortletResponse portletResponse ) throws PortletException, IOException { // Create the cancel return URI for the edit page PortletURI cancelURI = portletResponse.createReturnURI(); // Preserve the Cancel URI in the request to make it // accessible by the edit JSP portletRequest.setAttribute("cancelURI", cancelURI.toString());
// Create the save URI for the edit page PortletURI saveURI = portletResponse.createReturnURI(); // For the "Save" button the return URI must include the "Save" action // so the Action Listener for this portlet will be invoked SimpleActionHelper. addSimplePortletAction(getPortletConfig().getContext(), saveURI, "save");
// Preserve the Save URI in the request to make it accessible by // the edit JSP portletRequest.setAttribute("saveURI", saveURI.toString());
//Get the user's name to display from persistent storage String stringToDisplay = (String) portletRequest.getData().getAttribute("userName"); if (stringToDisplay == null) { stringToDisplay = defaultString; // none found, set default string }
// Add the display string to the request to make it accessible by the // edit JSP as an inital value of the input field on the edit form portletRequest.setAttribute("userName", stringToDisplay);
// Get a context for the current session for invoking the JSP PortletContext context = getPortletConfig().getContext(); context.include(editJSP, portletRequest, portletResponse); } public void actionPerformed(ActionEvent event) { String action = SimpleActionHelper.getActionString(getPortletConfig().getContext(), event); HelloWorld helloPortlet = (HelloWorld)event.getPortlet(); PortletLog log = helloPortlet.getPortletLog();
// If this is a save action, then see if the user specified a name if ( action!=null ) { if ( action.equals("save") ) { PortletRequest request = event.getRequest(); PortletData portData = request.getData(); String userName = request.getParameter("userName"); try { // Save the name specified by the user if ( userName != null ) { portData.setAttribute("userName", userName); portData.store(); } } catch ( AccessDeniedException ade ) { } catch ( IOException ioe ) { log.error( " Couldn't write the user date to persistence because an I/O Error occurred. " ); } } } } }
| 清单 2. JSR 示例Portlet 现在,看一看已修改为使用 JSR 168 的同一 Portlet。更改过的文本用粗体标记。
public class HelloWorld extends GenericPortlet { public void doView ( RenderRequest request, RenderResponse response) throws PortletException, IOException { //Get the user's name to display from persistent storage Portlet Preferences portletData = request.get Preferences(); String stringToDisplay = (String) portletData.get Value("userName", defaultString);
// Add the display string to the portlet request to make it // accessible by the view JSP request.setAttribute("userName", stringToDisplay);
// Get a context for the current session for invoking the JSP PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(viewJSP); rd.include(request, response);
} public void doEdit( RenderRequest portletRequest, RenderResponse portletResponse ) throws PortletException, IOException { // Create the cancel URI for the edit page PortletUR L cancelURI = portletResponse.create ActionUR L();
// Preserve the Cancel URI in the request to make it // accessible by the edit JSP portletRequest.setAttribute("cancelURI", cancelURI.toString());
// Create the save URI for the edit page PortletUR L saveURI = portletResponse.create ActionUR L();
// For the "Save" button the return URI must include the "Save" action // so the Action Listener for this portlet will be invoked saveURI.setParameter("action", "save");
// Preserve the Save URI in the request to make it accessible by // the edit JSP portletRequest.setAttribute("saveURI", saveURI.toString());
//Get the user's name to display from persistent storage String stringToDisplay = portletRequest.get Preferences().get Value("userName", defaultString);
// Add the display string to the request to make it accessible by the edit JSP // as an inital value of the input field on the edit form portletRequest.setAttribute("userName", stringToDisplay);
// Get a context for the current session for invoking the JSP PortletRequestDispatcher rd = getPortletContext().getRequestDispatcher(editJSP); rd.include(portletRequest, portletResponse); }
public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException { String action = request.getParameter("action");
// If this is a save action, then see if the user specified a name if ( action!=null ) { if ( action.equals("save") ) { Portlet Preferences portData = request.get Preferences(); String userName = request.getParameter("userName"); // Save the name specified by the user if ( userName != null ) { portData.setAttribute("userName", userName); portData.store(); } }
| IBM Portlet API 版本的 Portlet 的主要区别在于:
- 请求分配器使用
JSR 168 和 IBM Portlet API之间的请求分配器使用略有不同。JSR 168使用相同的机制以从上下文中取得一个请求分配器作为 servlet API 的参数。JSR168使用相同的机制从带有路径的上下文中获取请求分配器,以作为与Servlet API 一样的参数。 - 持久性 Portlet 数据
JSR 168 的持久性数据 PortletPreferences 有一个类似于 JDK 1.4 Preferences 的API,并且允许为 get方法指定缺省值。不再需要检查零返回值并且显式设置缺省值的代码。JSR168 只允许 String 和 String 数组作为首选项的值;IBMPortlet API 可以支持任意对象。 - 动作处理
在 JSR 168 中的动作处理被简化,并且不需要创建特定的动作对象被创建。自动创建动作 URL会触发调用 processAction 方法。 结束语 由于 JSR 168 的定义,最终有了一个标准的 Portlet API,它提供独立于 Portal 厂商的编程 Portlet,并且使您可以在不同的 Portal 上不加改变地运行相同的 Portlet。 JSR 168 的概念与 IBM Portlet API 有密切的联系。与 IBM Portlet API 相比,JSR 168的功能是有限的,因为它是 Portlet 规范的 第一个版本。熟悉 IBM Portlet API 的开发人员应该能够毫无困难地尽快学会使用JSR 168 API。 参考资料
关于作者
Stefan Hepper 是位于德国的 IBM Boeblingen 实验室 WebSphere Portal 组的一名软件架构师。它负责 Java 标准并且是 JSR 168 的两位规范领导者之一。他在国际会议上做过演说,出版过论文,申请过专利并且是 Pervasing Computing |
|