Commit a2315826370546744354f3505d0f376d35dc0a34

Authored by YunaiV
1 parent d29b0beb

增强 mq starter 组件,支持 interceptor 拦截器机制

yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java
... ... @@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.mq.config;
2 2  
3 3 import cn.hutool.system.SystemUtil;
4 4 import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
  5 +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
5 6 import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
6 7 import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
7 8 import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
... ... @@ -9,7 +10,6 @@ import lombok.extern.slf4j.Slf4j;
9 10 import org.springframework.boot.autoconfigure.AutoConfigureAfter;
10 11 import org.springframework.context.annotation.Bean;
11 12 import org.springframework.context.annotation.Configuration;
12   -import org.springframework.data.redis.connection.RedisConnectionFactory;
13 13 import org.springframework.data.redis.connection.stream.Consumer;
14 14 import org.springframework.data.redis.connection.stream.ObjectRecord;
15 15 import org.springframework.data.redis.connection.stream.ReadOffset;
... ... @@ -33,8 +33,12 @@ import java.util.List;
33 33 public class YudaoMQAutoConfiguration {
34 34  
35 35 @Bean
36   - public RedisMQTemplate redisMQTemplate(StringRedisTemplate stringRedisTemplate) {
37   - return new RedisMQTemplate(stringRedisTemplate);
  36 + public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
  37 + List<RedisMessageInterceptor> interceptors) {
  38 + RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
  39 + // 添加拦截器
  40 + interceptors.forEach(redisMQTemplate::addInterceptor);
  41 + return redisMQTemplate;
38 42 }
39 43  
40 44 // ========== 消费者相关 ==========
... ... @@ -44,13 +48,14 @@ public class YudaoMQAutoConfiguration {
44 48 */
45 49 @Bean
46 50 public RedisMessageListenerContainer redisMessageListenerContainer(
47   - RedisConnectionFactory factory, List<AbstractChannelMessageListener<?>> listeners) {
  51 + RedisMQTemplate redisMQTemplate, List<AbstractChannelMessageListener<?>> listeners) {
48 52 // 创建 RedisMessageListenerContainer 对象
49 53 RedisMessageListenerContainer container = new RedisMessageListenerContainer();
50 54 // 设置 RedisConnection 工厂。
51   - container.setConnectionFactory(factory);
  55 + container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory());
52 56 // 添加监听器
53 57 listeners.forEach(listener -> {
  58 + listener.setRedisMQTemplate(redisMQTemplate);
54 59 container.addMessageListener(listener, new ChannelTopic(listener.getChannel()));
55 60 log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]",
56 61 listener.getChannel(), listener.getClass().getName());
... ... @@ -65,7 +70,8 @@ public class YudaoMQAutoConfiguration {
65 70 */
66 71 @Bean(initMethod = "start", destroyMethod = "stop")
67 72 public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
68   - RedisTemplate<String, Object> redisTemplate, List<AbstractStreamMessageListener<?>> listeners) {
  73 + RedisMQTemplate redisMQTemplate, List<AbstractStreamMessageListener<?>> listeners) {
  74 + RedisTemplate<String, ?> redisTemplate = redisMQTemplate.getRedisTemplate();
69 75 // 第一步,创建 StreamMessageListenerContainer 容器
70 76 // 创建 options 配置
71 77 StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
... ... @@ -74,8 +80,8 @@ public class YudaoMQAutoConfiguration {
74 80 .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
75 81 .build();
76 82 // 创建 container 对象
77   - StreamMessageListenerContainer<String, ObjectRecord<String, String>> container = StreamMessageListenerContainer.create(
78   - redisTemplate.getRequiredConnectionFactory(), containerOptions);
  83 + StreamMessageListenerContainer<String, ObjectRecord<String, String>> container =
  84 + StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions);
79 85  
80 86 // 第二步,注册监听器,消费对应的 Stream 主题
81 87 String consumerName = buildConsumerName();
... ... @@ -85,7 +91,7 @@ public class YudaoMQAutoConfiguration {
85 91 redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
86 92 } catch (Exception ignore) {}
87 93 // 设置 listener 对应的 redisTemplate
88   - listener.setRedisTemplate(redisTemplate);
  94 + listener.setRedisMQTemplate(redisMQTemplate);
89 95 // 创建 Consumer 对象
90 96 Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
91 97 // 设置 Consumer 消费进度,以最小消费进度为准
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/RedisMQTemplate.java
1 1 package cn.iocoder.yudao.framework.mq.core;
2 2  
3 3 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  4 +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
  5 +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
4 6 import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
5 7 import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage;
6 8 import lombok.AllArgsConstructor;
  9 +import lombok.Getter;
7 10 import org.springframework.data.redis.connection.stream.RecordId;
8 11 import org.springframework.data.redis.connection.stream.StreamRecords;
9 12 import org.springframework.data.redis.core.RedisTemplate;
10 13  
  14 +import java.util.ArrayList;
  15 +import java.util.List;
  16 +
11 17 /**
12 18 * Redis MQ 操作模板类
13 19 *
... ... @@ -16,7 +22,13 @@ import org.springframework.data.redis.core.RedisTemplate;
16 22 @AllArgsConstructor
17 23 public class RedisMQTemplate {
18 24  
  25 + @Getter
19 26 private final RedisTemplate<String, ?> redisTemplate;
  27 + /**
  28 + * 拦截器数组
  29 + */
  30 + @Getter
  31 + private final List<RedisMessageInterceptor> interceptors = new ArrayList<>();
20 32  
21 33 /**
22 34 * 发送 Redis 消息,基于 Redis pub/sub 实现
... ... @@ -24,7 +36,13 @@ public class RedisMQTemplate {
24 36 * @param message 消息
25 37 */
26 38 public <T extends AbstractChannelMessage> void send(T message) {
27   - redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
  39 + try {
  40 + sendMessageBefore(message);
  41 + // 发送消息
  42 + redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
  43 + } finally {
  44 + sendMessageAfter(message);
  45 + }
28 46 }
29 47  
30 48 /**
... ... @@ -34,9 +52,36 @@ public class RedisMQTemplate {
34 52 * @return 消息记录的编号对象
35 53 */
36 54 public <T extends AbstractStreamMessage> RecordId send(T message) {
37   - return redisTemplate.opsForStream().add(StreamRecords.newRecord()
38   - .ofObject(JsonUtils.toJsonString(message)) // 设置内容
39   - .withStreamKey(message.getStreamKey())); // 设置 stream key
  55 + try {
  56 + sendMessageBefore(message);
  57 + // 发送消息
  58 + return redisTemplate.opsForStream().add(StreamRecords.newRecord()
  59 + .ofObject(JsonUtils.toJsonString(message)) // 设置内容
  60 + .withStreamKey(message.getStreamKey())); // 设置 stream key
  61 + } finally {
  62 + sendMessageAfter(message);
  63 + }
  64 + }
  65 +
  66 + /**
  67 + * 添加拦截器
  68 + *
  69 + * @param interceptor 拦截器
  70 + */
  71 + public void addInterceptor(RedisMessageInterceptor interceptor) {
  72 + interceptors.add(interceptor);
  73 + }
  74 +
  75 + private void sendMessageBefore(AbstractRedisMessage message) {
  76 + // 正序
  77 + interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message));
  78 + }
  79 +
  80 + private void sendMessageAfter(AbstractRedisMessage message) {
  81 + // 倒序
  82 + for (int i = interceptors.size() - 1; i >= 0; i--) {
  83 + interceptors.get(i).sendMessageAfter(message);
  84 + }
40 85 }
41 86  
42 87 }
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/interceptor/RedisMessageInterceptor.java 0 → 100644
  1 +package cn.iocoder.yudao.framework.mq.core.interceptor;
  2 +
  3 +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
  4 +
  5 +/**
  6 + * {@link AbstractRedisMessage} 消息拦截器
  7 + * 通过拦截器,作为插件机制,实现拓展。
  8 + * 例如说,多租户场景下的 MQ 消息处理
  9 + *
  10 + * @author 芋道源码
  11 + */
  12 +public interface RedisMessageInterceptor {
  13 +
  14 + default void sendMessageBefore(AbstractRedisMessage message) {
  15 + }
  16 +
  17 + default void sendMessageAfter(AbstractRedisMessage message) {
  18 + }
  19 +
  20 + default void consumeMessageBefore(AbstractRedisMessage message) {
  21 + }
  22 +
  23 + default void consumeMessageAfter(AbstractRedisMessage message) {
  24 + }
  25 +
  26 +}
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessageListener.java
... ... @@ -2,11 +2,16 @@ package cn.iocoder.yudao.framework.mq.core.pubsub;
2 2  
3 3 import cn.hutool.core.util.TypeUtil;
4 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
  6 +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
  7 +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
  8 +import lombok.Setter;
