第六章. common-core 工具类(四):StringUtils 聚合与增强

第六章. common-core 工具类(四):StringUtils 聚合与增强

摘要:本章我们将深入 StringUtils。RVP 的 StringUtils 并非从零开始,而是继承了 Apache Commons Lang3,并聚合了 Hutool StrUtil 的功能。我们将重点学习 RVP 在此基础上 增强 的核心方法,特别是与集合转换、路径匹配相关的功能。

在上一章中,我们深入剖析了 StreamUtils,这是一个与 Spring 无关的纯 Java 集合处理工具。我们掌握了它在 集合过滤、转换、分组和合并 中的“空安全”封装,并直接使用了 Lambda 表达式和方法引用来实战。

现在,我们来看 utils 包下的另一个“元老级”工具类——StringUtilsStringUtils 是任何 Java 项目中都不可或缺的,RVP 也不例外。但 RVP 的 StringUtils 并不是“重复造轮子”,而是巧妙地站在了巨人的肩膀上。

本章学习路径

我们将按照“知其然,知其所以然”的路径,先理解 RVP 的设计哲学,再实战其核心增强功能:

工具类(四):StringUtils 聚合与增强


6.1. RVP 的“聚合”哲学:为何继承

我们首先打开 StringUtils 的源码:

文件路径ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/StringUtils.java

看到的第一行类定义就揭示了 RVP 的设计哲学:

1
2
3
public class StringUtils extends org.apache.commons.lang3.StringUtils {
// ...
}

RVP 的 StringUtils 继承org.apache.commons.lang3.StringUtils。这意味着 RVP 的 StringUtils 自动拥有了 Apache Commons Lang3 库中所有强大的字符串处理方法。

6.1.1. Apache Commons Lang3:久经考验的基石

