惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
N
News and Events Feed by Topic
D
DataBreaches.Net
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Engineering at Meta
Engineering at Meta
T
Tailwind CSS Blog
博客园_首页
Microsoft Azure Blog
Microsoft Azure Blog
Y
Y Combinator Blog
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客
A
About on SuperTechFans
I
InfoQ
S
Securelist
Last Week in AI
Last Week in AI
S
Schneier on Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Hacker News: Ask HN
Hacker News: Ask HN
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
腾讯CDC
大猫的无限游戏
大猫的无限游戏
S
Security @ Cisco Blogs
博客园 - 三生石上(FineUI控件)
Simon Willison's Weblog
Simon Willison's Weblog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
美团技术团队
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
罗磊的独立博客
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Latest news
Latest news
Recent Announcements
Recent Announcements
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
LINUX DO - 热门话题
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Анатомия Spring Proxy: кто и зачем подменяет ваши бины
Денис · 2026-06-25 · via Все публикации подряд на Хабре

Продолжаю рассказ о том как безработный Java - разработчик ищет себе занятия...))

В этот раз, благодаря вопросам ребят, которых вынужденно менторю (кстати пользуясь случаем, всем благодарен, ценю), я решил окончательно расставить точки над "и" в вопросе - что же такое фактически Proxy в Spring, как создается и что у него "под капотом".


Вступление

Для проверки работы наших Proxy, я создал такой статический метод, который в будущем можно применять везде:

SpringProxyInspector
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.Ordered;

public class SpringProxyInspector {

    public static void inspect(Object bean, String beanName) {
        System.out.println("\n=======================================================");
        System.out.println("ПРОВЕРКА ПРОКСИ: " + beanName);
        System.out.println("=======================================================");
        System.out.println("Текущий класс: " + bean.getClass().getName());
    
        if (!(bean instanceof Advised advised)) {
            System.out.println(" Это обычный объект, не Spring AOP прокси.");
            System.out.println("=======================================================\n");
            return;
        }

        String proxyType = AopUtils.isCglibProxy(bean) ? "CGLIB (через наследование)" 
                         : AopUtils.isJdkDynamicProxy(bean) ? "JDK Dynamic Proxy (через интерфейс)" 
                         : "Неизвестный тип прокси";
        
        System.out.println(" ├── Тип прокси: " + proxyType);
        System.out.println(" ├── Целевой класс (Target): " + advised.getTargetSource().getTargetClass().getName());

        Advisor[] advisors = advised.getAdvisors();
        System.out.println(" └── Найдено Advisor'ов: " + advisors.length);

        for (int i = 0; i < advisors.length; i++) {
            Advisor advisor = advisors[i];
            
            String orderInfo = "Не задан";
            if (advisor instanceof Ordered ordered) {
                int order = ordered.getOrder();
                orderInfo = (order == Ordered.HIGHEST_PRECEDENCE) ? "HIGHEST_PRECEDENCE" 
                          : (order == Ordered.LOWEST_PRECEDENCE) ? "LOWEST_PRECEDENCE" 
                          : String.valueOf(order);
            }

            System.out.printf("     [%d] %s%n", i + 1, advisor.getClass().getSimpleName());
            System.out.printf("         ├── Order: %s%n", orderInfo);

            if (advisor.getAdvice() != null) {
                System.out.printf("         └── Advice: %s%n", advisor.getAdvice().getClass().getName());
            } else {
                System.out.println("         └── Advice: Отсутствует");
            }
        }
        System.out.println("=======================================================\n");
    }
}

Благодаря ему можно немного приоткрыть завесу - как же там все устроено?

Создание тестового стенда

Для чистоты эксперимента у нас есть минимальный проект. Определим интерфейс:

public interface OrderService {
    void processOrder();
    void outerMethod();
}

И реализацию с тремя аннотациями:

@Service
public class OrderServiceImpl implements OrderService {

    @Cacheable("orders")
    @Transactional
    @LogExecutionTime
    public void processOrder() {
        System.out.println("    [Target] Выполняется реальная бизнес-логика метода processOrder()!");
    }
}
  • @Cacheable("orders") — включает кеширование, требует перехвата метода для проверки кеша до вызова и сохранения результата после.

  • @Transactional — управление транзакциями, перехват для открытия/закрытия транзакции.

  • @LogExecutionTime — кастомная аннотация, которую мы обрабатываем через AspectJ-аспект.

Три аннотации на одном методе это типичная картина в современном Spring-приложении. Мы уже привыкли, что @Cacheable магическим образом кеширует результат, @Transactional открывает транзакцию, а кастомный @LogExecutionTime замеряет время выполнения.

