Linting and Formatting

Why Code Quality Tools Matter

Code quality tools automate detection of bugs, code smells, style violations, and security vulnerabilities. Manual code reviews alone cannot consistently catch all issues.

Core Benefits:

  • Early bug detection: Find bugs before they reach production
  • Consistency: Enforce coding standards across team
  • Security: Detect common vulnerabilities automatically
  • Maintainability: Prevent code smell accumulation
  • Learning: Teams learn from automated feedback

Problem: Manual code reviews are time-consuming, inconsistent, and miss subtle bugs that automated tools catch immediately.

Solution: Use automated linting, formatting, and static analysis tools integrated into build process and IDE.

Code Quality Tool Comparison

ToolTypeProsConsUse When
CheckstyleStyle checkerFast, configurable, widely adoptedStyle only (no bugs)Enforce coding standards
SpotBugsBug detectorFinds real bugs, low false positivesLimited to common patternsDetect common bugs
PMDCode smell detectorDetects design issues, extensibleMore false positivesFind code smells
Error ProneCompile-time checkerFast (compile-time), Google-backedLimited to javac integrationCatch bugs during compilation
SonarQubeQuality platformComprehensive, metrics, historyResource-intensiveEnterprise quality governance
Google Java FormatFormatterDeterministic, no config neededNo customizationConsistent formatting (Google style)
Manual reviewsHuman inspectionContext-aware, architectural insightsTime-consuming, inconsistentArchitecture and design decisions

Recommendation: Use Checkstyle (style), SpotBugs (bugs), PMD (code smells), and Google Java Format (formatting) in combination. Add SonarQube for centralized quality metrics.

Recommended progression: Start with manual reviews to understand quality issues → Add automated tools to catch common problems → Integrate comprehensive quality platform.

Manual Code Reviews

Human code reviews provide context-aware feedback that automated tools cannot replicate. Use checklists to ensure consistency.

Code Review Checklist

Correctness:

  • Logic implements requirements correctly
  • Edge cases handled (null, empty, boundary values)
  • Error handling appropriate for failure scenarios
  • No off-by-one errors in loops or array access

Design:

  • Single Responsibility Principle followed
  • Dependencies injected rather than hard-coded
  • Appropriate abstraction level
  • No premature optimization

Testing:

  • Unit tests cover core logic
  • Test names clearly describe scenarios
  • Test data realistic and varied
  • No test interdependencies

Security:

  • Input validation at system boundaries
  • No SQL injection or XSS vulnerabilities
  • Sensitive data not logged or exposed
  • Authentication and authorization appropriate

Performance:

  • No obvious performance bottlenecks
  • Database queries efficient (no N+1 queries)
  • Resources properly closed (files, connections)
  • Appropriate data structures chosen

Maintainability:

  • Code self-documenting with clear names
  • Complex logic explained with comments
  • No code duplication
  • Consistent with codebase style

Why Manual Reviews Are Insufficient

Limitations:

  1. Time-consuming: Thorough reviews take significant time
  2. Inconsistent: Different reviewers have different standards
  3. Fatigue: Humans miss issues in large changesets
  4. Bias: Reviewers may overlook familiar patterns
  5. Scale: Cannot review every line in large codebases
  6. Knowledge gaps: Reviewers may not know all anti-patterns

Before: Manual reviews only, inconsistent quality After: Automated tools catch common issues, humans focus on architecture and design

Checkstyle

Checkstyle enforces coding standards and style conventions. It checks formatting, naming, imports, and other stylistic aspects.

Adding Checkstyle

Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-checkstyle-plugin</artifactId>
    <version>3.3.1</version>
    <configuration>
        <configLocation>checkstyle.xml</configLocation>
        <consoleOutput>true</consoleOutput>
        <failsOnError>true</failsOnError>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Gradle:

plugins {
    id 'checkstyle'
}

checkstyle {
    toolVersion = '10.12.7'
    configFile = file("${rootDir}/checkstyle.xml")
}

// Fail build on violations
tasks.withType(Checkstyle) {
    reports {
        xml.required = true
        html.required = true
    }
}

Checkstyle Configuration

Create checkstyle.xml with rules to enforce.

Basic checkstyle.xml:

<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
    "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
    "https://checkstyle.org/dtds/configuration_1_3.dtd">

