1. Serializacja za pomocą refleksji
Już wiesz, że serializacja — to proces przekształcania obiektu w strumień bajtów lub reprezentację tekstową. W Javie istnieje standardowa serializacja (Serializable), ale częściej używa się formatów JSON/XML za pośrednictwem bibliotek takich jak Jackson i Gson.
Dlaczego tutaj potrzebna jest refleksja?
Aby zserializować obiekt, trzeba poznać jego pola i ich wartości. Pola mogą być private, i zwykły kod się do nich nie dostanie — natomiast refleksja może. Dlatego biblioteki JSON dynamicznie przechodzą po strukturze obiektu, odczytują/zapisują pola przez Field i Method.
Przykład: prosta serializacja obiektu do ciągu znaków
Klasa danych:
public class Person {
private String name;
private int age;
private boolean active;
public Person(String name, int age, boolean active) {
this.name = name;
this.age = age;
this.active = active;
}
}
Najprostszy serializator oparty na refleksji:
import java.lang.reflect.Field;
public class SimpleSerializer {
public static String serialize(Object obj) {
StringBuilder sb = new StringBuilder();
Class<?> clazz = obj.getClass();
sb.append(clazz.getSimpleName()).append("{");
Field[] fields = clazz.getDeclaredFields();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
field.setAccessible(true); // Udostępniamy pola prywatne
try {
sb.append(field.getName()).append("=")
.append(field.get(obj));
} catch (IllegalAccessException e) {
sb.append(field.getName()).append("=<?>");
}
if (i < fields.length - 1) sb.append(", ");
}
sb.append("}");
return sb.toString();
}
}
Użycie:
Person p = new Person("Alice", 30, true);
System.out.println(SimpleSerializer.serialize(p));
Person{name=Alice, age=30, active=true}
Jak to działa?
- Pobieramy zadeklarowane pola przez getDeclaredFields().
- Umożliwiamy dostęp poprzez setAccessible(true).
- Odczytujemy nazwy i wartości pól i składamy ciąg.
Ograniczenia przykładu: brak obsługi zagnieżdżonych obiektów, kolekcji, tablic i referencji cyklicznych — to jedynie demonstracja zasady.
Jak robią to pełnoprawne biblioteki?
Jackson/Gson potrafią pracować z zagnieżdżonymi obiektami i kolekcjami, uwzględniają adnotacje (@JsonIgnore, @SerializedName), formaty dat i wiele więcej — wszystko to na bazie refleksji.
Przykład z Jacksonem:
import com.fasterxml.jackson.databind.ObjectMapper;
Person p = new Person("Bob", 25, false);
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(p);
// {"name":"Bob","age":25,"active":false}
2. Dependency Injection (DI) i refleksja
Dependency Injection — wzorzec, w którym zależności są „wstrzykiwane” z zewnątrz, a nie tworzone wewnątrz klasy. To czyni kod elastycznym, testowalnym i rozszerzalnym. W Javie robią to frameworki Spring, Guice, Dagger, oznaczając miejsca wstrzyknięcia adnotacjami takimi jak @Autowired, @Inject.
Dlaczego tutaj potrzebna jest refleksja?
Kontener DI musi znajdować pola/konstruktory, odczytywać adnotacje i tworzyć instancje w czasie wykonywania — robi się to poprzez Class/Constructor/Field API.
Przykład: mini-DI oparte na refleksji
Adnotacja wstrzyknięcia:
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}
Klasy z zależnością:
public class Service {
public void doWork() {
System.out.println("Service is working!");
}
}
public class Client {
@Inject
private Service service;
public void useService() {
service.doWork();
}
}
Mini-kontener:
import java.lang.reflect.*;
public class MiniDIContainer {
// Tworzy obiekt na podstawie klasy i wstrzykuje zależności do pól z @Inject
public static Object createObject(Class<?> clazz) throws Exception {
Object obj = clazz.getDeclaredConstructor().newInstance();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
Object dependency = createObject(field.getType()); // rekurencyjnie
field.setAccessible(true);
field.set(obj, dependency);
}
}
return obj;
}
}
Użycie:
public class Main {
public static void main(String[] args) throws Exception {
Client client = (Client) MiniDIContainer.createObject(Client.class);
client.useService(); // Service is working!
}
}
Jak to działa? Kontener wyszukuje pola z @Inject, tworzy zależności na podstawie ich typu i poprzez refleksję ustawia je w polach prywatnych.
Ważne: to uproszczony schemat. Rzeczywiste kontenery DI obsługują zakresy, singletony, konfigurację, proxy, obsługę cyklicznych zależności itd.
3. Dynamiczne proxy
Proxy — „podstawiony” obiekt, który przechwytuje wywołania i dodaje zachowanie: logowanie, bezpieczeństwo, transakcje itp. W Javie realizuje to java.lang.reflect.Proxy wraz z InvocationHandler. To podstawa wielu możliwości Spring AOP, mocków w Mockito itd.
Przykład: logujące proxy
public interface HelloService {
void sayHello(String name);
}
public class HelloServiceImpl implements HelloService {
public void sayHello(String name) {
System.out.println("Hello, " + name + "!");
}
}
import java.lang.reflect.*;
public class LoggingProxy {
@SuppressWarnings("unchecked")
public static <T> T createProxy(T target, Class<T> iface) {
return (T) Proxy.newProxyInstance(
iface.getClassLoader(),
new Class<?>[]{iface},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Wywołanie metody: " + method.getName());
return method.invoke(target, args);
}
}
);
}
}
Użycie:
HelloService original = new HelloServiceImpl();
HelloService proxy = LoggingProxy.createProxy(original, HelloService.class);
proxy.sayHello("World");
Wywołanie metody: sayHello
Hello, World!
Jak to działa? Proxy.newProxyInstance tworzy obiekt implementujący interfejs, a wszystkie wywołania metod trafiają do InvocationHandler.invoke, gdzie możesz wykonać dowolny „przekrojowy” kod przed/po delegowaniu.
4. Rzeczywiste scenariusze i ograniczenia
Gdzie refleksja jest używana „w boju”?
- JUnit — znajduje metody z @Test i wywołuje je przez refleksję.
- Spring — tworzy beany, wstrzykuje zależności, skanuje adnotacje, generuje proxy.
- Jackson/Gson — serializują/deserializują, odczytując nawet pola prywatne.
- Hibernate — buduje modele ORM na podstawie struktury klas, zarządza polami i obiektami proxy.
- Mockito — tworzy mocki i przechwytuje wywołania przez proxy.
Dlaczego nie zawsze warto używać refleksji?
- Wydajność. Z reguły wolniejsza niż zwykłe wywołania (łagodzona przez cache’owanie/generowanie bytecode’u).
- Bezpieczeństwo. Narusza enkapsulację; dostęp do danych prywatnych.
- Modułowość Java 9+. Może pojawić się InaccessibleObjectException bez jawnego otwarcia pakietów/modułów.
5. Typowe błędy
Błąd nr 1: Ignorowanie wyjątków typu checked. Metody refleksji zgłaszają NoSuchFieldException, IllegalAccessException, InvocationTargetException itp. Obsługuj je lub opakowuj we własne wyjątki.
Błąd nr 2: setAccessible(true) nie zawsze działa. W Javie 9+ w aplikacjach modułowych możliwy jest InaccessibleObjectException. Potrzebne są parametry JVM/modułu (--add-opens) lub publiczne API.
Błąd nr 3: Cykliczne zależności w DI. Przy naiwnej rekursji (A zależy od B, a B od A) dostaniesz StackOverflowError. Rzeczywiste kontenery śledzą graf zależności i rozwiązują cykle specjalnymi technikami.
Błąd nr 4: Niepełna serializacja obiektów. Pola-referencje serializują się jako ClassName@hash, jeśli nie ma rekursywnego przechodzenia. Do poprawnej serializacji potrzebna jest obsługa zagnieżdżonych obiektów/kolekcji oraz ochrona przed cyklami.
Błąd nr 5: Utrata wydajności. Częste operacje refleksyjne (w pętlach, na gorących ścieżkach) stają się wąskim gardłem. Używaj cache’owania Field/Method, generowania bytecode’u, MethodHandle albo przetwarzania adnotacji.
Błąd nr 6: Naruszenie enkapsulacji. Zmiana pól prywatnych przez refleksję prowadzi do kruchego kodu i trudnych do wyłapania błędów. Staraj się preferować publiczne kontrakty.
GO TO FULL VERSION