为 WebSphere Portal 复合应用程序基础架构构建一个业务组件

本文提供一个关于如何在 IBM® WebSphere® Portal 中为复合应用程序基础架构编写一个业务组件的概述。包括业务组件的一个样例实现,演示如何使用复合应用程序基础构架、以及如何利用最佳实践和避免陷阱。 本文来自于 IBM WebSphere Developer Technical Journal 中文版

Hendrik Haddorp, 开发人员,IBM Boeblingen Development Lab, IBM

Hendrik Haddorp 是 WebSphere Portal 的 Composite Application Infrastructure 技术主管,在德国伯布林根 IBM Research and Development Laboratory 工作。他于 2003 年加入 WebSphere Portal Development 团队。



Izidor Jager, 软件开发人员, IBM

Izidor Jager 是伯布林根 IBM Development Laboratory 的一名软件开发人员。他于 2005 年加入 WebSphere Portal 开发团队。他的主要研究领域是 Composite Application Infrastructure。Izidor 从斯图加特大学获得他的学位证书。



Sven Stueven, 软件开发人员, IBM

Sven Stueven 是伯布林根 IBM Development Laboratory 的一名软件开发人员。他在 Lab based Services 工作 3 年之后,于 2009 年加入 WebSphere Portal 部门。Sven 从卡尔斯鲁厄应用科学大学获得商业信息学学位。



2011 年 1 月 27 日

简介

IBM WebSphere Portal 复合应用程序基础架构(CAI)支持复合应用程序的创建和管理。在 WebSphere Portal 中,一个复合应用程序是由 WebSphere Portal 工件构成,比如页面、 portlets 和社区(community)。社区定义提供访问权限的角色以及属于一个或多个角色的成员。

此外,一个复合应用程序可以包含定制的业务组件。业务组件扩展基础架构并在后端系统提供数据管理。多数时间,prolet 利用一个业务组件处理来自特定后端的数据。例如,在文档库中一个 prolet 可以使用一个业务组件来管理文档。

下面对 CAI 做一介绍,什么 SPI (service provider interfaces) 和 API 将被提供、这些 SPI 如何实现、这些 API 如何使用。一个业务组件如何被部署到 WebSphere Portal,也将做一介绍。

本文包含业务组件实现的一个示例。这个样例业务组件是一个文档库。下文中,文档库指的就是这个样例业务组件。

源代码和样例文档库的 Javadoc 在 下载 部分可见,还有详细介绍该样例的用户操作指南。此外,本文也展示了文档库的功能以及如何利用 CAI 特性。


CAI 接口

要使 CAI 同业务组件进行交互,业务组件代码需要包含一个实现 CAI SPI 的业务组件处理程序类。这使得当某个事件发生时 CAI 可以调用业务组件;例如,如果一个应用程序被序列化到一个模板中,或一个用户添加到应用程序中。

这些是一个业务组件处理程序可以实现的接口:

  • Lifecycle
  • Serializable
  • MembershipListener
  • Sensor
  • DisplayInfo
  • ApplicationListener
  • Variability
  • StreamBackupServiceClient

您在 com.ibm.portal.app.component2 包中可以全部找到它们。最后一个位于 com.ibm.portal.app.service.backup 中。这些接口的 Javadoc 在 /PortalServer/doc/Javadoc/api_docs/ 目录下的 WebSphere Portal 安装文件中。

除了 Lifecycle,所有其他接口都是可选的。业务组件处理程序需要实现哪些接口,取决于业务组件想要继承哪种特性。例如,如果一个业务组件想要集成 CAI 的备份功能,必须实现 Serializable 接口。

每当业务组件不能成功执行一个回调时,最好的方法是抛出一个 com.ibm.portal.app.exceptions.ComponentException。这使得 CAI 可以启动一个适当的活动;例如,删除一个初始化不成功的业务组件。

所有回调方法共有第一个参数作为一个特定业务组件的标识符。因此,一个业务组件处理程序知道具体哪个业务组件实例必须被处理。标识符通常是一个源自 WebSphere Portal 类 com.ibm.portal.ObjectID 的实例,由 CAI 创建。

除了 SPI 之外,CAI 也提供 API。值得注意的是 BusinessComponentService 和 ApplicationService。 BusinessComponentService 可用于在 CAI 中存储一个不能在后端反映的业务组件的数据。而 ApplicationService 可用于获取一个应用程序的其他业务组件的信息。

简而言之,您将看到,样例业务组件如何利用 CAI API。


部署一个 CAI 业务组件

CAI 使用在 IBM WebSphere Application Server 中集成的 Eclipse 扩展点框架来检索一个业务组件的业务组件处理程序。因此,CAI 使用 ID 为 com_ibm_portal_app.BusinessComponents 的插件定义一个扩展点。一个业务组件必须是这个扩展点的一个扩展。

要在扩展点注册一个业务组件,必须提供一个 plugin.xml 文件。文档库的 plugin.xml 文件如清单 1 所示。

清单 1. 文档库的 Plugin.xm
<plugin id="ai.docLib.sample" version="1.0.0">
 <extension point="com_ibm_portal_app.BusinessComponents" id="DocLibHandler">
      <provider class="com.ibm.wps.ai.sample.doclib.handler.DocLibBCHandlerImpl"/>
 </extension>

