4

I have tried a lot of things online but nothing seem to work for me. I want to know whether an annotation method has been @Overriden (either with the same value as its default).

Take a look at this example:

public class AnnoTest {

    @Anno
    private String something;

    public static void main(String[] args) throws NoSuchFieldException, SecurityException, NoSuchMethodException {
        Field field = AnnoTest.class.getDeclaredField("something");
        field.setAccessible(true);
        boolean isDefault= field.getAnnotation(Anno.class).annotationType().getDeclaredMethod("include").isDefault();
        System.out.println(isDefault); //returns false

    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.FIELD })
    public @interface Anno {
        boolean include() default false;
    }
}

For some reason it returns false. When i change it to:

@Anno(include = false)
private String something;

It returns false again. Is there a way to know whether the value has been declared in the annotation?

I know i could just compare the default value and its current value, but it will not work for me. I want to know if it has been declared.


With other words I need some kind of magic boolean that does the following:

@Anno
private String something;

return false.

@Anno(include = true)
private String something;

return true.

@Anno(include = false)
private String something;

return true.


The reason of this is that i am wishing to add a method (to my annotation) named "parent". When a parent (a String) has been declared the annotation, this field will inherit the annotation of the field named parent. Take a look at this example:

public class AnnoTest {

    @Anno(include = false)
    private Something something = new Something();

    @Anno(parent = "something")
    private Something somethingElse  = new Something();

    public static void main(String[] args) throws NoSuchFieldException, SecurityException, NoSuchMethodException {
        AnnoTest test = new AnnoTest();

        Field somethingField = AnnoTest.class.getDeclaredField("something");
        somethingField.setAccessible(true);

        Field somethingElseField = AnnoTest.class.getDeclaredField("somethingElse");
        somethingField.setAccessible(true);

        Anno anno = somethingElseField.getAnnotation(Anno.class);

        if (anno.parent().equals("something")) {
            boolean include = somethingField.getAnnotation(Anno.class).include();
            test.somethingElse.isIncluded = include;
        }

        //If not declared it will return true, which it should be false, because "something" field has it false.
        boolean include = somethingElseField.getAnnotation(Anno.class).include();
        //if somethingElse has declared "include", dominate the value, else keep it from the parent
        test.somethingElse.isIncluded = include;

    }

    public class Something {
        boolean isIncluded;
    }

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.FIELD })
    public @interface Anno {
        boolean include() default false;

        String parent() default "";
    }
}
George Z.
  • 6,643
  • 4
  • 27
  • 47
  • 1
    `isDefault()` has nothing to do with the annotation. It is for methods in interfaces that have the `default` keyword (added in Java 8). – jbx May 17 '19 at 10:49
  • @jbx I tried it since my annotation has the `default` keyword. – George Z. May 17 '19 at 10:50
  • Yeah, but it is not the same. It is the `default` that comes before the method. I think it is going to be difficult to distinguish between when the user set the value and it matched the default or when the user left it out and the default value was used. – jbx May 17 '19 at 10:55
  • @jbx I get the difference yes. But i had to try something right? I actually tried tons of things. – George Z. May 17 '19 at 10:56
  • Yes of course :). I think you are a bit out of luck. What are the circumstances that are forcing you to make a difference between explicit and implicit default? – jbx May 17 '19 at 10:57
  • What's the goal you want to achieve by this? – zolee May 17 '19 at 10:58
  • @zolee I have added the reason of this, but i have the feeling you will say "use maybe an Integer" and check the values. – George Z. May 17 '19 at 11:17
  • @jbx I described my goal. – George Z. May 17 '19 at 11:17
  • OK. Your use case seems very weird. Java does not support inheritance between annotations (for various complexity reasons). I wonder if you should be doing what you are trying to achieve in other ways rather than annotations. – jbx May 17 '19 at 12:58
  • @jbx I know. However, what alternatives should i consider ? Using an integer is a solution but i do not want to follow it. – George Z. May 17 '19 at 13:33
  • I dont know what is this need to inherit between annotated fields. I don't know enough about the real thing you are trying to model. It seems you decided to use annotations while in fact it might not be the right thing. – jbx May 17 '19 at 13:55

2 Answers2

4

The reflection api does not allow to query whether an annotation value has been specified explicitly or merely defaulted.

