Spring Boot
Автоконфигурация и условные бины, Actuator, Spring MVC против WebFlux, цепочка фильтров Spring Security и централизованная обработка исключений в REST.
9 вопросов
JuniorТеорияОчень частоКакие три аннотации объединяет в себе @SpringBootApplication?
Какие три аннотации объединяет в себе @SpringBootApplication?
@SpringBootApplication — это мета-аннотация, объединяющая три: @SpringBootConfiguration (это @Configuration, помечающая класс источником определений бинов), @EnableAutoConfiguration (включает автоконфигурацию Boot) и @ComponentScan (сканирует собственный пакет класса и вложенные на предмет бинов). Одна аннотация на главном классе заменяет объявление всех трёх.
Типичные ошибки
- ✗Думать, что
@SpringBootApplicationобъединяет стереотипные аннотации вроде@Service - ✗Считать, что сканирование компонентов охватывает весь classpath, а не пакет главного класса и вложенные
- ✗Полагать, что автоконфигурация перекрывает бины, которые вы уже определили сами
Уточняющие вопросы
- →Почему расположение пакета главного класса важно для сканирования компонентов?
- →Как исключить конкретный класс автоконфигурации из применения?
JuniorТеорияЧастоЧто даёт Spring Boot Actuator и что из него по умолчанию доступно по HTTP?
Что даёт Spring Boot Actuator и что из него по умолчанию доступно по HTTP?
Actuator добавляет готовые management-эндпоинты под /actuator — health, metrics, env, loggers, threaddump — как только его starter появляется в classpath. Включён и открыт наружу — не одно и то же: включены все эндпоинты кроме shutdown, но по HTTP по умолчанию отдаётся только health, а остальные открываются через management.endpoints.web.exposure.include.
Типичные ошибки
- ✗Считать, что каждый эндпоинт Actuator по умолчанию доступен по HTTP
- ✗Путать включённость эндпоинта с его открытостью наружу по HTTP
- ✗Ждать, что
shutdownзаработает без явного включения
Уточняющие вопросы
- →Почему опасно открывать
envилиheapdumpна публичном порту? - →Как добавить собственный индикатор состояния в
/actuator/health?
MiddleДизайнЧастоВы ревьюите REST-сервис перед тем, как с ним начнёт интегрироваться внешний партнёр. Каждый контроллер оборачивает тело в try/catch, каждый возвращает свою форму JSON с ошибкой, сбой bean-валидации выходит как 500 со стектрейсом Hibernate в теле, а not-found из сервисного слоя приходит как 200 с пустым payload. Ревью требует единую форму ошибки на весь API, статус, соответствующий классу сбоя, отсутствие внутренних деталей в ответах и никакого дублирования обработки по контроллерам — при этом один унаследованный эндпоинт обязан сохранить старое тело ошибки ради клиента, которого не изменить. Как вы перестроите обработку исключений и где будет жить отображение исключения в статус?
Вы ревьюите REST-сервис перед тем, как с ним начнёт интегрироваться внешний партнёр. Каждый контроллер оборачивает тело в try/catch, каждый возвращает свою форму JSON с ошибкой, сбой bean-валидации выходит как 500 со стектрейсом Hibernate в теле, а not-found из сервисного слоя приходит как 200 с пустым payload. Ревью требует единую форму ошибки на весь API, статус, соответствующий классу сбоя, отсутствие внутренних деталей в ответах и никакого дублирования обработки по контроллерам — при этом один унаследованный эндпоинт обязан сохранить старое тело ошибки ради клиента, которого не изменить. Как вы перестроите обработку исключений и где будет жить отображение исключения в статус?
Контроллеры перестают ловить и просто бросают; один @RestControllerAdvice держит методы @ExceptionHandler, отображающие каждый класс исключения в статус и в единую форму тела (ProblemDetail): валидация в 400, not-found в 404, перехватывающий всё Exception в 500 с общим сообщением, а стектрейс уходит только в лог. Локальный @ExceptionHandler контроллера бьёт глобальный advice — так унаследованный эндпоинт сохраняет старое тело.
Типичные ошибки
- ✗Оставлять try/catch в каждом контроллере вместо того, чтобы бросать и обрабатывать централизованно
- ✗Возвращать 200 с телом-ошибкой вместо статуса, соответствующего сбою
- ✗Класть стектрейс или текст SQL в тело ответа, а не в лог
Уточняющие вопросы
- →Как Spring выбирает между двумя методами
@ExceptionHandler, подходящими одному исключению? - →Почему исключение, брошенное в сервлет-фильтре, не доходит до
@ControllerAdvice?
MiddleДизайнЧастоКоманда выбирает стек для нового пограничного сервиса перед релизом. Каждый входящий запрос веером уходит в четыре внешних HTTP API и возвращает агрегат; внешние сервисы отвечают за 200-800 мс, а сервис должен держать около 10000 запросов в полёте на небольшом контейнере. Остальная платформа — Spring MVC на Tomcat, один унаследованный вызов идёт через блокирующий JDBC-драйвер, который в этом квартале не заменить, и никто в команде не писал реактивный код в production. Вы построите сервис на Spring MVC или на Spring WebFlux? Объясните, что каждый из них делает с потоками под такой нагрузкой, и назовите то, что склонило бы решение в другую сторону.
Команда выбирает стек для нового пограничного сервиса перед релизом. Каждый входящий запрос веером уходит в четыре внешних HTTP API и возвращает агрегат; внешние сервисы отвечают за 200-800 мс, а сервис должен держать около 10000 запросов в полёте на небольшом контейнере. Остальная платформа — Spring MVC на Tomcat, один унаследованный вызов идёт через блокирующий JDBC-драйвер, который в этом квартале не заменить, и никто в команде не писал реактивный код в production. Вы построите сервис на Spring MVC или на Spring WebFlux? Объясните, что каждый из них делает с потоками под такой нагрузкой, и назовите то, что склонило бы решение в другую сторону.
MVC — поток на запрос: 10000 запросов в полёте требуют 10000 заблокированных потоков Tomcat, потолок задают память и переключение контекста. WebFlux обслуживает их несколькими потоками event loop, освобождая их на время I/O, — но лишь если вся цепочка неблокирующая (WebClient), ведь один блокирующий вызов JDBC на потоке event loop останавливает все запросы на нём. Без реактивного опыта и с этим драйвером безопаснее MVC на virtual threads.
Типичные ошибки
- ✗Ждать масштабирования от WebFlux, пока блокирующий вызов JDBC живёт на потоке event loop
- ✗Считать WebFlux простым ускорителем, а не сквозной моделью программирования
- ✗Верить, что реактивный
WebClientвнутри контроллера MVC освобождает поток Tomcat
Уточняющие вопросы
- →Как virtual threads меняют потолок модели поток-на-запрос в Spring MVC?
- →Как безопасно вызвать один блокирующий унаследованный API из обработчика WebFlux?
MiddleТеорияЧастоКак запрос проходит через цепочку фильтров Spring Security?
Как запрос проходит через цепочку фильтров Spring Security?
Spring Security встраивает один сервлет-фильтр (DelegatingFilterProxy, за ним FilterChainProxy) перед DispatcherServlet. FilterChainProxy берёт первую подходящую SecurityFilterChain и выполняет её упорядоченные фильтры: загрузку контекста, CsrfFilter, аутентификацию, затем AuthorizationFilter. Любой фильтр может оборвать запрос; результат кладётся в SecurityContextHolder, и отклонённый запрос не доходит до контроллера.
Типичные ошибки
- ✗Думать, что security отрабатывает после того, как
DispatcherServletуже выбрал обработчик - ✗Считать, что применяются все объявленные
SecurityFilterChain, а не первая подходящая - ✗Забывать, что именно порядок фильтров ставит аутентификацию перед авторизацией
Уточняющие вопросы
- →Почему исключение, брошенное внутри сервлет-фильтра, не доходит до
@ControllerAdvice? - →Что
ExceptionTranslationFilterделает сAccessDeniedException?
MiddleТеорияИногдаКак Spring Boot решает, будет ли класс автоконфигурации создавать свои бины?
Как Spring Boot решает, будет ли класс автоконфигурации создавать свои бины?
Классы автоконфигурации перечислены в AutoConfiguration.imports, и каждый закрыт условиями — @ConditionalOnClass, @ConditionalOnProperty, @ConditionalOnMissingBean — которые вычисляются при старте контекста, а не при сборке. Автоконфигурация применяется после вашей конфигурации, поэтому @ConditionalOnMissingBean уже видит ваш бин и отступает: побеждает ваше определение. --debug печатает отчёт об условиях.
Типичные ошибки
- ✗Думать, что автоконфигурация перекрывает бин, который вы определили сами
- ✗Считать, что условия проверяются при сборке, а не при старте контекста
- ✗Полагать, что starter в classpath всегда отдаёт свои бины, что бы ни говорили свойства
Уточняющие вопросы
- →Как исключить один конкретный класс автоконфигурации из контекста?
- →Почему порядок вашей конфигурации и автоконфигурации важен для
@ConditionalOnMissingBean?
SeniorДизайнИногдаКоманда раскололась в вопросе, как аутентифицировать API на Spring Boot теперь, когда он работает на восьми инстансах за балансировщиком. Сейчас выдаётся cookie с HttpSession в памяти, а балансировщик настроен на sticky sessions; при этом добавляются браузерный SPA и мобильный клиент. Один лагерь хочет stateless JWT, чтобы любой инстанс обслуживал любой запрос без общего хранилища. Другой напоминает, что безопасность утвердила правило: бан пользователя или смена пароля должны вступать в силу за секунды, а процессом ротации ключа подписи никто не владеет. Разберите компромисс: во что реально обходится каждый вариант здесь и что вы выпустите?
Команда раскололась в вопросе, как аутентифицировать API на Spring Boot теперь, когда он работает на восьми инстансах за балансировщиком. Сейчас выдаётся cookie с HttpSession в памяти, а балансировщик настроен на sticky sessions; при этом добавляются браузерный SPA и мобильный клиент. Один лагерь хочет stateless JWT, чтобы любой инстанс обслуживал любой запрос без общего хранилища. Другой напоминает, что безопасность утвердила правило: бан пользователя или смена пароля должны вступать в силу за секунды, а процессом ротации ключа подписи никто не владеет. Разберите компромисс: во что реально обходится каждый вариант здесь и что вы выпустите?
Где живёт состояние аутентификации, то и решает вопрос отзыва. Сессия держит его на сервере: отзыв — одно удаление в хранилище, мгновенно, но восьми инстансам нужно общее хранилище (Spring Session с Redis), а не sticky sessions. JWT проверяется по подписи на любом инстансе, но валиден до истечения срока, поэтому бан за секунды требует blocklist или коротких TTL — того самого хранилища. Выпускать общие сессии.
Типичные ошибки
- ✗Верить, что выданный JWT можно отозвать без какого-либо обращения к серверному хранилищу
- ✗Считать JWT зашифрованным, а не просто подписанным и читаемым клиентом
- ✗Считать sticky sessions единственным способом держать сессии более чем на одном инстансе
Уточняющие вопросы
- →Насколько коротким должен быть TTL access-токена, чтобы blocklist перестал быть нужен?
- →Что ломается у мобильного клиента, если держать аутентификацию в cookie?
SeniorДизайнИногдаПосле релиза запрос с заголовком тенанта то и дело возвращается как 403 ещё до того, как выполнится код контроллера, а аудит-лог — реализованный сервлет-фильтром — не пишет ни аутентифицированного пользователя, ни метод-обработчик, обслуживший запрос. Исключения из этого же фильтра тоже не доходят до @RestControllerAdvice команды и выходят наружу сырой страницей ошибки Tomcat. Пройдите путь запроса от коннектора встроенного Tomcat через Spring MVC до сериализованного JSON-ответа, скажите, за что отвечает каждая стадия, и разместите на основе этого четыре сквозные заботы — аутентификацию, разрешение тенанта, аудит и отображение ошибок — там, где им место.
После релиза запрос с заголовком тенанта то и дело возвращается как 403 ещё до того, как выполнится код контроллера, а аудит-лог — реализованный сервлет-фильтром — не пишет ни аутентифицированного пользователя, ни метод-обработчик, обслуживший запрос. Исключения из этого же фильтра тоже не доходят до @RestControllerAdvice команды и выходят наружу сырой страницей ошибки Tomcat. Пройдите путь запроса от коннектора встроенного Tomcat через Spring MVC до сериализованного JSON-ответа, скажите, за что отвечает каждая стадия, и разместите на основе этого четыре сквозные заботы — аутентификацию, разрешение тенанта, аудит и отображение ошибок — там, где им место.
Рабочий поток Tomcat собирает HttpServletRequest и прогоняет цепочку сервлет-фильтров — там живёт Spring Security, поэтому его 403 опережает контроллер. Цепочка заканчивается на DispatcherServlet: HandlerMapping находит обработчик и его interceptors, HandlerAdapter вызывает метод, HttpMessageConverter сериализует результат. @ControllerAdvice работает внутри DispatcherServlet, поэтому фильтр не знает обработчика, а его исключения идут мимо advice.
Типичные ошибки
- ✗Верить, что
@ControllerAdviceспособен поймать исключение из сервлет-фильтра - ✗Ждать, что фильтр увидит метод-обработчик или разрешённый
Authentication - ✗Путать фильтры (вне
DispatcherServlet) с interceptors (внутри него)
Уточняющие вопросы
- →На какой стадии разрешать тенанта, чтобы его видели и аудит, и контроллер?
- →Как
HandlerExceptionResolverдоводит брошенное исключение до@ControllerAdvice?
SeniorПроизводительностьРедкоСервис на Spring Boot стартует 40 с, и это ломает автомасштабирование. Что съедает это время и как его сократить?
Сервис на Spring Boot стартует 40 с, и это ломает автомасштабирование. Что съедает это время и как его сократить?
Сначала измерьте: /actuator/startup хронометрирует каждый шаг, а --debug печатает отчёт об условиях. Время уходит на сканирование classpath, вычисление условий кандидатов автоконфигурации и жадное создание синглтонов — значит, надо сокращать это множество: убрать лишние starters, сузить сканирование, исключить ненужные автоконфигурации. Ленивая инициализация лишь меняет старт на задержку первого запроса; AOT с CDS или native image переносит работу на сборку.
Типичные ошибки
- ✗Винить в медленном старте прогрев JVM, а не построение контекста
- ✗Считать ленивую инициализацию бесплатной, а не переносом стоимости на первый запрос
- ✗Оптимизировать без замеров, из-за чего доминирующие starters так и не находятся
Уточняющие вопросы
- →Что именно откладывает ленивая инициализация и что она ломает в рантайме?
- →Как AOT-обработка или native-сборка убирает вычисление условий из старта?