🍃前言
本次开发任务
- 实现Router类, 使用这个类, 来实现交换机的转发规则.
- 同时也借助这个类验证 bindingKey 与 routingKey 是否合法。
🍀判断routingKey是否合法
routingKey:发布消息的时候,给消息上指定的特殊字符串,与bindingKey 做匹配的。
如果 rontingKey 是答案,那么bindingKey 就是题目
对于routingKey 的命名,我们做出以下规定
- 由数字、字母、下划线组成
- 使用
.
把整个 routingKey 分成多个部分
形如:aaa.bbb.cc
根据以上规定,我们只需要对传入的 routingKey 进行一个一个字符进行判断即可。
如果符合上述规定,则 continue ,继续循环。若不符合,返回false即可
代码实现如下:
// routingKey 的构造规则: // 1. 数字, 字母, 下划线 // 2. 使用 . 分割成若干部分 public boolean checkRoutingKey(String routingKey) { if (routingKey.length() == 0) { // 空字符串. 合法的情况. 比如在使用 fanout 交换机的时候, routingKey 用不上, 就可以设为 "" return true; } for (int i = 0; i < routingKey.length(); i++) { char ch = routingKey.charAt(i); // 判定该字符是否是大写字母 if (ch >= 'A' && ch <= 'Z') { continue; } // 判定该字母是否是小写字母 if (ch >= 'a' && ch <= 'z') { continue; } // 判定该字母是否是阿拉伯数字 if (ch >= '0' && ch <= '9') { continue; } // 判定是否是 _ 或者 . if (ch == '_' || ch == '.') { continue; } // 该字符, 不是上述任何一种合法情况, 就直接返回 false return false; } // 把每个字符都检查过, 没有遇到非法情况. 此时直接返回 true return true; }
🎄判断bindingKey是否合法
bindingKey:创建绑定的时候,给绑定指定的特殊字符串。
bindingKey 的命名规则如下:
- 由数字、字母、下划线组成
- 使用
.
把整个bindingKey分成多个部分 - 支持两种特殊的符号作为通配符,且
*
和#
必须作为被.
分割出来的独立部分
*
:可以匹配任何一个独立部分#
:可以匹配任何0个或者多个独立的部分
此外,为了后面方便匹配,博主这里规定,通配符之间有如下规定:
- aaa.#.#.bbb => 非法
- aaa.#.*.bbb => 非法
- aaa.*.#.bbb => 非法
- aaa...bbb => 合法
根据以上规定,我们首先先对传进来的 bindingKey 的每一个字符进行判断,若不和上述规定,直接返回false 即可;
若都符合,我们就以.
将bindingKey 分为几个独立的部分,用数组进行接收,
然后我们进行判断,如果每个独立部分,长度大于1且含有*
或者#
,我们就说他是非法的
最后就是相邻的两个独立部分不能是:
#
和#
*
和#
#
和*
代码实现如下:
// bindingKey 的构造规则: // 1. 数字, 字母, 下划线 // 2. 使用 . 分割成若干部分 // 3. 允许存在 * 和 # 作为通配符. 但是通配符只能作为独立的分段. public boolean checkBindingKey(String bindingKey) { if (bindingKey.length() == 0) { // 空字符串, 也是合法情况. 比如在使用 direct / fanout 交换机的时候, bindingKey 是用不上的. return true; } // 检查字符串中不能存在非法字符 for (int i = 0; i < bindingKey.length(); i++) { char ch = bindingKey.charAt(i); if (ch >= 'A' && ch <= 'Z') { continue; } if (ch >= 'a' && ch <= 'z') { continue; } if (ch >= '0' && ch <= '9') { continue; } if (ch == '_' || ch == '.' || ch == '*' || ch == '#') { continue; } return false; } // 检查 * 或者 # 是否是独立的部分. // aaa.*.bbb 合法情况; aaa.a*.bbb 非法情况. String[] words = bindingKey.split("\\."); for (String word : words) { // 检查 word 长度 > 1 并且包含了 * 或者 # , 就是非法的格式了. if (word.length() > 1 && (word.contains("*") || word.contains("#"))) { return false; } } // 约定一下, 通配符之间的相邻关系(人为(俺)约定的). // 为啥这么约定? 因为前三种相邻的时候, 实现匹配的逻辑会非常繁琐, 同时功能性提升不大~~ // 1. aaa.#.#.bbb => 非法 // 2. aaa.#.*.bbb => 非法 // 3. aaa.*.#.bbb => 非法 // 4. aaa.*.*.bbb => 合法 for (int i = 0; i < words.length - 1; i++) { // 连续两个 ## if (words[i].equals("#") && words[i + 1].equals("#")) { return false; } // # 连着 * if (words[i].equals("#") && words[i + 1].equals("*")) { return false; } // * 连着 # if (words[i].equals("*") && words[i + 1].equals("#")) { return false; } } return true; }
🌴判断bindingKey 与 routingKey 是否匹配
我们首先将 bindingKey 与 routingKey 进行切分,用数组进行接收。
引入两个下标, 指向上述两个数组. 初始情况下都为 0
然后进行一一比对,我们分为五种情况进行判断:
- 情况一:如果遇到普通字符串(不是
*
,也不是#
), 要求两边的内容是一样的.
- 情况二:如果遇到
*
, 直接进入下一轮. * 可以匹配到任意一个部分!!
- 情况三:遇到
#
,该#
后面没东西了, 说明此时一定能匹配成功了!
- 情况四:
#
后面还有东西, 拿着后面的内容, 去 routingKey 中往后找, 找到对应的位置.
- 情况五:判定是否是双方同时到达末尾
根据上述五种情况,代码书写如下:
// 这个方法用来判定该消息是否可以转发给这个绑定对应的队列. public boolean route(ExchangeType exchangeType, Binding binding, Message message) throws MqException { // 根据不同的 exchangeType 使用不同的判定转发规则. if (exchangeType == ExchangeType.FANOUT) { // 如果是 FANOUT 类型, 则该交换机上绑定的所有队列都需要转发 return true; } else if (exchangeType == ExchangeType.TOPIC) { // 如果是 TOPIC 主题交换机, 规则就要更复杂一些. return routeTopic(binding, message); } else { // 其他情况是不应该存在的. throw new MqException("[Router] 交换机类型非法! exchangeType=" + exchangeType); } } private boolean routeTopic(Binding binding, Message message) { // 先把这两个 key 进行切分 String[] bindingTokens = binding.getBindingKey().split("\\."); String[] routingTokens = message.getRoutingKey().split("\\."); // 引入两个下标, 指向上述两个数组. 初始情况下都为 0 int bindingIndex = 0; int routingIndex = 0; // 此处使用 while 更合适, 每次循环, 下标不一定就是 + 1, 不适合使用 for while (bindingIndex < bindingTokens.length && routingIndex < routingTokens.length) { if (bindingTokens[bindingIndex].equals("*")) { // [情况二] 如果遇到 * , 直接进入下一轮. * 可以匹配到任意一个部分!! bindingIndex++; routingIndex++; continue; } else if (bindingTokens[bindingIndex].equals("#")) { // 如果遇到 #, 需要先看看有没有下一个位置. bindingIndex++; if (bindingIndex == bindingTokens.length) { // [情况三] 该 # 后面没东西了, 说明此时一定能匹配成功了! return true; } // [情况四] # 后面还有东西, 拿着这个内容, 去 routingKey 中往后找, 找到对应的位置. // findNextMatch 这个方法用来查找该部分在 routingKey 的位置. 返回该下标. 没找到, 就返回 -1 routingIndex = findNextMatch(routingTokens, routingIndex, bindingTokens[bindingIndex]); if (routingIndex == -1) { // 没找到匹配的结果. 匹配失败 return false; } // 找到的匹配的情况, 继续往后匹配. bindingIndex++; routingIndex++; } else { // [情况一] 如果遇到普通字符串, 要求两边的内容是一样的. if (!bindingTokens[bindingIndex].equals(routingTokens[routingIndex])) { return false; } bindingIndex++; routingIndex++; } } // [情况五] 判定是否是双方同时到达末尾 // 比如 aaa.bbb.ccc 和 aaa.bbb 是要匹配失败的. if (bindingIndex == bindingTokens.length && routingIndex == routingTokens.length) { return true; } return false; } private int findNextMatch(String[] routingTokens, int routingIndex, String bindingToken) { for (int i = routingIndex; i < routingTokens.length; i++) { if (routingTokens[i].equals(bindingToken)) { return i; } } return -1; }
🌲测试匹配方法
我们对上述所书写的匹配方法进行测试一下。
测试数据如下:
测试代码如下:
@SpringBootTest public class RouterTests { private Router router = new Router(); private Binding binding = null; private Message message = null; @BeforeEach public void setUp() { binding = new Binding(); message = new Message(); } @AfterEach public void tearDown() { binding = null; message = null; } // [测试用例] // binding key routing key result // aaa aaa true // aaa.bbb aaa.bbb true // aaa.bbb aaa.bbb.ccc false // aaa.bbb aaa.ccc false // aaa.bbb.ccc aaa.bbb.ccc true // aaa.* aaa.bbb true // aaa.*.bbb aaa.bbb.ccc false // *.aaa.bbb aaa.bbb false // # aaa.bbb.ccc true // aaa.# aaa.bbb true // aaa.# aaa.bbb.ccc true // aaa.#.ccc aaa.ccc true // aaa.#.ccc aaa.bbb.ccc true // aaa.#.ccc aaa.aaa.bbb.ccc true // #.ccc ccc true // #.ccc aaa.bbb.ccc true @Test public void test1() throws MqException { binding.setBindingKey("aaa"); message.setRoutingKey("aaa"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test2() throws MqException { binding.setBindingKey("aaa.bbb"); message.setRoutingKey("aaa.bbb"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test3() throws MqException { binding.setBindingKey("aaa.bbb"); message.setRoutingKey("aaa.bbb.ccc"); Assertions.assertFalse(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test4() throws MqException { binding.setBindingKey("aaa.bbb"); message.setRoutingKey("aaa.ccc"); Assertions.assertFalse(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test5() throws MqException { binding.setBindingKey("aaa.bbb.ccc"); message.setRoutingKey("aaa.bbb.ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test6() throws MqException { binding.setBindingKey("aaa.*"); message.setRoutingKey("aaa.bbb"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test7() throws MqException { binding.setBindingKey("aaa.*.bbb"); message.setRoutingKey("aaa.bbb.ccc"); Assertions.assertFalse(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test8() throws MqException { binding.setBindingKey("*.aaa.bbb"); message.setRoutingKey("aaa.bbb"); Assertions.assertFalse(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test9() throws MqException { binding.setBindingKey("#"); message.setRoutingKey("aaa.bbb.ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test10() throws MqException { binding.setBindingKey("aaa.#"); message.setRoutingKey("aaa.bbb"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test11() throws MqException { binding.setBindingKey("aaa.#"); message.setRoutingKey("aaa.bbb.ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test12() throws MqException { binding.setBindingKey("aaa.#.ccc"); message.setRoutingKey("aaa.ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test13() throws MqException { binding.setBindingKey("aaa.#.ccc"); message.setRoutingKey("aaa.bbb.ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test14() throws MqException { binding.setBindingKey("aaa.#.ccc"); message.setRoutingKey("aaa.aaa.bbb.ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test15() throws MqException { binding.setBindingKey("#.ccc"); message.setRoutingKey("ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } @Test public void test16() throws MqException { binding.setBindingKey("#.ccc"); message.setRoutingKey("aaa.bbb.ccc"); Assertions.assertTrue(router.route(ExchangeType.TOPIC, binding, message)); } }
测试结果如下:
⭕总结
关于《【消息队列开发】 实现Router类——交换机的转发规则》就讲解到这儿,感谢大家的支持,欢迎各位留言交流以及批评指正,如果文章对您有帮助或者觉得作者写的还不错可以点一下关注,点赞,收藏支持一下