代码Bug太多?给新人Code Review头都大了?快来试试SpotBugs
如果你需要一个自动化的工具帮助你或者你的团队发现代码中的缺陷,在提升代码质量同时减少人工Code Review的成本,那这篇文章非常的适合你。本文围绕SpotBugs与Gradle集成,将相关配置和使用进行了详细介绍,并提供了各种能够为你的项目定制化配置的手段。来源和出处都已在文中关键处以超链接给出,尽情享受吧。
SpotBugs是什么?
SpotBugs是一个开源的Java静态分析工具,旨在帮助开发人员检测代码中的潜在缺陷和漏洞。SpotBugs可以通过扫描Java字节码来发现潜在的问题,例如空指针引用、类型转换错误、未使用的变量等等。它还可以检测代码中的潜在安全漏洞,例如SQL注入、XSS攻击等。SpotBugs提供了一个用户友好的GUI和命令行接口,可以轻松地与各种构建工具和IDE集成,例如Ant、Maven、Gradle、Eclipse和IntelliJ IDEA。SpotBugs还支持插件和自定义规则,使得开发人员可以根据项目的特定需求和标准对其进行定制化配置。更多详细信息可以查看SpotBugs官方文档。
SpotBugs是FindBugs的一个分支,它在FindBugs的基础上进行了改进和升级,它使用了更先进的算法和技术来提高分析的准确性和效率。SpotBugs还添加了对新的Java版本的支持,例如Java 8和Java 11。SpotBugs还提供了更好的用户界面和命令行界面,并支持更多的构建工具和IDE集成。
FindBugs也是一款非常流行的Java静态分析工具,但是已经停止更新。停更的原因似乎是项目拥有者不愿继续在这个项目上花时间,代码贡献者因为没有权限也无法继续迭代和发布新的版本,在FindBugs GitHub首页的README文件中提供了两个文档项目状态 2016-November和项目状态 2017-September,里面讲述了代码贡献者当时的一些担忧和无奈。
SpotBugs与Gradle集成
开始之前先简单介绍下本文中将会提到的几个依赖以及它们之间的关系,以便于理解后续内容:
com.github.spotbugs.snom:spotbugs-gradle-plugin是一个Gradle插件,它将SpotBugs集成到 Gradle构建中,生成SpotBugsTask并可以通过配置属性来扩展。com.github.spotbugs:spotbugs这个依赖基本包含了所有SpotBugs检测器,这些检测器实现了Bug descriptions中提到的相关Bug检测项的逻辑。com.h3xstream.findsecbugs:findsecbugs-plugin在com.github.spotbugs:spotbugs的基础上增加了安全相关的检查器也就是Bug descriptions#Security中描述的相关检查项。com.github.spotbugs:spotbugs-annotations是Spotbugs的一个扩展/辅助工具,开发者使用这里面注解可以让Spotbugs按照我们的意图来检查代码。
spotbugs-gradle-plugin
spotbugs-gradle-plugin是一个Gradle插件,它将SpotBugs集成到Gradle构建中,生成SpotBugsTask并提供相关配置来扩展,运行SpotBugsTask就可以执行检查,生成报告等。
默认情况下,这个插件里面已经包含了一个spotbugs,所以应用了这个插件后一般无需在另外添加com.github.spotbugs:spotbugs。从SpotBugs version mapping中可以知道spotbugs-gradle-plugin不同版本中默认包含的spotbugs版本之间的关系,比如:com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.13包含了com.github.spotbugs:spotbugs:4.7.3。
SpotBugs Gradle插件要求Gradle版本为7.0或者更高,JDK版本为1.8或者更高。
这个插件会为每个sourceSets生成SpotBugsTask。例如项目中有两个sourceSets main和test,插件将生成两个SpotBugsTask(spotbugsMain和spotbugsTest)。
如果不想自动生成SpotBugsTask,可以使用SpotBugs Base插件从头开始配置,参考com.github.spotbugs-base。
生成的SpotBugsTask执行时需要编译后的.class文件作为输入,因此它们将在Java编译后运行。SpotBugs Gradle增加了check相关的任务依赖,所以简单的运行./gradlew check也会执行生成的SpotBugsTask。
参考com.github.spotbugs在build.gradle文件中增加以下内容来引入SpotBugs Gradle插件:
buildscript {
repositories {
maven {
url "https://plugins.gradle.org/m2/"
}
}
dependencies {
classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:5.0.13"
}
}
apply plugin: "com.github.spotbugs"
插件里面提供了很多可选的属性来配置或者扩展插件的行为。
下面展示了在build.gradle文件增加spotbugs{}相关属性的例子,详细信息可以参考SpotBugsExtension,这里面有每个属性作用、可指定的值和其他相关信息。spotbugs{}中指定的大部分属性将作为生成的SpotBugsTask配置的默认值,意味着可以通过spotbugs{}给所有SpotBugsTask配置属性。
spotbugs {
ignoreFailures = false
showStackTraces = true
showProgress = false
reportLevel = 'default'
effort = 'default'
visitors = [ 'FindSqlInjection', 'SwitchFallthrough' ]
omitVisitors = [ 'FindNonShortCircuit' ]
reportsDir = file("$buildDir/reports/spotbugs")
includeFilter = file('spotbugs-include.xml')
excludeFilter = file('spotbugs-exclude.xml')
onlyAnalyze = ['com.foobar.MyClass', 'com.foobar.mypkg.*']
projectName = name
release = version
extraArgs = [ '-nested:false' ]
jvmArgs = [ '-Duser.language=ja' ]
maxHeapSize = '512m'
}
除了上述方式外,还可直接配置SpotBugsTask,以设置某个任务特定的属性,比如下面为名为spotbugsMain的任务单独进行了设置,跟spotbugs{}同名的属性将被覆盖,详细信息参考SpotBugsTask。
spotbugsMain {
sourceDirs = sourceSets.main.allSource.srcDirs
classDirs = sourceSets.main.output
auxClassPaths = sourceSets.main.compileClasspath
reports {
html {
required = true
outputLocation = file("$buildDir/reports/spotbugs/main/spotbugs.html")
stylesheet = 'fancy-hist.xsl'
}
}
ignoreFailures = false
showStackTraces = true
showProgress = false
reportLevel = 'default'
effort = 'default'
visitors = [ 'FindSqlInjection', 'SwitchFallthrough' ]
omitVisitors = [ 'FindNonShortCircuit' ]
reportsDir = file("$buildDir/reports/spotbugs")
includeFilter = file('spotbugs-include.xml')
excludeFilter = file('spotbugs-exclude.xml')
baselineFile = file('spotbugs-baseline.xml')
onlyAnalyze = ['com.foobar.MyClass', 'com.foobar.mypkg.*']
projectName = name
release = version
extraArgs = [ '-nested:false' ]
jvmArgs = [ '-Duser.language=ja' ]
maxHeapSize = '512m'
}
后面将会有一段描述比较常用或者重要的属性。
com.github.spotbugs:spotbugs
因为spotbugs-gradle-plugin中已经包含了spotbugs,所以一般情况下不需要再单独引入这个依赖。不过可能因为默认带的版本有Bug、需要跟IDEA的SpotBugs插件使用相同的版本,又或者新版本的检测器新加了实用的检测项等原因我们会需要单独指定版本,下面提供了两种方式实现。
在配置spotbugs-gradle-plugin的时候通过toolVersion指定spotbugs版本。
spotbugs {
toolVersion = '4.7.3'
}
在dependencies添加依赖并指定依赖版本。
dependencies {
spotbugs 'com.github.spotbugs:spotbugs:4.7.3'
}
findsecbugs-plugin
findsecbugs-plugin是Spotbugs的安全漏洞检测插件。它在spotbugs的基础上增加了自己的规则集,专注于检测安全相关的问题,例如密码泄漏、SQL 注入、XSS 等,也就是Bug descriptions#Security中描述的相关检查项。
dependencies {
spotbugsPlugins 'com.h3xstream.findsecbugs:findsecbugs-plugin:1.12.0'
}
spotbugs-annotations
spotbugs-annotations是Spotbugs的一个扩展/辅助工具,里面提供了很多注解,我们可以在编码时将它们加在被检测代码的相应位置(如属性、方法、形参、本地变量),让Spotbugs按照我们的意图来检查代码,以便SpotBugs可以更恰当地发出警告。在Annotations中列举了所有注解,需要注意里面有很多已经标记为Deprecated表明已经弃用了。
比如下面这段代码,只是输出value到终端,即使test()传入null也不会导致空指针。Spotbugs本来不会发出警告,但是由于我们在方法上加了注解edu.umd.cs.findbugs.annotations.NonNull,Spotbugs会按照我们的意图进行入参不允许为null的校验,从而发出警告。
import edu.umd.cs.findbugs.annotations.NonNull;
public class SpotbugsAnnotationsSample {
public static void main(String[] args) {
test(null);
}
public static void test(@NonNull Integer value) {
// 输出到终端
System.out.println(value);
}
}
下面是引入这个依赖的例子,参考Refer the version in the build script,这里面还提到从spotbugs v4版本开始,spotbugs.toolVersion 由 String 变为 Provider<String>,所以请使用 get() 或其他方式引用实际版本。
dependencies {
compileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}"
}
SpotBugs Gradle扩展属性
includeFilter和excludeFilter
过滤器(Filter file)文件可以定义匹配检测项、被检查的类和被检查方法的一套匹配规则,让我们可以为项目做定制化。文件配置好后,使用SpotBugsTask提供的属性指定过滤器文件的路径,实现包含(includeFilter)或者排除(excludeFilter)匹配的内容。
文件是xml格式的,每个匹配规则单独写在<Match></Match>中,使用Types of Match clauses描述的各种标签组合成匹配规则的内容,在Examples中给出了很多例子作为参考。
我们重点关注下这个标签,它可以通category、code、pattern来指定Bug descriptions中列出来的检查项,多个参数用逗号分隔,也可以使用正则表达式,以~字符开头。
每个检查项有唯一的pattern,比如下图中的XSS_REQUEST_PARAMETER_TO_SEND_ERROR和XSS_REQUEST_PARAMETER_TO_SERVLET_WRITER;多个检查项可能属于同一个code,比如下图中的XSS;多个不同的code可能属于同一个category。这三者之间是一个清晰的层级关系,这样就可以按不同的粒度来配置。
下面给出了一个使用<Bug>简单的例子和描述,更多例子请参考Examples。
<!--匹配指定的这两个检查项-->
<Match>
<Bug pattern="XSS_REQUEST_PARAMETER_TO_SEND_ERROR,XSS_REQUEST_PARAMETER_TO_SERVLET_WRITER" />
</Match>
<!--匹配所有code为XSS的检查项-->
<Match>
<Bug code="XSS" />
</Match>
<!--匹配SECURITY目录下所有检查项-->
<Match>
<Bug category="SECURITY" />
</Match>
visitors和omitVisitors
这两个属性可以分别指定使用(visitors)和禁用(omitVisitors)检查器,一个检查器会包含一个或者多个Bug descriptions中提到的检查项,所以这两个属性配置的目的和includeFilter和excludeFilter是一样的,让我们可以为项目做定制化,只是从检查器的维度进行配置。
在Detectors中将Spotbugs所有的检查器都列出来了,在不配置的情况下,Standard detectors中列出来的默认使用,Disabled detectors中列出来的默认禁用。
以检查器SynchronizationOnSharedBuiltinConstant为例,从下图我们可以看到,检查器名称下面有段简短的描述,再往下将检查器包含的检查项都列出来了,可以看到上文提到的pattern和code,点击可以跳转到Filter file文档相应的位置。我们配置visitors和omitVisitors的时候填检查器的名字SynchronizationOnSharedBuiltinConstant即可。

