Spring Boot 接入阿里云 RocketMQ 完整指南

Spring Boot 接入阿里云 RocketMQ 完整指南

一、核心概念

1.1 实例(Instance)

消息服务的顶层容器,按环境隔离。ID 格式:MQ_INST_<账号ID>_<标识>

复制代码
阿里云账号
  ├── 实例-dev (MQ_INST_xxx_BXsxZrYe)  → 开发环境
  └── 实例-prod (MQ_INST_xxx_BX3z6RD9) → 生产环境

1.2 Topic

消息通道,Producer 发到 Topic,Consumer 从 Topic 消费。

1.3 GroupId

消费者组标识。同组内多实例竞争消费(一条消息只被一个实例消费)。

阿里云硬性要求 :同一 GroupId 下所有消费者必须订阅完全相同 的 Topic 集合,否则报 subscription not consistent

1.4 Tag

消息二级标签,Consumer 可按 Tag 过滤。

1.5 Endpoint

接入地址。本地开发用公网,服务器部署用内网:

类型 格式 场景
公网 http://MQ_INST_xxx.mq-internet-access.mq-internet.aliyuncs.com:80 本地开发
内网 http://MQ_INST_xxx.cn-xxxx.mq-internal.aliyuncs.com:8080 同地域ECS

1.6 AccessKey / SecretKey

身份认证凭证。在阿里云 RAM 中创建,建议用子账号,最小权限原则。


注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、阿里云控制台操作步骤

2.1 创建实例

  1. 登录阿里云 → 搜索"消息队列 RocketMQ" → 创建实例
  2. 选择地域(如华北1青岛)、规格
  3. 获取实例 ID 和 Endpoint

2.2 创建 Topic

实例详情 → Topic 管理 → 创建 Topic:

  • 名称:my-app-import-check-dev
  • 消息类型:普通消息

2.3 创建 GroupId

实例详情 → Group 管理 → 创建 Group:

  • 名称必须以 GID-GID_ 开头
  • GID-MY-APP-IMPORT-CHECK

2.4 获取 AccessKey

右上角头像 → AccessKey 管理 → 创建 RAM 子账号 → 授权 AliyunMQFullAccess


三、YAML 配置:数组格式 vs Map 格式

3.1 数组格式

yaml 复制代码
rocketmq:
  consumers:
    - topic: my-app-import-check-dev
      groupId: GID-MY-APP-CHECK
      tag: app-tag
    - topic: my-app-import-save-dev
      groupId: GID-MY-APP-SAVE
      tag: app-tag

代码引用:${rocketmq.consumers[0].tag}${rocketmq.consumers[1].tag}

特点

  • Java 中映射为 List<ConsumerProperties>
  • 适合程序动态遍历创建消费者
  • 顺序依赖([0]/[1] 必须和配置顺序一致)

3.2 Map 格式

yaml 复制代码
rocketmq:
  consumers:
    import-check:
      topic: my-app-import-check-dev
      groupId: GID-MY-APP-CHECK
      tag: app-tag
    import-save:
      topic: my-app-import-save-dev
      groupId: GID-MY-APP-SAVE
      tag: app-tag

代码引用:${rocketmq.consumers.import-check.tag}

特点

  • Java 中映射为 Map<String, ConsumerProperties>
  • 按名称引用,不依赖顺序
  • 语义更清晰

3.3 关键区别

维度 数组格式 Map 格式
引用方式 [0][1](位置) .name(名称)
可读性 低(依赖顺序) 高(见名知义)
扩展性 插入/删除影响索引 独立不互相影响
框架注册 遍历 List 逐个创建 遍历 Map.entrySet 逐个创建

3.4 一个数组元素订阅多 Topic vs 多个独立 GroupId

方式A:一个消费者订阅多 Topic

yaml 复制代码
consumers:
  - topic: topicA|topicB|topicC
    groupId: GID-ALL-IN-ONE
  • 优点:配置简单,只需一个 GroupId
  • 缺点:无法对不同 Topic 设置不同的并发度/重试策略

方式B:多个消费者各自独立

yaml 复制代码
consumers:
  check:
    topic: topicA
    groupId: GID-CHECK
  save:
    topic: topicB
    groupId: GID-SAVE
  export:
    topic: topicC
    groupId: GID-EXPORT
  • 优点:各自独立控制,不会互相影响
  • 缺点:需要创建多个 GroupId

四、完整代码示例

4.1 依赖

xml 复制代码
<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>ons-client</artifactId>
    <version>1.8.8.1.Final</version>
</dependency>

4.2 配置属性类

java 复制代码
package com.example.mq.config;

import java.util.Map;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "rocketmq")
public class RocketMqProperties {

  private ProducerConfig producer;
  private Map<String, ConsumerConfig> consumers;

  @Data
  public static class ProducerConfig {
    private String accessKey;
    private String secretKey;
    private String endpoint;
    private String tag;
    private String instanceId;
  }

