The Power of Custom Annotations and Annotation Processors in Java

Doradla Hari Krishna
Javarevisited
Published in
8 min readDec 30, 2023
Photo by Christopher Gower on Unsplash

Background Story

I joined Amazon as a Software Development Engineer Intern. After joining i started working on a project, going through the existing code and also writing lot of new code. During this process i encountered annotations like @Override, @Deprecated, @Test, @MethodSource and many more to say…

I know about @Override usage that when we use this annotation on a child class method, at compile time this annotation makes sure corresponding case-sensitive method should be present in its parent class or implementing interface. But i have no idea about others. I know when to use them so i was using them blindly wherever required. At that point in time i do not know in the background how these annotations actually work. With time and experience i got the inner working knowledge.

I you are in the similar situation of having below questions

  1. How does a annotation work?
  2. What is the use of annotation?
  3. Can we create our custom annotations and how?
  4. How can we make the best out of custom annotations?

then you came to the right place. In this story i am going to uncover these things. I will explain in detail on how you can create custom annotation and use them in your daily job.

Lets grab a coffee for learning some cool stuff

Pre-requisites

  1. I am assuming you have working knowledge in java and know/used basic built in annotations like @Override @ParameterizedTest @Test @Deprecated
  2. Kindly go through below articles to get full basics of java annotations like what is annotation, what is the use of annotation..
  3. https://reflectoring.io/java-annotation-processing/#annotation-basics
  4. https://dzone.com/articles/how-annotations-work-java

Use of annotations

  1. Metadata Addition: Annotations allow developers to attach metadata to code elements (classes, methods, fields), providing additional information without affecting the code’s functionality.
  2. Compile-Time Checking and Validation using custom rules.
  3. We can automatically trigger config management and code files generation.
  4. Can be used for managing beans and dependency injection.

Problem

Lets assume you(`Sai`) are working as Software Engineer and your manager asks you “Hey Sai, lately many juniors have joined our team and lets force enforce some best coding practices in our code” starting with

  1. All the non static class fields should be in camelCase format.
  2. All the static class fields should be in SCREAMING_SNAKE_CASE format
  3. All the class fields length should not be less than 4 and more than 50 chars.

So that if any of the above condition is violated then package build will fail with compilation errors which forces developer to modify it before raising the merge request or pull request which saves reviewer time.

Solution

Lets see how we can solve the above problem using custom annotations in Java

Note: All the code used here is in my Github profile.

Requirement-1

All the non static class fields should be in camelCase format. Three steps to achieve it.

  1. Create a custom annotation: In the code mentioned down, we created annotation called CamelCase with Retention as SOURCE as we only need this at build/compile time which is when fields case is checked and Target means we can use this annotation on class, interface and enum.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface CamelCase {
}

2. Implement an Annotation processor for aboveCamelCase annotation: In the code mentioned down,

  • getSupportedAnnotationTypes is an overridden method from the AbstractProcessor class. It specifies which annotation types this processor supports. In this case, it indicates that the processor supports the @CamelCase annotation.
  • isCamelCase method contains logic to check if a given string is in camel case or not. You can place required logic according to your requirement here.
  • process method contains logic related to that particular annotation. Seems like logic is complex but it is nothing let me break it to you.
  • First, we are getting all the elements (classes, functions, attributes) on which CamelCase annotation is used and looping through each element.
  • If element is of class type then we get all the enclosed elements which includes fields, methods, constructors, and nested classes/interfaces/enums or enumerated constants within the class and looping through all the enclosed elements.
  • If the enclosed element is field kind and it’s not a static field and is not camelCase then we print an error message saying “Field test in class Driver is not in camelCase” which comes in build output.
  • @AutoService(Processor.class : It is from google auto service jar which automatically generates the necessary service provider registration metadata and allows java compiler and build tools to discover your custom processor during the annotation processing phase without additional configuration. If you are using SpringBoot or any other web framework there would be a inbuilt feature for registering these custom processors.
@AutoService(Processor.class)
public class CamelCaseProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(CamelCase.class)) {
if (element.getKind() == ElementKind.CLASS) {
for (Element enclosedElement : element.getEnclosedElements()) {
if (enclosedElement.getKind() == ElementKind.FIELD
&& !enclosedElement.getModifiers().contains(Modifier.STATIC)
&& !isCamelCase(enclosedElement.getSimpleName().toString())) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Field '" + enclosedElement.getSimpleName().toString() + "' in class '"
+ element.getSimpleName().toString() + "' is not in camelCase", enclosedElement);
}
}
}
}
return false;
}

@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(CamelCase.class.getCanonicalName());
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

private boolean isCamelCase(String name) {
// you can have whatever logic you want to be executed here
return name.matches("[a-z][a-zA-Z0-9]*");
}
}

3. Finally a class model to test above processor: So, here i have annotated CamelCaseTestClass with @CamelCase annotation so when we build the module all the non static fields means here FieldOne, fieldTwo are checkd if they are following camelCase format or not.

