扫一扫
分享文章到微信
扫一扫
关注官方公众号
至顶头条
近几年来,随同 Internet 以及支持 Web 的技术的成长,Web 应用程序变得极为流行。Web 编程带来了很多好处,包括快速开发、可伸缩的应用程序和部署瘦客户机时的管理优势。由于当前的趋势,致使应用程序架构师被迫把几乎所有类型的业务流程都设计为 Web 应用程序,这从根本上而言是双向的请求/响应流。然而对于一些场景,缺少发布/订阅机制可能会严重限制应用程序的功能。
本文描述了如何使用 Sametime (Lotus Instant Messaging 和 Web Conferencing)Java 工具包为 Web 应用程序构建事件发布模型。Sametime Java 工具包的 Community 和 Meeting Services 使开发人员能够在他们的应用程序中通过 Sametime Instant Messaging Server 实现单向消息传递。
这里提供的信息对于那些在他们的项目中需要用户通知或任何其他事件驱动的活动触发器机制的中级 Java 开发人员和软件架构师非常有用。
业务场景
假设我们需要为支持部门开发一个 Web 应用程序,该部门的服务人员直接接受来自客户的服务请求并将请求转发给感兴趣的 Field Service 小组(参见图 1)。
图 1. 业务问题
使用典型的 Web 开发工具,很容易将服务请求传送到应用服务器。然而真正的挑战是传送消息到感兴趣的组。可以应用一种称为“client pull”的技术,即通过编程使接收者 Web 页面定期刷新自身以查询事件,但是这种方法通常会导致网络和服务器资源极大的浪费。
Sametime 的单向消息传递功能为这个问题提供了有效的解决方案,即通过服务台用户界面上的一个事件发布组件利用 Sametime 来传递通知到 Field Service 界面上的事件接收者组件。图 2 中显示了建议的解决方案:
图 2. 建议的解决方案
|
Sametime 概述
IBM Lotus Sametime 软件是为业务提供的实时协作解决方案。它提供了三种基本服务:
虽然可以将 Sametime 作为企业级的独立应用程序以提高生产效率、降低成本和提供业务敏捷性,但是其更大的优势是它的定制和集成功能。Sametime API 为开发人员提供了构造块,用来将实时协作服务修改和集成到其他的电子商务应用程序中。
可以通过为几种流行的编程平台准备的工具包来访问 Sametime 编程接口。这里,我们将集中讨论 Sametime Java 工具包,该工具包为协作功能提供了一组独立于平台的用户和服务接口组件。能够应用于户客户端 Java 应用程序和嵌入到 Web 页面中的 Java applet,同时还可用于诸如 servlet、Web 服务和 Sametime bot 等服务器端 Java 程序。Microsoft 已经声明在 2007 年后将不再继续支持 MSJVM,并且 Sametime Java 工具包(版本 7.0)的新版本仅支持 Sun Microsystems 提供的 JVM,建议 Internet Explorer 客户端安装 Java VM 1.4.2。
|
发布/订阅
通信模式的常见、松散耦合形式是“发布/订阅”机制,由此,信息的发布者为感兴趣的组提供了通用接口来接收通知。该接口建立在两个函数的基础之上。最初,事件的接收者通过调用订阅方法向发布者声明他的兴趣。随着事件的发生,发布者通过回调过程将通知发送给订阅者。发布/订阅方法也称之为事件驱动的或基于通知的交互。
Sametime Post Service 为发布/订阅模式提供了简单的基础设施。这个 API 支持一次将消息发送给许多用户,并允许接收者对发送者进行响应。它支持不同消息类型的订阅,并能够从客户机和服务器过程进行访问。
|
应用程序的体系结构
在我们的解决方案中,将通过构建一个 Java applet 作为 Sametime 服务代理来实现发布/订阅模式。这个 applet 将嵌入到 HTML 页面中,并且它没有用户界面。相反,它将与 JavaScript 环境进行交互,来发送和接收事件。代理 applet 将扮演两个不同的角色:
图 3 说明了事件传递的机制。
图 3. 应用程序的体系结构
当服务台用户接收到服务请求时,他会填写 HTML UI 中显示的表单并使用 JavaScript 操作提交表单。事件处理程序的 JavaScript 方法将请求转发给事件发布者代理 applet。applet 使用所提供的信息利用 Sametime API 发布一个事件。然后,Sametime 服务器将通知传送到内嵌在 Field Service UI 中的事件接收者代理 applet。最后,接收者 applet 转发事件消息到生成 HTML UI 的 JavaScript 函数,以将服务请求显示给 field service 用户。
代理 applet 项目通过六个类实现。这些类及其主要角色如下所示:
|
定义事件类型
Sametime 的投递类型标识符将用作发布/订阅体系结构中的事件类型。投递类型标识符帮助 Sametime Post Service 对通知进行分类,并将它们路由到各自感兴趣的组。要接收投递,感兴趣的用户必须以指定的投递类型注册(订阅)Post Service。消息发送者也必须指定投递类型。
出于多种目的,包括用于投递类型,Sametime 使用整型常量作为标识符。为避免不同 Sametime 服务之间的常量冲突,应用了如下规定:IBM 为其自己的 Sametime 常量保留了 0 – 100,000 的范围。第三方开发人员可以为自己的的产品和服务分配除 IBM 之外的其他常量范围。
对于我们的业务案例,定义了下列事件类型:
管理投递消息分发的第二个参数是接收者地址列表。消息发送者在发送前必须指定收件人(参见图 4)。这乍一看可能有些奇怪,因为 Sametime 服务器必须已经具有收件人列表,这些收件人都已经声明他们对该消息类型感兴趣;然而,对设计而言这一特性添加了新一级别的灵活性。如果想在更高的级别上定义事件,并让发送者使用它实现不同的目的,为所有的投递尝试指定一个收件人列表将是非常有用的。
图 4. Post Service 消息传递逻辑
例如,使用收件人列表特性,可以重新定义具有更少事件的事件集:
这些事件类型被定义为投递型常数,如下所示:
// Event types
public final static int SystemAlertEventType = 145700;
public final static int ServiceRequestEventType = 145701;
|
初始化 Sametime
在开始发布和接收 Sametime 投递前,applet 必须完成一些任务以获得对 Sametime 服务的访问。通常,这些步骤在初始化阶段完成。首先,打开一个 Sametime 会话,加载所需的组件。下一步是使用有效的用户证书集登录到 Sametime。可以通过下列步骤获取这些用户证书:
下面的代码演示了初始化 Sametime 的简单过程。基于传递的参数值,函数决定使用何种登录方法。这个函数是 SessionManager 类的成员函数。
public void open( String serverName, String loginName, String password, String token) { try { this.session = new STSession( "SessionManager " + System.identityHashCode(this)); session.loadAllComponents(); session.start(); this.communityService = (CommunityService) session.getCompApi( CommunityService.COMP_NAME); communityService.addLoginListener(this); if (password != null) { communityService.loginByPassword( serverName, loginName, password); } else if (token != null) { communityService.loginByToken( serverName, loginName, token); } else { communityService.loginAsAnon( serverName, loginName); } } catch (DuplicateObjectException e) { e.printStackTrace(); } } public void loggedOut(LoginEvent event) { . . . } public void loggedIn(LoginEvent event) { . . . } |
|
订阅事件
启动会话后,事件接收器 applet 将使用 Post Service API 订阅应用程序的事件。为此,我们将定义一个 Receiver 类,并修改 SessionManager 类以初始化我们的事件接收器。使用事件 ID 调用 registerPostType 方法将获得该事件类型的 Post Service 订阅(参见下面的示例代码)。最后,Receiver 类将其自身作为 PostServiceListener 进行提交,这样,当通知到达时,Post 服务将回调 posted 函数。
public class Receiver implements PostServiceListener { private PostService postService; public Receiver(STSession stSession) { postService = (PostService) session.getCompApi(PostService.COMP_NAME); postService.registerPostType( SessionManager.SystemAlertEventType); postService.registerPostType( SessionManager.ServiceRequestEventType); postService.addPostServiceListener(this); } public void posted(PostEvent event) { // Event received. Take necessary actions. . . . } |
登录后,需要立即创建 Receiver 类。修改 SessionManager 类的 loggedIn 函数以实现这个操作。
public void loggedIn(LoginEvent event) {
this.receiver = new Receiver(session);
. . .
|
发布事件
发送 Post Service 消息与发送电子邮件没有多大区别。发布者对象创建一个 Sametime Post 对象,然后设置其标题、消息正文以及收件人(参见下面的示例代码)。如果业务逻辑要求用户手动选择收件人,可以利用 Sametime DirectoryPanel UI 组件向用户显示 Sametime 目录。可以通过创建实现了 DirectoryListViewListener 接口的对象来捕捉用户对该组件的操作(例如更改选择),然后使用 DirectoryPanel.addDirectoryListViewListener 方法来激活该操作。
public class Publisher implements PostListener { private final static long ResolutionTimeout = 10000L; // msec private final static long ResolutionPeriod = 250L; // msec private STSession stSession; private PostService postService; private GroupResolver groupResolver; private RecipientResolver recipientResolver; private Vector recipientsInPerson; // STUser objects private Vector recipientGroups; // STGroup objects private Post message; public Publisher(STSession session) { stSession = session; postService = (PostService) stSession.getCompApi(PostService.COMP_NAME); postService.registerPostType( SessionManager.SystemAlertEventType); postService.registerPostType( SessionManager.ServiceRequestEventType); recipientsInPerson = new Vector(10); recipientGroups = new Vector(5); groupResolver = new GroupResolver(); recipientResolver = new RecipientResolver(); } public void publishEvent( int eventType, String title, String body, String recipients) { message = postService.createPost(eventType); message.addPostListener(this); message.setMessage(body); message.setTitle(title); StringTokenizer st = new StringTokenizer(recipients, "##"); String[] recipient_list = new String[st.countTokens()]; int i = 0; while (st.hasMoreTokens()) { recipient_list[i++] = st.nextToken(); } recipientResolver.resolveRecipients(recipient_list); Enumeration e = recipientGroups.elements(); while (e.hasMoreElements()) { STGroup stg = (STGroup) e.nextElement(); groupResolver.resolveGroup(stg); } message.send(); recipientsInPerson.removeAllElements(); recipientGroups.removeAllElements(); } . . . private class RecipientResolver implements ResolveListener{ . . . private class GroupResolver implements GroupContentListener { . . . |
在我们的例子中,收件人列表是 HTML UI 中指定的一个由令牌进行分隔的字符串。下列代码片段中显示的帮助器类 (RecipientResolver) 使用 Sametime LookupService 和 Resolver 类将收件人名称转换为 Sametime 目录条目(STObject 实例)。对于 DirectoryPanel UI 操作来说,该操作不是必需的,因为对象的选择函数已经返回了一个 STObject 数组。
private class RecipientResolver implements ResolveListener { private Resolver resolver; private int waitFlag; private LookupService lookupService; public RecipientResolver() { lookupService = (LookupService) stSession.getCompApi(LookupService.COMP_NAME); resolver = lookupService.createResolver( false, // Return all matches. false, // Non-exhaustive lookup. true, // Return resolved users. true); // Do return resolved groups. resolver.addResolveListener(this); } public void resolveRecipients(String[] recipients) { waitFlag = recipients.length; resolver.resolve(recipients); long totalSlept = 0; while (waitFlag > 0 && totalSlept < ResolutionTimeout) { try { Thread.sleep(ResolutionPeriod); totalSlept += ResolutionPeriod; } catch (InterruptedException ie) { } finally { waitFlag = 0; } } } public void resolved(ResolveEvent event) { STObject sto = (STObject) event.getResolved(); addRecipient(sto); waitFlag--; } public void resolveConflict(ResolveEvent event) { // More than one match found STObject[] recipients = (STObject[]) event.getResolvedList(); for (int i = 0; i < recipients.length; i++) { addRecipient(recipients[i]); } waitFlag--; } public void resolveFailed(ResolveEvent event) { System.out.println("Resolve failed"); waitFlag--; } public void addRecipient(STObject sto) { if (sto.getClass().getName().endsWith("STUser")){ STUser stu = (STUser) sto; if (!recipientsInPerson.contains(stu.getId())) { message.addUser(stu); recipientsInPerson.addElement(stu.getId()); } } else if (sto.getClass().getName().endsWith("STGroup")) { recipientGroups.addElement(sto); } else { System.out.println("Resolved unknown STObject:" + sto.getId()); } } } |
Sametime Post Service API 只接受 Sametime 用户(STUser 对象)作为收件人。如果计划向 Sametime 群组(STGroup 对象)发送消息,将需要 Sametime LookupService 和 GroupContentGetter 类以查询群组内容。下面的示例代理显示了用于解析群组内容的帮助器类的实现。请注意,如果用户是多个收件人群组中的一员,那么 GroupResolver 类只列出该用户一次。这样做将防止用户多次接收相同的事件。图 5 演示了 RecipientResolver 和 GroupResolver 类如何一起运行以形成收件人用户对象集。
图 5. Directory lookup helper 对象
private class GroupResolver implements GroupContentListener { private GroupContentGetter gcg; private int waitFlag; private LookupService lookupService; public GroupResolver() { lookupService = (LookupService) stSession.getCompApi(LookupService.COMP_NAME); gcg = lookupService.createGroupContentGetter(); gcg.addGroupContentListener(this); waitFlag = 0; } public void resolveGroup(STGroup group) { waitFlag++; gcg.queryGroupContent(group); long totalSlept = 0; while (waitFlag > 0 && totalSlept < ResolutionTimeout) { try { Thread.sleep(ResolutionPeriod); totalSlept += ResolutionPeriod; } catch (InterruptedException ie) { } finally { waitFlag = 0; } } } public void queryGroupContentFailed(GroupContentEvent event) { System.out.println("Query Group Content failed"); waitFlag--; } public void groupContentQueried(GroupContentEvent event) { STObject[] sto = event.getGroupContent(); for (int idx = 0; idx < sto.length; idx++) { if (sto[idx].getClass().getName() .endsWith("STUser")) { STUser stu = (STUser) sto[idx]; if (!recipientsInPerson.contains(stu.getId())) { message.addUser(stu); recipientsInPerson.addElement(stu.getId()); } } else if (sto.getClass().getName() .endsWith("STGroup")) { STGroup stg = (STGroup) sto[idx]; resolveGroup(stg); } } waitFlag--; } } |
与大多数 Sametime 服务类似,目录查询服务以异步方式运作。在我们的例子中,在发送消息之前,需要完成收件人列表。因此,帮助器类会等待一段时间以完成目录查询。因此,每次有异步请求时,一个名为 waitFlag 的计数器变量会增加,当侦听器回滚时,计数器会减少。进行异步函数调用后,下列代码块会等待所有的任务完成:
long totalSlept = 0; while (waitFlag > 0 && totalSlept < ResolutionTimeout) { try { Thread.sleep(ResolutionPeriod); totalSlept += ResolutionPeriod; } catch (InterruptedException ie) { } finally { waitFlag = 0; } } |
为防止死锁,变量 totalSlept 将记录所有的等待时间,并在达到解析超时限制时结束循环。
|
接收事件
当消息到达时,Sametime Post Service 通过调用 PostServiceListener 接口的 posted 函数通知 Receiver 对象。可以通过调用 PostEvent 参数对象的 getPost 函数获取消息(Post 对象)(参见下面的示例代码)。接收者可以使用 Post 对象的 respond 函数答复消息。在我们的应用程序中,我们将该功能作为发送回事件发布者的消息收件人。由于我们的 applet 没有 UI 元素,事件信息被直接传送给 JavaScript 层。
public void posted(PostEvent event) { // Event received. Take necessary actions. Post message = event.getPost(); String messageText = message.getMessage(); String from = message.getSenderDetails().getDisplayName(); String subject = message.getTitle(); String messageType = new Integer(message.getType()).toString(); message.respond(1, "OK"); // Return receipt // Call JavaScript event handler String arguments[] = {messageText, from, subject, messageType}; AgentApplet.callJS("MessageReceived", arguments); } |
|
收集结果
随着消息被分发给收件人,Sametime Post Service 将根据结果更新事件发布者(参见下面的代码片段)。为此,Sametime PostListener 接口具有两个回调函数:
这些函数是在 Publisher 类中实现的。
public void userResponded(PostEvent event) { // Add your reporting logic here System.out.println("Message delivered to:" + event.getPostedUser().getDisplayName()); } public void sendToUserFailed(PostEvent event) { // Add your reporting logic here int reason = event.getReason(); String recipient = event.getPostedUser().getDisplayName(); switch (reason) { case STError.ST_USER_NOT_FOUND : System.out.println("User offline:" + recipient); break; case STError.ST_POST_TYPE_NOT_REGISTERED : System.out.println("User not subscribed:" + recipient); break; default : System.out.println( "Send failed:" + reason + " to user:" + recipient); } } |
|
与 JavaScript 交互
尽管您可以生成事件并在 Java applet 中用用户接口显示通知,但若没有 JavaScript,Web 应用程序的其余部分将仍然不知道事件,JavaScript 会将事件从沙箱中取出。该功能具有下列优点:
使用浏览器环境的脚本对象模型,JavaScript 代码可以访问内嵌在 HTML 页面中的 Java applet 的所有公共属性和方法。这个方法在将 HTML UI 组件生成的事件转发给 Sametime 时特别有用。下面的代码显示了来自服务台前端用于实现该技术的 HTML 源的 JavaScript 函数。
<SCRIPT> function publishEvent() { var etype = document.AgentApplet .getStSessionManager().ServiceRequestEventType; var rcps = document.inputform.recipients .options[document.inputform.recipients.selectedIndex].value; var title = document.inputform.client.value + "@" + document.inputform.location.value + "; Phone:" + document.inputform.phone.value; var body = document.inputform.problem.value; document.AgentApplet.getStSessionManager().getPublisher() .publishEvent(etype, title, body, rcps); } </SCRIPT> |
Java applet 与 JavaScript 之间的通信是由 netscape.javascript.JSObject 类实现的,该类是 Netscape 和 Internet Explorer 浏览器运行时环境的一部分。如果在开发环境中查找该类时遇到困难,请查阅本文结尾部分所列出的参考资料。正如下面的示例代码中所显示的,Sametime 代理 applet 提供了一种使用 JSObject API 的方法。需要这种连通性以将接收到的事件传输到 HTML UI。
public class AgentApplet extends Applet { private SessionManager sametimeSession; private static Applet currentApplet; public void init() { currentApplet = this; . . . public static String callJS(String methodName, String[] args) { try { JSObject window = (JSObject) JSObject.getWindow(currentApplet); Object retVal = window.call(methodName, args); if (retVal == null) return null; if (retVal.toString() == "undefined") return null; else return retVal.toString(); } catch (JSException jse) { System.err.println("Call to JScript::" + methodName + " failed!"); jse.printStackTrace(); } catch (Throwable error) { System.err.println("Error in JScript:" + error.getMessage()); } return null; } |
|
结束语
在本文中,我们看到了使用 Sametime Post Service 实现示例 Web 应用程序的事件发布模型。Sametime 软件开发工具包使应用程序能够在多种不同类型的环境中发布事件,包括服务器端进程。为提供精简的示例,我们在发送和接收事件时都使用了 Java applet。最后,使用 Java--JavaScript 接口从 HTML 上下文来回传递通知。请注意这里所演示的方法与所选择的 Web 应用程序平台无关。
我们还分享了向 Sametime 目录中定义的群组发送通知的技术。我们以递归方式为解析名称和查询群组成员提供了两个帮助器类。
如果您非常迫切的想了解IT领域最新产品与技术信息,那么订阅至顶网技术邮件将是您的最佳途径之一。
现场直击|2021世界人工智能大会
直击5G创新地带,就在2021MWC上海
5G已至 转型当时——服务提供商如何把握转型的绝佳时机
寻找自己的Flag
华为开发者大会2020(Cloud)- 科技行者