Вот кастомная аннотация:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}

И аспект, который её обрабатывает:

@Aspect
@Component
public class LogExecutionTimeAspect {

    @Around("@annotation(LogExecutionTime)")
    public Object logTime(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("    [Aspect] Старт замера времени...");
        Object result = joinPoint.proceed();
        System.out.println("    [Aspect] Конец замера времени.");
        return result;
    }
}

Не забудем включить кеширование через @EnableCaching на основном классе приложения и всё готово.

Теперь в main классе добавим в CommandLineRunner:

@SpringBootApplication
@EnableCaching
public class SpringProxyApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringProxyDemoApplication.class, args);
    }

    @Bean
    public CommandLineRunner runInspector(OrderService orderService) {
        return args -> {
          System.out.println(orderService.getClass());
          orderService.processOrder();
    };
}

Я вызываю OrderService. Но почему это не мой класс?

Запускаем приложение и видим в консоли:

class oleborn.spring.OrderServiceImpl$$SpringCGLIB$$0
    [Aspect] Старт замера времени...
    [Target] Выполняется реальная бизнес-логика метода processOrder()!
    [Aspect] Конец замера времени.

Первая строка будет явной неожиданностью для новичка. Ведь мы ожидали увидеть class OrderServiceImpl, а видим длинное имя с $$SpringCGLIB$$. И это совсем не наш класс.

Если Spring действительно подменил наш объект, значит где-то должен существовать и настоящий OrderServiceImpl. Но кто его создал? Где он сейчас находится? И почему вместо него мы получили совершенно другой объект?

Давайте разбираться...

Важный момент: если бы мы убрали все аннотации, требующие перехвата, Spring вернул бы нам обычный OrderServiceImpl, и getClass() показал бы именно его. Но в реальных проектах вы почти всегда встретите хотя бы @Transactional, поэтому прокси будет создаваться незаметно.

Кто подменил мой бин?

Жизнь нашего OrderServiceImpl начинается совершенно обычно. Spring считывает конфигурацию, создаёт BeanDefinition, вызывает конструктор - и получает чистый экземпляр класса. Никаких транзакций, никаких кэшей, никаких аспектов. Просто голый объект в памяти.

Но где-то между этим моментом и тем, когда бин попадает в ApplicationContext, происходит подмена. Давайте включим дебаггер и проследим этот путь по шагам.

В Spring есть четкое разделение на две фазы: фаза инициализации (при старте приложения) и фаза выполнения (когда вызывается метод). Сейчас мы исследуем первую.

Шаг 1. Перехват инициализации

Чтобы понять, где именно происходит подмена, поставим брейкпоинт в методе postProcessAfterInitialization() класса AbstractAutoProxyCreator. Это один из ключевых BeanPostProcessor'ов, отвечающих за автоматическое создание прокси. Именно сюда контейнер приносит наш свежесозданный объект перед тем, как отдать его наружу.

Схематично это выглядит так:

BeanDefinition → создание экземпляра OrderServiceImpl (голый объект)
                    ↓
    все BeanPostProcessor'ы отрабатывают
                    ↓
    AbstractAutoProxyCreator.postProcessAfterInitialization()
                    ↓
    wrapIfNecessary() — нужно ли создавать прокси?
                    ↓
    если да → сбор через getAdvicesAndAdvisorsForBean() Advisor'ов
                    ↓
    чеоез createProxy() и ProxyFactory начинается создание прокси
                    ↓
    если нет → возвращается оригинальный бин

Заходим в дебаггер. Видим, что наш OrderServiceImpl уже создан, но пока без всяких обёрток. AbstractAutoProxyCreator проверяет: есть ли для этого бина подходящие Advisor'ы? Если есть то бин отправляется на хирургический стол wrapIfNecessary(). Если нет - возвращается как есть.

Конкретная реализация, которая работает в большинстве Spring-приложений, это AnnotationAwareAspectJAutoProxyCreator. Класс сканирует все зарегистрированные аспекты и Advisor'ы и решает, какие бины нужно обернуть.

Шаг 2. Откуда берутся Advisor'ы?

Отпускаем дебаггер на шаг вперед, и выполнение проваливается в метод getAdvicesAndAdvisorsForBean(). Здесь Spring собирает все Advisor'ы, подходящие для нашего бина.

Когда я впервые посмотрел в отладчике на собранный массив, у меня возник логичный вопрос: 