<module name="Checker">
    <property name="charset" value="UTF-8"/>
    <property name="severity" value="error"/>
    <property name="fileExtensions" value="java"/>

    <!-- TreeWalker checks individual Java source files -->
    <module name="TreeWalker">
        <!-- Naming Conventions -->
        <module name="TypeName">
            <property name="format" value="^[A-Z][a-zA-Z0-9]*$"/>
        </module>
        <module name="MethodName">
            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
        </module>
        <module name="ConstantName">
            <property name="format" value="^[A-Z][A-Z0-9]*(_[A-Z0-9]+)*$"/>
        </module>
        <module name="LocalVariableName">
            <property name="format" value="^[a-z][a-zA-Z0-9]*$"/>
        </module>
        <module name="PackageName">
            <property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
        </module>

        <!-- Imports -->
        <module name="AvoidStarImport"/>
        <module name="UnusedImports"/>
        <module name="RedundantImport"/>
        <module name="ImportOrder">
            <property name="groups" value="java,javax,org,com"/>
            <property name="ordered" value="true"/>
            <property name="separated" value="true"/>
        </module>

        <!-- Whitespace -->
        <module name="WhitespaceAround"/>
        <module name="WhitespaceAfter"/>
        <module name="NoWhitespaceBefore"/>
        <module name="EmptyLineSeparator">
            <property name="allowNoEmptyLineBetweenFields" value="true"/>
        </module>

        <!-- Blocks -->
        <module name="LeftCurly"/>
        <module name="RightCurly"/>
        <module name="NeedBraces"/>
        <module name="EmptyBlock"/>

        <!-- Coding -->
        <module name="EmptyStatement"/>
        <module name="EqualsHashCode"/>
        <module name="SimplifyBooleanExpression"/>
        <module name="SimplifyBooleanReturn"/>
        <module name="StringLiteralEquality"/>
        <module name="OneStatementPerLine"/>
        <module name="MultipleVariableDeclarations"/>

        <!-- Design -->
        <module name="FinalClass"/>
        <module name="HideUtilityClassConstructor"/>
        <module name="InterfaceIsType"/>
        <module name="VisibilityModifier"/>

        <!-- Miscellaneous -->
        <module name="ArrayTypeStyle"/>
        <module name="UpperEll"/>
        <module name="ModifierOrder"/>
    </module>

    <!-- File checks -->
    <module name="FileTabCharacter"/>
    <module name="NewlineAtEndOfFile"/>
</module>

Common Checkstyle Rules

Naming conventions:

// GOOD: Class name PascalCase
public class PaymentService { }
// => PascalCase: first letter uppercase, each word capitalized
// => Naming convention: matches Java language standards for classes

// BAD: Class name lowercase
public class paymentService { }
// => VIOLATION: class names must start with uppercase letter
// => Checkstyle error: TypeName check fails

// GOOD: Method name camelCase
public void processPayment() { }
// => camelCase: first letter lowercase, subsequent words capitalized
// => Convention: standard Java naming for methods and variables

// BAD: Method name PascalCase
public void ProcessPayment() { }
// => VIOLATION: method names must start with lowercase letter
// => Checkstyle error: MethodName check fails
// => Looks like class: confuses readers

// GOOD: Constant UPPER_SNAKE_CASE
private static final int MAX_RETRIES = 3;
// => UPPER_SNAKE_CASE: all caps, words separated by underscores
// => static final: constants use screaming snake case convention

// BAD: Constant camelCase
private static final int maxRetries = 3;
// => VIOLATION: constants must use UPPER_SNAKE_CASE
// => Checkstyle error: ConstantName check fails

Import rules:

// BAD: Star imports
import java.util.*;
// => VIOLATION: star import (*) imports entire package
// => Problems: pollutes namespace, hides which classes used
// => Checkstyle error: AvoidStarImport check fails
import com.example.*;
// => Wildcard import: imports all public classes from package
// => Conflicts: higher risk of class name collisions

// GOOD: Explicit imports
import java.util.List;
// => Explicit import: only imports List interface
// => Clear dependency: shows exactly which classes used
import java.util.ArrayList;
// => Specific import: imports only ArrayList implementation
import com.example.PaymentService;
// => Fully qualified: shows exact class dependency
// => Readability: easier to understand code dependencies

