云中聊天:第 1 部分

工程 | Mark Fisher | 2011 年 8 月 16 日 | ...

上周,RabbitMQ 作为服务在 Cloud Foundry 上可用已宣布。现在,任何在 Cloud Foundry 上运行的应用程序都可以通过 RabbitMQ 代理发送和接收消息,该代理可以仅用一个命令(例如 'vmc create-service rabbitmq')作为服务进行配置。消息传递服务的实例可以在应用程序之间共享,并且由于 RabbitMQ 是一个基于协议的代理,这些应用程序甚至可以使用不同的语言编写。因此,对于那些对在云中运行模块化、多语言、事件驱动的应用程序感兴趣的人来说,这是一个令人兴奋的消息。我将发布一系列博客,重点介绍这些类型的应用程序。在这篇文章中,我将保持简单,重点介绍 Spring 开发人员的初步体验。

首先,我鼓励您查看此教程,这是即使您之前没有 Cloud Foundry 经验的最佳入门方式。在那里,您将看到一个简单的 Spring 应用程序如何使用 Maven 构建,并使用 VMC 命令行工具部署到 Cloud Foundry。该应用程序随后引入了 RabbitMQ,其 MVC 控制器得到增强以发布和检索消息。它展示了如何通过 Spring AMQP 库配置和使用 RabbitMQ 服务。

此外,在 Cloud Foundry 最初宣布的当天(与这些博客文章同一天),我发表了另一篇博客,介绍了“cloud”命名空间支持的基础知识。阅读该博客也可能有助于为您即将看到的内容做好准备。具体来说,我们扩展了“cloud”命名空间,以包含对 RabbitMQ ConnectionFactory 的支持,这一点将在我们下面的配置概述中介绍。

现在,我想介绍另一个示例应用程序,它演示了一个简单的聊天服务器。RabbitMQ 为多功能聊天应用程序提供了极好的骨干,因为它支持不同类型的交换器,例如“direct”/点对点、“topic”主题发布/订阅以及用于简单广播的“fanout”。此外,RabbitMQ 支持多种语言绑定。加上在云中启用消息传递基本上就像拨动开关一样简单,现在许多不同的应用程序都可以轻松共享该服务。如上所述,我将逐步增强该示例,并稍后发布更多博客文章来介绍这些交换器类型和一些多语言聊天,但目前我的目标是提供一个易于访问的起点,仅通过 fanout 交换器进行全局广播。该应用程序的当前状态并不比教程中介绍的复杂多少。我将逐步讲解一些配置和代码,但如果您想跟随并深入了解更多细节,我建议从 github 上的 SpringSource cloudfoundry-samples 仓库克隆示例。

‘rabbit-chat’ 示例应用程序

这是运行中的应用程序的样子

该表单将使用 jQuery 提交 HTTP POST 请求


$('#chatForm').submit(
	function() {
		$.post(
			$('#chatForm').attr("action"),
			$('#chatForm').serialize(),
			function(response) {
				if (response) {
					confirm(response.id);
				}
			});
		$('#text').val("");
		return false;
	});

并且,聊天日志将通过轮询定期更新 - 也使用 jQuery 的 AJAX 支持


$.ajax({
	url : "chatlog",
	success : function(message) {
		if (message && message.length) {
			var messagesDiv = $('#messages');
			messagesDiv.html(message);
			messagesDiv.animate({ scrollTop: messagesDiv.attr("scrollHeight") - messagesDiv.height() }, 150);
		}
		timer = poll();
	},
	error : function() {
		timer = poll();
	},
	cache : false
});

Java 代码

如果您克隆仓库并切换到 'rabbit-chat' 目录,您将看到以下结构

├── pom.xml
├── src
│   └── main
│       ├── java
│       │   └── org
│       │       └── cloudfoundry
│       │           └── samples
│       │               └── rabbitmq
│       │                   └── chat
│       │                       └── ChatController.java
│       ├── resources
│       │   └── static
│       │       └── js
│       │           └── jquery.min.js
│       └── webapp
│           └── WEB-INF
│               ├── spring
│               │   └── servlet-context.xml
│               ├── views
│               │   └── chat.jsp
│               └── web.xml

pom.xml 文件声明了依赖项。特别值得关注的有以下几项

  • spring-webmvc (3.0.5.RELEASE)
  • spring-rabbit (1.0.0.RC3)
  • cloudfoundry-runtime (0.7.1)

web.xml 文件声明了一个 Spring MVC DispatcherServlet 和一个单一的包罗万象的 servlet-mapping ("/")。

如您所见,只有一个名为 "ChatController" 的控制器。它通过注解配置。它使用了 @Controller 和 @RequestMapping 注解以及 @Autowired。由于 ChatController 是应用程序的核心(也是唯一的 Java 代码),让我们快速看一下完整的实现


