java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot接入阿里云RocketMQ

SpringBoot接入阿里云RocketMQ的完整指南

作者:霸道流氓气质

本文介绍了SpringBoot框架及其简化开发的特点,以及RocketMQ的分布式消息中间件功能,重点讲述了如何在SpringBoot项目中集成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 中创建,建议用子账号,最小权限原则。

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

2.1 创建实例

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

2.2 创建 Topic

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

2.3 创建 GroupId

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

2.4 获取 AccessKey

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

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

3.1 数组格式

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}

特点

3.2 Map 格式

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}

特点

3.3 关键区别

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

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

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

consumers:
  - topic: topicA|topicB|topicC
    groupId: GID-ALL-IN-ONE

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

consumers:
  check:
    topic: topicA
    groupId: GID-CHECK
  save:
    topic: topicB
    groupId: GID-SAVE
  export:
    topic: topicC
    groupId: GID-EXPORT

四、完整代码示例

4.1 依赖

<dependency>
    <groupId>com.aliyun.openservices</groupId>
    <artifactId>ons-client</artifactId>
    <version>1.8.8.1.Final</version>
</dependency>

4.2 配置属性类

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 配置

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 格式多消费者)

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 接口

package com.example.mq.listener;

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

4.6 具体 Listener 实现

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 发送工具

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 业务处理器(路由目标)

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 topicTopic 未创建/endpoint 不匹配确认阿里云上 Topic 存在且 instanceId 正确
subscription not consistent同一 GroupId 的多个实例订阅了不同 Topic确保所有实例代码一致,或使用独立 GroupId
消费者零消费Consumer 未注册成功检查启动日志中是否有"订阅成功",确认 GroupId 已创建
AUTH_FAILEDAK/SK 错误或无权限确认 RAM 用户有 MQ 权限
@Value 注入失败yml 格式和代码路径不匹配数组用 [0],Map 用 .key

以上就是SpringBoot接入阿里云RocketMQ的完整指南的详细内容,更多关于SpringBoot接入阿里云RocketMQ的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文