effort
effort是配置代码检测预期的级别,级别由低到高分别为min、less、more、max,级别越高计算成本越高,花费的时间也就越长,在这个文档Effort里面有表格清晰的列出了这几个级别分别包含了哪些检查内容。effort的默认值是default,等同于more。
jvmArgs
jvmArgs用于设置JVM参数,因为SpotBugs是Java写的,自然要在JVM上运行。我们在例子里面看到了-Duser.language=ja,这个意思是设置语言为日语,也就是代码分析的结果展示的语音(输出到终端或者报告中)。总共有英语(en 默认),日语(ja),法语(fr)三种,在GitHub中可以看到相关展示文本的配置文件。

maxHeapSize
maxHeapSize是设置JVM最大堆内存大小,在spotbugsextension#maxHeapSizea中说了默认值为空,因此会使用Gradle的默认配置,所以一般不用管。
onlyAnalyze
onlyAnalyze指定哪些代码要被SpotBugs分析,在大型项目里面用这个属性避免没必要的分析,可能会大大减少运行分析所需的时间。可以指定类名,或者包名,指定包名的时候用.*和.-的作用一样,意思是分析这个包下以及子包中的文件。
onlyAnalyze = ['com.foobar.MyClass', 'com.foobar.mypkg.*']
reportLevel
SpotBugs按严重程度高到低将Bug分为了三个等级P1、P2和P3,reportLevel属性指明达到哪个等级的Bug才需要展示在报告里面,它可配置值对应有HIGH、MEDIUM、LOW和DEFAULT,它们定义在Confidence.groovy中。默认值是DEFAULT等同于MEDIUM,意思是要达到P2级别,那就意味着低级别的P3 Bug将被忽略,
reports
reports配置报告的类型,有html、xml、sarif和text四种,配置在SpotBugsTask中(比如spotbugsMain {}),再往里一层可配置的属性不是很多,参考SpotBugsReport。下面是html类型报告的配置示例:
spotbugsMain {
reports {
html {
required = true
outputLocation = file("$buildDir/reports/spotbugs/main/spotbugs.html")
stylesheet = 'fancy-hist.xsl'
}
}
}
最佳实践
配置
在根目录gradle.properties中配置版本。
spotbugsGradlePluginVersion=5.0.13
findsecbugsPluginVersion=1.12.0
在根目录build.gradle中增加以下配置:
buildscript {
repositories {
mavenLocal()
maven {
url "https://plugins.gradle.org/m2/"
}
mavenCentral()
}
dependencies {
classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:${spotbugsGradlePlugin}"
}
}
apply plugin: "com.github.spotbugs"
repositories {
mavenLocal()
maven {
url "https://plugins.gradle.org/m2/"
}
mavenCentral()
}
dependencies {
compileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}"
spotbugsPlugins "com.h3xstream.findsecbugs:findsecbugs-plugin:${findsecbugsPluginVersion}"
}
spotbugs {
ignoreFailures = false
showStackTraces = false
showProgress = false
excludeFilter = file("${project.rootDir}/code-analysis/spotbugs/exclude-filter.xml")
extraArgs = [ '-nested:false' ]
}
spotbugsMain {
reports {
html {
required = true
stylesheet = 'fancy-hist.xsl'
}
}
}
如果是多模块项目按这种方式配置:
buildscript {
repositories {
mavenLocal()
maven {
url "https://plugins.gradle.org/m2/"
}
mavenCentral()
}
dependencies {
classpath "com.github.spotbugs.snom:spotbugs-gradle-plugin:${spotbugsGradlePlugin}"
}
}
allprojects {
apply plugin: "com.github.spotbugs"
repositories {
mavenLocal()
maven {
url "https://plugins.gradle.org/m2/"
}
mavenCentral()
}
dependencies {
compileOnly "com.github.spotbugs:spotbugs-annotations:${spotbugs.toolVersion.get()}"
spotbugsPlugins "com.h3xstream.findsecbugs:findsecbugs-plugin:${findsecbugsPluginVersion}"
}
spotbugs {
ignoreFailures = false
showStackTraces = false
showProgress = false
excludeFilter = file("${project.rootDir}/code-analysis/spotbugs/exclude-filter.xml")
extraArgs = ['-nested:false']
}
spotbugsMain {
reports {
html {
required = true
stylesheet = 'fancy-hist.xsl'
}
}
}
}
在项目根目录下创建排除检查项的文件/code-analysis/spotbugs/exclude-filter.xml,后期再根据需要配置。
<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter
xmlns="https://github.com/spotbugs/filter/3.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://github.com/spotbugs/filter/3.0.0 https://raw.githubusercontent.com/spotbugs/spotbugs/3.1.0/spotbugs/etc/findbugsfilter.xsd">
</FindBugsFilter>
使用
在Gradle窗口中双击运行spotbugsMain任务即可,检测完成会在Run窗口中打印出报告地址。
也可以在Terminal窗口中执行./gradlew spotbugsMain,可能需要先执行chmod +x gradlew给gradlew文件执行权限。