The usual workaround is to specify a default value that nobody in their right mind would specify explicitly, and check for that value instead. For instance, JPA uses "" for that purpose.

One might try

Boolean value() default null;

but as you rightly point out in the comments, java does not support Boolean annotation values, only boolean ones. You could use an enum with 3 values instead, but that's probably burdensome for your users.

That leaves dark magic: You could parse the classfile yourself. This would work, because the classfile only lists specified annotation attributes, as the following javap output shows:

Given

@Anno(false)
public void foo() 

we get

Constant pool:
    ...
    #16 = Utf8               Lstackoverflow/Anno;
    #17 = Utf8               value
    #18 = Integer            0

  public void foo();
    descriptor: ()V
    flags: ACC_PUBLIC
    RuntimeVisibleAnnotations:
      0: #16(#17=Z#18)

but given

@Anno()
public void foo() {

we get

Constant pool:
  ...
  #16 = Utf8               Lstackoverflow/Anno;

public void foo();
  descriptor: ()V
  flags: ACC_PUBLIC
  RuntimeVisibleAnnotations:
    0: #16()

That said, even if you manage to do this, you might surprise your users and confuse their tooling. For instance, it's quite possible that an IDE will flag explicit assignments of a default value as redundant.

If at all possible, I'd therefore change your annotation so you don't have to distinguish whether a boolean has been specified explicitly.

meriton
  • 68,356
  • 14
  • 108
  • 175
  • I have already tried this. It was actually one of my first attempts. The problem though, is that annotations do not allow wrapper classes (Integer.class, Boolean.class and etc). https://i.imgur.com/VIGUlsf.png – George Z. May 18 '19 at 21:00
  • Oops, totally forgot that stupid restriction. Updated. – meriton May 18 '19 at 21:43
  • 2
    This seems a bit dirty to me. I am not going to use it but hats off for the idea. Creating an enum is not in my taste for something like this too. After all, i have already picked the `String` way, using "false" and "true". I spent much time trying to figure out if it is possible and that's why i posted in SO. You know... for some more ideas just in case. Anyway, I guess the "The reflection api does not allow...." is my answer. Thanks. – George Z. May 18 '19 at 21:55
2

I know a few years has passed but for reference, there is a less dark magic way to achieve this. If you have access to the .java file, you can use the JavaCompiler api to process the annotations and know whether an Annotation Method is overriden. Small example:

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.*;
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Collections;

import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.Trees;

import java.util.List;
import java.util.Set;

@Retention(RetentionPolicy.RUNTIME)
@interface Anno {
    boolean include() default false;
}

@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("*")
class AnnotationProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element e : roundEnv.getElementsAnnotatedWith(Anno.class)) {
            final Trees trees = Trees.instance(processingEnv);
            List<? extends AnnotationTree> annotationTrees = ((MethodTree) trees.getTree(e)).getModifiers().getAnnotations();
            System.out.printf("%s is annotated with %s%n", e, annotationTrees);
            if (annotationTrees.size() > 0 && annotationTrees.get(0).getArguments().size() > 0) {
                System.out.println("Using overridden value");
            } else {
                System.out.println("Using default value");
            }
        }
        return true;
    }
}

class Main {
    @Anno(include = false)
    public void includeFalse() {
    }

    @Anno(include = true)
    public void includeTrue() {
    }

    @Anno()
    public void includeDefault() {
    }

    public static void main(String[] args) {
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
        File file = new File(System.getProperty("user.dir") + "/src/Annot.java"); // Location of the .java file
        Iterable<? extends JavaFileObject> fileObjects = fileManager.getJavaFileObjectsFromFiles(Collections.singletonList(file));
        JavaCompiler.CompilationTask task = compiler.getTask(null,
                fileManager,
                null,
                null,
                null,
                fileObjects);
        task.setProcessors(Collections.singletonList(new AnnotationProcessor()));
        task.call();
    }

}

I still wouldn't recommend doing this because as mentioned earlier it can cause a lot of confusion for the users. In fact the only reason I know this trick is because it caused a very hard to find bug in our code :)

Azad
  • 81
  • 2
  • In fact, no surprise, it pointed out that what I wanted to do was wrong. +1 though, I did not even know annotation processors back then. – George Z. Jun 07 '21 at 11:08
  • Not a solution, but the compiler usage is quite interresting. – pdem Aug 25 '22 at 12:29