引言

在前两篇文章中,我们分别探讨了 BeanUtilsReflectionUtils 两个强大的工具类。今天要聊的 StringUtils 可能是大家最“熟悉”的一个——毕竟每个项目都离不开字符串处理。但你可能不知道,Spring 自带的 StringUtils 藏着许多冷门却极为实用的方法,能在日常开发中帮你省下大量重复代码。
Spring 常用类深度解析系列开篇:那些我们每天都在用,却未必真正理解的类

提起字符串工具类,很多人第一时间会想到 Apache Commons Lang 的 StringUtils,或者 Guava 的 Strings。Spring 的 StringUtils 虽然名气稍逊,但它与 Spring 生态紧密结合,提供了许多专为框架场景设计的功能(如资源路径处理、区域解析、属性占位符等)。本文将深入 Spring StringUtils 的底层,挖掘那些“意想不到”的实用方法,并对比它们与流行替代方案的区别。

一、为什么要用 Spring 的 StringUtils?

因为 Spring 的 StringUtils 针对配置文件处理、国际化、Web 路径、集合转换等场景做了深度优化,这些功能在 Apache Commons 中要么没有,要么用法不同。掌握这些方法,可以让你的代码更符合 Spring 生态的习惯,也更简洁。

1.1 常见字符串操作的痛点

