Inject Jar Version into Java Components with Insertable Annotation Processors

This article demonstrates how to create a custom insertable annotation processor in Java to automatically inject the jar version into component constants at compile time, eliminating manual updates and enabling Prometheus monitoring of library usage across versions.

macrozheng
macrozheng
macrozheng
Inject Jar Version into Java Components with Insertable Annotation Processors

Requirement

Our company provides a set of common Java utility modules (circuit‑breaker, load‑balancer, RPC, etc.) packaged as JARs and published to an internal repository. We need to monitor the usage ratio of each version with Prometheus, as shown in the desired chart, to trace legacy users and manage compatibility.

Problem

Manually writing the version number into each component’s configuration or constant is error‑prone and requires updates on every release. We want a better solution, such as automatically injecting the version during the Gradle build, similar to how Lombok generates getters and setters via annotations.

Solution Overview

Java supports two ways to process annotations: compile‑time scanning and runtime reflection. Lombok’s @Setter works at compile time using the Pluggable Annotation Processing API (JSR‑269). We can define a custom insertable annotation processor that reads the JAR version at compile time and injects it into a constant field.

Defining the Annotation

@Documented
@Retention(RetentionPolicy.SOURCE) // only present at compile time
@Target({ElementType.FIELD}) // can annotate fields only
public @interface TrisceliVersion {}

Implementing the Processor

public class TrisceliVersionProcessor extends AbstractProcessor {
    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private ProcessingEnvironment processingEnv;

    @SneakyThrows
    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) {
        super.init(processingEnv);
        this.processingEnv = processingEnv;
        this.javacTrees = JavacTrees.instance(processingEnv);
        Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
        this.treeMaker = TreeMaker.instance(context);
    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latest();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        HashSet<String> set = new HashSet<>();
        set.add(TrisceliVersion.class.getName()); // supported annotation
        return set;
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement t : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(t)) {
                JCTree.JCVariableDecl jcv = (JCTree.JCVariableDecl) javacTrees.getTree(e);
                String varType = jcv.vartype.type.toString();
                if (!"java.lang.String".equals(varType)) {
                    printErrorMessage(e, "Type '" + varType + "' is not supported.");
                }
                jcv.init = treeMaker.Literal(getVersion()); // inject version value
            }
        }
        return true;
    }

    private void printErrorMessage(Element e, String m) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, m, e);
    }

    private String getVersion() {
        // In a real implementation, read the version from a file or build metadata.
        return "v1.0.1";
    }
}

Registering the Processor

The processor must be discoverable via the SPI mechanism. Create a file META-INF/services/javax.annotation.processing.Processor containing the fully qualified name of TrisceliVersionProcessor.

SPI registration diagram
SPI registration diagram

Testing

Create a test module that depends on the utility library, add the @TrisceliVersion annotation to a static String version field, and run gradle build. The generated bytecode will contain the injected version value.

Build output showing injected version
Build output showing injected version

Conclusion

Insertable annotation processors allow compile‑time modification of the abstract syntax tree, enabling powerful code generation such as automatic version injection. This technique can be extended to build sophisticated plugins like Lombok, reducing boilerplate and enhancing developer productivity.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaGradlePrometheusAnnotationProcessorCompileTime
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.