级别: 中级 Stefan Hepper (sthepper@de.ibm.com), WebSphere Portal 编程模型架构师, IBM Stephan Hesmer (stephan.hesmer@de.ibm.com), WebSphere Portal Portlet 运行时架构师, IBM
2005 年 8 月 31 日 学习如何在 JSR 168 Portlet 中缓存数据,以便免除不必要的后端请求。首先,您将看到,如何通过 IBM WebSphere Application Server dynacache 基础设施存储缓存数据。接下来,将看到如何为在 Web 应用程序的所有组件中共享的数据(具有会话范围)生成缓存键。然后,您将了解第二个缓存键生成技术,它可以满足缓存专用于 Portlet 窗口的数据的要求。示例书签 Portlet 对这两种缓存技术进行了演示。本文是为熟悉 Java™ Portlet API 且可以在 WebSphere Portal 创建和部署 Portlet 的 Java Portlet 编程人员准备的。请参阅参考资料以获得指向有助于您掌握这些技能的信息的链接。
引言
Portlet 是基于 Java 的 Web 组件,它可以处理请求以及生成动态内容。该 Portlet 生成的内容称为片段,此片段是遵循某些规则的一块标记(如 HTML、XHTML 和 WML)。该片段还可以与其他的片段集中在一起,以构成一个完整的文档,称之为门户页面。
为了生成该标记,用于 Portlet 的一种极其常见的模式调用会访问后端系统,以检索数据(如客户信息或股票报价)。该数据将通过 Portlet 标记呈现给最终用户。通常对后端系统的访问会有一些延迟,并且不会经常更改。在这些情况下,在 Portlet 中缓存后端数据可以向最终用户提供更佳的响应时间,以及整个门户的更好的伸缩性。
JSR 168 Java Portlet V1.0 规范没有定义任何缓存 API;专家组将缓存视为一个更加广泛的 J2EE 主题,而非一个特定于 Portlet 的问题。当前 J2EE 没有定义这样的缓存 API;因此,您必须将供应商特定 API 用于缓存。
在本文中,我们介绍了如何使用 IBM™ WebSphere™ Application Server dynacache 缓存基础设施,以及如何为两个用例生成缓存 ID:
- 缓存数据是特定于用户会话的,能够在 Portlet 之间共享
- 缓存数据是特定于 Portlet 窗口的,不应该共享
最后,我们在一个书签示例 Portlet 中使用了这两种缓存策略。本文和这些示例适用于 IBM WebSphere Portal V5.1(或更高版本),以及 IBM WebSphere Application server V5.1(或更高版本)。
示例简介
可以下载书签示例的示例代码,在阅读下文时作为参考。有关该示例的详细说明,请参阅示例:使用缓存的书签 Portlet。
比较缓存与 Portlet 会话
由于 JSR 168 Portlet API 中没有缓存 API,因此许多 Portlet 开发人员将 Portlet 会话用于缓存数据。最初,这似乎很合适,因为 Portlet API 提供了两个不同的范围:应用程序范围或 Portlet 窗口范围。因此,您会认为通过会话可以满足引言中描述的两个用例。
然而,通过会话存储与缓存相关的数据具有某些严重的缺陷。会话和缓存数据在以下方面有所不同:
- 会话数据具有用户会话的生存期,是基于用户执行的操作(将转换为 Portlet 中的 processAction 调用)创建的。Portlet 应该只在操作阶段修改会话数据;呈现阶段各数据的状态是一样的;也就是说,Portlet 不应该更改任何数据的状态。如果原始集群节点出现问题(故障转移情况),则会话数据还必须复制到其他的集群节点。会话数据所适合的示例场景是 Internet 站点的购物车。当您登录到网站时,向购物车添加项目,然后确认内容并提交以用于处理。
- 可以在任何时间重新创建缓存数据。存储该数据是为了优化性能,因为存储原始数据的后端系统可能很慢,或者与后端系统的连接很慢或者不稳定。缓存数据所适合的示例是客户记录,包括存储在后端系统的客户地址。
因此,实现不执行任何操作的缓存存储是一个有效的实现,因为缓存的客户机通常能够(并且通常必须需要)从后端系统重新提取数据。但会话实现并非如此。如果所选的项都没有存储,则购物体验将会让您灰心丧气(除了可能会给您节省一些钱之外)。
在使用不同的数据存储时,这些本质区别还会带来某些影响。如果将会话用于存储缓存数据,则您需要考虑下列不利条件:
因此,到目前为止,我们希望已说服您将真正的缓存 API 用于缓存数据,而不是使用会话,它将向您提供性能和可伸缩性方面的真正好处。现在,让我们看一下,如何访问 WebSphere Application Server 附带的缓存 API。
利用 WebSphere dynacache 基础设施
在 WebSphere Application Server 内有不同特点的 dynacache 基础设施。对于这种情况,我们感兴趣的是动态缓存服务。您可以将它用于在集群中以分布式方式存储对象。我们将其用于存储以前可能存储在会话中的对象。
首先,您需要启用动态缓存服务。在 Websphere Application Server 管理控制台中:
- 导航到 Servers => Application servers => server_name => Container services => Dynamic cache service。
- 在启动时,选择“Enable Service”启用服务。
- 应用更改。
- 重新启动服务器。
图 1 显示了该屏幕在启用动态缓存服务后的外观。
图 1:启用的动态缓存服务
WebSphere Application Server 提供的 Java API 称为 DistributedMap 和 DistributedObjectCache,用于以分布式方式存储对象。可以通过在任何 J2EE 应用程序或系统组件的缓存中存储指向对象的引用,使用这些接口缓存和共享 Java 对象。
当启用动态缓存并重新启动服务器时(如上所述),WebSphere Application Server 将创建一个缺省的动态缓存实例。该缺省实例绑定到使用名称 services/cache/distributedmap 的全局 Java
命名和目录接口(Java Naming and Directory Interface,JNDI)名称空间。
分布式映射的一个最大的优点就是 J2EE 应用程序开发人员可以决定动态地创建新实例。多个实例的这一功能使您能够根据需要单独配置缓存实例。DistributedMap 接口的每一个实例都具有其自己的属性,可以使用对象缓存实例设置设置这些属性。可以通过编程 API 或者管理控制台 Resources > Cache instances > Object cache instances 进行设置。要获得更多的可伸缩性,请使用我们在本文中用来创建缓存实例的编程 API。有关详细信息,请参阅 WebSphere Application Server Information Center。
要使用编程 API,在 J2EE 应用程序的类路径中创建一个名为 cachinstances.properties 的属性文件;例如,
/classes/cacheinstance.properties。
清单 1 显示了书签 Portlet 示例的文件的格式。
清单 1. 书签 Portlet cachinstances.properties 文件
cache.instance.0=/services/cache/bookmark/statistics
cache.instance.0.cacheSize=1000
cache.instance.0.enableDiskOffload=false
cache.instance.0.flushToDiskOnStop=false
cache.instance.0.useListenerContext=true
cache.instance.0.enableCacheReplication=true
cache.instance.0.replicationDomain=DynaCacheCluster
cache.instance.0.disableDependencyId=true
|
然后,在 Java 代码中,可以访问如下所示的分布式映射。
InitialContext ic = new InitialContext();
DistributedMap map = (DistributedMap)ic.lookup
("services/cache/bookmark/statistics");
|
通过用户会话缓存数据
既然您已经了解了如何使用 dynacache 基础设施缓存数据,现在让我们看一下如何在 Portlet 中生成正确的缓存键。在这一部分中,您将看到如何生成为每一个用户会话存储数据所用的键。在本例中,数据在与用户交互的所有组件中共享。
对于通过用户会话进行缓存,有许多用例,它们通常基于对同一后端数据进行操作的不同组件。因此,只一次获取数据,对数据进行缓存,并让所有其他组件使用该缓存的数据,而非再次访问后端系统,这是很有益的。共享缓存数据不仅适用于一个 Portlet 应用程序内的 Portlet,而且还包括其他 Portlet 应用程序,以及主题和皮肤。此共享是可能的,因为 WebSphere Application Server 将同一会话 ID 分配到与特定的用户连接的所有 Web 应用程序会话。
为了生成可供各个项目重用的缓存键,可以将键生成分类。在本例中,该类称为 PortletObjectCacheHelper,它包含一个方法 getKey(),可以使用两个 Portlet 会话范围——应用程序或 Portlet ——调用此方法。正如使用应用程序范围在 Portlet 会话中存储数据,以便它对于 Portlet 应用程序的其他组件是可见的一样,您也可以使用应用程序范围生成当前用户在组件中共享的键。清单 2 显示了如何实现会话范围的键的键生成。
清单 2:根据会话 ID 生成特定于会话的缓存键
public static String getKey(String key, PortletSession session, int scope) {
if (scope == PortletSession.APPLICATION_SCOPE) {
return getKey(key, session.getId());
} else ...
|
getKey 方法构造了一个缓存键,它包括自定义应用程序键和会话 ID。清单 2 显示了一个通过字符串串联创建此类键的示例算法。
清单 3:用于在不同的键和 ID 中生成一个缓存键的字符串串联
public static String getKey(String key, String sessionId) {
return getKey(key, sessionId, null);
}
public static String getKey(String key, String sessionId,
String windowId) {
final StringBuffer _key = new StringBuffer(key.length() + 50);
_key.append(sessionId);
_key.append('.'); // not required, added to show the key contruction
if (windowId != null) {
_key.append(windowId);
_key.append('.'); // not required
}
_key.append(key);
return _key.toString();
}
|
既然已经了解了如何创建基于会话的缓存键,在下一部分中,我们看一下如何生成 Portlet 窗口的唯一缓存。
通过 Portlet 窗口缓存数据
第二个用例涉及到了只与某个 Portlet 实例有关的缓存数据,并且数据是特定于 Portlet 窗口的。因此,您可以将股票报价 Portlet 放在页面两次,以显示不同的股票报价,同一 Portlet 的两个不同实例将创建两个单独的缓存条目,这样一个实例的缓存条目就不会覆盖另一个的缓存条目。
然而,创建 Portlet 窗口的唯一缓存 ID 并不像在第一个用例中使用会话 ID 那样简单,因为 Java Portlet 规范没有提供访问该 ID 的任何直接的方式。在 JSR 168 API 中有两种方式可以获取 Portlet 窗口缓存 ID:
- RenderResponse 中的 getNamespace()
- 会话名称空间
getNamespace() 方法可以向您提供每一个 Portlet 窗口的唯一 ID,因此,看上去是一个理想的解决方案。然而,该解决方案本身有两个问题:
- getNamespace() 方法只在呈现阶段可用。
无法在操作阶段或任何会话侦听器中访问 ID。
- Java Portlet 规范只能保证在一个请求中此值保持不变。
WebSphere Portal 超出了这一要求,可以保证 getNamespace 值在当前用户会话中保持不变。
使用 getNamespace 值的优点是,它是一个简单的 API 调用且非常高效。如果您可以接受只在呈现阶段访问缓存的数据,我们建议您使用 getNamespace 方法。从编程模型的观点来看,它是首选的解决方案。通常只需要在操作阶段写入后端系统,然后在呈现阶段从后端系统进行读取。这一模式还可以帮助您免除后端系统使用事务时引起的任何死锁。
另一种方式是,对于存储在 Portlet 范围的 Portlet 会话中的键,利用 Java Portlet 规范的已定义的名称空间机制。该规范要求在键的前面添加特定 Portlet 窗口的唯一 ID 作为前缀。
清单 4 中的键生成示例通过向 Portlet 范围写入已知键提取前缀 ID,然后从未加前缀的应用程序范围的 Portlet 会话中读取所有的值并搜索已知键。因为规范描述了应该如何添加前缀,我们可以再次提取前缀,并且拥有唯一的 Portlet 窗口 ID。您只需访问 Portlet 会话就可以获取该 ID,因此可以在操作阶段、呈现阶段以及会话侦听器中访问。该方法的缺点是需要向会话写入内容,您必须拥有一个会话。此写入操作还会有一些性能损失。
清单 4:使用 Java Portlet Specification 中定义的 Portlet 范围会话前缀生成特定于 Portlet 窗口的缓存键
final private static String CACHE_CONST = "__cache__";
final private static int len1 = "javax.portlet.".length();
final private static int len2 = CACHE_CONST.length();
public static String getKey(String key, PortletSession session,
int scope) {
if ...
} else if (scope == PortletSession.PORTLET_SCOPE) {
synchronized (session) {
String id = null;
session.setAttribute(CACHE_CONST, "");
final Enumeration enum =
session.getAttributeNames(PortletSession.APPLICATION_SCOPE);
while (enum.hasMoreElements()) {
final String name = (String)enum.nextElement();
if (name.indexOf(CACHE_CONST)>=0) {
id = name.substring(len1, name.length()-len1-len2);
break;
}
}
session.removeAttribute(CACHE_CONST);
return getKey(key, session.getId(), id);
}
} |
现在您可以为会话和 Portlet 窗口范围生成不同的缓存键。接下来,我们看一个示例,它使用了这两种键生成机制,并且通过 dynacache 基础设施存储数据。
示例:使用缓存的书签 Portlet
下载部分中的示例书签 Portlet 展示了如何在 Portlet 中使用缓存功能。虽然它是一个非常简单的 Portlet,但应用程序的某些部分对使用缓存很有帮助,例如不需要永久保留的重新创建的信息或非持久信息。
在本例中,我们创建了一个 Statistics 类,用于向用户展示他或她多长时间添加、修改或删除一次书签。该信息可用于私有会话和应用程序会话。统计信息示例并不适合于缓存,因为数据不是可重新创建的,而这一点对于缓存数据来说是必需的。我们选择该示例是为了使示例代码简单,并把注意力集中在如何使用缓存。在现实世界中,更好的应用程序示例通常缓存书签指向的页面的标题或图标。
Portlet 在视图的下面添加了统计信息部分,如图 2 所示。
图 2. 书签 Portlet 包括用户统计信息
Statistics 类由两部分组成。第一部分定义了两个静态方法,它们是处理缓存的一般访问方法。一个方法检索由 HTTP 会话确定范围的统计信息,另一个方法由 Portlet 会话确定范围。下面的清单显示了相关代码:
清单 5:定义访问方法的 Statistics 类
public static Statistics getApplicationStatistics(PortletSession session)
{
return getStatistics(PortletObjectCacheHelper.getKey(
STATISTICS,session,PortletSession.APPLICATION_SCOPE));
}
public static Statistics getPrivateStatistics(PortletSession session)
{
return getStatistics(PortletObjectCacheHelper.getKey(
STATISTICS,session,PortletSession.PORTLET_SCOPE));
}
private synchronized static Statistics getStatistics(String key)
{
Statistics stat = (Statistics)cache.get(key);
if (stat==null)
{
stat = new Statistics();
cache.put(key,stat);
}
return stat;
}
|
第二部分是一个非常简单的统计信息实现,为添加、修改和删除增加了计数器。
清单 6:实现简单统计信息的 Statistics 类
private int countModifications = 0;
private int countAdditions = 0;
private int countDeletions = 0;
public Statistics()
{
}
public void incModifications()
{
countModifications++;
}
public void incAdditions()
{
countAdditions++;
}
public void incDeletions()
{
countDeletions++;
}
public int getModifications()
{
return countModifications;
}
public int getAdditions()
{
return countAdditions;
}
public int getDeletions()
{
return countDeletions;
}
|
结束语
Java Portlet V1.0 规范中没有定义如何缓存数据;因此,您需要利用供应商对此功能的扩展。我们展示了如何使用 WebSphere Application Server dynacache 基础设施在 Portlet 中缓存数据。还说明了如何为两个用例会话范围和 Portlet 窗口范围生成缓存键。
生成会话范围缓存键很简单,开销也很低,因为会话向您提供了可以使用的特定 ID。对于一个用户来说,WebSphere Application Server 使该会话 ID 在 Web 应用程序中保持一致;因此,缓存内容可以在应用程序中共享。
缓存键是 Portlet 专用的,因而在 Portlet 窗口的范围内有效,它不容易生成,因为在 Portlet API 中没有显式 Portlet 窗口 ID。如果您只需要在呈现阶段访问缓存,则最简便的方法就是使用响应中的 getNamespace 方法。对于所有其他情况,可以使用我们提供的 Helper 类生成 ID,它将定义的会话前缀用于存储在 Portlet 会话的 Portlet 范围中的条目。
示例书签 Portlet 展示了如何使用不同的缓存键范围,以及如何使用 WebSphere dynacache 基础设施存储数据。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| Code samples | bookmarkportlet.zip | 11 KB |
FTP | HTTP |
|---|
参考资料 学习
获得产品和技术
作者简介  | 
|  | Stefan Hepper 是负责 WebSphere Portal 编程模型和公用 API 的架构师。他是 Java Portlet 规范 JSR 168 的两位规范领导者之一。他还在 Apache 启动了 Pluto 项目,该项目提供了 JSR 168 的参考实现。Stefan 在国际会议上做过许多演说(如 JavaOne),发表过多篇论文,并且是 Pervasive Computing (Addison-Wesley 2001) 一书的合著者。他的研究兴趣是基于组件的软件体系结构、普及基础设施,当然还有门户和 Portlet。Stefan 从德国的卡尔斯鲁厄大学 (University of Karlsruhe) 获得了计算机科学学位,并在 1998 年加入了 IBM Boeblingen 开发实验室。
|
 | 
|  | Stephan Hesmer 是负责 WebSphere Portal 中的 Portlet 运行时的架构师。还负责将 WebSphere Portal 与其基本产品 WebSphere Application Server 相集成。Stephan 从事 JSR 168 Java Portlet 规范方面的研究,并设计和实现了 JSR 168 参考实现的最初版本,Pluto。Stephan 在 2000 年获得了德国斯图加特
联合教育大学 (University of Cooperative Education Stuttgart) 的信息技术学位,毕业后,他加入了 IBM Boeblingen 开发实验室,在 WebSphere Portal 团队工作。 |
对本文的评价
|