在实际开发中,我们经常需要:

  • 判断字符串是否为空或空白
  • 将逗号分隔的字符串转为集合
  • 解析 Locale(如 zh_CNen_US
  • 将字符串转换为数组(考虑转义和引号)
  • 处理文件路径或 URL 的拼接

原生 JDK 提供的 String 方法往往不够用,我们不得不自己写工具类或引入第三方库。Spring 的 StringUtils 正好填补了这些空白,且没有额外依赖,是 Spring Boot 项目中的天然选择。

1.2 与 Apache Commons Lang 的差异
特性 Spring StringUtils Apache Commons Lang StringUtils
包名 org.springframework.util org.apache.commons.lang3
定位 轻量级,聚焦 Spring 框架需求 功能全面,通用工具库
依赖 无额外依赖 需引入 commons-lang3
特色方法 路径处理、区域解析、集合转换、Spring 专属格式 null-safe 操作、判空、填充、替换、分割等字符串算法(Levenshtein距离等)
性能 针对常见场景优化 功能丰富,性能稳定

Spring StringUtils 不追求大而全,但在某些领域提供了更符合 Java/Spring 习惯的 API。

如果你的项目已经用了 Spring,对于常见的基础操作(判空、分割、拼接)两者都可以;对于Spring 特有的场景(如路径匹配、资源解析),Spring 版本是首选。

二、核心 API 深度解析与实战

2.1 判空与空字符串处理
isEmpty vs hasLength vs hasText

Spring 提供了三个层次的判空方法:

	public void testStringUtils() {
		String str = "";

		// 1. 判断对象是否为 null,或字符串长度为 0
		// @deprecated as of 5.3, in favor of {@link #hasLength(String)}
		// and {@link #hasText(String)} (or {@link ObjectUtils#isEmpty(Object)})
		boolean empty = StringUtils.isEmpty(str);          // null / "" → true

		// 2. 判断字符串长度是否 > 0(不为 null 且长度 > 0)
		boolean hasLength = StringUtils.hasLength(str);    // null / "" → false

		// 3. 判断是否包含非空白字符(不为 null,长度 > 0 且不全是空白)
		boolean hasText = StringUtils.hasText(str);        // "   " → false
	}

实战场景:在 Controller 层校验请求参数时,hasText 通常最合适,因为它会过滤掉空字符串和空白字符。

@PostMapping("/user")
public ResponseEntity<?> createUser(@RequestBody UserDto dto) {
    if (!StringUtils.hasText(dto.getName())) {
        return ResponseEntity.badRequest().body("姓名不能为空");
    }
    // ...
}
trimAllWhitespace 与 trimWhitespace
  • trimWhitespace:去掉首尾空白,保留内部空白。
  • trimAllWhitespace:去掉所有空白字符(包括空格、制表符、换行等)。
String s = "  Hello   World  ";
System.out.println(StringUtils.trimWhitespace(s));      // "Hello   World"
System.out.println(StringUtils.trimAllWhitespace(s));   // "HelloWorld"
2.2 集合转换:字符串 ↔ 集合

这是 Spring StringUtils 的一大亮点,能将带分隔符的字符串轻松转换为 SetListString[]

commaDelimitedListToSet

将逗号分隔的字符串转换为 LinkedHashSet(去重且保持顺序):

String tags = "java,spring,spring,redis";
Set<String> tagSet = StringUtils.commaDelimitedListToSet(tags);
// [java, spring, redis]
commaDelimitedListToStringArray

与上述类似,返回 String[] 数组,保留重复元素:

String[] array = StringUtils.commaDelimitedListToStringArray("a,b,c,c");
// ["a", "b", "c", "c"]
tokenizeToStringArray:更强大的分词器

支持自定义分隔符、忽略空白、忽略空标记(如连续分隔符导致的空字符串):

String input = "hello;world;;  spring  ";
// 分隔符为 ; 和空格,忽略空白标记,跳过空标记
String[] tokens = StringUtils.tokenizeToStringArray(
    input, "; ", true, true);
// ["hello", "world", "spring"]

参数说明:

  • trimTokens:是否去除每个 token 的前后空白
  • ignoreEmptyTokens:是否忽略空 token
    这个方法的灵活性远超简单的 split,因为它能处理多种分隔符、自动修剪空白,并智能处理连续分隔符。
2.3 路径与 URL 处理

Spring 经常处理资源路径,因此 StringUtils 内置了路径规范化方法。

applyRelativePath:根据基路径计算相对路径
String base = "/WEB-INF/classes/";
String relative = "com/example/App.class";
String full = StringUtils.applyRelativePath(base, relative);
// "/WEB-INF/classes/com/example/App.class"

注意:它只是字符串拼接,不会做文件系统校验。

cleanPath:清理路径中的冗余元素

将路径中的 ./../ 等规范化,适用于 URL 或文件路径。

String messy = "/a/./b/../c/d/";
String cleaned = StringUtils.cleanPath(messy); // "/a/c/d"
2.4 区域解析:parseLocaleString

将类似 zh_CNen_US 的字符串解析为 Locale 对象:

Locale locale = StringUtils.parseLocaleString("zh_CN");
// Locale("zh", "CN")

Locale locale2 = StringUtils.parseLocaleString("en");
// Locale("en")

// 支持变体:zh_CN_#Hans
Locale locale3 = StringUtils.parseLocaleString("zh_CN_#Hans");
// Locale("zh", "CN", "Hans")

这个功能在国际化配置中非常实用,比如从配置文件读取语言标签。

2.5 数组与字符串互转
arrayToDelimitedString:将数组拼接成字符串
String[] arr = {"a", "b", "c"};
String result = StringUtils.arrayToDelimitedString(arr, "|");
// "a|b|c"
collectionToDelimitedString:同样适用于集合
List<String> list = Arrays.asList("x", "y", "z");
String result = StringUtils.collectionToDelimitedString(list, ",", "'", "'");
// "'x','y','z'"

第三个和第四个参数分别表示前缀和后缀,常用于生成 SQL 的 IN 子句。

2.6 占位符替换:replace 系列

Spring 的 StringUtils 提供了多种替换方法:

  • replace(String inString, String oldPattern, String newPattern):全局替换
  • delete(String inString, String pattern):删除所有出现的子串
  • deleteAny(String inString, String charsToDelete):删除任意指定字符
String s = "spring spring spring";
String replaced = StringUtils.replace(s, "spring", "boot");
// "boot boot boot"

String deleted = StringUtils.delete(s, "ing");
// "spr spr spr"
2.7 文件扩展名提取

getFilenameExtensionstripFilenameExtension 是专门处理文件名的工具:

String file = "myfile.tar.gz";
String ext = StringUtils.getFilenameExtension(file);   // "gz"
String name = StringUtils.stripFilenameExtension(file); // "myfile.tar"
2.8 其他冷门但实用的方法
方法 功能 示例
countOccurrencesOf 统计子串出现次数 countOccurrencesOf("ababa", "aba") → 1(非重叠)
quote / unquote 添加/移除引号 quote("hello") → "'hello'"
capitalize / uncapitalize 首字母大小写转换 capitalize("spring") → "Spring"
getFilename 从路径中提取文件名 getFilename("/a/b/c.txt") → "c.txt"

三、源码探秘:设计思路与关键实现

3.1 判空方法的实现

isEmpty 的实现非常直接:
在这里插入图片描述
hasLength 则判断长度:
在这里插入图片描述
hasTexthasLength 基础上增加了空白字符检查:
在这里插入图片描述
其中 containsText 会循环检查每个字符是否不是空白(!Character.isWhitespace(ch))。

3.2 tokenizeToStringArray 的解析逻辑

该方法核心是一个基于索引的状态机,支持多分隔符。关键步骤:

  1. 遍历输入字符串,定位 token 的起始和结束位置。
  2. 根据 trimTokens 决定是否修剪 token 两端空白。
  3. 根据 ignoreEmptyTokens 决定是否丢弃空 token。
  4. 使用 StringcharAt 逐个比较分隔符,效率较高。

在这里插入图片描述

性能考量
tokenizeToStringArray 内部使用 String.indexOfString.substring,时间复杂度 O(n),适合中小规模字符串。
所有方法均没有缓存,每次调用都会重新计算,但对于非极端高频场景足够。
如果需要进行海量字符串操作,建议使用更底层的 StringBuilder 或第三方库如 fastutils

3.3 parseLocaleString 的解析细节

该方法严格按照 Java 的 Locale 规范解析:

  • 下划线 _ 分割语言、国家、变体
  • 可选 # 后缀表示变体扩展
  • 使用 Locale.Builder 构造(JDK 7+)

在这里插入图片描述

四、实战案例:一个简单的配置解析器

结合以上方法,我们可以快速实现一个配置解析器,将类似 key=value 的文本转为 Properties,并支持逗号分隔的多值配置。

public class ConfigParser {
    public static Properties parse(String configText) {
        Properties props = new Properties();
        String[] lines = StringUtils.tokenizeToStringArray(configText, "\n", true, true);
        for (String line : lines) {
            if (!StringUtils.hasText(line) || line.trim().startsWith("#")) {
                continue; // 跳过空行和注释
            }
            int eqIndex = line.indexOf('=');
            if (eqIndex == -1) continue;
            String key = line.substring(0, eqIndex).trim();
            String value = line.substring(eqIndex + 1).trim();
            
            // 如果 value 是逗号分隔,转换为 Set
            if (value.contains(",")) {
                Set<String> set = StringUtils.commaDelimitedListToSet(value);
                props.put(key, set);
            } else {
                props.put(key, value);
            }
        }
        return props;
    }
    
    public static void main(String[] args) {
        String cfg = "name=Spring\n" +
                     "tags=java,spring,boot\n" +
                     "# 注释行\n" +
                     "   version = 5.3   ";
        Properties props = parse(cfg);
        System.out.println(props.get("name"));    // Spring
        System.out.println(props.get("tags"));    // [java, spring, boot]
        System.out.println(props.get("version")); // 5.3
    }
}

这个例子展示了如何利用 tokenizeToStringArray 分割行,commaDelimitedListToSet 处理多值,以及 hasText 过滤空白行。

五、与 Apache Commons Lang StringUtils 的对比总结

场景 Spring StringUtils Apache Commons Lang StringUtils
判空/空白 hasText isNotBlank
集合转换 内置 commaDelimitedListToSet 需自行实现或借助其他工具
路径处理 cleanPath, applyRelativePath
区域解析 parseLocaleString
字符串算法 丰富(Levenshtein、JaroWinkler 等)
随机字符串 randomAlphanumeric
性能 轻量级,适合 Spring 应用 功能全面,但额外引入依赖

六、源码亮点:Spring 的设计哲学

阅读 StringUtils 源码,你会发现几个值得学习的点:

  • 极致的 null-safety
    几乎每个方法都会先检查输入是否为 null,返回合理的默认值(通常是 null 或空数组)。例如 commaDelimitedListToSet 遇到 null 返回空 Set,而不是抛出 NPE。
  • 使用 LinkedHashSet 保留顺序
    commaDelimitedListToSet 中,返回的是 LinkedHashSet,既去重又保留原始顺序,这在配置解析场景非常实用。
  • 高效的数组扩容策略
    delimitedListToStringArray 内部,使用了类似 ArrayList 的动态数组扩容算法(初始容量 10,每次按需扩容),避免频繁创建新数组。
  • 不依赖 JDK 高版本特性
    Spring 为了保持兼容性,StringUtils 中的方法都是基于 JDK 8 的 API 实现的,没有使用 String#isBlank(JDK 11 引入)等新特性。这也给了我们一个启示:工具类要保守,保持最低依赖。

七、总结

Spring 的 StringUtils 绝不是鸡肋,它专注于解决 Spring 生态中常见的字符串处理需求,尤其在路径、区域、配置解析等方面表现出色。虽然它在通用性上不如 Apache Commons,但在 Spring 项目中,它是最顺手、最轻量的选择。本文重点介绍了:

  • 判空方法的层次(isEmptyhasLengthhasText
  • 强大的分词与集合转换方法(tokenizeToStringArraycommaDelimitedListToSet
  • 路径处理与区域解析等 Spring 特色功能
  • 一些容易被忽略但实用的小工具(countOccurrencesOfquotegetFilenameExtension

建议在日常开发中记住以下几点:

  • 遇到逗号分隔的配置,优先想到 commaDelimitedListToSet
  • 处理文件路径时,用 cleanPath 规范路径。
  • 国际化场景,用 parseLocaleStringtoLanguageTag
  • 不要重复造轮子,Spring 已经帮你写好了。

下一篇文章,我们将继续 “Spring 常用类深度剖析(工具篇 04):CollectionUtils 与 Stream API 的对比与融合”,看看 Spring 在集合操作上又提供了哪些惊喜。敬请期待!


思考题StringUtils.trimAllWhitespaceString.replaceAll("\\s+", "") 有何异同?在大量字符串处理时,哪个性能更好?(欢迎在评论区留下你的答案)

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