在一次日常版本迭代中,测试同学反馈了一个意料之外的bug:一个名为 source 的来源字段,前端传入仅包含空格的字符串后,后端竟然校验通过并成功更新了数据。测试的建议很明确:这个字段需要做非空校验。
我第一反应是疑惑,因为在我的实体类属性上,明明已经添加了校验注解:
@NotEmpty(message = "source must not be empty")
private String source;
在我的认知里,@NotEmpty 的校验逻辑应该和常用的 StringUtils.isEmpty 工具方法是一致的。然而,当我带着疑问去查看 isEmpty 的源码时,瞬间明白了问题所在:
public static boolean isEmpty(CharSequence cs) {
return cs == null || cs.length() == 0;
}
原来,StringUtils.isEmpty 的判断逻辑是:字符串为 null 或者 长度为 0。如果一个字符串只包含空格(例如 " "),它的 length() 是大于 0 的,因此 isEmpty 会返回 false。这意味着,@NotEmpty 注解无法拦截仅包含空白字符的字符串,这正是测试同学提到的bug根源。
复现场景与验证
我们可以通过一个简单的Demo来复现这个问题:
String source = " ";
System.out.println(StringUtils.isEmpty(source));
//输出长度
System.out.println(source.length());
运行结果如下:
false
2
传入一个由两个空格组成的字符串,isEmpty 的校验结果是 false,字符串长度是 2。
核心原因在于:isEmpty 校验的是字符串不能为 null 且长度必须为 0。空白字符串的长度大于 0,因此无法被此规则捕获。
正确的校验方式:使用 isBlank
当我们需要校验字符串是否为“空”(即排除 null、长度为零以及仅含空白字符的情况)时,正确的工具是 StringUtils.isBlank。这个方法用于检查字符串是否为空白,其定义涵盖了 null、长度为 0,或仅包含空白字符(如空格、制表符等)的情况。
使用 isBlank 来测试:
String source = " ";
System.out.println(StringUtils.isBlank(source));
运行结果为 true,成功识别出空白字符串。
我们来剖析一下 isBlank 的源码实现,就能更清楚地理解其原理:
public static boolean isBlank(CharSequence cs) {
int strLen = length(cs);
if (strLen == 0) {
return true;
} else {
for(int i = 0; i < strLen; ++i) {
if (!Character.isWhitespace(cs.charAt(i))) {
return false;
}
}
return true;
}
}
关键在于源码中的 Character.isWhitespace(cs.charAt(i)) 这一行。它会遍历字符串的每一个字符,检查其是否为空白字符。只有当所有字符都是空白字符时,才会返回 true。这便是 isBlank 能识别空白字符串的“真相”。
因此,在日常的后端开发中进行字段校验时,需要根据业务逻辑仔细选择 isEmpty 还是 isBlank,以避免出现类似的校验漏洞。
@NotNull、@NotEmpty 与 @NotBlank 的区别详解
在Java Bean Validation中,除了@NotEmpty,我们还会常用到@NotNull和@NotBlank。这三个注解看似相似,但校验的严格程度各有不同,理解它们的区别至关重要:
@NotNull:校验对象引用不能为 null,但允许其为空内容。例如,一个 String 变量可以是空字符串 "",一个 Collection 可以是空集合 []。
@NotEmpty:校验对象不能为 null,且其内容长度/大小必须大于0。对于 String,意味着不能是 "";对于 Collection、Map、数组,意味着不能是空的。但它不校验字符串内部的空白字符。
@NotBlank:专用于 CharSequence (如 String)。它校验字符串不能为 null,并且修剪 (trim) 掉首尾空白字符后的长度必须大于0。这意味着它能够有效拒绝 null、"" 以及 " " 这类仅包含空白字符的字符串。它正是 StringUtils.isBlank 的注解版实现。
总结来说,对于字符串字段的非空校验,如果业务上要求字符串必须包含可见字符(即不能是纯空格),那么 @NotBlank 才是正确的选择,而不是 @NotEmpty。这是一个在Spring Boot项目开发中容易被忽略但非常重要的细节。
希望通过这个踩坑经历,能帮助大家在未来的开发中更准确地使用校验注解,写出更健壮的代码。如果你在开发中也遇到过类似有趣的细节问题,欢迎到技术社区交流讨论。