Apache Commons Lang3 是 Java 社区久经考验、最著名、最稳定的工具库之一。RVP 通过 extends 它,获得了诸如:

  • isBlank(String str) / isNotBlank(String str):空值判断(null""" " 都算空)。
  • isEmpty(String str) / isNotEmpty(String str):空值判断(null"" 算空," " 不算)。
  • join(Iterable<?> iterable, String separator):集合拼接。
  • abbreviate(String str, int maxWidth):字符串缩略(例如 ...)。
  • …以及上百个其他方法。

RVP 的开发者 不需要 重新实现这些基础功能。

6.1.2. Hutool StrUtil:Hutool 的功能补充

RVP 不仅继承了 Apache 的工具,还在方法内部大量“委托”了 Hutool 的 StrUtil

例如,我们查看 RVP StringUtilsisEmpty 方法:

1
2
3
4
5
// 位于 RVP 的 StringUtils.java
public static boolean isEmpty(String str) {
// 实际实现委托给了 Hutool 的 StrUtil.isEmpty()
return StrUtil.isEmpty(str);
}

RVP StringUtils 内部封装了 StrUtil,提供了 formatblankToDefault 等更现代、更便捷的方法。

6.1.3. RVP 的职责:聚合、增强与(isMatch

所以,RVP StringUtils 的角色定位非常清晰:

角色实现方式举例
基础能力extends org.apache.commons.lang3.StringUtilsisBlank, join, abbreviate
现代补充内部调用 cn.hutool.core.util.StrUtilisEmpty, format, toCamelCase
RVP 增强框架 自己实现 的独有逻辑isMatch, str2List, splitTo

这种“聚合”设计的好处是,作为开发者,我们 只需要 import org.dromara.common.core.utils.StringUtils 这一个类,就能同时使用来自 Apache、Hutool 和 RVP 三方的字符串功能,极大提升了便利性。


6.2. 测试准备:创建 StringUtilsTest (main 方法)

StringUtilsStreamUtils 一样,是纯 Java 工具类,不依赖 Spring 容器。因此,我们同样使用 main 方法来测试它。

6.2.1. 创建 utils.test.StringUtilsTest.java

我们在上一章创建的 utils.test 包中,再创建一个新的测试类:

文件路径ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/utils/test/StringUtilsTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.dromara.demo.utils.test;

// 导入我们 RVP 的 StringUtils
import org.dromara.common.core.utils.StringUtils;
import cn.hutool.log.Log;
import cn.hutool.log.LogFactory;

/**
* StringUtils 工具类实战测试
* (这是一个纯 Java 测试,无需启动 Spring)
*/
public class StringUtilsTest {

private static final Log console = LogFactory.get();

// psvm 快捷键
public static void main(String[] args) {
console.info("StringUtils 测试开始...");

// ... 我们将在这里调用测试方法 ...
}

// ... 后续测试方法 ...
}

6.2.2. 编写 main 方法与分屏技巧

和上一章一样,为了方便我们一边看 StringUtils.java 的源码,一边写 StringUtilsTest.java 的测试,我们可以使用 IDEA 的“分屏”功能:

  1. StringUtils.java 的文件标签页上右键。
  2. 选择 Split Right(向右分屏)。
  3. 这样左边就是我们的 StringUtilsTest.java 测试类,右边就是 StringUtils.java 源码,方便我们随时查阅。

6.3. 基础增强:空值处理与格式化 (blankToDefault, format)

我们从最简单、最高频的两个增强方法开始。

6.3.1. 痛点:if (str == null) { str = "default"; }

在业务中,我们经常需要给一个可能为 null 的字符串赋予默认值,原生写法很啰嗦:

1
2
3
4
5
// 痛点代码 (Bad Practice)
String myVar = getMyVar(); // myVar 可能是 null
if (StringUtils.isBlank(myVar)) { // 必须先判断
myVar = "default-value"; // 再赋值
}

6.3.2. RVP 封装 (blankToDefault)

RVP StringUtils(内部委托 StrUtil)提供了 blankToDefault,一行代码搞定:

1
2
3
4
5
// 位于 RVP 的 StringUtils.java
public static String blankToDefault(String str, String defaultValue) {
// 实际委托给了 Hutool
return StrUtil.blankToDefault(str, defaultValue);
}

它会判断 str 是否为 null""" ",如果是,就返回 defaultValue,否则返回 str 本身。

6.3.3. 字符串格式化 (format)

另一个痛点是字符串拼接:

  • 原生"Hello, " + name + "! Welcome to " + location + "."(可读性差,有性能问题)
  • RVP 封装 (format)StringUtils.format("Hello, {}! Welcome to {}.", name, location)

RVP 的 format(委托 StrUtil.format)使用了 SLF4J 日志框架的 {} 占位符风格,比 String.format()%s 风格)更简洁易读。我们在 StringUtilsTest.java 中创建 testSimple 方法来测试这两个功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ...
public class StringUtilsTest {

private static final Log console = LogFactory.get();

public static void main(String[] args) {
console.info("StringUtils 测试开始...");
testSimple();
}

/**
* 测试 1:基础工具
*/
public static void testSimple() {
console.info("--- 1. 测试 blankToDefault ---");

String s1 = StringUtils.blankToDefault("123", "345");
console.info("【s1 (有值)】: {}", s1); // 结果 123

String s2 = StringUtils.blankToDefault(null, "345");
console.info("【s2 (null)】: {}", s2); // 结果 345

String s3 = StringUtils.blankToDefault(" ", "345");
console.info("【s3 (空格)】: {}", s3); // 结果 345

console.info("--- 2. 测试 isEmpty/isNotEmpty (Hutool) ---");
console.info("【isEmpty(' ')】: {}", StringUtils.isEmpty(" ")); // false
console.info("【isBlank(' ')】: {}", StringUtils.isBlank(" ")); // true (继承自 Apache)

console.info("--- 3. 测试 format ---");
String formatStr = StringUtils.format("Hi, {}. Your order ID is {}.", "Lion", 10086);
console.info("【format 结果】: {}", formatStr);

console.info("--- 4. 测试 trim (去首尾空格) ---");
String trimStr = StringUtils.trim(" hello world ");
console.info("【trim 结果】: '{}'", trimStr); // 'hello world'
}

// ...
}

运行 main 方法,控制台输出

1
2
3
4
5
6
7
8
9
10
11
12
... INFO ... StringUtils 测试开始...
... INFO ... --- 1. 测试 blankToDefault ---
... INFO ... 【s1 (有值)】: 123
... INFO ... 【s2 (null)】: 345
... INFO ... 【s3 (空格)】: 345
... INFO ... --- 2. 测试 isEmpty/isNotEmpty (Hutool) ---
... INFO ... 【isEmpty(' ')】: false
... INFO ... 【isBlank(' ')】: true
... INFO ... --- 3. 测试 format ---
... INFO ... 【format 结果】: Hi, Lion. Your order ID is 10086.
... INFO ... --- 4. 测试 trim (去首尾空格) ---
... INFO ... 【trim 结果】: 'hello world'

分析

  • blankToDefault 成功处理了 null 和空格。
  • isEmptyisBlank 的区别必须牢记:isEmpty 不认为空格是空,isBlank 认为空格是空。RVP 两者都支持(一个来自 Hutool,一个继承自 Apache)。
  • format 成功替换了占位符。

