前面的几篇文章介绍了 Spotbugs 的流程,如何进行 Bug 处理的,编写 plugin 的方式,以及字节码相关知识的简介。本篇开始,我们来着重从源码看一下,如何表达我们的检测意愿。
材料来源于 SpotBugs 的测试用例 spotbugs-test 工程。
Row Detector
行的检测是最多但是相对来说比较简单的部分。 以下面这个 String.indexOf 检测为例,推荐使用 int 参数,也就是单字符参数。
- Use whenever possible String.indexOf(int) instead of String.indexOf(String),or String.lastIndexOf(int) instead of String.lastIndexOf(String).
该检测的原因是要测试
相比较而言, indexof(int) 会更高效一点。
目标字节码:
getClassConstantOperand 调用类 “java/lang/String”
getNameConstantOperand 调用方法 “lastIndexOf” “indexOf”
getSigConstantOperand 方法参数 “(Ljava/lang/String;)I” 参数String,返回int; (“(Ljava/lang/String;I)I” 参数 String,int 返回 int。
seen 在这里是操作码的意思,可以参考 Const 中定义的常量值。
@Override
public void sawOpcode(int seen) {
if (seen == Const.INVOKEVIRTUAL && stack.getStackDepth() > 0 && "java/lang/String".equals(getClassConstantOperand())) {
boolean lastIndexOf = "lastIndexOf".equals(getNameConstantOperand());
if (lastIndexOf || "indexOf".equals(getNameConstantOperand())) {
int stackOff = -1;
if ("(Ljava/lang/String;)I".equals(getSigConstantOperand())) { // sig: String
stackOff = 0;
} else if ("(Ljava/lang/String;I)I".equals(getSigConstantOperand())) { // sig: String, int
stackOff = 1;
}
if (stackOff > -1) {
OpcodeStack.Item item = stack.getStackItem(stackOff);
Object o = item.getConstant();
if (o != null && ((String) o).length() == 1) {
bugReporter.reportBug(new BugInstance(this, lastIndexOf ? "IIO_INEFFICIENT_LAST_INDEX_OF" : "IIO_INEFFICIENT_INDEX_OF",
LOW_PRIORITY).addClassAndMethod(this)
.describe(StringAnnotation.STRING_MESSAGE).addCalledMethod(this).addSourceLine(this));
}
}
}
}
}
整个实现是很简单的检测调用堆栈栈深 > 0 的调用位置,符合匹配方法的,抛出 bug。
Method Detector
以 SynchronizeAndNullCheckField 为例。传入synchronized块的锁引用如果是null的话则执行到这个synchronized块时会立即抛出NPE。也就是说同步和空值检测发生在同一个属性上,判断没有必要。
目标代码:
Detector:
@Override
public void sawOpcode(int seen) {
switch (currState) {
case 0:
if (seen == Const.GETFIELD || seen == Const.GETSTATIC) {
syncField = FieldAnnotation.fromReferencedField(this);
currState = 1;
}
break;
case 1:
if (seen == Const.DUP) {
currState = 2;
} else {
currState = 0;
}
break;
case 2:
if (seen == Const.ASTORE || seen == Const.ASTORE_0 || seen == Const.ASTORE_1 || seen == Const.ASTORE_2 || seen == Const.ASTORE_3) {
currState = 3;
} else {
currState = 0;
}
break;
case 3:
if (seen == Const.MONITORENTER) {
currState = 4;
} else {
currState = 0;
}
break;
case 4:
if (seen == Const.GETFIELD || seen == Const.GETSTATIC) {
gottenField = FieldAnnotation.fromReferencedField(this);
currState = 5;
} else {
currState = 0;
}
break;
case 5:
if ((seen == Const.IFNONNULL || seen == Const.IFNULL) && gottenField.equals(syncField)) {
BugInstance bug = new BugInstance(this, "NP_SYNC_AND_NULL_CHECK_FIELD", NORMAL_PRIORITY).addClass(this)
.addMethod(this).addField(syncField).addSourceLine(this);
bugReporter.reportBug(bug);
} else {
currState = 0;
}
break;
default:
currState = 0;
}
}
如果注意到,就能发现,switch 是对currState判断的,而且顺着case依次递增加1,或者清空为0. 这种技巧在 Spotbugs 的 Detector 文件夹里有非常广泛的应用,通过状态层层递进来实现逻辑判断。
定位逻辑: monitorEntor 之后 调用了获取 Field ,且对 Field 判空,抛出 Bug。
对于面向 Class 的 Detector 范围更大,但是基础是一样的,可以参考官方代码,本系列不再赘述。