Definition und Zweck
Reflection ist ein Mechanismus, der es einem Programm ermöglicht, seine Struktur und sein Verhalten zur Laufzeit zu untersuchen und zu ändern: Klassen, Felder, Methoden, Konstruktoren, Annotationen, Zugriffsmodifizierer usw.
Basispakete und Klassen: java.lang.Class, java.lang.reflect.Field/Method/Constructor, java.lang.reflect.Modifier, java.lang.reflect.Proxy, java.lang.invoke.MethodHandles/VarHandle.
Wann ist es notwendig:
- Frameworks / Container (Spring, JPA/Hibernate, Jackson) – Injektionen, Serialisierung, Proxys;
- Universelle Bibliotheken (Mapper, Validierer, DI/AOP);
- Tests / Mocking (JUnit, Mockito);
- Inspektion von Metadaten (Annotationen), Plugin-Systeme.
Wie es unter der Haube funktioniert (kurze Übersicht)
- Klassen werden von ClassLoader geladen → ihre Metadaten sind über das
Class<?>-Objekt verfügbar. - Über
Class<?>holen wir Fields/Methods/Constructors, Annotationen, Modifikatoren. - Mit
AccessibleObject#setAccessible(true)(Java 8) odercanAccess/modulare “opens” (Java 9+) können wir auf nicht-öffentliche Mitglieder zugreifen (mit Einschränkungen des modularen Systems). - Wir rufen
Method.invoke, lesen/schreibenField.get/set, erstellen ein Objekt überConstructor.newInstance. - Für dynamische Verhaltensänderungen – dynamische Proxys (
Proxy.newProxyInstance) oder CGLIB (in Spring), für schnellen Aufruf –MethodHandles/VarHandle.
Wo wird es angewendet (besonders in Spring)
Spring Framework
- IoC/DI: Suche nach Konstruktoren/Settern/Feldern, die mit Annotationen (
@Autowired,@Value) versehen sind, und Einkapselung von Abhängigkeiten über Reflection. - AOP/Protokolle: Erstellung von dynamischen Proxys:
- JDK Proxy für Interfaces;
- CGLIB für Klassen (erstellt Subclasses zur Laufzeit).
- Annotationenverarbeitung:
@Transactional,@Cacheable,@RequestMappingusw. – Lesen von Metadaten und Erstellung von Advice/Interceptor-Ketten. - Datenbindung/Validierung: Binden von Feldern aus HTTP-Anfragen an Beans (
WebDataBinder), Lesen von Beschränkungen@NotNull,@Size.
Andere Beispiele:
- Hibernate/JPA – Zugriff auf private Felder/Methoden, Mapping von Entitäten.
- Jackson/Gson – Serialisierung/Deserialisierung über Felder/Getter.
- Bean Validation (Jakarta Validation) – Suche nach Beschränkungsannotationen.
- JUnit/Mockito – Suche nach Testmethoden, Erstellung von Mock/Stubs.
Grundlegende Operationen – Codebeispiele
1. Class<?> erhalten
Class<?> c1 = String.class;
Class<?> c2 = Class.forName("com.example.User");
Object obj = new User();
Class<?> c3 = obj.getClass();2. Felder (Fields)
class User {
private String name;
public int age;
}
// Lesen/Schreiben des Feldes
User u = new User();
Field f = User.class.getDeclaredField("name");
f.setAccessible(true); // Java 8; in Java 9+ können modulare Beschränkungen bestehen
f.set(u, "Alice"); // privates Feld schreiben
String name = (String) f.get(u); // lesen3. Methoden (Methods)
class Calculator {
private int add(int a, int b) { return a + b; }
}
Calculator calc = new Calculator();
Method m = Calculator.class.getDeclaredMethod("add", int.class, int.class);
m.setAccessible(true);
int sum = (int) m.invoke(calc, 2, 3); // 54. Konstruktoren (Constructors)
class Person {
private final String name;
private Person(String name) { this.name = name; }
}
Constructor<Person> ctor = Person.class.getDeclaredConstructor(String.class);
ctor.setAccessible(true);
Person p = ctor.newInstance("Bob");5. Annotationen
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Column { String name(); }
class Entity {
@Column(name = "user_name")
private String username;
}
Field f = Entity.class.getDeclaredField("username");
Column col = f.getAnnotation(Column.class);
if (col != null) {
System.out.println(col.name()); // "user_name"
}6. Arbeiten mit Typen und generischen Metadaten
class Repo<T> {
List<T> items;
}
Field items = Repo.class.getDeclaredField("items");
Type gType = items.getGenericType(); // java.util.List<T>
if (gType instanceof ParameterizedType pt) {
Type arg = pt.getActualTypeArguments()[0]; // T (erfunden zur Laufzeit, aber als TypeVariable sichtbar)
}7. Arrays über java.lang.reflect.Array
Object arr = Array.newInstance(String.class, 3);
Array.set(arr, 0, "a");
String first = (String) Array.get(arr, 0);8. Dynamischer Proxy (JDK Proxy)
interface Greeting {
String hello(String name);
}
class LoggingHandler implements InvocationHandler {
private final Object target;
LoggingHandler(Object target) { this.target = target; }
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(">>> " + method.getName());
return method.invoke(target, args);
}
}
Greeting original = name -> "Hi " + name;
Greeting proxy = (Greeting) Proxy.newProxyInstance(
Greeting.class.getClassLoader(),
new Class[]{Greeting.class},
new LoggingHandler(original)
);
System.out.println(proxy.hello("Alice")); // log + "Hi Alice"9. Schneller Aufruf über MethodHandles/VarHandle
class A { private int x = 42; }
A a = new A();
MethodHandles.Lookup lookup = MethodHandles.lookup();
VarHandle vh = MethodHandles.privateLookupIn(A.class, lookup)
.findVarHandle(A.class, "x", int.class);
int v = (int) vh.get(a); // 42
vh.set(a, 100); // schneller als Field.set im heißen CodeModulares System (Java 9+) und Zugänglichkeit
- In der Ära von
--add-exports/--add-openskann der Zugriff auf nicht-öffentliche Mitglieder verboten werden, wenn das Modul keinen Paket öffnet. - In Spring Boot wird oft die JVM-Option verwendet:
--add-opens java.base/java.lang=ALL-UNNAMED(oder spezifische Module), um Reflection mit privaten Mitgliedern zu ermöglichen. - In der Produktion ist es besser, “opens” zu minimieren und Reflection auf private Mitglieder.
Warum man vorsichtig sein sollte
1. Verletzung der Einkapselung
setAccessible(true)/privateLookupIn ermöglichen das Ändern privater Felder/Methoden.
Dies bricht den Vertrag der Klasse, erschwert die Wartung und kann zu brüchigem Code führen.
2. Sicherheit
- Zugriff auf private Daten, Umgehung von Invarianten, mögliche Datenlecks.
- Bei Laufzeit (insbesondere in Plugin-Systemen) kann bösartiger Code eingefangen werden.
3. Leistung
- Reflection ist teurer als direkter Aufruf; häufige
Method.invoke/Field.getin heißen Schleifen → Leistungsverschlechterung. - Dynamische Proxys/Klassengenerierung (CGLIB) → Zeit und Speicher, JIT-Aufwärmen.
4. Kompatibilität und Brüchigkeit
- Umbenennung/Refactoring von Feldern/Methoden bricht Code, der sich über String-Namen auf sie bezieht.
- Modulare Beschränkungen von Java 9+ (und verschiedenen JDKs) können plötzlich den Zugriff blockieren.
5. Typenänderung
- Generische Typen werden entfernt → Reflection kennt den konkreten Typ-Parameter zur Laufzeit nicht (sieht
Listanstelle vonList<String>), es sind Tricks mitType/ParameterizedTypeerforderlich.
Typische Fehler und wie man sie behebt
| Fehler | Ursache | Lösung |
|---|---|---|
NoSuchFieldException / NoSuchMethodException | Falsche Namen, Signaturen, Modifikatoren | Überprüfe die genauen Typparameter, verwende getDeclared* für nicht-öffentliche Felder/Methoden |
IllegalAccessException | Kein Zugriff (Modifikator/Modul) | setAccessible(true) (Java 8) oder privateLookupIn + --add-opens (Java 9+) |
InvocationTargetException | Ausnahme innerhalb der aufgerufenen Methode | Logge getCause(), wickle es in eine Domain-Exception ein |
ClassNotFoundException | Klasse nicht vom angegebenen ClassLoader gefunden | Überprüfe den Namen/CL, verwende TCCL (Thread Context ClassLoader) in Web-Containern |
| Speicherlecks | Langzeitreferenzen auf ClassLoader/Proxys | Bereinige Caches, halte CL nicht statisch, schließe Plugins korrekt |
| Leistungsverlust | Häufige reflektierte Aufrufe | Cache-Deskriptoren, verwende MethodHandle/VarHandle oder direkten Aufruf |
| Brüchiger Code beim Refactoring | String-Namen | Zentralisiere Konstanten für Namen, minimiere Reflexionspunkte |
Best Practices (kurze Checkliste)
- Zuerst der normale Code. Reflection ist nur dann notwendig, wenn es keine anderen Möglichkeiten gibt (universelle Frameworks, cross-typische Tools).
- Beschränke den Anwendungsbereich. Wickle es in kleine Utilities/Adapter ein, verteile es nicht im gesamten Code.
- Cache
Field/Method/Constructor. Suche sie nicht jedes Mal neu. - Bevorzuge
MethodHandles/VarHandlean heißen Stellen – sie sind nach dem Aufwärmen schneller. - Berücksichtige Module. Verwende
--add-opens/--add-exports, vermeide den Zugriff auf private Mitglieder, gestalte die API so, dass kein Zugriff erforderlich ist. - Verletze keine Invarianz. Wenn du ein privates Feld änderst, dokumentiere und teste die Invarianz.
- Sicherheit. Überprüfe die Quelle von Klassen/Plugins, gib Reflection nicht frei, um im gesamten ClassLoader herumzulaufen.
- Logge und wickle es ein. Alle reflektierten Ausnahmen – mit Kontext (welche Klasse/Feld/Methode), wickle sie in Domain-Exceptions ein.
- Für Annotationen – erstelle eine Metadaten-Schicht, lies Annotationen nicht überall.
- In Spring – stütze dich nach Möglichkeit auf standardmäßige Mechanismen (DI, Converter, Bean-PostProcessor), und verwende Reflection nur an Erweiterungspunkten.
Mini-FAQ für Vorstellungsgespräche
- Was ist gefährlich an Reflection? Verletzung der Einkapselung, kann den Vertrag brechen, verschlechtert die Leistung und Sicherheit; plus Probleme mit Modulen.
- Wann sollte man es verwenden? Frameworks/universelle Bibliotheken, dynamische Plugins, Serialisierung/Mapping, DI/AOP.
- JDK Proxy vs CGLIB? JDK Proxy – nur für Interfaces; CGLIB – proxziert Klassen (durch Vererbung), sei vorsichtig mit
final. Spring wählt selbst. - MethodHandles/VarHandle? Moderner und schneller, als Reflection über
Method/Field, besonders an heißen Stellen; aber komplexere API. - Wie geht man mit Java 9+ Modulen um? Verwende
--add-opens/--add-exports, vermeide den Zugriff auf private Mitglieder, gestalte die API so, dass kein Zugriff erforderlich ist.