  @Data
  public static class ConsumerConfig {
    private String accessKey;
    private String secretKey;
    private String endpoint;
    private String tag;
    private String topic;
    private String groupId;
    private String protocol;
    private String instanceId;
  }
}

4.3 Producer 配置

java 复制代码
package com.example.mq.config;

import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.Producer;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import java.util.Properties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ProducerConfiguration {

  @Bean(initMethod = "start", destroyMethod = "shutdown")
  public Producer producer(RocketMqProperties config) {
    Properties properties = new Properties();
    properties.put(PropertyKeyConst.AccessKey, config.getProducer().getAccessKey());
    properties.put(PropertyKeyConst.SecretKey, config.getProducer().getSecretKey());
    properties.put(PropertyKeyConst.NAMESRV_ADDR, config.getProducer().getEndpoint());
    if (config.getProducer().getInstanceId() != null) {
      properties.put(PropertyKeyConst.INSTANCE_ID, config.getProducer().getInstanceId());
    }
    return ONSFactory.createProducer(properties);
  }
}

4.4 Consumer 自动注册(核心------支持 Map 格式多消费者)

java 复制代码
package com.example.mq.config;

import com.aliyun.openservices.ons.api.Consumer;
import com.aliyun.openservices.ons.api.MessageListener;
import com.aliyun.openservices.ons.api.ONSFactory;
import com.aliyun.openservices.ons.api.PropertyKeyConst;
import com.example.mq.listener.MqListener;
import jakarta.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import jakarta.annotation.PostConstruct;

@Slf4j
@Configuration
public class ConsumerConfiguration {

  @Autowired
  private RocketMqProperties config;

  @Autowired
  private List<MqListener> listeners;

  private List<Consumer> consumers = new ArrayList<>();

  @PostConstruct
  public void init() {
    if (config.getConsumers() == null) return;

    // 遍历 Map 格式的消费者配置
    for (Map.Entry<String, RocketMqProperties.ConsumerConfig> entry
        : config.getConsumers().entrySet()) {

      String name = entry.getKey();
      RocketMqProperties.ConsumerConfig cc = entry.getValue();

      Properties properties = new Properties();
      properties.put(PropertyKeyConst.AccessKey, cc.getAccessKey());
      properties.put(PropertyKeyConst.SecretKey, cc.getSecretKey());
      properties.put(PropertyKeyConst.NAMESRV_ADDR, cc.getEndpoint());
      properties.put(PropertyKeyConst.GROUP_ID, cc.getGroupId());
      if (cc.getInstanceId() != null) {
        properties.put(PropertyKeyConst.INSTANCE_ID, cc.getInstanceId());
      }

      Consumer consumer = ONSFactory.createConsumer(properties);

      // 订阅 Topic(支持 | 分隔多个)
      String[] topics = cc.getTopic().split("\\|");
      for (String topic : topics) {
        // 找到负责这个 Topic 的 Listener
        MqListener listener = findListener(topic.trim());
        if (listener != null) {
          consumer.subscribe(topic.trim(), cc.getTag(), (message, context) -> {
            try {
              listener.consume(message.getBody());
              return com.aliyun.openservices.ons.api.Action.CommitMessage;
            } catch (Exception e) {
              log.error("消费异常, topic={}, msgId={}", topic, message.getMsgID(), e);
              return com.aliyun.openservices.ons.api.Action.ReconsumeLater;
            }
          });
          log.info("消费者[{}]订阅 topic={}, tag={}, groupId={}",
              name, topic.trim(), cc.getTag(), cc.getGroupId());
        }
      }

      consumer.start();
      consumers.add(consumer);
      log.info("消费者[{}]启动成功", name);
    }
  }

  @PreDestroy
  public void destroy() {
    for (Consumer consumer : consumers) {
      consumer.shutdown();
    }
  }

  private MqListener findListener(String topic) {
    for (MqListener listener : listeners) {
      if (topic.equals(listener.topic())) {
        return listener;
      }
    }
    return null;
  }
}

4.5 Listener 接口

java 复制代码
package com.example.mq.listener;

/**
 * 消息监听器接口.
 * 每个业务实现此接口,通过 topic() 方法告诉框架自己负责哪个 Topic.
 */
public interface MqListener {
  /** 消费消息. */
  void consume(byte[] body);
  /** 返回负责的 Topic 名称. */
  String topic();
}

4.6 具体 Listener 实现

java 复制代码
package com.example.mq.listener;

import com.example.entity.TaskMessage;
import com.example.handler.TaskHandler;
import com.example.handler.TaskHandlerRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ImportCheckListener implements MqListener {

  @Autowired
  private TaskHandlerRegistry registry;

  @Value("${xxx.mq.import.check}")
  private String topic;

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public void consume(byte[] body) {
    try {
      TaskMessage task = objectMapper.readValue(body, TaskMessage.class);
      log.info("校验消费开始, bizType={}, id={}", task.getBizType(), task.getId());
      TaskHandler handler = registry.getHandler(task.getBizType());
      handler.check(task);
    } catch (Exception e) {
      throw new RuntimeException("校验消费异常", e);
    }
  }

  @Override
  public String topic() {
    return topic;
  }
}
package com.example.mq.listener;

