How to use an array constant in an annotation

JavaAnnotations

Java Problem Overview


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?

Java Solutions


Solution 1 - Java

No, there is no workaround.

Solution 2 - Java

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.

Solution 3 - Java

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.)

Solution 4 - Java

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;
    }
}

Solution 5 - Java

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;

}

Solution 6 - Java

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.

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionThomas JungView Question on Stackoverflow
Solution 1 - JavaThomas JungView Answer on Stackoverflow
Solution 2 - JavaamarillionView Answer on Stackoverflow
Solution 3 - JavaMichael DeardeuffView Answer on Stackoverflow
Solution 4 - JavacobbzillaView Answer on Stackoverflow
Solution 5 - JavaShakeel KadamView Answer on Stackoverflow
Solution 6 - JavaJ-AlexView Answer on Stackoverflow