异步消息发送
JMS
JMS是一个标准,它定义了同消息中间件工作的通用API。Spring提供了JmsTemplate
,我们可以使用这个模板来使用JMS。
JMS设置
在使用JMS之前,我们需要在项目中添加JMS客户端。对于Spring Boot来说,我们只需要添加starter依赖就可以了。
如果我们使用ActiveMQ:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-activemq</artifactId>
</dependency>
ActiveMQ Artemis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-artemis</artifactId>
</dependency>
Artemis 是基于ActiveMQ实现的,它比ActiveMQ更加的高效。
使用哪种客户端对我们的编码并没有太大的影响,但是在如何与中间件连接的配置上有一定的区别
Artemis:
Spring Boot 默认我们的Artemis中间件监听本地的61616端口,我们可以通过下面的属性来配置与中间件的连接信息:
ActiveMQ:
与Artemis将hostname和port分开配置不同,ActiveMQ用一个属性来配置中间件URL,这个URL的格式应该是:tcp://URL。
我们还应该将spring.activemq.in-memory
设置为false
,否则Spring会启动一个in-memory的中间件。in-memory中间件只适合消息发送和接收在同一个应用中的情景。
Artemis安装与启动
创建一个名为mybroker 的中间件:
启动mybroker中间件:
$ F:\MQ+Broker\mybroker\bin\artemis run
使用JmsTemplate 发送消息
只要我们在项目中添加了JMS starter依赖(ActiveMQ或者Artemis),Spring Boot就会自动配置一个JmsTemplate
bean。
JmsTemplate
有一下9个发送消息的方法:
- 3个
send()
方法需要一个MessageCreator
去处理Message
对象; - 3个
convertAndSend()
方法接受一个Object
对象,改对象被会被自动的转换为Message
; - 3个
convertAndSend()
方法除了接受一个会被自动转换为Message
的Object
外,还接受一个MesssagePostProcessor
,该对象负责在Message
被发送之前对Message
对象进行自定义处理。
send
除此之外,上面三类方法中的每一类都根据消息被指定的终点的不同被重载为3个方法:
- 1个方法不接受终点参数,消息被传递到默认终点;
- 1个方法接受一个
Destination
对象,该对象指定了消息的终点; - 1个方法接受一个
String
,该方法通过名称指定消息的终点。
public void sendOrder(Order order){
jmsTemplate.send(new MessageCreator(){
@Override
public Message createMessage(Session session) throws JMSException {
return session.createObjectMessage(order);
}
});
}
因为我们没有指定消息的终点,所以JmsTemplate将会使用默认的destination,我们需要在配置文件中配置该属性:
spring:
jms:
template:
default-destination: tacocloud.order.queue
这样,默认将消息发送到名为:tacocloud.order.queue的队列中。
除了使用默认的消息队列外,我们还可以指定特定的队列:
@Bean
public Destination orderQueue(){
return new ActiveMQQueue("tacocloud.order.queue");
}
注意是org.apache.activemq.artemis.jms.client.ActiveMQQueue
,而不是org.apache.activemq.command.ActiveMQQueue
。我们将这个bean注入到Service中,然后就可以使用它来指定消息目的地了。
jmsTemplate.send(orderQueue,new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createObjectMessage(order);
}
});
我们也可以不注入Destination
,而是直接指定目的地的名称:
jmsTemplate.send("tacocloud.order.queue",new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
return session.createObjectMessage(order);
}
});
convertAndSend
使用send
方法发送消息,我们需要提供一个复杂的MessageCreator
对象;使用convertAndSend
,我们直接提供需要发送的对象,该方法会自动将对象转化为一个Message
对象,然后再发送。
@Override
public void sendOrder(Order order){
jmsTemplate.convertAndSend("tacocloud.order.queue",order);
}
消息转换器
对象的转化是通过实现了org.springframework.jms.support.converter.MessageConverter
接口的类来实现的。该接口是由Spring提供的,它只定义了两个方法:
public interface MessageConverter{
Message toMessage(Object object, Session session);
Object fromMessage(Message message);
}
Spring 提供了该接口的几个实现:
SimpleMessageConverter
是默认的转换器,但是它要求被发送的对象实现了Serializable
接口。为了避免这个限制,我们可以使用MappingJackson2MessageConverter
。
使用其他的转换器,我们可以创建它的bean:
@Bean
public MappingJackson2MessageConverter messageConverter(){
MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();
messageConverter.setTypeIdPropertyName("_typeId");
return messageConverter;
}
只要创建了MappingJackson2MessageConverter
实例,JmsTemplate就不再使用默认的转换器了。
在创建MappingJackson2MessageConverter
实例时,我们调用了setTypeIdPropertyName
方法,这十分重要,正如该方法的名称所说,我们在message中定义了一个_typeId
属性,该属性引用了一个Map
这个Map用来映射typeId与真实类型。默认情况下,typeId为类型的限定名称(这导致客户端要有具有相同限定名称的类)。只有设置了typtIdPropertyName
,接收者的转换器才能从Message
中获取到typeId与真实类型的映射,才能将Message
转换为正确的类型。
我们也可以调用setTypeIdMappings
方法来设置自定义类型名称与真实类型之间的映射,让应用更加的灵活:
@Bean
public MappingJackson2MessageConverter messageConverter(){
MappingJackson2MessageConverter messageConverter = new MappingJackson2MessageConverter();
messageConverter.setTypeIdPropertyName("_typeId");
Map<String,Class<?>> mappings = new HashMap<>();
mappings.put("order",Order.class);
messageConverter.setTypeIdMappings(mappings);
return messageConverter;
}
我们在消息发送端和消息接受端都声明了类似的消息转换器,都设置类型名称为"order"与真实类型的映射。但是两边的真实类型限定名称可能并不相同,甚至该类型的域并非包含发送端Order
域的一个字域。
Post-Processing Messages
我们还可以在不修改被发送对象的情况下,添加额外的信息
若我们使用send
发送信息,那么:
jmsTemplate.send("tacocloud.order.queue",new MessageCreator() {
@Override
public Message createMessage(Session session) throws JMSException {
Message message = session.createObjectMessage(order);
message.setStringProperty("X_ORDER_SOURCE","WEB");
return message;
}
});
message.setStringProperty("X_ORDER_SOURCE","WEB")
:该语句在消息中添加了一个自定义头部,我们可以认为这是将该order标记为从WEB端订购的。
若我们使用convertAndSend
,那么:
jmsTemplate.convertAndSend("tacocloud.order.queue",order,new MesssagePostProcessor(){
@Override
public Message postProcessMessage(Message message) throws JMSException {
message.setStringProperty("X_ORDER_SOURCE","WEB");
return message;
}
});
接受JMS消息
接收端有 拉取 和 推送 两种模式来消费消息。JmsTemplate
提供了集中接受消息的方法,它们都是拉取模式。我们使用其中一个方法来获取消息,线程将会别阻塞直至有一个可用的消息为止。我们也可以定义一个消息监听器以此来实现推送模式。我们先看看JmsTemplate
提供的拉取模式。
使用JmsTemplate接收消息
JmsTemplate
提供了一下几个接收消息的方法:
首先我们应该设置同发送者对应的消息转换器:
@Bean
public MessageConverter json2MessageConverter(){
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
converter.setTypeIdPropertyName("_typeI");//值必须与发送者的相同
Map<String,Class<?>> mappings = new HashMap<>();
mappings.put("order", Order.class);//key值必须是发送者中设置过的
converter.setTypeIdMappings(mappings);
return converter;
}
使用receive
方法:
public Order receiveOrder(){
Message message = jmsTemplate.receive("tacocloud.order.queue");
return (Order) converter.fromMessage(message);
}
使用receiveAndConvert
方法:
public Order receiveOrder(){
return (Order) jmsTemplate.receiveAndConvert("tacocloud.order.queue");
}
消息监听器
@Component
public class OrderListener {
@JmsListener(destination = "tacocloud.order.queue")
public void receiveOrder(Order order){
log.info(order.toString());
}
}
在组件中使用@JmsListener
注解一个方法,就创建了一个消息监听器。上面的监听器将响应消息目的地为"tacocloud.order.queue"且消息装载的对象类型为Order
的消息。
使用JmsTemplate
接收消息是拉取模式,该模式下,接收端只能接收到调用receive
或receiveAndConvert
方法之后,第一个到达指定队列的消息。
使用@JmsListener
创建消息监听器是推送模式,接收端启动后,将会被动的接收发送端发送过来的每一条消息。
RabbitMQ 和 AMQP(Advanced Message Queuing Protocol)
消息中间件通过目的地名称来处理JMS消息,接受者也通过目的地名称来取数据。RabbitMQ是AMQP的实现,它提供了比JMS更好的消息路由策略。它通过 exchange 名称和一个路由关键字来处理AMQP消息。下面是 exchange 和队列之间的关系图:
当消息到达RabbitMQ中间件时,消息被发送到将要处理它的 exchange ,这个 exchange 负责将消息路由到一个或多个队列中。至于路由策略,取决于 exchange 的类型、exchange 和 队列之间的 binding 以及消息中路由关键字的值。
下面是几种类型的 exchange:
- default : 一个有中间件自动创建的特殊 exchange,它会将消息路由到与消息中 路由关键字 名称相同的队列中。所有的队列都被自动的绑定到默认的 exchange 上。
- Direct : 消息被路由到这样的 一个 队列中:队列的 绑定关键字 与消息的 路由关键字 相同。
- Topic : 消息被路由到 一个或多个 这样的队列中: 队列的 绑定关键字(可能含有通配符) 与消息的 路由关键字 相同。
- Fanout :消息被路由到所有的队列中。
- Headers :与 Topic 类型差不多,只是这里是将消息的头部值与队列的绑定关键字匹配。
- Deadletter : 该类型的 exchange 接收任何不会被发送出去的消息的。
安装RabbitMQ
直接安装RabbitMQ到机器上需要机器安装了Erlang,而Docker上有RabbitMQ的官方镜像,所以使用Docker容器是最方便的。
$ docker pull rabbitmq:3-management
$ docker run -d --hostname my-rabbit -p 5672:5672 -p 15672:15672 --name some-rabbit rabbitmq:3-management
启动RabbitMQ容器是,指定--hostname
值是十分必要的。RabbitMQ基于节点名称来存储数据,而这个节点名称默认为hostname,所以指定hostname,我们就能够追踪我们的数据。容器默认监听5672端口。
我们也可以通过http://localhost:15672
来访问控制台。
若要修改默认的用户名和密码:
$ docker run -d --hostname my-rabbit -p 5672:5672 -e RABBITMQ_DEFAULT_USER=user -e RABBITMQ_DEFAULT_PASS=password rabbitmq:latest
添加RabbitMQ到Spring应用中
我们唯一要做的就是添加AMQP stater到Spring Boot项目中,我们就可以发送和接受RabbitMQ消息了。添加该依赖,Spring Boot就会自动的创建AMQP连接工厂和RabbitTemplate
Bean以及其它相关的组件。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
下面是一些关于RabbitMQ的配置参数:
username和password默认为"guest"
发送消息
RabbitTemplate
提供了几个发送消息的方法:
与JmsTemplate
类似,前面三个send
方法发送Message
对象消息,中间三个convertAndSend
方法发送Object
对象,该对象在消息发送之前会被自动转换位Message
对象;最后是三个converterAndSend
还接受一个PostProcessMessage
对象,这个三个方法在Message
被发送前对Message
对象进行处理。
这些方法区别与JmsTemplate
发送方法之处是:这些方法接受字符串来指定 exchange 和 routing key,而不是Destination
对象或者目的地名称。那些不接收 exchange 值的方法会将消息发送到默认 exchange 中。相同的,那些不接收 routing key 的方法会使用默认的 routing key 来路由消息。
一个发送消息的示例:
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tacos.messaging.OrderMessagingService;
import tacos.pojo.Order;
@Service
public class RabbitOrderMessagingService implements OrderMessagingService {
private RabbitTemplate rabbitTemplate;
@Autowired
public RabbitOrderMessagingService(RabbitTemplate rabbitTemplate) {
this.rabbitTemplate = rabbitTemplate;
}
@Override
public void sendOrder(Order order) {
MessageConverter converter = rabbitTemplate.getMessageConverter();
MessageProperties props = new MessageProperties();
Message message = converter.toMessage(order,props);
rabbitTemplate.send("tacocloud.order.queue",message);
}
}
如果是使用send
方法,我们需要将消息转化为Message
对象。RabbitTemplate
为我们提供了转换器,我们只需要调用getMessageConverter
方法即可。在将消息转换为Message
对象时,必须提供一个MessageProperties
对象,我们可以不设置消息参数,直接提供一个默认的实例就可以了。
在上面的例子中,我们只提供了 routing key, 那么默认的 exchange 将会被使用。消息将通过默认的 exchange 路由到名为tacocloud.order.queue
的队列中。所有我们需要在容器中定义一个名为tacocloud.order.queue
队列:
@Bean Queue queue(){
return new Queue("tacocloud.order.queue");
}
默认的 exchange 名称是"",它是中间件自动创建的。相同的,默认的 routing key 名称也是""。
我们可以在配置文件中配置默认 exchange 和 routing key
spring:
rabbitmq:
template:
exchange: tacocloud.orders
routing-key: kitchens.central
配置消息转换器
默认情况下,RabbitTemplate
使用SimpleMessageConverter
来完成消息转换,这个转换器只能转换简单类型和实现了Serializable
接口的对象。Spring 为RabbitTemplate
提供了几个消息转换器在org.springframework.amqp.support.converter包下:
我们只需要配置一个MessageConverter
Bean,Spring Boot发现这个bean并自动用这个Bean替换掉默认的MessageConverter
;
设置消息参数
当我们使用send
发送消息是,我们可以直接将参数放入Message
对象中:
public void sendOrder(Order order) {
MessageConverter converter = rabbitTemplate.getMessageConverter();
MessageProperties props = new MessageProperties();
props.setHeader("X_ORDER_SOURCE","WEB");
Message message = converter.toMessage(order,props);
rabbitTemplate.send("tacocloud.order.queue",message);
}
当我们使用convertAndSend
方法时,我们需要使用MessagePostProcessor
:
public void sendOrder(Order order) {
rabbitTemplate.convertAndSend("tacocloud.order.queue", order, new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
MessageProperties properties = message.getMessageProperties();
properties.setHeader("X_ORDER_SOURCE","WEB");
return message;
}
});
}
接收消息
RabbitTemplate
提供了以下几种接收消息的方法:
接收者不需要关注什么 exchange,routing key,它只需要关心从哪个队列取消息及可,所以这些接收方法都只需要queueName
。
有些方法还接受timeoutMillis
,这个参数指定方法的超时时间。默认超时时间为0,当调用这些接收方法后,线程被阻塞,直到从队列中取到数据,或者超时,从而得到一个null
返回值。
receive
:
public Order receiveOrder(){
MessageConverter converter = rabbitTemplate.getMessageConverter();
Message message = rabbitTemplate.receive("tacocoud.order,queue");
return message != null ? (Order)converter.fromMessage(message) : null;
}
我们可以在方法中设置超时时间,也可以在配置文件中设置默认的超时时间:
spring:
rabbit:
template:
receive-timeout: 3000;
receiveAndConvert
:
public Order recieveOrder(){
return (Order) rabbitTemplate.receiveAndConvert("tacocoud.order,queue");
}
只要我们配置了发送端相同的消息转换器,我们就可以这样来接受消息。唯一需要注意的是在Object
转Order
时,由于这里的消息转换器并没有像JMS那样设置typeId
与真实类型的映射,所以这里需要发送端和接受端的Order
对象有完全相同的 限定名称。
为了避免这个限制,我们可以使用ParameterizedTypeReference<T>
:
public Order recieveOrder(){
return (Order) rabbitTemplate.receiveAndConvert("tacocoud.order,queue",new ParameterizedTypeReference<Order>(){});
}
ParameterizedTypeReference<Order>
告诉消息转换器,我们需要将Massege
转换为Order
类型的对象。使用这中方式也有一个限制:消息转换器必须是实现了SmartMessageConverter
接口的类型,Jackson2JsonMessageConverter
和Jackson2XmlMessageConverter
实现了这个接口。
以上例子是接受者主动拉取(pull)消息。与JMS只能拉取接收者调用接收方法后发送者发送的消息不同,RabbitMQ会将消息缓存在队列中,直到接受者将消息取走。
我们还可以通过@RabbitListener
实现推送消息:
@Component
@Slf4j
public class RabbitOrderListener {
@RabbitListener(queues = "tacocloud.order.queue")
public void receive(Order order){
log.info(order.toString());
}
}
Kafka
- Kafka作为一个集群,运行在一台或多台机器上。
- Kafka通过 topic 对存储的流数据进行分类。
- 每条记录中包含一个key,一个value和一个timestamp。
使用Kafka
我们只需要加入Kafka的依赖就可以马上开始使用了:
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
当加入Kafka的依赖之后,Spring Boot会自动创建KafkaTemplate
Bean以及其它相关的组件。
发送消息
KafkaTemplate
提供了一下几个发送消息的方法:
我们注意到,并没有类似convertAndSend
方法,这是因为KafkaTemplate
使用了泛型类型,当发送消息时,可以直接处理domain类。
另外几个方法还接受这些参数:
topic
:指定将消息发送到那个 topic,对于send
方法来说是必须的。partition
:指定将 topic 写入那个 partition。key
:指定消息的key。timestamp
:可选,默认为System.currentTimeMillis()
。data
:消息。
另外send
方法还接受一个ProducerRecord
类型的参数,这个参数其实就是包含了上面参数的一个类型。
我们也可以传一个Message
类型的参数给send
方法,但是使用该方式需要我们将domain对象转换为Message
类型。
发送String
类型数据:
private KafkaTemplate kafkaTemplate;
@Autowired
public KafkaOrderMessageService(KafkaTemplate kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
@Override
public void sendOrder(Order order) {
String jOrder = JSON.toJSONString(order);//fastjson
kafkaTemplate.send("tacocloud.order.topic",jOrder);
}
发送自定义类:
Kafka发送和接收的消息,都是byte[]
数组,但是我们在上面的例子中,我们却发送了一个字符串对象,这得益于Kafka的序列化和反序列化。Spring Boot默认为Kafka配置使用Kafka原生的StringSerializer
来序列化key
和data
值。我们可以在配置文件中进行对其进行修改:
spring:
kafka:
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: tacos.messaging.OrderSerializer
上面我们修改了data
的序列化工具为tacos.messaging.OrderSerializer
,因此,我们需要实现这个类:
package tacos.messaging;
import com.alibaba.fastjson.JSON;
import org.apache.kafka.common.serialization.Serializer;
import tacos.pojo.Order;
public class OrderSerializer implements Serializer<Order> {
@Override
public byte[] serialize(String s, Order order) {
return JSON.toJSONBytes(order);
}
}
这样,我们就可以发送自定义对象了:
public void sendOrder(Order order) {
kafkaTemplate.send("tacocloud.order.topic",order);
}
接收消息
首先,我们需要在配置文件中指定消费者组Id:
kafka:
consumer:
group-id: group1
与JmsTemplate
或RabbitTemplate
不同的是,KafkaTemplate
不提供任何接收消息的方法,这意味着我们只能通过监听器来接收消息。
接收String
消息:
@KafkaListener(topics = "tacocloud.order.topic")
public void receive(String jOrder){
Order order = JSON.parseObject(jOrder,Order.class);
log.info(order.toString());
}
接收自定义对象:
我们在发送消息时,对消息进行了序列化,那么在接收到对象后,我们应该对byte[]
类型的消息进行反序列化:
package tacos.tacokitchens;
import com.alibaba.fastjson.JSON;
import org.apache.kafka.common.serialization.Deserializer;
import tacos.tacokitchens.pojo.Order;
public class OrderDeserializer implements Deserializer<Order> {
@Override
public Order deserialize(String s, byte[] bytes) {
return JSON.parseObject(bytes,Order.class);
}
}
在配置文件中配置反序列化工具:
kafka:
consumer:
group-id: group1
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: tacos.tacokitchens.OrderDeserializer
@KafkaListener(topics = "tacocloud.order.topic")
public void receive(Order order){
log.info(order.toString());
}
如果我们想要获取更多与消息相关的元数据的话,上面的方法还可以接收一个ConsumerRecord
或者Message
:
@KafkaListener(topics = "tacocloud.order.topic")
public void receive(Order order,ConsumerRecord<String,Order> record){
log.info("Receive from partition{} whit timestamp{}",record.partition(), record.timestamp());
log.info(order.toString());
}
或者:
@KafkaListener(topics = "tacocloud.order.topic")
public void receive(Order order,Message<Order> record){
MessageHeaders headers = message.getHeaders();
log.info("Receive from partition{} with timestamp{}",headers.get(KafkaHeaders.RECEIVED_PARTITION_ID),headers.get(KafkaHeaders.RECEIVED_TIMESTAMP));
log.info(order.toString());
}