Error Prone is a static analysis tool for Java that catches common programming mistakes at compile-time. It helps enforce coding standards and prevent bugs before they reach production.
Understanding Error Prone
What is Error Prone?
- Java compiler plugin that performs static analysis
- Catches bugs during compilation
- Provides compiler errors with detailed explanations
- Extensible with custom rules
Key Benefits:
- Early Bug Detection: Catches issues at compile-time
- Code Quality: Enforces best practices
- Performance: Identifies inefficient patterns
- Security: Detects potential security vulnerabilities
Setup and Configuration
1. Maven Configuration
<properties>
<errorprone.version>2.21.1</errorprone.version>
<maven.compiler.version>3.11.0</maven.compiler.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven.compiler.version}</version>
<configuration>
<source>11</source>
<target>11</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-XDcompilePolicy=simple</arg>
<arg>-Xplugin:ErrorProne</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.google.errorprone</groupId>
<artifactId>error_prone_core</artifactId>
<version>${errorprone.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
2. Gradle Configuration
plugins {
id 'java'
id 'net.ltgt.errorprone' version '3.1.0'
}
dependencies {
errorprone 'com.google.errorprone:error_prone_core:2.21.1'
}
tasks.withType(JavaCompile) {
options.compilerArgs += [
'-Xplugin:ErrorProne',
'-XDcompilePolicy=simple'
]
}
3. Custom Error Prone Configuration
<!-- errorprone.xml --> <project> <bugPatterns> <!-- Disable specific checks --> <bugPattern name="FutureReturnValueIgnored"> <enabled>false</enabled> </bugPattern> <!-- Enable with custom severity --> <bugPattern name="StringEquality"> <severity>ERROR</severity> </bugPattern> <bugPattern name="UnusedVariable"> <severity>WARNING</severity> </bugPattern> </bugPatterns> <excludes> <!-- Exclude specific paths --> <exclude>**/generated/**</exclude> <exclude>**/test/**</exclude> </excludes> </project>
Core Error Prone Rules
1. Common Bug Patterns
// BAD: String equality using == instead of .equals()
public class StringEqualityExample {
public boolean isProduction(String environment) {
return environment == "production"; // Error: StringEquality
}
// GOOD: Use .equals()
public boolean isProductionFixed(String environment) {
return "production".equals(environment);
}
}
// BAD: Array reference equality
public class ArrayEqualityExample {
public boolean areEqual(int[] a, int[] b) {
return a == b; // Error: ArrayEquality
}
// GOOD: Use Arrays.equals()
public boolean areEqualFixed(int[] a, int[] b) {
return Arrays.equals(a, b);
}
}
2. Nullness Annotations
import org.jspecify.annotations.Nullable;
import org.jspecify.annotations.NonNull;
public class NullnessExample {
// BAD: Potential null return
public String getName() {
return null; // Error: ReturnMissingNullable
}
// GOOD: Annotate with @Nullable
public @Nullable String getNameFixed() {
return null;
}
// BAD: Potential NPE
public int getLength(String str) {
return str.length(); // Error: NullableDereference
}
// GOOD: Add null check
public int getLengthFixed(@Nullable String str) {
return str != null ? str.length() : 0;
}
// BAD: Passing null to @NonNull parameter
public void processUser(User user) {
validateUser(null); // Error: NullablePassedToNonNull
}
private void validateUser(@NonNull User user) {
// Method implementation
}
}
3. Collection and Stream Issues
import java.util.*;
public class CollectionExamples {
// BAD: Modifying collection during iteration
public void removeElements(List<String> list) {
for (String item : list) {
if (item.isEmpty()) {
list.remove(item); // Error: ModificationDuringIteration
}
}
}
// GOOD: Use iterator or removeIf
public void removeElementsFixed(List<String> list) {
list.removeIf(String::isEmpty);
}
// BAD: Boxed primitive collection
public void processNumbers(List<Integer> numbers) {
// Inefficient for large collections
}
// GOOD: Use primitive collections
public void processNumbersFixed(IntList numbers) {
// More efficient
}
// BAD: Stream not consumed
public void unusedStream(List<String> items) {
items.stream().filter(s -> !s.isEmpty()); // Error: StreamResourceLeak
}
// GOOD: Consume the stream
public void usedStream(List<String> items) {
List<String> nonEmpty = items.stream()
.filter(s -> !s.isEmpty())
.collect(Collectors.toList());
}
}
4. Exception Handling
public class ExceptionExamples {
// BAD: Empty catch block
public void riskyOperation() {
try {
performOperation();
} catch (Exception e) {
// Error: EmptyCatchBlock
}
}
// GOOD: Handle or log the exception
public void riskyOperationFixed() {
try {
performOperation();
} catch (Exception e) {
logger.error("Operation failed", e);
}
}
// BAD: Exception caught but not handled properly
public void readFile() {
try {
Files.readString(Path.of("file.txt"));
} catch (IOException e) {
throw new RuntimeException(); // Error: ExceptionNotThrown
}
}
// GOOD: Preserve original exception
public void readFileFixed() {
try {
Files.readString(Path.of("file.txt"));
} catch (IOException e) {
throw new RuntimeException("Failed to read file", e);
}
}
// BAD: Overly broad catch
public void processData() {
try {
parseData();
} catch (Exception e) { // Error: OverlyBroadCatch
handleError();
}
}
// GOOD: Catch specific exceptions
public void processDataFixed() {
try {
parseData();
} catch (ParseException | IOException e) {
handleError();
}
}
}
5. Resource Management
public class ResourceExamples {
// BAD: Resource not closed
public void readFile(String path) throws IOException {
FileInputStream fis = new FileInputStream(path); // Error: ResourceLeak
// Use resource but never close
}
// GOOD: Use try-with-resources
public void readFileFixed(String path) throws IOException {
try (FileInputStream fis = new FileInputStream(path)) {
// Use resource
}
}
// BAD: Database connection not closed
public void queryDatabase() throws SQLException {
Connection conn = DriverManager.getConnection("url");
Statement stmt = conn.createStatement();
// Error: Multiple resources not closed properly
}
// GOOD: Proper resource management
public void queryDatabaseFixed() throws SQLException {
try (Connection conn = DriverManager.getConnection("url");
Statement stmt = conn.createStatement()) {
// Use resources
}
}
}
6. Concurrency Issues
public class ConcurrencyExamples {
private int counter = 0;
// BAD: Non-atomic operation without synchronization
public void increment() {
counter++; // Error: NonAtomicVolatileUpdate
}
// GOOD: Use AtomicInteger
private final AtomicInteger atomicCounter = new AtomicInteger(0);
public void incrementFixed() {
atomicCounter.incrementAndGet();
}
// BAD: Synchronization on non-final field
private Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) { // Error: LockNotBeforeTry
// Critical section
}
}
// GOOD: Use final lock object
private final Object finalLock = new Object();
public void synchronizedMethodFixed() {
synchronized (finalLock) {
// Critical section
}
}
// BAD: Thread.sleep in loop
public void waitForCondition() {
while (!condition) {
try {
Thread.sleep(100); // Error: SleepInLoop
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// GOOD: Use proper waiting mechanism
private final Object monitor = new Object();
public void waitForConditionFixed() throws InterruptedException {
synchronized (monitor) {
while (!condition) {
monitor.wait(100);
}
}
}
}
7. API Misuse
public class ApiMisuseExamples {
// BAD: Optional used as field
private Optional<String> name; // Error: OptionalNotAllowed
// GOOD: Use nullable field with getter
private @Nullable String name;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
// BAD: Calling equals on enum
public boolean isActive(Status status) {
return status.equals(Status.ACTIVE); // Error: EnumOrdinal
}
// GOOD: Use == for enum comparison
public boolean isActiveFixed(Status status) {
return status == Status.ACTIVE;
}
// BAD: StringBuilder in loop
public String buildString(List<String> parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
sb = new StringBuilder(sb.toString() + part); // Error: StringBuilderReplaceableByString
}
return sb.toString();
}
// GOOD: Efficient StringBuilder usage
public String buildStringFixed(List<String> parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
sb.append(part);
}
return sb.toString();
}
}
Custom Error Prone Rules
1. Custom Bug Checker
package com.yourapp.errorprone;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodTree;
import com.sun.tools.javac.code.Symbol;
@BugPattern(
name = "LoggerNotStaticFinal",
summary = "Logger should be static final",
severity = ERROR,
tags = {STANDARD_PRACTICES}
)
public class LoggerNotStaticFinalChecker extends BugChecker
implements BugChecker.MethodTreeMatcher {
@Override
public Description matchMethod(MethodTree tree, VisitorState state) {
Symbol.MethodSymbol method = ASTHelpers.getSymbol(tree);
// Check if method returns a Logger
if (isLoggerType(method.getReturnType())) {
// Check if logger is static final
if (!isStaticFinal(method)) {
return describeMatch(tree);
}
}
return Description.NO_MATCH;
}
private boolean isLoggerType(Symbol.TypeSymbol type) {
return type != null &&
type.getQualifiedName().toString().contains("Logger");
}
private boolean isStaticFinal(Symbol.MethodSymbol method) {
return method.isStatic() && method.getModifiers().contains(FINAL);
}
}
2. Custom Annotation Checker
package com.yourapp.errorprone;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.AnnotationTree;
@BugPattern(
name = "TransactionalReadOnlyCheck",
summary = "@Transactional methods should specify readOnly attribute",
severity = WARNING,
tags = {STANDARD_PRACTICES}
)
public class TransactionalReadOnlyChecker extends BugChecker
implements BugChecker.AnnotationTreeMatcher {
private static final String TRANSACTIONAL_ANNOTATION =
"org.springframework.transaction.annotation.Transactional";
@Override
public Description matchAnnotation(AnnotationTree tree, VisitorState state) {
// Check if this is @Transactional annotation
if (!isTransactionalAnnotation(tree, state)) {
return Description.NO_MATCH;
}
// Check if readOnly attribute is specified
if (!hasReadOnlyAttribute(tree)) {
return buildDescription(tree)
.setMessage("@Transactional annotation should specify readOnly attribute")
.build();
}
return Description.NO_MATCH;
}
private boolean isTransactionalAnnotation(AnnotationTree tree, VisitorState state) {
return tree.getAnnotationType().toString().contains("Transactional");
}
private boolean hasReadOnlyAttribute(AnnotationTree tree) {
return tree.getArguments().stream()
.anyMatch(arg -> arg.toString().contains("readOnly"));
}
}
3. Custom Method Invocation Checker
package com.yourapp.errorprone;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Description;
import com.google.errorprone.util.ASTHelpers;
import com.sun.source.tree.MethodInvocationTree;
@BugPattern(
name = "SystemOutPrintlnUsage",
summary = "Avoid using System.out.println in production code",
severity = WARNING,
tags = {STANDARD_PRACTICES}
)
public class SystemOutPrintlnChecker extends BugChecker
implements BugChecker.MethodInvocationTreeMatcher {
@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
String methodName = ASTHelpers.getSymbol(tree).getSimpleName().toString();
if ("println".equals(methodName) || "print".equals(methodName)) {
// Check if it's System.out.println
String receiver = tree.getMethodSelect().toString();
if (receiver.contains("System.out")) {
return buildDescription(tree)
.setMessage("Use logger instead of System.out.println")
.addFix(suggestLoggerFix(tree))
.build();
}
}
return Description.NO_MATCH;
}
}
4. Custom Resource Management Checker
package com.yourapp.errorprone;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.VariableTree;
@BugPattern(
name = "DataSourceFieldCheck",
summary = "DataSource fields should be final and initialized in constructor",
severity = ERROR,
tags = {STANDARD_PRACTICES}
)
public class DataSourceFieldChecker extends BugChecker
implements BugChecker.VariableTreeMatcher {
@Override
public Description matchVariable(VariableTree tree, VisitorState state) {
String typeName = tree.getType().toString();
// Check if this is a DataSource field
if (typeName.contains("DataSource") || typeName.contains("ConnectionPool")) {
// Check if field is final
if (!tree.getModifiers().getFlags().contains(FINAL)) {
return buildDescription(tree)
.setMessage("DataSource fields should be declared final")
.build();
}
// Check if field is initialized
if (tree.getInitializer() == null) {
return buildDescription(tree)
.setMessage("DataSource fields should be initialized")
.build();
}
}
return Description.NO_MATCH;
}
}
Integration with Build Tools
1. Maven with Custom Rules
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>11</source> <target>11</target> <compilerArgs> <arg>-Xplugin:ErrorProne -XepOpt:ErrorProne:CustomChecks:com/yourapp/errorprone/*.class -Xep:LoggerNotStaticFinal:WARN -Xep:TransactionalReadOnlyCheck:WARN -Xep:SystemOutPrintlnUsage:ERROR -Xep:DataSourceFieldCheck:ERROR </arg> </compilerArgs> <annotationProcessorPaths> <path> <groupId>com.google.errorprone</groupId> <artifactId>error_prone_core</artifactId> <version>2.21.1</version> </path> <path> <groupId>com.yourapp</groupId> <artifactId>custom-errorprone-checks</artifactId> <version>1.0.0</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </build>
2. Error Prone Suppressions
// Suppress specific warnings with annotations
@SuppressWarnings({
"StringEquality",
"FutureReturnValueIgnored"
})
public class SuppressedExamples {
// Suppress on specific method
@SuppressWarnings("EmptyCatchBlock")
public void methodWithSuppressedWarning() {
try {
riskyOperation();
} catch (Exception e) {
// Suppressed empty catch block
}
}
// Suppress on field
@SuppressWarnings("OptionalNotAllowed")
private Optional<String> suppressedOptional;
}
3. Error Prone Configuration File
# .errorprone/config.properties error_prone.enabled=true error_prone.checker.optional_not_allowed.severity=ERROR error_prone.checker.string_equality.severity=WARN error_prone.checker.unused_variable.severity=OFF # Custom checkers error_prone.checker.logger_not_static_final.severity=ERROR error_prone.checker.transactional_read_only_check.severity=WARN # Exclude patterns error_prone.exclude_patterns=**/test/**,**/generated/**
Advanced Error Prone Usage
1. Auto-Fix Capabilities
public class AutoFixExamples {
// Error Prone can automatically fix some issues
// BAD: Can be auto-fixed to use .equals()
public boolean badStringCompare(String a, String b) {
return a == b; // Auto-fix available
}
// BAD: Can be auto-fixed to try-with-resources
public void badResourceManagement() throws IOException {
FileInputStream fis = new FileInputStream("file.txt");
try {
// use fis
} finally {
fis.close(); // Auto-fix available
}
}
// BAD: Can be auto-fixed to use enhanced for loop
public void badLoop(List<String> items) {
for (int i = 0; i < items.size(); i++) { // Auto-fix available
String item = items.get(i);
process(item);
}
}
}
2. Integration with Testing
// Test custom Error Prone rules
public class CustomCheckerTest {
@Test
public void testLoggerNotStaticFinalChecker() {
Compilation compilation = Compilation.javac()
.withCompilerOptions("-Xplugin:ErrorProne")
.withSources(JavaFileObjects.forSourceString(
"TestClass",
"import org.slf4j.Logger;\n" +
"import org.slf4j.LoggerFactory;\n" +
"public class TestClass {\n" +
" private Logger logger = LoggerFactory.getLogger(TestClass.class);\n" +
"}"
));
compilation.compile();
assertThat(compilation).hadErrorContaining("Logger should be static final");
}
}
3. Continuous Integration Integration
# GitHub Actions example name: Error Prone Check on: [push, pull_request] jobs: errorprone: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up JDK 11 uses: actions/setup-java@v3 with: java-version: '11' distribution: 'temurin' - name: Run Error Prone run: mvn compile -X
Best Practices
1. Rule Selection and Configuration
# Recommended rule configuration error_prone.checker.string_equality.severity=ERROR error_prone.checker.future_return_value_ignored.severity=WARN error_prone.checker.unused_variable.severity=WARN error_prone.checker.empty_catch_block.severity=ERROR error_prone.checker.overly_broad_catch.severity=WARN error_prone.checker.resource_leak.severity=ERROR error_prone.checker.non_atomic_volatile_update.severity=ERROR
2. Team Adoption Strategy
// Phase 1: Start with warnings
@SuppressWarnings("all") // Temporary during migration
public class MigrationExample {
// Existing code with various issues
}
// Phase 2: Enable critical rules as errors
public class CriticalRulesEnabled {
// Must fix critical issues
}
// Phase 3: Enable all recommended rules
public class AllRulesEnabled {
// All code follows Error Prone standards
}
3. Custom Rule Development
// Good custom rule characteristics
@BugPattern(
name = "MeaningfulRuleName",
summary = "Clear, actionable description",
severity = ERROR, // Or WARN for less critical issues
tags = {STANDARD_PRACTICES, PERFORMANCE} // Appropriate categories
)
public class GoodCustomChecker extends BugChecker
implements BugChecker.MethodTreeMatcher {
// Focus on specific, common problems
// Provide clear error messages
// Include suggested fixes when possible
}
Common Pitfalls and Solutions
1. False Positives
// Legitimate use cases that might trigger Error Prone
// Intentional resource not closed (managed by framework)
@SuppressWarnings("resource")
public DataSource getDataSource() {
return dataSource; // Framework manages lifecycle
}
// Intentional string comparison for interned strings
@SuppressWarnings("StringEquality")
public boolean isKnownConstant(String value) {
return value == KNOWN_CONSTANT; // KNOWNCONSTANT is interned
}
// Intentional empty catch for specific exceptions
@SuppressWarnings("EmptyCatchBlock")
public void optionalOperation() {
try {
performOptionalWork();
} catch (SpecificException e) {
// Ignore - this operation is optional
}
}
2. Performance Considerations
public class PerformanceExamples {
// Error Prone can help identify performance issues
// BAD: Inefficient string concatenation in loop
public String buildMessage(List<String> parts) {
String result = "";
for (String part : parts) {
result += part; // Error: StringConcatenationInLoop
}
return result;
}
// GOOD: Use StringBuilder
public String buildMessageFixed(List<String> parts) {
StringBuilder sb = new StringBuilder();
for (String part : parts) {
sb.append(part);
}
return sb.toString();
}
// BAD: Unnecessary boxed primitives
public void processNumbers(List<Integer> numbers) {
// Auto-boxing overhead
}
// GOOD: Use primitive collections
public void processNumbersFixed(IntList numbers) {
// More efficient
}
}
Conclusion
Error Prone provides:
- Early bug detection at compile-time
- Consistent code quality across the codebase
- Performance optimization suggestions
- Security vulnerability detection
- Custom rule development for project-specific standards
By integrating Error Prone into your build process and following the patterns shown above, you can significantly reduce bugs, improve code quality, and maintain consistent coding standards across your Java projects. The combination of built-in rules and custom checkers makes Error Prone a powerful tool for any Java development team.
Advanced Java Supply Chain Security, Kubernetes Hardening & Runtime Threat Detection
Sigstore Rekor in Java – https://macronepal.com/blog/sigstore-rekor-in-java/
Explains integrating Sigstore Rekor into Java systems to create a transparent, tamper-proof log of software signatures and metadata for verifying supply chain integrity.
Securing Java Applications with Chainguard Wolfi – https://macronepal.com/blog/securing-java-applications-with-chainguard-wolfi-a-comprehensive-guide/
Explains using Chainguard Wolfi minimal container images to reduce vulnerabilities and secure Java applications with hardened, lightweight runtime environments.
Cosign Image Signing in Java Complete Guide – https://macronepal.com/blog/cosign-image-signing-in-java-complete-guide/
Explains how to digitally sign container images using Cosign in Java-based workflows to ensure authenticity and prevent unauthorized modifications.
Secure Supply Chain Enforcement Kyverno Image Verification for Java Containers – https://macronepal.com/blog/secure-supply-chain-enforcement-kyverno-image-verification-for-java-containers/
Explains enforcing Kubernetes policies with Kyverno to verify container image signatures and ensure only trusted Java container images are deployed.
Pod Security Admission in Java Securing Kubernetes Deployments for JVM Applications – https://macronepal.com/blog/pod-security-admission-in-java-securing-kubernetes-deployments-for-jvm-applications/
Explains Kubernetes Pod Security Admission policies that enforce security rules like restricted privileges and safe configurations for Java workloads.
Securing Java Applications at Runtime Kubernetes Security Context – https://macronepal.com/blog/securing-java-applications-at-runtime-a-guide-to-kubernetes-security-context/
Explains how Kubernetes security contexts control runtime permissions, user IDs, and access rights for Java containers to improve isolation.
Process Anomaly Detection in Java Behavioral Monitoring – https://macronepal.com/blog/process-anomaly-detection-in-java-comprehensive-behavioral-monitoring-2/
Explains detecting abnormal runtime behavior in Java applications to identify potential security threats using process monitoring techniques.
Achieving Security Excellence CIS Benchmark Compliance for Java Applications – https://macronepal.com/blog/achieving-security-excellence-implementing-cis-benchmark-compliance-for-java-applications/
Explains applying CIS security benchmarks to Java environments to standardize hardening and improve overall system security posture.
Process Anomaly Detection in Java Behavioral Monitoring – https://macronepal.com/blog/process-anomaly-detection-in-java-comprehensive-behavioral-monitoring/
Explains behavioral monitoring of Java processes to detect anomalies and improve runtime security through continuous observation and analysis.
JAVA CODE COMPILER