// BAD: Unused import
import java.util.Map; // Not used in code
// => VIOLATION: imports class never used in file
// => Checkstyle error: UnusedImports check fails
// => Dead code: adds noise, confuses readers

// GOOD: Only used imports
import java.util.List;
// => Used import: List actually referenced in code
// => Clean: only necessary imports present

Whitespace rules:

// GOOD: Proper spacing
public class Example {
// => Space before brace: "Example {" not "Example{"
// => WhitespaceAround: spaces around operators and keywords
    public void method(int x, int y) {
// => Space after comma: "x, int y" not "x,int y"
// => WhitespaceAfter: comma, semicolon spacing
        if (x > 0) {
// => Space after keyword: "if (" not "if("
// => Space around operator: "x > 0" not "x>0"
// => Readability: spacing improves visual parsing
            return;
        }
    }
}

// BAD: Missing spaces
public class Example{
// => VIOLATION: no space before brace "Example{" should be "Example {"
// => Checkstyle error: WhitespaceAround check fails
    public void method(int x,int y){
// => VIOLATION: missing space after comma "x,int" should be "x, int"
// => VIOLATION: no space before brace "{" after parameter list
        if(x>0){
// => VIOLATION: no space after if keyword "if(" should be "if ("
// => VIOLATION: no spaces around operator "x>0" should be "x > 0"
// => Hard to read: cramped spacing reduces readability
            return;
        }
    }
}

Running Checkstyle

# Maven
mvn checkstyle:check

# Generate HTML report
mvn checkstyle:checkstyle
# Report: target/site/checkstyle.html

# Gradle
./gradlew checkstyleMain checkstyleTest

# Reports: build/reports/checkstyle/

Suppressing Violations

Suppress specific violations when necessary (use sparingly).

Suppress in code:

// Suppress specific check
@SuppressWarnings("checkstyle:MagicNumber")
// => @SuppressWarnings: annotation disables specific Checkstyle check
// => checkstyle:MagicNumber: suppresses magic number warnings for this method
public void calculate() {
    int result = value * 100; // Magic number acceptable here
// => 100 allowed: magic number suppression permits literal
// => Use sparingly: document why suppression needed
}

// Suppress for entire file
// checkstyle:off
// => Comment directive: disables all Checkstyle checks below this line
public class LegacyCode {
// => Legacy code: may have many violations during migration
    // Legacy code with many violations
// => Temporary suppression: should fix violations and remove later
}
// checkstyle:on
// => Re-enables checks: subsequent code must follow standards

Suppress in configuration:

<module name="SuppressionFilter">
    <property name="file" value="checkstyle-suppressions.xml"/>
</module>

checkstyle-suppressions.xml:

<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
    "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
    "https://checkstyle.org/dtds/suppressions_1_2.dtd">

<suppressions>
    <!-- Suppress all checks for generated code -->
    <suppress files=".*[/\\]generated[/\\].*" checks=".*"/>

    <!-- Suppress specific check for specific files -->
    <suppress files="LegacyService\.java" checks="MethodLength"/>
</suppressions>

SpotBugs

SpotBugs (successor to FindBugs) detects common bugs through bytecode analysis. It finds real bugs like null pointer dereferences, resource leaks, and threading issues.

Adding SpotBugs

Maven:

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>4.8.3.0</version>
    <configuration>
        <effort>Max</effort>
        <threshold>Low</threshold>
        <failOnError>true</failOnError>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Gradle:

plugins {
    id 'com.github.spotbugs' version '6.0.6'
}

spotbugs {
    effort = 'max'
    reportLevel = 'low'
}

spotbugsMain {
    reports {
        html.required = true
        xml.required = false
    }
}

Bug Categories

SpotBugs detects bugs in multiple categories.

Common bug patterns:

CategoryExample BugsSeverity
CorrectnessNull pointer dereference, equals() comparison bugsHigh
Bad practiceIgnored return value, equals() without hashCode()Medium
Dodgy codeDead local store, useless control flowLow-Med
PerformanceInefficient string concatenation in loopsMedium
MultithreadedInconsistent synchronization, double-checked lockingHigh
SecuritySQL injection, XSS vulnerabilitiesHigh
Malicious codePublic mutable fields, finalizer vulnerabilitiesMedium

