CodeGym /Java-Blog /Random-DE /Reflection-API: Reflection. Die dunkle Seite von Java
Autor
Pavlo Plynko
Java Developer at CodeGym

Reflection-API: Reflection. Die dunkle Seite von Java

Veröffentlicht in der Gruppe Random-DE
Grüße, junger Padawan. In diesem Artikel erzähle ich Ihnen von der Macht, einer Macht, die Java-Programmierer nur in scheinbar unmöglichen Situationen einsetzen. Die Schattenseite von Java ist die Reflection API. In Java wird Reflektion mithilfe der Java Reflection API implementiert.

Was ist Java-Reflektion?

Im Internet gibt es eine kurze, genaue und beliebte Definition. Reflexion ( von spätlateinisch reflexio – umkehren ) ist ein Mechanismus, um Daten über ein Programm zu untersuchen, während es läuft. Mit Reflection können Sie Informationen zu Feldern, Methoden und Klassenkonstruktoren untersuchen. Mit Reflection können Sie mit Typen arbeiten, die zur Kompilierungszeit nicht vorhanden waren, aber zur Laufzeit verfügbar wurden. Reflexion und ein logisch konsistentes Modell zur Ausgabe von Fehlerinformationen ermöglichen die Erstellung korrekten dynamischen Codes. Mit anderen Worten: Wenn Sie verstehen, wie Reflexion in Java funktioniert, eröffnen sich Ihnen eine Reihe erstaunlicher Möglichkeiten. Sie können Klassen und ihre Komponenten buchstäblich unter einen Hut bringen. Hier ist eine grundlegende Liste dessen, was Reflexion ermöglicht:
  • Die Klasse eines Objekts lernen/bestimmen;
  • Erhalten Sie Informationen zu den Modifikatoren, Feldern, Methoden, Konstanten, Konstruktoren und Superklassen einer Klasse.
  • Finden Sie heraus, welche Methoden zu implementierten Schnittstellen gehören.
  • Erstellen Sie eine Instanz einer Klasse, deren Klassenname bis zur Laufzeit unbekannt ist.
  • Werte der Felder eines Objekts nach Namen abrufen und festlegen;
  • Rufen Sie die Methode eines Objekts nach Namen auf.
Reflection wird in fast allen modernen Java-Technologien verwendet. Es ist schwer vorstellbar, dass Java als Plattform ohne Nachdenken eine so weite Verbreitung hätte erreichen können. Höchstwahrscheinlich wäre das nicht der Fall gewesen. Nachdem Sie nun allgemein mit der Reflexion als theoretischem Konzept vertraut sind, gehen wir nun zu ihrer praktischen Anwendung über! Wir lernen nicht alle Methoden der Reflection-API kennen, sondern nur diejenigen, die Ihnen in der Praxis tatsächlich begegnen. Da die Reflexion die Arbeit mit Klassen beinhaltet, beginnen wir mit einer einfachen Klasse namens MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Wie Sie sehen, ist dies eine sehr einfache Klasse. Der Konstruktor mit Parametern ist bewusst auskommentiert. Wir werden später darauf zurückkommen. Wenn Sie sich den Inhalt der Klasse genau angesehen haben, ist Ihnen wahrscheinlich das Fehlen eines Getters für das Namensfeld aufgefallen . Das Namensfeld selbst ist mit dem privaten Zugriffsmodifikator gekennzeichnet: Wir können außerhalb der Klasse selbst nicht darauf zugreifen, was bedeutet, dass wir seinen Wert nicht abrufen können. „ Also, was ist das Problem ?“ du sagst. „Fügen Sie einen Getter hinzu oder ändern Sie den Zugriffsmodifikator.“ Und Sie hätten Recht, es sei dennMyClassbefand sich in einer kompilierten AAR-Bibliothek oder in einem anderen privaten Modul ohne Möglichkeit, Änderungen vorzunehmen. In der Praxis kommt das ständig vor. Und irgendein unvorsichtiger Programmierer hat einfach vergessen, einen Getter zu schreiben . Dies ist genau die Zeit, sich an die Reflexion zu erinnern! Versuchen wir, zum privaten Namensfeld der MyClassKlasse zu gelangen:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; // No getter =(
   System.out.println(number + name); // Output: 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name); // Output: 0default
}
Lassen Sie uns analysieren, was gerade passiert ist. In Java gibt es eine wunderbare Klasse namens Class. Es repräsentiert Klassen und Schnittstellen in einer ausführbaren Java-Anwendung. ClassWir werden die Beziehung zwischen und nicht behandeln ClassLoader, da dies nicht das Thema dieses Artikels ist. Um die Felder dieser Klasse abzurufen, müssen Sie als Nächstes die Methode aufrufen getFields(). Diese Methode gibt alle zugänglichen Felder dieser Klasse zurück. Das funktioniert bei uns nicht, da unser Feld privat ist , also verwenden wir die getDeclaredFields()Methode. Diese Methode gibt ebenfalls ein Array von Klassenfeldern zurück, enthält jetzt jedoch private und geschützte Felder. In diesem Fall kennen wir den Namen des Feldes, an dem wir interessiert sind, und können daher die getDeclaredField(String)Methode where verwendenStringist der Name des gewünschten Feldes. Notiz: getFields()und getDeclaredFields()geben Sie nicht die Felder einer übergeordneten Klasse zurück! Großartig. Wir haben ein FieldObjekt erhalten, das auf unseren Namen verweist . Da das Feld nicht öffentlich war , müssen wir Zugriff gewähren, um damit arbeiten zu können. Die setAccessible(true)Methode lässt uns weitermachen. Jetzt steht das Namensfeld unter unserer vollständigen Kontrolle! Sie können seinen Wert abrufen, indem Sie die Methode Fielddes Objekts aufrufen , bei der es sich um eine Instanz unserer Klasse handelt. Wir konvertieren den Typ in und weisen den Wert unserer Namensvariablen zu . Wenn wir keinen Setter finden , um einen neuen Wert für das Namensfeld festzulegen, können Sie die Set- Methode verwenden: get(Object)ObjectMyClassString

