最佳实践:路径路由匹配规则的设计与实现

这篇具有很好参考价值的文章主要介绍了最佳实践:路径路由匹配规则的设计与实现。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

最佳实践:路径路由匹配规则的设计与实现

作者:哲思
时间: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?什么是路径?

首先,需要明确一下什么是资源?什么是路径?

上面提到的 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<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
}

后记

本次分享了在项目中的一个细节设计,后续会继续分享在工作、学习和生活中的点点滴滴,也欢迎大家在评论区共同讨论或与我邮件沟通。文章来源地址https://www.toymoban.com/news/detail-438096.html

到了这里,关于最佳实践:路径路由匹配规则的设计与实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Spring-cloud-gateway 路由配置方式及匹配规则

    1.1 基础路由配置⽅式 如果请求的⽬标地址,是单个的URI资源路径,配置⽂件实例如下: 各字段含义如下。 id:我们⾃定义的路由 ID,保持唯⼀ uri:⽬标服务地址 predicates:路由条件,Predicate 接受⼀个输⼊参数,返回⼀个布尔值结果。该接⼝包含多种默 认⽅法来将 Predicate

    2024年02月04日
    浏览(45)
  • 浅谈OpenCV的多对象匹配图像的实现,以及如何匹配半透明控件,不规则图像

    OpenCV提供的templateMatch只负责将(相关性等)计算出来,并不会直接提供目标的对应坐标,一般来说我们直接遍历最高的相关度,就可以得到匹配度最高的坐标。但是这样一般只能得到一个坐标。 在实际操作中,我们可能需要匹配一个不规则的图像,把这个不规则的图像放进

    2024年02月10日
    浏览(53)
  • python -- 实现路径的匹配,剔除掉指定路径,并保存路径

    在处理nc数据时,由于部分数据在插值的过程中,存在过多的0值,使得在制作标签时该时刻的数据出现报错,但是对于一年的数据量来说,无关紧要,所以只是记录了出现报错的时刻的路径,方面在后续变量读取过程中进行剔除,报错后续文件的处理。 下面记录一下主要的代

    2024年02月10日
    浏览(41)
  • GaussDB表设计最佳实践

    目录 如何选择存储模型 如何选择数据分布方式 如何选择分布列 其他最佳设计建议 使用局部聚簇 使用分区表 选择数据类型 进行数据库设计时,表设计上的一些关键项将严重影响后续整库的查询性能。表设计对数据存储也有影响:好的表设计能够减少I/O操作及最小化内存使

    2023年04月09日
    浏览(43)
  • 软件架构设计最佳实践(课程大纲)

    课程介绍: 1、深入阐述软件架构设计的思想、方向及趋势; 2、剖析软件架构的全景视图; 3、结合实际案例分析架构设计过程及需求对架构的影响; 4、如何实用设计模式来实现好的架构; 5、实践分享多种类型架构设计的实现; 6、SOA架构、企业集成系统架构、企业门户架

    2023年04月21日
    浏览(50)
  • 一文了解函数设计的最佳实践

    良好设计的函数具有清晰的职责和逻辑结构,提供准确的命名和适当的参数控制。它们促进代码复用、支持团队协作,降低维护成本,并提供可测试的代码基础。通过遵循最佳实践,我们能够编写出高质量、可读性强的代码,从而提高开发效率和软件质量。下面我们将一一描

    2024年02月10日
    浏览(31)
  • 延迟队列的设计与最佳实践

    在现代分布式系统中,延迟任务是一种非常重要的概念。它们可以用来处理需要特殊关注或执行的任务,如发邮件、推送消息或生成报告等。为了实现这些任务,我们需要一种强大而可靠的工具,即延迟队列。 在本博客中,我们将介绍延迟队列的设计和最佳实践。我们将使用

    2023年04月13日
    浏览(33)
  • 【最佳实践】如何设计 RESTful API ?

    良好的 API 设计是一个经常被提及的话题,特别是对于那些试图完善其 API 策略的团队来说。一个设计良好的 API 的好处包括:改进开发者体验、更快速地编写文档以及更高效地推广你的 API。但是,到底什么才构成了良好 API 设计呢?在这篇博客文章中,我将详细介绍几个为

    2024年02月07日
    浏览(34)
  • 精通代码复用:设计原则与最佳实践

    在你开始设计的所有层次上,从单一函数、类,到整个库和框架,都需要从一开始就考虑到代码复用。在接下来的文本中,所有这些不同的层次都被称为组件。以下策略将帮助你合理地组织你的代码。注意,所有这些策略都专注于使你的代码具有通用性。设计可复用代码的第

    2024年02月08日
    浏览(48)
  • 【Web开发 | Django】数据库分流之道:探索Django多数据库路由最佳实践

    🤵‍♂️ 个人主页: @AI_magician 📡主页地址: 作者简介:CSDN内容合伙人,全栈领域优质创作者。 👨‍💻景愿:旨在于能和更多的热爱计算机的伙伴一起成长!!🐱‍🏍 🙋‍♂️声明:本人目前大学就读于大二,研究兴趣方向人工智能硬件(虽然硬件还没开始玩,但一直

    2024年02月07日
    浏览(96)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包