А откуда вообще взялся объект BeanFactoryTransactionAttributeSourceAdvisor? Ведь я вроде нигде не писал new BeanFactoryTransactionAttributeSourceAdvisor()?

Они не появляются из воздуха. Если проследить их происхождение, всё встает на свои места:

  • Мы написали @EnableCaching на основном классе приложения и под капотом создался инфраструктурный бин BeanFactoryCacheOperationSourceAdvisor.

  • Мы добавили @Transactional и Spring загрузил BeanFactoryTransactionAttributeSourceAdvisor (через @EnableTransactionManagement, даже если мы не ставили её явно, Spring Boot автоматически включает транзакции при наличии spring-tx на classpath).

  • Мы создали кастомный аспект с @Aspect и @Around("@annotation(LogExecutionTime)") и Spring создал InstantiationModelAwarePointcutAdvisorImpl на основе нашего аспекта.

Теперь, когда наш голый OrderServiceImpl попадает в getAdvicesAndAdvisorsForBean(), Spring сканирует его методы и сверяет их с правилами каждого Advisor'а.

Совпало? Кладём в список.

Напомню, что в нашем случае метод processOrder() помечен тремя аннотациями:

@Cacheable("orders")
@Transactional
@LogExecutionTime
public void processOrder() { ... }

Поэтому в список попадают три Advisor'а: для кеша, для транзакций и для кастомного аспекта. Плюс системный ExposeInvocationInterceptor добавляется автоматически.

Важнейший момент для понимания:

До этого момента многие думали что аннотации @Transactional@Cacheable@LogExecutionTime и дальше неявно как-то продолжают работу. Но оказывается, что после того как Spring собрал Advisor'ы, аннотации сделали свою работу и навсегда уходят со сцены. В памяти остаются только конкретные Java-объекты: TransactionInterceptorCacheInterceptor и ваш LogExecutionTimeAspect (обёрнутый в AspectJAroundAdvice).

Аннотации нужны только на этапе bootstrap. После создания Advisor'ов они больше не участвуют. Это важно понять: если вы меняете аннотацию в рантайме (что вряд ли возможно), это не повлияет на уже созданный прокси.

Шаг 3. Жесткая сортировка

Итак, у нас на руках список из перехватчиков. Следующий вопрос логичен: в каком порядке они будут выполняться? Ведь если кэш отработает внутри транзакции БД, мы получим проблемы, или наоборот транзакция откроется, но результат не закэшируется.

Список отправляется в метод sortAdvisors(). Здесь работает AnnotationAwareOrderComparator. Алгоритм прагматичен и проверяет интерфейсы в строгом порядке:

  1. PriorityOrdered однозначно высший приоритет. Здесь системный ExposeInvocationInterceptor гордо забирает себе индекс 0.

  2. Ordered имеет базовый приоритет. Инфраструктурные перехватчики (транзакции, кэш) по умолчанию возвращают LOWEST_PRECEDENCE и отправляются в конец.

  3. @Order если есть, то читаются пользовательские приоритеты на кастомных аспектах, чтобы расставить их между системными.

Для наглядности того что получилось давайте запустим наш SpringProxyInspector и посмотрим что он вернет в консоль:

=======================================================
ПРОВЕРКА ПРОКСИ: OrderService
=======================================================
Текущий класс: oleborn.spring.OrderServiceImpl$$SpringCGLIB$$0
 ├── Тип прокси: CGLIB (через наследование)
 ├── Целевой класс (Target): oleborn.taskswithspring.Issue.OrderServiceImpl
 └── Найдено Advisor'ов: 4
     [1] ExposeInvocationInterceptor
         ├── Order: HIGHEST_PRECEDENCE
         └── Advice: org.springframework.aop.interceptor.ExposeInvocationInterceptor
     [2] BeanFactoryCacheOperationSourceAdvisor
         ├── Order: LOWEST_PRECEDENCE
         └── Advice: org.springframework.cache.interceptor.CacheInterceptor
     [3] BeanFactoryTransactionAttributeSourceAdvisor
         ├── Order: LOWEST_PRECEDENCE
         └── Advice: org.springframework.transaction.interceptor.TransactionInterceptor
     [4] InstantiationModelAwarePointcutAdvisorImpl
         ├── Order: LOWEST_PRECEDENCE
         └── Advice: org.springframework.aop.aspectj.AspectJAroundAdvice
=======================================================

Обратите внимание: первый Advisor имеет Order: HIGHEST_PRECEDENCE (значение -2147483647). Все остальные — LOWEST_PRECEDENCE (2147483647). Маршрут выполнения предрешен. Массив отсортирован. Теперь он готов к передаче в ProxyFactory.

