diff --git a/mq-starter-core/src/main/java/com/njcn/mq/spi/MqTagMatcher.java b/mq-starter-core/src/main/java/com/njcn/mq/spi/MqTagMatcher.java new file mode 100644 index 0000000..9e564f3 --- /dev/null +++ b/mq-starter-core/src/main/java/com/njcn/mq/spi/MqTagMatcher.java @@ -0,0 +1,42 @@ +package com.njcn.mq.spi; + +/** + * 订阅 tag 表达式匹配,语义对齐 RocketMQ 原生 tag 表达式**子集**: + * {@code *}/空 = 全收、单 tag 精确匹配、{@code A || B} 多选一。不支持 SQL92 / 否定 / 通配。 + * + *
无服务端过滤能力的 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; + } +} diff --git a/mq-starter-core/src/test/java/com/njcn/mq/spi/MqTagMatcherTest.java b/mq-starter-core/src/test/java/com/njcn/mq/spi/MqTagMatcherTest.java new file mode 100644 index 0000000..83e2c13 --- /dev/null +++ b/mq-starter-core/src/test/java/com/njcn/mq/spi/MqTagMatcherTest.java @@ -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)); + } +}