级别: 中级 Nathan A. Good, 高级信息工程师, 自由开发人员
2009 年 3 月 16 日 如今,应用程序利用了人们已经广泛使用的一个界面:即时信息传递(IM)。应用程序提供与 IM 的集成,因为这使用户可以通过他们熟悉的、很多人已经在使用的界面轻松地访问应用程序。IM 应用程序还可以在很多移动平台上使用,这使用户可以从移动设备连接到应用程序。
即时消息传递(Instant messaging,IM)可作为一种为已有或新应用程序构建界面的好方法。很多人使用 IM,并且有些人只要在计算机运行的情况下就会打开并运行他们的 IM 应用程序 — 例如 AOL Instant Messenger(AIM)。IM 客户机不但出现在计算机上,而且还出现在移动设备上,例如 Personal Digital Assistants(PDA)和手机。
通过为应用程序构建一个界面,使用户能通过 IM 连接到应用程序,从而利用很多现有的网络通信基础设施。对于已经具有
IM ID 并且运行 IM 客户机的用户,这样做还为他们提供了一种便利的方式来访问应用程序。
本文演示如何构建一个 Java™ 应用程序,该应用程序使用 AOL 的客户机软件开发工具包(SDK)库从用户那里获取命令。该应用程序将能够处理命令,并将结果返回给用户。与此同时,本文还介绍一些设计模式,这些设计模式可用于构建易于扩展和维护的应用程序。
系统需求
为了能够有效地练习本文的示例,计算机上应该安装有
Eclipse integrated development environment(IDE)V3.4 或更高版本。
另外要理解并运行示例,还应熟悉 Java 编程语言。
AIM API 简介
现在有很多 IM 服务。本文主要关注 AOL 的 AIM 服务。AOL 提供了一个免费的 SDK,可以用它来构建可连接并使用 AOL 服务的应用程序(参见 参考资料)。
要下载 SDK,必须同意 AOL 关于这个库的使用条款。另外还需要获得该 API 的开发人员密匙(developer key)。请按照在线说明获取定制的客户机密匙(client key),因为后面将构建一个自动化的、定制的 AIM 客户机。
下载打包为 ZIP 或 tar.gz 文件(accsdk_macosx_univ_1_6_8.tar.gz)的 SDK 后,将它保存到计算机中的某个位置。该归档文件中包括 Java Archive(JAR)文件和需要的其他库文件。它还包含 Java 应用程序编程接口(API)的 JavaDoc,所以您可能希望解压这些文件,以便阅读适用于所下载的 API 版本的 JavaDoc。由于 Eclipse 允许从归档文件中导入库文件,所以不一定需要解压这些文件。
Microsoft® Windows®、Mac OS® X 和 Linux® 上都有可用的 AOL AIM SDK 版本。首先,应确保下载了适用于操作系统的正确版本。如果计划在某个操作系统上开发应用程序,然后将它部署到另一个操作系统中,那么需要同时具有两个版本的库。此外还有其他一些用于与 AIM 通信的库,尤其是用 Java 代码编写的开源库。我选择使用 AOL 的 SDK,因为我正在使用那个服务。
AOL 的站点提供了一些例子,通过这些例子可以熟悉该 API。
获得 AIM Bot ID
在登录和测试服务之前,需要一个 AIM ID。而且,还需要在 AIM 站点上对将用于应用程序的 ID 执行一个 “Bot My Screenname” 过程(参见 参考资料)。建议立即创建这个特别的 ID。它将使测试更加容易,因为可以使用个人 AIM 屏幕名尝试与应用程序通信。
创建项目
如果还没有要使用的 Java 项目,那么需要增加一个新的 Java 项目。使用 File > New 打开 new Java project 向导,遵从向导中的步骤增加新的 Java 项目。如果要使用一个已有的 Java 项目 — 例如已经在构建的一个应用程序 — 那么可以跳过这一步。
导入和安装 Java API
将 IM 功能添加到应用程序时所使用的 Java 类和接口位于 accwrap.jar 文件中,该文件位于归档文件中的 dist/release/lib 目录。在构建最终实现 AccEvents 接口的类之前,需要导入这些库,并将它们添加到类路径中。
 | 日志记录
