web-dev-qa-db-fra.com

Ajout de Java annotations au moment de l'exécution

Est-il possible d'ajouter une annotation à un objet (dans mon cas en particulier, une méthode) lors de l'exécution?

Pour un peu plus d'explication: j'ai deux modules, moduleA et moduleB. moduleB dépend de moduleA, qui ne dépend de rien. (modA est mes principaux types de données et interfaces et tel, modB est db/couche de données) modB dépend également de externalLibrary. Dans mon cas, modB transfère une classe de modA à externalLibrary, qui nécessite l'annotation de certaines méthodes. Les annotations spécifiques font toutes partie de externalLib et, comme je l'ai dit, modA ne dépend pas de externalLib et j'aimerais qu'il en soit ainsi.

Alors, est-ce possible, ou avez-vous des suggestions pour d'autres façons de résoudre ce problème?

67
Clayton

Il n'est pas possible d'ajouter une annotation à l'exécution, il semble que vous ayez besoin d'introduire un adaptateur que le module B utilise pour encapsuler l'objet du module A exposant les méthodes annotées requises.

21
Tom

C'est possible via la bibliothèque d'instrumentation bytecode telle que Javassist .

En particulier, jetez un œil à la classe AnnotationsAttribute pour un exemple sur la façon de créer/définir des annotations et section du didacticiel sur l'API bytecode pour des directives générales sur la façon de manipuler les fichiers de classe.

Ceci est tout sauf simple et direct, cependant - je ne recommanderais PAS cette approche et je vous suggère de considérer la réponse de Tom à la place, sauf si vous devez le faire pour un grand nombre de classes (ou que lesdites classes ne sont pas disponibles jusqu'à l'exécution et donc écrivent) un adaptateur est impossible).

39
ChssPly76

Il est également possible d'ajouter une annotation à une classe Java lors de l'exécution à l'aide de l'API de réflexion Java. Il faut essentiellement recréer les cartes d'annotation internes définies dans la classe Java.lang.Class (ou pour Java 8 défini dans la classe interne Java.lang.Class.AnnotationData). Naturellement, cette approche est assez hacky et peut casser à tout moment pour les nouvelles versions Java. Mais pour des tests/prototypes rapides et sales, cette approche peut être utile à certains moments.

Exemple de preuve de concept pour Java 8:

public final class RuntimeAnnotations {

    private static final Constructor<?> AnnotationInvocationHandler_constructor;
    private static final Constructor<?> AnnotationData_constructor;
    private static final Method Class_annotationData;
    private static final Field Class_classRedefinedCount;
    private static final Field AnnotationData_annotations;
    private static final Field AnnotationData_declaredAnotations;
    private static final Method Atomic_casAnnotationData;
    private static final Class<?> Atomic_class;