Example bugs detected:

// BUG: Null pointer dereference
public void process(String value) {
// => BUG: calls method on potentially null value
    if (value == null) {
// => Null check: detects null case
        System.out.println("Null value");
// => Logs null: but doesn't return, continues execution
    }
    // SpotBugs: Possible null pointer dereference
// => SpotBugs warning: value could be null at this point
    System.out.println(value.length());
// => CRASH RISK: NullPointerException if value is null
// => Bug detected: if condition doesn't prevent continuation
}

// FIXED: Proper null handling
public void process(String value) {
// => FIXED: early return prevents null pointer dereference
    if (value == null) {
// => Guard clause: checks null condition
        System.out.println("Null value");
// => Logs null case
        return;
// => Early exit: prevents execution of value.length() when null
    }
    System.out.println(value.length());
// => Safe call: guaranteed non-null after guard clause
}

// BUG: equals() without hashCode()
public class User {
// => BUG: violates equals-hashCode contract
    private String id;

    @Override
    public boolean equals(Object obj) {
// => Overrides equals: custom equality logic
        if (!(obj instanceof User)) return false;
// => Type check: ensures obj is User instance
        return this.id.equals(((User) obj).id);
// => ID comparison: considers users equal if IDs match
    }
    // SpotBugs: equals() defined without hashCode()
// => VIOLATION: equals() overridden but hashCode() not implemented
// => Contract broken: equal objects may have different hash codes
// => HashMap/HashSet broken: cannot find equal objects
}

// FIXED: Implement both
public class User {
// => FIXED: implements both equals() and hashCode()
    private String id;

    @Override
    public boolean equals(Object obj) {
// => Equality: custom comparison logic
        if (!(obj instanceof User)) return false;
        return this.id.equals(((User) obj).id);
// => ID-based equality: users equal if IDs equal
    }

    @Override
    public int hashCode() {
// => hashCode implementation: satisfies equals-hashCode contract
        return Objects.hash(id);
// => Objects.hash(): generates hash from fields used in equals()
// => Contract satisfied: equal objects have same hash code
// => HashMap/HashSet work: can find equal objects in hash-based collections
    }
}

Running SpotBugs

# Maven
mvn spotbugs:check

# Generate HTML report
mvn spotbugs:spotbugs
# Report: target/spotbugsXml.xml, target/site/spotbugs.html

# Gradle
./gradlew spotbugsMain spotbugsTest

# Reports: build/reports/spotbugs/

Excluding False Positives

Create exclusion filter for false positives.

spotbugs-exclude.xml:

<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
    <!-- Exclude specific bug in specific class -->
    <Match>
        <Class name="com.example.LegacyService"/>
        <Bug pattern="NP_NULL_ON_SOME_PATH"/>
    </Match>

    <!-- Exclude entire class from analysis -->
    <Match>
        <Class name="com.example.GeneratedCode"/>
    </Match>

    <!-- Exclude specific bug pattern everywhere -->
    <Match>
        <Bug pattern="URF_UNREAD_FIELD"/>
    </Match>

    <!-- Exclude generated code -->
    <Match>
        <Package name="~.*\.generated\..*"/>
    </Match>
</FindBugsFilter>

Maven configuration:

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <version>4.8.3.0</version>
    <configuration>
        <excludeFilterFile>spotbugs-exclude.xml</excludeFilterFile>
    </configuration>
</plugin>

PMD

PMD detects code smells, potential bugs, and inefficient code through source code analysis. It’s more opinionated than SpotBugs.

Adding PMD

Maven:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-pmd-plugin</artifactId>
    <version>3.21.2</version>
    <configuration>
        <rulesets>
            <ruleset>/rulesets/java/quickstart.xml</ruleset>
        </rulesets>
        <failOnViolation>true</failOnViolation>
        <printFailingErrors>true</printFailingErrors>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Gradle:

plugins {
    id 'pmd'
}

pmd {
    toolVersion = '6.55.0'
    ruleSetFiles = files("pmd-ruleset.xml")
    ignoreFailures = false
}

PMD Rule Categories

Common rule sets:

Rule SetFocusExample Rules
Best PracticesGeneral coding best practicesUnusedPrivateMethod, SystemPrintln
Code StyleNaming and formatting conventionsShortVariable, LongVariable
DesignDesign flaws and complexityGodClass, TooManyMethods
DocumentationComment and documentation requirementsUncommentedEmptyConstructor
Error ProneCode likely to cause errorsEmptyCatchBlock, CloseResource
MultithreadingThreading issuesAvoidThreadGroup, AvoidSynchronizedAtMethodLevel
PerformancePerformance anti-patternsAvoidInstantiatingObjectsInLoops
SecuritySecurity vulnerabilitiesHardCodedCryptoKey

Custom PMD Ruleset

Create custom ruleset (pmd-ruleset.xml).

pmd-ruleset.xml:

<?xml version="1.0"?>
<ruleset name="Custom Rules"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0
                             https://pmd.sourceforge.io/ruleset_2_0_0.xsd">

    <description>Custom PMD ruleset for project</description>

    <!-- Best Practices -->
    <rule ref="category/java/bestpractices.xml">
        <!-- Exclude specific rules -->
        <exclude name="JUnitTestsShouldIncludeAssert"/>
    </rule>

    <!-- Code Style -->
    <rule ref="category/java/codestyle.xml/ShortVariable">
        <properties>
            <property name="minimum" value="3"/>
        </properties>
    </rule>

    <rule ref="category/java/codestyle.xml/LongVariable">
        <properties>
            <property name="maximum" value="40"/>
        </properties>
    </rule>

    <!-- Design -->
    <rule ref="category/java/design.xml">
        <exclude name="LawOfDemeter"/>
    </rule>

    <!-- Error Prone -->
    <rule ref="category/java/errorprone.xml"/>

    <!-- Performance -->
    <rule ref="category/java/performance.xml"/>

    <!-- Security -->
    <rule ref="category/java/security.xml"/>
</ruleset>

Running PMD

# Maven
mvn pmd:check

# Generate HTML report
mvn pmd:pmd
# Report: target/site/pmd.html

# Gradle
./gradlew pmdMain pmdTest

# Reports: build/reports/pmd/

Common PMD Violations

Example violations:

// VIOLATION: EmptyCatchBlock
try {
// => Try block: executes risky operation
    riskyOperation();
// => May throw exception: but catch block ignores it
} catch (Exception e) {
// => Empty catch: swallows exception without handling
    // Empty catch block
// => PMD VIOLATION: EmptyCatchBlock rule triggered
// => Silent failure: errors go unnoticed, debugging impossible
}

// FIXED: Handle or log exception
try {
// => Try block: same risky operation
    riskyOperation();
} catch (Exception e) {
// => Catches exception: proper error handling
    logger.error("Risk operation failed", e);
// => Logs error: records failure with stack trace for debugging
    throw new ServiceException("Operation failed", e);
// => Wraps exception: converts to domain exception, propagates to caller
// => Proper handling: errors visible, actionable, traceable
}

// VIOLATION: AvoidInstantiatingObjectsInLoops
for (int i = 0; i < 1000; i++) {
// => Loop: executes 1000 times
    Object temp = new Object(); // Creates 1000 objects
// => PMD VIOLATION: creates new object every iteration
// => Performance impact: 1000 objects allocated, garbage collected
// => Memory churn: unnecessary allocations strain GC
    process(temp);
}

// FIXED: Reuse object if possible
Object temp = new Object();
// => Creates once: single object allocation before loop
for (int i = 0; i < 1000; i++) {
// => Reuses object: same instance for all iterations
    process(temp);
// => Better performance: one allocation instead of 1000
// => Reduced GC pressure: fewer objects to collect
}

// VIOLATION: UnusedPrivateMethod
public class Service {
// => Service class with dead code
    private void helperMethod() {
// => PMD VIOLATION: UnusedPrivateMethod detected
        // Never called
// => Dead code: method defined but never invoked
// => Code smell: confuses readers, increases maintenance burden
    }
}

// FIXED: Remove unused method
public class Service {
// => Clean class: no dead code
    // Method removed
// => Improved: only necessary methods remain
}

Error Prone

Error Prone is a compile-time static analysis tool from Google that catches common Java mistakes during compilation.

Adding Error Prone