field.set(myClass, (String) "new value");
Glückwunsch! Sie haben gerade die Grundlagen der Reflexion gemeistert und ein privates Feld betreten! Achten Sie auf den try/catchBlock und die Arten der behandelten Ausnahmen. Die IDE teilt Ihnen automatisch mit, dass ihre Anwesenheit erforderlich ist, aber Sie können anhand ihrer Namen deutlich erkennen, warum sie hier sind. Weiter geht's! Wie Sie vielleicht bemerkt haben, MyClassverfügt unsere Klasse bereits über eine Methode zum Anzeigen von Informationen zu Klassendaten:

private void printData(){
       System.out.println(number + name);
   }
Aber auch hier hat dieser Programmierer seine Fingerabdrücke hinterlassen. Die Methode verfügt über einen privaten Zugriffsmodifikator und wir müssen unseren eigenen Code schreiben, um jedes Mal Daten anzuzeigen. Was für ein Chaos. Wo ist unser Spiegelbild geblieben? Schreiben Sie die folgende Funktion:

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
Die Vorgehensweise hier ist in etwa die gleiche wie beim Abrufen eines Feldes. Wir greifen per Namen auf die gewünschte Methode zu und gewähren Zugriff darauf. Und für das MethodObjekt rufen wir die invoke(Object, Args)Methode auf, wo Objectsich auch eine Instanz der MyClassKlasse befindet. Argssind die Argumente der Methode, obwohl unsere keine hat. Jetzt verwenden wir die printDataFunktion, um Informationen anzuzeigen:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// Output: 0new value
}
Hurra! Jetzt haben wir Zugriff auf die private Methode der Klasse. Aber was ist, wenn die Methode Argumente hat und warum der Konstruktor auskommentiert ist? Alles zu seiner Zeit. Aus der Definition am Anfang geht klar hervor, dass Sie mit Reflection zur Laufzeit (während das Programm läuft) Instanzen einer Klasse erstellen können! Wir können ein Objekt mit dem vollständigen Namen der Klasse erstellen. Der vollständige Name der Klasse ist der Klassenname, einschließlich des Pfads ihres Pakets .
Reflection-API: Reflection.  Die dunkle Seite von Java - 2
In meiner Pakethierarchie wäre der vollständige Name von MyClass „reflection.MyClass“. Es gibt auch eine einfache Möglichkeit, den Namen einer Klasse zu erfahren (den Namen der Klasse als String zurückgeben):

