内容


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

支持应用程序与 AIM 交互

Comments

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

在构建 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 的类
添加实现 AccEvents 的类
添加实现 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 接受用户的消息并向用户返回响应。通过使用本文提供的模式,可以创建易于维护和扩展的应用程序扩展。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Open source, Java technology
ArticleID=376166
ArticleTitle=在 Eclipse 中构建支持 AIM 的应用程序
publish-date=03162009