Note: Intentionally i put one field called FieldOne in such a way it fails validation just to show you how error message looks like.

@CamelCase
public class CamelCaseTestClass {
public static String Field = "test";
private int FieldOne;
private String fieldTwo;
}

Output of above class

When i build the package for this class i see the error as below. Because fieldTwo is following the camelCase format but FieldOne is not following camelCase as it’s first letter is capital F. Until we change FieldOne to fieldOne build will not pass.

You can ask there is another field called Field but why it got passed validation. It is a very good question. Because if you see in the above processor logic we are not doing this camelCase validation if field has STATIC modifier means static variable.

Hacks:
1. If you are getting error like `java: Compilation failed: internal java compiler error` then delete the target folder and build again.
2. if you are not seeing your test class model is not being checked then refactor the class by changing the class name and build again. It fetches class data freshly upon renaming.

So, One requirement of enforcing camelCase for non static fields is solved.

Requirement-2

Second requirement where static fields should have SCREAMING_SNAKE_CASE can be done simply removing ! condition for enclosed element modifier contains static kind in CamelCaseAnnotationProcessor and modifying the logic according to snake_case format in isCamelCase function. This is very similar one. Explore yourself the code from my Github profile.

Requirement-3

All the class fields length should be in the range min and max configured values.

Note: min and max can be configured independently for each element we put annotation to.

  1. So 1st step is to create custom annotation: Here different to above CamelCase annotation you see two extra members min and max which has default values of 4 and 50. When annotating a class with this LengthValidation annotation if you do not specify any values for min and max then 4 and 50 are considered. If not you can also configure like min = 10 and max=30 wherever you use this annotation.
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface LengthValidation {
int min() default 4;
int max() default 50;
}

2. Second step is to create LengthValidation annotation Processor . This time i will not explain the logic indepth as i have explained the logic for CamelCaseProcessor.

In the below process method logic the only thing different from CamelCaseProcessor is adding null check for lengthValidation after checking if the element kind is CLASS.

Why did we add null check is because here our annotation process logic depends on min and max members of an annotation fetched for a class. So if a class does not contain LengthValidation annotation then element.getAnnotation(LengthValidation.class) return null so fetching min and max from null results in NullPointerException.

So we check if that class has lengthValidation annotation if not we proceed to next element. If present then we proceed by passing lengthValidation annotation to isLengthValid function where we get min and max to check length of field name.

@AutoService(Processor.class)
public class LengthValidationProcessor extends AbstractProcessor {

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element element : roundEnv.getElementsAnnotatedWith(LengthValidation.class)) {
if (element.getKind() == ElementKind.CLASS) {

LengthValidation lengthValidation = element.getAnnotation(LengthValidation.class);
if (lengthValidation == null) {
return false;
}

for (Element enclosedElement : element.getEnclosedElements()) {
if (enclosedElement.getKind() == ElementKind.FIELD
&& !isLengthValid(enclosedElement.getSimpleName().toString(), lengthValidation)) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Field '" + enclosedElement.getSimpleName().toString() + "' in class '"
+ element.getSimpleName().toString() + "' failed in length validation", enclosedElement);
}
}
}
}
return false;
}

@Override
public Set<String> getSupportedAnnotationTypes() {
return Set.of(LengthValidation.class.getCanonicalName());
}

@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}

private boolean isLengthValid(String name, LengthValidation lengthValidation) {
return name.length() >= lengthValidation.min() && name.length() <= lengthValidation.max();
}
}

3. Finally, a class model to test the length validation annotation:

@LengthValidation(min = 5, max = 6)
public class LengthValidationTestClass {
public String field;
public String fieldTwo;
}

In the above class, I have mentioned min as 5 and max as 6, so field names inside LengthValidationTestClass should not cross these boundaries.

Output of above class

After building the module, we get the below build failure output. Because fieldfieldTwo length is 8 but allowed max is 6.

Conclusion

  1. In this story, we learned how to create a custom annotation for any kind of requirement. I am not saying this is the best way to implement enforcing format constraints; rather, I explained how to implement these with custom annotations.
  2. In order to enable these three constraints on any class, you just have to annotate those classes with @LengthValidation, @CamelCase, @StaticFieldCase
  3. During the implementation of anything, check if the framework you use already has this functionality enablement feature.
  4. Do read these articles for more in-depth understanding of annotations
  5. https://reflectoring.io/java-annotation-processing/#annotation-basics
  6. https://dzone.com/articles/how-annotations-work-java

Thanks for reading!! I hope you learned something useful from this article. Kindly suggest any similar articles you need or improvements to be made here. Code is hosted in my Github profile

Feel free to follow me on LinkedIn and Medium for more such tech content.

--

--

Doradla Hari Krishna
Javarevisited

Software Engineer who likes writing about tech concepts