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