6.4. 核心增强(一):字符串与集合的“双向”转换

在业务中,我们经常需要将数据库中存储的 ",1,2,3," 这种字符串,与 List<String>Set<String> 进行相互转换。RVP 在这方面提供了强大的封装。

6.4.1. str2Set (字符串转 Set,带去重)

str2Set 会自动按分隔符(默认为 ,)切割字符串,并存入一个 Set天然具有去重属性

6.4.2. str2List (字符串转 List,带 filterBlanktrim 选项)

str2List 是 RVP 源码中大量使用的一个增强方法。它比 str.split(",") 健壮得多,它提供了两个关键的布尔参数:

  • filterBlank (过滤空白):如果为 true"1,,2" 这种字符串中间的 空字符串 会被自动过滤掉。
  • trim (去除首尾空白):如果为 true" 1 , 2 " 会被处理成 "1""2",而不是 " 1 "" 2 "

6.4.3. [重点] splitTo (自定义转换 Function)

splitTostr2List 的“升级版”。str2List 只能返回 List<String>,而 splitTo 允许你传入一个 Function (转换函数),将切割后的字符串 立即转换为你想要的任何类型,例如 List<Integer>List<Long>

【实战闭环】
我们在 StringUtilsTest.java 中创建 testSplit 方法来测试这些功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// ...
import cn.hutool.core.convert.Convert; // 导入 Hutool 的类型转换器
import java.util.List;
import java.util.Set;
// ...
public class StringUtilsTest {

// ... main 和 testSimple ...
public static void main(String[] args) {
// ...
// testSimple();
testSplit();
}

/**
* 测试 2:字符串与集合转换
*/
public static void testSplit() {
console.info("--- 5. 测试 str2Set (自动去重) ---");
String str1 = "1,2,2,3,4,4";
Set<String> set = StringUtils.str2Set(str1, ",");
console.info("【str2Set 结果】: {}", set); // [1, 2, 3, 4]

console.info("--- 6. 测试 str2List (健壮切割) ---");
String str2 = " 1 , 2 ,, 3 ,"; // 包含首尾空格、空字符串

// 场景 1:不处理空白
List<String> list1 = StringUtils.str2List(str2, ",", false, false);
console.info("【str2List (不过滤不trim)】: {}", list1);

// 场景 2:RVP 推荐用法 (过滤空、去除首尾空白)
List<String> list2 = StringUtils.str2List(str2, ",", true, true);
console.info("【str2List (过滤且trim)】: {}", list2);

console.info("--- 7. 测试 splitTo (自定义转换) ---");
String str3 = "100,200,300";
// 目标:直接转为 List<Integer>
List<Integer> intList = StringUtils.splitTo(str3, ",", Convert::toInt);
console.info("【splitTo<Integer> 结果】: {}", intList);
}
// ...
}

运行 main 方法,控制台输出

1
2
3
4
5
6
7
... INFO ... --- 5. 测试 str2Set (自动去重) ---
... INFO ... 【str2Set 结果】: [1, 2, 3, 4]
... INFO ... --- 6. 测试 str2List (健壮切割) ---
... INFO ... 【str2List (不过滤不trim)】: [ 1 , 2 , , 3 , ]
... INFO ... 【str2List (过滤且trim)】: [1, 2, 3]
... INFO ... --- 7. 测试 splitTo (自定义转换) ---
... INFO ... 【splitTo<Integer> 结果】: [100, 200, 300]

分析str2ListfilterBlank=truetrim=true 参数非常实用。splitTo 结合 Convert::toInt(Hutool 的类型转换方法引用)可以一步到位地实现 StringList<Integer>


6.5. 核心增强(二):驼峰与下划线 (toUnderScoreCase, toCamelCase)

在 Java 中我们使用驼峰命名(userName),在数据库中我们常用下划线命名(user_name)。StringUtils(委托 StrUtil)提供了它们之间的高效转换。

方法示例(输入)示例(输出)用途
toUnderScoreCaseuserNameuser_name驼峰转下划线
toCamelCaseuser_nameuserName小驼峰(首字母小写)
convertToCamelCaseuser_nameUserName大驼峰(首字母大写)
我们在 StringUtilsTest.java 中创建 testCamelCase 方法:

这些方法非常简单我们就不再过多演示了


6.6. [RVP 核心] 路径匹配:isMatchmatches

这是 StringUtils最核心 的 RVP 独有增强功能。它在框架的 权限拦截、URL 放行 等场景中扮演着至关重要的角色。

