web-dev-qa-db-fra.com

Comment utiliser une constante de tableau dans une annotation

Je voudrais utiliser des constantes pour les valeurs d'annotation.

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

Les constantes Info.A et Info.B peuvent être utilisées dans l'annotation, mais pas le tableau Info.AB car il doit s'agir d'un initialiseur de tableau à cet endroit. Les valeurs d'annotation sont limitées aux valeurs pouvant être insérées dans le code d'octet d'une classe. Ce n'est pas possible pour la constante de tableau car il doit être construit lorsque Info est chargé. Existe-t-il une solution de contournement à ce problème?

46
Thomas Jung

Non, il n'y a pas de solution de contournement.

42
Thomas Jung

Pourquoi ne pas faire des valeurs d’annotation une énumération, qui sont des clés pour les valeurs de données réelles que vous voulez?

par exemple.

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

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

@SomeAnnotation (values = InfoKeys.AB)

Cela pourrait être amélioré pour la sécurité de type, mais vous voyez l'idée.

13
amarillion

Bien qu'il n'y ait aucun moyen de passer un tableau directement en tant que valeur de paramètre d'annotation, il y a est un moyen d'obtenir efficacement un comportement similaire cas d'utilisation).

Voici un exemple - Disons que nous avons une classe InternetServer et qu'elle a une propriété hostname. Nous aimerions utiliser la validation Java régulière pour nous assurer qu'aucun objet ne possède un nom d'hôte "réservé". Nous pouvons (de manière assez détaillée) passer un tableau de noms d’hôtes réservés à l’annotation qui gère la validation du nom d’hôte.

avec Java Validation, il serait plus habituel d’utiliser le "payload" pour transmettre ce type de données. Je voulais que cet exemple soit un peu plus générique alors j'ai utilisé une classe d'interface personnalisée.

// 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;
    }
}
2
cobbzilla

C'est parce que les éléments des tableaux peuvent être modifiés au moment de l'exécution (Info.AB[0] = "c";) alors que les valeurs des annotations sont constantes après la compilation.

En gardant cela à l'esprit, quelqu'un sera inévitablement dérouté lorsqu'il tentera de modifier un élément de Info.AB et s'attendra à ce que la valeur de l'annotation change (ce ne sera pas le cas). Et si la valeur de l'annotation était autorisée à changer au moment de l'exécution, elle serait différente de celle utilisée à la compilation. Imaginez la confusion alors!

(Où confusion ici signifie qu'il y a un bogue dans lequel quelqu'un peut trouve et passe des heures à déboguer.)

1
Michael Deardeuff

Comme cela a déjà été mentionné dans de précédents articles, les valeurs d'annotation sont des constantes à la compilation et il n'existe aucun moyen d'utiliser une valeur de tableau en tant que paramètre.

J'ai résolu ce problème un peu différemment.

Si vous possédez la logique de traitement, profitez-en.

Par exemple, donnez un paramètre supplémentaire à votre annotation:

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

Utilisez ce paramètre:

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

Et ce sera un marqueur de AnnotationProcessor, qui peut tout faire - initialisez-le avec un tableau, utilisez String[] ou utilisez Enums comme Enum.values() et mappez-les sur String[].

J'espère que cela guidera quelqu'un qui a la même situation dans la bonne direction.

0
J-Alex