сеньорчикОткрыть в Telegram
← вся теориятеория к собесу · Spring и Hibernate

Spring Boot и AOP: автоконфигурация и прокси

Boot: автоконфигурация, которую надо уметь проверять

Собрал автоконфигурацию с условием «создавай мой бин, только если пользователь не объявил свой». Без своего бина в контексте оказался автоматический. Со своим - только мой, всего бинов этого типа один. Правило вытеснения работает.

А потом я перенёс то же самое условие в обычный класс с @Configuration и подключил его руками. Создались ОБА бина, и приложение упало на неоднозначности. Условие не сработало, потому что смотреть было ещё не на что.

// Формулировки: «как работает автоконфигурация?», «как понять, почему бин не создался?», «что умеет Actuator?», «на чём держится @Transactional?».

Автоконфигурация и правило вытеснения

Автоконфигурация - это готовые куски настройки, которые Boot применяет условно. Каждый обвешан условиями: @ConditionalOnClass срабатывает, если нужный класс есть в classpath (списке мест, где JVM ищет классы), @ConditionalOnMissingBean - если ты не объявил свой бин этого типа. Поэтому добавленный в зависимости стартер приносит рабочую настройку сам, а твой собственный бин её вытесняет.

Второй мой замер объясняет, почему это работает только у настоящих автоконфигураций. Boot собирает их из специального файла-реестра и применяет ПОСЛЕ всех твоих конфигураций - к этому моменту твои бины уже объявлены, и условие их видит. Тот же @ConditionalOnMissingBean в обычном классе с @Configuration отработал раньше твоего бина, ничего не нашёл и создал дубль: в итоге NoUniqueBeanDefinitionException ... found 2: autoClock,myClock.

// Когда бин не создался или создался не тот, гадать не надо: запуск с флагом --debug печатает отчёт об условиях, где по каждой автоконфигурации написано, какие условия совпали, а какие нет. Настройки при этом накладываются слоями: application.yml, затем профили под среду, затем переменные окружения и аргументы запуска - последние переопределяют предыдущие.

автоконфигурация
готовая настройка, применяемая по условиям и после твоих конфигураций
отчёт об условиях
вывод --debug: почему каждая автоконфигурация сработала или нет

Actuator: глаза на проде

Actuator - отдельный стартер, который добавляет эксплуатационные адреса, не требуя твоего кода. health показывает живость и готовность приложения, и его опрашивают оркестраторы вроде Kubernetes. metrics отдаёт счётчики через Micrometer - прослойку, которая умеет писать их в Prometheus и другие хранилища. env показывает итоговые настройки, loggers меняет уровень логирования на лету, без перезапуска.

Это прод-инструмент, а не игрушка: по health принимается решение слать ли трафик на экземпляр приложения, по метрикам срабатывают оповещения дежурному.

// И ровно поэтому наружу его открывают только за аутентификацией. Actuator, целиком выставленный в интернет, отдаёт переменные окружения с паролями и позволяет снять снимок памяти со всеми данными пользователей. Это пункт любого чек-листа безопасности.

Actuator
готовые адреса для эксплуатации: живость, метрики, настройки, логи
Micrometer
прослойка, через которую метрики уезжают в Prometheus и подобные

AOP и его границы

AOP (aspect-oriented programming, аспектно-ориентированное программирование) - способ вынести сквозную логику из методов: логирование, метрики, повторные попытки, кэш, транзакции. Пишется это аспектом, в котором две части: срез - описание, к каким методам подключаться, и совет - код, выполняемый вокруг вызова. Именно так под капотом устроены @Transactional, @Cacheable, @Async и @Retryable.

Ограничения тут те же, что у транзакций, и по той же причине: аспект подключается через прокси - обёртку вокруг бина. Перехват работает на публичных методах спринговых бинов, вызванных снаружи. final-класс не обернуть подклассом. Вызов через this проходит мимо. Всё, что я замерил на транзакциях, применимо к любой из этих аннотаций без изменений.

