概述
RabbitTemplate.convertAndSend() 的参数一直是 Object,说明 SpringAMQP 替你做了序列化。但默认用的是 JDK 序列化,发一个 Map 进队列,管理台里会看到一长串乱码。这篇把默认行为踩一遍,再换成 JSON 转换器,并说清发送端、接收端为什么必须同时配。
纲要
- 默认行为:
SimpleMessageConverter与 JDK 序列化 - 实测:给
object.queue发一个Map,控制台看到什么 - JDK 序列化 vs JSON 的对比(体积 / 可读性 / 跨语言 / 安全)
- 链路:
MessageConverter序列化 → 消息体 →MessageConverter反序列化 - 改造:引入 Jackson 依赖、声明
@Bean MessageConverter - publisher 与 consumer 两侧的完整代码
- 三种典型报错的成因与改法
- 管理台前后对比、安全提醒、实战坑
- API 速览与官方文档
为什么 convertAndSend 能收 Object
原生 RabbitMQ Java Client 的 basicPublish 只接受 byte[],消息体在 AMQP 协议里本来就是字节数组。SpringAMQP 在中间加了一层 MessageConverter,把 Java 对象和字节之间来回转换,所以 API 看起来"什么都能发"。
链路是这样走的。
#mermaid-svg-UjJw33AT2WpXePhV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UjJw33AT2WpXePhV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UjJw33AT2WpXePhV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UjJw33AT2WpXePhV .error-icon{fill:#552222;}#mermaid-svg-UjJw33AT2WpXePhV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UjJw33AT2WpXePhV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UjJw33AT2WpXePhV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UjJw33AT2WpXePhV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UjJw33AT2WpXePhV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UjJw33AT2WpXePhV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UjJw33AT2WpXePhV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UjJw33AT2WpXePhV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UjJw33AT2WpXePhV .marker.cross{stroke:#333333;}#mermaid-svg-UjJw33AT2WpXePhV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UjJw33AT2WpXePhV p{margin:0;}#mermaid-svg-UjJw33AT2WpXePhV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UjJw33AT2WpXePhV .cluster-label text{fill:#333;}#mermaid-svg-UjJw33AT2WpXePhV .cluster-label span{color:#333;}#mermaid-svg-UjJw33AT2WpXePhV .cluster-label span p{background-color:transparent;}#mermaid-svg-UjJw33AT2WpXePhV .label text,#mermaid-svg-UjJw33AT2WpXePhV span{fill:#333;color:#333;}#mermaid-svg-UjJw33AT2WpXePhV .node rect,#mermaid-svg-UjJw33AT2WpXePhV .node circle,#mermaid-svg-UjJw33AT2WpXePhV .node ellipse,#mermaid-svg-UjJw33AT2WpXePhV .node polygon,#mermaid-svg-UjJw33AT2WpXePhV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UjJw33AT2WpXePhV .rough-node .label text,#mermaid-svg-UjJw33AT2WpXePhV .node .label text,#mermaid-svg-UjJw33AT2WpXePhV .image-shape .label,#mermaid-svg-UjJw33AT2WpXePhV .icon-shape .label{text-anchor:middle;}#mermaid-svg-UjJw33AT2WpXePhV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UjJw33AT2WpXePhV .rough-node .label,#mermaid-svg-UjJw33AT2WpXePhV .node .label,#mermaid-svg-UjJw33AT2WpXePhV .image-shape .label,#mermaid-svg-UjJw33AT2WpXePhV .icon-shape .label{text-align:center;}#mermaid-svg-UjJw33AT2WpXePhV .node.clickable{cursor:pointer;}#mermaid-svg-UjJw33AT2WpXePhV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UjJw33AT2WpXePhV .arrowheadPath{fill:#333333;}#mermaid-svg-UjJw33AT2WpXePhV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UjJw33AT2WpXePhV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UjJw33AT2WpXePhV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UjJw33AT2WpXePhV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UjJw33AT2WpXePhV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UjJw33AT2WpXePhV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UjJw33AT2WpXePhV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UjJw33AT2WpXePhV .cluster text{fill:#333;}#mermaid-svg-UjJw33AT2WpXePhV .cluster span{color:#333;}#mermaid-svg-UjJw33AT2WpXePhV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UjJw33AT2WpXePhV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UjJw33AT2WpXePhV rect.text{fill:none;stroke-width:0;}#mermaid-svg-UjJw33AT2WpXePhV .icon-shape,#mermaid-svg-UjJw33AT2WpXePhV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UjJw33AT2WpXePhV .icon-shape p,#mermaid-svg-UjJw33AT2WpXePhV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UjJw33AT2WpXePhV .icon-shape .label rect,#mermaid-svg-UjJw33AT2WpXePhV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UjJw33AT2WpXePhV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UjJw33AT2WpXePhV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UjJw33AT2WpXePhV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} publisher 业务代码
convertAndSend(queue, object)
MessageConverter
序列化 toMessage()
消息体 byte\[\]
Content-Type 标记
RabbitMQ Broker
队列中的 bytes
MessageConverter
反序列化 fromMessage()
consumer 监听方法
形参 Object / Map / POJO
两端各自持有一个 MessageConverter,发送用它的 toMessage(),接收用它的 fromMessage()。它们互相看不见对方,只通过消息体里的 Content-Type 和字节内容协商。这一点决定了后面那条最容易踩的坑:两边必须配同一种转换器。
默认实现是 SimpleMessageConverter,底层就是 JDK 的 ObjectOutputStream。SpringBoot 的自动配置在没有自定义 MessageConverter Bean 时会装上它,属于"自动装配的默认值"。
实测默认转换器
先弄清默认行为长什么样。在 consumer 的配置类里声明一个独立队列,避免和之前的 simple.queue 混在一起。之所以不写 @RabbitListener 注解声明,是因为注解方式在队列创建后消息会被立刻消费掉,看不到消息体。
java
package cn.itcast.mq.config;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FanoutConfig {
@Bean
public Queue objectQueue() {
return new Queue("object.queue");
}
}
重启 consumer,队列就声明出来了。接着在 publisher 的测试类里发一个 Map。
java
package cn.itcast.mq.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.HashMap;
import java.util.Map;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendObjectQueue() {
Map<String, Object> msg = new HashMap<>();
msg.put("name", "柳岩");
msg.put("age", 21);
// 直接发 Map,转换器在底层完成序列化,业务代码不用管
rabbitTemplate.convertAndSend("object.queue", msg);
}
}
代码跑绿了,消息也进了队列。但到 RabbitMQ 管理台点开 Get Message,看到的不是 {"name":"柳岩","age":21},而是一堆以 rO0AB... 开头的乱码,Content-Type 显示为 application/x-java-serialized-object。
text
rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhw
PAAAAAAAAA8IAAAAAHNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2
YS5sYW5nLk51bWJlciThFjZ+g3cOAgAAeHAAAAAVdAAB5p-z5aa7dAAD5YWz...
JDK 序列化的问题
这不是 Spring 的怪癖,是 JDK 序列化本身的问题。
- 体积大。
HashMap要带上全限定类名、字段描述符、serialVersionUID,内容还没几条元数据先占一大块。 - 可读性为零。非 Java 语言(Go、Python、Node)拿到这串字节没法解析,跨语言服务之间直接卡死。
- 版本敏感。发送方和接收方的类名、字段结构、
serialVersionUID任何一项不一致,反序列化就抛InvalidClassException。 - 有安全史。JDK 反序列化可以触发 gadget 链,历史 CVE 不少,生产环境不建议把序列化字节当可信数据。
对比一下两种方案。
| 维度 | JDK 序列化(SimpleMessageConverter) | JSON(Jackson2JsonMessageConverter) |
|---|---|---|
| 消息体 | 二进制,rO0AB... |
纯文本,{"name":"柳岩","age":21} |
| 可读性 | 无 | 高,管理台直接看懂 |
| 体积 | 大(含类元信息) | 小 |
| 跨语言 | 只能 Java 反序列化 | 任意语言可解析 |
| 版本兼容 | 类结构变化即失败 | 字段增减容忍度高 |
| 安全 | 反序列化 gadget 风险 | 需避免使用未受限的多态类型 |
| Content-Type | application/x-java-serialized-object |
application/json |
换成 Jackson2JsonMessageConverter
改造分两步:引依赖 + 声明 Bean。
依赖要放到父工程 mq-demo/pom.xml 的 <dependencies> 里,publisher 和 consumer 是两个独立微服务,各自打包运行,谁都躲不开序列化/反序列化,所以依赖必须两端都有。
xml
<!-- Jackson 的 JSON 处理核心库 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
讲义里给的是 com.fasterxml.jackson.dataformat:jackson-dataformat-xml:2.9.10,实际工程用的是 jackson-databind,二者都会把 Jackson 核心带进来。用 spring-boot-starter-parent 管理版本时不必写 <version>,交给 SpringBoot 的依赖管理即可。
第二步,声明 MessageConverter Bean。SpringBoot 的自动配置是条件装配,一旦容器里有了自定义的 MessageConverter,默认的 SimpleMessageConverter 就不再注册,新 Bean 直接接管 RabbitTemplate 和监听容器。
项目结构如下,类很多但只需要动两个启动类。
tree
mq-demo
├── pom.xml # 父工程,声明 spring-boot-starter-amqp / jackson / lombok
├── publisher
│ ├── pom.xml
│ └── src
│ ├── main
│ │ ├── java/cn/itcast/mq
│ │ │ └── PublisherApplication.java # 声明 MessageConverter Bean
│ │ └── resources
│ │ └── application.yml # RabbitMQ 连接配置
│ └── test/java/cn/itcast/mq
│ ├── helloworld/PublisherTest.java
│ └── spring/SpringAmqpTest.java # 发送测试
└── consumer
├── pom.xml
└── src
├── main
│ ├── java/cn/itcast/mq
│ │ ├── ConsumerApplication.java # 同样声明 MessageConverter Bean
│ │ ├── config/FanoutConfig.java # 声明 object.queue 等
│ │ └── listener/SpringRabbitListener.java # 监听并接收 Map
│ └── resources
│ └── application.yml
└── test/java/cn/itcast/mq
└── helloworld/ConsumerTest.java
publisher 侧配置。
java
package cn.itcast.mq;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class PublisherApplication {
public static void main(String[] args) {
SpringApplication.run(PublisherApplication.class);
}
// 覆盖默认的 SimpleMessageConverter,改用 JSON 序列化
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
consumer 侧配置,几乎一字不差。
java
package cn.itcast.mq;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
// 与 publisher 保持一致:发送端序列化,接收端反序列化
@Bean
public MessageConverter messageConverter() {
return new Jackson2JsonMessageConverter();
}
}
注意 import 的是 org.springframework.amqp.support.converter.MessageConverter,不是 org.springframework.messaging.converter.MessageConverter,同名接口在 Spring 里有好几个,导错编译期不报错但装配不上。
为什么两边都要配
这是本篇最核心的一条。转换器不是"客户端设置",而是两端各自独立的编解码器。
- publisher 的转换器决定
toMessage()怎么写字节,Jackson2JsonMessageConverter会写入Content-Type: application/json。 - consumer 的转换器决定
fromMessage()怎么读字节。
只要有一侧还是默认的 SimpleMessageConverter,就会出现非对称:
- 发送端 JSON、接收端默认:接收端拿到 JSON 文本,
SimpleMessageConverter发现当前消息不是Serializable,尝试把字符串塞进Map形参,抛转换异常或直接得到乱码字符串。 - 发送端默认、接收端 JSON:接收端拿到
rO0AB...的 JDK 序列化流,Jackson 按 JSON 解析直接失败。
所以"改一半"的直觉是错的,两边都得改。
改造后代码不用动。convertAndSend 只管把对象交出去,序列化发生在底层。发完消息再回管理台,Content-Type 变成 application/json,消息体直接可读。
text
{"name":"柳岩","age":21}
看到这个,说明序列化已经生效。
接收自定义对象
监听方法把形参从 String 换成接收类型即可,SpringAMQP 会用 fromMessage() 把 JSON 还原成对应的 Java 类型。
java
package cn.itcast.mq.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "object.queue")
public void listenObjectQueue(Map<String, Object> msg) {
System.out.println("接收到object.queue的消息:" + msg);
}
}
发送方发的 Map、接收方接的 Map,类型对称,能正常还原并打印出 {name=柳岩, age=21}。换成 POJO 同理,只要字段能对上 JSON 键名。
用 POJO 时字段命名有个常见分歧。Jackson 默认按 getter 推断属性名,userName 字段会序列化成 userName;如果对端期望下划线风格,需要加 @JsonProperty("user_name") 显式指定,别指望两边能猜一致。
三类报错与排查
SimpleMessageConverter only supports String, byte\[\] and Serializable payloads
完整的异常信息通常是这样。
text
org.springframework.amqp.AmqpException: No method found for class ...
Caused by: org.springframework.amqp.support.converter.MessageConversionException:
SimpleMessageConverter only supports String, byte[] and Serializable payloads, received: cn.itcast.mq.pojo.User
原因:发送对象既不是 String、也不是 byte[],还没实现 java.io.Serializable,默认转换器没有处理它的能力。
改法:要么给 POJO 加 implements Serializable,要么(推荐)换成 Jackson2JsonMessageConverter。JSON 方案不要求实现 Serializable,这也是它比 JDK 序列化更省心的一个点。
ClassNotFoundException
text
Caused by: java.lang.ClassNotFoundException: cn.itcast.mq.pojo.User
原因:拿到的是 application/x-java-serialized-object 字节流,JDK 反序列化时要按字节流里记录的全限定类名去 Class.forName() 找类,接收端本地没有这个类。常见于两端分别在两个微服务里,DTO 没有共享。
改法:两端使用同一份 DTO(抽到公共模块),或者改走 JSON------JSON 是文本,反序列化到哪个类由接收端方法形参决定,不依赖发送端的类是否可见。
java.io.InvalidClassException
text
Caused by: java.io.InvalidClassException: cn.itcast.mq.pojo.User;
local class incompatible: stream classdesc serialVersionUID = -1234567890, local class serialVersionUID = 1234567890
原因:类名一样,但 serialVersionUID 对不上。JDK 序列化会校验这个值,只要发送端类改过(加了字段、IDE 自动生成的 UID 变了),对端就拒绝还原。
改法:手动声明固定的 serialVersionUID,或者改用 JSON。字段增删这种日常操作,JSON 兼容性友好得多。
java
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 手动固定,避免 IDE 生成的默认值随类结构变化
private String name;
private Integer age;
}
管理台验证
清掉队列里的旧消息(管理台队列详情页有 Purge Messages),重启 consumer,重新发一次,对比前后。
改造前,JDK 序列化看到的样式。
text
Content-Type: application/x-java-serialized-object
Payload:
rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhw
PAAAAAAAAA8IAAAAAHNyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2
改造后,JSON 序列化看到的样式。
text
Content-Type: application/json
Payload:
{"name":"柳岩","age":21}
同一份业务代码,消息体从几百字节压到二十几字节,传输开销和排查成本都下来了。
安全提醒
Jackson 有个历史悠久的选项 ObjectMapper.enableDefaultTyping(),开启后序列化会额外写入 @class 之类的类型信息,反序列化时按这个类型信息去实例化任意类。这正是历史上的反序列化漏洞来源------攻击者可以伪造消息体,诱导服务端实例化 gadget 类完成利用链。
Jackson2JsonMessageConverter 默认不开启这个开关,正常用是安全的。要守住的边界只有一条:不要为了一时方便打开默认类型信息,也不要让 JSON 映射到不受限的任意类型。反序列化的目标类型应当由监听方法形参或显式 DTO 固定下来,而不是来自消息内容。
实战坑
只配了一边。 最典型。publisher 配了、consumer 忘了,或者反过来。表现是能发出消息、管理台也正常,但消费端报解析异常。排查手段是看消息体的 Content-Type:一端是 application/json、另一端还是 application/x-java-serialized-object,说明哪边漏了。
Bean 名字与默认冲突。 自定义 Bean 的方法名不必和默认的 messageConverter 一样,Spring 按类型装配。但如果你自己在 RabbitTemplate 上再调一次 setMessageConverter(...),手写设置会覆盖自动装配进来的那个,两处不一致时以手写为准,容易成为隐藏的不一致源。统一走 Bean 方式即可。
POJO 缺无参构造。 Jackson 反序列化有时需要先 new 出空对象再填字段,只有带参构造的 POJO 会抛 InvalidDefinitionException: cannot construct instance。加无参构造,或标 @JsonCreator / @JsonProperty 指定构造参数。用 Lombok 的 @Data 时注意 @AllArgsConstructor 会消掉默认无参构造,需要 @NoArgsConstructor 补回来。
时间类型格式不对。 LocalDateTime 这类 JSR-310 类型,Jackson 默认按数组或 ISO 字符串输出,Java 8 环境需要引入 jackson-datatype-jsr310 模块,并注册 JavaTimeModule。用 SpringBoot 自动装配的 ObjectMapper 通常已经注册,但自己 new ObjectMapper() 时要手动加。
java
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); // 输出 2026-09-30T10:00:00 而不是时间戳数组
API 速览
| 类 / 方法 | 作用 |
|---|---|
MessageConverter |
消息体与 Java 对象互转的顶层接口(org.springframework.amqp.support.converter) |
SimpleMessageConverter |
默认实现,底层 JDK 序列化,只支持 String / byte[] / Serializable |
Jackson2JsonMessageConverter |
JSON 实现,消息体可读、体积小、跨语言 |
MessageConverter.toMessage(Object, MessageProperties) |
发送端序列化 |
MessageConverter.fromMessage(Message) |
接收端反序列化 |
RabbitTemplate.setMessageConverter(MessageConverter) |
手动为模板设置转换器 |
Jackson2JsonMessageConverter.setClassMapper(ClassMapper) |
自定义类型映射规则,控制反序列化目标类型 |
官方文档
总结
convertAndSend 收 Object 不是魔法,是 MessageConverter 在底下做转换。默认的 SimpleMessageConverter 走 JDK 序列化,体积大、不可读、跨语言不通、还能踩安全坑,生产上基本不用。
换成 Jackson2JsonMessageConverter 只需一个 Bean,代码零改动。关键是记住它是两端各自的编解码器------publisher 负责序列化、consumer 负责反序列化,只配一边必定有一方解析失败,这是这段内容里最值得记住的一条。
三类报错都指向同一个根源:序列化协议不匹配,或者接收端缺少还原目标类型所需的上下文。排查时先看管理台的 Content-Type,再顺藤摸瓜,比对着堆栈瞎猜快得多。