Шаг 4. Фабрика и заморозка

Идем дальше. У нас есть оригинальный объект и отсортированный массив перехватчиков. Кто их склеит?

В дело вступает ProxyFactory. Запуск происходит через метод createProxy(). Заглянем в код (взят оригинальный, осмелился добавить моих комментариев):

createProxy()
protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {

		return buildProxy(beanClass, beanName, specificInterceptors, targetSource, false);
	}

	private Class<?> createProxyClass(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource) {

		return (Class<?>) buildProxy(beanClass, beanName, specificInterceptors, targetSource, true);
	}

	private Object buildProxy(Class<?> beanClass, @Nullable String beanName,
			@Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) {

		if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) {
			AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass);
		}

		ProxyFactory proxyFactory = new ProxyFactory();
		proxyFactory.copyFrom(this);

		if (proxyFactory.isProxyTargetClass()) {
			// Explicit handling of JDK proxy targets and lambdas (for introduction advice scenarios)
			if (Proxy.isProxyClass(beanClass) || ClassUtils.isLambdaClass(beanClass)) {
				// Must allow for introductions; can't just set interfaces to the proxy's interfaces only.
				for (Class<?> ifc : beanClass.getInterfaces()) {
					proxyFactory.addInterface(ifc);
				}
			}
		}
		else {
			// No proxyTargetClass flag enforced, let's apply our default checks...
			if (shouldProxyTargetClass(beanClass, beanName)) {
				proxyFactory.setProxyTargetClass(true);
			}
			else {
				evaluateProxyInterfaces(beanClass, proxyFactory);
			}
		}
        //именно тут в buildAdvisors() идет определение advisors
		Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);

        //Загружаем отсортированный массив Advisor'ов
		proxyFactory.addAdvisors(advisors);
      
        //Прячем оригинальный бин
		proxyFactory.setTargetSource(targetSource);
		customizeProxyFactory(proxyFactory);

        //Заморозка по умолчанию private boolean freezeProxy = false;
		proxyFactory.setFrozen(this.freezeProxy);
		if (advisorsPreFiltered()) {
			proxyFactory.setPreFiltered(true);
		}

		// Use original ClassLoader if bean class not locally loaded in overriding class loader
		ClassLoader classLoader = getProxyClassLoader();
		if (classLoader instanceof SmartClassLoader smartClassLoader && classLoader != beanClass.getClassLoader()) {
			classLoader = smartClassLoader.getOriginalClassLoader();
		}
		return (classOnly ? proxyFactory.getProxyClass(classLoader) : proxyFactory.getProxy(classLoader));
	}

И, наконец, финальный шаг — вызов proxyFactory.getProxy(). CGLIB генерирует байт-код для того самого OrderServiceImpl$$SpringCGLIB$$0.

Но как именно отсортированный массив перехватчиков попадает внутрь этого сгенерированного класса? Лежит ли он прямо в полях?

Если мы копнем исходники ObjenesisCglibAopProxy, мы увидим настоящую, многослойную архитектуру:

Метод setFrozen(true) часто упускают из вида. Зачем он нужен? Не будем гадать, откроем JavaDoc самих разработчиков Spring:

"Set whether the config is frozen. When config is frozen, no advice changes can be made."

Тут Spring может аппаратно запечатывать конфигурацию. Это значит, что если он это сделает, вы больше не сможете в рантайме привести этот прокси к интерфейсу Advised и динамически добавить туда новый Advisor. Массив зафиксирован. Это сделано для производительности и потокобезопасности. Поэтому после создания прокси его конфигурация становится неизменной.

А кто в итоге произведет сборку нашего прокси? Сейчас почти всегда Spring создаёт прокси через CGLIB.

Почему сейчас почти всегда CGLIB?

Ведь Java есть встроенный механизм динамического проксирования с первого дня java.lang.reflect.Proxy.

На самом деле Spring поддерживает оба механизма. Более того, в старых версиях Spring (до Boot 2.x) по умолчанию использовался JDK Dynamic Proxy. Но времена изменились.

Давайте разберёмся, в чём разница.

JDK Dynamic Proxy это встроенный механизм, который работает только с интерфейсами. Он создаёт прокси-объект, реализующий те же интерфейсы, что и целевой класс. При вызове метода управление передаётся в InvocationHandler.invoke(), где через рефлексию (Method.invoke()) вызывается оригинальный метод.