MyClass.class.getName()
Lassen Sie uns mithilfe der Java-Reflektion eine Instanz der Klasse erstellen:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass); // Output: created object reflection.MyClass@60e53b93
}
Beim Start einer Java-Anwendung werden nicht alle Klassen in die JVM geladen. Wenn Ihr Code nicht auf die MyClassKlasse verweist, ClassLoaderwird die Klasse, die für das Laden von Klassen in die JVM verantwortlich ist, niemals geladen. Das heißt, Sie müssen ClassLoaderdas Laden erzwingen und eine Klassenbeschreibung in Form einer ClassVariablen erhalten. Aus diesem Grund haben wir die forName(String)Methode, in der Stringder Name der Klasse steht, deren Beschreibung wir benötigen. Nachdem das СlassObjekt abgerufen wurde, wird durch den Aufruf der Methode newInstance()ein Objekt zurückgegeben, Objectdas mit dieser Beschreibung erstellt wurde. Jetzt müssen wir nur noch dieses Objekt an uns liefernMyClassKlasse. Cool! Das war schwierig, aber verständlich, hoffe ich. Jetzt können wir eine Instanz einer Klasse buchstäblich in einer Zeile erstellen! Leider funktioniert der beschriebene Ansatz nur mit dem Standardkonstruktor (ohne Parameter). Wie ruft man Methoden und Konstruktoren mit Parametern auf? Es ist Zeit, den Kommentar unseres Konstruktors zu entfernen. Wie erwartet newInstance()kann der Standardkonstruktor nicht gefunden werden und funktioniert nicht mehr. Schreiben wir die Klasseninstanziierung neu:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);// Output: created object reflection.MyClass@60e53b93
}
Die getConstructors()Methode sollte für die Klassendefinition aufgerufen werden, um Klassenkonstruktoren abzurufen, und dann getParameterTypes()sollte sie aufgerufen werden, um die Parameter eines Konstruktors abzurufen:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Dadurch erhalten wir alle Konstruktoren und ihre Parameter. In meinem Beispiel beziehe ich mich auf einen bestimmten Konstruktor mit bestimmten, zuvor bekannten Parametern. Und um diesen Konstruktor aufzurufen, verwenden wir die newInstanceMethode, an die wir die Werte dieser Parameter übergeben. Dasselbe gilt auch für den Aufruf invokevon Methoden. Dies wirft die Frage auf: Wann ist der Aufruf von Konstruktoren durch Reflektion sinnvoll? Wie bereits eingangs erwähnt, kommen moderne Java-Technologien nicht ohne die Java Reflection API aus. Zum Beispiel Dependency Injection (DI), das Annotationen mit der Reflexion von Methoden und Konstruktoren kombiniert, um das beliebte Darer zu bildenBibliothek für die Android-Entwicklung. Nachdem Sie diesen Artikel gelesen haben, können Sie davon ausgehen, dass Sie mit der Java Reflection API vertraut sind. Sie nennen Reflexion nicht umsonst die dunkle Seite von Java. Es bricht das OOP-Paradigma vollständig. In Java verbirgt und schränkt die Kapselung den Zugriff anderer auf bestimmte Programmkomponenten ein. Wenn wir den privaten Modifikator verwenden, soll auf dieses Feld nur innerhalb der Klasse zugegriffen werden, in der es vorhanden ist. Und nach diesem Prinzip bauen wir die spätere Architektur des Programms auf. In diesem Artikel haben wir gesehen, wie Sie mithilfe von Reflexion Ihren Weg überallhin erzwingen können. Das kreative Designmuster Singletonist ein gutes Beispiel hierfür als architektonische Lösung. Die Grundidee besteht darin, dass eine Klasse, die dieses Muster implementiert, während der Ausführung des gesamten Programms nur eine Instanz hat. Dies wird erreicht, indem der private Zugriffsmodifikator zum Standardkonstruktor hinzugefügt wird. Und es wäre sehr schlecht, wenn ein Programmierer Reflektion nutzen würde, um mehr Instanzen solcher Klassen zu erstellen. Übrigens habe ich kürzlich einen Kollegen eine sehr interessante Frage stellen hören: Kann eine Klasse, die das Singleton-Muster implementiert, vererbt werden? Könnte es sein, dass in diesem Fall sogar die Reflexion machtlos wäre? Hinterlassen Sie Ihr Feedback zum Artikel und Ihre Antwort in den Kommentaren unten und stellen Sie dort Ihre eigenen Fragen!

Mehr Lektüre:

Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION