Einführung in Reflection

Die Java Reflection API ist Bestandteil des JDK und ermöglicht den Zugriff auf Methoden, Felder und Annotationen von Klassen und Objekte, also den Instanzen von Klassen. Mit Ausnahme von Annotationen geschieht der Zugriff immer auf einer Meta-Ebene.

Mit einem einfachen Beispiel ist diese Meta-Ebene erklärt.

class Foo {
  private String bar = "private value";
}

Stellen wir uns vor, wir wollen ganz exemplarisch von dem Objekt foo (foo = new Foo())den Inhalt von dem Feld bar erhalten. Auf dem üblichen Weg ist das nicht möglich, da das Feld ein privates ist.

Über den zugehörigen Klassentyp (via getClass()) stellt Java eine Reihe von Methoden aus dem Reflection-Paket zur Verfügung, u.a. auch Field getDeclaredField(String). Ein declared field ist ein Instanzattribut eines Objektes, während ein field (Field getField(String)) statische Felder finden.

Field field = foo.getClass().getDeclaredField('bar');

Die Variable field enthält aber – wie man dem Typ entnehmen kann – aber nicht den Inhalt von bar, sondern ein Field-Objekt. Um an den Inhalt zu kommen, verwendet man die Methode get(Object). Der Parameter bezeichnet den entsprechenden Kontext, also das Objekt foo von weiter oben.

Object value = field.get(foo);

Das bedeutet im Klartext: Auch für eine weitere Instanz der Klasse Foo (etwa foo2) kann dieses Field für dieses Feld verwendet werden. Es beinhaltet nur die Metainformationen, nicht die eigentlichen Inhalte.

Dabei ist jedoch zu beachten, dass der SecurityManager aktiv werden kann; in diesem Falle wird eine Exception bei get(Object) geschmissen, weil das Feld nicht sichtbar ist.

field.setAccessible(true);

schafft dabei Abhilfe.

Und Methoden?

Im Prinzip funktioniert das Schema bei Methoden genauso – aber bevor ich jetzt einen Radio Eriwan-Witz loslasse…

Im Gegensatz zu den vorherigen Feldern werden Methoden nicht nur über den Namen, sondern auch über die Parametertypen identifiziert (daher können Methoden sich vom Namen her in Java auch “überladen”).

class Bar {
  private String name;

  String getName() {
    return name;
  }

  String getName(String defaultName) {
    return (name == null) ? defaultName : name;
  }
}

Die Metainformationen werden entsprechenderweise geladen:

Method method1 = bar.getClass().getDeclaredMethod("getName");
Method method2 = bar.getClass().getDeclaredMethod("getName", String.class);

Selbstverständlich ist bei Kenntnisnahme der Klasse auch folgende Zeile gleichwertend:

Method method3 = Bar.class.getDeclaredMethod("getName");

Methoden haben im Gegensatz zu Feldern keinen “Inhalt”, sondern sind eine Aktivität oder Operation – und zwar auf einem Objekt. Dementsprechend funktioniert das Ausführen mittels der Method Object invoke(Object, Object…args):

Object result1 = method1.invoke(bar); // getName()
Object result2 = method2.invoke(bar, "default name"); // getName("defaultName")

Und Annotationen?

Annotationen sind “Anmerkungen” im Quellcode, und seit Java 5 fester Bestandteil des JDKs. Technisch gesehen sind Annotationen besondere Klassen mit eingeschränkten (Java-) Möglichkeiten, deren Instanz auf einem Meta-Attribut wie Klasse, Feld, Methode oder Methodenparameter “hängen”. Mit anderen Worten bedeutet dass, dass man mittels Reflection tatsächlich eine Instanz einer Annotation erhält – allerdings mittels einem Proxy (aus der Java API).

@Entity
class Account {
  @Column(name="id_x")
  private Long id;
}

Die Methode getAnnotation(Class) existiert in allen Typen: Class, Field und Method.

Account account = new Account();

Entity annotation1 = Account.class.getAnnotation(Entity.class)
                                  .annotationType();
Column annotation2 = Account.class.getDeclaredField("id")
                                  .getAnnotation(Column.class)
                                  .annotationType();
Assert.assertEquals("id_x", annotation2.name());

Performance

Im Regelfall benötigt man keinen Zugriff auf Reflection und sollte es vermeiden. Nichtsdestotrotz gestalten sich Konfigurationen, welche auf Basis von Annotationen, meist als sehr einfach, simpel und vor allem Code-konzentriert. Das JPA-Mapping (etwa mit Hilfe von Hibernate) gestaltet sich via Annotationen wesentlich einfacher, schneller und schlanker als mit XML.

Jeder Zugriff über die Reflection API kostet Zeit. Für Konfigurationen und Pläne ist das meist nebensächlich; die JPA-Konfiguration wird beim Start der Applikation eingelesen, analysiert und gespeichert. Nebenbei profitiert das natürlich von der anfänglichen “Warmup-Phase”.

Besteht jedoch ein Access on Demand, so sollte man sich Gedanken um eine geeignete Cachestruktur machen. Wichtig ist dabei, die Metadaten von den eigentlichen Inhalten zu trennen.

Cachen & Lösungsansätze

Jeder der oben genannten Reflection-Getter gibt es auch jeweils eine getAll-Variante: Class.getDeclaredFields(),Class.getDeclaredMethods(), Class.getAnnotations(), Field.getAnnotations() und Method.getAnnotations().

Mit einem einfachen Algorithmus kann man die Laufzeit drastisch senken, wenn Objekte des gleichen Typs mittels Reflection untersucht werden.

public class CacheReflectionUtil {

  // cache of declared fields of class types
  private final Map<Class<?>, Field[]> classDeclaredFields = new HashMap<>();

  // Return the declared fields of the given class type.
  public Field[] getDeclaredFields(Class<?> type) {
    Field[] fields = classDeclaredFields.get(type);

    if (fields == null) {
      fields = type.getDeclaredFields();
      classDeclaredFields.put(type, fields);
      ensureAccessibility(fields);
    }

    return fields;
  }

  // Ensure that the given fields are accessible.
  public void ensureAccessibility(Field[] fields) {
    for (Field field : fields) {
      // setAccessible will not only set a property but invoke SecurityManager stuff
      if (field.isAccessible()) {
        field.setAccessible(true);
      }
    }
  }

  // Return the declared field of the given class type.
  public Field getDeclaredField(Class<?> type, String name) {
    for (Field field : getDeclaredFields(type)) {
      if (field.getName().equals(name)) {
        return field;
      }
    }
    return null;
  }
}

Um einen stetigen Speicherverbrauch zu verhindern, empfiehlt sich das Nutzen einer Least-Recently-Used-Struktur. Das Apache-Commons-Paket bietet dies etwa mit der LRUMap an.