CGLIB (Code Generation Library) работает иначе. Вместо реализации интерфейсов он генерирует байт-код и создаёт подкласс целевого класса. Вызов метода перехватывается через MethodInterceptor.intercept(). Но ключевое отличие — внутри CGLIB использует FastClass вместо рефлексии. FastClass генерирует для каждого класса специальный вспомогательный класс, который вызывает методы по индексу, а не по имени через reflection. Это даёт прирост производительности при вызовах.

Правда, есть нюанс: CGLIB создаёт прокси медленнее, чем JDK-вариант. Но поскольку в Spring бины обычно создаются один раз при старте, это не критично. А вот скорость вызовов в рантайме важнее.

Почему Spring Boot предпочитает CGLIB

Начиная со Spring Boot 2.x, по умолчанию используется CGLIB. Этому есть несколько причин.

Первая и главная - не нужно создавать интерфейсы. JDK Dynamic Proxy требует, чтобы класс реализовывал хотя бы один интерфейс. В современной разработке часто пишут сервисы без интерфейсов и это нормально. CGLIB позволяет проксировать такие классы без лишних телодвижений.

Вторая удобство для разработчиков. Вы просто пишете класс, ставите @Service, добавляете @Transactional и всё работает. Никаких дополнительных интерфейсов, никаких настроек.

Третья производительность. CGLIB быстрее выполняет вызовы методов благодаря FastClass.

Шаг 5. Анатомия CGLIB-прокси

И, наконец-то, финальный шаг - вызов proxyFactory.getProxy().

CGLIB генерирует байт-код для того самого OrderServiceImpl$$SpringCGLIB$$0.

Но как именно отсортированный массив перехватчиков попадает внутрь этого сгенерированного класса? Лежит ли он прямо в полях?

Если мы копнем исходники ObjenesisCglibAopProxy, мы увидим настоящую, многослойную архитектуру(схематичное отражение структуры Proxy Spring):

Сгенерированный класс (OrderServiceImpl$$SpringCGLIB$$0)
    │
    ├── Callback[] (массив коллбеков)
    │       │
    │       └── [0] DynamicAdvisedInterceptor (главный обработчик)
    │               │
    │               └── AdvisedSupport
    │                       ├── Advisor[] (наш отсортированный массив)
    │                       └── TargetSource (ссылка на оригинальный бин)
    │
    └── методы-обёртки (переопределяют все публичные методы)
            │
            └── каждый метод вызывает DynamicAdvisedInterceptor.intercept()

Ключевой момент: массив Advisor[] не лежит прямо в полях прокси. Он спрятан глубоко внутри AdvisedSupport, который хранится в DynamicAdvisedInterceptor. Именно поэтому наш SpringProxyInspector использует интерфейс Advised, чтобы добраться до этой структуры.

И вот, теперь мы точно знаем, где и в каком виде хранится массив перехватчиков. Но до сих пор он просто лежит в оперативной памяти. Контекст поднялся, приложение готово к работе.

А что произойдёт, когда из соседнего сервиса кто-то вызовет orderService.processOrder()?

Кто начнёт распаковывать эту матрёшку и выполнять наш массив?

ReflectiveMethodInvocation - вот кто запускает весь движ

Где-то в коде происходит вызов:

orderService.processOrder();

Мы знаем, что orderService это не наш OrderServiceImpl, а CGLIB-прокси с именем OrderServiceImpl$$SpringCGLIB$$0. Что же произойдетт внутри этого сгенерированного класса?

Шаг 1. Вход в прокси

Когда вы вызываете метод на CGLIB-прокси, на самом деле вызывается метод, который сгенерировал CGLIB. Каждый публичный метод прокси выглядит примерно так (упрощённо):

public void processOrder() {
    //внутри CGLIB хранит ссылку на массив Callback[]
    //вызывает метод intercept() у главного обработчика (DynamicAdvisedInterceptor)
    MethodInterceptor interceptor = ... //DynamicAdvisedInterceptor
    interceptor.intercept(this, method, args, methodProxy);
}

Этот код генерируется во время старта приложения, когда proxyFactory.getProxy() создаёт байт-код. Он не виден в вашем исходном коде, но именно он выполняется, когда вы вызываете метод.

Управление передаётся в DynamicAdvisedInterceptor.intercept().

Шаг 2. DynamicAdvisedInterceptor главный диспетчер

DynamicAdvisedInterceptor это реализация MethodInterceptor (из CGLIB), которая является центральным узлом. Именно здесь лежит AdvisedSupport с нашим отсортированным массивом перехватчиков и ссылкой на оригинальный бин.

В методе intercept() происходит следующее: создаётся объект ReflectiveMethodInvocation, который получает на вход все перехватчики в виде списка interceptorsAndDynamicMethodMatchers, а также ссылку на целевой объект и метод.

