第十二章. common-core 工具类(九):RegexUtils 与 ReUtil 文本处理
第十二章. common-core 工具类(九):RegexUtils 与 ReUtil 文本处理
Prorise第十二章. common-core 工具类(九):RegexUtils 与 ReUtil 文本处理
摘要:本章我们将深入 RVP 的 文本处理 利器 RegexUtils 及其父类 Hutool ReUtil。我们将从 Java 原生 Pattern 和 Matcher 的繁琐出发,引出 ReUtil 的便捷封装。本章将包含 正则表达式核心语法 的速成讲解,并重点实战 ReUtil 在 查找、提取、替换、分组 等方面的强大功能。
本章学习路径

12.1. RegexUtils 概览与 ReUtil 继承关系
我们首先定位 RVP 框架中的正则表达式工具类。
12.1.1. 文件定位:RegexUtils (RVP) 与 ReUtil (Hutool)
RVP 在 common-core 中提供了 RegexUtils。
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java
打开源码,我们看到的第一行就揭示了它的“身份”:
1 | import cn.hutool.core.util.ReUtil; |
分析:RegexUtils 继承了 Hutool 的 cn.hutool.core.util.ReUtil。ReUtil 是 Hutool 提供的正则表达式工具类,它已经非常强大和全面。RVP 再次使用了“继承与增强”的设计模式。
这意味着,我们在 RVP 项目中调用 RegexUtils 时,实际上是在调用 ReUtil 中所有的方法,同时 RVP 额外增加了一些它自己独有的方法。
12.1.2. RegexUtils 的增强:extractFromString() 方法
RVP RegexUtils 增强的方法非常少,主要是 extractFromString():
1 | // 位于 RVP 的 RegexUtils.java |
分析:
RVP 的 extractFromString 是一个“带有默认值”且“专用于获取分组 1”的 ReUtil.get() 封装。
既然 RVP 的工具类 99% 的功能都来自 Hutool ReUtil,那么本章的 重点 就是 学懂 ReUtil 是如何解决 Java 原生正则表达式痛点的。
12.2. 【痛点引入】原生 Pattern 与 Matcher 的繁琐
在 Hutool ReUtil 出现之前,如果我们想用 Java 原生的 API 从字符串 "aeb" 中匹配出所有的小写字母(a 和 b),需要编写如下代码:
12.2.1. 源码演示:Pattern.compile(), matcher.find(), matcher.group()
假设我们没有 ReUtil,我们想实现这个简单的需求,需要记住并使用两个核心类:java.util.regex.Pattern 和 java.util.regex.Matcher。
文件路径:ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/utils/test/RegexUtilsTest.java
(我们创建一个新的 main 方法测试类)
1 | package org.dromara.demo.utils.test; |
12.2.2. 实战 (rag1):演示原生 API 如何匹配多个结果
运行 main 方法,控制台输出:
1 | ... INFO ... --- 1. 测试原生 Java 正则 --- |
痛点总结:我们成功匹配到了 a 和 c(E 和 1 被忽略)。但是,为了这么一个简单的功能,我们必须:
- 记忆
Pattern和Matcher两个类。 - 执行
compile()->matcher()->find()->group()四个步骤。 - 编写一个
while循环 来处理多个匹配。
这对于“只是想快速拿个结果”的场景来说,太繁琐了。
12.3. 【核心理论】正则表达式语法速成
在深入了解 RVP 提供的 RegexUtils 工具之前,我们必须先掌握其背后的语言——正则表达式。RegexUtils 只是一个强大的“扳手”,而正则表达式语法才是你需要拧的“螺母”。不掌握语法,再好的工具也无从下手。
本节内容旨在快速掌握正则表达式的核心语法,目标是能够 看懂 RVP RegexConstants 中的常量,并能 动手修改 和 编写 常见的业务正则。
12.3.1. 必备工具:在线正则测试平台
永远不要在 IDE 里盲写正则表达式。 专业的开发者会使用在线工具来实时验证和调试。
- 首选: regex101.com (功能最强,能详细解释匹配过程、支持多种语言引擎)
- 备选: 菜鸟工具 (中文界面,对新手友好)
学习方法:将下文中的所有示例,都亲手在
regex101中输入一遍,观察匹配结果,这是最快的学习路径。
12.3.2. 基础构建块:元字符与字符集
“元字符”是正则表达式中有特殊含义的字符,它们是构成表达式的“原子”。
| 元字符 | 名称 | 含义与解释 | 示例 (正则) | 能匹配的字符串 (部分) |
|---|---|---|---|---|
. | 点 | 匹配 除换行符外 的任意 单个 字符。 | a.c | abc, axc, a_c |
| | 或 | 匹配 ` | ` 左边 或 右边的表达式。 | cat|dog |
[] | 字符集 | 匹配 [] 内的 任意一个 字符。 | gr[ae]y | gray, grey |
[a-z] | 范围 | 匹配指定范围内的任意一个字符。 | [0-9] | 0, 5, 9 |
[^] | 排除型字符集 | 匹配 非 [] 内的任意一个字符。 | [^0-9] | a, _, (空格) |
\ | 转义符 | 将紧随其后的元字符转义为普通字符。 | \d+\.\d+ | 12.34 (匹配带小数点的数字) |
12.3.3. 控制数量:量词 (贪婪 vs. 懒惰)
“量词”紧跟在元字符或分组之后,用于指定其出现的次数。
| 量词 | 名称 | 含义 (默认贪婪模式) | 示例 (正则) | 能匹配的字符串 (部分) |
|---|---|---|---|---|
* | 星号 | 匹配 0 次或多次 | go*gle | ggle, google, gooooogle |
+ | 加号 | 匹配 1 次或多次 | go+gle | google, gooooogle |
? | 问号 | 匹配 0 次或 1 次 | colou?r | color, colour |
{n} | n 次 | 精确匹配 n 次 | \d{3} | 123, 987 |
{n,m} | n 到 m 次 | 匹配 至少 n 次,至多 m 次 | \d{2,4} | 12, 123, 1234 |
{n,} | 至少 n 次 | 匹配 至少 n 次 | \d{2,} | 12, 12345 |
核心概念:贪婪 (Greedy) vs. 懒惰 (Lazy) 模式 默认情况下,量词 *, +, {} 都是 贪婪的,它们会尽可能多地匹配符合规则的字符。
贪婪:a[a-z]*c 匹配 “a bcdefg c”
懒惰:在量词后加 ? 可变为懒惰模式,即 尽可能少地 匹配。
懒惰:a[a-z]*?c 匹配 “a b c”(在 abcdefgc 中)
12.3.4. 常用简写与位置断言
为了提高编写效率,正则表达式提供了一些常用字符集的简写。
| 简写 | 等价于 | 含义 |
|---|---|---|
\d | [0-9] | 匹配任意 数字 |
\D | [^0-9] | 匹配任意 非数字 |
\w | [a-zA-Z0-9_] | 匹配任意 字母、数字、下划线 (word character) |
\W | [^a-zA-Z0-9_] | 匹配任意 非 字母、数字、下划线 |
\s | [ \t\n\r\f] | 匹配任意 空白符 (space) |
\S | [^ \t\n\r\f] | 匹配任意 非空白符 |
“位置断言”不匹配任何字符,而是匹配一个 位置。
| 元字符 | 名称 | 含义 |
|---|---|---|
^ | 开头 | 匹配字符串的 开始位置。^a 匹配 "abc",不匹配 "bac"。 |
$ | 结尾 | 匹配字符串的 结束位置。c$ 匹配 "abc",不匹配 "cba"。 |
关键用法:
^和$结合使用,如^\d+$,就将匹配模式从“包含数字”变成了“必须完全由数字组成”。
12.4. 【核心理论】分组与查找(实战)
这是正则表达式中最重要、但也最易混淆的部分。ReUtil.get(..., 1) 和 ReUtil.replaceAll(..., "$1") 完全依赖 于“分组”。
12.4.1. 捕获分组 ():group(0) vs group(1)
() (小括号) 的作用是“捕获分组”。它将括号内的匹配结果单独“抓取”出来,存入内存中。
- Group 0:永远代表 整个正则表达式匹配到的全部内容。
- Group 1:代表 第 1 个 小括号
()捕获的内容。 - Group 2:代表 第 2 个 小括号
()捕获的内容。
实战 (rag2 模拟):我们来实战原生 API,看它如何处理分组。
1 | // 位于 RegexUtilsTest.java |
运行 main 方法,控制台输出:
1 | ... INFO --- 2. 测试分组 (Grouping) --- |
分析:
group(0)拿到了aB和cD。group(1)拿到了a和c。group(2)拿到了B和D。- RVP
RegexUtils.extractFromString默认拿group(1),就是拿a和c。
12.4.2. 非捕获分组 (?:...):提升性能
有时候,我们使用 () 只是为了 提高优先级(例如 a(b|c),匹配 ab 或 ac),并 不关心 (b|c) 这个分组的结果。
a(b|c):会创建group(1)来存储b或c,消耗内存。a(?:b|c):?:告诉正则引擎:“这只是个普通括号,不要 为它创建捕获组”。group(1)将不存在。
在编写复杂正则时,对不需要捕获的 () 使用 (?:...) 是提升匹配性能的好习惯。
12.5. 【ReUtil 实战精解】告别繁琐,拥抱便捷
掌握了前面的语法“内功”后,我们再来看 Hutool ReUtil (即 RVP RegexUtils 的父类) 提供的“招式”。它将原生 Java API 的 Pattern 编译、Matcher 创建、循环查找、结果提取等繁琐步骤,全部封装进了简单易用的静态方法中。
12.5.1. 核心提取 API:get() 和 findAll()
这类 API 用于从文本中 获取 匹配的内容,是日常使用频率最高的。
| 方法签名 | 功能解释 |
|---|---|
get(regex, content, groupIndex) | 从 content 中查找 第一个 匹配 regex 的子串,并返回指定 groupIndex 的内容。 |
findAll(regex, content, groupIndex, list) | 从 content 中查找 所有 匹配 regex 的子串,将每个匹配的 groupIndex 内容存入 list 中。 |
1 | // 测试文本和正则 |
解读:get() 和 findAll() 的 groupIndex 参数,完美对应了我们 12.4.5 节学习的分组概念。0 代表完整匹配,1 代表第一个 (),以此类推。
12.5.2. 替换与删除 API:replaceAll() 和 delAll()
这类 API 用于对文本进行修改。
| 方法签名 | 功能解释 |
|---|---|
replaceAll(content, regex, template) | 将 content 中所有匹配 regex 的部分,替换为 template 字符串。模板中可用 $1, $2 引用捕获分组。 |
delAll(regex, content) | 删除 content 中所有匹配 regex 的部分。它本质上是 replaceAll(content, regex, "") 的快捷方式。 |
1 | // 场景:脱敏用户手机号,将中间四位替换为 **** |
解读:replaceAll() 的强大之处在于 template 参数,它利用 $n 语法,让我们能够灵活地重组和格式化匹配到的内容。
12.5.3. 判断与预处理 API
| 方法签名 | 功能解释 |
|---|---|
isMatch(regex, content) | 判断 整个 content 字符串是否 从头到尾 完全匹配 regex。对应原生 Matcher.matches()。 |
contains(regex, content) | 判断 content 字符串中是否 包含 能匹配 regex 的 子串。对应原生 Matcher.find()。 |
escape(content) | 转义。将 content 中所有正则元字符 (如 *, +, ?) 前面加上 \,使其变为普通字符。 |
1 | String text = "我的手机号是 13812345678"; |
解读:isMatch 和 contains 是最容易混淆的 API,务必记住它们的区别:isMatch 是“等于”,contains 是“包含”。而 escape 在处理用户输入作为正则表达式一部分时,是保证安全和正确的关键步骤。
12.6. 【RVP 增强】 extractFromString() 源码与实战
我们已经知道 RegexUtils 继承了 ReUtil,但它也增加了一个 RVP 独有的方法:extractFromString()。
【二开思考】:为什么 ReUtil.get() 还不够用?
ReUtil.get(regex, content, groupIndex) 有两个“陷阱”:
null陷阱:如果正则表达式 没有匹配到 任何内容,ReUtil.get()会返回null。Exception陷阱:如果你请求groupIndex = 1,但你的regex中 没有写()捕获分组,ReUtil.get()会 直接抛出IndexOutOfBoundsException: No group 1异常,导致程序崩溃。
RVP RegexUtils.extractFromString() 的设计目的,就是为了“容错”——它是一个更健壮、更安全的 ReUtil.get(..., 1)。
12.6.1. extractFromString() 源码解析
文件路径:ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/regex/RegexUtils.java
1 | /** |
分析:extractFromString 是一个“容错型”的 ReUtil.get(..., 1)。它假设你的目的 永远是获取“分组 1”,并且不希望程序因为“没写分组”或“没匹配到”而崩溃,于是提供了一个 defaultInput 兜底。
12.6.2. extractFromString() 实战
我们在 RegexUtilsTest.java 中添加 testRvpEnhancement 方法来验证这一点:
1 | // 位于 RegexUtilsTest.java |
结论:
- 场景 1:
ReUtil.get(..., 1)成功捕获 “a”,返回 “a”。 - 场景 2:
ReUtil.get(..., 1)抛出IndexOutOfBoundsException,被catch块捕获,返回 “我是默认值”。 - 场景 3:
ReUtil.get(..., 1)返回null,被三元表达式str == null捕获,返回 “我是默认值”。
extractFromString 是一个非常健壮的“安全提取器”
12.7. 本章总结
在本章中,我们完整地学习了 RegexUtils(ReUtil)这一强大的文本处理工具。我们没有“记流水账”,而是从“痛点”出发,深入了其设计哲学与核心用法。
痛点与价值:我们首先体验了 Java 原生
Pattern和MatcherAPI 的繁琐(4 个步骤 + 循环),从而理解了 HutoolReUtil(RegexUtils的父类)的核心价值——将所有繁琐步骤封装为简洁的静态方法。核心语法:我们快速掌握了正则表达式的必备语法,包括 元字符(
.,[],\d,\w)、量词(*,+,?)、位置(^,$)以及 贪婪/懒惰 (.*?)。分组(核心中的核心):我们深刻理解了“捕获分组
()”的意义。group(0):代表 完整的匹配。group(1):代表 第一个()捕获的内容。
ReUtil实战:我们掌握了ReUtil的核心 API:- 提取:
get(..., group)(获取第一个)和findAll(..., group)(获取所有)。 - 替换:
replaceAll(..., "$1"),利用$1,$2模板引用捕获分组。 - 判断:
isMatch()(完全匹配)和contains()(包含子串)的 重要区别。
- 提取:
RVP 增强:RVP
RegexUtils.extractFromString()是一个**“容错型”的ReUtil.get(..., 1)封装,它通过try-catch和null判断,确保在“无分组”或“未匹配”时也能安全返回一个 默认值**。我们本章学习的RegexUtils/ReUtil是为了 文本处理(提取、替换)。在 RVPcommon-core中,还存在 另一套 基于正则的工具体系,它专门用于 数据校验。
在下一章中,我们将深入 RVP 独创的“Constants -> Factory -> Validator”三层校验器架构。









