本文假定读者熟知 JSR 168 Portlet,并对 J2EE 基本常识有一定了解。本文主要以理论的方式向读者介绍 JSR 286 Portlet 的以下新增特性:
- 资源服务
- 事件
- 共享呈现参数
- Portlet 过滤器
- Portlet 窗口
|
Portlet 是部署在容器内用来生成动态内容的 Web 组件,与 servlet 类似,portlet 的整个生命周期从 init 到 destroy 的过程都在 portlet 容器中进行。Java Portlet Specification 对 portlet API、 标准化用户数据、参数设置、portlet 请求以及响应、部署、打包以及安全等方面都做了详细的规定,以此来实现 portlet 之间以及 portlet 与 portlet 容器之间的交互和协作。 Java Portlet Specification 1.0, 即 Java Specification Request(JSR)168 发布于 2003 年 10 月。
|
JSR 168 目前在业界受到广泛支持,而且它由开放源码支持。标准和产品的第一个版本存在一定的缺陷,仅支持最基本的用例,在功能上有一些限制。而且 Java Portlet Specification V1.0 也存在这种情况,因此,经过三年之后,大多数支持 Java Portlet Specification V1.0 的门户产品都提供一些附加扩展,以支持更高级的用例,这些附加的扩展造成了各个门户产品的标准不统一,彼此间的交互协作成了不可避免的问题。为了更好地规范 portlet 开发,以适应业界发展,并提供适应于最高级别用例的标准解决方案,从而为这些高级功能提供互操作性,在 2005 年 11 月开始了 Java Portlet Specification V2.0(称为 JSR 286)的开发,Java Portlet Specification V2.0 目前已经进入 Final draft 的等待审批阶段,并计划在 2008 年 3 月正式发布。JSR 286 最终草案兼容了 JSR 168,并完善了 JSR 168 的部分功能,并提供了诸多 JSR 168 所没有的新特性,例如资源服务、事件、portlet 过滤器、共享呈现参数及 portlet 窗口等。与 V1.0 类似,V2.0 也将基于 J2EE 1.4,因此可让 Portlet 使用 J2EE 1.4 增强(如 JSP 2.0)。下面是该新规范的一些主要功能及特性:
- 资源服务:一种新的通过 portlet 呈现资源的方式。
- 事件:通过发送事件和接收事件来实现 portlet 之间的通信。
- Portlet 过滤器:与 servlet 过滤器类似,根据 Portlet 请求和响应动态的呈现内容的变换。存在以下四种类型的 portlet 过滤器:
- Action 过滤器
- Render 过滤器
- Resource 过滤器
- Event 过滤器
- 共享呈现参数:除了 portlet 私有的呈现参数之外,新增了可以在 portlet 之间共享的呈现参数。
- Portlet 窗口:提供 portlet 窗口 ID 供 portlet 使用。
下面我们将对 JSR 286 所提供的这些新功能及其使用逐一做详细介绍。
|
在 JSR 168 中,Portlet 服务于资源的方法只有两种:直接链接到资源,或者通过 Portlet 服务于资源。两种方法分别适用于不同目的的需要,各有优缺点。
直接链接对于所有 Portlet 状态都相同的静态资源非常有效,但对于其他用例效果却不太好,因为需要考虑来自 Portlet 上下文的信息。这样的示例包括基于 Portlet 模式、窗口状态、当前呈现参数或 Portlet 首选项呈现不同资源。
以一个 JSP 文件 test.jsp 为例,如果要访问该资源,可以直接通过超链接访问该文件,如清单 1 所示:
清单 1. 直接访问资源文件
<a href="<c:url value="/test.jsp" />">test.jsp</a> |
或者通过 Servlet 转向,如清单 2 和清单 3 所示:
清单 2. 直接访问 Servlet
<a href="<c:url value="/testServlet" />">testServlet</a> |
清单 3. Servlet 对资源文件的访问控制
public void service(ServletRequest request, ServletResponse response) throws ServletException,IOException { ... 在此添加访问控制等业务逻辑代码 ... RequestDispatcher rd = request.getRequestDispatcher("/WEB-INF/jsp/test.jsp"); rd.forward(request, response); //或者为 rd.include(request, response); } |
从清单 1、2、3 可以看到,直接链接到资源这种方式,无法访问到相关 Portlet 的信息,包括 Portlet 模式、窗口状态、当前呈现参数或 Portlet 首选项等。而通过 Portlet 呈现资源的优势是可以通过门户访问资源,因此可以通过控制门户访问而对资源提供保护。但是,这也带来了额外的门户请求开销,加重了门户服务器的负载。
为了更好的解决这两种方法的局限性,JSR 286 采用了一种新的资源服务方式 —— Portlet 资源服务。即 JSR 286 引入了一个新的具有 serveResource
方法的可选生命周期接口 ResourceServingPortlet
,该接口可以由 ResourceURL 触发,Portlet 可以通过 PortletResponse.createResourceURL
方法创建它。资源 URL 包含当前 Portlet 的瞬时状态(Portlet 模式、窗口状态和呈现参数),但不能为此状态设置新值。资源 URL 可以有在资源 URL 上设置的其他资源参数。
通过调用 ResourceServingPortlet
接口的 serveResource()
方法, Portlet 不仅可以通过控制门户访问而对资源进行保护,并且 Portlet 容器不会呈现任何除 serveResource()
方法返回的内容之外的附加输出。这样,用户由于可以直接通过操作响应对象而被赋予了更多的控制权限,并且没有额外门户请求的开销,减轻了门户服务的负载。而 Portal 服务器此时只是充当了一个代理服务器的作用。
JSR 286 资源服务的使用方法:
- Portlet 类需实现
javax.portlet.Portlet
和javax.portlet.ResourceServingPortlet
接口并实现serveResource()
方法, 如 清单 4 所示:
清单 4. Portlet 对资源文件的访问控制
public class TestPortlet implements Portlet, ResourceServingPortlet { ...... public void serveResource(ResourceRequest resourceRequest, ResourceResponse resourceResponse) throws PortletException, IOException { ... 在此添加访问控制等业务逻辑代码 ... PortletRequestDispatcher portletRequestDispatcher = portletConfig .getPortletContext().getRequestDispatcher( "/WEB-INF/jsp/TestPortletResource.jsp"); portletRequestDispatcher.include(resourceRequest, resourceResponse); } ...... }
- 使用 JSP 标签通过
PortletResponse.createResourceURL
方法创建 RecourceURL:
清单 5. 创建资源访问 URL
<a href="<portlet:resourceURL/>">Click me to request Resource URL</a>
- 所保护的访问资源,在此例中即为 TestPortletResource.jsp 。
接下来,我们就可以充分体验 JSR 286 资源服务新特性所带来的简单便捷以及高性能了。
对照该介绍,读者可参照本系列第 2 部分对资源服务特性的实例开发加深对该部分相关内容的理解。
|
JSR 286 定义的事件模型是一种松耦合的代理事件模型。在此模型中,Portlet 定义可以接收以及在 Portlet 部署描述符中公布的事件。在运行时,门户管理员(或业务用户)可以将不同的 Portlet 连接在一起。
Portlet 事件服务并不是一个可信任消息服务(例如 JMS)的替代。很多情况下 Portlet 事件并不能总是保证能够传送到目的地。因此 Portlet 必须能够在部分或即使所有事件都不能正确接收的情况下仍然能够工作。
另外,有的时候 Portlet 为了响应某一个事件,也会向另外的 Portlet 发布新的事件,这样就形成了事件的衍生代。这在一定程度上可能造成事件的死锁,JSR 286 本身没有对衍生代做出限制,但是很多 Portlet 容器会定义事件的最大衍生代以防止死锁的发生。读者在开发相关应用时请注意其本身的限制。
对于一个事件的声明包括三个部分,分别是事件的定义声明、事件的发布载体声明也就是发布该事件的 Portlet 声明、事件接收载体的 Portlet 声明。
- 事件定义声明:我们需要在 portlet.xml 中使用 <event-definition> 元素对事件进行声明,并且该元素与 <portlet> 元素并列作为 <portlent-app> 的子元素。
清单 6. event-definition 声明
<portlet-app id="myPortletApp" version="2.0"> <portlet> ... </portlet> <event-definition> . . .</event-definition> ... </portlet-app>
对于一个事件的声明有两点需要注意:事件的名称和值的类型。对于事件名称,JSR 286 既可以为事件定义默认的命名空间,其作用域为所有未声明 QName 的事件;也可以为事件单独定义自己的 QName。对于 QName 和命名空间的理解,请读者参考 XML 规范的相关文档,本文不做详细介绍。对于事件值的类型,既可以是简单的 Java 对象,例如 Integer,String 等,也可以是预先定义的 Java 复杂对象,但是前提是该对象必须实现 Serializable 接口。其中 <event-definition> 的具体格式如 清单 7 和 清单 8 所示:
清单 7. 默认命名空间下事件定义声明
<default-namespace>http://cn.ibm.com/</default-namespace> ...... <event-definition> <name>event-with-simple-value</name> <value-type>java.lang.String</value-type> </event-definition> <event-definition> <name>event-with-complex-value</name> <value-type>com.ibm.jsr286.TestEventBean</value-type> </event-definition>
清单 8. 自定义 QName 下事件定义声明
<event-definition> <qname xmlns:key="http://www.ibm.com">event-with-simple-value</qname> <value-type>java.lang.Integer</value-type> </event-definition> <event-definition> <qname xmlns:key="http://www.ibm.com">event-with-qname</qname> <value-type>com.ibm.jsr286..TestEventBean</value-type> </event-definition>
- 事件发布载体声明:事件的发布载体声明需要在 portlet.xml 的 <portlet> 元素中用 <supported-publishing-event> 关键字。对应事件声明格式,事件发布载体 Portlet 声明亦有默认命名空间和自定义命名空间以及简单对象和复杂对象的情况,见示例:
清单 9. 默认命名空间下事件发布声明
<supported-publishing-event> <name>event-with-simple-value</name> </supported-publishing-event> <supported-publishing-event> <name>event-with-complex-value</name> </supported-publishing-event>
清单 10. 自定义 QName 下事件发布声明
<supported-publishing-event> <qname xmlns:key="http://www.ibm.com">event-with-simple-value</qname> </supported-publishing-event> <supported-publishing-event> <qname xmlns:key="http://www.ibm.com">event-with-complex-value</qname> </supported-publishing-event>
- 事件接收载体声明与事件发布载体声明类似,事件的接收载体声明需要在 portlet.xml 的 <portlet> 元素中用 <supported-processing-event> 关键字。见示例清单 11 和 清单 12:
清单 11. 默认命名空间下事件接收载体声明
<supported-processing-event> <name>event-with-simple-value</name> </supported-processing-event> <supported-processing-event> <name>event-with-complex-value</name> </supported-processing-event>
清单 12: 自定义 QName 下事件接收载体声明
<supported-processing-event> <qname xmlns:key="http://www.ibm.com">event-with-simple-value</qname> </supported-processing-event> <supported-processing-event> <qname xmlns:key="http://www.ibm.com">event-with-complex-value</qname> </supported-processing-event>
- 事件发布:我们可以在希望发布事件的 Portlet 的
processAction()
方法里,通过调用 ActionResponse 的setEvent()
进行事件发布,setEvent()
方法的输入参数为事件的名称和对应的值,这些参数必须与我们前面在 portlet.xml 中的事件声明一致。
清单 13. 事件发布
public class TestSenderPortlet implements Portlet { ...... public void processAction(ActionRequest request, ActionResponse response) throws PortletException, IOException { ...... response.setEvent(eventName, eventObject); ...... } ...... }
- 事件接收:事件的接收 Portlet 必须实现
javax.Portlet.EventPortlet
接口,事件的接收处理则在该接口包含processEvent()
方法中进行,JSR 286 定义该方法提供了两个输入参数:EventRequest
和EventResponse
,我们可以通过调用 EventRequest 实例的getEvent()
方法来获得当前事件,该方法返回一个事件对象的实例,该实例封装了事件的唯一标识和对应的值。
清单 14: 事件接收
public class TestReceiverPortlet implements Portlet,EventPortlet { ...... public void processEvent(EventRequest request, EventResponse response) throws PortletException, IOException { ...... Event event = request.getEvent(); ...... } ...... }
- 事件处理:获得事件对象后,我们也可以通过
getQNames()
方法或者getName()
获得事件的名称。两种获得事件方法的区别是getQNames()
可以得到事件的全称标识,而getName()
只是取得本地标识名。而取得事件的值则可以通过事件的getValue()
方法获得。
清单 15: 事件处理
...... public void processEvent(EventRequest request, EventResponse response) throws PortletException, IOException { ...... Event event = request.getEvent(); String name = event.getName();// 或者 String qname = event.getQNames(); String value = event.getValue(); ...... } ......
在 JSR 168 Portlet 的开发中,开发者通常继承抽象类 javax.portlet.GenericPortlet
来实现自己的 Portlet 逻辑代码。 同 JSR 168 相比, JSR 286 的 GenericPortlet 增加了 javax.portlet.EventPortlet
和 javax.portlet.ResourceServingPortlet
接口的实现,从而增加了事件处理和资源服务的功能。读者可以从 类图 1 和 类图 2 看出 JSR 286 GenericPortlet API 的变化。
图 1. JSR 168 GenericPortlet 类图
图 2. JSR 286 GenericPortlet 类图
|
我们先来看一看共享呈现参数的官方介绍:共享呈现用于在 Portlet 之间共享呈现参数,从而创建一个页面上下文。共享呈现参数在 portlet.xml 文件中声明。如果这些参数不是空值,Portlet 将自动接收它们。与非共享呈现参数相比,如果应该更改值,Portlet 仅需要设置共享呈现参数。
共享呈现参数,顾名思义,就是指 Portlet 之间共享参数,每一个 Portlet 对该参数的修改都能够直接被另外支持该参数 Portlet 所获得。共享呈现参数与 JSR 168 中已经有私有呈现参数的区别就在于,私有呈现参数只为 Portlet 内部使用,而共享呈现参数则为多个 Portlet之间通信协作而设置。共享呈现参数与事件相比的优势就在于避免了事件处理过程调用的繁琐。
我们举一个简单的事例来说明共享呈现参数的优点。假如我们开发了一个关于天气的 Portlet, 这个 Portlet 可以根据选择的城市来显示该城市的天气情况。我们为这个 Portlet 定制了邮政编码这个共有呈现参数来表示用户选择的城市。这样,如果我们再开发一个也有这个共享呈现参数的 Portlet,例如显示该城市地图或者旅游信息的 Portlet。在这种情况下,当我们修改天气 Portlet 所选择的城市的话,地图以及旅游信息的 Portlet 也会自动做出相应变化。从而实现了不同 Portlet 之间的协作。
对共享呈现参数的使用声明包括两个部分,对共享呈现参数定义的声明和支持共享呈现参数的 Portlet 声明。
- 共享呈现参数定义声明:对于共享呈现参数定义的声明必须在 portlet.xml 部署文件中使用 <public-render-parameter> 关键字,该元素与 <portlet> 元素并列为 <portlet-app> 的分支。
清单 16. 共享呈现参数定义声明
<portlet-app ...> <portlet> <portlet-name>Portlet A</portlet-name> ... </portlet> ... <public-render-parameter> <identifier>public-render-param1</identifier> </public-render-parameter> <public-render-parameter> <identifier>public-render-param2</identifier> </public-render-parameter> </portlet-app>
- 支持共享呈现参数 Portlet 声明:对于支持共享呈现参数的 Portlet 的声明需要在 portlet.xml 中 <portlet> 元素中使用 <supported-public-render-parameter> 关键字。
清单 17. 共享呈现参数 Portlet 定义声明
<portlet> <portlet-name>Portlet B</portle-name> ...... <supported-public-render-parameter> <identifier>public-render-param1</identifier> </supported-public-render-parameter> </portlet> <portlet> <portlet-name>Portlet C</portle-name> ...... <supported-public-render-parameter> <identifier>public-render-param2</identifier> </supported-public-render-parameter> </portlet>
与非共享呈现参数的使用方法相同,共享呈现参数可以通过 ActionResponse 的 setRenderParameter("标识","值")
方法设定,并通过 RenderRequest 的 getParameter("标识")
来获得。见 清单 18 和 清单 19
清单 18. Portlet A 设定共享呈现参数
... public void processAction(ActionRequest actionRequest, ActionResponse actionResponse) throws PortletException, IOException { String publicRenderParamValue1 = actionRequest.getParameter("public-render-param1"); actionResponse.setRenderParameter("public-render-param1", publicRenderParamValue1); } ... |
清单 19. Portlet B 获取共享呈现参数
... public void render(RenderRequest renderRequest, renderResponse renderResponse) throws PortletException, IOException { ... String publicRenderParamValue1 = renderRequest.getParameter("public-render-param1"); ... } ...... |
|
Portlet 过滤器是 JSR 286 提供的有一个非常重要的新特性。事实上,在 JSR 286 之前,就已经有很多厂商(包括 IBM)自定义扩展了 JSR 168,提供了过滤器功能。由此可见,Portlet 过滤器的重要性。为了避免各种厂商不同 Portlet 过滤器的不兼容性,JCP(Java Community Process)对 JSR 286 定义了标准的过滤器实现。
与 Servlet 相似,Portlet 过滤器可以使用户可以改变一个 request 和修改一个 response。Filter 不是一个 Portlet,它不能产生一个 response,它能够在一个 request 到达 Portlet 之前预处理 request,也可以在离开 Portlet 时处理 response。换句话说,过滤器其实是一个“Portlet chaining”(Portlet 链)。它能够实现的功能包括:
- 在 Portlet 被调用之前截获;
- 在 Portlet 被调用之前检查 servlet request;
- 根据需要修改 request 头和 request 数据;
- 根据需要修改 response 头和 response 数据;
- 在 Portlet 被调用之后截获;
事实上,从宏观功能的角度看来,Portlet 过滤器和 Servlet 过滤器是很相似的。这是因为二者都可以声明性地嵌入,从而截获并修改请求和响应。但是理解它们之间存在着很大的不同是非常重要的。在一定程度上,它们之间的差异是与 Servlet 和Portlet 之间的差异相联系的:Servlet 过滤器是一个门户级过滤器,它可以修改由一些小的部分(来自页面上所有 Portlet 的响应)集合而成的整个门户页面;而 Portlet 过滤器只能用于那些小的部分。Servlet 过滤器(如果已经安装的话)是接收和修改客户端请求的第一个组件,同时也是修改对客户端的响应的最后一个组件(请参见图 3)。
图 3. 带有 Servlet 过滤器和 Portlet 过滤器的客户端请求事件序列
如图 3 所示,Servlet 请求(步骤 1)在分派给一个或多个 Portlet 请求(步骤 3)之前首先通过一个 Servlet 过滤器链进行处理(步骤 2)。在结果集聚到一起(步骤 6 和 7)之前,Portlet 请求进一步转发到 Portlet 过滤器链进行处理(步骤 4)。接着,将集聚的结果发送回 Servlet 过滤器进行处理,之后,将集聚的结果最终显示给用户(步骤 9)。
另外一点需要注意的是,Servlet 过滤器比 Portlet 过滤器的优先级别要高,容器将首先进行 Servlet 过滤,其次是 Portlet 过滤。一个过滤器链包含一个或多个过滤器。在一个过滤器完成处理之后,新的请求和响应将传送到链上的下一个过滤器;链上的最后一个过滤器调用目标资源(Servlet 或 Portlet)。
Portlet 过滤器可以放置在 V2.0 规范提供的任何生命周期方法调用的前面或者后面(processAction、processEvent、render、serveResource),而且还支持这些生命周期方法使用包装的请求和响应。与 Servlet 过滤器类似,Portlet 过滤器需要在 portlet.xml 部署描述符中进行定义,不同的是,servlet只有一个service()的请求处理方法,因此servlet只有一种类型的过滤器。而portlet却有四种请求处理方法,于是有四种类型的过滤器,包括:
- Action 过滤器
- Render 过滤器
- Resource 过滤器
- Event 过滤器
下面我们来介绍四种过滤器的工作原理。
JSR 286 为 Portlet 过滤器提供了接口类 PortletFilter,该接口提供了两个方法。
- void init(FilterConfig config)
容器调用一次这个方法来准备用于服务的过滤器。对象 filterConfig 使得过滤器能够访问配置参数以及对门户上下文的引用。 - void destroy()
这个方法是在将过滤器从服务移除之后调用的。这个方法使得过滤器能够清除任何存放的资源。
图 4. Portlet 过滤器继承图
四种过滤器分别对 Portlet 的四个方法进行拦截。用户自定义的过滤器必须实现相应的过滤器接口,通过其 doFilter() 方法来实现相应的动作。其对应关系见表 1:
表 1. 过滤器与拦截方法关系图
过滤器 | 拦截方法 |
---|---|
Action 过滤器 | processAction(ActionRequest request, ActionResponse response) |
Render 过滤器 | render(RenderRequest request, RenderResponse response) |
Resource 过滤器 | serveResource(ResourceRequest request, ResourceResponse response) |
Event 过滤器 | processEvent(EventRequest request, EventResponse response) |
下面我们通过一个具体的 Portlet 过滤器部署实例来说明。参见清单 20:
清单 20. Portlet 过滤器声明
<portlet-app ...> ... <filter> <filter-name>TestRenderFilter</filter-name> <filter-class>com.ibm.jsr286.TestRenderFilter</filter-class> <lifecycle>RENDER_PHASE</lifecycle> </filter> <filter> <filter-name>TestAllFilter</filter-name> <filter-class>com.ibm.jsr286.TestAllFilter</filter-class> <lifecycle>ACTION_PHASE</lifecycle> <lifecycle>RENDER_PHASE</lifecycle> <lifecycle>EVENT_PHASE</lifecycle> <lifecycle>RESOURCE_PHASE</lifecycle> </filter> <filter-mapping> <filter-name>TestRenderFilter</filter-name> <portlet-name>DocumentPortlet</portlet-name> </filter-mapping> <filter-mapping> <filter-name>TestAllFilter</filter-name> <portlet-name>Test*</portlet-name> </filter-mapping> </portlet-app> |
关于 Portlet 过滤器有几点需要声明的是:
- 对于一个 Portlet 过滤器的声明亦包括两部分,过滤器的定义声明以及过滤器的映射声明。
- 一个 Portlet 过滤器可以为多个 Portlet 服务,而且 一个 Portlet 可以同时有多个 Portlet 过滤器。
- 一个 Portlet 过滤器可以有多个生命周期阶段,当然前提是该 Portlet 过滤器实现了相应过滤器接口。
|
Portlet 窗口在 JSR 168 中仅间接地得到反映,并作为容器为 Portlet 范围的会话条目生成的前缀的一部分。JSR 286 规范现在使该 Portlet 窗口 ID 可通过请求供 Portlet 使用,简单的说就是,PortletRequest 新增了一个方法 getWindowID()
,可以获得 Portlet 的窗口 ID,这个 ID 是由容器生成的。在 Portal 容器中布局同一个 Portlet 多次的情况下,windowID 可以用来区分同一个 Portlet 的不同窗口,从而可以使这些 Portlet 窗口缓存并呈现不同的数据。
Portlet 窗口支持的一个常见用例是在 Portlet 中缓存数据;例如,一个 Portlet,呈现所使用的数据是通过 web services 从远端服务器获得,而且网络调用速度比较慢,且呈现的数据为只读,这种情况下就可以把 web services 获得的数据和窗口 ID 关联并缓存起来,在频繁调用 Portlet 的时候,就不必频繁等待 web services 的返回。
|