ID 为 ai.docLib.sample.DocLibHandler 的扩展点定义业务组件中哪个 Java 类是业务组件处理程序类。完全限定类名是 provider 元素 class 属性的值。文档库业务组件处理程序类是 com.ibm.wps.ai.sample.doclib.handler.DocLibBCHandlerImpl。正如之前提到的,CAI 使用这个处理程序同业务组件进行交互。

如果一个业务组件是在扩展框架中注册的,那么这个处理程序可以通过 JNDI 循环访问。将绑定一个 CAI 业务组名称的 JNDI 映射到以下模式:

portal:extreg/<plugin id><extension id>

这个文档库是:portal:extreg/ai.docLib.sample.DocLibSampleHandler。

如果一个 portlet 想要使用一个业务组件,它可以通过提供一个名为 com.ibm.portal.bc.ref 的首选项通知 CAI。首选项以一个 JNDI 名称的形式给出。这使得只要 Portlet 被添加到应用程序,CAI 就可以创建一个业务组件实例。

文档库的 portlet.xml 包含清单 2 所示的首选项。

清单 2. 定义业务组件的 Portlet 首选项
<portlet-preferences>
      <preference>
	    <name>com.ibm.portal.bc.ref</name>
		<value>portal:extreg/ai.docLib.sample.DocLibSampleHandler </value>
		<read-only>true</read-only>
	  </preference>
    <portlet-preferences>

一个绑定 portlet 和业务组件的推荐方法是将它们打包到一个应用程序归档文件中,然后将这个 portlet 应用程序部署到 WebSphere Portal。然后,这个业务组件自动在扩展注册表中注册,可访问 CAI。


一个业务组件的 Lifecycle

CAI 包含一个业务组件容器来管理业务组件的生命周期,考虑一个业务组件生命周期的以下 5 个阶段:

  1. 创建
  2. 激活
  3. 预删除(pre-deleted)
  4. 删除
  5. 后删除(post-deleted)

当一个业务组件被实例化时,首先要创建它,然后激活。该阶段在 Lifecycle 接口方法 Create 和 Activate 中反映出来了:

  • Create 在实例化的初始阶段调用,触发一个新业务组件示例的创建和所需资源的实例化。
  • Activate 在实例化的结尾调用,激活业务组件的使用。

这两个阶段是为了统一创建业务组件的各种方法。业务组件可以在应用程序运行时创建,也可以在应用程序的实例化过程中在一个应用程序模板外部或者一个备份中创建。实例化遵循这种模式:创建一个空业务组件,填入数据,然后激活它。

清单 3 显示了文档库的 Lifecycle 创建方法。该方法仅创建库的根文件夹。

清单 3. 创建业务组件
publicvoid create(ObjectID bcOID, Map<String, Object> initParameters) 
		throws ComponentException {
	final String method = "create";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method, bcOID);
	}
	try {
 	//use the system time to generate a unique id for the folder
		createRootFolder(bcOID, new Folder(Long.toHexString(System.nanoTime()), 
			"doclib", "Root Folder"));
	} catch (PersisterException e) {
		throw new ComponentException(e);
	} catch (DocLibException e) {
		throw new ComponentException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
}

业务组件处理程序必须保留一个具体业务组件标识符的标识符。这个标识符是由 CAI 创建的,在创建方法中作为第一个参数传递。随后,在所有 CAI 回调函数中,这个标识符将被传入来识别哪个具体业务组件实例受到影响。业务组件代码必须能够将这个标识符映射到其后端数据中。文档库在其后端存储序列化业务组件标识符;在相应的表中为其预留一个字段。相比将标识符存储在后端,还可以保存在 CAI 中。BusinessComponentService 可用来存储一个业务组件的数据。

文档库的激活方法在 WebSphere Portal 的门户访问控制(PAC)组件中注册它的角色(清单 4)。正如您所看到的,ApplicationCatalogService 用于获取业务组件所属的复合应用程序的标识符。ApplicationService 用于决定在 PAC 中注册角色时需要哪个社区标识符。

清单 4. 激活业务组件
publicvoid activate(ObjectID bcOID) throws ComponentException {
	final String method = "activate";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method);
	}
	try {
		ObjectID appOID = getAcsHome().getApplicationCatalogService()
			.findApplicationByContainedBC(bcOID);
		ObjectID cmtyOID = getAsHome().getApplicationService(appOID)
			.getCommunityID();
		createDomainObjectWithRoles(cmtyOID, bcOID);
	} catch (DocLibException e) {
		throw new ComponentException(e);
	} catch (ObjectNotFoundException e) {
		throw new ComponentException(e);
	} catch (ApplicationDataException e) {
		throw new ComponentException(e);
	} catch (NamingException e) {
		throw new ComponentException(e);
	} catch (UnknownApplicationException e) {
		throw new ComponentException(e);
	} catch (AccessControlException e) {
		throw new ComponentException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
}

在业务组件的激活阶段定义和注册域角色是一个很好的练习。

如果要销毁一个业务组件,需要 3 个阶段:首先预删除组件,然后删除,最后是后删除。这些阶段反映在 Lifecycle 接口方法 preDeletedeletepostDelete 中。要理解这几个阶段的含义,我们来看看删除一个复合应用程序意味着什么。

