Backend Development 8 min read

Injecting Jar Version into Java Components with an Insertion Annotation Processor

This article demonstrates how to create a compile‑time insertion annotation processor in Java that automatically injects the current jar version into static constants of shared components, eliminating manual updates and enabling Prometheus monitoring of version usage across the organization.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Injecting Jar Version into Java Components with an Insertion Annotation Processor

Insertion annotation processors were briefly mentioned in the book "Deep Understanding of the Java Virtual Machine" but rarely used; this article records a practical application of such a processor.

Developers familiar with Lombok know it relies on insertion annotations, and the author uses a real scenario to showcase the technique.

Requirement

The company provides a common Java component library containing modules like circuit‑breaker, load‑balancer, RPC, etc., packaged as JARs and published to an internal repository for other projects to depend on.

They need to monitor, via Prometheus, the proportion of each version of these components used internally, as illustrated in the accompanying diagram, to aid version compatibility decisions and trace legacy users.

Problem

Obtaining the JAR version at runtime is cumbersome. A simple but tedious approach is to hard‑code the version in each component's configuration or constant, requiring manual updates on every release.

The author asks whether the version can be injected at build time, similar to Lombok’s automatic generation of getters/setters, by adding a custom annotation.

Example of a placeholder constant with a custom annotation:

@TrisceliVersion
public static final String version = "";

After processing, the constant should contain the actual version, e.g.:

@TrisceliVersion
public static final String version = "1.0.31‑SNAPSHOT";

Solution

Java resolves annotations either at compile time (via annotation processors) or at runtime (reflection). Lombok’s @Setter is a compile‑time (SOURCE‑retention) annotation.

The answer is to define an insertion annotation processor using the JSR‑269 Pluggable Annotation Processing API.

The processor can modify the abstract syntax tree (AST) during compilation, allowing the injection of the JAR version into the constant field.

First, define the annotation:

@Documented
@Retention(RetentionPolicy.SOURCE) // only effective during compilation
@Target({ElementType.FIELD}) // can be placed on fields only
public @interface TrisceliVersion {}

Then implement a processor extending AbstractProcessor :

/**
 * {@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
     * @param processingEnv provides utility tools
     */
    @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
getSupportedAnnotationTypes() {
        HashSet
set = new HashSet<>();
        set.add(TrisceliVersion.class.getName()); // supported annotation
        return set;
    }

    @Override
    public boolean process(Set
annotations, RoundEnvironment roundEnv) {
        for (TypeElement t : annotations) {
            for (Element e : roundEnv.getElementsAnnotatedWith(t)) {
                // JCVariableDecl is the AST node for a field/variable
                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;
    }

    /** Output error messages via Messager */
    private void printErrorMessage(Element e, String m) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, m, e);
    }

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

The processor must be discoverable via the SPI mechanism by providing a META-INF/services/javax.annotation.processing.Processor file.

Testing

Create a test module that depends on the newly built processor and annotation.

Example Test class (illustrated in the original screenshots) simply declares a field annotated with @TrisceliVersion . After running gradle build , the generated bytecode contains the injected version value.

This demonstrates only a fraction of what insertion annotation processors can achieve; by manipulating the AST they enable powerful compile‑time code generation similar to Lombok, opening possibilities for many creative plugins.

Source: juejin.cn/post/7176428890896728101
backendJavaGradleannotation-processingLombokcompile-timeversion-injection
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.