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. Він може взагалі не чіпати вихідний об’єкт, а просто стояти між вами й ним. Тому, коли ви дебажите, важливо розрізняти: «усередині є об’єкт з логікою» і «в контейнері вам видали обгортку». Ця ясність економить години життя — а іноді й цілі нервові клітини (їх, кажуть, не відновлюють, але програмісти все одно намагаються).
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