@Controller
public class ChatController {

	@Autowired
	private volatile AmqpTemplate amqpTemplate;

	private final Queue<String> messages = new LinkedBlockingQueue<String>();

	@RequestMapping(value = "/")
	public String home() {
		return "WEB-INF/views/chat.jsp";
	}

	@RequestMapping(value = "/publish", method = RequestMethod.POST)
	@ResponseStatus(value = HttpStatus.OK)
	public void publish(@RequestParam String username, @RequestParam String text) {
		this.amqpTemplate.convertAndSend(username + ": " + text);
	}

	@RequestMapping(value = "/chatlog")
	@ResponseBody
	public String chatlog() {
		return StringUtils.arrayToDelimitedString(this.messages.toArray(), "<br/>");
	}

	/**
	 * This method is invoked when a RabbitMQ Message is received.
	 */
	public void handleMessage(String message) {
		if (messages.size() > 100) {
			messages.remove();
		}
		messages.add(message);
	}
}

有 3 个控制器方法(用 @RequestMapping 注解的方法),每个方法只有一行代码。其中最简单的是 home(),它返回要渲染的 JSP 的位置。我通常会使用 Spring MVC ViewResolver,但由于这是一个使用 AJAX 的单页面应用程序,所以这是唯一一个直接渲染的视图。正如您所见,无论何时请求应用程序根目录,都会调用 home()

publish(..) 方法用于处理对相对 URL "/publish" 的 HTTP POST 请求,并且它期望请求中有两个参数:username 和 text。这些参数由您在上一节中看到的 HTML 表单提供。该表单由 chat.jsp 渲染。publish 方法除了将 username + text 值连接成一个字符串,然后由 AmqpTemplate 将其转换为 AMQP 消息并发送之外,不执行任何其他操作,之后它返回一个简单的 HTTP 200 (OK) 状态。模板实例已自动装配到 Controller 中。我们很快将查看 AmqpTemplate 的配置以及启用在 Cloud Foundry 上使用 RabbitMQ 服务的底层 ConnectionFactory

chatlog() 方法仅返回最多 100 条最近的聊天消息。它是上一节中显示的 AJAX 请求轮询的方法。handleMessage(..) 方法负责将这些聊天消息排队,因此它是连接到底层消息监听器的方法。这几乎涵盖了应用程序的功能。

Spring 配置

现在,我们可以逐步了解此应用程序的配置。这本来可以用 Java 和注解完全实现,但希望您同意这是一个相当简洁的配置文件


<context:component-scan base-package="org.cloudfoundry.samples.rabbitmq.chat"/>

<mvc:annotation-driven/>

<mvc:resources location="file:./src/main/resources/static/,classpath:/static/" mapping="static/**"/>

<rabbit:queue id="chatQueue"/>

<rabbit:fanout-exchange name="chatExchange">
	<rabbit:bindings>
		<rabbit:binding queue="chatQueue"/>
	</rabbit:bindings>
</rabbit:fanout-exchange>

<rabbit:template connection-factory="rabbitConnectionFactory" exchange="chatExchange"/>

<rabbit:admin connection-factory="rabbitConnectionFactory"/>

<rabbit:listener-container>
	<rabbit:listener queues="chatQueue" ref="chatController" method="handleMessage"/>
</rabbit:listener-container>

<cloud:rabbit-connection-factory id="rabbitConnectionFactory"/>

'component-scan' 元素使得带有 @Controller 注解的类可以注册为 Spring 管理的对象,并激活了对 @Autowired 的支持。带有 'mvc' 前缀的两个元素简单地设置了 MVC 的 @RequestMapping 支持,并启用了静态资源的加载(在此用于提供 'resources/static/js' 目录中的 jQuery 支持)。

其余元素与 RabbitMQ 配置相关。“rabbit:admin”生成一个 RabbitAdmin 实例,负责识别在同一应用程序上下文中定义的 Exchanges、Queues 和 Bindings。请注意,queue 元素将其 id 设置为 "chatQueue",但该 id 没有 "name" 属性。这将触发创建一个具有唯一、生成的名称的 Queue,该 Queue 专属于此特定应用程序。换句话说,“id”属性的值不映射到 Queue 的名称;它是 Spring bean 的 id,而不是 RabbitMQ Queue 的 id。尽管它具有生成的名称,但它需要在此应用程序上下文中可识别以供引用。例如,您可以看到它在在此定义为 Fanout Exchange 的 "chatExchange" 的绑定中被引用。该交换器也将向 broker 声明,因为存在 "rabbit:admin" 元素。