Maven (with Compiler Plugin):

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.12.1</version>
            <configuration>
                <source>21</source>
                <target>21</target>
                <compilerArgs>
                    <arg>-XDcompilePolicy=simple</arg>
                    <arg>-Xplugin:ErrorProne</arg>
                </compilerArgs>
                <annotationProcessorPaths>
                    <path>
                        <groupId>com.google.errorprone</groupId>
                        <artifactId>error_prone_core</artifactId>
                        <version>2.24.1</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Gradle:

plugins {
    id 'java'
    id 'net.ltgt.errorprone' version '3.1.0'
}

dependencies {
    errorprone 'com.google.errorprone:error_prone_core:2.24.1'
}

tasks.withType(JavaCompile) {
    options.errorprone.enabled = true
}

Error Prone Bug Patterns

Error Prone detects bugs at compile time.

Common patterns:

// BUG: Precedence confusion
if (x = y) { // Assignment instead of comparison
// => ERROR PRONE BUG: assignment (=) instead of comparison (==)
// => Assigns y to x: always evaluates to y's value (truthy if non-zero)
    // Error Prone: "Assignment in condition"
// => Compile-time error: Error Prone catches this during compilation
// => Common typo: single = instead of double ==
}

// FIXED
if (x == y) {
// => FIXED: proper comparison operator (==)
    // Proper comparison
// => Compares values: returns boolean (true if equal, false if not)
// => Error Prone passes: correct comparison operator
}

// BUG: String comparison
if (str == "test") { // Reference comparison
// => ERROR PRONE BUG: reference equality (==) instead of value equality
// => Compares references: only true if same object in memory
    // Error Prone: "StringEquality"
// => Wrong for strings: "test" and new String("test") are different references
// => Compile-time warning: Error Prone detects string == pattern
}

// FIXED
if (str.equals("test")) {
// => FIXED: value comparison using equals()
    // Value comparison
// => Compares content: returns true if strings have same characters
// => Correct for strings: works regardless of object identity
}

// BUG: Collection incompatible type
List<String> list = Arrays.asList("a", "b");
list.contains(123); // Wrong type
// Error Prone: "CollectionIncompatibleType"

// FIXED
list.contains("123"); // Correct type

Configuration

Disable specific checks:

tasks.withType(JavaCompile) {
    options.errorprone {
        // Disable specific check
        disable("StringSplitter")

        // Set severity
        error("NullAway")
        warn("FallThrough")
    }
}

Code Formatting

Consistent code formatting improves readability and reduces diff noise in version control.

Google Java Format

Google Java Format is a deterministic formatter with zero configuration.

Maven:

<plugin>
    <groupId>com.spotify.fmt</groupId>
    <artifactId>fmt-maven-plugin</artifactId>
    <version>2.21.1</version>
    <executions>
        <execution>
            <goals>
                <goal>format</goal>
            </goals>
        </execution>
    </executions>
</plugin>
# Format code
mvn fmt:format

# Check formatting
mvn fmt:check

Gradle:

plugins {
    id 'com.diffplug.spotless' version '6.23.3'
}

spotless {
    java {
        googleJavaFormat('1.18.1')
    }
}
# Format code
./gradlew spotlessApply

# Check formatting
./gradlew spotlessCheck

Benefits:

  • Zero config: No configuration debates
  • Deterministic: Always produces same output
  • Fast: Quick formatting
  • Google style: Follows Google Java Style Guide

EditorConfig

EditorConfig maintains consistent coding styles across different editors.

.editorconfig:

# Root configuration
root = true

# Java files
[*.java]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 4
max_line_length = 100

# XML files
[*.xml]
indent_style = space
indent_size = 2

# Properties files
[*.properties]
charset = utf-8

SonarQube

SonarQube is an enterprise code quality platform providing centralized metrics, issue tracking, and quality gates.

SonarQube Architecture

Components:

  • SonarQube Server: Web interface, database, analysis engine
  • Scanner: Analyzes code and sends results to server
  • Quality Gate: Pass/fail criteria for code quality
  • Quality Profile: Rule configuration for analysis

Adding SonarQube Scanner

Maven:

<plugin>
    <groupId>org.sonarsource.scanner.maven</groupId>
    <artifactId>sonar-maven-plugin</artifactId>
    <version>3.10.0.2594</version>
</plugin>
# Run analysis (requires SonarQube server)
mvn clean verify sonar:sonar \
  -Dsonar.projectKey=myproject \
  -Dsonar.host.url=http://localhost:9000 \
  -Dsonar.login=sqp_token

