在实际项目开发中经常会有应用集成的需求,将几个分离的应用程序整合到一起,相互之间进行通信(传递消息或协同工作)或数据共享。Spring Integration可以很好地满足这一需求。SpringIntegration能在基于Spring的应用中进行轻量级的消息通信,并通过适配器与外部系统集成。这些适配器提供了一个更高级别的抽象,超越了Spring对远程调用、消息队列和调度的支持。
wgrus样例程序(实在想不通这名字怎么来的)就使用了SpringIntegration实现了两个简单的Spring应用的集成。两个Spring应用分别是wgrus-store和wgrus-inventory,前者接收用户提交的订单并通过Integration的消息通道(channel)转发,后者则从通道中提取消息并放入一个订单队列中缓存,该队列只记录最近25条订单信息。当然消息传递的过程比这稍微复杂一点,要流经一条channel链条。另外虽然程序中注入了mongodb依赖,但运行中并未发生数据的持久化。说白了就是多此一举
一、消息生产者wgrus-store
store部分的controller只有1个,StoreFront.java :
package org.wgrus.web;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.integration.MessageChannel;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.wgrus.Order;
/**
* Handles order requests.
*/
@Controller
@RequestMapping(value="/")
public class StoreFront {
private final AtomicLong orderIdCounter = new AtomicLong(1);
@Autowired @Qualifier("orderChannel")
private MessageChannel orderChannel;
@RequestMapping(method=RequestMethod.GET)
public String displayForm() {
return "order";
}
@RequestMapping(method=RequestMethod.POST)
public String placeOrder(@RequestParam String email, @RequestParam int quantity, @RequestParam String productId, Model model) {
long orderId = orderIdCounter.getAndIncrement();
Order order = new Order(orderId);
order.setEmail(email);
order.setQuantity(quantity);
order.setProductId(productId);
MessagingTemplate template = new MessagingTemplate(this.orderChannel);
template.convertAndSend(order);
model.addAttribute("orderId", orderId);
return "order";
}
}
在它的post方法中生成了一个Order订单对象并使用Spring Integration的消息模板MessagingTemplate发送到了一个MessageChannel通道中,这就完成了一个订单的提交。看到这里不免心生疑惑,这个通道究竟是干什么的,消息进入通道就OK了?下面我们就来看一下通道内部的细节,这些细节就在应用程序上下文配置wgrus-store / src / main / resources / root-context.xml 当中。
应用程序上下文中除了我们所熟悉的rabbit、mongo以及Cloud Foundry的cloud等命名空间之外,在最顶层的bean中还引用了一个新的xmlns:int
xmlns:int="http://www.springframework.org/schema/integration"
xsi:schemaLocation=http://www.springframework.org/schema/integration/amqp http://www.springframework.org/schema/integration/amqp/spring-integration-amqp-2.1.xsd
http://www.springframework.org/schema/integration http://www.springframework.org/schema/integration/spring-integration-2.1.xsd
"int:"是Spring Integration的命名空间。引入它,就可以进行应用集成的消息通道配置了,像下面这样:
<int:channel id="orderChannel"/>
<int:object-to-json-transformer input-channel="orderChannel" output-channel="jsonOrders"/>
<int:claim-check-in input-channel="jsonOrders" output-channel="amqpOut"/>
<int:channel id="amqpOut"/>
<amqp:outbound-channel-adapter channel="amqpOut" amqp-template="rabbitTemplate" routing-key="orders"/>
这里首先定义了消息通道的入口"orderChannel",也就是controller中消息所发向的目标;之后消息再流向第二段通道"jsonOrders",从标签元素的名字不难看出这一过程将Order对象转换成了json格式。之所以进行消息格式转换,是为了使消息生产者和消息消费者之间达到松耦合,从而使得双方在处理消息时均不需要担心消息格式是否有效。转换为json的消息继而流向第三段通道"amqpOut",这可以看做是通道的终点,只是还需一个adapter适配一下,使Integration消息与rabbit模板相匹配。订单消息经过以上的流程之后就被放入了rabbit消息队列"orders"中,供wgrus-inventory消费(consume)。Rabbit队列定义:
<rabbit:queue name="orders"/>
<rabbit:admin connection-factory="rabbitConnectionFactory"/>
<rabbit:template id="rabbitTemplate" connection-factory="rabbitConnectionFactory"/>
再来看看消息的消费者wgrus-inventory在干什么。
二、消息消费者wgrus-inventory
inventory部分也只有1个controller,将从消息队列中获得的订单信息罗列出来:
/**
* Handles order requests.
*/
@Controller
@RequestMapping(value="/")
public class OrdersView {
@Autowired
private OrderQueue orderQueue;
@RequestMapping(method=RequestMethod.GET)
public String display(Model model) {
model.addAttribute("count", orderQueue.count());
model.addAttribute("orders", orderQueue.list());
return "orders";
}
}
其中OrderQueue类型的属性是一个队列,存储最近的至多25条订单信息。那么inventory是如何获得订单信息的呢?当然也是从Spring Integration的通道了。应用程序上下文 wgrus-inventory / src / main / resources / root-context.xml 中的通道配置如下:
<amqp:inbound-channel-adapter channel="orderChannel"
connection-factory="rabbitConnectionFactory"
queue-names="orders"
advice-chain="retryInterceptor" />
<bean id="retryInterceptor" class="org.springframework.amqp.rabbit.config.StatelessRetryOperationsInterceptorFactoryBean" />
<int:chain input-channel="orderChannel" output-channel="warehouseChannel">
<int:claim-check-out/>
<int:json-to-object-transformer type="org.wgrus.Order"/>
</int:chain>
<int:publish-subscribe-channel id="warehouseChannel"/>
<int:chain input-channel="warehouseChannel">
<int:object-to-string-transformer/>
<int:service-activator ref="orderQueue" method="add"/>
</int:chain>
<int:logging-channel-adapter id="logger" channel="warehouseChannel" level="WARN"/>
读取消息时也需要适配器,这里的amqp:inbound-channel-adapter元素的属性值应与wgrus-store部分对应。消息消费者先将消息从"orderChannel"导向"warehouseChannel",在这条channel链中将json消息转换回Order类对象。"warehouseChannel"是一条发布/订阅通道,消息从这里流过时,订阅了"warehouseChannel"的订阅者通道会依次收到通知。这里有两个订阅者,第一个订阅者是一条chain,它先经由转换器把Order对象消息转换成字符串表示(第12行),然后激活orderQueue的add方法把此订单添加到订单队列中(第13行);第二个订阅者是日志通道适配器"logger"。这就是inventory收到消息的处理过程。发布订阅通道适用于一对多通信的场景,消息进入一条发布订阅通道时,所有订阅了此通道的其他通道都会收到该消息。
三、Cloud Foundry运行时环境检测
这两个程序是基于Spring Integration实现的消息传递,另外各自都包含用于检测程序运行环境的监听器,src / main / java / org / wgrus / web /EnvironmentContextListener.java:
public class EnvironmentContextListener implements ServletContextListener {
private static Logger logger = LoggerFactory
.getLogger(EnvironmentContextListener.class);
public void contextInitialized(ServletContextEvent sce) {
if (System.getenv("VMC_APP_VERSION") != null
|| System.getenv("VCAP_APP_VERSION") != null) {
System.setProperty("spring.profiles.active", "cloud");
// Extract the base domain from this cloud platform (e.g. cloudfoundry.com)
String services = System.getenv("VCAP_APPLICATION");
if (services == null) {
services = System.getenv("VMC_APP_INSTANCE");
}
if (services != null) {
System.setProperty("BASE_DOMAIN", services.replaceAll(
".*wgrus-inventory\\.([a-zA-Z0-9.]*)\\\".*", "$1"));
}
logger.info("Cloud profile activated with base domain: "+System.getProperty("BASE_DOMAIN"));
} else {
System.setProperty("spring.profiles.active", "default");
logger.info("Default profile set");
}
}
public void contextDestroyed(ServletContextEvent sce) {
}
}
这里读取系统环境变量,判断程序运行的环境是本地default还是Cloud Foundry的云环境。当然,为使程序能够在Cloud Foundry上运行,需要在pom.xml和应用程序上下文中添加必要的Cloud Foundry运行时依赖,这是每个Cloud Foundry应用都相同且必不可少的基本配置,想必之前几个样例程序的演示已经让让大家再熟悉不过了。
另外,虽然程序构建时引用了MySQL库,但程序上下文中并未注入MySQL数据源的依赖Beans,用来初始化数据库的 wgrus-store / src / main / resources / setup.sql 脚本也没运行过,即使是MongoDB也仅仅是注入了Bean而并未进行数据持久化。可能是由于程序代码更新过之后这些旧的资源和配置没有一并删除,研究代码时大可无视之。