5 9 import lombok.SneakyThrows;
6 10 import org.springframework.data.redis.connection.Message;
7 11 import org.springframework.data.redis.connection.MessageListener;
8 12  
9 13 import java.lang.reflect.Type;
  14 +import java.util.List;
10 15  
11 16 /**
12 17 * Redis Pub/Sub 监听器抽象类,用于实现广播消费
... ... @@ -25,6 +30,11 @@ public abstract class AbstractChannelMessageListener&lt;T extends AbstractChannelMe
25 30 * Redis Channel
26 31 */
27 32 private final String channel;
  33 + /**
  34 + * RedisMQTemplate
  35 + */
  36 + @Setter
  37 + private RedisMQTemplate redisMQTemplate;
28 38  
29 39 @SneakyThrows
30 40 protected AbstractChannelMessageListener() {
... ... @@ -44,7 +54,13 @@ public abstract class AbstractChannelMessageListener&lt;T extends AbstractChannelMe
44 54 @Override
45 55 public final void onMessage(Message message, byte[] bytes) {
46 56 T messageObj = JsonUtils.parseObject(message.getBody(), messageType);
47   - this.onMessage(messageObj);
  57 + try {
  58 + consumeMessageBefore(messageObj);
  59 + // 消费消息
  60 + this.onMessage(messageObj);
  61 + } finally {
  62 + consumeMessageAfter(messageObj);
  63 + }
48 64 }
49 65  
50 66 /**
... ... @@ -68,4 +84,20 @@ public abstract class AbstractChannelMessageListener&lt;T extends AbstractChannelMe
68 84 return (Class<T>) type;
69 85 }
70 86  
  87 + private void consumeMessageBefore(AbstractRedisMessage message) {
  88 + assert redisMQTemplate != null;
  89 + List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
  90 + // 正序
  91 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
  92 + }
  93 +
  94 + private void consumeMessageAfter(AbstractRedisMessage message) {
  95 + assert redisMQTemplate != null;
  96 + List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
  97 + // 倒序
  98 + for (int i = interceptors.size() - 1; i >= 0; i--) {
  99 + interceptors.get(i).consumeMessageAfter(message);
  100 + }
  101 + }
  102 +
71 103 }
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessage.java
1 1 package cn.iocoder.yudao.framework.mq.core.stream;
2 2  
  3 +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
3 4 import com.fasterxml.jackson.annotation.JsonIgnore;
4 5  
5 6 /**
... ... @@ -7,7 +8,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore;
7 8 *
8 9 * @author 芋道源码
9 10 */
10   -public abstract class AbstractStreamMessage {
  11 +public abstract class AbstractStreamMessage extends AbstractRedisMessage {
11 12  
12 13 /**
13 14 * 获得 Redis Stream Key
... ...
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessageListener.java
... ... @@ -2,15 +2,18 @@ package cn.iocoder.yudao.framework.mq.core.stream;
2 2  
3 3 import cn.hutool.core.util.TypeUtil;
4 4 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
  5 +import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
  6 +import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
  7 +import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
5 8 import lombok.Getter;
6 9 import lombok.Setter;
7 10 import lombok.SneakyThrows;
8 11 import org.springframework.beans.factory.annotation.Value;
9 12 import org.springframework.data.redis.connection.stream.ObjectRecord;
10   -import org.springframework.data.redis.core.RedisTemplate;
11 13 import org.springframework.data.redis.stream.StreamListener;
12 14  
13 15 import java.lang.reflect.Type;
  16 +import java.util.List;
14 17  
15 18 /**
16 19 * Redis Stream 监听器抽象类,用于实现集群消费
... ... @@ -39,10 +42,10 @@ public abstract class AbstractStreamMessageListener&lt;T extends AbstractStreamMess
39 42 @Getter
40 43 private String group;
41 44 /**
42   - * RedisTemplate
  45 + * RedisMQTemplate
43 46 */
44 47 @Setter
45   - private RedisTemplate<String, ?> redisTemplate;
  48 + private RedisMQTemplate redisMQTemplate;
46 49  
47 50 @SneakyThrows
48 51 protected AbstractStreamMessageListener() {
... ... @@ -54,14 +57,20 @@ public abstract class AbstractStreamMessageListener&lt;T extends AbstractStreamMess
54 57 public void onMessage(ObjectRecord<String, String> message) {
55 58 // 消费消息
56 59 T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
57   - this.onMessage(messageObj);
58   - // ack 消息消费完成
59   - redisTemplate.opsForStream().acknowledge(group, message);
60   - // TODO 芋艿:需要额外考虑以下几个点:
61   - // 1. 处理异常的情况
62   - // 2. 发送日志;以及事务的结合
63   - // 3. 消费日志;以及通用的幂等性
64   - // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
  60 + try {
  61 + consumeMessageBefore(messageObj);
  62 + // 消费消息
  63 + this.onMessage(messageObj);
  64 + // ack 消息消费完成
  65 + redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message);
  66 + // TODO 芋艿:需要额外考虑以下几个点:
  67 + // 1. 处理异常的情况
  68 + // 2. 发送日志;以及事务的结合
  69 + // 3. 消费日志;以及通用的幂等性
  70 + // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
  71 + } finally {
  72 + consumeMessageAfter(messageObj);
  73 + }
65 74 }
66 75  
67 76 /**
... ... @@ -85,4 +94,20 @@ public abstract class AbstractStreamMessageListener&lt;T extends AbstractStreamMess
85 94 return (Class<T>) type;
86 95 }
87 96  
  97 + private void consumeMessageBefore(AbstractRedisMessage message) {
  98 + assert redisMQTemplate != null;
  99 + List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
  100 + // 正序
  101 + interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
  102 + }
  103 +
  104 + private void consumeMessageAfter(AbstractRedisMessage message) {
  105 + assert redisMQTemplate != null;
  106 + List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
  107 + // 倒序
  108 + for (int i = interceptors.size() - 1; i >= 0; i--) {
  109 + interceptors.get(i).consumeMessageAfter(message);
  110 + }
  111 + }
  112 +
88 113 }
... ...