Spring Boot и AOP: автоконфигурация и прокси
Собрал автоконфигурацию с условием «создавай мой бин, только если пользователь не объявил свой». Без своего бина в контексте оказался автоматический. Со своим - только мой, всего бинов этого типа один. Правило вытеснения работает.
А потом я перенёс то же самое условие в обычный класс с @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, остальные разбираются в тренажёре.
- Из чего состоит аннотация @SpringBootApplication?A)Только из @ComponentScan — она лишь запускает сканирование пакетов и больше ничего не включаетB)@Configuration + @EnableAutoConfiguration + @ComponentScanC)Из @RestController и @Service, поэтому главный класс сам является контроллером и сервисом сразуD)Из @Transactional и @EnableCaching — она глобально включает транзакции и кэш во всём приложении
показать ответ и разбор
+B)@Configuration + @EnableAutoConfiguration + @ComponentScan// разбор: @SpringBootApplication — удобная МЕТА-аннотация на главном классе, объединяющая три: @Configuration (класс — источник определений бинов), @EnableAutoConfiguration (включить автоконфигурацию Boot) и @ComponentScan (сканировать пакет главного класса и подпакеты на стереотипы). Отсюда правило: главный класс кладут в КОРНЕВОЙ пакет, чтобы скан охватил всё приложение. При необходимости их можно указать и по отдельности, тонко настроив сканирование или исключив автоконфигурации (exclude).
- Как переопределить значение свойства из 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 читают значения в бины.
- Что такое стартер (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), без внешнего контейнера сервлетов.
- Что такое сквозная функциональность (cross-cutting concern) и как её решает AOP?A)Общая забота (логирование, транзакции, безопасность), вынесенная в аспект вместо дублированияB)Функциональность, которая работает сразу на нескольких серверах, требуя сетевого согласования между нимиC)Код, пересекающий границы модулей компиляции, из-за чего его не получится разбить на отдельные классыD)Общий интерфейс, который обязаны реализовать все классы приложения, чтобы участвовать в контейнере
показать ответ и разбор
+A)Общая забота (логирование, транзакции, безопасность), вынесенная в аспект вместо дублирования// разбор: Сквозная функциональность — задача, нужная во МНОГИХ местах, но не относящаяся к бизнес-логике: логирование, транзакции, безопасность, метрики, кэширование. Без AOP её пришлось бы дублировать в каждом методе. AOP выносит её в АСПЕКТ: advice (что делать: before/after/around) применяется в точках, заданных pointcut (где: какие методы), — и внедряется прозрачно, не засоряя бизнес-код. Именно на AOP-прокси реализованы @Transactional, @Cacheable, @Async, Spring Security. Так забота описана в ОДНОМ месте.
- На чём основана работа @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), а рантайм-обёртка.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.