“rabbit:template”相当简单。它需要引用 ConnectionFactory(别担心,我们稍后会讲到),如果您希望其“send”方法发布到除了无名的默认 Exchange 之外的 Exchange,您可以在此处提供。我们正在发布到刚刚讨论过的 "chatExchange"。

“rabbit:listener-container”与 Spring 的 JMS 支持中同名元素几乎完全相同。这个正在监听 "chatQueue",请记住这只是对 bean id 的引用,该特定 Queue 的真实名称由 broker 生成。每当消息到达该 Queue 时,我们之前看到的 "handleMessage" 方法将被调用。在方法参数为 String 的情况下,监听器容器的适配器将自动处理 Message 主体的转换。由于该方法参数不需要接受实际的 Message 实例,并且方法名称可以是任意名称,我们将这称为“消息驱动 POJO”。换句话说,它与消息 API 没有直接依赖关系。它被监听器容器调用是一种控制反转的形式。

最后是 Connection Factory 配置。在这种情况下,我们使用“cloud”命名空间及其“rabbit-connection-factory”元素。只要您的应用程序绑定到 Cloud Foundry 中的单个“rabbitmq”服务,创建 ConnectionFactory 实例就不需要其他信息。该命名空间支持的基础代码将从环境本身确定凭据。该命名空间支持由“cloudfoundry-runtime”库提供,您可以在此应用程序的 pom.xml 文件中看到其声明。

运行应用程序

您可以使用 vmc 命令行工具SpringSource Tool Suite 运行应用程序。使用 vmc,您会看到类似如下内容

$ vmc push
Would you like to deploy from the current directory? [Yn]: y
Application Name: rabbit-chat-sample
Application Deployed URL: 'rabbit-chat-sample.cloudfoundry.com'? 
Detected a Java SpringSource Spring Application, is this correct? [Yn]: y
Memory Reservation [Default:512M](64M, 128M, 256M, 512M or 1G)   
Creating Application: OK
Would you like to bind any services to 'rabbit-chat-sample'? [yN]: y
Would you like to use an existing provisioned service [yN]? n
The following system services are available:
1. mongodb
2. mysql
3. postgresql
4. rabbitmq
5. redis
Please select one you wish to provision: 4
Specify the name of the service [rabbitmq-5e262]:          
Creating Service: OK
Binding Service: OK
Uploading Application:
  Checking for available resources: OK
  Processing resources: OK
  Packing application: OK
  Uploading (3K): OK   
Push Status: OK
Staging Application: OK                                                         
Starting Application: OK

如果使用 STS,您只需要启用 Cloud Foundry 支持(可在 Dashboard 的“Extensions”选项卡中找到),然后创建一个新的服务器实例(请参阅 入门指南了解所有详细信息),然后您可以简单地将应用程序拖到其中。一旦应用程序添加到服务器实例中,您就可以通过 UI 配置和绑定服务。以下截图显示了 'rabbit-chat' 示例应用程序。

伸缩应用程序

还记得关于 "chatQueue" id 的讨论以及 Queue 的名称是如何生成的吗,因为它使用了 id 而不是 name 属性?嗯,我们在这里使用 id 而不是 name 的原因是我们希望应用程序的每个实例都有自己的专属且实际上是匿名的 Queue。Fanout Exchange 有一个单一的命名实例。应用程序的每个实例都会将其自己的 Queue 绑定到该 Exchange。这种 Exchanges 和 Queues 的解耦非常适合可伸缩的云应用程序(特别是那些名称是生成的 Queue 在其拥有实例关闭时会自动删除)。

要伸缩应用程序,您可以在命令行使用 VMC

$ vmc instances rabbit-chat-sample +1
Scaling Application instances up to 2: OK

或者,您可以使用 STS 支持。这是从上面同一截图截取的“实例”配置的聚焦视图

下一步是什么?

本博客旨在成为系列文章的第一篇。在接下来的文章中,我们将探讨以下内容

  • 增强消息传递功能,超越当前的全局广播,以演示点对点消息传递(与单个命名用户聊天)以及使用动态分配的 Exchanges 来表示“聊天室”的发布/订阅。
  • 添加一个 Node.js 应用程序,与 Java 应用程序并行运行并参与同一聊天,因为两个应用程序都绑定到同一个 RabbitMQ 服务实例。
  • 包含 Spring 3.1 profile 支持,以展示如何修改此同一配置文件,使其在 Cloud Foundry 或替代部署(例如本地 Tomcat 实例)上同样良好运行。

订阅 Spring 新闻通讯

通过 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,助您加速进步。

了解更多

获得支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,只需一次简单订阅。

了解更多

即将到来的活动

查看 Spring 社区所有即将到来的活动。

查看全部