目录
- 概述
- 1.搭建 JMS 环境
- 2.使用JmsTemplate 发送消息
- 3.接收JMS 消息
概述
JMS
是一个Java
标准,定义了使用消息代理(message broker
)的通用API
,在2001年提出。长期以来,JMS
一直是Java
中实现异步消息的首选方案。在JMS
出现之前每个消息代理都有其私有的API
,这就使得不同代理之间的消息代码很难互通。但是借助JMS
所有遵从规范的实现都使用通用的接口,这就类似于JDBC为数据库操作提供了通用的接口。
Spring
通过基于模板的抽象为JMS
功能提供了支持,这个模板就是JmsTemplate
。借助 JmsTemplate
,能够非常容易地在消息生产方发送队列和主题消息,消费消息的一方也能够非常容易地接收这些消息。Spring
还支持消息驱动POJO
的理念:这是一个简单的Java
对象,能够以异步的方式响应队列或主题上到达的消息。
我们将会讨论Spring
对JMS
的支持,包括JmsTemplate
和消息驱动POJO
。我们的关注点主要在于Spring
对JMS
消息的支持,如果你想要了解关于JMS的更多内容,请参阅 Bruce Snyder
、Dejan Bosanac
和 Rob Davies
合著的ActiveMQ in Action
(Manning;2011年)。
在发送和接收消息之前,我们首先需要一个消息代理(broker
),它能够在消息的生产者和消费者之间传递消息。对 Spring JMS
的探索就从在 Spring
中搭建消息代理开始吧。
1.搭建 JMS 环境
在使用JMS
之前我们必须要将JMS
客户端添加到项目的构建文件中。借助 SpringBoot
,这再简单不过了。我们要做的就是添加一个 starter
依赖到构建文件中。但是,首先,我们需要决定该使用Apache ActiveMQ
还是更新的Apache ActiveMQ Artemis
代理如果使用Apache ActiveMQ
,那么需要添加如下的依赖到项目的 pom.xml
文件中:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
如果使用ActiveMQ Artemis
,那么 starter
依赖如下所示:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
在使用 Spring
Initializr
(或者使用 IDE 作为 Initializr
的前端)的时候,我们能够以starter
依赖的方式为项目选择这两个方案。它们分别显示为“Spring
for
Apache
ActiveMQ5
”和“Spring
for
Apache
ActiveMQ
Artemis
。如图:
Artemis
是重新实现的下一代ActiveMQ
,使ActiveMQ
变成了遗留方案。因此,TacoCloud
(本次项目主题)会选择使用Artemis
。但是在编写发送和接收消息的代码方面,选择哪种方案几乎没有什么区别。唯一需要注意的重要差异就是如何配置 Spring
创建到代理的连接。
运行
Artemis
代理
要运行本次实验中的代码,我们需要有一个Artemis
代理。如果没有可运行的Artemis
实例,可以参阅Artemis
文档中的指南。
默认情况下,Spring
会假定Artemis
代理在 localhost
的61616端口运行。对于开发来说,这样是没有问题的,但是一旦要将应用部署到生产环境,我们就需要设置一些属性来告诉 Spring
如何访问代理。常用的属性如表所示。
属性 | 描述 |
---|---|
spring.artemis.host | 代理的主机 |
spring.artemis.port | 代理的端口 |
spring.artemis.user | 用来访问代理的用户(可选) |
spring.artemis.password | 用来访问代理的密码(可选) |
例如,如下的application.yml
文件条目可以用于一个非开发的环境:
spring:
artemis:
host: artemis.tacocloud.com
port: 61617
user: tacoweb
password: 13tm31n
这会让 Spring
创建到 Artemis
代理的连接,该 Artemis
代理监听 artemis.tacocloud.com
的61617端口。它还为应用设置了与代理交互的凭证信息。凭证信息是可选的,但是对于生产环境来说,我们推荐使用它们。
如果你选择使用ActiveMQ
而不是 Artemis
,那么需要使用 ActiveMQ
特定的属性如表所示:
属性 | 描述 |
---|---|
spring.activemq.broker-url | 代理的 URL |
spring.activemq.user | 用来访问代理的用户(可选) |
spring.activemq.password | 用来访问代理的密码(可选) |
spring.activemq.in-memory | 是否启用在内存中运行的代理(默认为 true) |
需要注意,ActiveMQ
代理不是分别设置代理的主机和端口,而是使用了一个名为spring.activemgbroker-url
的属性来指定代理的地址。URL应该是“tcp://”协议的地址,如下面的YAML片段所示:
spring:
activemg:
broker-url: tcp://activemq.tacocloud.com
user: tacoweb
password: 13tm31n
不管选择 Artemis
还是ActiveMQ
,在本地开发运行时,都不需要配置这些属性。
但是,使用ActiveMQ
时,则需要将 spring.activemq.in-memory
属性设置为 false
,防止 Spring
启动内存中运行的代理。内存中运行的代理看起来很有用,但是只有在同个应用中发布和消费消息的情况下,才能使用它(这限制了它的用途 )。
在继续下面的内容之前,我们要安装并启动一个 Artemis
(或 ActiveMQ
)代理而不是选择使用嵌人式的代理。
现在,我们已经在构建文件中添加了对JMS
starter
依赖,代理也已经准备好将消息从一个应用传递到另一个应用,接下来,我们就可以开始发送消息了。
2.使用JmsTemplate 发送消息
将 JMS starter
依赖(不管是 Artemis
还是 ActiveMQ
)添加到构建文件之后,Spring Boot
会自动配置一个 JmsTemplate
(以及其他内容),我们可以将它注入其他 bean
,并使用它来发送和接收消息
JmsTemplate
是 Spring
对JMS
集成支持功能的核心与Spring
其他面向模板的组件类似,JmsTemplate
消除了大量传统使用JMS
时所需的样板代码如果没有JmsTemplate
我们就需要编写代码来创建与消息代理的连接和会话,还要编写更多的代码来处理发送消息过程中可能出现的异常。JmsTemplate
能够让我们关注真正要做的事情: 发送消息JmsTemplate
有多个用来发送消息的方法,包括:
// 发送原始的消息
void send(MessageCreator messageCreator) throws JmsException;
void send(Destination destination, MessageCreator messageCreator)
throws JmsException;
void send(String destinationName, MessageCreator messageCreator)
throws JmsException;
//发送根据对象转换而成的消息
void convertAndSend(Object message) throws JmsException;
void convertAndSend(Destination destination, Object message)
throws JmsException;
void convertAndSend(String destinationName, Object message)
throws JmsException;
//发送根据对象转换而成的消息,且带有后期处理的功能
void convertAndSend(Object message, MessagePostProcessor postProcessor)
throws JmsException;
void convertAndSend(Destination destination,Object messaqe, MessagePostProcessor postProcessor)
throws JmsException;
void convertAndSend(String destinationName, Object message, MessagePostProcessor postProcessor)
throws JmsException;
我们可以看到,上面的代码实际上只展示了两个方法,也就是 send()
和convertAndSend()
,但每个方法都有重载形式以支持不同的参数。如果我们仔细观察一下,convertAndSend()
的各种形式又可以分成两个子类。在考虑这些方法作用的时候,我们对它们进行细分:
- 3个
send()
方法都需要MessageCreator
来生成Message
对象; - 3个
convertAndSend()
方法会接受Object
对象,并且会在幕后自动将Object
转
换为Message
; - 3个
convertAndSend()
会自动将Object
转换为Message
,但同时还能接受一个MessagePostProcessor
对象,用来在发送之前对Message
进行自定义。
这 3种方法分类都分别包含 3 个重载方法,它们的区别在于指定JMS 目的地(队列或主题)的方式:
- 有1个方法不接受目的地参数,它会将消息发送至默认的目的地:
- 有1个方法接受
Destination
对象,该对象指定了消息的目的地; - 有1个方法接受
String
,它通过名字的形式指定了消息的目的地,
为了让这些方法真正发挥作用,我们看一下下面程序中的JmsOrderMessagingService
。它使用了形式最简单的 send()
方法。
//使用send()方法将订单发送至默认的目的地
package tacos.messaging;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.core.MessageCreator;
import org.springframework.stereotype.Service;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Session;
@Service
public class JmsOrderMessagingService implements OrderMessagingService {
private JmsTemplate jms;
@Autowired
public JmsOrderMessagingService(JmsTemplate jms) {
this.jms = jms;
}
@Override
public void sendOrder(TacoOrder order) {
jms.send(new MessageCreator() {
@Override
public Message createMessage(Session session)
throws JMSException {
return session.createObjectMessage(order);
}
});
}
}
sendOrder()
方法调用了jms.send()
,并传递了 MessageCreator
接口的一个匿名内部类实现。这个实现类重写了 createMessage()
方法,从而能够通过给定的 TacoOrder
对象创建新的消息对象。
因为面向JMS
功能的JmsOrderMessagingService
实现了更通用的OrderMessagingService
接口,所以为了使用这个类,我们可以将其注入 OrderApiController
,并在创建订单的时候调用 sendOrder()
方法,如下所示:
@RestController
@RequestMapping(path = "/api/orders",
produces = "application/json")
@CrossOrigin(origins = "http;//localhost:8080")
public class OrderApiController {
private OrderRepository repo;
private OrderMessagingService messageService;
public OrderApiController(OrderRepository repo, OrderMessagingService messageService) {
this.repo = repo;
this.messageService = messageService;
}
@PostMapping(consumes = "application/json")
@ResponseStatus(HttpStatus.CREATED)
public TacoOrder postOrder(@RequestBody TacoOrder order) {
messageService.sendOrder(order);
return repo.save(order);
}
.....
}
现在,我们通过 Taco Cloud
的 Web 站点创建订单时,就会有一条消息发送至代理以便将其路由至另外一个接收订单的应用。不过,我们还没有任何接收消息的功能。即便如此,依然可以使用Artemis
控制台来查看队列的内容。请参阅 Artemis
文档了解实
现的细节。不知道你的感觉如何,但是我认为程序清单中的代码虽然比较简单,但还是有点啰唆。声明匿名内部类的过程使得原本很简单的方法调用变得很复杂。我们发现MessageCreator
是一个函数式接口,所以可以通过lambda
表达式简化 sendOrder()
方法:
@Override
public void sendOrder(TacoOrder order) {
jms.send(session -> session.createObjectMessage(order));
}
但是需要注意,对 jms.send()
的调用并没有指定目的地。为了让它能够运行,我们需要通过名为spring.jms.template.default-destination
的属性声明一个默认的目的地名称。例如,可以在application.yml
文件中这样设置该属性:
spring:
jms:
template:
default-destination: tacocloud.order.queue
在很多场景下,使用默认的目的地是最简单的可选方案。借助它,我们只声明一次目的地名称就可以了,代码只关心发送消息,而不关心消息会发到哪里。但是如果要将消息发送至默认目的地之外的其他地方,那么就需要通过为 send()
设置参数来指定。
其中一种方式是传递 Destination
对象作为 send()
方法的第一个参数。最简单的方式就是声明一个Destination
bean
并将其注人处理消息的 bean
。例如,如下的 bean
声明了Taco Cloud
订单队列的 Destination
:
@Bean
public Destination orderQueue() (
return new ActiveMQQueue("tacocloud.order.queue");
}
在需要通过JMS
发送和接收消息的应用中,这个 bean
方法可以添加到任意的配类里面。但是为了良好的代码组织,最好将其添加到一个专为消息相关的配置所创建配置类中,比如MessagingConfig
。
很重要的一点是,这里的 ActiveMQQueue
来源于 Artemis
(来自 org.apache.activemq.artemis,jmsclient
包)。如果你使用 ActiveMQ
(而不是 Artemis
),同样会找到一个名为ActiveMQQueue
的类(来自org.apache.activemq.command
包)。
Destination bean
注人JmsOrderMessagingService
之后,调用 send()
的时候,我们就可以使用它来指定目的地了:
private Destination orderQueue;
@Autowired
public JmsOrderMessagingService(JmsTemplate jms,Destination orderQueue) {
this.jms= jms;
this.orderQueue = orderQueue;
}
@Override
public void sendOrder(TacoOrder order) {
jms.send(
orderQueue,
session -> session.createObjectMessage(order));
}
通过Destination
指定目的地时,其实可以设置 Destination
的更多属性,而不仅仅是目的地的名称。但是,在实践中,除了目的地名称,我们几乎不会设置其他的属性。因此,使用名称作为send()
的第一个参数会更加简单:
@Override
public void sendOrder(TacoOrder order) (
jms,send("tacocloud.order.queue",
session -> session.createObjectMessage(order));
}
尽管 send()
方法使用起来并不是特别困难(通过 lambda
表达式来实现 MessageCreator
更是如此),但是它要求我们提供 MessageCreator
,还是有些复杂。如果能够只指定要发送的对象(以及可能要用到的目的地),岂不是更简单?这其实就是 convetAndSend()
的工作原理。接下来,我们看一下这种方式。
消息发送之前进行转换
JmsTemplates
的 convertAndSend()
方法简化了消息的发布因为它不再需要MessageCreator
。我们将要发送的对象直接传递给 convertAndSend()
,这个对象在发送之前会被转换成 Message
。
例如,在如下重新实现的 sendOrder()
方法中,使用 convertAndSend()
将 TacoOrder
对象发送到给定名称的目的地
@Override
public void sendOrder(TacoOrder order) {
jms.convertAndSend("tacocloud.order.queue", order);
}
与send()
方法类似,convertAndSend()
将会接受一个Destination
对象或String
值来确定目的地,我们也可以完全忽略目的地,这样一来,消息会发送到默认目的地上。
不管使用哪种形式的convertAndSend()
,传递给convertAndSend()
的TacoOrder
都会在发送之前转换成Message
。在底层,这是通过MessageConverter
的实现类来完成的
它替我们完成了将对象转换成Message
的任务。
配置消息转换器
MessageConverter
是 Spring
定义的接口,它只有两个需要实现的方法:
public interface MessageConverter {
Message toMessage(Object object, Session session)throws JMSException, MessaqeConversionException;
Object fromMessage(Message message)
}
尽管这个接口实现起来很简单,但我们通常并没有必要创建自定义的实现。Spring已经提供了多个实现,如表所示。
Spring
为通用的转换任务提供了多个消息转换器(所有的消息转换器都位于org.springframework,jms.support.converter
包中)。
消息转换器 | 功能 |
---|---|
MappingJackson2MessageConverter | 使用Jackson 2 JSON 库实现消息与JSON 格式的相互转换。 |
MarshallingMessageConverter | 使用JAXB 库实现消息与XML 格式的相互转换。 |
MessagingMessageConverter | 对于消息载荷,使用底层的MessageConverter 实现抽象Message 与javaxjmsMessageMessage 的相互转换,同时会使用JmsHeaderMapper 实现JMS 头信息与标准消息头信息的相互转换。 |
SimpleMessageConverter | 实现String 与TextMessage 的相互转换、字节数组与BytesMessage 的相互转换、Map 与MapMessage 的相互转换,以及Serializable 对象与ObjectMessage 的相互转换。 |
默认情况下,将会使用 SimpleMessngeConverter
,但是它需要被发送的对象实现serializable
。这种办法也不错,但有时候我们可能想要使用其他的消息转换器来消除这种限制,比如MappingJackson2MessageConverter
。
为了使用不同的消息转换器,我们必须将选中的消息转换器实例声明为一个bean
。例如如下的 bean
声明将会使用 MappingJackson2MessageConverter
替代 SimpleMessageConverter
:
@Bean
public MappingJackson2MessageConverter messageConverter(){
MappingJackson2MessageConverter messageConverter=new MappingJackson2MessageConverter();
messageConverter.setTypeIdPropertyName("_typeId");
return messageConverter;
}
在需要通过JMS
发送和接收消息的应用中,这个 bean
方法可以添加到任意的配置类里面,包括定义Destination
的MessagingConfig
中。
需要注意,在返回之前,我们调用了 MappingJackson2MessageConverter
的 setTypeloPropertyName()
方法。这非常重要,因为这样能够让接收者知道传人的消息需要转换成什么类型。默认情况下,它会包含要转换的类型的全限定类名。但是,这要求接收端也包含相同的类型,并且具有相同的全限定类名,未免不够灵活。
为了提升灵活性,我们可以通过调用消息转换器的 setTypeldMappings()
方法将一个合成类型名映射到实际类型上。举例来说,消息转换器 bean
方法的如下代码变更会将一个合成的 TacoOrder
类型ID
映射为TacoOrder
类:
@Bean
public MappingJackson2MessageConverter messageConverter() {
MappingJackson2MessageConverter messageConverter =new MappingJackson2MessageConverter();
messageConverter.setTypeIdPropertyName("_typeId");
Map<String,Class<?>> typeIdMappings = new HashMap<String,class<?>>();
typeIdMappings.put("order", TacoOrder.class);
messageConverter.setTypeIdMappings(typeIdMappings);
return messageConverter;
}
这样,消息的_typeld
属性中就不会发送全限定类型,而是会发送 TacoOrder
值了在接收端的应用中会配置类似的消息转换器,将 TacoOrder
映射为它自己能够理解的单类型。在接收端的订单可能位于不同的包中、有不同的类名,甚至可以只包含发送者Order
属性的一个子集。
对消息进行后期处理
假设在经营利润丰厚的 Web 业务之外,Taco Cloud
还决定开几家实体的连锁 taco店。
鉴于任何一家餐馆都可能成为 Web 业务的运行中心,需要有一种方式告诉厨房订单的来源。这样一来,厨房的工作人员就能为店面里的订单和 Web 上的订单执行不同的流程。
我们可以在 TacoOrder
对象上添加一个新的 source
属性,让它携带订单来源的相关信息:如果是在线订单,就将其设置为 WEB
;如果是店面里的订单,就将其设置为STORE
。但是,这需要我们同时修改 Web
站点的 TacoOrde
r 和厨房应用的 TacoOrde
类,但实际上,只有准备 taco
的人需要该信息。
有种更简单的方案是为消息添加一个自定义的头部,让它携带订单的来源。如果使用send()
方法来发送 taco
订单,就可以通过调用 Message
对象的 setStringProperty()
方法非常容易地实现该功能:
jms.send("tacocloud.order.queuen",
session->{
Message message = session.createObjectMessage(order);
message.setStringProperty("X_ORDER_ SOURCE","WEB");
});
但是,这里的问题在于我们并没有使用 send()
。使用 convertAndSend()
方法时Message
是在底层创建的,我们无法访问到它。
幸好,还有一种方式能够在发送之前修改底层创建的 Message
对象。我们可以传递个MessagePostProcessr
作为 convertAndSend()
的最后一个参数,借助它,我们可以在Message
创建之后做任何想做的事情。如下的代码依然使用了 convertAndSend()
,但是它能够在消息发送之前,使用MessagePostProcessor
添加X_ORDER SOURCE
头信息:
jms.convertAndSend("tacocloud.order.queue",order, new MessagePostProcessor(){
@Override
public Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("X_ ORDER_SOURCE","WEB");
return message;
}
});
你可能已经发现,MessagePostProcessor
是一个函数式接口。这意味着我们可以将匿名内部类替换为 lambda
表达式,进一步简化它:
jms.convertAndSend("tacocloud,order.queue", order,
message ->{
message.setStringProperty("X_ORDER_ SOURCE","WEB");
return message;
});
尽管在这里我们只是将这个特殊的 MessagePostProcessor
用到了本次 convertAndSend()
方法调用中,但是你可能会发现代码在不同的地方多次调用 convertAndSend()
,它们均会用到相同的MessagePostProcessor
。在这种情况下,方法引用是比 lambda
表达式更好的方案,因为它能避免不必要的代码重复:
@GetMapping("/convertAndSend/order")
public String convertAndSendOrder() {
TacoOrder order = buildOrder();
jms.convertAndSend("tacocloud.order.queue",order,.
this::addOrderSource);
return "Convert and sent order";
}
private Message addorderSource (Message message) throws JMSException {
message.setStringProperty("X_ORDER_ SOURCE","WEB”);
return message;
}
我们已经看到了多种发送消息的方式。但是如果只发送消息而无人接收,那么这其实没有什么价值。接下来,我们看一下如何使用 Spring 和JMS 接收消息。
3.接收JMS 消息
在消费消息时,我们可以选择遵循拉取模式(pull model
)或推送模式(push model
)前者会在我们的代码中请求消息并一直等到消息到达,而后者则会在消息可用的时候自动在你的代码中执行。
JmsTemplate
提供了多种方式来接收消息,但它们使用的都是拉取模式。我们可以调用其中的某个方法来请求消息,这样线程会一直阻塞直到一个消息抵达为止(这可能马上发生,也可能需要等待一会儿)。
另外,我们也可以使用推送模式。在这种情况下,我们会定义一个消息监听器,每当有消息可用时,它就会被调用。
这两种方案适用于不同使用场景。通常人们觉得推送模式是更好的方案,因为它不会阻塞线程。但是,在某些场景下,如果消息抵达的速度太快,监听器可能会过载,而拉取模式允许消费者声明自己何时为接收新消息做好准备。
我们首先关注JmsTemplate提供的拉取模式。
使用JmsTemplate 接收消息
JmsTemplate提供了多个对代理的拉取方法,其中包括:
Message receive() throws JmsException;
Message receive(Destination destination) throws JmsException;
Message receive(String destinationName) throws JmsException;
Object receiveAndConvert() throws JmsException;
Object receiveAndConvert(Destination destination) throws JmsException;
Object receiveAndConvert(String destinationName) throws JmsException;
我们可以看到,这6个方法简直就是 JmsTemplate
中 send()
和 convertAndSend()
方法的镜像。receive()
方法接收原始的 Message
,而receiveAndConvert()
则会使用一个配置好的消息转换器将消息转换成领域对象。对于其中的每种方法,我们都可以指定 Destination
或者包含目的地名称的 String
值,若不指定,则从默认目的地拉取消息。
为了实际看一下它是如何运行的,我们编写代码从 tacocloud.order.queue
目的地拉取一个 TacoOrder
对象。下方程序展现了 OrderReceiver
,这个服务组件会使用JmsTemplate.receive()
来接收订单数据。
//从队列拉取订单
package tacos.kitchen.messaging.jms;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.jms.support.converter.MessageConverter;
import org.springframework.stereotype.Component;
import javax.jms.Message;
@Component
public class JmsOrderReceiver implements OrderReceiver {
private JmsTemplate jms;
private MessageConverter converter;
@Autowired
public JmsOrderReceiver(JmsTemplate jms, MessageConverter converter) {
this.jms = jms;
this.converter = converter;
}
public TacoOrder receiveOrder() {
Message message = jms.receive("tacocloud.order.queue");
return (TacoOrder) converter.fromMessage(message);
}
}
这里我们使用 String
值来指定从哪个目的地拉取订单。receive()
返回的是没有经过转换的 Message
,但是,我们真正需要的是 Message
中的 TacoOrder
,所以接下来要做的事情就是使用注入的消息转换器对消息进行转换。消息中的 type ID
属性将会指导转换器将消息转换成 TacoOrder
,但它返回的是 Object
,因此在最终返回之前要进行类型转换。
如果要探查消息的属性和消息头信息,接收原始的 Message
对象可能会非常有用但是,通常来讲,我们只需要消息的载荷。将载荷转换成领域对象是一个需要两步操作的过程,而且它需要将消息转换器注入到组件中。如果只关心载荷,那么使用receiveAndConvert()
会更简单一些。下面将展现如何使用 receiveAndConvert()
替换 receive()
来重新实现JmsOrderReceiver
。
//接收已经转换好的 TocoOrder 对象
package tacos.kitchen.messaging.jms;
import org.springframework.jms.core.JmsTemplate;
//接收已经转换好的 TocoOrder 对象
package tacos.kitchen.messaging.jms;
import org.springframework.jms.core.JmsTemplate;
import org.springframework.stereotype.Component;
import tacos.TacoOrder;
import tacos.kitchen.OrderReceiver;
@Component
public class JmsOrderReceiver implements OrderReceiver {
private JmsTemplate jms;
public JmsOrderReceiver(JmsTemplate jms) {
this.jms = jms;
}
@Override
public TacoOrder receiveOrder() {
return (TacoOrder) jms.receiveAndConvert("tacocloud.order.queue");
}
}
新版本的JmsOrderReceiver
的 receieveOrder()
方法简化到了只包含一行代码。同时我们不再需要注人 MessageConverter
,因为所有的操作都会在 receiveAndConvert()
方法的“幕后”完成。
在继续下面的内容学习之前,我们考虑一下如何在 Taco Cloud
厨房应用中使用receiveOrder()
。Taco Cloud
厨房中的厨师可能会按下一个按钮或者采取其他操作,表明准备好制作 taco
。此时,receiveOrder()
会被调用,然后对 receive()
或 receiveAndConvert()
的调用会阻塞。在订单消息抵达之前,这里不会发生任何的事情。一旦订单抵达,对receiveOrder()
的调用会把该订单的信息返回,订单的详细信息会展现给厨师,这样他就可以开始制作了。对于拉取模式来说,这似乎是一种很自然的选择。
接下来,我们看一下如何通过声明JMS
监听器来实现推送模式
声明消息监听器
拉取模式需要显式调用 receive()
或 receiveAndConvert()
接收消息,与之不同,消息监听器是一个被动的组件。在消息抵达之前,它会一直处于空闲状态。
要创建能够对 JMS
消息做出反应的消息监听器,我们需要为组件中的某个方法添加@JmsListener
注解。下面程序展示了一个新的 OrderListener
组件。它会被动地监听消息,而不主动请求消息。
//监听订单消息的 OrdeListener组件
package tacos.kitchen.messaging.jms.listener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.jms.annotation.JmsListener;
import org.springframework.stereotype.Component;
import tacos.kitchen.KitchenUI;
@Profile("jms-listener")
@Component
public class OrderListener {
private KitchenUI ui;
@Autowired
public OrderListener(KitchenUI ui) {
this.ui = ui;
}
@JmsListener(destination = "tacocloud.order.queue")
public void receiveOrder(Tacoorder order) {
ui.displayOrder(order);
}
}
receiveOrder()
方法使用了 JmsListener
注解,这样它就会监听 tacocloud.order.queue
目的地的消息。该方法不需要使用JmsTemplate
,也不会被我们的应用显式调用。相反Spring
中的框架代码会等待消息抵达指定的目的地,当消息到达时,receiveOrder()
方法会自动调用,并且会将消息中的 TacoOrder
载荷作为参数。
在很多方面,@JmsListener
注解都和Spring MVC
中的请求映射注解很相似,比如@GetMapping
或@PostMapping
。在 Spring MVC
中带有请求映射注解的方法会响应指定路径的请求。与之类似,使用@JmsListener
注解的方法会对到达指定目的地的消息做出响应。
消息监听器通常被视为最佳选择,因为它不会导致阻塞,并且能够快速处理多个消息但是在Taco Cloud
中,它可能并不是最佳的方案。在系统中,厨师是一个重要的瓶颈。他可能无法在接收到订单的时候立即准备 taco
。当新订单出现在屏幕上的时候,上一个订单可能刚刚完成一半。厨房用户界面需要在订单到达时进行缓冲,避免给厨师带来过重的负载。
这并不是说消息监听器不好。相反,如果消息能够快速得到处理,那这是非常适合的方案。但是,如果消息处理器需要根据自己的时间请求更多消息,那么 JmsTemplate
提供的拉取模式会更加合适。
JMS
是由标准 Java
规范定义的,所以它得到了众多代理实现的支持,在 Java
中实现消息时,它是很常见的可选方案。但是JMS
有一些缺点,尤其是作为 Java
规范的它只能用在 Java
应用中。RabbitMQ
和 Kafka
等较新的消息传递方案克服了这些缺点,可以用于JVM
之外的其他语言和平台。