JavaRush /Курси /JAVA 25 SELF /Рефлексія та Dependency Injection

Рефлексія та Dependency Injection

JAVA 25 SELF
Рівень 62 , Лекція 4
Відкрита

1. Серіалізація за допомогою рефлексії

Ви вже знаєте, що серіалізація — це процес перетворення об’єкта на потік байтів або текстове подання. У Java є стандартна серіалізація (Serializable), але частіше застосовують формати JSON/XML через бібліотеки на кшталт Jackson і Gson.

Чому тут потрібна рефлексія?
Щоб серіалізувати об’єкт, потрібно дізнатися його поля та їхні значення. Поля можуть бути private, і звичайний код до них не дістанеться — зате рефлексія зможе. Тому JSON‑бібліотеки динамічно обходять структуру об’єкта, зчитують і записують поля через Field і Method.

Приклад: проста серіалізація об’єкта в рядок

Клас даних:

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;
    }
}

Найпростіший серіалізатор на базі рефлексії:

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); // Відкриваємо приватні поля
            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();
    }
}

Використання:

Person p = new Person("Alice", 30, true);
System.out.println(SimpleSerializer.serialize(p));
Person{name=Alice, age=30, active=true}

Як це працює?

  • Отримуємо оголошені поля через getDeclaredFields().
  • Робимо їх доступними через setAccessible(true).
  • Зчитуємо імена та значення полів і формуємо рядок.

Обмеження прикладу: немає оброблення вкладених об’єктів, колекцій, масивів і циклічних посилань — це лише демонстрація принципу.

Як це роблять повноцінні бібліотеки?

Jackson/Gson уміють працювати з вкладеними об’єктами й колекціями, враховують анотації (@JsonIgnore, @SerializedName), формати дат і багато іншого — усе це на базі рефлексії.

Приклад із Jackson:

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) та рефлексія

Dependency Injection — шаблон проєктування, за якого залежності «впроваджуються» ззовні, а не створюються всередині класу. Це робить код гнучким, тестованим і розширюваним. У Java це забезпечують фреймворки Spring, Guice, Dagger, позначаючи точки впровадження анотаціями на кшталт @Autowired, @Inject.

Чому тут потрібна рефлексія?
DI‑контейнеру потрібно знаходити поля/конструктори, читати анотації й створювати екземпляри під час виконання — це робиться через Class/Constructor/Field API.

Приклад: міні‑DI на рефлексії

Анотація для впровадження:

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}

Класи із залежністю:

public class Service {
    public void doWork() {
        System.out.println("Service is working!");
    }
}

public class Client {
    @Inject
    private Service service;

    public void useService() {
        service.doWork();
    }
}

Міні‑контейнер:

import java.lang.reflect.*;

public class MiniDIContainer {
    // Створює об’єкт за класом і впроваджує залежності в поля з @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()); // рекурсивно
                field.setAccessible(true);
                field.set(obj, dependency);
            }
        }
        return obj;
    }
}

Використання:

public class Main {
    public static void main(String[] args) throws Exception {
        Client client = (Client) MiniDIContainer.createObject(Client.class);
        client.useService(); // Service is working!
    }
}

Як це працює? Контейнер шукає поля з @Inject, створює залежності за їхнім типом і через рефлексію встановлює їх у приватні поля.

Важливо: це спрощена схема. Реальні DI‑контейнери підтримують скоупи, синглтони, конфігурацію, проксі, оброблення циклічних залежностей тощо.

3. Динамічні проксі

Проксі — «підставний» об’єкт, який перехоплює виклики й додає поведінку: логування, безпека, транзакції тощо. У Java це робить java.lang.reflect.Proxy разом із InvocationHandler. Основа багатьох можливостей Spring AOP, моки в Mockito тощо.

Приклад: проксі з логуванням

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("Виклик методу: " + method.getName());
                    return method.invoke(target, args);
                }
            }
        );
    }
}

Використання:

HelloService original = new HelloServiceImpl();
HelloService proxy = LoggingProxy.createProxy(original, HelloService.class);

proxy.sayHello("World");
Виклик методу: sayHello
Hello, World!

Як це працює? Proxy.newProxyInstance створює об’єкт, що реалізує інтерфейс, а всі виклики методів ідуть до InvocationHandler.invoke, де ви можете виконати будь‑який «наскрізний» код до/після делегування.

4. Реальні сценарії та обмеження

Де рефлексія використовується «в бою»?

  • JUnit — знаходить методи з @Test і викликає їх через рефлексію.
  • Spring — створює біни, впроваджує залежності, сканує анотації, генерує проксі.
  • Jackson/Gson — серіалізують і десеріалізують, читаючи навіть приватні поля.
  • Hibernate — будує ORM‑моделі на основі структури класів, керує полями та проксі‑об’єктами.
  • Mockito — створює моки й перехоплює виклики через проксі.

Чому не завжди варто використовувати рефлексію?

  • Продуктивність. Загалом повільніша за звичайні виклики (це пом’якшується кешуванням і генерацією байт‑коду).
  • Безпека. Порушує інкапсуляцію; надає доступ до приватних даних.
  • Модульність Java 9+. Може з’явитися InaccessibleObjectException без явного відкриття пакетів/модулів.

5. Типові помилки

Помилка № 1: Ігнорування перевірюваних (checked) винятків. Методи рефлексії кидають NoSuchFieldException, IllegalAccessException, InvocationTargetException тощо. Обробляйте їх або загортайте у власні винятки.

Помилка № 2: setAccessible(true) працює не завжди. На Java 9+ у модульних застосунках можливий InaccessibleObjectException. Потрібні параметри JVM/модуля (--add-opens) або публічні API.

Помилка № 3: Циклічні залежності в DI. За наївної рекурсії (A залежить від B, а B від A) можете отримати StackOverflowError. Реальні контейнери відстежують граф залежностей і розв’язують цикли спеціальними техніками.

Помилка № 4: Неповна серіалізація об’єктів. Поля‑посилання серіалізуються як ClassName@hash, якщо немає рекурсивного обходу. Для коректної серіалізації потрібні оброблення вкладених об’єктів і колекцій та захист від циклів.

Помилка № 5: Втрата продуктивності. Часті рефлексивні операції (у циклах, на гарячих шляхах) стають вузьким місцем. Використовуйте кешування Field/Method, генерацію байт‑коду, MethodHandle або оброблення анотацій.

Помилка № 6: Порушення інкапсуляції. Зміна приватних полів через рефлексію призводить до крихкості та важковиявлюваних багів. Надавайте перевагу публічним контрактам.

1
Опитування
Рефлексія, рівень 62, лекція 4
Недоступний
Рефлексія
Рефлексія і динамічні можливості
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