Hinzufügen von Java-Anmerkungen zur Laufzeit

76

Ist es möglich, einem Objekt (in meinem Fall insbesondere einer Methode) zur Laufzeit eine Anmerkung hinzuzufügen?

Für ein bisschen mehr Erklärung: Ich habe zwei Module, Modul A und Modul B. Modul B hängt von Modul A ab, das von nichts abhängt. (modA ist meine Kerndatentypen und -schnittstellen und so ist modB die Datenbank / Datenschicht) modB hängt auch von externalLibrary ab. In meinem Fall übergibt modB eine Klasse von modA an externalLibrary, für die bestimmte Methoden mit Anmerkungen versehen werden müssen. Die spezifischen Anmerkungen sind alle Teil von externalLib, und wie gesagt, modA hängt nicht von externalLib ab, und ich möchte, dass dies auch so bleibt.

Ist dies also möglich oder haben Sie Vorschläge für andere Sichtweisen auf dieses Problem?

Clayton
quelle
Überprüfen Sie, ob dies hilfreich sein kann. Stackoverflow.com/a/14276270/4741746 Zumindest können wir es ändern
sushant gosavi

Antworten:

21

Es ist nicht möglich, zur Laufzeit eine Annotation hinzuzufügen. Es scheint, als müssten Sie einen Adapter einführen , mit dem Modul B das Objekt aus Modul A umschließt und die erforderlichen annotierten Methoden verfügbar macht.

Tom
quelle
1
Ich unterstütze das. Aber ich könnte in Betracht ziehen, das Original zu kommentieren, ich sehe hier kein großes Problem. Wir tun dies normalerweise im Fall von JPA-Entitäten, die Sie an eine entfernte EJB-Komponente übergeben, um sie in der Datenbank zu speichern. Und Sie verwenden dasselbe, um Ihre Benutzeroberfläche zu füllen.
Adeel Ansari
Tom: Ah natürlich. Vielleicht mit Vererbung: Erweitern Sie die Klasse aus Modul A, überschreiben Sie die betreffende Methode und kommentieren Sie diese dann?
Clayton
Essig: Das ist wahrscheinlich die einfachste Lösung für mich. Ich habe versucht, mein "Datenmodell" von meiner "Datenimplementierung" zu trennen, aber ich sehe ehrlich gesagt keine Zeit, in der ich eine andere Datenimplementierung anschließen müsste.
Clayton
Also, geh einfach mit dem Einfachen. Trotzdem ist es praktisch, Ihren Adapter später zu codieren, wann immer es Zeit ist. Wie Sie sagten, können Sie eine Vererbung in Betracht ziehen, also wird der Umgang mit dem Supertyp dies tun.
Adeel Ansari
1
Ich entschied mich für einen hybriden Ansatz. Im Moment habe ich gerade die ursprüngliche Methode mit Anmerkungen versehen (Hinzufügen der Abhängigkeit zu modA), um herauszufinden, dass ich die Anmerkung später immer nur abrufen und einen Adapter verwenden kann. Danke Leute!
Clayton
42

Dies ist über eine Bytecode-Instrumentierungsbibliothek wie Javassist möglich .

In der AnnotationsAttribute- Klasse finden Sie ein Beispiel zum Erstellen / Festlegen von Annotationen und einen Tutorial-Abschnitt zur Bytecode-API mit allgemeinen Richtlinien zum Bearbeiten von Klassendateien.

Dies ist jedoch alles andere als einfach und unkompliziert. Ich würde diesen Ansatz NICHT empfehlen und vorschlagen, stattdessen Toms Antwort in Betracht zu ziehen, es sei denn, Sie müssen dies für eine große Anzahl von Klassen tun (oder diese Klassen stehen Ihnen erst zur Laufzeit und damit zum Schreiben zur Verfügung ein Adapter ist nicht möglich).

ChssPly76
quelle
26

Es ist auch möglich, einer Java-Klasse zur Laufzeit mithilfe der Java-Reflection-API eine Anmerkung hinzuzufügen. Im Wesentlichen müssen die in der Klasse definierten internen Annotation Maps java.lang.Class(oder für in der internen Klasse definiertes Java 8 java.lang.Class.AnnotationData) neu erstellt werden. Natürlich ist dieser Ansatz ziemlich hackig und kann bei neueren Java-Versionen jederzeit unterbrochen werden. Für schnelle und schmutzige Tests / Prototypen kann dieser Ansatz jedoch manchmal nützlich sein.

Beispiel für ein Konzept für 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);
            }
        });
    }
}

Anwendungsbeispiel:

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

Ausgabe:

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

Einschränkungen dieses Ansatzes:

  • Neue Versionen von Java können den Code jederzeit beschädigen.
  • Das obige Beispiel funktioniert nur für Java 8 - damit es für ältere Java-Versionen funktioniert, muss die Java-Version zur Laufzeit überprüft und die Implementierung entsprechend geändert werden.
  • Wenn die mit Anmerkungen versehene Klasse neu definiert wird (z. B. während des Debuggens), geht die Anmerkung verloren.
  • Nicht gründlich getestet; Ich bin mir nicht sicher, ob es irgendwelche schlimmen Nebenwirkungen gibt - die Verwendung erfolgt auf eigenes Risiko ...
Balder
quelle
Gute Arbeit, ich würde mich sehr freuen,
wenn
Irgendwelche Vorschläge, wie dies für Felder funktioniert?
Heez
2
@heez Ich habe diese Arbeit an Feldern und Methoden gemacht. Sie können AnnotationUtil.java sehen .
Dean Xu
6

Es ist möglich, Anmerkungen zur Laufzeit über einen Proxy zu erstellen . Sie können sie dann über Reflektion zu Ihren Java-Objekten hinzufügen, wie in anderen Antworten vorgeschlagen (aber Sie sollten wahrscheinlich besser einen alternativen Weg finden, um damit umzugehen, da das Herumspielen mit den vorhandenen Typen über Reflektion gefährlich und schwer zu debuggen sein kann).

Aber es ist nicht sehr einfach ... Ich habe eine Bibliothek namens Javanna geschrieben , um dies einfach mit einer sauberen API zu tun.

Es ist in JCenter und Maven Central .

Es benutzen:

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

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

Wenn ein Eintrag in der Karte nicht mit den mit Anmerkungen deklarierten Feldern und Typen übereinstimmt, wird eine Ausnahme ausgelöst. Wenn ein Wert fehlt, der keinen Standardwert hat, wird eine Ausnahme ausgelöst.

Auf diese Weise kann davon ausgegangen werden, dass jede erfolgreich erstellte Annotationsinstanz genauso sicher ist wie eine Annotationsinstanz zur Kompilierungszeit.

Als Bonus kann diese Bibliothek auch Annotationsklassen analysieren und die Werte der Annotation als Map zurückgeben:

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

Dies ist praktisch für die Erstellung von Mini-Frameworks.

Renato
quelle