How to use an array constant in an annotation
No, there is no workaround.
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.)
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.
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;
}
}