I have written a custom annotation that I use to find methods that can be invoked via a IoT platform. It's a method level annotation:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface DirectMethod {
String value();
}
I look this annotation up in runtime, and to have the call succeed, the expected signature must be:
#DeviceMethod("metod")
public ReturnType methodName(final String data) {...}
i.e, the return type and the input parameters are crucial.
Is there any way to have an annotation be "smart" when its target type is METHOD? Like integrated IDE warnings and such. Or do I simply have to process each annotation manually at startup and have the startup procedure fail if any method breaks my intended method contract?
Yes, you can write annotation processor to validate your calls, the only downside of this is that annotation processors needs to be passed to javac (gradle and maven support easy syntax to register them) so someone could just not do it and not see any warnings/errors.
But otherwise all you need to do is create special annotation and processor, like that:
#SupportedAnnotationTypes("com.gotofinal.direct.DirectMethod")
#SupportedSourceVersion(SourceVersion.RELEASE_8)
public class DirectAnnProcessor extends AbstractProcessor {
#Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
TypeElement stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String");
TypeElement expectedReturnType = processingEnv.getElementUtils().getTypeElement("com.gotofinal.direct.ReturnType");
for (Element element : roundEnv.getElementsAnnotatedWith(DirectMethod.class)) {
if (! (element instanceof ExecutableElement)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Annotation should be on method.");
continue;
}
ExecutableElement executableElement = (ExecutableElement) element;
if (! executableElement.getReturnType().equals(expectedReturnType)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Method should return ReturnType");
}
List<? extends VariableElement> parameters = executableElement.getParameters();
if (parameters.size() != 1 && parameters.get(0).asType().equals(stringType)) {
processingEnv.getMessager().printMessage(Kind.ERROR, "Method should have single String argument");
}
}
return true; // no further processing of this annotation type
}
}
And register it in META-INF/services/javax.annotation.processing.Processor file:
com.gotofinal.direct.DirectAnnProcessor
And then you can add such lib to maven/gradle as annotation processor and it should report any issues. In gradle such library must be added using annotationProcessor "my:lib:0.1" declaration.
Related
Question:
Is it possible to access elements annotated with a #Target(ElementType.TYPE_USE) annotation via an annotation processor?
Is it possible to access the annotated type bounds via an annotation processor?
Links to related documentation I missed are highly appreciated.
Context:
The annotation:
#Target(ElementType.TYPE_USE)
#Retention(RetentionPolicy.SOURCE)
public #interface TypeUseAnno {}
An example class:
public class SomeClass extends HashMap<#TypeUseAnno String, String> {}
The processor:
#SupportedSourceVersion(SourceVersion.RELEASE_8)
#SupportedAnnotationTypes("base.annotations.TypeUseAnno")
public class Processor extends AbstractProcessor {
#Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Initialized.");
}
#Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "Invoked.");
for (TypeElement annotation : annotations) {
this.processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "" + roundEnv.getElementsAnnotatedWith(annotation));
}
return true;
}
}
Compiling the above SomeClass with Processor on the classpath will show the "Intialized" message but the process(...) method is never invoked.
Adding another annotation to the processor with #Target(ElementType.PARAMETER) works fine when the annotation is present on a method parameter. If the method parameter is annotated with #TypeUseAnno the process will again ignore the element.
The TYPE_USE annotations are a bit tricky, because the compiler treats them differently, than the "old usage" annotations.
So as you correctly observed, they are not passed to annotation processor, and your process() method will never receive them.
So how to use them at compilation time?
In Java 8, where these annotations got introduced, there was also introduced new way to attach to java compilation. You can now attach listener to compilation tasks, and trigger your own traversal of the source code. So your task to access the annotation splits into two.
Hook to the compiler.
Implement your analyzer.
Ad 1.
There are 2 options to hook on the compiler in Java 8:
Using new compiler plugin API.
Using annotation processor.
I haven't used option #1 much, because it needs to be explicitely specified as javac parameter. So I'll describe option #1:
You have to attach TaskListener to the propper compilation phase. There are various phases. Following one is the only one, during which you have accessible syntax tree representing full source code including method bodies (remember, that TYPE_USE annotations can be used even on local variable declarations.
#SupportedSourceVersion(SourceVersion.RELEASE_8)
public class EndProcessor extends AbstractProcessor {
#Override
public synchronized void init(ProcessingEnvironment env) {
super.init(env);
Trees trees = Trees.instance(env);
JavacTask.instance(env).addTaskListener(new TaskListener() {
#Override
public void started(TaskEvent taskEvent) {
// Nothing to do on task started event.
}
#Override
public void finished(TaskEvent taskEvent) {
if(taskEvent.getKind() == ANALYZE) {
new MyTreeScanner(trees).scan(taskEvent.getCompilationUnit(), null);
}
}
});
}
#Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
// We don't care about this method, as it will never be invoked for our annotation.
return false;
}
}
Ad 2.
Now the MyTreeScanner can scan the full source code, and find the annotations. That applies no matter if you used the Plugin or AnnotationProcessor approach. This is still tricky. You have to implement the TreeScanner, or typically extend the TreePathScanner.
This represents a visitor pattern, where you have to properly analyze, which elements are of your interest to be visited.
Let's give simple example, that can somehow react on local variable declaration (give me 5 minutes):
class MyTreeScanner extends TreePathScanner<Void, Void> {
private final Trees trees;
public MyTreeScanner(Trees trees) {
this.trees = trees;
}
#Override
public Void visitVariable(VariableTree tree, Void aVoid) {
super.visitVariable(variableTree, aVoid);
// This method might be invoked in case of
// 1. method field definition
// 2. method parameter
// 3. local variable declaration
// Therefore you have to filter out somehow what you don't need.
if(tree.getKind() == Tree.Kind.VARIABLE) {
Element variable = trees.getElement(trees.getPath(getCurrentPath().getCompilationUnit(), tree));
MyUseAnnotation annotation = variable.getAnnotation(MyUseAnnotation.class);
// Here you have your annotation.
// You can process it now.
}
return aVoid;
}
}
This is very brief introduction. For real examples you can have a look at following project source code:
https://github.com/c0stra/fluent-api-end-check/tree/master/src/main/java/fluent/api/processors
It's also very important to have good tests while developing such features, so you can debug, reverse engineer and solve all the tricky issues you'll face in this area ;)
For that you can also get inspired here:
https://github.com/c0stra/fluent-api-end-check/blob/master/src/test/java/fluent/api/EndProcessorTest.java
Maybe my last remark, as the annotations are really used differently by the javac, there are some limitations. E.g. it's not suitable for triggering java code generation, because the compiler doesn't pick files created during this phase for further compilation.
Why do I get java.lang.annotation.AnnotationFormatError: Attempt to create proxy for a non-annotation type. when I'm trying to get the annotation list from the field?
My annotation:
#Target({ ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
private #interface TypicalAnnotation
{
int size();
}
Usage:
public static class MockAnnotatedClass
{
#TypicalAnnotation(size = 3)
public Integer number = 2;
}
Call:
ReflectionUtils.getAllFields(clazz1).getAnnotations() <- got the exception.
I had the same problem when I used Jdeveloper 12.2.1.0.0 and integrated Weblogic with enabled Fast Swap feature.
if you look at the source code (github) you will see that an exception is thrown if the type is either not an annotation or implements more than one interface or does not implement the java.lang.annotation.Annotation interface:
AnnotationInvocationHandler(Class<? extends Annotation> type, Map<String, Object> memberValues) {
Class<?>[] superInterfaces = type.getInterfaces();
if (!type.isAnnotation() ||
superInterfaces.length != 1 ||
superInterfaces[0] != java.lang.annotation.Annotation.class)
throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
this.type = type;
this.memberValues = memberValues;
}
Using this code, I checked which interfaces my annotation implements:
Annotation declaration:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface MyAnnotation {}
Checking of type:
Class<?> type = MyAnnotation.class;
System.out.println("type.isAnnotation() " + type.isAnnotation());
System.out.println("type.getInterfaces().length " + type.getInterfaces().length);
for (Class<?> superType : type.getInterfaces()) {
System.out.println("type.getInterfaces()[i] " + superType);
}
Output:
type.isAnnotation() true
type.getInterfaces().length 2
type.getInterfaces()[i] interface java.lang.annotation.Annotation
type.getInterfaces()[i] interface com.bea.wls.redef.Redefinable // << ??????
As it turned out, another interface is added to the annotation.
I did not find information specifically about com.bea.wls.redef.Redefinable. After a bit of searching, I realized that the package com.bea.wls.redef is used by the Fast Swap function.
I see 2 solutions:
Disable Fast Swap function.
Move all custom annotations to separate jar. It is what I did.
Perhaps there is a way to turn off wrapping for annotations. But I did not find it.
I am trying to come up with a custom annotation, wanted to see if my use-case fit a allowed way of using custom annotation.
I want to replicate what Spring #Value does, but instead of reading a property off of a property, i want to my custom thing.
#Documented
#Target(ElementType.FIELD)
#Retention(RetentionPolicy.RUNTIME)
#SupportedSourceVersion(SourceVersion.RELEASE_8)
public #interface EncryptedValue {
String value();
}
public Class TestEncrypted {
#EncryptedValue("dGVzdCBzdHJpbmc=");
public String someEncryptedValue;
}
I am hoping in annotation processor, i decrypt value and set to the field someEncryptedValue.
/**
*
*/
#SupportedAnnotationTypes("annotation.EncryptedValue")
#SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CustomProcessor extends AbstractProcessor{
private Types typeUtils;
private Elements elementUtils;
private Filer filer;
private Messager messager;
#Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
typeUtils = processingEnv.getTypeUtils();
elementUtils = processingEnv.getElementUtils();
filer = processingEnv.getFiler();
messager = processingEnv.getMessager();
}
#Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
for(Element ele : annotatedElements) {
EncryptedValue encryptedValue = ele.getAnnotation(EncryptedValue.class);
if(!ele.getKind().isField()){
messager.printMessage(Diagnostic.Kind.ERROR,"EncryptedValue is supported for field");
return false;
}
String annotationValue = encryptedValue.value();
// now get the enclosing type
Set<Modifier> modifiers = ele.getModifiers();
String nameOfVariable = ele.getSimpleName().toString();
// check to see what fields we can modify (i think we can't modify static).
messager.printMessage(Diagnostic.Kind.NOTE,"ClassType: "+ele.getSimpleName().toString()+", nameOf="+annotationValue);
String simpleName = ele.getEnclosingElement().getSimpleName().toString();
for (Element elem : roundEnv.getRootElements()) {
messager.printMessage(Diagnostic.Kind.NOTE, "Enclosing ClassName: "+elem.getSimpleName().toString());
if (elem.getSimpleName().toString().equals(simpleName)) {
for (Element variableDeclaration : elem.getEnclosedElements()) {
if (variableDeclaration instanceof VariableElement) {
messager.printMessage(Diagnostic.Kind.NOTE, "variable: "+((VariableElement) variableDeclaration).getSimpleName().toString());
}
}
}
}
}
}
return true;
}
}
I get the variable, its return types and everything, but not sure how to set value of the variable from this annotation, even if i figure it out, is it good way of using custom annotations.
*Note: This might be sample, what I am planning to do is much more complicated than above sample.
There's no way to modify existing source files via the current publicly-available API. Tools like Lombok which do this are using undocumented internal Javac features to edit the abstract syntax tree. For example, you could use the Sun compiler tree API to obtain a VariableTree, cast it to a JCVariableDecl, then modify it and hope there are no unforeseen consequences. There's no guarantee that tools like Lombok will actually work, and they could break tomorrow with no warning.
What you could do instead is have the annotated classes reference a class which your annotation processor generates, as in the following example:
public class TestEncrypted {
#EncryptedValue("dGVzdCBzdHJpbmc=");
public String someEncryptedValue =
TestEncryptedDecryptedValues.someEncryptedValue;
}
// then generate this class with the annotation processor
final class TestEncryptedDecryptedValues {
static final String someEncryptedValue = "test string";
}
Another way to do something like this would be to use the annotation processor to generate a factory object or method which creates instances of e.g. TestEncrypted with the field assigned to the decrypted value.
A good tutorial for code generation with annotation processors is here: https://deors.wordpress.com/2011/10/08/annotation-processors/
Also, as a side note in case you don't know this, String literals and names appear in the compiled class file, so none of these examples which decrypt the data at compile-time provide any security.
I'm trying to create an annotation that will change the content of the annotated field. So far this is my annotation:
#Retention(RetentionPolicy.CLASS)
#Target(ElementType.FIELD)
public #interface MyAnnotation {
String value();
}
And I want to use it like this
public class MyClass {
#MyAnnotation("test")
String myField;
}
And then I want to set the value of myField to "test" at compile time. I just don't know how I can access the annotated field from my annotation processor and if it is even possible to change its content at compile time. This is what my annotation processor looks like right now:
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.stream().flatMap(a -> roundEnv.getElementsAnnotatedWith(a).stream())
.forEach(e -> {
if (!String.class.getName().equals(((VariableElement) e).asType().toString())) {
out.printMessage(Kind.ERROR
, "#MyAnnotation annotation can only be applied to Strings", e);
}
else {
// what to do here?
}
});
return true;
}
I am new to annotations and a little bit lost so any ideas are highly appreciated.
This should be easy using annotation processing, e.g. https://www.javacodegeeks.com/2015/09/java-annotation-processors.html
I have a custom java annotation like below
#customelement(folder = "/path/")
public testMethod() {
}
I want to validate the folder path ("/path/") for the folder member variable and report error inside java source. How I can do that?
Java Annotation Element Method return
#Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment roundEnv) {
Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(CustomAnnotation.class);
for(Element te : elements){
CustomAnnotation annotation = te.getAnnotation(CustomAnnotation.class);
String folder = annotation.folder();
//... do sth with folder in context of te.
//te represent annotated method, e.g. testMethod from your example
}
return true;
}
Also remember to annotate your CompileTimeAnnotationProcessor with #SupportedAnnotationTypes("package.CustomAnnotation") and #SupportedSourceVersion(SourceVersion.RELEASE_7) (or whatever version of Java syntax are you going to support).
In case of further problems, I recommend to read this great tutorial blog post.