web-dev-qa-db-fra.com

Comment créer une instance d'une annotation

J'essaie de faire de la magie d'annotation Java. Je dois dire que je suis toujours en train de rattraper les astuces d'annotation et que certaines choses ne sont toujours pas très claires pour moi.

Alors ... j'ai quelques classes, méthodes et champs annotés. J'ai une méthode qui utilise la réflexion pour exécuter des vérifications sur les classes et injecter des valeurs dans une classe. Tout fonctionne bien.

Cependant, je suis maintenant confronté à un cas où j'ai besoin d'une instance (pour ainsi dire) d'une annotation. Donc ... les annotations ne sont pas comme des interfaces régulières et vous ne pouvez pas faire une implémentation anonyme d'une classe. J'ai compris. J'ai parcouru quelques articles ici concernant des problèmes similaires, mais je n'arrive pas à trouver la réponse à ce que je recherche.

Je voudrais essentiellement obtenir une instance d'annotation et pouvoir définir certains de ses champs à l'aide de la réflexion (je suppose). Existe-t-il un moyen de le faire?

52
carlspring

Eh bien, ce n'est apparemment rien de si compliqué. Vraiment !

Comme l'a souligné un collègue, vous pouvez simplement créer une instance anonyme de l'annotation (comme n'importe quelle interface) comme ceci:

MyAnnotation:

public @interface MyAnnotation
{

    String foo();

}

Appel de code:

class MyApp
{
    MyAnnotation getInstanceOfAnnotation(final String foo)
    {
        MyAnnotation annotation = new MyAnnotation()
        {
            @Override
            public String foo()
            {
                return foo;
            }

            @Override
            public Class<? extends Annotation> annotationType()
            {
                return MyAnnotation.class;
            }
        };

        return annotation;
    }
}

Crédits à Martin Grigorov .

64
carlspring

L'approche proxy, comme suggéré dans réponse de Gunnar est déjà implémentée dans GeAnTyRef :

Map<String, Object> annotationParameters = new HashMap<>();
annotationParameters.put("name", "someName");
MyAnnotation myAnnotation = TypeFactory.annotation(MyAnnotation.class, annotationParameters);

Cela produira une annotation équivalente à ce que vous obtiendriez:

@MyAnnotation(name = "someName")

Les instances d'annotation produites de cette façon agiront de la même manière que celles produites par Java normalement, et leurs hashCode et equals ont été correctement implémentées pour des raisons de compatibilité, donc pas de bizarre mises en garde comme avec l'instanciation directe de l'annotation comme dans la réponse acceptée. En fait, JDK utilise en interne cette même approche: Sun.reflect.annotation.AnnotationParser # annotationForMap .

La bibliothèque elle-même est minuscule et n'a aucune dépendance.

Divulgation: Je suis le développeur derrière GeAnTyRef.

11
kaqqao

Vous pouvez utiliser un proxy d'annotation tel que celui-ci , ou celui-ci du projet Hibernate Validator (avertissement: je suis un committer de ce dernier).

6
Gunnar

Vous pouvez utiliser Sun.reflect.annotation.AnnotationParser.annotationForMap(Class, Map):

public @interface MyAnnotation {
    String foo();
}

public class MyApp {
    public MyAnnotation getInstanceOfAnnotation(final String foo) {
        MyAnnotation annotation = AnnotationParser.annotationForMap(
            MyAnnotation.class, Collections.singletonMap("foo", "myFooResult"));
    }
}

Inconvénient: les classes de Sun.* Sont susceptibles de changer dans les versions ultérieures (bien que cette méthode existe depuis Java 5 avec la même signature) et ne sont pas disponibles pour tous Java, voir cette discussion .

Si c'est un problème: vous pouvez créer un proxy générique avec votre propre InvocationHandler - c'est exactement ce que AnnotationParser fait pour vous en interne. Ou vous utilisez votre propre implémentation de MyAnnotation comme défini ici . Dans les deux cas, n'oubliez pas d'implémenter annotationType(), equals() et hashCode() car le résultat est documenté spécifiquement pour Java.lang.Annotation.

5
Tobias Liefke

Manière assez grossière en utilisant l'approche proxy avec l'aide d'Apache Commons AnnotationUtils

public static <A extends Annotation> A mockAnnotation(Class<A> annotationClass, Map<String, Object> properties) {
    return (A) Proxy.newProxyInstance(annotationClass.getClassLoader(), new Class<?>[] { annotationClass }, (proxy, method, args) -> {
        Annotation annotation = (Annotation) proxy;
        String methodName = method.getName();

        switch (methodName) {
            case "toString":
                return AnnotationUtils.toString(annotation);
            case "hashCode":
                return AnnotationUtils.hashCode(annotation);
            case "equals":
                return AnnotationUtils.equals(annotation, (Annotation) args[0]);
            case "annotationType":
                return annotationClass;
            default:
                if (!properties.containsKey(methodName)) {
                    throw new NoSuchMethodException(String.format("Missing value for mocked annotation property '%s'. Pass the correct value in the 'properties' parameter", methodName));
                }
                return properties.get(methodName);
        }
    });
}

Les types de propriétés transmises ne sont pas vérifiés avec le type réel déclaré sur l'interface d'annotation et toutes les valeurs manquantes sont découvertes uniquement pendant l'exécution.

Fonction assez similaire au code mentionné dans réponse de kaqqao (et probablement réponse de Gunnar également), sans les inconvénients de l'utilisation interne Java API comme dans réponse de Tobias Liefke.

0
oujesky