feat(mq-starter-core): 新增 MqTagMatcher,tag 表达式匹配语义单一来源
对齐 RocketMQ tag 表达式子集:*/空=全收、单 tag 精确、A || B 多选一; 供无服务端过滤能力的 driver 在消费端模拟过滤时调用。 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
package com.njcn.mq.spi;
|
||||
|
||||
/**
|
||||
* 订阅 tag 表达式匹配,语义对齐 RocketMQ 原生 tag 表达式**子集**:
|
||||
* {@code *}/空 = 全收、单 tag 精确匹配、{@code A || B} 多选一。不支持 SQL92 / 否定 / 通配。
|
||||
*
|
||||
* <p>无服务端过滤能力的 driver(如 redis-stream)在消费端调用本类模拟过滤;
|
||||
* 语义保持单一来源,勿在 driver 内自行实现比对逻辑。rocketmq driver 无需本类(broker 原生过滤)。
|
||||
*/
|
||||
public final class MqTagMatcher {
|
||||
|
||||
private MqTagMatcher() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param subscriptionTag 订阅方声明的 tag 表达式({@code @MqListener.tag},缺省 {@code *})
|
||||
* @param messageTag 消息携带的 tag(null/空 = 未打标签,只被全收表达式命中)
|
||||
* @return true = 该消息归本订阅消费
|
||||
*/
|
||||
public static boolean matches(String subscriptionTag, String messageTag) {
|
||||
if (subscriptionTag == null) {
|
||||
return true;
|
||||
}
|
||||
String expr = subscriptionTag.trim();
|
||||
if (expr.isEmpty() || "*".equals(expr)) {
|
||||
return true;
|
||||
}
|
||||
boolean hasValidPart = false;
|
||||
for (String part : expr.split("\\|\\|")) {
|
||||
String p = part.trim();
|
||||
if (p.isEmpty()) {
|
||||
continue; // 空段忽略(如 "A ||" 的尾段)
|
||||
}
|
||||
hasValidPart = true;
|
||||
if (p.equals(messageTag)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 全空段(如 "||")= 没声明任何有效 tag = 视同全收;有有效段但都没命中 = 不匹配
|
||||
return !hasValidPart;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.njcn.mq.spi;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class MqTagMatcherTest {
|
||||
|
||||
@Test
|
||||
void catchAllExpressions_alwaysMatch() {
|
||||
assertTrue(MqTagMatcher.matches(null, "A"));
|
||||
assertTrue(MqTagMatcher.matches("", "A"));
|
||||
assertTrue(MqTagMatcher.matches(" ", "A"));
|
||||
assertTrue(MqTagMatcher.matches("*", "A"));
|
||||
assertTrue(MqTagMatcher.matches("||", "A")); // 全空段 = 空表达式 = 全收
|
||||
assertTrue(MqTagMatcher.matches("*", null)); // 全收对无 tag 消息同样放行
|
||||
assertTrue(MqTagMatcher.matches("*", ""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void singleTag_exactCaseSensitiveMatch() {
|
||||
assertTrue(MqTagMatcher.matches("A", "A"));
|
||||
assertFalse(MqTagMatcher.matches("A", "B"));
|
||||
assertFalse(MqTagMatcher.matches("A", "a")); // 大小写敏感
|
||||
assertTrue(MqTagMatcher.matches(" A ", "A")); // 订阅侧逐段 trim
|
||||
assertFalse(MqTagMatcher.matches("A", "A ")); // 消息侧不 trim
|
||||
}
|
||||
|
||||
@Test
|
||||
void orExpression_matchesAnyValidPart() {
|
||||
assertTrue(MqTagMatcher.matches("A || B", "A"));
|
||||
assertTrue(MqTagMatcher.matches("A || B", "B"));
|
||||
assertFalse(MqTagMatcher.matches("A || B", "C"));
|
||||
assertTrue(MqTagMatcher.matches("A ||", "A")); // 空段忽略,不影响有效段
|
||||
assertFalse(MqTagMatcher.matches("A ||", "B"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void untaggedMessage_onlyMatchedByCatchAll() {
|
||||
assertFalse(MqTagMatcher.matches("A", null));
|
||||
assertFalse(MqTagMatcher.matches("A", ""));
|
||||
assertFalse(MqTagMatcher.matches("A || B", null));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user