Applying the Spotbugs Gradle Plugin for Android
First let me start off by saying that this was a long battle of trial and error. The documentation at the time of writing this is scattered across their Read the Docs, their GitHub, and the Gradle Plugin Site. All of these in concert are confusing at best and contradictory far too often. Add in the complexity of wanting to keep Spotbugs in a separate gradle file and your project having flavors and build types... everything starts to fall apart pretty fast from there on out.
But I put in the time and have, what I think, is a solution for everyone.
Gradle Version
As an FYI, I was using Gradle version 5.6.4
and Android Gradle Plugin version 3.6.3
when I built this, but I have verified that it does work for Gradle version 6.1.1
and Android Gradle Plugin version 4.0.0
as well.
The Setup
First I'm going to tackle the build flavor and type variant problem. Over the years I have developed a handy little gradle script that lets me define these in one place, in two arrays, and from there generate and run closures that can really do anything for me.
./variants.gradle
def buildTypes = ['debug', 'release']
def buildFlavors = ['apple', 'blackberry']
ext {
createFlavorTypeVariants = { closure ->
buildTypes.each { type ->
buildFlavors.each { flavor ->
closure(type, flavor)
}
}
}
createTypeOnlyVariants = { closure ->
buildTypes.each { type ->
closure(type, buildFlavors)
}
}
}
In this file, I create the two arrays for my build types and flavors. Next, I add the createFlavorTypeVariants
function that takes in a closure, iterates through both the types and the flavors, and then finally passes the current flavor and type to the closure so it can do its magic. The second function, createTypeOnlyVariants
does basically the same thing, but instead of cycling through the flavors, it passes the whole array of flavors into the closure. This was made as a convenience to me in some more complex projects, and now it is a habit of mine.
The beauty of this is that you could just make the buildFlavors
array contain only a single element that is an empty string, like: def buildFlavors = ['']
and everything would work for a flavorless project too.
Prepping your Project
Now it is time to open the root level build.gradle
file and make some additions to get ready for Spotbugs. In this example, I'm going to only show the areas that should matter for getting this up and running.
./build.gradle
// ..
buildscript {
repositories {
// ...
maven { url "https://plugins.gradle.org/m2/" }
}
dependencies {
// ...
classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.3.0"
}
}
apply from: "$project.rootDir/variants.gradle"
createFlavorTypeVariants { type, flavor ->
def flavorType = "$flavor${type.capitalize()}"
task "spotbugs${flavorType.capitalize()}"(type: DefaultTask) {
group 'Verification'
description "Run Spotbugs on the whole project."
}
}
ext {
SpotBugsTask = com.github.spotbugs.snom.SpotBugsTask
}
subprojects {
// ...
apply from: "$project.rootDir/spotbugs.gradle"
}
// ...
In here I'm applying the ./variants.gradle
file by using apply from:
, think of this as an <include>
tag, it basically just runs the script as if it were copied and pasted into the calling gradle file. Once I have that applied I can actually use it to generate some shell tasks to help us setup the Spotbugs gradle tasks' dependency structure to something we can use.
Next, you will see that I'm creating something called SpotBugsTask
in ext
. This is a quirk in the Spotbugs plugin, the class name isn't findable in the separate ./spotbugs.gradle
file unless you define classpath "gradle.plugin.com.github.spotbugs.snom:spotbugs-gradle-plugin:4.3.0"
there (which also means providing the repository for it). As this is meant to reduce the amount of code you need to write, I included this workaround to allow the ./spotbugs.gradle
file to use the SpotBugsTask
without the need for copying all of that into another place. Hacky? A bit. Better than the alternative? Yes.
Finally, you can see that I'm applying the ./spotbugs.gradle
in the subprojects
section. This will (when we create the file) apply it to all sub-projects of our root project, but not the root project itself.
Bring on the Spotbugs
./spotbugs.gradle
apply from: "$project.rootDir/variants.gradle"
apply plugin: "com.github.spotbugs"
project.tasks.withType(SpotBugsTask) {
group 'Verification'
description 'Run Spotbugs on this project.'
reports {
xml.enabled = false
html.enabled = true
}
}
spotbugs {
effort = 'max'
reportLevel = 'high'
}
project.plugins.whenPluginAdded { newPlugin ->
switch (newPlugin.class.name) {
case 'com.android.build.gradle.LibraryPlugin':
createFlavorTypeVariants { type, flavor ->
project.rootProject.tasks.findByName("spotbugs${flavor.capitalize()}${type.capitalize()}").dependsOn(":${project.path}:spotbugs${type.capitalize()}")
}
break
case 'com.android.build.gradle.AppPlugin':
createFlavorTypeVariants { type, flavor ->
def flavorType = "$flavor${type.capitalize()}"
project.rootProject.tasks.findByName("spotbugs${flavorType.capitalize()}").dependsOn(":${project.path}:spotbugs${flavorType.capitalize()}")
}
break
}
}
As you can see it is a pretty standard implementation of Spotbugs. Our old friend ./variants.gradle
shows up right off the bat, then we actually apply the plugin. Next we find each task that the plugin made for us and update its group
and description
... why those are missing, I'll never know. I set up the reports I care about... html.enabled = true
. The last run of the mill things I do is set some plugin-wide parameters, like effort
and reportLevel
.
Last but not least, we setup a listener for when new plugins are added to this project. When the plugin gets added we see if it is either an apply plugin: com.android.application
or apply plugin: com.android.library
and we make ./variants.gradle
earn its keep by finding the root level tasks we made in ./build.gradle
and having them depend on the new Spotbugs tasks we just created by applying the plugin above.
You might be asking why go through the hassle of waiting for new plugins to be added before setting this up? Well we need to know if we care about flavors for the module, and it is a best practice to keep the idea of flavors out of your Android Library Modules if you can, this results in the need to set them up differently than the Application Module. Next, gradle throws some ordering issues your way. Everything in the ./build.gradle
is run before the sub-projects' equivalent. Meaning, the Android Application and Library plugins haven't even been applied yet when we call from the root ./build.gradle
, so you can't tell what version of dependencies you need to set up.
So, we wait for plugins to be applied. When one is, we check to see if it was an Android Application or Library Plugin and then create the task dependencies accordingly.
Running Spotbugs
Now everything is set up and you should be able to run your Java through Spotbugs and view the HTML reports in your multi-module, flavored, Android Project. You can either do this by calling individual spotbugs tasks on each project, or you can use the rolled up root level ones we created to run them efficiently on the whole project. In this example, lets say I had an :app
module for the Android Application and a :my-android-library
module as an Android Library. I could call ./gradlew spotbugsBlackberryDebug
and the following would run...
...
> Task :my-android-library:spotbugsDebug
...
> Task :app:spotbugsBlackberryDebug
...
> Task :spotbugsBlackberryDebug
BUILD SUCCESSFUL in 2s
23 actionable tasks: 11 executed, 12 up-to-date
11:42:22 PM: Task execution finished 'spotbugsBlackberryDebug'.
Going Forward
The best part of this technique for applying the Spotbugs Plugin, is that it is future proof for your project. Notice how we never touched ./app/build.gradle
or ./my-android-library/build.gradle
, that means that anytime we add a new module to our project, it will automatically have Spotbugs running on it! That makes it so much easier to stay on top of these types of tools, because the risk of discovering that these types of tools weren't running on some section of the codebase for various reasons and now there are +600 issues that they flagged and no time to fix them... not like I'm speaking from experience.