    static{
        // static initialization of necessary reflection Objects
        try {
            Class<?> AnnotationInvocationHandler_class = Class.forName("Sun.reflect.annotation.AnnotationInvocationHandler");
            AnnotationInvocationHandler_constructor = AnnotationInvocationHandler_class.getDeclaredConstructor(new Class[]{Class.class, Map.class});
            AnnotationInvocationHandler_constructor.setAccessible(true);

            Atomic_class = Class.forName("Java.lang.Class$Atomic");
            Class<?> AnnotationData_class = Class.forName("Java.lang.Class$AnnotationData");

            AnnotationData_constructor = AnnotationData_class.getDeclaredConstructor(new Class[]{Map.class, Map.class, int.class});
            AnnotationData_constructor.setAccessible(true);
            Class_annotationData = Class.class.getDeclaredMethod("annotationData");
            Class_annotationData.setAccessible(true);

            Class_classRedefinedCount= Class.class.getDeclaredField("classRedefinedCount");
            Class_classRedefinedCount.setAccessible(true);

            AnnotationData_annotations = AnnotationData_class.getDeclaredField("annotations");
            AnnotationData_annotations.setAccessible(true);
            AnnotationData_declaredAnotations = AnnotationData_class.getDeclaredField("declaredAnnotations");
            AnnotationData_declaredAnotations.setAccessible(true);

            Atomic_casAnnotationData = Atomic_class.getDeclaredMethod("casAnnotationData", Class.class, AnnotationData_class, AnnotationData_class);
            Atomic_casAnnotationData.setAccessible(true);

        } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | NoSuchFieldException e) {
            throw new IllegalStateException(e);
        }
    }

    public static <T extends Annotation> void putAnnotation(Class<?> c, Class<T> annotationClass, Map<String, Object> valuesMap){
        putAnnotation(c, annotationClass, annotationForMap(annotationClass, valuesMap));
    }

    public static <T extends Annotation> void putAnnotation(Class<?> c, Class<T> annotationClass, T annotation){
        try {
            while (true) { // retry loop
                int classRedefinedCount = Class_classRedefinedCount.getInt(c);
                Object /*AnnotationData*/ annotationData = Class_annotationData.invoke(c);
                // null or stale annotationData -> optimistically create new instance
                Object newAnnotationData = createAnnotationData(c, annotationData, annotationClass, annotation, classRedefinedCount);
                // try to install it
                if ((boolean) Atomic_casAnnotationData.invoke(Atomic_class, c, annotationData, newAnnotationData)) {
                    // successfully installed new AnnotationData
                    break;
                }
            }
        } catch(IllegalArgumentException | IllegalAccessException | InvocationTargetException | InstantiationException e){
            throw new IllegalStateException(e);
        }

    }

    @SuppressWarnings("unchecked")
    private static <T extends Annotation> Object /*AnnotationData*/ createAnnotationData(Class<?> c, Object /*AnnotationData*/ annotationData, Class<T> annotationClass, T annotation, int classRedefinedCount) throws InstantiationException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        Map<Class<? extends Annotation>, Annotation> annotations = (Map<Class<? extends Annotation>, Annotation>) AnnotationData_annotations.get(annotationData);
        Map<Class<? extends Annotation>, Annotation> declaredAnnotations= (Map<Class<? extends Annotation>, Annotation>) AnnotationData_declaredAnotations.get(annotationData);

        Map<Class<? extends Annotation>, Annotation> newDeclaredAnnotations = new LinkedHashMap<>(annotations);
        newDeclaredAnnotations.put(annotationClass, annotation);
        Map<Class<? extends Annotation>, Annotation> newAnnotations ;
        if (declaredAnnotations == annotations) {
            newAnnotations = newDeclaredAnnotations;
        } else{
            newAnnotations = new LinkedHashMap<>(annotations);
            newAnnotations.put(annotationClass, annotation);
        }
        return AnnotationData_constructor.newInstance(newAnnotations, newDeclaredAnnotations, classRedefinedCount);
    }

    @SuppressWarnings("unchecked")
    public static <T extends Annotation> T annotationForMap(final Class<T> annotationClass, final Map<String, Object> valuesMap){
        return (T)AccessController.doPrivileged(new PrivilegedAction<Annotation>(){
            public Annotation run(){
                InvocationHandler handler;
                try {
                    handler = (InvocationHandler) AnnotationInvocationHandler_constructor.newInstance(annotationClass,new HashMap<>(valuesMap));
                } catch (InstantiationException | IllegalAccessException
                        | IllegalArgumentException | InvocationTargetException e) {
                    throw new IllegalStateException(e);
                }
                return (Annotation)Proxy.newProxyInstance(annotationClass.getClassLoader(), new Class[] { annotationClass }, handler);
            }
        });
    }
}

Exemple d'utilisation:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface TestAnnotation {
    String value();
}

public static class TestClass{}

public static void main(String[] args) {
    TestAnnotation annotation = TestClass.class.getAnnotation(TestAnnotation.class);
    System.out.println("TestClass annotation before:" + annotation);

    Map<String, Object> valuesMap = new HashMap<>();
    valuesMap.put("value", "some String");
    RuntimeAnnotations.putAnnotation(TestClass.class, TestAnnotation.class, valuesMap);

    annotation = TestClass.class.getAnnotation(TestAnnotation.class);
    System.out.println("TestClass annotation after:" + annotation);
}

Sortie:

TestClass annotation before:null
TestClass annotation after:@RuntimeAnnotations$TestAnnotation(value=some String)

Limites de cette approche:

  • Les nouvelles versions de Java peuvent casser le code à tout moment.
  • L'exemple ci-dessus ne fonctionne que pour Java 8 - le faire fonctionner pour les anciennes versions Java nécessite de vérifier la version Java à l'adresse runtime et modifier l'implémentation en conséquence.
  • Si la classe annotée obtient redéfinie (par exemple pendant le débogage), l'annotation sera perdue.
  • Pas complètement testé; Je ne sais pas s'il y a de mauvais effets secondaires - utilisez à vos risques et périls ...
22
Balder

Il est possible de créer des annotations lors de l'exécution via un Proxy . Vous pouvez ensuite les ajouter à vos Java via la réflexion comme suggéré dans d'autres réponses (mais vous feriez probablement mieux de trouver une autre façon de gérer cela, car jouer avec les types existants via la réflexion peut être dangereux et difficile à déboguer).

Mais ce n'est pas très facile ... J'ai écrit une bibliothèque appelée, j'espère de façon appropriée, Javanna juste pour le faire facilement en utilisant une API propre.

C'est dans JCenter et Maven Central .

En l'utilisant:

@Retention( RetentionPolicy.RUNTIME )
@interface Simple {
    String value();
}

Simple simple = Javanna.createAnnotation( Simple.class, 
    new HashMap<String, Object>() {{
        put( "value", "the-simple-one" );
    }} );

Si une entrée de la carte ne correspond pas aux champs et types d'annotation déclarés, une exception est levée. Si une valeur sans valeur par défaut est manquante, une exception est levée.

Cela permet de supposer que chaque instance d'annotation créée avec succès est aussi sûre à utiliser qu'une instance d'annotation au moment de la compilation.

En prime, cette bibliothèque peut également analyser les classes d'annotations et renvoyer les valeurs de l'annotation sous forme de carte:

Map<String, Object> values = Javanna.getAnnotationValues( annotation );

C'est pratique pour créer des mini-frameworks.

4
Renato