Java 中间件之 RabbitMQ 快速入门: SpringAMQP 消息转换器

概述

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,再顺藤摸瓜,比对着堆栈瞎猜快得多。

相关推荐
Re_zero1 小时前
一次 SSE 断流排查:PrintWriter 吞掉了我的 IOException
java·后端
我是人✓2 小时前
Java核心语法:作用域、循环与大数值处理
java·开发语言·后端
夜之眷属2 小时前
PriorityQueue:for-each 遍历和 poll 取出,顺序一样吗?
java·服务器·数据结构·后端·算法
蜗牛互联网3 小时前
Java标准HTTP调用多模态模型的票据抽取前置校验
java·人工智能·python·http
工具派3 小时前
Agent 不是玄学:用 Java 状态机思维理解 LangGraph
java·人工智能·python
十年Java程序媛3 小时前
String / StringBuilder / StringBuffer|别再乱用字符串拼接,底层原理一次讲透
java·spring boot·后端
傲世仙尊3 小时前
类和对象中-构造函数与析构函数把初始化清理自动化
java·运维·自动化
她的男孩3 小时前
权限通了、列表有数,一点分页就炸Forge Admin 用 COUNT(1) 扛住复杂待办
java·spring boot·mybatis
骑着蜗牛撵大象3273 小时前
豆包 Seed 驱动的记账智能体:从拍票抽取到月报生成的全链路落地记录
java·前端·javascript·结构化输出·豆包seed·多模态识别·记账系统