JavaRush /Курси /Spring Core /Інспекція proxy-bean у Spring-контексті

Інспекція proxy-bean у Spring-контексті

Spring Core
Рівень 21 , Лекція 4
Відкрита

1. Інспекція bean у ApplicationContext

Якщо ви вперше стикаєтеся з тим, що Spring «повернув не той об’єкт», це справді схоже на цирковий фокус: ви очікували побачити ReportingService, а в дебагері раптом якийсь jdk.proxy2.$Proxy42. У цьому місці важливо не нервувати: контейнер не знущається, він просто робить те, що ми самі попросили — явно або неявно — через свої механізми розширення.

Тут корисно влаштувати невеликий «рентген» контейнера й подивитися, як proxy з’являється всередині ApplicationContext.

Ключова ідея така: Spring зберігає й видає не «об’єкт, який ви створили», а «об’єкт, що вийшов після всієї контейнерної обробки». Саме тому інспекція — корисна навичка: коли поведінка дивна, перше, що хочеться зрозуміти, — хто насправді стоїть між вами й логікою?

Для візуалізації корисно тримати в голові такий ланцюг:

flowchart TD
  A["Bean створюється контейнером"] --> B["BeanPostProcessor(и)"]
  B --> C["Підсумковий bean instance у контексті (може бути proxy)"]
  C --> D["Цей instance отримують інші beans і ваш код"]

BeanPostProcessor вам уже знайомий як частина конвеєра запуску. Тут він потрібен як легальний гачок, на якому контейнер може замінити вихідний об’єкт обгорткою.

2. Контракт для проксі: інтерфейс

Щоб JDK dynamic proxy взагалі міг існувати, йому потрібен інтерфейс, тому що він уміє «вдавати» інтерфейс, але не вміє «ставати» конкретним класом. Тому почнемо з дуже малого, акуратного кроку: виділимо контракт для сервісу звітів. Навіть якщо у вас уже є ReportingService, додати інтерфейс — це нормальна еволюція, а не «інтерфейс заради інтерфейсу» (тут у нас є прикладна мета: proxy-модель).

Створимо інтерфейс ReportOperations (пакети — як у нашому проєкті ContextFlow):

package com.example.contextflow.application.reporting;

public interface ReportOperations {
    // Код, що викликає, залежить від інтерфейсу, а не від конкретної реалізації
    String generate();
}

Тепер зробимо target object — звичайну реалізацію. Тут навмисно немає жодної магії: просто метод, що повертає рядок.

package com.example.contextflow.application.reporting;

public class ReportingService implements ReportOperations {
    @Override
    public String generate() {
        // Проста реалізація, щоб у прикладі було видно: логіка живе в target-об’єкті
        return "daily-report";
    }
}

Зверніть увагу на тонкий, але важливий сенс: контракт для коду, що викликає, — ReportOperations, а ReportingService — лише конкретна реалізація. Якщо завтра ReportingService почне проксуватися, код, що викликає, не має від цього страждати. Він і так працює через інтерфейс.

Тепер нам потрібен максимально прозорий спосіб побачити саму підміну bean instance. Тому підемо через BeanPostProcessor: він уже стоїть на шляху контейнерної обробки й саме показує момент, коли вихідний bean перестає бути тим об’єктом, який контейнер віддає назовні. Це не побутовий рецепт «робити proxy руками в Spring», а навчальний рентген тієї самої моделі підміни, на якій будується й AOP-інфраструктура.

3. Proxy через BeanPostProcessor

Зараз буде момент «ага!». Ми напишемо свій BeanPostProcessor, який після ініціалізації конкретного bean-а поверне не вихідний об’єкт, а JDK proxy навколо нього. Це буде навчальний, максимально прозорий приклад: proxy просто надрукує службовий вивід і заміряє час виконання. Бізнес-логіка при цьому залишиться в target object.

Скелет BeanPostProcessor виглядає так: ми фільтруємо лише потрібний bean за іменем і типом, а решту не чіпаємо.

package com.example.contextflow.support.postprocessor;

import com.example.contextflow.application.reporting.ReportOperations;
import org.springframework.beans.factory.config.BeanPostProcessor;

import java.lang.reflect.Proxy;

