Spring Integration 对 Spring 编程模型进行了扩展,使得后者能够支持著名的“企业集成模式”。通过SI(Spring Integration)可以在基于Spring的应用中引入轻量级的“消息驱动模式”,并且支持“通过声明式的适配器”与外部系统进行集成。这些“适配器”相较于Spring对于“remoting(远程调用)”、“messaging(事件消息)”、“scheduling(任务调度)”方面的支持,提供了更高层次的一种抽象。SI的首要目标是:为“构建企业集成方案、维护系统间通信”提供一种简单模型,应用该模型所产出的代码是可维护、可测试的。
2. Spring Integration 概览
2.1 背景
IoC——“控制反转”,是Spring Framework的一个关键特性。这种IoC,从广义上来说,意味着Spring Framework将负责代表被其上下文所管理的各种组件,而组件本身却由于被减轻了部分职责而简单化了。例如:“依赖注入”使得组件摆脱了定位与创建自身依赖的职责。再比如:“面向切面编程”则通过可复用切面的透明织入,使得业务与横切交叉点解耦,使得业务组件避免了被普遍的耦合横切,做到了更好的模块化。在上述各种情况下,最终结果都是:系统更容易被测试、理解、维护和扩展。
此外,Spring Framework及相关工具包为构建企业应用提供了一个无所不包的编程模型。开发者受益于这个模型的一致性。尤其是该模型以公认的最佳实践为基础,使得开发者受益匪浅,比如“面向接口编程”,“尽量使用聚合而不是继承”等等。来自Spring的简化抽象与强大类库,不仅增强了系统的可测试性和可移植性,同时也大大提高了开发者的生产力。
Spring Integration秉承了与前文所述相同的目标和原则。它将Spring的编程模型拓展到了消息领域,在Spring现有的企业集成支持基础上构建了更高次的抽象。在它所支持的消息驱动架构中,“控制反转”被应用于运行期的关键连接处,例如:在何时特定的业务逻辑应该执行,以及响应结果应该被发送到何处。它提供了消息路由和消息转换方面的支持,所以不同的传输协议和不同的数据格式都能在不影响易测试性的前提下被集成。换句话说,消息和集成点都被框架所处理,所以业务组件能很好地与基础设施隔离,从而使得开发者能够从复杂的集成工作中解脱出来。
作为Spring编程模型的扩展,Spring Integration提供了多种配置方式可供选择,包括注解、基于命名空间的XML、通用Bean元素的XML,当然也可以直接使用底层API。底层的API都均是基于“精心定义的策略接口”与“保证了非侵入性的代理适配器”。Spring Integration的设计启发于“Spring中的普遍模式”与“企业集成模式”间强烈的共鸣。“企业集成模式”是由Gregor Hohpe和Bobby Woolf在《企业集成模式》一书中提出的,该书由Addison Wesley出版社于2004年出版。读过此书的开发者应该能更快地适应Spring Integration的概念和术语。
2.2 目标与原则
Spring Integration 面向如下目标:
- 提供一个简单的模型来实现复杂的企业集成解决方案
- 为基于Spring的应用提供异步、消息驱动方面的基础支持
- 让现有的Spring用户可以更容易、直观的掌握,并让更多的用户去使用
Spring Integration 遵循以下原则:
- 组件间应该是模块化、松耦合的,且可测试的
- 框架应该保证分离“业务逻辑”和“集成逻辑”
- 扩展点本质上应该是抽象的,而且限定在一个清晰的边界内,进而提升可复用性和可移植性
2.3 主要组件
从垂直的视角来看,“分层架构”会更有利于关键点的剥离,各层间通过基于接口的契约来确保松耦合。基于Spring的应用就是如此设计的典型。Spring Framework与相关工具包从全栈范围为企业应用提供了一个遵从最佳实践强大基础。“消息驱动架构”则为我们带来了一个“横向的视角”。正如“分层架构”是一种极通用、极抽象的范式一样,消息系统非常符合同样抽象的“管道-过滤器”模型。“过滤器”代表任何能够产出和(或)消费消息的组件。“管道”则负责在过滤器间传输消息。所以在管道的作用下,各“过滤器”组件间保持松耦合。有必要指出的是,这两个高级范式(分层架构与消息驱动架构)并非互斥。支持“管道”的消息基础设施应当被封装在相应的垂直“层”中,且该层对外的契约被定义为接口。同样地,“过滤器”往往被安排于“应用服务层”之上的(业务)层中进行管理,并且与底层服务的交互方式与它类无异。
2.3.1 消息
在Spring Integration中,“消息”是对任何Java对象的一种通用包装,这种包装将会给Java对象附着一些元信息以供消息框架处理。一条“消息”由“消息体”(payload)和“消息头”(header)组成。消息体可以是任何类型,消息头一般用于保存一些必要信息,比如id、时间戳、过期时间和返回地址等。消息头也可以用来在不同的传输协议之间传递参数。比如,当需要包装一个文件来创建一个消息时,可以将文件名保存于消息头中,以供下游的消息组件读取使用。再比如,如果一个消息的内容会最终被Mail适配器发出,那么各种属性值(to、from、cc、subject等)可被上游的消息组件保存在消息头中。开发者可以利用消息头来保存任意的键值对。
2.3.2 消息通道
“消息通道”对应着“管道-过滤器”架构中的“管道角色”。消息生产者发送消息到通道,消息消费者从通道接收消息。从而,消息通道解耦了消息组件,同时也为消息拦截和监控提供了便利的切入点。
一个消息通道可以是“点对点”意义的,或者也可以是“发布-订阅”意义的。
- 如果是点对点模式的通道,发布到通道中的每个消息,最多只有一个消费者可以接收。
- 如果是发布-订阅模式的通道,则会尝试广播消息给其所有的订阅者。
Spring Integration对这两种模式均提供支持。
鉴于“点对点模式”和“发布订阅模式”提供了两种关于“最终有多少消息消费者接收消息”的选择,此处还有另外一项重要考虑:通道是否应该缓存消息?在Spring Integration中,轮询通道(Pollable Channels)具备缓存消息能力。缓存的优势在于它能够调节接入消息流量,从而避免消息消费者负荷过载。然而,正如其名称所示,这也会引入了一些复杂性,只有配置了轮询器后,消息消费者才能从这个通道中接收消息。而另一方面,订阅通道(Subscribable Channel)要求连接它的消费者依从简单的消息驱动模式。Spring Integration中还有多种通道的可用实现,将在第3.2章节“消息通道实现”中详细讨论。
2.3.2 消息终端
Spring Integration的主要目标之一是通过“控制反转”来简化企业集成解决方案的开发。这意味着你应该不需要直接实现消息消费者和生产者,更不需要构建消息或者在消息通道上调用发送与接收操作。相反地,你只需要关注于你基于普通对象(POJO)实现的特定领域模型。然后,通过声明式配置,你可以“连接”业务领域代码到Spring Integration提供的消息基础设施。而负责这些连接的组件就是“消息终端”。这并不是说你必须直接性地连接现有应用。任何真实的企业集成解决方案,都需要一些用于集成相关的逻辑代码,例如路由选择和协议转换。其中暗含的要点就是:实现集成逻辑和业务逻辑的分离。类比来说,作为Web应用中的MVC模式,其目标应该是提供一个简单而专用的层,转换接入的请求到服务层调用,然后再转换服务层响应到请求端。下一节将概述处理这些响应的各种消息终端类型,且在以后的章节中,将为你展示如何使用Spring Integration的声明式配置来保证非侵入性的效果。
2.4 各种消息终端简介
“消息终端”对应着“管道-过滤器”架构中的“过滤器”角色。就像前文提到的,消息终端的的主要作用在于“连接”业务领域代码到Spring Integration提供的消息基础设施,当然前提是使用非侵入的方式。换句话说,应用代码应该完全不会知晓消息对象或者消息通道的存在。这类似于MVC模式中控制器角色的处理范式,例如:
- “消息端点处理消息”就像“控制器处理HTTP请求”。
- “消息终端被映射到消息通道”就像“控制器被映射到URL Pattern”
上述两个例子中存在相同的目标:隔离“业务应用代码”与“基础设置”。《企业应用集成》一书中详细地讨论了这其中涉及的各种概念与模式。本文只概括地描述Spring Integration支持的主要消息终端类型和作用。本章节内的详细描述将提供代码与配置样例。
2.4.1 转换器(Transformer)
“消息转换器”的作用在于“转换消息的内容或结构,返回翻转换后的消息”。可能最为常见的转换器应用方式就是将消息体(Message Payload)从一种格式转换为另一种格式(例如从XML文档转换成java.lang.String字符串)。同样地,转换器也可以被用于添加、删除和修改消息头(Message Header)中的值。
2.4.2 过滤器(Filter)
“消息过滤器”可用来限定消息能否被传送到输出通道上。这里仅需要依据一个布尔测试来做判定。该布尔测试检查的范围诸如:“消息是否包含特定类型的消息体”,“检查消息中的某个属性值”,“消息是否包含某个消息头”,等等。如果消息通过检查就会被发送到输出通道,否则,消息将被丢弃(或者更加严格地说,应当抛出异常)。消息过滤器通常结合“发布-订阅通道”一起使用,“发布-订阅通道”会使得多个消费者接收到同样的消息,而基于一定的过滤条件设置过滤器则可以减少所需处理的消息数量。
注意:前段提到的“消息过滤器”与“管道-过滤器”架构中的‘过滤器’并非一致的概念。“消息过滤器”是指限制消息通过的消息处理装置,而“管道-过滤器”中的‘过滤器’是泛指所有处理消息的消息处理装置。
2.4.3 路由器(Router)
“消息路由器”负责确定消息的下一步传送将由哪些通道接收。通常,路由决策都是基于消息内容和(或)消息头中可用的元数据来完成的。消息路由常常代替静态的配置,作为一种动态地、运行时确定输出通道的装置,接入到“服务激活器”或者其他能够响应消息的装置。另外,针对前文所述的消息多播的场景,相对于被动的“消息过滤器”,“消息路由器”提供了一种主动的控制方式,来确定多个消息订阅者中的消息接收范围。
2.4.4 消息分解器(Splitter)
“消息分解器”是另外一种类型的消息终端,它从对应输入通道中接收消息,然后把接收到的一个消息分解成多个消息,最终把它们发送到对应输出通道上。典型应用场景就是把一个“复合消息”分解成包含原消息各子部分的一组“子消息”。
2.4.5 消息聚合器(Aggregator)
“消息聚合器”基本上就是“消息分解器”的反模式。它也是一种消息终端类型,接收多条消息,然后把他们合并成一条消息。事实上,聚合器通常出现在消息管道线路中的下游位置,且相对该组件的上游位置往往会存在“消息分解器”。从技术上来说,聚合器往往比分解器更复杂,因为它需要维护状态(也就是维护正被聚合的消息),确定被聚合的整组消息何时可用,以及必要地话,还要处理超时的状况。更进一步地,在超时的情况下,聚合器还要明确是仍然发送残缺消息,或是丢弃它们。对此,Spring Integration提供了可配置的超时处理策略。
2.4.6 服务激活器(Service Activitor)
“服务激活器”是一种将“服务实例”连接到“消息系统”的通用终端。对于该类型的终端,配置输入通道是必须的。而且,若被调用的服务方法具有返回值,那么此情况下或需要配置输出通道。
注意:输出通道的配置是可选的。因为每则消息在消息头中可能会提供它自身的“返回地址”(Return Address)信息。这个规则同样适用于其他的消费终端。
服务激活器会调用指定服务对象上的操作,来处理请求消息。该过程中会抽取请求消息的消息体并作必要的转换(若方法参数非消息类型参数)。每当服务方法产生返回值,这个返回值同样地会作出必要的转换,而成为一条响应消息(若方法返回值非消息类型)。响应消息将会被发送到输出通道上;若没有配置输出通道,且消息的“返回地址”可用,那么该响应将会被发送到返回地址指定的通道上。
2.4.7 通道适配器(Channel Adapter)
“通道适配器”是一种连接消息通道到“其他系统”或是“传输端口”的消息终端。通道适配器分为“接入”或者“接出”两种。通常通道适配器被用来映射消息到其他任何发送/接收系统所需的对象或资源上(比如:文件、HTTP请求,JMS消息等等)。依赖于传输端口的情况下,通道适配器也可以填充或是抽取消息头中的值。Spring Integration提供了一些通道适配器,以后的章节中会讲述它们。
接入型通道适配器连接源系统到消息系统中
接出型通道适配器连接消息系统到目标系统中
2.5 关于配置
贯穿本篇文档,都利用了XML命名空间的支持,来声明Spring Integration消息流中的各种元素。这背后由一系列命名空间解析器给予支撑,这些解析器自动生成了实现了特定组件功能的Bean定义。
当Spring Integration的名字空间元素被第一次解析时,框架将会自动声明若干bean,这些bean会被用来支撑运行时环境(比如:任务调度器、隐式通道构造器,等等)。
从4.0版本开始,当使用注解类,添加@EnableIntegration注解时,以上这些支撑bean也同样被创建。这样会利于使用纯Java配置来声明一个简单的Spring Integration消息流。另外,当存一个“父上下文”和多个“子上下文”时@EnableIntegration注解可以使得Spring Integration组件仅需声明一次。
2.6 编程实践
通常的建议是:务必使用POJOs(plain old java objects),不到万不得已之时不要在自己的代码中引入框架依赖。
如果你确实已经在自己的类中引用了框架,那么下面的建议可供参考:
- 如果实现了“ApplicationContextAware”接口,那么不要在setApplicationContext()方法中使用ApplicationContext对象;在这个方法里仅保存该引用,延迟对该对象的使用。
- 如果实现了“InitializingBean”接口或者使用@PostConstruct注解了方法,那么不要从这种初始化方法中发送任何消息出去,因为这些方法执行时应用上下文尚没有完成初始化,此时发送消息很可能会失败。如果需要在启动时刻发送消息,那么实现“ApplicationListener”接口,等待“ContextRefreshedEvent”事件发生时即可。或者,另外一种方式是,实现“SmartLifecycle”接口,将bean置于晚期阶段,然后在send()方法中发送消息。