当一个复合应用程序被删除后,它并没有全部删除,只是标记为删除,并禁止访问应用程序。这是因为一个复合应用程序的删除可能是耗时的。CAI 允许您将消耗时间的删除部分延迟到系统负载不是很高的时候。这是由一个特定 CAI 任务应用程序 purger 完成的,它在一个可定制的端点上运行,最后将标记为 deleted 的应用程序删除。

要将删除流程耗时较多的部分和较少的部分分离,要在一个业务组件的生命周期中创建不同的删除阶段。

当一个应用程序被 CAI 标记为删除时,调用 preDelete 方法。业务组件应该执行不能延迟的且必须立即执行的活动。例如,业务组件撤销对后端数据的访问。

当应用程序被 CAI 应用程序 purger 完全删除时,可以延迟的活动 — 理论上是耗时活动 — 在这个阶段被移动。然后,调用方法 delete 和 postDelete。

在实现文档库时,preDelete 方法是空的,因为没有什么需要立即完成(清单 5)。

清单 5. 业务组件的 Predeletion
public void preDelete(ObjectID bcOID) throws ComponentException {
		// nothing to do	}

文档库的删除延迟到 delete 方法(清单 6)。

清单 6. 删除业务组件
publicvoid delete(ObjectID bcOID) throws ComponentException {
	final String method = "delete";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method);
	}
	try {
		persister.deleteLibrary(Util.getIdentification().serialize(bcOID));
	} catch (PersisterException e) {
		throw new ComponentException(e);
	} catch (SerializationException e) {
		throw new ComponentException(e);
	} catch (DocLibException e) {
		throw new ComponentException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
}

当业务组件销毁时,不需要撤销对 PCA 中的域角色注册。这是由 CAI 自动处理的。

文档库从数据库中删除。

在文档库业务组件被销毁时,文档库删除是唯一执行的一个活动,没有为 postDelete 留下什么,因此它是空的(清单 7)。

清单 7. 后删除业务组件
public void postDelete(ObjectID bcOID) throws ComponentException {
	// nothing to do
}

使用 CAI 特性

CAI 提供了备份和恢复应用程序的功能,以及管理复合应用程序社区和支持策略的功能。要使用这些特性,业务组件必须实现相应的 CAI SPI。

下一小节将大概介绍一下这些特性,特别介绍这些文档库的实现方法。用于 Document Library 的业务组件处理程序实现清单 8 所示的 CAI SPI。

清单 8. 文档库实现的 CAI SPI
public class DocLibBCHandlerImpl implements DocLibBCHandler, Lifecycle, Serializable, 
	Sensor, StreamBackupServiceClient, MembershipListener{
...

序列化到一个备份

一个复合应用程序可以以两种方法序列化:一个复合应用程序模板(复合应用程序的一个设计图)和一个备份。一个备份包含角色、社区成员、应用程序数据、通常还有业务组件的后端数据。

要加入序列化流程,业务组件处理程序必须实现 Serializable Interface。一个业务组件可以支持不同类型的系列化。它通过 supportsSerialization 方法声明它所支持的类型,文档库两个都支持,序列化到模版和序列化到备份(清单 9)。

清单 9. 文档库支持备份和模板序列化
publicboolean supportsSerialization(SerializationType type) {
		return ((SerializationType.TEMPLATE.equals(type)) || 
			(SerializationType.BACKUP.equals(type)));			
}

尽管存储的数据不同,但序列化到模板操作几乎都一样。当备份存储的正好是实例中提供的数据时,模板是专为这类数据设计的,应当包含从模板创建的每个新应用程序实例。

当一个备份被执行时, CAI 检查应用程序的哪个业务组件始终支持备份。为了实现这个目标,CAI 首先验证业务组件是否实现 Serializable 接口。然后,它证实了组件通过 supportSerialization 方法支持备份(清单 9)。如果这样的话,业务组件处理程序的 serializeInstance 方法被调用,当 CAI 为所有实例化类型调用该方法时,实例化类型被传递给此方法。

CAI 期望业务组件返回一个源自 javax.xml.transform.Source 接口的输入源。它可能是一个 DOMSource、SAXSource 或一个 StreamSource。

从性能角度看,当这有大量的后端数据时,您应该提供一个 SAXSource,因为其他的实现要求将数据在内存中保存一段时间。

文档库返回一个 SAXSource 以备备份时用(清单 10),因为一个文档库可以保存很多数据。对于实例化到一个模板,它返回一个 DOMSource。

清单 10. 文档库的实例化返回源实例
public Source serializeInstance(ObjectID bcOID, SerializationType type, 
Map<String, Object> parameters) throws ComponentException {
	...
	try {
		if (SerializationType.BACKUP.equals(type)) {

			Model model = getModel(bcOID);
                  ...
			result = new SAXSource(new BackupSerializationXMLReader(), 
new SerializationInputSource(model, fileToHandleMap));
		}
            else if (SerializationType.TEMPLATE.equals(type)) {
			result = new DOMSource();
		}	
	...
	return result;
	}

BackupSerializationXMLReader 作为一个 XMLReader 传入 SAXSource 的构造器,这个阅读程序通过将文档库模型转换为一个 XML 表示来创建 SAX 事件。这是在 BackupSerializationXMLReader 的 sendFolder 方法(清单 11)中完成的。CAI 提供一个 ContentHandler 来接收 SAX 事件,该事件然后被序列化到 XML 并存储在一个名为 appliction-instance.xml 的文件中,这是备份的一部分。

清单 11:为文档库实体创建 SAX 事件
private void sendFolder(Folder folder, Model model, ContentHandler contentHandler, 
Map<String, String> fileToHandleMap) throws SAXException {
	final String method = "sendFolder";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method);
	}
	AttributesImpl atts = new AttributesImpl();
	atts.addAttribute("", ID_ATTRIBUT, ID_ATTRIBUT, "CDATA", folder.getID());
	atts.addAttribute("", NAME_ATTRIBUT, NAME_ATTRIBUT, "CDATA", folder.getName());
	atts.addAttribute("", DESC_ATTRIBUT, DESC_ATTRIBUT, "CDATA", folder.getDesc());
	contentHandler.startElement("", FOLDER_TAG, FOLDER_TAG, atts);
	for (Folder childFolder : model.getChildFolders(folder)) {
		sendFolder(childFolder, model, contentHandler, fileToHandleMap);
	}
	for (File file : model.getFiles(folder)) {
		atts = new AttributesImpl();
		atts.addAttribute("", ID_ATTRIBUT, ID_ATTRIBUT, "CDATA", file.getID());
		atts.addAttribute("", NAME_ATTRIBUT, NAME_ATTRIBUT, "CDATA", 
			file.getName());
		atts.addAttribute("", DESC_ATTRIBUT, DESC_ATTRIBUT, "CDATA", 
			file.getDesc());
		atts.addAttribute("", MIME_TYPE_ATTRIBUT, MIME_TYPE_ATTRIBUT, "CDATA", 
			file.getMimeType());
		atts.addAttribute("", FILE_HANDLE_ATTRIBUT, FILE_HANDLE_ATTRIBUT, 
			"CDATA", fileToHandleMap.get(file.getID()));
		contentHandler.startElement("", FILE_TAG, FILE_TAG, atts);
		contentHandler.endElement("", FILE_TAG, FILE_TAG);
	}
	contentHandler.endElement("", FOLDER_TAG, FOLDER_TAG);
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
}