public class ProxyWrappingBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        // Важливо: BPP бачить *усі* beans, тому фільтрація обов’язкова
        if (!beanName.equals("reportingService") || !(bean instanceof ReportOperations target)) {
            return bean; // Усі інші beans не чіпаємо
        }

        // Повертаємо proxy замість вихідного об’єкта (target залишається жити "всередині")
        return Proxy.newProxyInstance(
                ReportOperations.class.getClassLoader(),
                new Class
  []{ReportOperations.class},
                (proxy, method, args) -> {
                    // Технічна обв’язка навколо виклику: вимірюємо час
                    long started = System.nanoTime();
                    try {
                        // Важливо: реальну логіку виконує target
                        return method.invoke(target, args);
                    } finally {
                        // Логуємо "ззовні" — це поведінка proxy, а не target
                        System.out.println("Час (ns): " + (System.nanoTime() - started));
                    }
                }
        );
    }
}

Якщо вас бентежить слово invoke і рефлексія — це нормально. Тут не потрібно ставати експертом із рефлексії. Нам важлива ідея: є об’єкт-перехоплювач, який вирішує, що зробити навколо виклику. Spring AOP робить те саме, тільки акуратніше й у масштабі.

І ще одна важлива деталь. Ми використовуємо метод postProcessAfterInitialization, бо хочемо показати: bean уже створений, залежності вже впроваджено, init-логіка вже відпрацювала, і лише потім контейнер каже: «а тепер я тебе загорну». Це добре вкладається в mental model життєвого циклу.

4. Підключаємо демо в конфігурації Spring

Тут нам важливіша прозорість, ніж «красивість» конфігурації, тому зробимо окремий маленький конфіг для інспекції proxy. Так простіше побачити сам факт підміни bean instance.

Ось конфігурація, яка реєструє і target bean, і наш BeanPostProcessor:

package com.example.contextflow.config.core;

import com.example.contextflow.application.reporting.ReportOperations;
import com.example.contextflow.application.reporting.ReportingService;
import com.example.contextflow.support.postprocessor.ProxyWrappingBeanPostProcessor;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ProxyInspectionConfig {

    @Bean
    ReportOperations reportingService() {
        // Повертаємо контракт (інтерфейс), а не реалізацію — так proxy-модель природніша
        return new ReportingService();
    }

    @Bean
    static BeanPostProcessor proxyWrappingBeanPostProcessor() {
        // Реєструємо BPP: саме він підмінить bean на proxy після ініціалізації
        return new ProxyWrappingBeanPostProcessor();
    }
}

BeanPostProcessor тут оголошений як static @Bean: для ранньої інфраструктури це добре поєднується з уже знайомою логікою конвеєра запуску й не розмиває межу між звичайними beans і спеціальними контейнерними механізмами.

Тут є корисний прийом: @Bean повертає ReportOperations, а не ReportingService. Ми заздалегідь фіксуємо опорний тип — інтерфейс. Якщо в проєкті у вас компонентне сканування і @Service, то принцип лишається тим самим: впроваджуємо залежності за контрактом, а не за конкретним класом, коли це розумно.

5. Перевіряємо runtime-тип bean із контексту

Тут нам потрібні ті самі базові перевірки, що й для будь-якого proxy: реальний runtime-клас, інтерфейсний контракт і один швидкий тест на JDK proxy. Не треба гадати за іменем bean-а — треба подивитися, що контейнер справді віддав.

Зробимо маленький застосунок для запуску. Це може бути окремий main(), не обов’язково переписувати основний запуск ContextFlow.

package com.example.contextflow;

import com.example.contextflow.application.reporting.ReportOperations;
import com.example.contextflow.config.core.ProxyInspectionConfig;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ProxyInspectionApp {

    public static void main(String[] args) {
        // Контекст створюємо вручну, бо це навчальний запуск/інспекція
        try (var ctx = new AnnotationConfigApplicationContext(ProxyInspectionConfig.class)) {
            // Дістаємо bean за контрактом — так і має бути в DI
            ReportOperations ops = ctx.getBean(ReportOperations.class);

            // Діагностика: дивимося runtime-class (швидше за все, це буде proxy)
            System.out.println(ops.getClass().getName()); // jdk.proxy2.$Proxy...
            // Бізнес-виклик проходить через proxy і делегується в target
            System.out.println(ops.generate());           // daily-report
        }
    }
}