6.6.1. 核心原理:Spring AntPathMatcher (?, *, **)

RVP 的 isMatch 方法内部封装了 Spring 框架大名鼎鼎的 AntPathMatcher(Ant 路径匹配器)。它使用一套简洁的通配符规则:

  • ?:匹配 单个 字符。
    • 规则:/user/find?
    • 匹配:/user/find1 (✔), /user/findX (✔)
    • 不匹配:/user/find (✘), /user/find11 (✘)
  • *:匹配 一层路径 内的任意字符串(0 或多个字符)。
    • 规则:/user/*/info
    • 匹配:/user/123/info (✔), /user/admin/info (✔)
    • 不匹配:/user/info (✘), /user/123/abc/info (✘, 不可跨层)
  • **:匹配 任意多层路径(0 或多层),注意,在新版本 Spring3 中 ** 只能放置于路径最后方
    • 规则:/user/**
    • 匹配:/user/info (✔), /user/123/info (✔), /user/123/abc/info (✔)

6.6.2. [实战] isMatch (单规则匹配)

我们来实战验证 AntPathMatcher 的规则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// ...
public class StringUtilsTest {

// ... main 和 testCamelCase ...
public static void main(String[] args) {
// ...
// testCamelCase();
testMatch();
}

/**
* 测试 4:Ant 路径匹配 (RVP 核心)
*/
public static void testMatch() {
console.info("--- 10. 测试 isMatch (单规则) ---");

// 1. 测试 ? (单个字符)
String p1 = "/sp/find?";
console.info("【{}】匹配【{}】: {}", p1, "/sp/find1", StringUtils.isMatch(p1, "/sp/find1")); // true
console.info("【{}】匹配【{}】: {}", p1, "/sp/find11", StringUtils.isMatch(p1, "/sp/find11")); // false

// 2. 测试 * (一层路径)
String p2 = "/sp/*/abc";
console.info("【{}】匹配【{}】: {}", p2, "/sp/cd/abc", StringUtils.isMatch(p2, "/sp/cd/abc")); // true
console.info("【{}】匹配【{}】: {}", p2, "/sp/a/b/abc", StringUtils.isMatch(p2, "/sp/a/b/abc")); // false (不能跨层)

// 3. 测试 ** (任意层路径)
String p3 = "/sp/**/abc";
console.info("【{}】匹配【{}】: {}", p3, "/sp/abc", StringUtils.isMatch(p3, "/sp/abc")); // true (匹配 0 层)
console.info("【{}】匹配【{}】: {}", p3, "/sp/a/b/abc", StringUtils.isMatch(p3, "/sp/a/b/abc")); // true (匹配多层)
}
// ...
}

运行 main 方法,控制台输出

1
2
3
4
5
6
7
... INFO ... --- 10. 测试 isMatch (单规则) ---
... INFO ... 【/sp/find?】匹配【/sp/find1】: true
... INFO ... 【/sp/find?】匹配【/sp/find11】: false
... INFO ... 【/sp/*/abc】匹配【/sp/cd/abc】: true
... INFO ... 【/sp/*/abc】匹配【/sp/a/b/abc】: false
... INFO ... 【/sp/**/abc】匹配【/sp/abc】: true
... INFO ... 【/sp/**/abc】匹配【/sp/a/b/abc】: true

6.6.3. [实战] matches (多规则列表匹配)

isMatch 只能判断一个规则,而 matches 允许我们传入一个 规则列表 (List),只要 URL 命中了 任意一个 规则,就返回 true。这正是 RVP 权限框架放行匿名 URL(如 /login, /captchaImage)的实现方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ...
import java.util.ArrayList; // 导入 ArrayList
// ...
public static void testMatch() {
// ... (省略 isMatch 的代码) ...

console.info("--- 11. 测试 matches (多规则) ---");

// 1. 准备规则列表
List<String> patterns = new ArrayList<>();
patterns.add("/sp/login"); // 规则1:精确匹配
patterns.add("/sp/admin/**"); // 规则2:admin 下所有

// 2. 准备 URL
String url1 = "/sp/login";
String url2 = "/sp/admin/user/list";
String url3 = "/sp/user/list"; // 未命中

// 3. 匹配
console.info("【{}】匹配列表: {}", url1, StringUtils.matches(url1, patterns)); // true
console.info("【{}】匹配列表: {}", url2, StringUtils.matches(url2, patterns)); // true
console.info("【{}】匹配列表: {}", url3, StringUtils.matches(url3, patterns)); // false
}
// ...