如果执行过程中发生类似下面这种异常可以不用管,只是有部分相关的分析会无法执行,并不会中断整个过程,其他不相关的部分都会正常检查完成。在GitHub上issues#527很多人反映了这个问题,但是暂时没有完美的解决。
The following classes needed for analysis were missing:
apply
test
accept
解读报告
报告概要(Summary)按包和BUG等级统计了数量。

按目录浏览(Browse by Categories)将发现的缺陷代码按检查项中的category、code、pattern分层展示。

按包名浏览(Browse by Packages)就是换了个角度,按照包名 > 类名 > 检查项pattern分层展示。

最后一个窗口Info展示分析的代码文件(Analyzed Files)、分析的源文件夹(Source Files)使用的依赖(Used Libraries)、使用到的SpotBugs插件(Plugins)和分析中产生的异常(Analysis Errors)。
过滤配置
通过includeFilter和excludeFilter指定过滤器(Filter file)文件是最灵活的,基本什么维度都可以控制。但是建议这里面只配置要包含或者排除哪些检查项,使用category、code、pattern配置不同的粒度。
从整体缩小被检查的代码范围使用onlyAnalyze,如果有必要的话。如果要忽略不检查具体的类或者方法可以使用注解@SuppressFBWarnings来标记,它来自spotbugs-annotations。
其他技巧
SpotBugs Links里面列出了跟SpotBugs集成或者相似的工具,有需要可以了解下。
参考
[GitHub]spotbugs-gradle-plugin
代码Bug太多?给新人Code Review头都大了?快来试试SpotBugs的更多相关文章
- Linux 内核的代码仓库太不一样了,光克隆都让我挠头,克隆后居然还丢文件,你肯定也会遇到!!!
一个肯定能让你节省几个小时的小知识 大家好,我是 小猿来也,一个人称撸(划)码(水)小能手的程序猿. 最近一段时间,每次经过旁边大佬工位,总是发现他在快速的切屏,不知道在搞什么?难道他发现了快乐星球? ...
- Code Review最佳实践
我一直认为Code Review(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题.包括像Google.微软这些公司,Code Review都是基本要求,代 ...
- Code Review最佳实践(转)
我一直认为Code Review(代码审查)是软件开发中的最佳实践之一,可以有效提高整体代码质量,及时发现代码中可能存在的问题.包括像Google.微软这些公司,Code Review都是基本要求,代 ...
- 如何在团队中做好Code Review
一.Code Review的好处 想要做好Code Review,必须让参与的工程师充分认识到Code Review的好处 1.互相学习,彼此成就 无论是高手云集的架构师团队,还是以CURD为主的业务 ...
- 什么是Code Review(转)
Code Review是一种通过复查代码提高代码质量的过程,在XP方法中占有极为重要的地位,也已经成为软件工程中一个不可缺少的环节.本文通过对Code Review的一些概念和经验的探讨,就如何进行C ...
- 什么是Code Review
Code Review 是一种通过复查代码提高代码质量的过程,在XP方法中占有极为重要的地位,也已经成为软件工程中一个不可缺少的环节. 本文通过对Code Review的一些概念和经验的探讨,就如何进 ...
- 如何在python脚本开发做code review
在软件项目开发中,我们经常提到一个词“code review”.code review中文翻译过来就是代码评审或复查,简而言之就是编码完成后由其他人通过阅读代码来检查代码的质量(可编译.可运行.可读. ...
- 从code review到Git commit log
最近在读一本技术类的书:朱赟——<跃迁:从技术到管理的硅谷路径>,其中聊了很多很有趣的观点,比如:技术管理.技术实践.硅谷文化.个人成长等. 读到关于硅谷人如何做code review这一 ...
- Code Review(转)
Code Review是一种通过复查代码提高代码质量的过程,在XP方法中占有极为重要的地位,也已经成为软件工程中一个不可缺少的环节.本文通过对Code Review的一些概念和经验的探讨,就如何进行C ...
- 漫谈Code Review的错误实践
从刚开始工作时到现在,已经写了7年的代码,大部分代码都被人review过,自己也review了很多人的代码.在上一家公司的时候,我负责的一轮面试是专门进行Code Review的练习和经验谈. 通过在 ...
随机推荐
- react 获取文件流导出功能
记录一下: 根据后台接口返回的文件流,前端实现导出下载,使用(react+ts) 1.请求方法 (这里写法绕开拦截器) // 导出日志 export async function exportLog( ...
- memoize
function getArea(r){ console.log(r); return Math.PI * r * r } function memoize(f){ let cache = {}; r ...
- Spring系列之基于 Java 的容器配置-9
目录 组合基于 Java 的配置 使用`@Import`注解 有条件地包含`@Configuration`类或`@Bean`方法 结合 Java 和 XML 配置 组合基于 Java 的配置 Spri ...
- PTA1004 成绩排名 (20 分)
PTA1004 成绩排名 读入 n(>0)名学生的姓名.学号.成绩,分别输出成绩最高和成绩最低学生的姓名和学号. 输入格式: 每个测试输入包含 1 个测试用例,格式为 第 1 行:正整数 n 第 ...
- MAC读写模式自动挂载硬盘/不自动挂载硬盘
一.卸载硬盘 sudo umount /dev/disk1s1 自己从磁盘工具获取设备ID 或使用终端命令:diskutil list 来获取 二.新建文件夹以供挂载,位子自选 sudo mkdir ...
- mysql 循环 例子
mysql 循环生成数据demo: DROP PROCEDURE IF EXISTS test_insert; DELIMITER;; CREATE PROCEDURE test_insert() B ...
- obj文件格式解读
学习了很长一段时间的建模,obj文件一直都在使用,但是却很少去研究过,只是知道这是软件之间的通用格式,直到最近因为刚好要在python中加载obj文件,才发现原来obj文件是如此的有规律 随便用记事本 ...
- css 多行文本展开收起
<template> <div class="content"> <div :class="[isOpen ? 'text' : 'text ...
- Spyglass CDC工具使用(五)
最近一直在搞CDC (clock domain crossing) 方面的事情,现在就CDC的一些知识点进行总结. 做CDC检查使用的是Spyglass工具.以下内容转载自:Spyglass之CDC检 ...
- 持续集成环境(3)-Jenkins用户权限管理
我们可以利用Role-based Authorization Strategy 插件来管理Jenkins用户权限 安装Role-based Authorization Strategy插件 开启权限全 ...