Затем вызывается invocation.proceed().

Это сердце системы. Именно здесь оживают наши Advisor'ы.

Шаг 3. Как работает ReflectiveMethodInvocation.proceed()

ReflectiveMethodInvocation сгенерированный в любой реализации содержит рекурсивный метод proceed().

Он увеличивает индекс и передаёт самого себя в следующий перехватчик как в паттерне Chain of Responsibility. Каждый перехватчик, в свою очередь, внутри своего invoke() делает что-то до и после вызова proceed().

Ключевая идея паттерна: цепочка не хранится в отдельной структуре. Она раскручивается динамически во время вызова.

Шаг 4. Реальный запуск

Запускаем наше приложение. В консоли видим:

--- Нормальный вызов (через прокси) ---
    [Aspect] Старт замера времени...
    [Target] Выполняется реальная бизнес-логика метода processOrder()!
    [Aspect] Конец замера времени.

Но что произошло в хронологическом порядке? Давайте разложим по шагам.

Шаг 5. Порядок выполнения

Помним, что у нас есть четыре перехватчика в отсортированном порядке:

  1. ExposeInvocationInterceptor (HIGHEST_PRECEDENCE)

  2. CacheInterceptor (LOWEST_PRECEDENCE)

  3. TransactionInterceptor (LOWEST_PRECEDENCE)

  4. AspectJAroundAdvice (LOWEST_PRECEDENCE)

Теперь запускается proceed(). Вот как это выглядит на уровне стека:

Примерный вид
ReflectiveMethodInvocation.proceed()
    │
    ├── currentInterceptorIndex = 0 → берёт перехватчик [0]
    │   └── ExposeInvocationInterceptor.invoke()
    │       │
    │       ├── что-то делает до (выставляет текущий вызов в ThreadLocal)
    │       ├── вызывает invocation.proceed()  ← рекурсивный вызов!
    │       │
    │       │   ReflectiveMethodInvocation.proceed()
    │       │   │
    │       │   ├── currentInterceptorIndex = 1 → берёт [1]
    │       │   │   └── CacheInterceptor.invoke()
    │       │   │       │
    │       │   │       ├── проверяет кэш до вызова
    │       │   │       ├── вызывает invocation.proceed()
    │       │   │       │
    │       │   │       │   ReflectiveMethodInvocation.proceed()
    │       │   │       │   │
    │       │   │       │   ├── currentInterceptorIndex = 2 → берёт [2]
    │       │   │       │   │   └── TransactionInterceptor.invoke()
    │       │   │       │   │       │
    │       │   │       │   │       ├── открывает транзакцию
    │       │   │       │   │       ├── вызывает invocation.proceed()
    │       │   │       │   │       │
    │       │   │       │   │       │   ReflectiveMethodInvocation.proceed()
    │       │   │       │   │       │   │
    │       │   │       │   │       │   ├── currentInterceptorIndex = 3 → берёт [3]
    │       │   │       │   │       │   │   └── AspectJAroundAdvice.invoke()
    │       │   │       │   │       │   │       │
    │       │   │       │   │       │   │       ├── [Aspect] Старт замера времени...
    │       │   │       │   │       │   │       ├── вызывает invocation.proceed()
    │       │   │       │   │       │   │       │
    │       │   │       │   │       │   │       │   ReflectiveMethodInvocation.proceed()
    │       │   │       │   │       │   │       │   │
    │       │   │       │   │       │   │       │   └── currentInterceptorIndex = 4 (последний)
    │       │   │       │   │       │   │       │       └── invokeJoinpoint()
    │       │   │       │   │       │   │       │           └── [Target] Выполняется реальная бизнес-логика...
    │       │   │       │   │       │   │       │
    │       │   │       │   │       │   │       ├── [Aspect] Конец замера времени.
    │       │   │       │   │       │   │       └── возвращает результат
    │       │   │       │   │       │   │
    │       │   │       │   │       │   └── результат возвращается TransactionInterceptor
    │       │   │       │   │       │
    │       │   │       │   │       ├── коммит/rollback транзакции
    │       │   │       │   │       └── возвращает результат
    │       │   │       │   │
    │       │   │       │   └── результат возвращается CacheInterceptor
    │       │   │       │
    │       │   │       ├── сохраняет результат в кэш
    │       │   │       └── возвращает результат
    │       │   │
    │       │   └── результат возвращается ExposeInvocationInterceptor
    │       │
    │       └── очищает ThreadLocal
    │
    └── конечный результат

