75

I would like to use constants for annotation values.

interface Client {

    @Retention(RUNTIME)
    @Target(METHOD)
    @interface SomeAnnotation { String[] values(); }

    interface Info {
        String A = "a";
        String B = "b";
        String[] AB = new String[] { A, B };
    }

    @SomeAnnotation(values = { Info.A, Info.B })
    void works();

    @SomeAnnotation(values = Info.AB)
    void doesNotWork();
}

The constants Info.A and Info.B can be used in the annotation but not the array Info.AB as it has to be an array initializer in this place. Annotation values are restricted to values that could be inlined into the byte code of a class. This is not possible for the array constant as it has to be constructed when Info is loaded. Is there a workaround for this problem?

1
  • 1
    The Eclipse compile error is quite explicit: "The value for annotation attribute Client.doesNotWork.values must be an array initializer". That's very clear, I don't think there's a workaround. Commented Sep 7, 2009 at 7:24

7 Answers 7

66

No, there is no workaround.

Sign up to request clarification or add additional context in comments.

2 Comments

@JensSchauder The annotations are processed at compile-time, so even before the code ever runs. So the array AB does not exist yet.
if the compiler expects an "Array Initializer" to be passed to the Annotation, declaring a compile-time constant like private static final String[] AB = { ... }; should do. it's understood that Annotation processing happens before the actual compilation, but then the error message is not accurate.
18

Why not make the annotation values an enum, which are keys to the actual data values you want?

e.g.

enum InfoKeys
{
 A("a"),
 B("b"),
 AB(new String[] { "a", "b" }),

 InfoKeys(Object data) { this.data = data; }
 private Object data;
}

@SomeAnnotation (values = InfoKeys.AB)

This could be improved for type safety, but you get the idea.

6 Comments

+1 Nice thinking. An example that compiled would have been even nicer ;-)
Good idea. This is okay if you're able to change the annotation. You have to use @interface SomeAnnotation { InfoKeys values(); }. Sadly, it cannot change the annotation type itself.
Changing the annotation type would restrict the use to values of this enumeration. This is to retrictive for most use cases.
@Thomas: yes, this approach also has disadvantages. It really depends on what you want to achieve exactly.
You have an given annotation. The use of this annotation should be not redundant. The ideal solution would be to support full and partial reuse of an "annotation instance". Full reuse: @x = @SomeAnnotation(...); @x m(); @x y();. Partial reuse: @SomeAnnotation(childAnnotation=@x) m(). Referencing annotation values is a compromise not a goal.
|
6

It is because arrays' elements can be changed at runtime (Info.AB[0] = "c";) while the annotation values are constant after compile time.

With that in mind someone will inevitably be confused when they try to change an element of Info.AB and expect the annotation's value to change (it won't). And if the annotation value were allowed to change at runtime it would differ than the one used at compile time. Imagine the confusion then!

(Where confusion here means that there is a bug that someone may find and spend hours debugging.)

Comments

3

While there is no way to pass an array directly as an annotation parameter value, there is a way to effectively get similar behavior (depending on how you plan on using your annotations, this may not work for every use case).

Here's an example -- let's say we have a class InternetServer and it has a hostname property. We'd like to use regular Java Validation to ensure that no object has a "reserved" hostname. We can (somewhat elaborately) pass an array of reserved hostnames to the annotation that handles hostname validation.

caveat- with Java Validation, it would be more customary to use the "payload" to pass in this kind of data. I wanted this example to be a bit more generic so I used a custom interface class.

// InternetServer.java -- an example class that passes an array as an annotation value
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Pattern;

public class InternetServer {

    // These are reserved names, we don't want anyone naming their InternetServer one of these
    private static final String[] RESERVED_NAMES = {
        "www", "wwws", "http", "https",
    };

    public class ReservedHostnames implements ReservedWords {
        // We return a constant here but could do a DB lookup, some calculation, or whatever
        // and decide what to return at run-time when the annotation is processed.
        // Beware: if this method bombs, you're going to get nasty exceptions that will
        // kill any threads that try to load any code with annotations that reference this.
        @Override public String[] getReservedWords() { return RESERVED_NAMES; }
    }

    @Pattern(regexp = "[A-Za-z0-9]{3,}", message = "error.hostname.invalid")
    @NotReservedWord(reserved=ReservedHostnames.class, message="error.hostname.reserved")
    @Getter @Setter private String hostname;
}

// NotReservedWord.java -- the annotation class
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({FIELD, ANNOTATION_TYPE})
@Retention(RUNTIME)
@Constraint(validatedBy=ReservedWordValidator.class)
@Documented
public @interface NotReservedWord {

    Class<? extends ReservedWords> reserved ();

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String message() default "{err.reservedWord}";

}

// ReservedWords.java -- the interface referenced in the annotation class
public interface ReservedWords {
    public String[] getReservedWords ();
}

// ReservedWordValidator.java -- implements the validation logic
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ReservedWordValidator implements ConstraintValidator<NotReservedWord, Object> {

    private Class<? extends ReservedWords> reserved;

    @Override
    public void initialize(NotReservedWord constraintAnnotation) {
        reserved = constraintAnnotation.reserved();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null) return true;
        final String[] words = getReservedWords();
        for (String word : words) {
            if (value.equals(word)) return false;
        }
        return true;
    }

    private Map<Class, String[]> cache = new ConcurrentHashMap<>();

    private String[] getReservedWords() {
        String[] words = cache.get(reserved);
        if (words == null) {
            try {
                words = reserved.newInstance().getReservedWords();
            } catch (Exception e) {
                throw new IllegalStateException("Error instantiating ReservedWords class ("+reserved.getName()+"): "+e, e);
            }
            cache.put(reserved, words);
        }
        return words;
    }
}

Comments

1

You can use {} instead of new String[0] for example.

It will finally result like that:

@interface SomeAnnotation {
    String[] values() default {};
}

Comments

0
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Handler {

    enum MessageType { MESSAGE, OBJECT };

    String value() default "";

    MessageType type() default MessageType.MESSAGE;

}

1 Comment

This is a poor quality answer even if it is correct. Adding an explanation would greatly improve it (as @Peanut said)
0

As already was mentioned in previous posts, annotation vales are compile-time constants and there is no way to use an array value as a parameter.

I solved this problem a bit differently.

If you're owning the processing logic, take advantage of it.

For example, give an additional parameter to your annotation:

@Retention(RUNTIME)
@Target(METHOD)
@interface SomeAnnotation { 
    String[] values();
    boolean defaultInit() default false;
}

Use this parameter:

@SomeAnnotation(defaultInit = true)
void willWork();

And this will be a marker to the AnnotationProcessor, which can do anything - initialize it with an array, use String[], or use Enums like Enum.values() and map them to String[].

Hope this will guide someone who has the similar situation in the right direction.

4 Comments

Can you show how we would use this to pass an array of values to the annotation?
How does this solve the OP problem? You are still left passing array initialisers into the annoation. How do I use a constant like structure to centralise for repeated data?
Well, you didn't clarify the context. You asked me a direct question and I answered. If you still want the solution for the OP problem, what I suggested is to use constants in annotation processor. E.g. you have an enum, if defaultInit is true, then take enum.values() in annotation processor. You can also create mapping between an array of values and a parameter, if there are not so many.
OK sorry, to clarify, the context for my comment is the question you are answering.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.