以这种方式序列化所有后端数据是可行的。因而,全部后端数据被写入 application-instance.xml。然而,这可能会生成一个非常巨大的文件。与模板序列化相比,CAI 为备份序列化提供一个机制来将数据写入除 application-instance.xml 之外的附加文件。

将大量数据写入单独文件。

Document Library 使用该机制将每个文档的目录存储到一个单独的文件中。这是通过执行 StreamBackupServiceClient 接口实现的。在序列化过程中,业务组件处理程序在 StreamBackupService 中通过传入文件标识符注册每个文件。服务返回一个文件句柄,正如在清单 12 中所看到的。

清单 12. 在 StreamBackupService 注册每个文件
public Source serializeInstance(ObjectID bcOID, SerializationType type, Map<String, 
Object> parameters) throws ComponentException {
…	
   try {
	if (SerializationType.BACKUP.equals(type)) {

		Model model = getModel(bcOID);
		Map<String, String> fileToHandleMap = new HashMap<String, 
			String>();

		// prepare for serialization of files:
		StreamBackupService streamBackupService = (StreamBackupService) 
((List) Util.getContext().lookup(StreamBackupService.JNDI_NAME)).get(0);
		Set<String> fileIDs = model.getAllFilesID();
		for (String fileID : fileIDs) {
			String handle = streamBackupService.registerBackupData
(parameters.get(BACKUP_CONTEXT), fileID);
			fileToHandleMap.put(fileID, handle);
		}
		// serialize other stuff into application-instance.xml
		result = new SAXSource(new BackupSerializationXMLReader(), 
new SerializationInputSource(model, fileToHandleMap));

文件句柄被映射到文件标识符,显示在 application-instance.xml 中。该数据在恢复过程中是必须的,用于将文件目录映射到 Document Library 模型的一个文档中。

要序列化文件目录,CAI 调用业务组件处理程序的 backupData 方法(清单 13)。

清单 13. 序列化文件目录到独立文件
public void backupData(OutputStream out, String componentID, String handback) 
throws ComponentException {
		final String method = "backupData";
		final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
		if (isTraceOn) {
			LOGGER.entering(LOG_CLASS, method, String.format("componentID: 
%s  | handback(=fileID): %s", componentID, handback));
		}
		try {
			ObjectID bcOID = Util.getIdentification().deserialize
(componentID);
			String fileID = handback;
			InputStream in = getFileContent(bcOID, fileID);
			try {
				Util.pipe(in, out);
			} finally {
				in.close();
			}
		} catch (PersisterException e) {
			throw new ComponentException(e);
		} catch (SerializationException e) {
			throw new ComponentException(e);
		} catch (DocLibException e) {
			throw new ComponentException(e);
		} catch (IOException e) {
			throw new ComponentException(e);
		}

		if (isTraceOn) {
			LOGGER.exiting(LOG_CLASS, method);
		}
	}

handback 参数是在 serializeInstance 方法中传递给 StreamBackupService 的文件标识符。

当备份活动完成时,对于文档库的每个文档备份包含一个独立的文件。

从备份中恢复

一个复合应用程序可以用两种方法反序列化:从一个复合应用程序模板或从一个备份。两种类型的基本控制流是相同的。在反序列化过程中,所有进程,属于复合应用程序的所有业务组件都被创建,并且用数据填充。

当一个现有复合应用程序被恢复时,首先删除,然后重新创建。因而,包含的所有业务组件需要被重新创建。

当一个恢复被执行时,CAI 检查应用程序中的哪个业务组件支持恢复特性。这与备份执行时的处理方式相同:CAI 首先检查是否业务组件实现 Serializable 接口,之后,由 supportSerialization 方法验证组件是否支持备份(清单 9)。

用数组填充一个业务组件由以下 3 步来完成:

  1. CAI 调用业务组件处理程序 initDeserializeInstance 方法来为业务组件数据获取一个目标。
  2. 然后将 XML 数据发送到提供的 Result 对象。
  3. 最后,调用 finishDeserializeInstance 方法。

实际上只有在该组件不使用 SAXResult 时,才需要最后一步,否则组件可以通过 endDocument 方法探测终端数据流。万一处理过程中出现异常,组件的 cancelDeserializeInstance 方法被调用,这样,组件就可以删除迄今为止创建的任何内容。

CAI 再次调用这些业务组件处理程序方法,按照以下顺序:

  1. create
  2. initDeserializeInstance
  3. finishDeserializeInstance
  4. activate

创建和激活方法之前已经介绍过了。initDeserializeInstance 方法为业务组件数据返回一个目标。CAI 期望业务组件返回一个源自 javax.xml.transform.Result 接口的目标,可以是一个 DOMResult、SAXResult 或者一个 StreamResult。

从性能角度来看,如果后端数据足够多的时候,您应该提供一个 SAXResult,因为其他实现首先需要将整体数据读入到内存。

文档库返回一个 SAXResult 以备恢复之用(清单 14)。对于从一个模板反序列化,它返回 DOMResult 以作展示之用。

清单 14. 返回序列化数据目标
public Result initDeserializeInstance(ObjectID bcOID, SerializationType type, 
Map<String, Object> parameters) throws ComponentException {
	final String method = "initDeserializeInstance";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method, parameters);
	}
	Result result = null;
	if (SerializationType.BACKUP.equals(type)) {
	// delete root folder
		delete(bcOID);
			// within create there was already a root folder created. 
			// But the ID was generic and do not fit to the restore 
			// root folder id
		result = new SAXResult(new RestoreHandler(bcOID, parameters));
	} else {
		result = new DOMResult();
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method, result);
	}
	return result;
}

返回的 SAXResult 使用一个名为 RestoreHandler 的 org.xml.sax.ContentHandler 的实现进行初始化,在序列化的业务组件表示的解析过程中由 CAI 调用。

例如,在解析过程中探测到一个 XML 元素的起点,CAI 将在 RestoreHandler(清单 15)中调用 startElement。

清单 15. 当 SAX 事件出现时文档库整体恢复
public void startElement(String namespaceURI, String localName, String qName, 
Attributes atts) throws SAXException {
	final String method = "startElement";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method, new Object[] { namespaceURI, 
localName, qName, atts });
	}
	try {
		if (FOLDER_TAG.equals(localName)) {
			Folder folder = new Folder(atts.getValue("", ID_ATTRIBUT), 
atts.getValue("", NAME_ATTRIBUT), atts.getValue("", DESC_ATTRIBUT));

			if (parentFolderStack.empty()) {
				createRootFolder(getBcOID(), folder);
			} else {
				addFolder(getBcOID(), parentFolderStack.peek(), 
folder.getID(), folder.getName(), folder.getDesc());
			}
			parentFolderStack.push(folder);
		} else if (FILE_TAG.equals(localName)) {
			File file = new File(atts.getValue("", ID_ATTRIBUT), 
atts.getValue("", NAME_ATTRIBUT), atts.getValue("", DESC_ATTRIBUT), 
atts.getValue("", MIME_TYPE_ATTRIBUT));
			addFile(getBcOID(), parentFolderStack.peek(), file.getID(), 
file.getName(), file.getDesc(), null);
			setHandleToFileIdMap(getBcOID(),atts.getValue("", 
FILE_HANDLE_ATTRIBUT), atts.getValue("", ID_ATTRIBUT));
		}
	} catch (PersisterException e) {
		throw new SAXException(e);
	} catch (DocLibException e) {
		throw new SAXException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
}

该代码检查该元素是代表一个文件夹还是一个文档,并在后端创建了适当的库条目。此外,还检索每个文件的映射,并保存到到其文件句柄中。该数据在 finishDeserializeInstance 方法进行处理。

正如之前所述的,CAI 提供一个机制来在不同的文件中存储目录。如果它实现 treamBackupServiceClient 接口这可以被一个业务组件利用。

文档库在 finishDeserializeInstance 方法(清单 16)的 StreamBackupService 中注册每个句柄。

清单 16. 在 StreamBackupService 中注册文件句柄
public void finishDeserializeInstance(Result result) throws ComponentException {
	final String method = "finishDeserializeInstance";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method);
	}

	if (result instanceof SAXResult) {
		if (isTraceOn) {
			LOGGER.log(Level.FINEST, "Finish Restoring DocLib");
		}
		try {
			// restore files
		      RestoreHandler restoreHandler = ((RestoreHandler) (
((SAXResult) result).getHandler()));						
			StreamBackupService streamBackupService = (StreamBackupService) 
((List) Util.getContext().lookup(StreamBackupService.JNDI_NAME)).get(0);
			Set<String> handles = getHandleOfFiles(
restoreHandler.getBcOID());  
			for (String handle : handles) {
					streamBackupService.registerRestoreData(
restoreHandler.getParameters().get(BACKUP_CONTEXT), handle);
			}
		} catch (Exception e) {
			throw new ComponentException(e);
		}
	}

	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
}

