在本系列的第一篇文章中,我向您展示了如何设置和执行FindBugs。 现在我们来看看FindBugs最强大的功能-自定义错误检测器。 首先,我将提供理由说明为何自定义错误检测器很有用,然后再为您提供详细的示例。
编写自定义错误检测器
您为什么要编写自定义错误检测器? 当我被要求检查一个团队的绩效问题时,我遇到了这个问题。 很明显,该团队的本地日志记录框架(如所有日志记录框架)已经随着时间的推移而增长。 最初, Loggers
被Loggers
抛弃。 不幸的是,随着团队的成长,其应用程序的性能也会下降,因为它们总是创建昂贵的日志消息-只是在使日志记录框架意识到禁用日志记录时才丢弃它们。 解决此问题的标准方法是在构造昂贵的日志消息之前先检查是否已启用日志。 换句话说,使用清单1所示的guard子句:
清单1.受保护的日志记录示例
if(Logger.isLogging()) {
Logger.log("perf", anObjectWithExpensiveToString + anotherExpensiveToString);
}
团队认为这种做法是适当的日志习惯用法,并着手更改了现有代码以反映新的做法。 当这个大型项目的截止日期临近时,您会惊讶不已,在代码中有很多地方被遗漏了。 团队需要一种更好的方法来识别错过的地方。 因为本文是有关FindBugs的,所以我们将使用FindBugs解决问题。
目的是编写一个FindBugs检测器,该检测器将查找代码中调用日志记录框架的所有位置,而无需将它们包装在保护子句中。
最初编写此检测器时,我将事情分解为几个离散的步骤:
- 首先,我编写了一个带有不受保护的日志语句的测试用例。
- 接下来,我浏览了FindBugs源代码,以查找我认为与我要编写的检测器相似的检测器。
- 然后,我(使用构建脚本)创建了正确打包的JAR文件,以便FindBugs知道如何加载不受保护的检测器。
- 我运行了测试并实现了代码,以使测试通过。
- 最后,我添加了更多测试并继续执行序列直到完成。
特别是在浏览代码时,我检查了BytecodeScanningDetector
和ByteCodePatternDetector
的子类型。 扫描检测器需要更多的工作来实现,但是在它们可以检测到的问题种类上也更加通用。 当您要检测的内容可以表示为字节码模式序列时,模式检测器是一个不错的选择。 BCPMethodReturnCheck
检测器就是一个很好的例子,它检测可疑地忽略了各种方法的返回类型的地方。 BCPMethodReturnCheck
可以很容易地描述为一系列模式,以寻找某些方法的调用,然后调用POP
或POP2
指令。 目前,绝大多数检测器被编写为扫描检测器,尽管我认为这仅仅是因为开发人员没有足够的时间将其中许多检测器移至ByteCodePatternDetector
。
我选择使用FindRunInvocations
作为示例,主要是因为它是最小的检测器之一。 对我来说,如何使用一系列模式实现检测器并不明显。
FindBugs的使用字节码工程库,或BCEL(请参阅相关信息 ),以实现它的探测器。 所有字节码扫描检测器均基于FindBugs实现的访问者模式。 它提供了这些方法的默认实现,在实现自定义检测器时将覆盖这些方法。 请查看BetterVisitor
及其子类以获取更多详细信息。 就我们的目的而言,我们将仅关注两种方法sawOpcode(int)
visit(Code)
和sawOpcode(int)
。 当FindBugs分析类时,当遍历方法的内容时,它将调用visit(Code)
方法。 同样,FindBugs在分析方法体内的每个操作码时会调用sawOpcode(int)
方法。
在此背景下,让我们看一下用于构建无保护的日志检测器的这些方法的实现,如清单2所示:
清单2.无人保护的日志检测器:visit()方法
18 public void visit(Code code) {
19 seenGuardClauseAt = Integer.MIN_VALUE;
20 logBlockStart = 0;
21 logBlockEnd = 0;
22 super.visit(code);
23 }
读取开箱即用的检测器的代码时,跳出来的一件事就是关注检测器在分析过程中是否需要建立状态。 换句话说,检测器是否需要记住在方法,类,层次结构或整个程序级别看到的内容? 例如, Inconsistent Synchronization
检测器会为整个程序建立状态,以便它可以确定何时按照同步方式以不一致的方式访问字段。 我们的检测器仅在字节码扫描阶段需要保持状态,因为我们正在寻找方法级的问题。
检测器所累积的特定于方法的状态可以在visit(Code)
方法中刷新或重置(如清单2所示),因为FindBugs在扫描方法的字节码之前会调用此方法。 在这种情况下,检测器将保持三个状态位:
-
seenGuardClauseAt
:在分析的代码中发现日志保护子句时,程序计数器的值 -
logBlockStart
:guard子句开头的索引 -
logBlockEnd
:保护子句末尾的指令索引
关于visit(Code)
方法的实现,应注意两个重要问题。 首先要注意的是对super.visit()
的调用,这是关键,因为此方法的超类的实现负责访问我们要分析的方法的内容。 如果我们不调用超类的实现,那么将永远不会检查所分析的方法。
第二点是,在调用超类的实现之前会重置累积状态,这很重要,因为这些变量将由我们将要看的下一个方法sawOpcode()
方法sawOpcode()
使用。 我们要确保在此之前将其重置。 清单3显示了sawOpcode()
方法的实现:
清单3.无人保护的日志记录检测器:sawOpcode()方法
25 public void sawOpcode(int seen) {
26 if ("cbg/app/Logger".equals(classConstant) &&
27 seen == INVOKESTATIC &&
28 "isLogging".equals(nameConstant) && "()Z".equals(sigConstant)) {
29 seenGuardClauseAt = PC;
30 return;
31 }
32 if (seen == IFEQ && (PC >= seenGuardClauseAt + 3 && PC < seenGuardClauseAt + 7)) {
33 logBlockStart = branchFallThrough;
34 logBlockEnd = branchTarget;
35 }
36 if (seen == INVOKEVIRTUAL && "log".equals(nameConstant)) {
37 if (PC < logBlockStart || PC >= logBlockEnd) {
38 bugReporter.reportBug(
39 new BugInstance("CBG_UNPROTECTED_LOGGING", HIGH_PRIORITY)
40 .addClassAndMethod(this).addSourceLine(this));
41 }
42 }
43 }
如前所述,当FindBugs分析一种方法时,它将为该方法中包含的每个字节码指令调用sawOpcode()
。 此方法正在做三件事。 实际上,原始代码被重构为三种方法,但是出于本文的目的,我对其进行了内联以减少空间量。 此方法执行三件事:
- 确定是否调用了静态方法
Logger.isLogging()
,并且确定是否调用了程序计数器(PC) - 确定是否在对
Logger.isLogging()
的调用之后执行if
指令 - 查找在保护子句之外调用
log()
方法的情况
清单4分别详细显示了每个部分:
清单4.无人保护的日志记录检测器:sawOpcode(),isLogging()被调用
25 public void sawOpcode(int seen) {
26 if ("cbg/app/Logger".equals(classConstant) &&
27 seen == INVOKESTATIC &&
28 "isLogging".equals(nameConstant) && "()Z".equals(sigConstant)) {
29 seenGuardClauseAt = PC;
30 return;
31 }
该classConstant
, nameConstant
和sigConstant
领域的保护域,从它的超探测器继承。 它们包含有关当前操作码的详细信息。 编写检测器时,为它们打印值通常很有用。 浏览BytecodeScanningDetector
层次结构,以在DismantleBytecode
类中找到更多有用的字段和方法。 编写检测器时使用的另一个非常有用的工具是多年生javap
。 Java反汇编程序是一个非常方便的工具,用于了解编写检测器的逻辑流程和方法名称。 通用方法是编写您要查找的模式(在这种情况下,将guard子句写在Java文件中),保存它,然后编译它。 然后,使用javap -c
查看反汇编的字节码,并学习如何构建您的sawOpcode(int)
方法。 例如,清单5显示了在我用于测试用例的类上运行后的javap
输出(该方法正确使用了日志保护子句):
清单5.带源的guard子句的分解示例
public void methodWithLogging_guarded();
Code:
0: invokestatic #28; //Method cbg/app/Logger.isLogging:()Z
3: ifeq 18
6: new #16; //class Logger
9: dup
10: invokespecial #17; //Method cbg/app/Logger."<init>":()V
13: ldc #19; //String bob
15: invokevirtual #23; //Method cbg/app/Logger.log:(Ljava/lang/Object;)V
18: aload_0
19: invokespecial #31; //Method doWork:()V
22: return
corresponds to the Java source code
public void methodWithLogging_guarded() {
if (Logger.isLogging()) {
new Logger().log("bob");
}
doWork();
}
检查javap
的输出可帮助您了解该方法的控制流程以及如何形成需要在sawOpcode()
方法中指定的类,签名和名称常量。 例如,清单6显示了清单5中来自javap
的第一行代码:
清单6.反汇编方法调用
0: invokestatic #28; //Method cbg/app/Logger.isLogging:()Z
如果仔细查看清单4中 sawOpcode()
方法的第26至28行,您会发现它们正在描述一种匹配javap
清单5中所见内容的方法。 javap
是确定如何匹配这些形式的有用工具。
确定已经调用Logger.isLogging()
方法后,我们想要保存程序计数器的值,如清单7所示。需要使用程序计数器来确定if
子句是否在调用之后。 Logger.isLogging()
,将我们带到下一部分代码。
清单7.保存程序计数器的值
32 if (seen == IFEQ && (PC >= seenGuardClauseAt + 3 && PC < seenGuardClauseAt + 7)) {
33 logBlockStart = branchFallThrough;
34 logBlockEnd = branchTarget;
35 }
清单3中的这段代码正在检查我们if
在与前面提到的Logger.isLogging()
调用相距3到7个字节代码之间的任意位置看到了一条if
分支语句。 这些值是通过查看javap
的输出并通过实验确定的。 你说实验吗? 那就对了; 您有时必须求助于反复试验,才能在误报与有用结果之间找到正确的平衡点。 将该过程视为具有启发式方法而不是计算机科学的计算机工程。 一旦确定此语句为if(Logger.isLogging())
语句,就需要找出if
的代码块的边界。 我们通过保存branchFallThrough
和branchTarget
。 branchFallThrough
是if
子句的开头, branchTarget
表示if
子句之外的第一行。 有了这些信息,我们现在可以继续进行该方法的最后一部分,如清单8所示:
清单8.检查对log()的调用
36 if (seen == INVOKEVIRTUAL && "log".equals(nameConstant)) {
37 if (PC < logBlockStart || PC >= logBlockEnd) {
38 bugReporter.reportBug(
39 new BugInstance("CBG_UNPROTECTED_LOGGING", HIGH_PRIORITY)
40 .addClassAndMethod(this).addSourceLine(this));
41 }
42 }
同样是清单3中的这段代码,查找对Logger
的log()
方法的调用。 找到对log()
方法的调用后,我们将检查程序计数器是否在前面确定的if
块之外。 如果是这样,我们将通过创建一个新的错误实例并指定错误的类型(我们将在后面详细讨论)及其优先级来报告错误。 向错误中添加类,方法和源代码行也很方便,以便用户知道在哪里可以解决问题。
编写代码后,您需要创建一个特别打包的JAR文件,FindBugs将其识别为插件JAR。 清单9显示了用于创建JAR文件并将其复制到正确位置的构建脚本的目标:
清单9.构建脚本以打包我们的FindBugs检测器
<property name="FindBugs.home" value="C:\apps\FindBugs-0.7.3"></property>
<target name="build">
<jar destfile="cbgFindbugsPlugin.jar">
<fileset dir="bin"/>
<fileset dir="src"/>
<zipfileset dir="etc" includes="*.xml" prefix=""></zipfileset>
</jar>
<copy file="cbgFindbugsPlugin.jar" todir="${FindBugs.home}/plugin" />
</target>
此代码创建一个JAR文件,其中包含源文件,类文件,FindBugs.xml和messages.xml。 清单10和清单11显示了两个XML文件的内容:
清单10. FindBugs.xml的内容
<FindbugsPlugin>
<Detector class="cbg.findBugs.FindUnprotectedLogging" speed="fast" />
<BugPattern abbrev="CBGL" type="CBG_UNPROTECTED_LOGGING" category="PERFORMANCE" />
</FindbugsPlugin>
对于每个新检测器,您都将一个Detector
元素和一个BugPattern
元素添加到FindBugs.xml文件中。 Detector
元素指定用于实现检测器的类,以及它是快速还是慢速检测器。 当在UI中查看检测器时,使用speed属性,如图1所示。speed属性的可能值为lower,medium和fast。
图1.配置检测器UI

BugPattern
元素指定三个属性。 abbrev
属性定义检测器的缩写。 从命令行客户端运行时,该缩写用于标识检测到的错误。 可以通过共享相同的缩写将几个相关的检测器组合在一起。
type
属性是唯一的标识符,可用于两个目的。 当使用Find版本的Ant版本或将输出格式设置为XML的命令行版本时,可以使用type
属性来识别问题。 type
属性也是您在检测器的Java代码中指定的属性,以创建正确的bug类型。 注意,这里列出的类型与清单8的第39行上使用的名称匹配。
category
属性是枚举类型。 这是以下之一:
-
CORRECTNESS
:一般正确性问题 -
MT_CORRECTNESS
:多线程正确性问题 -
MALICIOUS_CODE
:如果暴露于恶意代码,则可能是一个潜在漏洞 -
PERFORMANCE
:性能问题
FindBugs.xml文件就是这样。 清单11显示了messages.xml文件包含的内容:
清单11. messages.xml的内容
<MessageCollection>
<Detector class="cbg.FindBugs.FindUnprotectedLogging">
<Details>
<![CDATA[
<p> This detector finds logs statements that aren't contained in an if-logging block.
It is a fast detector.
]]>
</Details>
</Detector>
<BugPattern type="CBG_UNPROTECTED_LOGGING">
<ShortDescription>Found unprotected logging</ShortDescription>
<LongDescription>Found unprotected logging in {1}</LongDescription>
<Details>
<![CDATA[
<p> This method logs without first checking that logging is enabled; for example
... more text omitted...
]]>
</Details>
</BugPattern>
<BugCode abbrev="CBGL">Found unprotected logging</BugCode>
</MessageCollection>
messages.xml文件由三个元素组成: Detector
, BugPattern
和BugCode
。
检测器的class
属性应指定检测器的class
名称。 Details
元素包含检测器的简短HTML描述,因此应包含在CDATA
部分中。 UI使用此描述,如图2所示:
图2. FindBugs UI突出显示未受保护的日志记录检测器

BugPattern
元素类似于FindBugs.xml中定义的BugPattern
元素。 type
属性是必需的,并且应该与您在FindBugs.xml和检测器的Java代码中使用的唯一标识符匹配。 BugPattern
包含影响如何对检测信息显示在UI三个要素: ShortDescription
, LongDescription
,并Details
-所有这些都是相当自我解释。
在UI中关闭“视图”>“完整描述”时,将使用ShortDescription
。 同样,当打开“视图”>“完整描述”时,将使用LongDescription
。 您可以使用注释将错误检测器的Java代码中的信息传递到完整描述中。 在描述中,使用{0}
表示第一个注释,使用{1}
表示第二个注释,以此类推来指定变量。 在运行时,当发现错误时,您附加到错误实例的任何注释都将替换为描述。 注意,在清单8的第40行中,将类和方法注释添加到BugInstance
。 类注释位于位置0,方法注释位于位置1。有关更多详细信息,请查看BugInstance
上的各种add*()
方法。
和以前一样, Details
元素应在CDATA
节中包含HTML描述。 图2显示了我们探测器的细节示例。 “视图”>“完整描述”已打开。
使用“按错误类型”选项卡时,UI会使用BugCode
元素。 元素的文本在树中显示为红色节点, 如图2所示。 通用检测器均共享相同的缩写,因此BugCode
元素必须将缩写指定BugCode
元素的属性。
创建了这两个XML文件之后,我们现在就可以打包完整的JAR。 构建JAR并将其放置在FIND_BUGS_HOME \ plugin目录中之后,您就可以测试新的检测器了。
特定于应用程序的错误检测器
FindBugs可能是您军械库中的有用工具。 但是,与所有工具一样,您必须知道何时使用它。 但是,静态分析工具应补充您的单元/系统测试和代码审查。
除了提高代码质量的实用程序外,FindBugs还具有许多特定于应用程序的用途,我鼓励您进行探索。 例如,您可以编写一组发现新手样式问题的检测器。 或者,您可以编写检测器来检查您的代码是否符合团队的准则。 也许您正在构建一个框架,并且需要确保包中的所有类都具有零参数构造函数,或者所有以下划线为前缀的字段都具有getter但没有setter。 或者,您可以编写一组检测器来验证您的J2EE代码是否遵循适当的限制,例如没有创建Thread
或Socket
。
不受保护的日志记录示例中的团队也遇到了捕获异常的问题。 值得称赞的是,他们并没有简单地忽略这些例外。 相反,他们要求他们打印堆栈跟踪,这在您构建和调试应用程序时很好,但是在部署它时并不理想-尤其是当可能有成千上万个异常时。 (当然,如果您的应用程序引发了数千个异常,则与大日志文件相比,您遇到的问题更为严重,但出于说明目的,请耐心等待。)团队需要一个检测器,以在其代码中查找捕获异常的位置,要求打印其堆栈跟踪。 然后,他们可以更改代码,以将异常传递给其日志记录框架。
我创建了一个不受保护的测井仪的有趣变体。 该检测器用于查找代码中在guard子句之外创建要记录消息的所有位置,这也是一个相当普遍的问题,如果您喜欢toString
,这可能会非常昂贵。
摘要
无论您是FindBugs的新手还是熟悉它的人,我都建议您尝试使用自己的专用探测器。 同时,我希望本文能为您提供一个清晰的示例,说明如何实现自定义检测器,并启发您将这些想法应用于团队的独特情况。
翻译自: https://www.ibm.com/developerworks/java/library/j-findbug2/index.html