微服务之五消息驱动
一: 概述
JavaEE提供了Message Driven Bean(消息驱动bean),用于处理企业组件间的消息驱动。
Spring Cloud也引入了相应的驱动,Spring Cloud Stream
市面上存在的消息代理中间件
- ActivitiMQ
- RabbitMQ
- Kafka
类似邮局,有生产者、消息代理、消费者
Stream框架:
- 持久化订阅的支持
- 消费者组的支持
- Topic分区支持
- 支持RabbitMQ和Kafka两种消息代理组件
好处:不同考虑中间使用的什么代理机制,利用Stream实现消息的生产与发送
消息代理中间件模式
使用了Spring Cloud Stream结构后
使用了Stream后,生产者和消费者可以更加专注自己的业务,至于消息是如何投递、使用的那个消息代理则无需关心
二: RabbitMQ框架
RabbitMQ使用AMQP协议
2.1 消息生产者
消息的生产者/消费者都属于客户端,均使用AMQP协议与RabbitMQ服务器进行通信
添加依赖
<!-- AMQP -->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.2.0</version>
</dependency>
<!-- SLF4J日志 -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.9</version>
</dependency>
发消息
package com.atm.cloud;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class SendMessage {
public static void main(String[] args) throws Exception {
/*
* 1.生产者会发送消息给RabbitMQ服务器。 2.通过渠道叫消息发送给交换器。 3.交换器会发送给队列。 4.队列将消息发送给消费者。
*/
// 建立连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 设置host,其实无需设置,默认为localhost,用户名/密码默认guest,端口默认5671
// factory.setHost("localhost");
// 创建新的连接
Connection connection = factory.newConnection();
// 通过连接创建渠道(向该渠道发送消息)
Channel channel = connection.createChannel();
// 声明交换器(默认绑定),交换器会将消息发送给队列,对列再发送给消费者
// 直接声明队列,使用默认交换器
String queueName = "MyQueueName";
channel.queueDeclare(queueName, false, false, false, null);
// 创建消息,使用渠道发布消息,""使用默认交换器,本列子中routingKey就使用queueName
String messageBody = "Hello Wrold!";
channel.basicPublish("", queueName, null, messageBody.getBytes());
// 发送之后,关闭渠道等(先关渠道,再关连接)
channel.close();
connection.close();
}
}
2.2 消费者
package com.atm.cloud;
import java.io.IOException;
import com.rabbitmq.client.AMQP.BasicProperties;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
public class ReadMessage {
public static void main(String[] args) throws Exception {
// 建立连接工厂
ConnectionFactory factory = new ConnectionFactory();
// 创建新的连接
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String queueName = "MyQueueName";
channel.queueDeclare(queueName, false, false, false, null);
// 通过队列创建Consumer
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
BasicProperties properties, byte[] body) throws IOException {
String msg = new String(body, "UTF-8");
System.out.println("接收到的消息:" + msg);
}
};
// 渠道绑定consumer
channel.basicConsume(queueName, consumer);
}
}
2.3 交换器、绑定与队列
在RabbitMQ中,生产者的消息不会直接到队列中,只会讲消息发送给交换器,交换器一边从生产者接受信息,一边将信息发送给各个队列。
消息发送过来,消息自身带着一个routingKey,交换器根据该key进行队列的绑定
RabbitMQ提供四种交换器
- direct:根据生产者传过来的routingKey是否等于bindingkey,来决定将消息发送给哪个队列
- topic:根据传过来的routingkey是否匹配一定的表达式,来决定消息发送给哪个或者哪些队列
- fanout:将消息发送给交换器知道的全部队列,这种交换器会忽略掉设置的routingkey(广播机制)
- headers:根据消息的头消息,来决定将消息发送给哪些队列
三:Kafka框架
3.1 生产者
依赖
<dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-clients</artifactId>
<version>0.11.0.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.9</version>
</dependency>
发消息
package com.atm.cloud;
import java.util.Properties;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.Producer;
import org.apache.kafka.clients.producer.ProducerRecord;
/**
* 向Kafka服务器发送消息
*/
public class SendMessage {
public static void main(String[] args) throws Exception {
// 配置信息
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// String的序列化类
// 设置数据key的序列化处理类
props.put("key.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
// 设置数据value的序列化处理类
props.put("value.serializer",
"org.apache.kafka.common.serialization.StringSerializer");
// 创建生产者实例
Producer<String, String> producer = new KafkaProducer<String, String>(
props);
// 创建一条新的记录,第一个参数为Topic名称
// 会向topic发送userName-aitemi键值,所有的数据都是通过键值保存的
ProducerRecord record = new ProducerRecord<String, String>("my-topic",
"userName", "aitemi");
// 发送记录
producer.send(record);
producer.close();
}
}
3.2 消费者
package com.atm.cloud;
import java.util.Arrays;
import java.util.Properties;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords;
import org.apache.kafka.clients.consumer.KafkaConsumer;
/**
* 消费者,订阅"my-topic",获取其中的信息
*/
public class ReadMessage {
public static void main(String[] args) throws Exception {
// 配置信息
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
// 必须指定消费者组
props.put("group.id", "test");
props.put("key.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer",
"org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(
props);
// 订阅 my-topic 的消息,可以订阅多个topic
consumer.subscribe(Arrays.asList("my-topic"));
// 到服务器中读取记录,会一直拉取
while (true) {
// 通过consumer的一个拉取方法
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.println("这是消费者A,key: " + record.key() + ", value: "
+ record.value());
}
}
}
}
3.3 消费者组
在同一个消费者组的,在接受同一个TOPIC时,会体现负载均衡的效果。轮换接受信息。
在不同消费者组的,接受同一个TOPIC时,都会收到信息,广播的效果
二:开发消息微服务
Spring Cloud Stream简化了步骤,进行少量的配置可以实现前面两个框架的功能,不需要调用上面两种框架的API
2.1 准备工作
三个项目
- spring-service:Eureka 服务端
- spring-consumer:消费者
- spring-producer:生产者
整个集群如下图所示:
2.2 application配置
server:
port: 9000
spring:
application:
name: atm-msg-consumer
rabbitmq:
host: localhost
port: 5762
username: guest
password: guest
# 均使用默认的配置,所以可以无需配置,需要修改时,再配置
2.3 生产者
2.3.1 依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
2.3.2 启动类
添加@EnableBinding注解,指定服务接口
package com.atm.cloud;
import java.util.Scanner;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
@SpringBootApplication
@EnableEurekaClient
@EnableBinding(SendMessageInterface.class)//开启绑定
public class MsgProducerApp {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String port = scanner.nextLine();
new SpringApplicationBuilder(MsgProducerApp.class).properties(
"server.port=" + port).run(args);
}
}
2.3.3 服务接口
package com.atm.cloud;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.SubscribableChannel;
/**
* 发送消息的服务接口,用来绑定Topic
*/
public interface SendMessageInterface {
/**
* 声明一个方法用来订阅渠道,使用output注解,声明渠道名称,从这里输出一个消息
*/
@Output("myInput")
SubscribableChannel sendMsg();
}
使用@Output注解会创建myInput的消息通道,调用次方法后会向myInput通道投递消息。
2.3.4 控制层
package com.atm.cloud;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 使用SendMessageInterface中的消息渠进行发送消息
*/
@RestController
public class SendMessageController {
@Autowired
private SendMessageInterface sendMessageInterface;
@GetMapping("/send")
public String sendMsg() {
Message msg = MessageBuilder.withPayload("Hello World".getBytes())
.build();
sendMessageInterface.sendMsg().send(msg);
return "Success";
}
}
2.4 消费者
2.4.1 依赖
和生产者的依赖一样
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
2.4.2 服务接口
接受消息的通道接口
package com.atm.cloud;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
/**
* 用于接收消息
*/
public interface ReadMessageInterface {
// 绑定myInput的渠道
@Input("myInput")
SubscribableChannel readMsg();
}
2.4.3 启动类和监听、接受消息
package com.atm.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
@SpringBootApplication
@EnableDiscoveryClient
@EnableBinding(ReadMessageInterface.class)
public class MsgConsumerApp {
public static void main(String[] args) {
SpringApplication.run(MsgConsumerApp.class, args);
}
/**
* 用于监听接收的消息
*/
@StreamListener("myInput")
public void onListen(byte[] msg) {
System.out.println("接收到的消息:" + new String(msg));
}
}
2.5 更换绑定器
若需要更换为kafka,则需要在生产者和消费者中同时更改依赖项
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-kafka</artifactId>
</dependency>
2.6 消费者组
若需要使用消费者组,则需要更改配置文件
spring:
application:
name: spring-msg-consumer3
##配置消费者组
cloud:
stream:
bindings:
myInput:
group: groupB
2.7 Sink、Source、Processor
- 为了简化开发,SpringCloud Stream内置了三个接口,Sink、Source、Processor
- Processor继承了Sink和Source,实际应用中可以考虑只使用Processor
Sink
public interface Sink {
String INPUT = "input";
@Input(Sink.INPUT)
SubscribableChannel input();
}
Source
public interface Source {
String OUTPUT = "output";
@Output(Source.OUTPUT)
SubscribableChannel output();
}
根据这两个接口可知,实际上帮我们内置了”input”和“output”两个通道,那么在大多数情况下,我们就可以不必编写服务接口,甚至不必使用@input和@output两个注解,在绑定通道时加入Sink.class
package com.atm.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.cloud.stream.messaging.Source;
@SpringBootApplication
@EnableDiscoveryClient
//@EnableBinding(ReadMessageInterface.class)
@EnableBinding(value={Sink.class})
public class MsgConsumerApp {
public static void main(String[] args) {
SpringApplication.run(MsgConsumerApp.class, args);
}
/**
* 用于监听接收的消息
*/
//@StreamListener("myOutput)
@StreamListener(Sink.INPUT)
public void onListen(byte[] msg) {
System.out.println("A:接收到的消息:" + new String(msg));
}
}