З високою ймовірністю ви побачите щось на кшталт jdk.proxy2.$Proxy12 (точний номер буде іншим), а потім — рядок daily-report. Але важливіше інше: між цими рядками ви побачите вивід часу з proxy:

Час (ns): 12345
daily-report

Отже, виклик справді пройшов через proxy, а не безпосередньо в target.

Тепер додамо кілька перевірок типів. Це саме те, що потрібно для реальної інспекції: зрозуміти, який об’єкт лежить у контексті, і не переплутати контракт із реалізацією.

import com.example.contextflow.application.reporting.ReportOperations;
import com.example.contextflow.application.reporting.ReportingService;

import java.lang.reflect.Proxy;

// Перевіряємо: це взагалі proxy-клас?
System.out.println(Proxy.isProxyClass(ops.getClass()));     // true
// Контракт (інтерфейс) дотримано — це найважливіше
System.out.println(ops instanceof ReportOperations);        // true
// А ось реалізація — ні, тому що це не ReportingService, а обгортка
System.out.println(ops instanceof ReportingService);        // false

Саме цього ми й добивалися: логіка залишається в ReportingService, але з контексту приходить proxy-екземпляр. Якщо залежності проєктуються через інтерфейс, бізнес-код спокійно продовжує працювати за контрактом.

6. getBean() за ім’ям і runtime-тип

Ім’я bean-а не гарантує точний клас об’єкта. У Spring це радше «комірка», а не «паспорт екземпляра».

Покажемо це без зайвої філософії: дістанемо bean за іменем і порівняємо поведінку.

// Дістаємо bean за іменем — це саме "комірка", а не обіцянка щодо точного класу
Object bean = ctx.getBean("reportingService");

System.out.println(bean.getClass().getName()); // jdk.proxy2.$Proxy...
System.out.println(bean instanceof ReportOperations); // true

І тепер найспокусливіший — і найнебезпечніший — крок: «ну раз це reportingService, давай я приведу його до ReportingService». Давайте зробимо це обережно, через try/catch, щоб побачити реальність, а не просто отримати червоний екран.

try {
    // Небезпечний каст: у випадку JDK proxy це майже напевно призведе до ClassCastException
    ReportingService impl = (ReportingService) bean;
    System.out.println(impl.generate());
} catch (ClassCastException ex) {
    // Тут ми фіксуємо факт: контейнер повернув обгортку, а не реалізацію
    System.out.println("Каст не вдався: " + ex.getClass().getSimpleName()); // ClassCastException
}

Сенс не в тому, щоб запам’ятати: «буде ClassCastException». Сенс у дисципліні: якщо ви на рівні коду починаєте кастувати beans до реалізації, ви самі ламаєте собі proxy-модель. Контейнер може повернути обгортку, і ви зобов’язані тримати це в голові. Тут працює те саме правило, що й для будь-якого proxy: перевірка за контрактом (instanceof ReportOperations) надійна, а спроба впертися в exact class швидко ламається.

7. Де тримати діагностику контейнера

Дуже легко після таких експериментів зробити погану річ: почати тягати ApplicationContext по всьому застосунку і в кожному сервісі писати ctx.getBean() «щоб точно було те, що потрібно». Це вже знайомий запах із Дня 9: service locator замість DI.

Щоб залишатися в межах здорової архітектури ContextFlow, можна дотримуватися простого правила. Інспекція контейнера — це інфраструктура або діагностика, а не бізнес-логіка. Отже, їй місце або в support.*, або в стартовому чи сценарному коді, але не в application.service.

Якщо у вас уже є окремий інфраструктурний bean для діагностики контексту, його зручно доповнити мінімальною утилітою «покажи runtime class». Наприклад, у дусі:

package com.example.contextflow.support.diagnostics;

import org.springframework.context.ApplicationContext;

public class ProxyDiagnostics {

    private final ApplicationContext ctx;

    public ProxyDiagnostics(ApplicationContext ctx) {
        // Контекст зберігаємо лише для інфраструктурної діагностики, не для бізнес-логіки
        this.ctx = ctx;
    }

    public void printRuntimeType(String beanName) {
        // Інспекція: дістаємо bean за іменем і друкуємо його runtime-тип
        Object bean = ctx.getBean(beanName);
        System.out.println(beanName + " -> " + bean.getClass().getName());
    }
}

