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:
2026-07-16 06:14:05 +01:00
parent 1f29c71dba
commit ac7559151c
2 changed files with 87 additions and 0 deletions

View File

@@ -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 消息携带的 tagnull/空 = 未打标签,只被全收表达式命中)
* @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;
}
}

View File

@@ -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));
}
}