Reputation: 388
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
Reputation: 46181
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.
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.
EnforceAnnotation.value
element typeYour @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>[]
).
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
.
AnnotationMirror
APIAnnotations 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 aClass
object by invoking the relevant method on the returned annotation will result in aMirroredTypeException
, from which the correspondingTypeMirror
may be extracted. Similarly, attempting to read aClass[]
-valued element will result in aMirroredTypesException
.
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.
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
).
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.
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:
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.
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
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
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