Це не ідеальна «бойова» утиліта (і ми не робимо з цього окремий продукт), але методично вона важлива: ви тримаєте вміння дивитися в контейнер в одному місці, а не розмазуєте getBean() по бізнес-сервісах. Бізнес-сервіси при цьому й надалі отримують залежності через конструктор, як ми вже вчили.

А якщо ви хочете взагалі без ApplicationContext у конструкторі (що часто приємніше), можна залишити діагностику на рівні main/scenario runner, де getBean() — допустимий інструмент саме як bootstrap/inspection API.

8. Схема: proxy і target

Іноді корисно зупинитися на секунду й просто подивитися на картинку. Ми не «прискорювали звітність магією Spring», а зробили просту, майже механічну річ: вставили об’єкт на шляху виклику.

flowchart LR
  A["Ваш код: ctx.getBean(ReportOperations)"] --> B["Proxy (JDK dynamic proxy)"]
  B -->|делегування виклику| C["Target: ReportingService"]
  B --> D["Технічна обв’язка: час виконання / логування"]

Якщо ви запам’ятаєте цю схему й додасте до неї думку «proxy з’являється через точки розширення контейнера (наприклад, BPP)», то 80% «магії Spring» перестануть бути магією і стануть інженерією.

9. Типові помилки під час інспекції proxied beans

Помилка № 1: перевіряти тип через getClass() == ... і будувати на цьому логіку.
Коли ви починаєте порівнювати точний клас об’єкта, ви фактично забороняєте контейнеру обгортати цей об’єкт. У світі proxy це майже завжди погана ідея: контейнер може повернути інший runtime-клас, але об’єкт при цьому повністю коректний за контрактом. Краще спиратися на інтерфейс або базовий тип, а getClass() залишати як діагностичний інструмент, а не як умову в бізнес-логіці.

Помилка № 2: намагатися привести інтерфейсний proxy до конкретної реалізації.
Для JDK dynamic proxy це гарантована пастка: проксі реалізує інтерфейс, але не є екземпляром класу реалізації. Найкращий спосіб уникнути цього — не вимагати реалізацію там, де вам потрібен контракт. Якщо дуже хочеться «дістати target», це вже окрема інфраструктурна історія, і її не можна перетворювати на щоденну звичку.

Помилка № 3: писати BeanPostProcessor, який проксуватиме «все підряд».
Новачку здається: «раз це цікаво, давайте загорнемо всі beans». Після цього застосунок починає поводитися дивно, ламаються типи, з’являється купа неочікуваних ефектів, а дебаг перетворюється на жахіття. У навчальних (і реальних) проєктах проксування має бути точковим: фільтрація за іменем, типом, анотацією, пакетом — чим завгодно, аби не перетворювати контейнер на м’ясорубку.

Помилка № 4: переносити контейнерну діагностику в бізнес-сервіси.
Щойно в OrderPlacementService з’являється ApplicationContext ctx «для зручності», ви зробили крок до service locator і прихованих залежностей. У ContextFlow ми свідомо тримаємо такі речі в support.* або в bootstrap-коді. Інакше ви втрачаєте головну перевагу DI: прозорість залежностей через конструктор.

Помилка № 5: вважати, що proxy «переписує» target object або «додає» в нього код.
Proxy не зобов’язаний змінювати target. Він може взагалі не чіпати вихідний об’єкт, а просто стояти між вами й ним. Тому, коли ви дебажите, важливо розрізняти: «усередині є об’єкт з логікою» і «в контейнері вам видали обгортку». Ця ясність економить години життя — а іноді й цілі нервові клітини (їх, кажуть, не відновлюють, але програмісти все одно намагаються).

1
Задача
Spring Core, 21 рівень, 4 лекція
Недоступна
Proxy bean через `BeanPostProcessor`
Proxy bean через `BeanPostProcessor`
1
Задача
Spring Core, 21 рівень, 4 лекція
Недоступна
Інспекція bean за ім'ям з `ApplicationContext`
Інспекція bean за ім'ям з `ApplicationContext`
1
Опитування
Проксі Spring, рівень 21, лекція 4
Недоступний
Проксі Spring
Цільовий об’єкт і виклики
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