Gradle:

plugins {
    id 'org.sonarqube' version '4.4.1.3373'
}

sonar {
    properties {
        property "sonar.projectKey", "myproject"
        property "sonar.host.url", "http://localhost:9000"
    }
}
./gradlew sonar \
  -Dsonar.login=sqp_token

Quality Metrics

SonarQube tracks multiple quality metrics.

Key metrics:

MetricDescriptionTarget
CoveragePercentage of code covered by tests≥ 80%
DuplicationsPercentage of duplicated code blocks< 3%
BugsReliability issues0 (or minimal)
VulnerabilitiesSecurity issues0
Code SmellsMaintainability issues< 0.5% density
Technical DebtEstimated time to fix all issues< 5% ratio
Cognitive ComplexityHow hard code is to understandLow

Quality Gates

Quality gates define pass/fail criteria for builds.

Example quality gate:

Conditions:
  - Coverage ≥ 80%
  - Duplicated Lines < 3%
  - New Bugs = 0
  - New Vulnerabilities = 0
  - New Code Smells ≤ 5
  - Security Rating = A
  - Maintainability Rating ≤ A

Fail build on quality gate:

# Maven
mvn verify sonar:sonar -Dsonar.qualitygate.wait=true

# Gradle
./gradlew sonar -Dsonar.qualitygate.wait=true

CI Integration

GitHub Actions example:

name: SonarQube Analysis

on: [push, pull_request]

jobs:
  sonarqube:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0 # Shallow clones disabled for better analysis

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: "21"
          distribution: "temurin"

      - name: Cache SonarQube packages
        uses: actions/cache@v3
        with:
          path: ~/.sonar/cache
          key: ${{ runner.os }}-sonar

      - name: Build and analyze
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
          SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
        run: mvn verify sonar:sonar -Dsonar.qualitygate.wait=true

Best Practices

Failing Builds on Violations

Configure tools to fail builds when violations occur.

Checkstyle:

<configuration>
    <failOnViolation>true</failOnViolation>
</configuration>

SpotBugs:

<configuration>
    <failOnError>true</failOnError>
</configuration>

PMD:

<configuration>
    <failOnViolation>true</failOnViolation>
</configuration>

Gradual Adoption

Don’t enable all rules at once on existing codebases.

Adoption strategy:

  1. Start with errors only: Enable critical bug detection
  2. Add style checks: Enforce on new code only
  3. Refactor gradually: Fix violations in modules being modified
  4. Increase strictness: Tighten rules as codebase improves
  5. Full enforcement: Apply all rules to entire codebase

Example (gradual Checkstyle):

<!-- Phase 1: Critical only -->
<module name="NullPointerException"/>
<module name="EmptyCatchBlock"/>

<!-- Phase 2: Add naming -->
<module name="TypeName"/>
<module name="MethodName"/>

<!-- Phase 3: Add formatting -->
<module name="WhitespaceAround"/>
<module name="Indentation"/>

Team Consensus on Rules

Process:

  1. Review defaults: Examine default rule sets
  2. Discuss preferences: Team meeting to discuss controversial rules
  3. Document decisions: Record rationale for rule choices
  4. Trial period: Test rules for 2-4 weeks
  5. Adjust: Fine-tune based on feedback
  6. Lock: Commit final configuration to version control

Avoid:

  • Individual developers disabling rules without discussion
  • Excessive suppression comments
  • Ignoring tool output

CI Integration

Run quality checks in continuous integration.

Benefits:

  • Automatic enforcement: Every commit checked
  • Pull request feedback: Violations shown in PR
  • Quality trends: Track improvements over time
  • Fail fast: Catch issues before code review

GitHub Actions example (all tools):

name: Code Quality

on: [push, pull_request]

jobs:
  quality:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: "21"
          distribution: "temurin"
          cache: "maven"

      - name: Checkstyle
        run: mvn checkstyle:check

      - name: SpotBugs
        run: mvn spotbugs:check

      - name: PMD
        run: mvn pmd:check

      - name: Tests with Coverage
        run: mvn test jacoco:report

      - name: SonarQube
        env:
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
        run: mvn sonar:sonar

Related Content

Last updated