We can do this with annotations!
To raise an error, use Messager
to send a message with Diagnostic.Kind.ERROR
. Short example:
processingEnv.getMessager().printMessage(
Diagnostic.Kind.ERROR, "Something happened!", element);
Here's a fairly simple annotation I wrote just to test this out.
This @Marker
annotation indicates the target is a marker interface:
package marker;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Marker {
}
And the annotation processor causes an error if it's not:
package marker;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.*;
import javax.tools.Diagnostic;
import java.util.Set;
@SupportedAnnotationTypes("marker.Marker")
@SupportedSourceVersion(SourceVersion.RELEASE_6)
public final class MarkerProcessor extends AbstractProcessor {
private void causeError(String message, Element e) {
processingEnv.getMessager()
.printMessage(Diagnostic.Kind.ERROR, message, e);
}
private void causeError(
Element subtype, Element supertype, Element method) {
String message;
if (subtype == supertype) {
message = String.format(
"@Marker target %s declares a method %s",
subtype, method);
} else {
message = String.format(
"@Marker target %s has a superinterface " +
"%s which declares a method %s",
subtype, supertype, method);
}
causeError(message, subtype);
}
@Override
public boolean process(
Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
Elements elementUtils = processingEnv.getElementUtils();
boolean processMarker = annotations.contains(
elementUtils.getTypeElement(Marker.class.getName()));
if (!processMarker)
return false;
for (Element e : roundEnv.getElementsAnnotatedWith(Marker.class)) {
ElementKind kind = e.getKind();
if (kind != ElementKind.INTERFACE) {
causeError(String.format(
"target of @Marker %s is not an interface", e), e);
continue;
}
if (kind == ElementKind.ANNOTATION_TYPE) {
causeError(String.format(
"target of @Marker %s is an annotation", e), e);
continue;
}
ensureNoMethodsDeclared(e, e);
}
return true;
}
private void ensureNoMethodsDeclared(
Element subtype, Element supertype) {
TypeElement type = (TypeElement) supertype;
for (Element member : type.getEnclosedElements()) {
if (member.getKind() != ElementKind.METHOD)
continue;
if (member.getModifiers().contains(Modifier.STATIC))
continue;
causeError(subtype, supertype, member);
}
Types typeUtils = processingEnv.getTypeUtils();
for (TypeMirror face : type.getInterfaces()) {
ensureNoMethodsDeclared(subtype, typeUtils.asElement(face));
}
}
}
For example, these are correct uses of @Marker
:
But these uses of @Marker
will cause a compiler error:
Here's a blog post I found very helpful getting started on the subject:
Small note: what the commentor below is pointing out is that because MarkerProcessor
references Marker.class
, it has a compile-time dependency on it. I wrote the above example with the assumption that both classes would go in the same JAR file (say, marker.jar
), but that's not always possible.
For example, suppose there's an application JAR with the following classes:
com.acme.app.Main
com.acme.app.@Ann
com.acme.app.AnnotatedTypeA (uses @Ann)
com.acme.app.AnnotatedTypeB (uses @Ann)
Then the processor for @Ann
exists in a separate JAR, which is used while compiling the application JAR:
com.acme.proc.AnnProcessor (processes @Ann)
In that case, AnnProcessor
would not be able to reference the type of @Ann
directly, because it would create a circular JAR dependency. It would only be able to reference @Ann
by String
name or TypeElement
/TypeMirror
.