// Пара практических деталей. У @Async с типом возврата void исключение просто исчезает, если не поставить обработчик: метод выполняется в другом потоке, и бросать ошибку уже некому. А @Cacheable запоминает результат по аргументам, но за очистку кэша отвечаешь ты - иначе пользователи будут долго видеть старые данные.

срез / совет
где подключиться / что выполнить вокруг вызова
@Cacheable
запоминает результат по аргументам, очистку кэша пишешь сам

Как отвечать: «Как работает автоконфигурация и как понять, почему бин не создался?»

Автоконфигурации это готовые куски настройки, обвешанные условиями: есть ли нужный класс в classpath, не объявил ли я уже свой бин этого типа. Boot берёт их из отдельного реестра и применяет после всех моих конфигураций - поэтому стартер приносит рабочую настройку сам, а мой собственный бин её вытесняет. Я это проверял: без моего бина в контексте оказался автоматический, с моим - только мой, ровно один. Важная деталь: условие on-missing-bean работает именно у настоящих автоконфигураций. Когда я перенёс его в обычный класс с @Configuration, оно отработало раньше моего бина и создало дубль - приложение упало на неоднозначности. А когда что-то не создалось, я не гадаю: запускаю с флагом --debug и читаю отчёт об условиях, там по каждой автоконфигурации написано, какое условие не сошлось.

Почему это сильный ответ: механизм объяснён через порядок применения, а не через слово «магия»; названа неочевидная граница применимости условия и дан точный инструмент диагностики.

На чём валят

  • Говорить «Boot-магия сломалась» вместо чтения отчёта об условиях. Флаг --debug прямо называет несовпавшее условие.
  • Ставить @ConditionalOnMissingBean в свой обычный @Configuration. У меня так создались оба бина и старт упал на неоднозначности.
  • Секреты в application.yml под системой контроля версий. Конфиг не хранилище секретов, им место в переменных окружения или отдельном сервисе.
  • Открыть Actuator целиком наружу. Это отдаёт переменные окружения с паролями и снимок памяти всем желающим.
  • @Async или @Cacheable на вызове через this. Прокси обойдён: работает синхронно и без кэша, молча.

Проверьте себя

