前言作者:哲思时间:2023.5.9邮箱:zhe__si@163.comGitHub:zhe-si (哲思) (github.com)
时间一晃研究生都过去大半年了,学了些东西,也做了些项目,借着博客总结一下。这次先聊一个简单的话题开个头。
(资料图)
开发中,常用形似 “a/b/c” 的描述方式来描述路径、定位资源,有着层次化和可读性高的特点,最经典的例子就是 URL(统一资源定位符),第二节会进行简要介绍。
将资源都路径化后,可以通过每一段路径精确的匹配来唯一的确定一个资源。但有时候,需要对具有相关特征的一组资源进行统一的描述或操作。比如,将所有获得用户信息的请求都路由到一个指定的处理程序上,请求的 URL 中包含不同用户 id 路径分段指向不同用户信息资源。再比如,界面中导航栏包含图片组(包含图1、2、3)和文本组(包含文本1、2、3),在访问图片组下不同图片时打开图片展示器而在访问文本组的文本时打开文本展示器。
基于上述场景的需求,需要一种简单而通用的路径路由匹配规则。最强大的方式是直接使用正则表达式来描述一组路径,但在描述一些复杂的路径场景时,正则表达式使用起来非常繁琐和困难。比如,匹配这样一组路径 "x1/a/x2/a",x1 表示任意长的最短匹配路径,x2表示任意长的最长匹配路径,大家可以尝试用正则表达式实现,并和本文设计的匹配规则的描述进行对比。
本文设计并实现了一种专用于路径路由匹配的规则,以一种简单而通用的方式描述一组路径的特征,来简化这种场景路由描述难度,让小白可以快速学习并上手。
什么是URL?什么是路径?首先,需要明确一下什么是资源?什么是路径?
上面提到的 URL(统一资源定位符)是 URI(统一资源标识符)的一种分类。
URI 的本质语义是标识一个资源,资源可以是一张图片、一个文档、一个服务、一个用户等具体或抽象的实体,官方(RFC2396)将其格式标准化为如下格式(就是 URL 的格式)
该格式的大致含义是某人(user:pass)用某种方式(protocol)访问某个主机端口(hostname:port)某个路径(pathName)的资源,同时可用 search 对该资源做筛选、排序等操作、用 hash 访问资源的片段(子资源)。
而标识一个资源,可以通过描述位置或名字的方式,所以 URI 包括 URL 和 URN(统一资源名称)。
描述位置:用资源所处的地址来描述该资源,该描述指定了在特定地址的资源而不特指某一个具体的资源,也就是说实际指向的资源可能会随时间发生变化,资源的位置描述也会随资源本身位置的变化而变化,如 URL。描述名字:用一个全局唯一的标识符持久的标记一个特定的资源,不会随着时间或位置变化而改变指向的资源,如 URN(例:urn:oasis:names:specification:docbook:dtd:xml:4.1.2),常用于 Map、Redis 中 KEY 的定义等场景。但不管是位置描述还是名字描述、不管具体的格式是什么,都可以把它们抽象为一种“路径”,只是路径的描述的含义不同、分隔符不同。
比如,URL 中,最核心的部分就是hostname:port/path
这一部分,如下图蓝色区域,
蓝色区域已经完整描述了资源的位置,protocol 是补充描述了访问资源的方式,username:password 是附带的认证信息,search 和 hash 则描述的对某一个资源的进一步处理。而hostname:port/path
就是一个路径,每个路径分段描述的是某个层级的位置节点。
比如,URN 中,路径的每个路径分段则描述不同命名空间及命名空间下的名字。
路径路由匹配规则的设计了解了什么是路径,接下来给出路径路由匹配规则的定义描述:
R 模式:正则模式,格式:R:正则表达式
该模式下,完全采用正则表达式格式进行匹配。
标准模式:标准路径路由描述表达式,格式形如:/**/xxx/*/xxx
,由多个路径分段的分段列表组成,要求路径分段列表全匹配
具体语法:
分隔方法
默认使用 "/" 分隔符分隔多个路径分段
支持自定义路由分隔方法和路径分隔方法路径分段
每个具体的路径分段默认采用完全字符串匹配r 模式:路径分段正则模式,该路径分段采用正则表达式进行匹配,格式:r:正则表达式
通配符 "?"
匹配任意一段或 0 段路径分段,在满足后续部分匹配的情况下优先不匹配
通配符 "*"
匹配任意一端路径,不可匹配空或不匹配
通配符 "**"
匹配任意多段路径分段,不保证尽可能满足的最短匹配原则,即在满足紧接的后续非通配符部分匹配的情况下尽可能少的匹配
通配符 "***"
匹配任意多段路径分段,保证尽可能满足的最长匹配原则,即在满足后续匹配的情况下尽可能多的匹配
举一些例子:
路由 | 匹配路径 | 不匹配路径 |
---|---|---|
a/?/c | a/b/c、a//c、a/c | a/c/d |
a/*/c | a/b/c | a/c |
a/b/* | a/b/c | a/b |
**/b/c | a/b/c、b/c、a/a/b/b/c | b/c/b/c |
a/***/c/* | a/c/c、a/c/b/c/d | a/b/c |
a/**/c/* | a/c/c | a/c/b/c/d |
一组路径 "x1/a/x2/a",x1 表示任意长的最短匹配路径,x2表示任意长的最长匹配路径,使用标准路径路由描述表达式描述就是**/a/***/b
。
其中,最常用的通配符是 "**",通过不保证尽可能匹配的方式最短匹配,确保匹配的是我们直观预期的路径。比如如下目录结构,
- common- A.java- B.java- a.conf- impl- common- Utils.java- AImpl.java- BImpl.java
我们希望匹配接口 A 和 B 的 java 文件,而不匹配到 impl 里的实现类,可以采用如下匹配方式:**/common/r:.*\.java
。
本文采用 kotlin 进行实现,重点位置已经进行注释说明,源代码可见仓库。
/** * **路由匹配** * - 若 "R:" 开头,则为正则模式,采用正则表达式直接匹配 * - 其他情况,采用标准路由模式[matchStdRoutePattern]进行匹配 * * @author lq * @version 1.0 */fun matchRoutePattern(routePattern: String, path: String): Boolean { return if (routePattern.startsWith("R:")) { Regex(routePattern.substring(2)).matches(path) } else { matchStdRoutePattern(routePattern, path) }}/** * **路由模式**:路由的特定描述表达式,形如 / ** /xxx/ * /xxx/ * * 语法: * - 以 "/"([PATH_DELIMITER]) 分隔的路径表达式,要求全匹配路径,首尾的分隔符可以省略 * - 每一段路径描述默认采用字符串完全匹配方式,也可通过 "r:" 开头标记该段采用正则表达式匹配 * - 使用通配符 "?" 可以匹配任意一段或 0 段路径,优先不匹配 * - 使用通配符 "*" 可以匹配任意一段路径 * - 使用通配符 "**" 可以匹配任意多段路径,最短匹配原则 * - 使用通配符 "***" 可以匹配任意多段路径,最长匹配原则 */fun matchStdRoutePattern(routePattern: String, path: String): Boolean { val routeSplit = splitRoute(routePattern) val pathSplit = splitPath(path) return matchRoutePatternSplit(routeSplit, 0, pathSplit, 0)}/** * 路由分隔方法 */val splitRoute: (String) -> List = ::splitPathSimple/** * 路径分隔方法 */val splitPath: (String) -> List = ::splitPathSimple/** * 简单解析路径为路径分段列表 */private fun splitPathSimple(routePattern: String): List { val pathDelimiter = "/" return routePattern.trim(pathDelimiter).split(pathDelimiter).filter { p -> p.isNotEmpty() }}private fun matchRoutePatternSplit(routeSplit: List, ri: Int, pathSplit: List, pi: Int): Boolean { if (ri >= routeSplit.size) { return pi >= pathSplit.size } if (pi >= pathSplit.size) { for (i in ri until routeSplit.size) { if (routeSplit[i] !in listOf("?", "**", "***")) return false } return true } when (routeSplit[ri].trim()) { "?" -> { if (matchRoutePatternSplit(routeSplit, ri + 1, pathSplit, pi)) return true return matchRoutePatternSplit(routeSplit, ri + 1, pathSplit, pi + 1) } "*" -> { return matchRoutePatternSplit(routeSplit, ri + 1, pathSplit, pi + 1) } "**" -> { for (i in 0 until pathSplit.size - pi) { val isShortMatch = matchRoutePatternShort(routeSplit, ri + 1, pathSplit, pi + i, false) if (isShortMatch.first) return true if (isShortMatch.second) return false } return matchRoutePatternSplit(routeSplit, ri + 1, pathSplit, pi + pathSplit.size - pi) } "***" -> { for (i in pathSplit.size - pi downTo 1) { if (matchRoutePatternSplit(routeSplit, pi + 1, pathSplit, pi + i)) return true } return matchRoutePatternSplit(routeSplit, ri + 1, pathSplit, pi) } else -> { if (!checkRouteSegPattern(routeSplit[ri], pathSplit[pi])) return false return matchRoutePatternSplit(routeSplit, ri + 1, pathSplit, pi + 1) } }}/** * 最短原则匹配,返回 (是否匹配, 是否已经最短匹配) */private fun matchRoutePatternShort(routeSplit: List, ri: Int, pathSplit: List, pi: Int, isShortMatch: Boolean): Pair { if (ri >= routeSplit.size) { return (pi >= pathSplit.size) to isShortMatch } if (pi >= pathSplit.size) { for (i in ri until routeSplit.size) { if (routeSplit[i] !in listOf("?", "**", "***")) return false to isShortMatch } return true to isShortMatch } when (routeSplit[ri].trim()) { "?" -> { val isMatch = matchRoutePatternShort(routeSplit, ri + 1, pathSplit, pi, isShortMatch) if (isMatch.first) return isMatch return matchRoutePatternShort(routeSplit, ri + 1, pathSplit, pi + 1, isShortMatch) } "*" -> { return matchRoutePatternShort(routeSplit, ri + 1, pathSplit, pi + 1, isShortMatch) } "**" -> { return matchRoutePatternSplit(routeSplit, ri, pathSplit, pi) to isShortMatch } "***" -> { return matchRoutePatternSplit(routeSplit, ri, pathSplit, pi) to isShortMatch } else -> { return if (checkRouteSegPattern(routeSplit[ri], pathSplit[pi])) { matchRoutePatternShort(routeSplit, ri + 1, pathSplit, pi + 1, true) } else { false to isShortMatch } } }}/** * 路径分段匹配检查 */private fun checkRouteSegPattern(routeSeg: String, pathSeg: String): Boolean { if (routeSeg.startsWith("r:")) { if (Regex(routeSeg.substring(2)).matches(pathSeg)) { return true } } else if (routeSeg == pathSeg) return true return false}
后记本次分享了在项目中的一个细节设计,后续会继续分享在工作、学习和生活中的点点滴滴,也欢迎大家在评论区共同讨论或与我邮件沟通。
标签:
- 当前短讯!最佳实践:路径路由匹配规则的设计与实现
- 皮肤干燥对白癜风有什么危害吗?早期节段型白癜风该怎样判断?
- (区域发展新亮点·黑土地上的耕耘)谁来种好黑土地?——东北三省粮食生产蹲点调研之二
- 预防地质灾害 确保人民生命安全 微动态
- 赛季结束,曼联三位租将汉尼拔、萨维奇、麦克尼尔告别各自球队-环球观天下
- 京报读书㊲丨施一公《自我突围》、《肥尾效应》,本周书单来了!
- 中国空间站,您的快递今日发出!内附“生鲜清单”
- 巴黎奥运会排球资格赛落户宁波
- 世界视讯!外媒:联合国秘书长古特雷斯称目前俄乌和谈不可能
- 300年花王雍容华贵!北京世园公园牡丹花海盛放
- 怀孕女子被骗到缅甸 警方介入调查|当前观点
- 猥亵男被揪下跪求饶:你别毁了我!-环球报道
- AI技术带动搜索引擎升级融合 在技术迭代中获得竞争优势
- 李小鹏任交通运输部党组书记|当前短讯
- 领英职场将停服 8月9日起正式停止服务-全球观速讯
- 河北:激活科技创新动力源,打造高质量发展新引擎
- 科技创新为经济发展蓄势赋能 热推荐
- 最新快讯!武汉双年展论坛成功举办 跨界专家共论“融合与创新”
- HICOOL 2023全球创业大赛完成招募:AI项目增45%
- 龙鼎滨水公园“换新装”全部工程预计6月底完工_每日快看
- 寻根觅韵,有礼衢城!400余名小学生同庆十岁成长礼|每日热讯
- 赴美航班新冠疫苗接种要求将取消
- 美国作家协会成员罢工 多名业内知名人士参加_当前热讯
- 全球观天下!龙源电力(00916)已收到广西电力和华北电力缴纳的业绩补偿款合计约1.09亿元
- 2023 CNCIC中国数字农业峰会:数智赋能乡村振兴 科技助力高质量发展
- 环球微头条丨以智慧促监管 长江安庆通信管理局完成牛头山智慧锚地项目建设工作
- 中安创谷科技园三期工程1-1区首块底板顺利浇筑 全球视讯
- 晋能控股装备制造集团天庆公司“三强化”为班组建设提速
- 天天热推荐:红白喜事登记台账_红白喜事一点通
- 恒立实业:收入会计核算方法错误、收入确认跨期等触及违规 收深交所监管函 全球热消息