运行 main 方法,控制台输出

1
2
3
4
... INFO ... --- 11. 测试 matches (多规则) ---
... INFO ... 【/sp/login】匹配列表: true
... INFO ... 【/sp/admin/user/list】匹配列表: true
... INFO ... 【/sp/user/list】匹配列表: false

6.7. 其他便捷工具 (containsAnyIgnoreCase, inStringIgnoreCase, padl)

StringUtils 还提供了一些其他便捷的方法。

6.7.1. 模糊包含 (containsAnyIgnoreCase)

判断一个字符串是否 包含 列表中的任意一个子串(忽略大小写)。

  • containsAnyIgnoreCase("abc", "A", "D") -> true (因为 "abc" 包含了 "a")

6.7.2. 精确包含 (inStringIgnoreCase)

判断一个字符串是否 等于 列表中的任意一个字符串(忽略大小写)。

  • inStringIgnoreCase("abc", "ABC", "D") -> true (因为 "abc" 等于 "ABC")
  • inStringIgnoreCase("abc", "A", "D") -> false (不等于)

6.7.3. 左侧补齐 (padl)

常用于生成固定长度的编号,例如将 123 补齐为 000123。我们在 StringUtilsTest.java 中创建 testOthers 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// ...
public class StringUtilsTest {

// ... main 和 testMatch ...
public static void main(String[] args) {
// ...
// testMatch();
testOthers();
}

/**
* 测试 5:其他便捷工具
*/
public static void testOthers() {
console.info("--- 12. 测试 containsAnyIgnoreCase (模糊包含) ---");
boolean b1 = StringUtils.containsAnyIgnoreCase("HelloWorld", "world", "java");
console.info("【HelloWorld】模糊包含【world, java】: {}", b1); // true

console.info("--- 13. 测试 inStringIgnoreCase (精确等于) ---");
boolean b2 = StringUtils.inStringIgnoreCase("hello", "HELLO", "java");
console.info("【hello】精确等于【HELLO, java】: {}", b2); // true

boolean b3 = StringUtils.inStringIgnoreCase("hello", "HE", "java");
console.info("【hello】精确等于【HE, java】: {}", b3); // false

console.info("--- 14. 测试 padl (左侧补齐) ---");
// 1. 数字补齐 (默认补 '0')
String p1 = StringUtils.padl(123, 6);
console.info("【123】补齐到 6 位: {}", p1); // "000123"

// 2. 字符串补齐 (自定义补 '@')
String p2 = StringUtils.padl("abc", 5, '@');
console.info("【abc】补齐到 5 位: {}", p2); // "@@abc"

// 3. 截断 (如果原长度超过)
String p3 = StringUtils.padl("1234567", 5, '@');
console.info("【1234567】截断到 5 位: {}", p3); // "34567"
}
// ...
}

运行 main 方法,控制台输出

1
2
3
4
5
6
7
8
9
... INFO ... --- 12. 测试 containsAnyIgnoreCase (模糊包含) ---
... INFO ... 【HelloWorld】模糊包含【world, java】: true
... INFO ... --- 13. 测试 inStringIgnoreCase (精确等于) ---
... INFO ... 【hello】精确等于【HELLO, java】: true
... INFO ... 【hello】精确等于【HE, java】: false
... INFO ... --- 14. 测试 padl (左侧补齐) ---
... INFO ... 【123】补齐到 6 位: 000123
... INFO ... 【abc】补齐到 5 位: @@abc
... INFO ... 【1234567】截断到 5 位: 34567

6.8. 本章总结

在本章中,我们深入了 RVP StringUtils 的设计与实战。我们必须理解,RVP 的 StringUtils 不是一个孤立的类,而是一个“聚合器”和“增强器”。

  1. 聚合器:它通过 继承 Apache Commons Lang3内聚 Hutool StrUtil,让我们只 import 一个类,就能使用三方工具包的功能。
  2. 增强器:RVP 提供了许多独有的、高价值的增强方法,我们重点掌握了:
    • 集合/字符串转换str2List(带 trimfilterBlank)、str2Set(去重)和 splitTo(自定义转换 Function)。
    • 命名转换toCamelCase(小驼峰)和 toUnderScoreCase(下划线)。
    • 路径匹配(RVP 核心)isMatch(单规则)和 matches(多规则列表),它们是 RVP 权限体系的基石,基于 AntPathMatcher (?, *, **) 规则。

掌握 StringUtils 的这些增强功能,能让我们在处理字符串时写出更健壮、更简洁的代码。