《Spring in Action》第8章-异步消息发送

异步消息发送

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就会自动配置一个JmsTemplatebean。

JmsTemplate 有一下9个发送消息的方法:

  • 3个send()方法需要一个MessageCreator去处理Message对象;
  • 3个convertAndSend()方法接受一个Object对象,改对象被会被自动的转换为Message;
  • 3个convertAndSend()方法除了接受一个会被自动转换为MessageObject外,还接受一个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接收消息是拉取模式,该模式下,接收端只能接收到调用receivereceiveAndConvert方法之后,第一个到达指定队列的消息。

使用@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连接工厂和RabbitTemplateBean以及其它相关的组件。

<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发送方法之处是:这些方法接受字符串来指定 exchangerouting 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 名称也是""。

我们可以在配置文件中配置默认 exchangerouting key

spring:
	rabbitmq:
		template:
			exchange: tacocloud.orders
			routing-key: kitchens.central
配置消息转换器

默认情况下,RabbitTemplate使用SimpleMessageConverter来完成消息转换,这个转换器只能转换简单类型和实现了Serializable接口的对象。Spring 为RabbitTemplate提供了几个消息转换器在org.springframework.amqp.support.converter包下

我们只需要配置一个MessageConverterBean,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");
}

只要我们配置了发送端相同的消息转换器,我们就可以这样来接受消息。唯一需要注意的是在ObjectOrder时,由于这里的消息转换器并没有像JMS那样设置typeId与真实类型的映射,所以这里需要发送端和接受端的Order对象有完全相同的 限定名称

为了避免这个限制,我们可以使用ParameterizedTypeReference<T>

public Order recieveOrder(){
	return (Order) rabbitTemplate.receiveAndConvert("tacocoud.order,queue",new ParameterizedTypeReference<Order>(){});
}

ParameterizedTypeReference<Order>告诉消息转换器,我们需要将Massege转换为Order类型的对象。使用这中方式也有一个限制:消息转换器必须是实现了SmartMessageConverter接口的类型,Jackson2JsonMessageConverterJackson2XmlMessageConverter实现了这个接口。

以上例子是接受者主动拉取(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会自动创建KafkaTemplateBean以及其它相关的组件。

发送消息

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来序列化keydata值。我们可以在配置文件中进行对其进行修改:

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

JmsTemplateRabbitTemplate不同的是,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());
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值