BBloggsbott
BBloggsbott

Reputation: 388

Using annotation processors to check for other annotations

I have an abstract class A with a function funcA, implemented. I want to enforce that the classes that extend class A override funcA to add an annotation @SampleAnnotation.

To achieve this, I tried using annotation processors. But I am unable to get the desired result. When my child classes override funcA without the @SampleAnnotation, I do not see an error during compilation.

I am not able to figure out what I am missing.

Definition of the Annotation Processor: https://github.com/BBloggsbott/annotation-enforcer/

EnforceAnnotation.java

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface EnforceAnnotation {

    Class value();

}

EnforceAnnotationProcessor.java

@SupportedAnnotationTypes("org.bbloggsbott.annotationenforcer.EnforceAnnotation")
@SupportedSourceVersion(SourceVersion.RELEASE_21)
public class EnforceAnnotationProcessor extends AbstractProcessor {

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Starting EnforceAnnotationProcessor");
        StringBuilder errorMessageBuilder = new StringBuilder();
        for (final Element element: roundEnv.getElementsAnnotatedWith(EnforceAnnotation.class)){
            if (element instanceof VariableElement variableElement){
                EnforceAnnotation enforce = variableElement.getAnnotation(EnforceAnnotation.class);
                Class enforcedAnnotationClass = enforce.value();
                Annotation enforcedAnnotation = variableElement.getAnnotation(enforcedAnnotationClass);
                if (enforcedAnnotation == null){
                    processingEnv.getMessager().printMessage(
                            Diagnostic.Kind.ERROR,
                            String.format(
                                    "%s.%s does not have the necessary annotation %s",
                                    variableElement.getEnclosingElement().getClass().getName(),
                                    variableElement.getSimpleName(),
                                    enforcedAnnotationClass.getSimpleName()
                            )
                    );
                }
            }
        }
        return true;
    }
}

Sample app that uses the Annotation: https://github.com/BBloggsbott/sample-annotation-processor-example build.gradle

plugins {
    id 'java'
}

group = 'org.bbloggsbott'
version = '1.0-SNAPSHOT'

repositories {
    mavenCentral()
    flatDir {
        dirs '<path to the annotation-enforcer jar>'
    }
}

dependencies {
    compileOnly 'com.bbloggsbott:annotation-enforcer-1.0-SNAPSHOT'

    testImplementation platform('org.junit:junit-bom:5.10.0')
    testImplementation 'org.junit.jupiter:junit-jupiter'
}


test {
    useJUnitPlatform()
}

SampleClass.java

public abstract class SampleClass {

    @EnforceAnnotation(SampleAnnotation.class)
    public boolean isSample(){
        return true;
    }

}

SampleClassExtended.java

public class SampleClassExtended extends SampleClass{

    @Override
    // Compilation should fail since the annotation is not present
    public boolean isSample() {
        return super.isSample();
    }
}

In sample-annotation-processor-example, I have added the annotation processor Jar as a compileOnly dependency in build.gradle

Upvotes: 0

Views: 48

Answers (3)

Slaw
Slaw

Reputation: 46181

No Output

The reason you aren't seeing any output from your annotation processor is that it's not actually running. You have to tell Gradle which dependencies are annotation processors by adding them to the annotationProcessor configuration. So, your sample project's build file should have the following:

dependencies {
    // To be able to use the @EnforceAnnotation annotation in your project
    compileOnly 'com.bbloggsbott:annotation-enforcer-1.0-SNAPSHOT'
    // To have the processor run when compiling your project
    annotationProcessor 'com.bbloggsbott:annotation-enforcer-1.0-SNAPSHOT'
    
    // ... other dependencies ...
}

Note every project that should run this annotation processor has to do this. I'm not aware of any way to make annotation processors transitive.


Implementation Tips

Implementing what you want is not as straightforward as it may seem at first. And while this answer doesn't provide a working annotation processor, there are a few problems with your current implementation that can be addressed.

Bound the EnforceAnnotation.value element type

Your @EnforceAnnotation currently looks like this:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface EnforceAnnotation {

    Class value();
}

The type of value is the raw type of Class. You should never use raw types when writing code with Java 5 or newer. But on top of that, this allows non-annotation types to be specified for value. You want to limit the types to annotation types. To do that, define the element like so:

// import java.lang.annotation.Annotation
Class<? extends Annotation> value();

You may also want to consider making the element's type an array (e.g., Class<? extends Annotation>[]).

Check for the correct element type

Your implementation checks if elements are an instance of VariableElement. However, you've meta-annotated your @EnforceAnnotation with @Target(ElementType.METHOD), which means it can only be placed on methods. A method is represented by ExecutableElement. So, you should be checking if the elements are an instance of ExecutableElement instead.

Of course, since the @EnforceAnnotation is only applicable to methods, you don't have to perform any instanceof checks at all. It's guaranteed that elements returned by:

roundEnv.getElementsAnnotatedWith(EnforceAnnotation.class)

Will be an instance of ExecutableElement with a kind of ElementKind.METHOD.

Use the AnnotationMirror API

Annotations that have Class elements are a little harder to work with during annotation processing. From the documentation of AnnotatedConstruct#getAnnotation(Class):

The annotation returned by this method could contain an element whose value is of type Class. This value cannot be returned directly: information necessary to locate and load a class (such as the class loader to use) is not available, and the class might not be loadable at all. Attempting to read a Class object by invoking the relevant method on the returned annotation will result in a MirroredTypeException, from which the corresponding TypeMirror may be extracted. Similarly, attempting to read a Class[]-valued element will result in a MirroredTypesException.

This means your enforce.value() call with throw an exception at run-time. As noted, you can then get the TypeMirror from the exception, but it would probably be better to use the AnnotationMirror API from the start.

Validate the correct methods

Your current code does this:

Annotation enforcedAnnotation = variableElement.getAnnotation(enforcedAnnotationClass);
if (enforcedAnnotation == null) {
  // print error  
}

The problem with this is that variableElement is the element annotated with @EnforceAnnotation. Your goal is to validate that overrides of such a method have the required annotations. Unfortunately, annotations on methods are never inherited, even when they're meta-annotated with @Inherited (only annotations on classes—not interfaces—may be inherited). Which means any overrides will not have the @EnforceAnnotation annotation present and thus will not be returned by the RoundEnvironment::getElementsAnnotatedWith method.

You'll have to scan for the overrides (see Elements::overrides) in any subtypes you can find from the types being compiled. One way to do this is to get the root elements from RoundEnvironment::getRootElements and then use an ElementVisitor to scan through them. Specifically, you'll probably want to extend from the "scanner family" (e.g., ElementScanner14).

Perform some validation on EnforceAnnotation

Your current annotation processor is obviously just a prototype, but in your full implementation you will probably want to validate @EnforceAnnotation is in places that makes sense. For instance, it doesn't make sense to allow that annotation on static, final, or private methods since they cannot be overridden. For the same reason, it doesn't make sense for that annotation to be allowed on methods declared in an annotation type, enum constant, record, or final class.

Cross-project validation

The way your annotation processor is implemented, it will only work if the method annotated with @EnforceAnnotation is currently being compiled. If project A has:

interface Foo {
  
  @EnforceAnnotation(SomeAnnotationType.class)
  void bar();
}

And project B, which depends on project A, has:

class FooImpl implements Foo {
  
  @Override
  public void bar() {}
}

Then your annotation processor will not catch the error in FooImpl for two reasons:

  1. Your @EnforceAnnotation annotation is annotated with Retention(RetentionPolicy.SOURCE), which means it will exist on Foo::bar when compiling project A, but will not exist when compiling project B.

  2. The Foo::bar method is compiled with project A, so that's when your annotation processor will process that method's @EnforceAnnotation annotation. But project A is compiled separately before project B, and so it will not be processed when compiling project B.

If you want to validate overrides of an @EnforceAnnotation-annotated method have the required annotations even across projects, then from some light testing I found you need to at least:

  • Give your @EnforceAnnotation annotation a retention policy of at least CLASS (note that's the default policy if no @Retention annotation is present).

  • Support all annotations with @SupportedAnnotationTypes("*"). The reason for this is that a project may not have any @EnforceAnnotation annotations present (they're only present in a dependency), which causes the annotation processor to be (rightly) skipped. By supporting all annotations, the processor won't be skipped.

    Make sure to return false from process in this case.

  • Scan all elements being compiled, which can be obtained via RoundEnvironment::getRootElements, looking at their interfaces and superclasses for any method annotated with @EnforceAnnotation. Then check any overrides in the being-compiled elements to validate they have the required annotations.

  • Do the actual validation during the last processing round (not sure if this is required).

Upvotes: 0

Sushil Behera
Sushil Behera

Reputation: 971

You can also achieve this using Reflection.

    import java.lang.reflect.Method;

public class AnnotationChecker {

    public static void checkAnnotation(Class<?> clazz) throws Exception {
        for (Method method : clazz.getDeclaredMethods()) {
            if (method.isAnnotationPresent(MyAnnotation.class)) {
                System.out.println("Annotation is present on method: " + method.getName());
            } else {
                throw new Exception("Annotation is not present on method: " + method.getName());
            }
        }
    }

    public static void main(String[] args) {
        try {
            checkAnnotation(MyClass.class);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

Upvotes: 0

Sushil Behera
Sushil Behera

Reputation: 971

You are missing registering the Annotation Processor.

You can register the annotation processor like below.

Register the Annotation Processor: Create a file named javax.annotation.processing.Processor in the META-INF/services directory and add the fully qualified name of your annotation processor class. org.bbloggsbott.annotationenforcer.EnforceAnnotationProcessor

Upvotes: 0

Related Questions