目录
MQ
消息队列 是指利用 高效可靠 的 消息传递机制 进行与平台无关的 数据交流,并基于 数据通信 来进行分布式系统的集成
存放消息的队列,是一种跨进程的通信机制,用于上下游传递消息。
MQ三大应用
削峰
比如说下订单系统最多能处理一万次订单,但是高峰期超出,要限制超出的部分,就使用消息队列做缓冲,把一秒内的订单分散一段时间处理
异步
一个下单操作,业务流程很长,响应时间长,用户体验不好。
使用消息队列,下单时只用调用支付系统,其他业务可以通过消息方式发出,其他系统接收到消息去执行,实现异步处理
解耦
电商系统有很多子系统,下单系统、库存系统、物流系统、支付系统,用户下单,只要有一个系统出现故障,都会影响其他。
使用消息队列,一个系统出故障后,请求可以暂放到消息队列中,等修复后再处理,就不影响其他操作。
缺点
- 系统可用性降低:消息队列万一挂了就g了
- 系统复杂性增加:一致性问题、如何保证消息不被重复消费,如何保证消息可靠传输
JMS & AMQP
JMS:定义了对消息操作的统一接口,限定必须使用Java语言;只有队列模式和主题模式两种模式;ActiveMQ和RocketMQ都基于JMS
AMQP:高级消息对列协议,规定的是数据格式,所以不限制语言;多种模式。RabbitMQ
MQ分类
- Kafka:适应产生大量数据收集业务,日志采集
- RocketMQ:金融互联网,扣订单
- RabbitMQ:时效性,延迟低
RabbitMQ
接收、存储、转发的中间件
组成
- Broker:接收和分发消息的应用,RabbitMQ Server
- Connection:发送消息和接收消息的TCP长连接
- Channel:信道,发消息的通道,一个信道可以发送多个消息,轻量级的连接减少建立TCP连接开销
- Exchange:交换机
- Binging:交换机与队列的虚拟连接
- Queue:消息会最终送到这,被消费者取走
安装
安装链接: Linux下安装RabbitMQ
看着这个教程一下就能成功
启动:/usr/local/software/rabbitmq_software/rabbitmq_server-3.7.16/sbin/rabbitmq-server -detached
浏览器访问:http://192.168.32.129:15672/
添加依赖
<!--rabbitmq依赖客户端-->
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.8.0</version>
</dependency>
<!--操作文件流的依赖-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
模式总汇
- hello word 模式:一个消费者对应一个队列对应一个消费者
- 工作队列模式:队列中的消息可以被多个消费者消费,但消息只能被消费一次;用于提高消费消息的速度
- 广播模式:消息发送到交换机,交换机与队列建立绑定关系完成消息分发;消息可以被共同消费
- 路由模式:绑定时需要指定routing key,发送消息也要携带routing key,就可以进行类似筛选发送
- 主题模式:routing key支持通配符,更加灵活
Hello World
发送
/**
* 生产者:发消息
*
* @author Deevan
*/
public class Producer {
//队列名称
public static final String QUEEN_NAME = "hello";
//发消息
public static void main(String[] args) throws IOException, TimeoutException {
//创建一个连接工厂
ConnectionFactory factory = new ConnectionFactory();
//设置连接相关信息 (主机、用户名、密码)
factory.setHost("192.168.32.129");
factory.setUsername("admin");
factory.setPassword("000");
//通过工厂创建连接
Connection connection = factory.newConnection();
//通过连接创建信道
Channel channel = connection.createChannel();
/*
生成一个队列
1.队列名称
2.队列里的消息是否持久化(磁盘),默认情况存储在内存中
3.该队列是否直供一个消费者进行消费 是否进行消费共享:true可以多个 false只有一个
4.是否自动删除 最后一个消费者断开连接后 该队是否自动删除 true自动删除
5.其他参数 null
*/
channel.queueDeclare(QUEEN_NAME, false, false, false, null);
//编辑消息
String message = "Hello World";
/*
发送一个消息
1.发送到哪个交换机
2.路由的Key值是哪个 本次是队列的名称
3.其他消息参数
4.发送的消息体
*/
channel.basicPublish("", QUEEN_NAME, null, message.getBytes());
System.out.println("消息发送成功");
}
}
有一个消息正在准备被消费,总共一个消息
接收
/**
* 消费者:接收消息
*
* @author Deevan
*/
public class Consumer {
//队列名称
public static final String QUEUE_NAME = "hello";
//接收消息
public static void main(String[] args) throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("192.168.32.129");
factory.setUsername("admin");
factory.setPassword("000");
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明接收消息(3)
DeliverCallback deliverCallback = (var1, var2) -> {
System.out.println(new String(var2.getBody()));
};
//取消消息时回调(4)
CancelCallback cancelCallback = (var1) -> {
System.out.println("消息被中断");
};
/*
消费者消费消息
1.消费哪个队列
2.消费之后是否要自动答应 true自动 false手动
3.消费者未成功消费的回调
4.消费者取消消费的回调
*/
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
消息队列弹出消息,消费者接收消息
Work Queues
工作队列(任务队列):大量任务到来,避免立即执行资源密集型任务---->把任务封装为消息并发送到队列,后台多个进程将任务弹出并已轮询的方式执行
轮询接收消息
/**
* 消息生产者,从控制台输入
*
* @author Deevan
*/
public class Task {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtil.getChannel();
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("发送消息完成:\t" + message);
}
}
}
/**
* 消费者接收消息,多个消费者采用轮询的方式接收
*
* @author Deevan
*/
public class Worker {
public static final String QUEUE_NAME = "hello";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtil.getChannel();
//消息接收回调
DeliverCallback deliverCallback = (var1, var2) -> {
System.out.println("接收到的消息:" + new String(var2.getBody()));
};
//消息取消时回调
CancelCallback cancelCallback = (var1) -> {
System.out.println(var1 + "消费者取消消息回调");
};
channel.basicConsume(QUEUE_NAME, true, deliverCallback, cancelCallback);
}
}
消息应答
问题:MQ向消费者发送消息,消费者挂掉了,但是MQ不知道,MQ会删除发出的消息,就会造成消息丢失
消息应答就是在消费者处理消息后,告诉MQ它已经处理了,MQ就可以把该消息删除了
自动应答
接收到消息就会应答
手动应答
可以批量应答(multiple)减少网络开销,保证消息不丢失
- Channel.basicAck():用于肯定确认,通知MQ可以丢弃
- Channel.basicNack():用于否定确认
- Channel.basicReject():用于否定确认,少个参数(是否批量处理)
消息自动重新入队
消息没有被执行完没有响应Ack,MQ知道了就把该消息重新分配给其他消费者;保证消息不丢失
更改消费者代码
一个消费者挂掉了,他未完成接收的消息会被其他消费者执行,不会丢失
Rabbit持久化
在RabbitMQ挂掉之后仍能保证消息不丢失
队列 和 消息都应持久化
队列持久化
即使MQ宕机队列仍存在
消息持久化
将消息保存到磁盘,也不是绝对不丢失(绝对要:发布确认)
不公平分发
轮询分发公平
不公平分发:按能力分发
预取值
指定分发
发布确认
队列、消息持久化要保存到磁盘上,也可能会出现消息丢失
生产者产出消息,MQ将消息保存到磁盘上 告诉生产者进行确认,即可解决
开启发布确认
channel.confirmSelect();
单个确认
发布速度很慢,出问题知道问题在哪
//单个确认
boolean flag = channel.waitForConfirms();
批量确认
等待100条消息一次确认
batchSize = 100;
异步批量确认
无论MQ是否收到消息,都会给发送者一个回调
- 使用一个线程安全有序的map记录发出的消息
- 确认的消息在确认的消息回调中进行删除(map中该消息)
- 剩余的就是未确认的消息,在未确认的消息回调中处理
性能最好,可准确确认到哪个消息确认失败并能作出处理
/**
* 异步确认应答
*
* @author Deevan
*/
public class Task {
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtil.getChannel();
String queueName = UUID.randomUUID().toString();
channel.queueDeclare(queueName, true, false, false, null);
//开启发布确认
channel.confirmSelect();
long s = System.currentTimeMillis();
//线程安全的有序哈希表:1.将序号和消息关联 2.给到序号可以批量删除消息 3.线程安全
ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
//准备消息监听器 监听那些消息发送成功 那个消息失败
//消息成功回调
ConfirmCallback ackCallback = (var1, var3) -> {
//批量确认
if (var3) {
//删除map中确认的消息,剩下的就是未确认的
ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(var1);
confirmed.clear();
} else {
//单个确认
outstandingConfirms.remove(var1);
}
};
//消息失败回调 1.消息标记 2.是否批量确认
ConfirmCallback nackCallback = (var1, var3) -> {
//接收未确认消息和消息体
String nackMessage = outstandingConfirms.get(var1);
System.out.println("未确认的消息标记:" + var1 + "\t该消息内容是:" + nackMessage);
};
//异步通知 1.监听那些消息成功 2.那些消息失败
channel.addConfirmListener(ackCallback, nackCallback);
//1000个消息
for (int i = 0; i < 1000; i++) {
String message = i + "";
channel.basicPublish("", queueName, null, message.getBytes());
//记录发送的所有消息,放在map中
outstandingConfirms.put(channel.getNextPublishSeqNo(), message);
}
long e = System.currentTimeMillis();
System.out.println("耗时:" + (e - s) + "ms");
}
}
三种方式对比
交换机
- 接收来自生产者的消息
- 将消息推送到队列
交换机可以确定这些消息是以哪种方式推送到队列
类型
- 直接(direct)路由类型
- 主题(topic)
- 标题(headers)
- 扇出(fanout)发布订阅
- 无名类型:“ ”表示,默认
临时队列
绑定
交换机与队列的捆绑关系
广播模式(扇出交换机)
简单模式/工作模式:生产的消息只能被消费一次
队列中的一个消息仍只能被消费一次,单使用交换机绑定队列,生产者发送的一个消息可以被多个消费
交换机绑定队列,生产者发消息向交换机,交换机根据模式确定发送给哪些队列;同时多个消费者可以获取这些队列中的消息
一个交换机logs绑定了两个队列
生产者发送消息(发布)
/**
* 发送者
*
* @author Deevan
*/
public class EmitLog {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtil.getChannel();
//channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String message = scanner.next();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
System.out.println("生产者发出消息:" + message);
}
}
}
多个消费者(订阅)
/**
* 接受者接收消息(其中之一)
*
* @author Deevan
*/
public class ReceiveLogs01 {
public static final String EXCHANGE_NAME = "logs";
public static void main(String[] args) throws IOException, TimeoutException {
Channel channel = RabbitMqUtil.getChannel();
//声明交换机 名称 类型(发布订阅)
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//声明临时队列:名称随机,消费者断开连接队列自动删除
String queueName = channel.queueDeclare().getQueue();
//绑定交换机与队列
channel.queueBind(queueName, EXCHANGE_NAME, "");
System.out.println("01等待接收消息");
//接收消息回调
DeliverCallback deliverCallback = (var1, var2) -> {
System.out.println("01接收到的消息" + new String(var2.getBody()));
};
//接收消息
channel.basicConsume(queueName, true, deliverCallback, cancel -> {});
}
}
路由模式(直接交换机)
与扇出交换机的区别就是多重绑定routingKey,交换机根据routingKey决定发送给谁
主题模式(主题交换机)
利用类似正则表达式的模式(#,*)绑定router_key,更加灵活
可包含 扇出交换机和直接交换机
接受者
发送者
消息传输保证层级
At most once:最多一次。消息可能会丢失,但不会重复传输。
At least once:最少一次。消息绝不会丢失,但可能会重复传输。
Exactly once: 恰好一次,每条消息肯定仅传输一次
队列结构
通常由以下两部分组成:
rabbit_amqqueue_process
:负责协议相关的消息处理,即接收生产者发布的消息、向消费者交付消息、处理消息的确认(包括生产端的 confirm 和消费端的 ack) 等。
backing_queue
:是消息存储的具体形式和引擎,并向 rabbit amqqueue process 提供相关的接口以供调用。
死信队列
无法被消费消息
为保证业务消息数据不丢失,当消息发生异常时,将消息发送到死信队列。比如用户下单后超过指定时间不支付时订单自动失效
死信的三大产生
- 消息TTL过期
- 队列满了
- 消息被拒绝(basic.nack且不重新放到队列中)
延时队列
存放延迟消息的队列,延迟消息就是当消息被消费者发送后,并不想让消费者立即拿到消息,而是等待指定的时间,才让消费者消费。
RabbitMQ的延时队列需要通过设置过期时间和死信队列模拟出来。将消息加入队列并设置过期时间,时间过后就会放入死信队列,通过监听死信队列。
可以用两种方式设置消息过期时间:①设置队列属性,队列中的消息都有相同的过期时间;②设置每个消息的过期时间。
使用场景
- 订单在指定时间未支付自动销毁
- 新创建的店铺在十天内未上传商品,自动发消息提醒
- 用户注册成功后,三天没有登录就发短信提醒
- 预定会议后,在预定时间前十分钟通知大家参会
如何保证消息可靠传输
也就是防止消息丢失,可以分三个方面:生产者端、MQ本身、消费者端
生产者端
发送的消息MQ没接收到,生产者却不知道,误以为消息发送成功,导致消息丢失。在生产者端channel.confirmSelect()
开启发布确认confirm模式后,在信道设置一个回调监听函数channel.addConfirmListener(ackCallback, nackCallback)
,每个消息都会分配一个唯一的id, 这样对于每个消息,MQ有没有接收到都会告知生产者,发送失败的消息就可以重发,保证消息不丢失。有(单个、批量、异步批量)三种模式可选。这个发布确认机制是异步的,不会使发送消息阻塞。
MQ自身
使用MQ持久化,也就是将交换机、消息、队列持久化到磁盘上,这样MQ停机了也可以恢复,防止消息丢失。
交换机持久化:在生成交换机时可以通过构造方法设置交换机持久化durable
和是否自动删除;
队列持久化:也是在生成对列是通过durable
参数设置
消息持久化:这个不用配置;因为底层MessageProperties
的持久化策略默认是MessageDeliveryMode.PERSISTENT
,初始化时默认消息时持久化的。
消费者端
MQ向消费者发送消息,消费者挂掉了,但是MQ不知道,MQ会删除发出的消息,就会造成消息丢失。
消息应答就是在消费者处理消息后,告诉MQ它已经处理了,MQ就可以把该消息删除了
RabbitMQ默认使用自动应答,也就是接收到消息就会应答,如果下面出错,该消息仍被MQ删除,所以要先关闭自动应答,开启手动应答。
也就是在程序处理完成后再对MQ应答,MQ超过指定时间没有接收到这个应答或者接收到否定应答,就会让该消息重新入队。
但要防止消息重复消费
如何防止重复消费
生产者端
生产者发送消息给MQ,在MQ确认的时候出现了网络波动,生产者没有收到确认,实际上MQ已经接收到了消息。这时候生产者就会重新发送一遍这条消息。
生产者中如果消息未被确认,或确认失败,我们可以使用定时任务+(redis/db)来进行消息重试
消费者端
消息发送到消费者后,消费者确认的应答由于网络没有到MQ,MQ误认为消费者没有成功消费,将消息重新分发,造成消息重复消费。
需要保证消费者幂等性
- 数据库修改操作、redis的set操作 天然幂等
- 数据库全局唯一id
- 给消息加上全局唯一id,消费了就放到redis里,操作时看redis有没有
消息积压
大量消息积压在MQ的一个队列中,消费者需要很长时间才能完成消费
需要完成临时紧急扩容 队列 和 消费者
- 停掉现有的消费者,新建多个队列
- 写一个临时分发消息、不处理业务的程序,接收原有队列中的消息,直接轮询分发到新建的多个对列中
- 新建同等数量的消费者去处理新建队列中的请求
保证消息有序性
比如说一个生产者向对列中按顺序发送ABC三个数据,这个队列由三个消费者轮询消费,结果存入数据库顺序就不一定是ABC
拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个 consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理
事务机制
RabbitMQ 客户端中与事务机制相关的方法有三个:
channel.txSelect 用于将当前的信道设置成事务模式。
channel . txCommit 用于提交事务 。
channel . txRollback 用于事务回滚,如果在事务提交执行之前由于 RabbitMQ 异常崩溃或者其他原因抛出异常,通过txRollback来回滚
MQ集群
RabbitMQ 有三种模式:单机模式,普通集群模式,镜像集群模式。
单机模式:就是demo级别的,一般就是你本地启动了玩玩儿的,没人生产用单机模式
普通集群模式:意思就是在多台机器上启动多个RabbitMQ实例,每个机器启动一个。
镜像集群模式:这种模式,才是所谓的RabbitMQ的高可用模式,跟普通集群模式不一样的是,你创建的queue,无论元数据(元数据指RabbitMQ的配置数据)还是queue里的消息都会存在于多个实例上,然后每次你写消息到queue的时候,都会自动把消息到多个实例的queue里进行消息同步。
SpringAMQP
spring-amqp是基础抽象
spring-rabbit是RabbitMQ的实现
- 用于发布和接收消息
RabbitTemplate
- 监听器容器,用于异步处理入栈消息
RabbitAdmin
自动声明队列,交换和绑定
简单模式
发送消息
在父pom中引入依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
更改配置
spring:
rabbitmq:
host: 192.168.32.129
port: 5672
virtual-host: /
username: admin
password: '000'
注入RabbitTemplate
并发送消息
@Autowired
RabbitTemplate rabbitTemplate;
@Test
public void send() {
String queueName = "hello";
String message = "hello spring amqp";
rabbitTemplate.convertAndSend(queueName, message);
}
接收消息
消息一旦接收,就会从mq删除,RabbitMQ没有回溯功能
同样加配置
/**
* 接收消息
*/
@Component
public class SpringRabbitListener {
//指定队列名称直接可以接收
@RabbitListener(queues = "hello")
public void listenHello(String message) {
System.out.println("消费者接收到:\t" + message);
}
}
工作模式
发送
@Test
public void send2() throws InterruptedException {
String queueName = "hello";
String message = "hello spring amqp _";
for (int i = 1; i <= 50; i++) {
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
接收
默认,消息预取机制,多个消费者会先将队列中的消息分配,然后才消费,这样不能按照消费者的能力分配。所以进行配置,以便消息按能力分配
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
接收消息
@RabbitListener(queues = "hello")
public void listenHello1(String message) throws InterruptedException {
System.out.println("消费者1 接收到:\t" + message + "\t" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "hello")
public void listenHello2(String message) throws InterruptedException {
System.out.println("消费者2 接收到:\t" + message + "\t" + LocalTime.now());
Thread.sleep(200);
}
广播模式
生产者向交换机发送消息
@Test
public void send3() throws InterruptedException {
String exchangeName = "fanout1";
String message = "hello spring amqp fanout";
rabbitTemplate.convertAndSend(exchangeName, "", message);
Thread.sleep(20);
}
消费者配置绑定关系
声明交换机队列并进行绑定
/**
* 广播模式
*/
@Configuration
public class FanoutConfig {
/**
* 声明交换机
*/
@Bean
public FanoutExchange fanoutExchange() {
return new FanoutExchange("fanout1");
}
/**
* 声明队列
*/
@Bean
public Queue fanoutQueue1() {
return new Queue("fanout.queue1");
}
@Bean
public Queue fanoutQueue2() {
return new Queue("fanout.queue2");
}
/**
* 绑定队列和交换机
*/
@Bean
public Binding bindingQueue1(FanoutExchange fanoutExchange, Queue fanoutQueue1) {
return BindingBuilder.bind(fanoutQueue1).to(fanoutExchange);
}
@Bean
public Binding bindingQueue2(FanoutExchange fanoutExchange, Queue fanoutQueue2) {
return BindingBuilder.bind(fanoutQueue2).to(fanoutExchange);
}
}
消费者接收消息
//广播模式
@RabbitListener(queues = "fanout.queue1")
public void listenHello1(String message) throws InterruptedException {
System.out.println("消费者1 接收到:\t" + message + "\t" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "fanout.queue2")
public void listenHello2(String message) throws InterruptedException {
System.out.println("消费者2 接收到:\t" + message + "\t" + LocalTime.now());
Thread.sleep(200);
}
路由模式
接收消息
//路由模式,直接声明 交换机 队列 bindingkey
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue1"),
exchange = @Exchange(name = "direct1", type = ExchangeTypes.DIRECT),
key = {"red", "blue"}))
public void listenDirectQueue1(String message) {
System.out.println("消费者1接收到:\t" + message + "\t" + LocalTime.now());
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "direct.queue2"),
exchange = @Exchange(name = "direct1", type = ExchangeTypes.DIRECT),
key = {"red", "yellow"}))
public void listenDirectQueue2(String message) {
System.out.println("消费者2接收到:\t" + message + "\t" + LocalTime.now());
}
发送消息
写入bindingkey属性
/**
* 路由模式
*/
@Test
public void send4() throws InterruptedException {
String exchangeName = "direct1";
String message = "hello spring amqp direct red";
rabbitTemplate.convertAndSend(exchangeName, "red", message);
}
主题模式
接收消息
//主题模式
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue1"),
exchange = @Exchange(name = "topic1", type = ExchangeTypes.TOPIC),
key = "china.#"
))
public void listenTopicQueue1(String message) {
System.out.println("消费者1接收到:\t" + message);
}
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "topic.queue2"),
exchange = @Exchange(name = "topic1", type = ExchangeTypes.TOPIC),
key = "#.news"
))
public void listenTopicQueue2(String message) {
System.out.println("消费者2接收到:\t" + message);
}
发送消息
/**
* 主题模式
*/
@Test
public void send5() {
String exchangeName = "topic1";
String message = "hello spring amqp topic";
rabbitTemplate.convertAndSend(exchangeName, "china.news", message);
}
发送Object消息
我们可以直接发送object类型消息,spring默认使用jdk序列化方式,非常复杂而且容易注入,安全性不好。
所有使用Json方式
使用
- 在父工程pom中引入依赖
<!--jackson-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
- 接 / 发 消息的启动类中加入bean
@Bean
public MessageConverter jsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}