Пять вопросов из банка по этой подтеме. Всего их 18, остальные разбираются в тренажёре.

  1. #spring_boot_aop1 / 5
    Из чего состоит аннотация @SpringBootApplication?
    A)Только из @ComponentScan — она лишь запускает сканирование пакетов и больше ничего не включает
    B)@Configuration + @EnableAutoConfiguration + @ComponentScan
    C)Из @RestController и @Service, поэтому главный класс сам является контроллером и сервисом сразу
    D)Из @Transactional и @EnableCaching — она глобально включает транзакции и кэш во всём приложении
    показать ответ и разбор
    +B)@Configuration + @EnableAutoConfiguration + @ComponentScan

    // разбор: @SpringBootApplication — удобная МЕТА-аннотация на главном классе, объединяющая три: @Configuration (класс — источник определений бинов), @EnableAutoConfiguration (включить автоконфигурацию Boot) и @ComponentScan (сканировать пакет главного класса и подпакеты на стереотипы). Отсюда правило: главный класс кладут в КОРНЕВОЙ пакет, чтобы скан охватил всё приложение. При необходимости их можно указать и по отдельности, тонко настроив сканирование или исключив автоконфигурации (exclude).

  2. #spring_boot_aop2 / 5
    Как переопределить значение свойства из application.yml для другого окружения?
    A)Свойства из application.yml зашиты намертво при сборке JAR и не могут быть изменены после неё никак
    B)Отредактировать значение прямо внутри собранного JAR-архива вручную перед каждым запуском на сервере
    C)Использовать профили (application-prod.yml) или переменные окружения/аргументы запуска
    D)Свойства можно менять только пересобрав приложение заново из исходников под каждое окружение
    показать ответ и разбор
    +C)Использовать профили (application-prod.yml) или переменные окружения/аргументы запуска

    // разбор: Boot поддерживает внешнюю конфигурацию и профили. Профиль-специфичные файлы application-{profile}.yml активируются через spring.profiles.active=prod и переопределяют базовый application.yml. Плюс есть строгий ПОРЯДОК источников (property source order): аргументы командной строки > переменные окружения (SPRING_DATASOURCE_URL) > профильные файлы > базовый файл > дефолты. Так одно и то же приложение (один JAR) настраивают под dev/stage/prod без пересборки — секреты и URL подают снаружи (12-factor). @Value/@ConfigurationProperties читают значения в бины.

  3. #spring_boot_aop3 / 5
    Что такое стартер (starter), например spring-boot-starter-web?
    A)Скрипт запуска приложения, который стартует встроенный сервер и открывает браузер на нужной странице
    B)Класс с методом main, генерируемый Boot автоматически, чтобы разработчику не писать точку входа
    C)Отдельный микросервис-загрузчик, который скачивает остальные модули приложения из сети при старте
    D)Набор согласованных зависимостей под задачу, подтягиваемых одной строкой
    показать ответ и разбор
    +D)Набор согласованных зависимостей под задачу, подтягиваемых одной строкой

    // разбор: Стартеры — «пакеты» согласованных зависимостей под типовую задачу: добавив один spring-boot-starter-web, вы транзитивно получаете Spring MVC, встроенный Tomcat, Jackson и всё нужное СОВМЕСТИМЫХ версий (версии управляет Boot BOM/родительский pom — не надо подбирать вручную). Есть starter-data-jpa, starter-security, starter-test и др. Вместе с автоконфигурацией это и даёт «подключил зависимость — заработало». Плюс встроенный сервер делает приложение самодостаточным исполняемым JAR (java -jar), без внешнего контейнера сервлетов.

  4. #spring_boot_aop4 / 5
    Что такое сквозная функциональность (cross-cutting concern) и как её решает AOP?
    A)Общая забота (логирование, транзакции, безопасность), вынесенная в аспект вместо дублирования
    B)Функциональность, которая работает сразу на нескольких серверах, требуя сетевого согласования между ними
    C)Код, пересекающий границы модулей компиляции, из-за чего его не получится разбить на отдельные классы
    D)Общий интерфейс, который обязаны реализовать все классы приложения, чтобы участвовать в контейнере
    показать ответ и разбор
    +A)Общая забота (логирование, транзакции, безопасность), вынесенная в аспект вместо дублирования

    // разбор: Сквозная функциональность — задача, нужная во МНОГИХ местах, но не относящаяся к бизнес-логике: логирование, транзакции, безопасность, метрики, кэширование. Без AOP её пришлось бы дублировать в каждом методе. AOP выносит её в АСПЕКТ: advice (что делать: before/after/around) применяется в точках, заданных pointcut (где: какие методы), — и внедряется прозрачно, не засоряя бизнес-код. Именно на AOP-прокси реализованы @Transactional, @Cacheable, @Async, Spring Security. Так забота описана в ОДНОМ месте.

  5. #spring_boot_aop5 / 5
    На чём основана работа @Transactional/@Cacheable в Spring (проксирование)?
    A)На модификации байткода самих классов компилятором javac прямо во время сборки проекта
    B)На AOP-прокси: вызовы бина идут через прокси, добавляющий поведение вокруг метода
    C)На отдельном фоновом потоке, который перехватывает и переписывает вызовы методов в рантайме
    D)На наследовании: аннотированные классы обязаны расширять специальный базовый класс TransactionalBean
    показать ответ и разбор
    +B)На AOP-прокси: вызовы бина идут через прокси, добавляющий поведение вокруг метода

    // разбор: Spring применяет декларативные аспекты через ПРОКСИ: при создании бина контейнер оборачивает его прокси-объектом (JDK dynamic proxy, если есть интерфейс, иначе CGLIB-подкласс). Внешние вызовы идут ЧЕРЕЗ прокси, который добавляет поведение (открыть/закоммитить транзакцию, проверить кэш) вокруг реального метода. Отсюда два важных следствия-ловушки: работает только для вызовов ИЗВНЕ (self-invocation минует прокси) и по умолчанию для PUBLIC-методов. Это не правка байткода (то — AspectJ compile/load-time weaving), а рантайм-обёртка.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.