我将 log4j 用于应用程序中的日志记录。为了减少依赖,也可以使用 java.util.logging 名称空间中的 Java Logging。请参阅 参考资料,了解不同的日志记录实现。强烈建议使用基于 System.out.println() 的日志记录解决方案。
|
|
在构建 Java 应用程序时,我通常在 Java 项目中创建一个 lib 目录,并将所有 JAR 文件放入到 lib 文件夹中的目录。我使用该文件夹中的库的名称和版本来命名目录。例如,我使用 Apache 的 log4J
日志记录实用程序记录消息,以便进行调试。JAR 文件相对于工作区的路径是 lib/apache-log4j-1.2.15/log4j-1.2.15.jar。
为了导入 Java API 和用于 AOL SDK 的依赖文件,我打破了这个惯例。这一次,我将 JAR 文件与其他文件一起放在项目根目录下的 dist/release/lib 文件夹中。这是因为无论在类路径中指定任何位置,当运行应用程序时,Java Runtime Environment(JRE)都将查找 accwrap.jar。但是,它将在当前工作目录中查找该库。如果仍然想把这些文件放入到一个指定的文件夹中,那么完全可以这样做,只需更新应用程序的运行配置,指定该文件夹的位置。我发现这个解决方案在团队环境中存在问题,在这种环境下,定制的运行配置可能变得有些繁琐。
如果库文件较多,易于分散注意力,或者在 Package Explorer 在过于杂乱,那么可以添加一个视图过滤器,隐藏这些文件。
在 Package
Explorer 中选择 accwrap.jar 文件,并从上下文菜单中选择 Build Path > Add to Build Path,将该 JAR 文件添加到项目的构建路径中。或者,使用项目属性配置构建路径,添加 accwrap.jar 文件。
使用 AIM Java API
至此,您应该具有了一个 Java 项目,并且已经将 accwrap.jar 和库文件导入到该项目中。
要开始构建连接应用程序和 AIM 的界面,需要添加一个实现
AccEvents 接口的类。本文中的示例类还包括 main() 函数,但这不是必需的。
添加这个类的最简便方法是使用 File > New > Class 向导。输入包名和类名,如图 1 所示。单击 Interfaces 旁的 Add,添加一个新的接口,并输入名称 AccEvents。由于已经将 accwrap.jar 文件添加到构建路径中,这时应该可以在列表中找到这个接口。
图 1. 添加实现 AccEvents 的类
添加类之后,它看上去如清单 1 所示。为简单起见,清单 1 没有包括该接口中的许多方法,因为本文不需要使用所有这些方法。
清单 1. 新的实现类
package com.nathanagood.shopper;
import com.aol.acc.*;
public class MySuperShopperBot implements AccEvents {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
}
public void OnImReceived(AccSession session, AccImSession imSession,
AccParticipant participant, AccIm im) {
// TODO: Add implementation
}
public void OnStateChange(AccSession session, AccSessionState state,
AccResult result) {
// TODO: Add implementation
}
/* Many other methods for AccEvents snipped... */
}
|
添加代码
如清单 2 所示,main() 方法创建这个类的一个新的实例,并调用
signOn() 方法。
清单 2. main 方法
public static void main(final String[] args) {
logger.info("Starting My Super Shopper bot...");
MySuperShopperBot bot = new MySuperShopperBot();
try {
bot.signOn();
} catch (AccException ae) {
logger.error("An error occurred while trying to sign on.", ae);
}
logger.info("Shutting down bot...");
}
|
如清单 3 所示,构造函数使用一个工厂 MessageHandlerFactory 创建 MessageHandler 的一个实例,并将它赋给 messageHandler 变量。AccEvents.OnIMReceived() 方法的实现稍后将使用该对象做实际的工作。
清单 3. MySuperShopperBot 构造函数
public MySuperShopperBot() {
messageHandler = MessageHandlerFactory.createMessageHandler();
}
|
如清单 4 所示,signOn() 方法创建 AccSession 对象的一个新的实例,设置该实例,并开始循环侦听传入的消息。
清单 4. signOn() 方法
public void signOn() throws AccException {
session = new AccSession();
session.setEventListener(this);
AccClientInfo info = session.getClientInfo();
info.setDescription(AIM_KEY);
session.setIdentity(AIM_USERNAME);
session.setPrefsHook(new MySuperShopperPrefs());
session.signOn(AIM_PASSWORD);
while (isRunning) {
try {
AccSession.pump(50);
} catch (Exception e) {
logger.error("Exception occurred while handling message", e);
}
try {
Thread.sleep(50);
} catch (InterruptedException e) {
logger.warn("Thread was interrupted", e);
}
}
info = null;
session = null;
System.gc();
System.runFinalization();
}
|
signOn() 方法在登录之前设置很多的属性。下载 SDK 时获得的开发人员密匙在 setDescription() 中设置。AIM 屏幕名通过 setIdentity() 方法设置,密码则作为 signOn() 的参数提供。
循环中的代码(pump()、Thread.sleep())使服务一直运行,并侦听消息。
对于本文中的示例,我只将实现放在两个方法中。其中一个是 OnIMReceived() 方法,如清单 5 所示。它获取传入的 IM 消息的纯文本字符串值。然后询问 messageHandler 是否能处理该消息。如果 messageHandler 知道如何处理该消息,则调用 handleMessage() 方法。另一种方案是构建一个大的方法,使用 if/else 语句来控制流。设计模式 小节中介绍了更多有关这些模式的信息。
清单 5. OnIMReceived() 方法
public void OnImReceived(AccSession session, AccImSession imSession,
AccParticipant participant, AccIm im) {
String message;
try {
message = im.getConvertedText(PLAIN_TEXT);
/* Provide a way to cleanly shut down the client */
if (message.equals(SHUTDOWN_COMMAND)) {
session.signOff();
} else {
if (messageHandler.canHandle(message)) {
String response = messageHandler.handle(message,
participant.getName());
im.setText(response);
imSession.sendIm(im);
}
}
} catch (AccException e) {
logger.error("Error receiving message.", e);
}
}
|
最后,我硬编码了一个值,以便在测试时使用它干净地关闭 IM 服务。如清单 6 所示,我在 OnStateChange() 方法中添加了代码,将 isRunning 状态设为 false,以便让应用程序完全退出循环。
清单 6. OnStateChange() 方法
public void OnStateChange(AccSession session, AccSessionState state,
AccResult result) {
if (state == AccSessionState.Offline) {
isRunning = false;
}
}
|
设计模式
在本文中,我演示了一些设计模式,这些设计模式可用于构建可扩展、易于维护的应用程序,并且只需要进行简单修改就可以结合使用已有应用程序,而不必将 AccEvents 实现代码与其他应用程序代码紧密耦合。
第一种模式是 strategy 模式,MessageHandler 接口采用的就是这种模式。它允许任何实现以特定的方式处理消息。strategy 模式的一个变体是提供一个 canHandle() 方法,让实现本身表示它是否能适当处理消息。如果使用这个变体,当为消息选择适当的实现时,就不必借助工厂来获知是否能够适当处理消息。
清单 7 中的 DelegatingMessageHandler 类使用了 decorator 模式。在类似于过滤器链的结构中,构造函数中包含一系列的 MessageHandler 对象。在 MessageHandler 接口的 canHandle() 方法的自身实现中,它遍历这些注册过的处理程序,以寻找知道如何处理传入消息的处理程序。当发现一个这样的处理程序时,便将消息传递给它。
清单 7. DelegatingMessageHandler 类
package com.nathanagood.shopper.handlers;
import java.util.List;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
public final class DelegatingMessageHandler implements MessageHandler {
private final static Logger logger = LogManager
.getLogger(DelegatingMessageHandler.class);
private List<MessageHandler> handlers;
public DelegatingMessageHandler(final List<MessageHandler> handlers) {
this.handlers = handlers;
}
/**
*
* @param handler
*/
public void registerHandler(MessageHandler handler)
{
handlers.add(handler);
}
public boolean canHandle(final String message) {
return (handlers != null);
}
public String handle(final String message, final String user) {
String result = "I don't understand that...";
for (MessageHandler handler : handlers) {
if (handler.canHandle(message)) {
logger.debug("Processing message \"" + message
+ "\" from user \"" + user + "\" with handler \""
+ handler.getClass()。getCanonicalName() + "\"");
result = handler.handle(message, user);
logger.debug("Returning result \"" + result + "\"");
break;
}
}
return result;
}
}
|
清单 8 中的 MessageHandlerFactory.createMessageFactory() 方法使用了 factory 模式,该方法创建、初始化和返回 DelegatingMessageHandler 的一个实例。通过使用一个工厂来创建实现,调用者不需要知道任何关于初始化的细节。
清单 8. MessageHandlerFactory 类
package com.nathanagood.shopper.handlers;
import java.util.ArrayList;
import java.util.List;
/**
* Factory for creating a {@link MessageHandler}.
* @author Nathan A. Good
*/
public class MessageHandlerFactory {
/**
* Creates a {@link MessageHandler} implementation.
* @return MessageHandler.
*/
public static MessageHandler createMessageHandler() {
List<MessageHandler> handlers = new ArrayList<MessageHandler>();
handlers.add(new ShoppingListMessageHandler());
// handlers.add(new EchoMessageHandler()); // useful for testing...
DelegatingMessageHandler handler = new DelegatingMessageHandler(handlers);
return handler;
}
}
|
使用这些模式而不是将代码直接放在 OnIMReceived() 方法中,这样做有一些优点。例如,只需稍作修改,就可以为 state 模式引入持久性,以跟踪会话状态。
对消息作出响应
用实现类处理消息后,可能还需要将一条消息返回给用户。如果同步地处理和响应消息(例如本文中的例子),那么可以使用 sendIm() 方法作出响应。
清单 9. 在 OnImReceived 中向用户作出响应
public void OnImReceived(AccSession session, AccImSession imSession,
AccParticipant participant, AccIm im) {
String message;
try {
message = im.getConvertedText(PLAIN_TEXT);
if (message.equals("goodbye")) {
session.signOff();
} else {
if (messageHandler.canHandle(message)) {
String response = messageHandler.handle(message,
participant.getName());
im.setText(response);
imSession.sendIm(im);
}
}
} catch (AccException e) {
logger.error("Error receiving message.", e);
}
}
|
如果异步地处理消息,那么需要使用接受响应的用户的屏幕名创建 AccImSession
对象的一个新的实例,如清单 10 所示。screenname 变量是用户的屏幕名,message 是 IM 内容,其类型为字符串。如果消息处理较为费时,异步处理消息比较有用。
清单 10. 创建新的 IM 消息并发送它
AccImSession imSession = session.createImSession(screenname, AccImSessionType.Im);
imSession.sendIm(session.createIm(message, "text/plain"));
|
添加一个 MessageHandler 实现
如清单 11 所示,MessageHandler 的实现 ShoppingListMessageHandler 允许 MySuperShopper 将购物清单中的商品添加到数据库中,以便之后可以从移动设备上检索它们。
清单 11. ShoppingListMessageHandler 类
package com.nathanagood.shopper.handlers;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import com.nathanagood.shopper.persistence.ShoppingListItem;
import com.nathanagood.shopper.persistence.ShoppingListManager;
public class ShoppingListMessageHandler implements MessageHandler {
private static final Logger logger = LogManager.getLogger(ShoppingListManager.class);
private static final Pattern addCommandPattern = Pattern
.compile("^\\s*add +([\\d]+)[ -]+(.*)$");
public boolean canHandle(final String message) {
return addCommandPattern.matcher(message)。matches();
}
public String handle(final String message, final String user) {
String result = "An error occurred while adding your item.";
try {
if ( addCommandPattern.matcher(message)。matches() ) {
ShoppingListManager manager = new ShoppingListManager();
manager.addShoppingListItem(user, parse(message));
result = "Successfully added item.";
}
} catch (Exception e) {
logger.error("Error while handling item.", e);
}
return result;
}
private ShoppingListItem parse(final String value) {
int quantity = 0;
String description = "";
Matcher match = addCommandPattern.matcher(value);
if ( match.find() ) {
quantity = Integer.parseInt(match.group(1));
description = match.group(2);
}
logger.debug("Parsed item with \"" + quantity + "\" number of \"" +
description + "\"");
return new ShoppingListItem(quantity, description);
}
}
|
本文附带的代码中包括了 ShoppingListManager 类。该实现对于这个例子不太重要。重要的是,
ShoppingListManager 类做了一些事情来持久化用户的购物清单中的商品。
运行应用程序
在运行应用程序之前,使用个人屏幕名登录到 AIM 中,并将应用程序的屏幕名加为好友。这样一来,当应用程序启动时,就可以看到它上线。可以发送一些消息,对它进行测试。
添加了所有实现类后,就可以使用 Project > Run 运行应用程序。项目启动后,应该可以在 IM 客户机上看到它上线。
故障排除
由于一开始我并没有将库文件放在项目的基目录中,所以我收到这样的消息:Exception in thread "main" java.lang.UnsatisfiedLinkError。
在 Windows 上,只需确保本地库(例如动态链接库或 DDL)位于工作目录中,就可以解决这个问题。但是,在 Mac 上,需要将环境变量 DYLD_LIBRARY_PATH 设为项目的工作区位置。
图 2. 将环境变量添加到配置中
如果没有正确地将 description 设置为开发人员密匙,则会遇到错误 com.aol.acc.AccException: IAccClientInfo_SetDescription。
我直接从 AOL 站点复制开发人员密匙,所以 AIM_KEY 常量的值为
My Super Shopper (Key:my1XzlXXXXXXXXXX)。在代码下载中,我增加了一个 EchoMessageHandler 用于回显消息。它还记录传入的消息和屏幕名,所以它对于测试比较有用。
结束语
通过使用 AIM SDK,可以创建一个定制的 Java 客户机,它使 Java 应用程序可以使用 IM 接受用户的消息并向用户返回响应。通过使用本文提供的模式,可以创建易于维护和扩展的应用程序扩展。
下载 | 描述 | 名字 | 大小 | 下载方法 |
|---|
| 样例代码 | os-blackberry2-IBMRssReader_src.zip | 112KB | HTTP |
|---|
参考资料 学习
获得产品和技术
讨论
关于作者  | |  | Nathan Good 居住在明尼苏达州的双子城。他的专长是软件开发、软件架构和系统管理。平时不编写软件时,他喜欢组装 PC 和服务器、阅读和撰写技术文章,并鼓励他的所有朋友转用开源软件。他是许多书籍和文章的作者或合著者,包括 Professional Red Hat Enterprise Linux 3, Regular Expression Recipes: A Problem-Solution Approach 和 Foundations of PEAR: Rapid PHP Development。 |
对本文的评价
|