StreamBackupServiceClient 的 restoreData 方法由 CAI 为每个句柄调用,文件的 InputStream 作为一个参数传递给方法(清单 17)。

清单 17. 从 InputStream 读取文档内容
public void restoreData(InputStream in, String componentID, String backupHandle) 
throws ComponentException {
	final String method = "restoreData";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method, String.format("componentID: %s  | 
backupHandle: %s", componentID, backupHandle));
	}
	try {
		ObjectID bcOID = Util.getIdentification().deserialize(componentID);
		String fileID = getFileIdForHandle(bcOID,backupHandle);
		insertFile(bcOID, fileID, in);
		removeHandleToFileIdMap(bcOID,backupHandle);
	} catch (SerializationException e) {
		throw new ComponentException(e);
	} catch (DocLibException e) {
		throw new ComponentException(e);
	} catch (PersisterException e) {
		throw new ComponentException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
}

在该方法中,文件从 InputStream 中读取然后存储在后端。

最后 activate 方法被调用。完成这些之后,恢复的业务组件可以使用了!

策略

WebSphere Portal 提供策略确定如何使用门户资源函数,策略应用于复合应用程序。CAI 评估每个复合应用程序的策略状态。一个应用程序的策略状态通过确认应用程序大小、应用程序最后被访问的日期、以及针对相应策略进行最后一次修改的日期来确定。这些值被称之为 Sensor 数据元素。一个复合应用程序的传感器数据是从业务组件实例的传感器数据中采集来的。

传感器数据的确定对于业务组件来说可能是一项比较昂贵的操作。例如,在文档库的不完整实现中,文档库大小的计算需要从数据库中检索属于文档库的所有文件,来确定文件大小。

CAI 提供一个机制(pull 机制)来获取某个时间的传感器数据。这是由一个定期运行的任务(CAI 策略处理程序)完成的。当附加负载不能影响门户服务器的性能时,应该不时地安排这个任务。因此,两个运行策略处理程序的应用程序之间的修改不能立即反应在传感器数据中,导致应用程序状态不能及时更新。

业务组件总是提供传感器数据,这将提高一个应用程序的策略状态的精确度。有着昂贵操作的传感器数据元素应该通过实现 Sensor 接口来提供;否则,传感器数据应该在相应业务组件服务的元数据中直接设置。

要使策略状态报告更为精确,一个业务组件可以直接调用 CAI 来更新它的传感器数据(push 机制)。因此,它通过 BusinessComponentService 为特定元数据项设置传感器数据。

要支持 pull 机制,一个业务组件必须实现 Sensor 接口。CAI 调用业务组件处理程序的 getSensorData 方法,获取一些 SensorData 实例。

正如您在清单 18 中所看到的,文档库通过 Sensor 接口实现返回的只是它的大小。

清单 18. 提供文档库大小
public ListModel<SensorData> getSensorData(ObjectID bcOID, ListModel<SensorData> 
sensorDataList) throws ComponentException {
	final String method = "getSensorData";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method);
	}
	Iterator it;
	try {
		it = sensorDataList.iterator();
		while (it.hasNext()) {
			SensorData data = (SensorData) it.next();
			if (SensorDataConstants.SIZE_SENSOR_DATA_NAME.equals
(data.getName())) {
				Integer size = getSize(bcOID); 
					// size returned in megabytes
				data.setValue(size);
				break;
			}
		}
	} catch (ModelException e) {
		throw new SensorDataListModelException(e);
	} catch (PersisterException e) {
		throw new SensorDataListModelException(e);
	} catch (DocLibException e) {
		throw new SensorDataListModelException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method);
	}
	return sensorDataList;
}

