Backend Development 9 min read

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

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

Implementing the Processor

<code>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";
    }
}
</code>

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.

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

login 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.