在 Eclipse 中构建支持 AIM 的应用程序

即时消息传递(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 接口的类之前,需要导入这些库,并将它们添加到类路径中。

在 Eclipse 中构建支持 AIM 的应用程序
日志记录

我将 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 的类

在 Eclipse 中构建支持 AIM 的应用程序

添加类之后,它看上去如清单 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. 将环境变量添加到配置中

在 Eclipse 中构建支持 AIM 的应用程序

如果没有正确地将 description 设置为开发人员密匙,则会遇到错误 com.aol.acc.AccException: IAccClientInfo_SetDescription

我直接从 AOL 站点复制开发人员密匙,所以 AIM_KEY 常量的值为 My Super Shopper (Key:my1XzlXXXXXXXXXX)。在代码下载中,我增加了一个 EchoMessageHandler 用于回显消息。它还记录传入的消息和屏幕名,所以它对于测试比较有用。

结束语

通过使用 AIM SDK,可以创建一个定制的 Java 客户机,它使 Java 应用程序可以使用 IM 接受用户的消息并向用户返回响应。通过使用本文提供的模式,可以创建易于维护和扩展的应用程序扩展。