文档库也提供关于最后一次修改的时间点的信息。这个信息是推入 CAI 的,因此,我们不需要在后台存储此信息,但是在库中进行修改时,要将其推入 CAI 作为我们最后修改的时间点。

清单 19 显示在样例中的执行。

清单 19. 将传感器数据传入到 CAI
privatevoid updateLastModified(ObjectID bcOID) throws ApplicationException, 
NamingException, DocLibException {
	String lastModified;
	synchronized (UTC_DATE_METADATA_FORMAT) { 
             //SimpleDateFormat is not thread-safe
		lastModified=UTC_DATE_METADATA_FORMAT.format(new Date());
	}
		
	BusinessComponentService bcService = getBcsHome().
getBusinessComponentService(bcOID);		
		bcService.setMetaData(
Constants.BUSINESS_COMPONENT_METADATA_SENSOR_LASTMODIFIED,lastModified );
}

updateLastModified 方法在一个文档或文件夹被创建或删除的时候调用。当前日期被检索、格式化,并设置为 BusinessComponentService 的元数据。CAI 保留传感器数据作为一个业务组件的元数据。下次当策略状态将被一个用户或策略任务调用时,这个元数据将作为一个评估依据提供一个更为精确的策略状态确定。

如清单 19 所示,BusinessComponentService 是从 BusinessComponentServiceHome 对象中检索的。只加载本地对象一次,重用它来避免额外的昂贵 JDNI 查找,这不失为一个好方法。样例代码实现这个懒加载模式(清单 20)。ApplicationService 使用类似方法检索。