Шаг 6. Важные моменты

Вы видите, что цепочка выполняется строго в порядке сортировкиExposeInvocationInterceptor всегда первый, но он не влияет на бизнес-логику, его задача сделать текущий MethodInvocation доступным через ThreadLocal для других компонентов.

CacheInterceptor проверяет кэш до вызова целевого метода. Если данные есть в кэше, он может даже не вызывать proceed() и тогда транзакция не откроется, и аспект не сработает. Это важно понимать для проектирования.

TransactionInterceptor открывает транзакцию до вызова и закрывает (коммит/rollback) после.

AspectJAroundAdvice это наш кастомный аспект, который замеряет время до и после всего остального, потому что он последний в цепочке перед целевым методом. Если бы мы изменили его приоритет через @Order, он мог бы оказаться раньше транзакции или кэша.

Итог: proceed() это движок, который передаёт управление по цепочке перехватчиков. Каждый перехватчик может выполнить свою логику до и после вызова proceed(), тем самым оборачивая не только целевой метод, но и все последующие перехватчики.

Фактически это классический паттерн Chain of Responsibility, реализованный через рекурсию. Каждый перехватчик может:

  • Выполнить действия до proceed()

  • Решить не вызывать proceed() (например, вернуть значение из кэша)

  • Выполнить действия после proceed()

  • Обернуть исключение или изменить результат

Почему self-invocation ломает транзакции?

Разобравшись с механизмом ReflectiveMethodInvocation, мы легко объясним самую частую боль Spring-разработчиков. Если вы хоть раз сталкивались с ситуацией, когда @Transactional внутри класса не работает, то это оно.

Взглянем на наш OrderServiceImpl. У нас есть метод outerMethod(), который вызывает processOrder() внутренним образом:

@Service
public class OrderServiceImpl implements OrderService {

    @Cacheable("orders")
    @Transactional
    @LogExecutionTime
    public void processOrder() {
        System.out.println("    [Target] Выполняется реальная бизнес-логика метода processOrder()!");
    }

    public void outerMethod() {
        System.out.println("    [Target] Вызван outerMethod(). Сейчас вызову processOrder() через this...");
        processOrder();   //вызов через this
    }
}

Теперь вызываем outerMethod() через внедрённый сервис. Смотрим на вывод:

--- Вызов с проблемой Self-Invocation ---
[Target] Вызван outerMethod(). Сейчас вызову processOrder() через this...
[Target] Выполняется реальная бизнес-логика метода processOrder()!

Обратите внимание: нет ни одной строки от аспекта. Нет [Aspect] Старт замера времени..., нет [Aspect] Конец замера времени. Нет никаких следов транзакций или кэша.

Почему?

Вспомним, как выглядит архитектура прокси:

Прокси (OrderServiceImpl$$SpringCGLIB$$0)
    │
    └── DynamicAdvisedInterceptor
            └── AdvisedSupport
                    ├── Advisor[] (массив перехватчиков)
                    └── TargetSource → OrderServiceImpl (оригинальный бин)

Когда вы вызываете orderService.outerMethod() извне, вы обращаетесь к прокси. Прокси перехватывает вызов, запускает DynamicAdvisedInterceptor, и создаётся ReflectiveMethodInvocation, который раскручивает цепочку перехватчиков.

Но внутри outerMethod() происходит this.processOrder()this это ссылка на оригинальный объект OrderServiceImpl, а не на прокси. Вызов идёт напрямую к целевому объекту, минуя Callback[]DynamicAdvisedInterceptorAdvisedSupport и все Advisor'ы.

Цепочка перехватчиков просто не запускается. Поэтому @Transactional@Cacheable@LogExecutionTime и другие аннотации не срабатывают при внутренних вызовах.

Это фундаментальное ограничение Spring AOP, основанного на прокси. Прокси работает только на уровне внешних вызовов через внедрённый бин. Внутренние вызовы через this всегда идут напрямую к объекту.

Как это все в итоге работает? (Собираем пазл)

Давайте соберём всё в единую картину:

Картина

Фаза инициализации (при старте приложения)

