批量将"".equals()替换成StringUtils.isEmpty()
甲方前一段时间使用360代码卫士对我们项目进行了“代码审计”,其中一个问题是“使用equals()来判断字符串是否为空”。由于甲方只关心问题数量,不关心问题性质(甚至连算不算问题都不在乎),我们只能硬着头皮去按甲方要求修改。
修改的必要性
首先看一下equals()和length()的实现:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
public int length() {
return value.length;
}
很明显,equals也是先比较字符串长度,而且空串长度是0,即使进入while处也跑不起来,因此在业务系统这种意义上不会有任何性能差异。另一方面,我们进行了简单的性能测试(循环一千万次),结果同样表明完全没有必要改。
施工
为了交差,我们不得不去做没有必要的事情,所以我们需要考虑一种代价最小、没有副作用的改法。考虑到业务系统并不差调用一个函数的时间,我们干脆直接使用StringUtils工具类,既增加代码可读性,也便于装逼。代码通过awk和bash进行自动处理。
检查公共函数
先确认import org.apache.commons.lang.StringUtils是否能正确导入。如果不能,那么需要导入jar包(commons-lang.jar)。
equals.awk
# 注意:mac系统要使用gawk
BEGIN {
imported = 0;
last_import_line = -1;
bufflen = 0;
changed = 0;
}
# 修正第一类equals判断问题 "".equals(xxx),提取括号里的内容,pos需要是恰好为括号的位置,如果失败则返回-1。
function extract1(str, pos, i, level, endpos, result, len, arr) {
level = 0;
endpos = -1;
result = "";
len = length(str);
split(str, arr, "");
for (i=pos; i<=length(arr); i++) {
result = result arr[i];
if (arr[i]=="(") {
level++;
} else if (arr[i]==")") {
level--;
if (level == 0) {
endpos=i;
break;
}
}
}
if (endpos == -1) {
return -1;
} else {
return result;
}
}
# 修正第一类equals判断问题: "".equals(xxx)
function fix_equals1(str, result, m, txt, tmp) {
result = str;
m = 0;
while (1) {
m = match(substr(result, m + 1), /[^\\]""\.equals/);
if (m > 0) {
txt = extract1(result, m + 10);
if (txt != -1) {
result = substr(result, 1, m) "StringUtils.isEmpty" txt substr(result, m + 10 + length(txt));
m = 0;
}
} else {
break;
}
}
return result;
}
# 修正第二类equals判断问题xxx.equals(""),提取其中的xxx,匹配规则:括号、字母、数字,碰到别的东西就停止
function extract2(str, pos, i, level, result, ch, arr) {
level = 0;
split(str, arr, "");
result = "";
for (i = pos; i > 0; i--) {
ch = arr[i];
if (match(ch, /[a-zA-Z0-9_ \t\.]/)) {
# 放行
} else if (ch == ")") {
level++;
} else if (ch == "(") {
level--;
if (level < 0) {
break;
}
} else {
if (level == 0) {
break;
}
}
result = ch result;
}
return result;
}
# 修正第二类equals判断问题:xxx.equals("")
function fix_equals2(str, result, m, txt, tmp) {
result = str;
m = 0;
while (1) {
m = match(substr(result, m + 1), /\.equals\(""\)/);
if (m > 0) {
txt = extract2(result, m - 1);
if (txt != "") {
result = substr(result, 1, m - length(txt) - 1) "StringUtils.isEmpty(" txt ")" substr(result, m + 11);
m = 0;
}
} else {
break;
}
}
return result;
}
/^import.*;$/ {
# 检查有没有import字符串工具类
if (imported == 0 && match($0, /StringUtils;/)) {
imported = 1;
}
last_import_line = NR;
}
{
line = $0;
line = fix_equals1(line);
line = fix_equals2(line);
gsub(/!StringUtils\.isEmpty/, "StringUtils.isNotEmpty", line);
# 实际上不必要的trim。如果你的项目确实会出现空格,请谨慎处理。
line = gensub(/StringUtils\.isEmpty\(([a-zA-Z0-9_ ]+)\.trim\(\)[ ]*\)/, "StringUtils.isEmpty(\\1)", "g", line);
line = gensub(/StringUtils\.isNotEmpty\(([a-zA-Z0-9_ ]+)\.trim\(\)[ ]*\)/, "StringUtils.isNotEmpty(\\1)", "g", line);
if ($0 != line) {
changed = 1;
}
# 在完成分析import之前不要直接输出,以便插入import StringUtils语句
buff[bufflen + 1] = line;
bufflen = bufflen + 1;
}
END {
for (i = 1; i <= bufflen; i++) {
print buff[i];
# 插入 import StringUtils。如果有import语句就插到所有import后面,否则放在第二行(正常情况下package com.xxx是第一行)
if (imported != 1 && (i == last_import_line || last_import_line == -1) && changed == 1) {
print "import org.apache.commons.lang.StringUtils;";
imported = 1;
}
}
}
equals.sh
#!/bin/bash
export LC_ALL=zh_CN.GBK # 代码用UTF-8的,本行需要删掉
cd 代码所在路径
find . -name "*.java" | while read file; do
echo $file
awk -f equals.awk $file > _tmp.java # macOS需要用gawk,安装命令brew install gawk或port install gawk
cp _tmp.java $file
done
rm _tmp.java
运行程序
编译程序,发现程序出现了很多编译错误,这些错误都是因类型不一致导致的低级错误,例如字符串与日期比较、对着int值进行Interger.valueOf、判断int值是否为空串等等。这样看来,无用功做得倒也值了。
已知问题
str != null && !"".equals(str)
以及str == null || "".equals(str)
转换之后会变成str != null && StringUtils.isNotEmpty(str)
和str == null || StringUtils.isEmpty(str)
,但空指针判断已经没有必要,应当删除。
后续
写脚本和调试脚本花了半天时间,下午脚本执行完成,然后手工处理了一些暴露出来的代码错误。隔壁两位新员工分别改另外两类简单问题,结果各花了一星期,因此非常建议大家学习一两门辅助语言,并且自动化处理事情。