清单 20. 获取业务组件服务的本地实例
public static synchronized BusinessComponentServiceHome getBcsHome() throws 
NamingException, DocLibException {
	if (bcsHome == null)
		bcsHome = (BusinessComponentServiceHome) Util.getContext().lookup
(BusinessComponentServiceHome.JNDI_NAME);
	return bcsHome;
}

社区和域角色

复合应用程序包括一个分配给社区角色的成员社区。只有这个社区的成员被允许使用复合应用程序(除了有特殊权限的门户管理员之外)。要给应用程序定义不同的访问权限,成员应该以不同的社区角色来组织。

一个业务组件可以通过定义自己的角色域角色,参与这个基于角色的方法。对于应用程序中的每个业务组件,一个社区角色可以映射到一个特定域角色。每个域角色可以被映射到一个或多个社区角色。这些映射在复合应用程序的模板中定义。它们也可以在运行时被定制。

一个业务组件在安装过程中通过使用社区 API 注册它的域角色。

社区 API 的主入口点:

  • CommunityLocator 提供检索关于社区角色、域角色以及成员的信息的功能。
  • CommunityController 提供修改社区角色、域角色以及成员分配的功能。
  • CommunityObjectFactory 提供创建填充的 Localized、LocalizedDomainRole 以及 LocalizedCommunityRole 对象的功能,这些对象可以用作 Community API 方法调用的输入参数。

在样例代码中,域角色的注册是在 Lifecycle 的方法 activate 中完成的,调用 createDomainObjectWithRoles 方法(清单 21)。

清单 21. 注册域角色
privatevoid createDomainObjectWithRoles(ObjectID cmtyOID, ObjectID bcOID) 
throws ComponentException {
	final String method = "createDomainObjectWithRoles";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method);
	}
	Set<DomainRole> createdDomainRoles = null;
	try {
		Set<LocalizedDomainRole> domainRoles = createDomainRoles();
		createdDomainRoles = getCommunityHome().getController().
addDomainObjectToCommunity(cmtyOID, bcOID, domainRoles);
	} catch (MissingAccessRightsException e) {
		throw new ComponentException(e);
	} catch (CommunityException e) {
		throw new ComponentException(e);
	} catch (NamingException e) {
		throw new ComponentException(e);
	} catch (DocLibException e) {
		throw new ComponentException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method, createdDomainRoles);
	}
}

在 addDomainObjectToCommunity 方法中 CommunityController 用于将域角色添加到一个特定社区中。域角色是在 createDomainRoles 方法中创建的(清单 22)。

清单 22. 创建 LocalizedDomainRole 实例
private Set<LocalizedDomainRole> createDomainRoles() {
	Set<LocalizedDomainRole> domainRoles = new HashSet<LocalizedDomainRole>();
	LocalizedDomainRole domainRole = chome.getObjectFactory().
createLocalizedDomainRoleObject(EDITOR_ROLE, editorRoleLocData, true);
	domainRoles.add(domainRole);
	domainRole = chome.getObjectFactory().
createLocalizedDomainRoleObject(READER_ROLE, readerRoleLocData, false);
	domainRoles.add(domainRole);
	return domainRoles;
}

文档库定义两个域角色:Editors 被授权修改文档库的内容,而 Readers 只能查看这些内容。

CommunityObjectFactory 用于创建本地化的域角色实例。每个域角色有一个不同的角色名。因为一个域角色(标题和描述)的本地化数据对所有域角色实例都是一样的,这个数据是在静态方法 createLocalizedRoleDate 中生成的(清单 23)。此方法在业务组件处理程序类静态初始化过程中调用。

清单 23. 为域角色创建 LocalizedObject 实例
private static void createLocalizedRoleDate() {
	// Localized data - at least one title is needed
	// Editor role
	HashMap<Locale, String> mapOfTitles = new HashMap<Locale, String>();
	HashMap<Locale, String> mapOfDescriptions = new HashMap<Locale, String>();
	mapOfTitles.put(Locale.ENGLISH, "Editor");
	mapOfTitles.put(Locale.GERMAN, "Schreiber");
	mapOfDescriptions.put(Locale.ENGLISH, "Editors of the document library");
	mapOfDescriptions.put(Locale.GERMAN, "Bearbeiter der Dokumentenbibliothek");
	editorRoleLocData = chome.getObjectFactory().
createLocalizedObject(mapOfTitles, mapOfDescriptions);

	// Reader role
	mapOfTitles = new HashMap<Locale, String>();
	mapOfDescriptions = new HashMap<Locale, String>();
	mapOfTitles.put(Locale.ENGLISH, "Reader");
	mapOfTitles.put(Locale.GERMAN, "Leser");
	mapOfDescriptions.put(Locale.ENGLISH, "Reader of the document library");
	mapOfDescriptions.put(Locale.GERMAN, "Leser der okumentenbibliothek");
	readerRoleLocData = chome.getObjectFactory().
createLocalizedObject(mapOfTitles, mapOfDescriptions);
}

