Inject Jar Version into Java Components with Compile‑Time Annotation Processors
This article demonstrates how to create a custom compile‑time annotation processor that automatically injects the jar version into Java component constants, enabling Prometheus monitoring of version usage without manual updates.
Insertable annotation processors were mentioned in "深入理解Java虚拟机" and are now applied to a real scenario.
Requirement
The company provides a set of Java base component packages (circuit breaker, load balancing, RPC, etc.) packaged as JARs in an internal repository. They need to monitor the usage ratio of each version via Prometheus, as shown in the desired chart.
Problem
Manually adding a version constant to each component is cumbersome; a better solution is to inject the version at compile time, similar to Lombok, by using a custom annotation.
Define an empty constant with a custom annotation:
<code>@TrisceliVersion
public static final String version = "";</code>The processor will replace the empty string with the actual version.
Solution
Java can resolve annotations at compile time or runtime. Lombok uses a source‑retention annotation, which is processed only during compilation via the Pluggable Annotation Processing API (JSR‑269). An insertable annotation processor can modify the abstract syntax tree (AST) at compile time.
Define the annotation:
<code>@Documented
@Retention(RetentionPolicy.SOURCE) // only at compile time
@Target({ElementType.FIELD})
public @interface TrisceliVersion {}
</code>Define a processor extending
AbstractProcessor:
<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());
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 support.");
}
jcv.init = treeMaker.Literal(getVersion());
}
}
return true;
}
private void printErrorMessage(Element e, String m) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, m, e);
}
private String getVersion() {
// simplified: return fixed value
return "v1.0.1";
}
}
</code>The processor must be discovered via the SPI mechanism by adding a
META‑INF/servicesentry.
Test
Create a test module, include the processor, and run a Gradle build. The generated bytecode now contains the version value.
Result screenshot shows the field populated with the version.
Conclusion: Insertable annotation processors can modify the AST during compilation, enabling powerful plugins like Lombok and allowing developers to create creative compile‑time code generation solutions.
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.
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.