
1. 为什么要在 SpringBoot 里引入 JMSTemplate1.1 从一次消息堆积事故说起前两年我接手过一个订单履约系统上游是交易服务下游是仓储和风控。最初为了图快交易服务在下单成功后直接通过 HTTP 同步调用仓储接口扣减库存结果大促当天仓储服务响应变慢交易线程池被拖垮整个下单链路雪崩。事后复盘问题根因不是仓储服务本身而是同步调用把两个系统的可用性绑死在了一起。后来我们把扣减库存、发券、风控埋点这些非核心链路全部改成异步消息交易服务只负责把消息投递到消息中间件剩下的交给下游慢慢消费。这个改造里JMSTemplate就是我们用来发消息的核心工具。很多人一提到消息队列就想到 Kafka、RocketMQ但在企业级 Java 生态里JMSJava Message Service是一套非常经典且稳定的规范ActiveMQ、Artemis、IBM MQ 等产品都实现了这套规范。Spring 提供的JmsTemplate就是对 JMS 规范的一层轻量封装它帮你处理了连接获取、会话创建、生产者/消费者创建、异常转换、资源释放这些繁琐的样板代码。你只需要注入一个JmsTemplate调一个convertAndSend消息就出去了。1.2 JMSTemplate 到底解决了什么问题我先把结论摆在这儿JmsTemplate解决的核心问题是把 JMS 原生 API 的模板化操作抽象出来让发送和接收消息像调用一个普通方法一样简单。如果你写过原生 JMS 代码一定对下面这套流程印象深刻ConnectionFactory factory new ActiveMQConnectionFactory(tcp://localhost:61616); Connection connection factory.createConnection(); connection.start(); Session session connection.createSession(false, Session.AUTO_ACKNOWLEDGE); Destination destination session.createQueue(order.queue); MessageProducer producer session.createProducer(destination); TextMessage message session.createTextMessage(hello); producer.send(message); producer.close(); session.close(); connection.close();这段代码里真正跟业务相关的只有producer.send(message)这一行其余全是资源管理。而用JmsTemplate同样的逻辑只需要jmsTemplate.convertAndSend(order.queue, hello);连接、会话、生产者的创建和关闭Spring 全帮你做了。这就是模板方法模式的价值——把不变的流程固化把变化的部分留给你。1.3 适合哪些场景不适合哪些场景JmsTemplate不是银弹我踩过坑之后总结出它的适用边界。适合的场景包括系统间解耦下单后异步通知多个下游、削峰填谷秒杀请求先入队再慢慢消费、异步任务发邮件、生成报表、事件驱动领域事件广播。不适合的场景也很明确超高吞吐十万级 TPS 以上Kafka/RocketMQ 更合适、需要消息轨迹和事务消息RocketMQ 更强、需要复杂路由RabbitMQ 的 Exchange 更灵活。提示如果你的团队已经在用 Spring 生态且消息量在万级 TPS 以内JmsTemplate配合 ActiveMQ 或 Artemis 是完全够用的别为了追新而过度设计。2. 环境搭建与核心依赖选型2.1 依赖引入的取舍SpringBoot 整合 JMS 的依赖其实很干净。核心就两个spring-boot-starter-activemq或者spring-boot-starter-artemis和spring-boot-starter-web如果你需要 Web 层触发发送。我一般用 ActiveMQ 的经典版做演示因为它的配置最直观。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-activemq/artifactId /dependency dependency groupIdorg.apache.activemq/groupId artifactIdactivemq-pool/artifactId /dependency这里有个细节很多人忽略一定要引入连接池依赖。JmsTemplate默认每次发送都会创建连接和会话虽然 Spring 做了缓存但在高并发下没有连接池仍然会成为瓶颈。引入activemq-pool后配合spring.activemq.pool.enabledtrue性能会有明显提升。我实测过单机发送 1 万条消息开池比不开池耗时少了将近 40%。2.2 配置文件的关键参数application.yml里的配置决定了消息中间件的行为我挑几个最关键的讲spring: activemq: broker-url: tcp://127.0.0.1:61616 user: admin password: admin pool: enabled: true max-connections: 50 packages: trust-all: false trusted: com.example.dto jms: template: default-destination: order.queue receive-timeout: 5000 delivery-mode: persistentbroker-url是连接地址pool.enabled开启连接池。packages.trusted这个参数特别重要——JMS 默认不允许反序列化任意包下的对象这是为了防止反序列化漏洞。你必须显式声明哪些包下的类可以被信任否则消费端会抛ClassNotFoundException或者安全异常。我第一次遇到这个问题时排查了大半天后来才明白这是 ActiveMQ 的安全机制。delivery-mode: persistent表示消息持久化broker 重启后消息不丢。如果追求极致吞吐且能接受丢消息可以改成non-persistent性能会提升不少但生产环境我强烈建议保持持久化。2.3 队列与主题的选择逻辑JMS 有两种消息模型Queue点对点和Topic发布订阅。Queue 是一条消息只能被一个消费者消费适合任务分发Topic 是一条消息广播给所有订阅者适合事件通知。我见过不少团队无脑用 Queue结果需要广播时又加了一堆代码去模拟。选型逻辑其实很简单如果多个下游都需要收到同一条消息用 Topic如果多个消费者竞争消费同一条消息用 Queue。比如订单创建事件仓储、风控、积分都要处理那就用 Topic而订单超时关闭任务只需要一个消费者处理用 Queue 即可。3. 发送端实现与核心细节3.1 注入 JmsTemplate 并发送消息SpringBoot 自动配置会帮你创建一个JmsTemplateBean你直接注入即可。发送文本消息最简单Service public class OrderMessageProducer { Autowired private JmsTemplate jmsTemplate; public void sendOrderCreated(OrderDTO order) { jmsTemplate.convertAndSend(order.created.queue, order); } }convertAndSend会自动把对象转换成Message。默认情况下如果传入的是String会转成TextMessage如果是Serializable对象会转成ObjectMessage如果是Map会转成MapMessage。这个转换逻辑由MessageConverter决定。3.2 自定义 MessageConverter 的坑默认的SimpleMessageConverter对Serializable对象使用 Java 原生序列化这有两个问题一是性能差二是跨语言不友好。我一般会换成Jackson2JsonMessageConverter把对象转成 JSON 文本发送。Configuration public class JmsConfig { Bean public MessageConverter jacksonJmsMessageConverter() { MappingJackson2MessageConverter converter new MappingJackson2MessageConverter(); converter.setTargetType(MessageType.TEXT); converter.setTypeIdPropertyName(_type); return converter; } }这里有个关键点setTypeIdPropertyName(_type)会在消息头里写入类的全限定名消费端靠这个字段反序列化成正确的类型。如果你不设置消费端拿到 JSON 后不知道转成什么类就会报错。我踩过一次坑发送端和消费端用了不同的typeIdPropertyName结果消费端一直反序列化失败排查了很久才发现是两边配置不一致。3.3 发送时的消息头与优先级有时候你需要给消息加一些元数据比如业务追踪 ID、重试次数。JmsTemplate提供了MessagePostProcessor来在发送前修改消息jmsTemplate.convertAndSend(order.created.queue, order, message - { message.setStringProperty(traceId, MDC.get(traceId)); message.setIntProperty(retryCount, 0); message.setJMSPriority(5); return message; });setJMSPriority可以设置消息优先级0 最低9 最高。但要注意优先级只在 Queue 中生效且需要 broker 支持。ActiveMQ 默认开启优先级需要配置broker的prioritizeMessages参数否则优先级会被忽略。这个细节文档里写得不明显我是看源码才确认的。注意消息头里的属性名不要用JMS开头的保留字否则会抛JMSException。自定义属性建议加业务前缀比如biz_traceId。4. 消费端实现与并发控制4.1 使用 JmsListener 监听消息消费端最优雅的方式是用JmsListener注解Spring 会自动帮你创建消息监听容器Component public class OrderMessageConsumer { JmsListener(destination order.created.queue) public void handleOrderCreated(OrderDTO order) { // 处理业务逻辑 System.out.println(收到订单 order.getOrderId()); } }这个注解背后是DefaultMessageListenerContainer它负责管理消费者线程、连接、会话。默认情况下每个JmsListener会启动一个消费者线程。如果你的消息量很大需要提高并发度可以通过concurrency参数控制JmsListener(destination order.created.queue, concurrency 5-10) public void handleOrderCreated(OrderDTO order) { ... }5-10表示最小 5 个消费者线程最大 10 个。这个参数要根据消息量和处理耗时来调。我一般会先压测观察消费延迟再决定并发数。盲目调大并发不一定好因为每个消费者都会占用一个数据库连接并发太高反而会把数据库连接池打满。4.2 消息确认模式的选择JMS 有几种确认模式AUTO_ACKNOWLEDGE、CLIENT_ACKNOWLEDGE、DUPS_OK_ACKNOWLEDGE、SESSION_TRANSACTED。Spring 的DefaultMessageListenerContainer默认使用AUTO_ACKNOWLEDGE也就是消息监听方法正常返回后自动确认。但这里有个陷阱如果监听方法抛异常消息会重新投递。默认重投递次数是 6 次超过后消息会进入死信队列DLQ。我见过一个 bug消费端因为一个空指针异常导致消息无限重投最后把 broker 的 DLQ 撑爆了。所以消费端一定要做好异常处理对于不可恢复的异常要么捕获后记录日志并确认要么配置死信队列做兜底。JmsListener(destination order.created.queue) public void handleOrderCreated(OrderDTO order) { try { // 业务逻辑 } catch (BusinessException e) { log.error(业务异常消息丢弃, e); // 不抛出消息会被确认 } catch (Exception e) { log.error(系统异常消息重投, e); throw e; // 抛出触发重投 } }4.3 事务与消息的一致性这是最容易出问题的地方。假设消费端处理消息时要写数据库如果数据库写入成功但消息确认失败消息会重投导致重复消费。解决办法有两个幂等设计和事务消息。幂等设计是根本。我一般会在消费端加一个去重表用消息 ID 做唯一索引处理前先插入去重记录插入成功才继续处理。这样即使消息重投第二次插入会失败直接跳过。Transactional public void handleOrderCreated(OrderDTO order, String messageId) { try { dedupMapper.insert(messageId); } catch (DuplicateKeyException e) { log.warn(重复消息忽略{}, messageId); return; } // 业务处理 }至于事务消息JMS 规范本身支持SESSION_TRANSACTED但配置起来比较麻烦而且和数据库事务是两套事务无法做到真正的原子性。我的经验是优先用幂等设计事务消息只在极少数强一致场景下才考虑。5. 常见问题排查与避坑实录5.1 消息发送成功但消费端收不到这个问题我遇到过好几次排查思路可以总结成一张表排查点可能原因解决方法目的地名称发送端和消费端队列名不一致检查两边字符串是否完全一致消息类型Queue 发到了 Topic确认目的地类型匹配连接地址连到了不同的 broker检查 broker-url 配置消费端启动监听器未注册检查 JmsListener 是否生效权限用户无消费权限检查 broker 的权限配置最常见的是目的地名称不一致。有一次我发送端写的是order.created.queue消费端写的是order.create.queue少了一个d结果消息全进了 DLQ。这种低级错误排查起来最费时间建议把目的地名称抽成常量类两边引用同一个常量。5.2 消息重复消费重复消费的根因是确认机制和业务处理不在同一个事务里。消息处理成功但确认前消费者宕机消息就会重投。解决办法就是前面说的幂等设计。我补充一个细节幂等键最好用消息自带的 JMSMessageID而不是业务 ID因为同一条业务消息可能被发送多次业务 ID 相同但消息 ID 不同。JmsListener(destination order.created.queue) public void handle(OrderDTO order, Header(JmsHeaders.MESSAGE_ID) String messageId) { // 用 messageId 做幂等 }5.3 消费端线程池被打满如果消费端处理逻辑里有阻塞操作比如调用外部 HTTP 接口消费者线程会被长时间占用导致消息堆积。我的做法是把阻塞操作异步化消费端只负责把消息落库然后交给业务线程池处理。这样消费者线程能快速释放吞吐量会大幅提升。JmsListener(destination order.created.queue, concurrency 10-20) public void handle(OrderDTO order) { // 只做落库快速返回 orderMapper.insert(order); // 异步处理后续逻辑 businessExecutor.submit(() - processOrder(order)); }提示异步化之后要注意线程池的拒绝策略我一般用CallerRunsPolicy队列满了就让消费者线程自己执行起到背压作用避免任务丢失。5.4 消息体过大导致发送失败JMS 对消息大小有限制ActiveMQ 默认最大 100MB但实际使用中超过几 MB 就会明显变慢。我遇到过发送一个包含大量明细的订单对象序列化后 8MB发送耗时超过 2 秒。解决办法是消息里只放 ID消费端根据 ID 回查数据。这样消息体小传输快也避免了数据不一致。// 不推荐 jmsTemplate.convertAndSend(order.queue, hugeOrderObject); // 推荐 jmsTemplate.convertAndSend(order.queue, order.getId());6. 性能调优与生产实践6.1 连接池参数调优连接池的核心参数是max-connections和maximumActiveSessionPerConnection。前者控制到 broker 的最大连接数后者控制每个连接上最大的活跃会话数。我一般会把max-connections设为消费者并发数加发送端并发数之和再留 20% 余量。spring: activemq: pool: enabled: true max-connections: 50 maximum-active-session-per-connection: 100 idle-timeout: 30000 expiry-timeout: 0idle-timeout是空闲连接超时时间expiry-timeout是连接过期时间。生产环境我建议expiry-timeout设为 0表示永不过期避免连接频繁重建。6.2 批量发送与异步发送如果一次要发很多消息逐条发送效率很低。JmsTemplate本身不支持批量但你可以开启异步发送spring: activemq: pool: enabled: true # 开启异步发送 broker-url: tcp://127.0.0.1:61616?jms.useAsyncSendtrue异步发送的意思是生产者把消息写入本地缓冲区就返回不等 broker 确认。这样吞吐量会大幅提升但如果 broker 宕机缓冲区里的消息会丢失。所以异步发送只适合能容忍少量丢失的场景比如日志采集。6.3 监控与告警生产环境一定要监控消息队列的健康状况。我一般会关注这几个指标队列深度QueueSize、消费者数量ConsumerCount、入队速率EnqueueCount、出队速率DequeueCount。队列深度持续增长说明消费能力不足需要扩容消费者消费者数量为 0 说明消费端挂了要立即告警。ActiveMQ 提供了 JMX 接口可以通过 JMX 采集这些指标。SpringBoot 的 Actuator 也提供了 JMS 的健康检查端点配置management.endpoint.health.show-detailsalways就能看到 broker 的连接状态。7. 一些个人经验与后续扩展7.1 我踩过的三个印象最深的坑第一个坑是反序列化白名单。前面提过packages.trusted不配置消费端直接报错。这个问题的隐蔽性在于发送端一切正常只有消费端报错很容易误以为是消费端代码问题。第二个坑是消息优先级不生效。我设置了setJMSPriority(9)结果消息还是按 FIFO 顺序消费。后来查文档才知道ActiveMQ 默认不开启优先级需要在 broker 配置里加prioritizeMessagestrue。第三个坑是事务回滚导致消息重投。消费端方法上加了Transactional业务异常时数据库回滚但消息确认也一起回滚了导致消息重投。后来我把事务边界缩小到只包裹数据库操作消息确认放在事务外。7.2 后续可以怎么扩展如果你已经把JmsTemplate用起来了下一步可以考虑这几个方向消息轨迹追踪在消息头里透传 traceId配合链路追踪系统、死信队列处理把 DLQ 里的消息捞出来人工干预或自动重试、多 broker 容灾配置多个 broker-url实现故障转移。这些扩展能让你的消息系统从能用变成好用。最后分享一个小技巧在开发环境把receive-timeout设小一点比如 1000ms这样消费端启动时能快速感知到 broker 不可用而不是傻等 5 秒。生产环境再调回 5000ms避免频繁超时。这个参数虽小但对开发调试体验影响很大。