在复合应用程序的实例化过程中,社区角色被映射到域角色。通过将社区角色映射到域角色,可以保证将复合应用程序的一个用户分配给包含业务组件的映射的域角色。

当一个用户使用业务组件工作时,业务组件代码可以通过调用 CommunityLocator 方法 isUserInDomainRole 确定该用户属于哪个域角色。

在文档库样例中,只有 Editor 角色中的用户被允许添加或删除文档或文件夹,portlet 代码调用 isUserInRole 方法来判断当前用户是否是一个 Editor 角色(清单 24)。

清单 24. 检查当前用户是否是一个 Editor
public boolean isUserInRole(String bcID, String roleName) throws DocLibException {
	final String method = "isUserInRole";
	final boolean isTraceOn = LOGGER.isLoggable(Level.FINEST);
	if (isTraceOn) {
		LOGGER.entering(LOG_CLASS, method, roleName);
	}
	boolean result = true;
	try {
		ObjectID bcOID = Util.getIdentification().deserialize(bcID);
		ObjectID appOID = getAcsHome().getApplicationCatalogService().
findApplicationByContainedBC(bcOID);
		ObjectID cmtyID = getAsHome().getApplicationService(appOID).
getCommunityID();
		String componentJNDIName = DOC_LIB_BC_HANDLER_JNDI_NAME;
		result = getCommunityHome().getLocator().isUserInDomainRole(cmtyID, bcID, 
componentJNDIName, roleName);
	} catch (CommunityException e) {
		throw new DocLibException(e);
	} catch (SerializationException e) {
		throw new DocLibException(e);
	} catch (ObjectNotFoundException e) {
		throw new DocLibException(e);
	} catch (ApplicationDataException e) {
		throw new DocLibException(e);
	} catch (NamingException e) {
		throw new DocLibException(e);
	} catch (UnknownApplicationException e) {
		throw new DocLibException(e);
	} catch (AccessControlException e) {
		throw new DocLibException(e);
	}
	if (isTraceOn) {
		LOGGER.exiting(LOG_CLASS, method, result);
	}
	return result;
}

如果用户是一个 Editor,那么添加文档和文件夹的按钮就会出现在 portlet UI 中,正如您在 portlet jsp DocLibView.jsp 片段中看到的那样(清单 25)。

清单 25. 显示 Editors 按钮
//is current user is in Editor role then the action buttons are displayed
if (isEditor) {
%>
<div class="lotusActionBar lotusBtnContainer" id="docLibActionButtons">
<span class="lotusBtn lotusBtnAction lotusLeft">
<a href="javascript:<portlet:namespace />hideDiv('docLibFileDialog'); 
<portlet:namespace/>centerOnElement('docLibActionButtons','docLibFileDialog'); 
<portlet:namespace/>showDiv('docLibFileDialog');">
Add Document</a>
</span>
<span class="lotusBtn lotusBtnAction lotusLeft">
<a href="javascript:<portlet:namespace />hideDiv('docLibFolderDialog'); 
<portlet:namespace/>centerOnElement('docLibActionButtons','docLibFolderDialog'); 
<portlet:namespace/>showDiv('docLibFolderDialog');">
Create Folder</a>
</span>
</div>
<%
}
%>

会员

当一个代理被分配给一个域角色时或者当分配删除时,业务组件将被通知。要实现这个目标,组件必须实现 MembershipListener SPI 的方法 memberAdded 和 memberRemoved。

当一个代理被添加到一个社区角色(之前已经映射到组件的一个域角色中)时,组件的 memberAdded 方法被调用,这个受影响的域角色(以及添加的代理)被传递。当代理被从社区角色删除后,memberRemoved 方法以一种模拟方法被调用。这些通告使组件域主角色映射保持联系,当组件想要从 PAC 访问独立资源时这是必需的。


结束语

WebSphere Portal Composite Application Infrastructure (CAI) 支持复合应用程序及其社区的创建和管理,它提供一些特性,比如从复合应用程序创建备份,以及在应用程序的基础上支持角色管理。客户可以通过实现各种 CAI SPI 插入业务组件。CAI 也提供可被业务组件和其他代码所用的 API,来检索关于应用程序的信息或进行修改。

本文对一个业务组件的生命周期以及它的部署方法,包括在备份和存储过程中如何加入业务组件,提出了一些见解。介绍了业务组件提供传感器数据的不同方法,使得 CAI 可以确定复合应用程序是否超出了一个策略的阈值设置。此外本文还展示了业务组件如何以及何时创建域角色,以及当用户被分配给某个域角色时什么 API 可用于测试。

本文还介绍业务组件和样例代码实现的一些建议,进一步说明了一个实现的样子。完整的样例代码在下载 部分展示。


关于下载资料

压缩了归档文件之后,您将发现文件包含:

  • Src 目录含有文档库的完整源代码。
  • JavaDoc 目录包括样例的 Javadoc 。

下载

描述名字大小
代码样例CAI_BC_Sample-B.zip3.4MB

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=WebSphere, Lotus
ArticleID=619059
ArticleTitle=为 WebSphere Portal 复合应用程序基础架构构建一个业务组件
publish-date=01272011