┌────────────────────────────────────────────────────────────────────┐
│ BeanDefinition → OrderServiceImpl (голый объект)                  │
│                    ↓                                               │
│ AbstractAutoProxyCreator.postProcessAfterInitialization()         │
│                    ↓                                               │
│ getAdvicesAndAdvisorsForBean()                                    │
│     ↓                                                             │
│     Сбор Advisor'ов:                                              │
│     - @EnableCaching → BeanFactoryCacheOperationSourceAdvisor    │
│       → CacheInterceptor                                          │
│     - @Transactional → BeanFactoryTransactionAttributeSourceAdvisor│
│       → TransactionInterceptor                                    │
│     - @Aspect → InstantiationModelAwarePointcutAdvisorImpl        │
│       → AspectJAroundAdvice                                       │
│     - Системный → ExposeInvocationInterceptor (добавляется всегда)│
│                    ↓                                               │
│ sortAdvisors() (AnnotationAwareOrderComparator)                   │
│    → ExposeInvocationInterceptor (HIGHEST_PRECEDENCE)            │
│    → остальные (LOWEST_PRECEDENCE)                               │
│                    ↓                                               │
│ ProxyFactory                                                      │
│     ↓                                                             │
│ setTargetSource(new SingletonTargetSource(bean))                 │
│ addAdvisors(advisors)                                             │
│ setFrozen(true) — заморозка конфигурации                         │
│     ↓                                                             │
│ getProxy() → OrderServiceImpl$$SpringCGLIB$$0                    │
│                    ↓                                               │
│ Внутри сгенерированного прокси:                                   │
│     Callback[] (массив коллбеков)                                 │
│         → [0] DynamicAdvisedInterceptor                           │
│             → AdvisedSupport                                      │
│                 → Advisor[] (отсортированный, замороженный)      │
│                 → TargetSource (ссылка на оригинальный бин)      │
│     Методы-обёртки (переопределяют все публичные методы)          │
│         → каждый вызывает DynamicAdvisedInterceptor.intercept()  │
└────────────────────────────────────────────────────────────────────┘

Фаза выполнения (вызов метода)

┌────────────────────────────────────────────────────────────────────┐
│ orderService.processOrder()                                      │
│                    ↓                                               │
│ CGLIB-прокси (OrderServiceImpl$$SpringCGLIB$$0)                   │
│     → сгенерированный метод processOrder()                        │
│                    ↓                                               │
│ DynamicAdvisedInterceptor.intercept()                            │
│     → создаёт ReflectiveMethodInvocation                         │
│     → передаёт список interceptors (Advisor'ы преобразованы)      │
│                    ↓                                               │
│ ReflectiveMethodInvocation.proceed() (рекурсивно)                │
│     ↓                                                             │
│     Interceptor1: ExposeInvocationInterceptor                     │
│         → сохраняет вызов в ThreadLocal                           │
│         → вызывает proceed()                                      │
│     ↓                                                             │
│     Interceptor2: CacheInterceptor                                │
│         → проверяет кэш до вызова                                 │
│         → если нет в кэше — вызывает proceed()                   │
│         → сохраняет результат после вызова                        │
│     ↓                                                             │
│     Interceptor3: TransactionInterceptor                          │
│         → открывает транзакцию до вызова                          │
│         → вызывает proceed()                                      │
│         → коммитит или откатывает после вызова                    │
│     ↓                                                             │
│     Interceptor4: AspectJAroundAdvice (кастомный аспект)          │
│         → [Aspect] Старт замера времени...                        │
│         → вызывает proceed()                                      │
│         → [Aspect] Конец замера времени.                          │
│     ↓                                                             │
│     invokeJoinpoint() → OrderServiceImpl.processOrder()          │
│         → [Target] Выполняется реальная бизнес-логика!           │
│                    ↑                                               │
│     (обратный проход по цепочке: каждый перехватчик возвращает   │
│      управление и выполняет свои after-действия)                 │
└────────────────────────────────────────────────────────────────────┘

Теперь каждый фрагмент встал на своё место. Мы знаем, где рождаются Advisor'ы, как они сортируются, как попадают в прокси и как оживают при вызове метода.

Послесловие (если дочитали до этого места)

Я уверен, что после этого разбора, Spring AOP перестанет быть для Вас магией.

Её больше не будет в @Transactional как в особенной аннотации. Или @Async или в @Cacheble . Теперь вы видите обычные MethodInterceptor'ы в массиве, которые ждут своей очереди вызвать proceed().

Ведь это всё, что там есть. Список объектов, сортировка и рекурсивный обход. Никакой чёрной магии. Только байт-код, сгенерированный CGLIB, и хорошо продуманная архитектура вокруг паттерна Chain of Responsibility.

Когда вы в следующий раз увидите $$SpringCGLIB$$ в стектрейсе или логе, вы будете точно знать, что это не ошибка, а фундаментальная часть Spring, делающая вашу жизнь проще.


Материал подготовлен автором telegram-канала о изучении Java.


Использованные материалы (спасибо авторам):