Inject Jar Version into Java Components with Compile‑Time Annotation Processors

This article explains how to create a custom insertable annotation processor in Java that automatically injects the jar version into component constants during the Gradle build, eliminating manual updates and enabling version monitoring via Prometheus.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Inject Jar Version into Java Components with Compile‑Time Annotation Processors

Insertable annotation processors are mentioned in "Deep Understanding of the JVM" and can be applied to real‑world scenarios. This tutorial demonstrates using such a processor to inject the jar version into Java components at compile time.

We maintain a common Java component library containing modules like circuit‑breaker, load‑balancer, and RPC, packaged as JARs and published to an internal repository. To monitor the usage ratio of each version with Prometheus, we need the version number embedded in the code.

Manually updating a constant with the version in each component is cumbersome. Instead, we can inject the version during the Gradle build, similar to how Lombok generates getters and setters.

Solution Overview

Java annotation processing can be performed at compile time (using the Pluggable Annotation Processing API, JSR‑269) or at runtime via reflection. Lombok uses compile‑time processing. We define a custom insertable annotation processor that reads the jar version (e.g., from a file) and sets the value of a constant field in the abstract syntax tree (AST), so the generated bytecode contains the correct version.

Define the Annotation

@Documented
@Retention(RetentionPolicy.SOURCE) // only retained in source, not in class files
@Target({ElementType.FIELD}) // applicable to fields only
public @interface TrisceliVersion {}

Implement the Processor

/**
 * {@link AbstractProcessor} belongs to the Pluggable Annotation Processing API
 */
public class TrisceliVersionProcessor extends AbstractProcessor {

    private JavacTrees javacTrees;
    private TreeMaker treeMaker;
    private ProcessingEnvironment processingEnv;

    /** Initialize the processor */
    @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)) {
                // JCVariableDecl represents a field/variable declaration node in the AST
                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()); // assign the 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 other source.
        return "v1.0.1";
    }
}

Register the Processor via SPI

The processor must be discovered through the Service Provider Interface (SPI). Create a file META-INF/services/javax.annotation.processing.Processor containing the fully qualified name of TrisceliVersionProcessor.

SPI registration diagram
SPI registration diagram

Testing the Processor

Create a test module and add the component library with the new annotation.

Importing the library in a test module
Importing the library in a test module

Define a test class that uses a field annotated with @TrisceliVersion:

Test class example
Test class example

After running gradle build, the generated bytecode contains the injected version value:

Bytecode with injected version
Bytecode with injected version

This demonstrates only a fraction of what insertable annotation processors can achieve. By manipulating the AST, developers can create powerful plugins—like Lombok—to eliminate repetitive boilerplate code and unlock creative possibilities.

JavaASTGradleAnnotation ProcessinglombokCompile-time
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.