如何实现检查项目中所有package-info文件的包级注解一致性?
Great question! Checking consistent package-level annotations across all package-info.java files is a common need for maintaining code standards, and it's totally reasonable to feel frustrated that default rules for Checkstyle/PMD/FindBugs don’t cover this out of the box. Here are four practical, actionable ways to implement this check:
1. Custom Checkstyle Rule
Checkstyle is highly extensible, so you can build a custom check tailored to your annotation requirement:
First, create a check class that extends AbstractCheck:
import com.puppycrawl.tools.checkstyle.api.AbstractCheck; import com.puppycrawl.tools.checkstyle.api.DetailAST; import com.puppycrawl.tools.checkstyle.api.TokenTypes; import com.puppycrawl.tools.checkstyle.utils.AnnotationUtil; public class RequiredPackageAnnotationCheck extends AbstractCheck { private static final String REQUIRED_ANNOTATION = "javax.annotation.ParametersAreNonnullByDefault"; private static final String ANNOTATION_SIMPLE_NAME = REQUIRED_ANNOTATION.substring(REQUIRED_ANNOTATION.lastIndexOf('.') + 1); @Override public int[] getDefaultTokens() { return new int[]{TokenTypes.PACKAGE_DEF}; } @Override public int[] getAcceptableTokens() { return getDefaultTokens(); } @Override public int[] getRequiredTokens() { return getDefaultTokens(); } @Override public void visitToken(DetailAST ast) { // Verify the package declaration has the required annotation boolean hasRequiredAnnotation = AnnotationUtil.containsAnnotation(ast, REQUIRED_ANNOTATION); if (!hasRequiredAnnotation) { log(ast.getLineNo(), "Package must be annotated with @" + ANNOTATION_SIMPLE_NAME); } } }
Next:
- Package this class into a JAR and add it to your Checkstyle classpath
- Update your Checkstyle config XML to include the new rule:
<module name="RequiredPackageAnnotationCheck"/>
2. Custom PMD Rule
PMD supports both XPath-based and Java-based custom rules. For simplicity, here's an XPath rule that targets missing annotations in package-info.java:
Add this to your PMD ruleset XML:
<rule name="RequiredPackageAnnotation" language="java" message="Package must include @ParametersAreNonnullByDefault"> <description>Enforces consistent package-level annotations across all package-info.java files</description> <priority>3</priority> <pattern> <![CDATA[ //PackageDeclaration[not(Annotation/MarkerAnnotation/Name[text()='ParametersAreNonnullByDefault'] or Annotation/NormalAnnotation/Name[text()='ParametersAreNonnullByDefault'])] ]]> </pattern> <example> <![CDATA[ // Bad: Missing required annotation package com.example; // Good: Annotation and import are present @ParametersAreNonnullByDefault package com.example; import javax.annotation.ParametersAreNonnullByDefault; ]]> </example> </rule>
Run PMD with this ruleset, and it will flag any package-info.java files missing the annotation.
3. Gradle/Maven Build Script Check
If you want a lightweight solution without adding static analysis tool customizations, you can add a build task to validate the files directly:
Gradle Example
task checkPackageAnnotations { doLast { def requiredAnnotation = "@ParametersAreNonnullByDefault" def requiredImport = "import javax.annotation.ParametersAreNonnullByDefault;" fileTree(dir: "src/main/java", include: "**/package-info.java").each { file -> def content = file.text if (!content.contains(requiredAnnotation) || !content.contains(requiredImport)) { throw new GradleException("Invalid package-info file: ${file.path}\nMissing required annotation or import") } } } } // Attach to the standard check task so it runs automatically check.dependsOn(checkPackageAnnotations)
Maven Example
Use the exec-maven-plugin to run a simple script (or a small Java utility class) that checks the files during the verify phase.
4. Custom Java Utility with AST Parsing
For more robust validation (avoiding false positives from text-only matching), use an AST parser like JavaParser to inspect the structure of package-info.java files:
First, add JavaParser as a dependency, then write this utility:
import com.github.javaparser.JavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.PackageDeclaration; import com.github.javaparser.ast.expr.AnnotationExpr; import java.io.File; import java.io.FileInputStream; import java.io.IOException; public class PackageAnnotationChecker { private static final String REQUIRED_ANNOTATION = "ParametersAreNonnullByDefault"; private static final String REQUIRED_IMPORT = "javax.annotation.ParametersAreNonnullByDefault"; public static void main(String[] args) throws IOException { File srcDir = new File("src/main/java"); scanDirectory(srcDir); } private static void scanDirectory(File dir) throws IOException { File[] files = dir.listFiles(); if (files == null) return; for (File file : files) { if (file.isDirectory()) { scanDirectory(file); } else if (file.getName().equals("package-info.java")) { validateFile(file); } } } private static void validateFile(File file) throws IOException { CompilationUnit cu = JavaParser.parse(new FileInputStream(file)); // Check package declaration exists and has the annotation PackageDeclaration pkg = cu.getPackageDeclaration() .orElseThrow(() -> new IllegalArgumentException("No package declaration in " + file.getPath())); boolean hasAnnotation = pkg.getAnnotations().stream() .map(AnnotationExpr::getNameAsString) .anyMatch(REQUIRED_ANNOTATION::equals); // Check the required import is present boolean hasImport = cu.getImports().stream() .map(imp -> imp.getNameAsString()) .anyMatch(REQUIRED_IMPORT::equals); if (!hasAnnotation || !hasImport) { System.err.println("ERROR: Invalid package-info file - " + file.getPath()); System.err.println("Missing: " + (!hasAnnotation ? "@" + REQUIRED_ANNOTATION : "") + (!hasImport ? " " + REQUIRED_IMPORT : "")); System.exit(1); } } }
You can run this utility manually, or integrate it into your build pipeline.
内容的提问来源于stack exchange,提问作者sushmita chaudhari

