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.
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.
Testing the Processor
Create a test module and add the component library with the new annotation.
Define a test class that uses a field annotated with @TrisceliVersion:
After running gradle build, the generated bytecode contains the injected version value:
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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
