CodeGym /Kursy /JAVA 25 SELF /Refleksja i Dependency Injection

Refleksja i Dependency Injection

JAVA 25 SELF
Poziom 62, Lekcja 4
Dostępny

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.

1
Zadanie
JAVA 25 SELF, poziom 62, lekcja 4
Niedostępne
Automatyczny montaż robotów z inteligentnymi komponentami 🤖
Automatyczny montaż robotów z inteligentnymi komponentami 🤖
1
Zadanie
JAVA 25 SELF, poziom 62, lekcja 4
Niedostępne
Szpiegowskie przechwytywanie wiadomości w jednostce odszyfrowującej 🕵️
Szpiegowskie przechwytywanie wiadomości w jednostce odszyfrowującej 🕵️
1
Ankieta/quiz
Refleksja, poziom 62, lekcja 4
Niedostępny
Refleksja
Refleksja i możliwości dynamiczne
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION