最佳实践:路径路由匹配规则的设计与实现
时间:2023.5.9
邮箱:zhe__si@163.com
GitHub:zhe-si (哲思 (github.com
前言
开发中,常用形似 “a/b/c” 的描述方式来描述路径、定位资源,有着层次化和可读性高的特点,最经典的例子就是 URL(统一资源定位符),第二节会进行简要介绍。
路径化后,可以通过每一段路径精确的匹配来唯一的确定一个资源。但有时候,需要对具有相关特征的一组资源进行统一的描述或操作。比如,将所有获得用户信息的请求都路由到一个指定的处理程序上,请求的 URL 中包含不同用户 id 路径分段指向不同用户信息资源。再比如,界面中导航栏包含图片组(包含图1、2、3)和文本组(包含文本1、2、3),在访问图片组下不同图片时打开图片展示器而在访问文本组的文本时打开文本展示器。
路径路由匹配规则。最强大的方式是直接使用正则表达式来描述一组路径,但在描述一些复杂的路径场景时,正则表达式使用起来非常繁琐和困难。比如,匹配这样一组路径 "x1/a/x2/a",x1 表示任意长的最短匹配路径,x2表示任意长的最长匹配路径,大家可以尝试用正则表达式实现,并和本文设计的匹配规则的描述进行对比。
什么是URL?什么是路径?
首先,需要明确一下什么是资源?什么是路径?
URI 的本质语义是标识一个资源,资源可以是一张图片、一个文档、一个服务、一个用户等具体或抽象的实体,官方(RFC2396)将其格式标准化为如下格式(就是 URL 的格式)
而标识一个资源,可以通过描述位置或名字的方式,所以 URI 包括 URL 和 URN(统一资源名称)。
- 描述位置:用资源所处的地址来描述该资源,该描述指定了在特定地址的资源而不特指某一个具体的资源,也就是说实际指向的资源可能会随时间发生变化,资源的位置描述也会随资源本身位置的变化而变化,如 URL。
- 描述名字:用一个全局唯一的标识符持久的标记一个特定的资源,不会随着时间或位置变化而改变指向的资源,如 URN(例:urn:oasis:names:specification:docbook:dtd:xml:4.1.2),常用于 Map、Redis 中 KEY 的定义等场景。
路径”,只是路径的描述的含义不同、分隔符不同。
hostname:port/path这一部分,如下图蓝色区域,
hostname:port/path就是一个路径,每个路径分段描述的是某个层级的位置节点。
路径路由匹配规则的设计
了解了什么是路径,接下来给出路径路由匹配规则的定义描述:
-
标准模式:标准路径路由描述表达式,格式形如:
/**/xxx/*/xxx
,由多个路径分段的分段列表组成,要求路径分段列表全匹配-
分隔方法
- 支持自定义路由分隔方法和路径分隔方法
-
路径分段
- 每个具体的路径分段默认采用完全字符串匹配
- r 模式:路径分段正则模式,该路径分段采用正则表达式进行匹配,格式:
r:正则表达式
-
-
匹配任意一段或 0 段路径分段,在满足后续部分匹配的情况下优先不匹配
-
匹配任意一端路径,不可匹配空或不匹配
-
匹配任意多段路径分段,不保证尽可能满足的最短匹配原则,即在满足紧接的后续非通配符部分匹配的情况下尽可能少的匹配
-
匹配任意多段路径分段,保证尽可能满足的最长匹配原则,即在满足后续匹配的情况下尽可能多的匹配
R:正则表达式
路由 | 匹配路径 | 不匹配路径 |
---|---|---|
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
。
路径路由匹配规则的实现
/**
* **路由匹配**
* - 若 "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<String> = ::splitPathSimple
/**
* 路径分隔方法
*/
val splitPath: (String -> List<String> = ::splitPathSimple
/**
* 简单解析路径为路径分段列表
*/
private fun splitPathSimple(routePattern: String: List<String> {
val pathDelimiter = '/'
return routePattern.trim(pathDelimiter.split(pathDelimiter.filter { p -> p.isNotEmpty( }
}
private fun matchRoutePatternSplit(routeSplit: List<String>, ri: Int, pathSplit: List<String>, 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<String>, ri: Int, pathSplit: List<String>, pi: Int, isShortMatch: Boolean: Pair<Boolean, Boolean> {
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
}
后记
本次分享了在项目中的一个细节设计,后续会继续分享在工作、学习和生活中的点点滴滴,也欢迎大家在评论区共同讨论或与我邮件沟通。