import com.example.entity.TaskMessage;
import com.example.handler.TaskHandlerRegistry;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class ImportSaveListener implements MqListener {

  @Autowired
  private TaskHandlerRegistry registry;

  @Value("${xxx.mq.import.save}")
  private String topic;

  private final ObjectMapper objectMapper = new ObjectMapper();

  @Override
  public void consume(byte[] body) {
    try {
      TaskMessage task = objectMapper.readValue(body, TaskMessage.class);
      log.info("导入消费开始, bizType={}, id={}", task.getBizType(), task.getId());
      registry.getHandler(task.getBizType()).save(task);
    } catch (Exception e) {
      throw new RuntimeException("导入消费异常", e);
    }
  }

  @Override
  public String topic() {
    return topic;
  }
}

4.7 Producer 发送工具

java 复制代码
package com.example.mq;

import com.aliyun.openservices.ons.api.Message;
import com.aliyun.openservices.ons.api.Producer;
import com.example.entity.TaskMessage;
import com.example.mq.config.RocketMqProperties;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class TaskMqProducer {

  @Autowired
  private Producer producer;

  @Autowired
  private RocketMqProperties config;

  @Value("${xxx.mq.import.check}")
  private String checkTopic;

  @Value("${xxx.mq.import.save}")
  private String saveTopic;

  private final ObjectMapper objectMapper = new ObjectMapper();

  public void sendCheck(TaskMessage task) {
    send(checkTopic, task);
  }

  public void sendConfirm(TaskMessage task) {
    send(saveTopic, task);
  }

  private void send(String topic, TaskMessage task) {
    try {
      byte[] body = objectMapper.writeValueAsBytes(task);
      Message message = new Message(topic, config.getProducer().getTag(), body);
      message.setKey(String.valueOf(task.getId()));
      producer.send(message);
      log.info("MQ发送成功, topic={}, id={}", topic, task.getId());
    } catch (Exception e) {
      throw new RuntimeException("MQ发送失败", e);
    }
  }
}

4.8 业务处理器(路由目标)

java 复制代码
package com.example.handler;

import com.example.entity.TaskMessage;

public interface TaskHandler {
  String getBizType();
  void check(TaskMessage task);
  void save(TaskMessage task);
}
package com.example.handler;

import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class TaskHandlerRegistry {
  @Autowired
  private List<TaskHandler> allHandlers;
  private Map<String, TaskHandler> map = new HashMap<>();

  @PostConstruct
  public void init() {
    for (TaskHandler h : allHandlers) {
      map.put(h.getBizType(), h);
    }
  }

  public TaskHandler getHandler(String bizType) {
    TaskHandler h = map.get(bizType);
    if (h == null) throw new RuntimeException("未找到处理器: " + bizType);
    return h;
  }
}
package com.example.handler.impl;

import com.example.entity.TaskMessage;
import com.example.handler.TaskHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class OrderHandler implements TaskHandler {
  @Override
  public String getBizType() { return "ORDER"; }

  @Override
  public void check(TaskMessage task) {
    log.info("订单校验, id={}", task.getId());
  }

  @Override
  public void save(TaskMessage task) {
    log.info("订单入库, id={}", task.getId());
  }
}

五、常见问题排查

错误 原因 解决
No route info of this topic Topic 未创建/endpoint 不匹配 确认阿里云上 Topic 存在且 instanceId 正确
subscription not consistent 同一 GroupId 的多个实例订阅了不同 Topic 确保所有实例代码一致,或使用独立 GroupId
消费者零消费 Consumer 未注册成功 检查启动日志中是否有"订阅成功",确认 GroupId 已创建
AUTH_FAILED AK/SK 错误或无权限 确认 RAM 用户有 MQ 权限
@Value 注入失败 yml 格式和代码路径不匹配 数组用 [0],Map 用 .key
相关推荐
卓怡学长1 小时前
w272基于springboot便民医疗服务小程序
java·spring boot·spring·小程序·intellij-idea
豆瓣鸡2 小时前
Spring Boot 全局异常处理与参数校验
spring boot·后端
Arya_aa2 小时前
植物病虫害识别系统–––登录模块
spring boot
万亿少女的梦1682 小时前
基于Spring Boot与Vue的繁星技术论坛系统设计与实现
java·spring boot·mysql·vue·系统设计
万亿少女的梦16821 小时前
基于SpringBoot的考研学习交流系统设计与开发
spring boot·mysql·vue·系统设计·论坛系统
二宝哥1 天前
创建Gradle多模块SpringBoot项目
java·spring boot·后端
杨运交1 天前
[044][Web模块]基于 Google Authenticator 的 TOTP 双因素认证框架设计与实现
spring boot
二宝哥1 天前
springboot项目使用Gradle工具实现依赖版本控制
java·spring boot·后端·gradle·版本