array(2) { ["docs"]=> array(10) { [0]=> array(10) { ["id"]=> string(3) "428" ["text"]=> string(77) "Visual Studio 2017 单独启动MSDN帮助(Microsoft Help Viewer)的方法" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(8) "DonetRen" ["tagsname"]=> string(55) "Visual Studio 2017|MSDN帮助|C#程序|.NET|Help Viewer" ["tagsid"]=> string(23) "[401,402,403,"300",404]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400964" ["_id"]=> string(3) "428" } [1]=> array(10) { ["id"]=> string(3) "427" ["text"]=> string(42) "npm -v;报错 cannot find module "wrapp"" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "zzty" ["tagsname"]=> string(50) "node.js|npm|cannot find module "wrapp“|node" ["tagsid"]=> string(19) "[398,"239",399,400]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400760" ["_id"]=> string(3) "427" } [2]=> array(10) { ["id"]=> string(3) "426" ["text"]=> string(54) "说说css中pt、px、em、rem都扮演了什么角色" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(12) "zhengqiaoyin" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511400640" ["_id"]=> string(3) "426" } [3]=> array(10) { ["id"]=> string(3) "425" ["text"]=> string(83) "深入学习JS执行--创建执行上下文(变量对象,作用域链,this)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "Ry-yuan" ["tagsname"]=> string(33) "Javascript|Javascript执行过程" ["tagsid"]=> string(13) "["169","191"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511399901" ["_id"]=> string(3) "425" } [4]=> array(10) { ["id"]=> string(3) "424" ["text"]=> string(30) "C# 排序技术研究与对比" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "vveiliang" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(8) ".Net Dev" ["catesid"]=> string(5) "[199]" ["createtime"]=> string(10) "1511399150" ["_id"]=> string(3) "424" } [5]=> array(10) { ["id"]=> string(3) "423" ["text"]=> string(72) "【算法】小白的算法笔记:快速排序算法的编码和优化" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(9) "penghuwan" ["tagsname"]=> string(6) "算法" ["tagsid"]=> string(7) "["344"]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511398109" ["_id"]=> string(3) "423" } [6]=> array(10) { ["id"]=> string(3) "422" ["text"]=> string(64) "JavaScript数据可视化编程学习(二)Flotr2,雷达图" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "chengxs" ["tagsname"]=> string(28) "数据可视化|前端学习" ["tagsid"]=> string(9) "[396,397]" ["catesname"]=> string(18) "前端基本知识" ["catesid"]=> string(5) "[198]" ["createtime"]=> string(10) "1511397800" ["_id"]=> string(3) "422" } [7]=> array(10) { ["id"]=> string(3) "421" ["text"]=> string(36) "C#表达式目录树(Expression)" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(4) "wwym" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(4) ".NET" ["catesid"]=> string(7) "["119"]" ["createtime"]=> string(10) "1511397474" ["_id"]=> string(3) "421" } [8]=> array(10) { ["id"]=> string(3) "420" ["text"]=> string(47) "数据结构 队列_队列实例:事件处理" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(7) "idreamo" ["tagsname"]=> string(40) "C语言|数据结构|队列|事件处理" ["tagsid"]=> string(23) "["246","247","248",395]" ["catesname"]=> string(12) "数据结构" ["catesid"]=> string(7) "["133"]" ["createtime"]=> string(10) "1511397279" ["_id"]=> string(3) "420" } [9]=> array(10) { ["id"]=> string(3) "419" ["text"]=> string(47) "久等了,博客园官方Android客户端发布" ["intro"]=> string(288) "目录 ECharts 异步加载 ECharts 数据可视化在过去几年中取得了巨大进展。开发人员对可视化产品的期望不再是简单的图表创建工具,而是在交互、性能、数据处理等方面有更高的要求。 chart.setOption({ color: [ " ["username"]=> string(3) "cmt" ["tagsname"]=> string(0) "" ["tagsid"]=> string(2) "[]" ["catesname"]=> string(0) "" ["catesid"]=> string(2) "[]" ["createtime"]=> string(10) "1511396549" ["_id"]=> string(3) "419" } } ["count"]=> int(200) } 222 在 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 接受用户的消息并向用户返回响应。通过使用本文提供的模式,可以创建易于维护和扩展的应用程序扩展。

相关文章: