Spring Flashcards
(14 cards)
✅ Что такое IoC и DI? Как они реализованы в Spring?
1. Проблема
В традиционном Java-коде управление зависимостями (создание и связывание объектов) выполняется вручную:
class OrderService { private PaymentService paymentService = new PaymentService(); // Жёсткая связь }
Недостатки:
- Тесная связанность (tight coupling), усложняющая тестирование и замену реализаций.
- Нарушение принципа Single Responsibility (класс управляет не только логикой, но и своими зависимостями).
2. Решение → Inversion of Control (IoC) и Dependency Injection (DI)
IoC — принцип инверсии управления, при котором контейнер (Spring) управляет жизненным циклом объектов и их зависимостями.
DI — частный случай IoC, реализующий внедрение зависимостей.
Как реализовано в Spring:
1. Контейнер IoC:
- ApplicationContext
— центральный интерфейс, управляющий бинами.
- Бины объявляются через аннотации (@Component
, @Service
) или конфигурации (@Bean
).
-
Способы внедрения зависимостей:
-
Через конструктор (рекомендуется для неизменяемых зависимостей):
```
@Service
public class OrderService {
private final PaymentService paymentService;@Autowired // Опционально (начиная с Spring 4.3) public OrderService(PaymentService paymentService) { this.paymentService = paymentService; } } ```
- Через сеттеры/поля (для опциональных зависимостей).
-
Через конструктор (рекомендуется для неизменяемых зависимостей):
-
Типы бинов:
- Singleton (по умолчанию), Prototype, Request, Session.
3. Пример/Опыт
Кейс из практики:
В проекте с микросервисной архитектурой нужно было обеспечить замену реализации PaymentService
для разных окружений (например, MockPaymentService
для тестов).
Решение:
1. Использовали DI через конструктор:
```
@Service
public class OrderService {
private final PaymentService paymentService;
public OrderService(PaymentService paymentService) { this.paymentService = paymentService; } } ``` 2. Настроили конфигурацию для подмены реализации: ``` @Profile("test") @Configuration public class TestConfig { @Bean public PaymentService mockPaymentService() { return new MockPaymentService(); } } ``` **Результат:** - Упростили тестирование (Mockito легко подменяет зависимости). - Избежали жёсткой связности — переход на новую платежную систему (например, Stripe → PayPal) потребовал только изменения конфигурации.
4. Важные детали для Senior
- Как работает под капотом:
Spring создает прокси для бинов, управляя их жизненным циклом через BeanFactory
. Для DI используется Reflection + CGLIB (если нет интерфейсов).
- Потенциальные проблемы:
- Циклические зависимости (решение: @Lazy
или рефакторинг).
- Неоднозначность бинов (решение: @Qualifier
).
Итог:
IoC/DI в Spring — это не просто «аннотации для внедрения зависимостей», а архитектурный подход, который:
✅ Уменьшает связанность.
✅ Упрощает тестирование.
✅ Позволяет гибко конфигурировать систему.
✅ Что такое Spring Context? Как он создается?
Проблема
В enterprise-разработке ручное управление зависимостями и жизненным циклом объектов становится непрактичным при масштабировании. Возникают сложности с:
- Тестированием компонентов
- Управлением конфигурацией
- Внедрением зависимостей
- Поддержкой различных окружений
Решение
Spring Context (ApplicationContext) — центральный контейнер IoC, который:
1. Управляет созданием и жизненным циклом бинов
2. Обеспечивает механизм внедрения зависимостей
3. Предоставляет единую точку конфигурации приложения
4. Обрабатывает события и профили
Создание контекста
1. Загрузка конфигурации:
- Через аннотации (@Configuration, @ComponentScan)
- Через XML (в legacy-проектах)
- Программно через Java Config
- Инициализация:
- Сканирование компонентов
- Построение графа зависимостей
- Решение конфликтов (@Primary, @Qualifier)
- Постобработка:
- Применение BeanPostProcessor
- Инициализация прокси (AOP, транзакции)
- Вызов callback-методов (@PostConstruct)
Опыт
В распределенной системе с 50+ микросервисами мы столкнулись с проблемой медленного старта из-за монолитного контекста. Решение включало:
1. Разделение на иерархию контекстов
2. Ленивую инициализацию неключевых компонентов
3. Оптимизацию @ComponentScan
В результате время запуска уменьшилось на 65%, а потребление памяти — на 40%.
Ключевые аспекты для Senior
1. Типы контекстов:
- AnnotationConfigApplicationContext
- ClassPathXmlApplicationContext
- WebApplicationContext
- Жизненный цикл:
- refresh() → создание бинов → post-processing → готовность
- close() → уничтожение бинов
- Производительность:
- Влияние количества бинов на время инициализации
- Оптимизация через ленивую загрузку
- Кеширование конфигурации
- Расширяемость:
- Кастомные BeanFactoryPostProcessor
- Программная регистрация бинов
- Динамическое обновление конфигурации
Понимание внутренней работы Spring Context критически важно для проектирования масштабируемых Spring-приложений и решения сложных проблем в production-средах.
✅ Что такое BeanPostProcessor?
Проблема
В сложных Spring-приложениях стандартного механизма внедрения зависимостей часто недостаточно, когда требуется:
- Модифицировать экземпляры бинов после их создания
- Добавлять дополнительное поведение к определенным классам
- Реализовывать кастомную логику инициализации
- Интегрировать сторонние библиотеки с Spring-контейнером
Решение
BeanPostProcessor — это ключевой расширяемый интерфейс Spring, позволяющий вмешиваться в процесс создания бинов. Он предоставляет две точки расширения:
1. postProcessBeforeInitialization — вызывается после создания экземпляра, но до вызова методов инициализации
2. postProcessAfterInitialization — вызывается после завершения всей инициализации бина
Опыт
В высоконагруженном микросервисе потребовалось реализовать автоматическое логирование вызовов методов для компонентов, помеченных специальной аннотацией. Использование BeanPostProcessor позволило:
- Создать динамические прокси для нужных бинов
- Добавить унифицированное логирование без модификации исходного кода
- Сохранить производительность за счет избирательного применения только к аннотированным классам
Ключевые аспекты для Senior
- BeanPostProcessor лежит в основе многих встроенных возможностей Spring (AOP, транзакции, кэширование)
- Порядок выполнения процессоров критически важен и контролируется через Ordered/PriorityOrdered
- Чрезмерное использование может негативно сказаться на производительности при старте приложения
- Альтернативы для сложных сценариев: BeanFactoryPostProcessor, FactoryBean, AspectJ
Практическое применение
- Создание AOP-прокси
- Реализация кастомных аннотаций
- Интеграция сторонних библиотек
- Добавление перехватчиков вызовов
- Валидация бинов после инициализации
Понимание работы BeanPostProcessor необходимо для создания сложных расширений Spring и диагностики проблем в enterprise-приложениях.
✅ Опишите жизненный цикл бина в Spring.
Spring Framework обеспечивает строго определенный жизненный цикл для управляемых бинов, состоящий из нескольких ключевых этапов:
-
Инстанциирование
Контейнер создает экземпляр бина через конструктор по умолчанию или указанный фабричный метод. Для singleton-бинов это происходит при запуске контекста, для prototype - по требованию. -
Популяция свойств
Spring внедряет зависимости через:
- Автовайринг (@Autowired)
- Явное указание (@Resource, @Value)
- XML-конфигурацию (в legacy-системах) -
Предварительная инициализация
- Вызов Aware-интерфейсов (BeanNameAware, ApplicationContextAware)
- Обработка BeanPostProcessor.postProcessBeforeInitialization -
Инициализация
Выполняется в строгом порядке:
1) Методы с @PostConstruct
2) afterPropertiesSet() (из InitializingBean)
3) Кастомные init-методы (указанные в initMethod @Bean) -
Использование
Полностью инициализированный бин готов к работе. Может быть:
- Проксирован для AOP
- Инжектирован в другие компоненты
- Использован для обработки бизнес-логики -
Уничтожение (только для singleton)
Перед закрытием контекста:
1) @PreDestroy методы
2) destroy() (из DisposableBean)
3) Кастомные destroy-методы (destroyMethod в @Bean)
Ключевые моменты для Senior-разработчика:
-
Порядок выполнения критичен для:
- Транзакционных прокси
- Кеширующих механизмов
- Интеграций с внешними системами -
Особенности scope:
- prototype-бины не получают destroy-хуков
- request/session-бины имеют дополнительный жизненный цикл -
Производительность:
- Тяжелые init-методы замедляют старт
- Избыточные BeanPostProcessor влияют на время инициализации -
Обработка ошибок:
- Исключения на этапе инициализации приводят к недоступности бина
- Проблемы при уничтожении могут вызвать утечки ресурсов -
Расширяемость:
- BeanPostProcessor позволяют модифицировать стандартное поведение
- CustomScope обеспечивает гибкое управление жизненным циклом
Глубокое понимание этих механизмов необходимо для построения надежных enterprise-приложений и решения сложных проблем в production-средах.
Что такое @Profile? Как его использовать?
✅ Почему рекомендуется внедрение бина через конструктор?
Проблема 1:
Неявные зависимости и невалидное состояние объекта
При использовании сеттеров или field injection зависимости становятся опциональными - класс может быть создан без обязательных зависимостей. Это приводит к:
- Runtime ошибкам (NullPointerException), когда код пытается использовать неинициализированную зависимость
- Нарушению инвариантов класса, так как объект может существовать в частично инициализированном состоянии
- Сложностям в понимании, какие зависимости действительно необходимы для работы класса
- Проблемам при рефакторинге, так как зависимости не видны в публичном API класса
Проблема 2:
Нарушение принципов иммутабельности и thread-safety
Field injection и сеттеры:
- Не позволяют объявлять зависимости как final, что делает объект изменяемым
- Создают риск перезаписи зависимостей после инициализации объекта
- Усложняют работу в многопоточной среде, так как зависимости могут быть заменены в runtime
- Нарушают принцип “закрытости для модификации” (Open-Closed principle), позволяя изменять внутреннее состояние объекта после создания
Решение
Внедрение через конструктор решает эти проблемы следующим образом:
1. Явно декларирует все обязательные зависимости в сигнатуре конструктора
2. Гарантирует, что объект будет создан в валидном состоянии
3. Гарантирует полную инициализацию объекта перед использованием
4. Позволяет делать зависимости final, обеспечивая неизменяемость
4. Упрощает тестирование (можно передавать моки напрямую)
Опыт
В крупном проекте с микросервисной архитектурой мы столкнулись с проблемами:
1. Сервисы могли работать с частично инициализированными зависимостями
2. Возникали NPE в runtime из-за неявных зависимостей
3. Тесты были хрупкими и сложными в поддержке
После перехода на внедрение через конструктор:
1. Код стал более предсказуемым и надежным
2. Упростилось тестирование (на 40% сократилось количество mock-объектов)
3. Улучшилась читаемость и поддерживаемость кода
4. Исчезли runtime-ошибки, связанные с зависимостями
Ключевые аспекты для Senior
1. Thread-safety: final-поля, инициализированные через конструктор, безопасны для многопоточной среды
2. Циклические зависимости: легче выявляются на этапе компиляции
3. Совместимость с современными подходами: хорошо работает с реактивными стеками и immutable-объектами
4. Инверсия управления: явно показывает зависимость класса от абстракций, а не реализаций
5. Интеграция с фреймворками: Spring (начиная с 4.3) и другие DI-контейнеры оптимально работают с constructor injection
Constructor injection стал де-факто стандартом в современной Spring-разработке, так как обеспечивает надежность, предсказуемость и удобство поддержки enterprise-приложений.
✅ Что такое Spring Boot? Чем он отличается от Spring Framework?
Проблема
Разработка на чистом Spring Framework требует значительных усилий по настройке инфраструктуры:
- Ручное объявление бинов в XML или JavaConfig
- Необходимость явного подключения совместимых версий зависимостей
- Трудоемкая настройка сервера приложений и интеграция с ним
- Большое количество шаблонного кода для стандартных задач
Решение
Spring Boot — это расширение Spring Framework, которое:
- Предоставляет готовые конфигурации (auto-configuration) на основе анализа classpath
- Включает встроенные серверы (Tomcat, Jetty, Undertow)
- Упрощает подключение зависимостей через starter-пакеты
- Добавляет production-фичи: мониторинг, health checks, externalized configuration
Ключевые отличия
-
Подход к конфигурации
-
Spring Framework: Требует явного объявления бинов через XML, аннотации (
@Bean
,@ComponentScan
) или JavaConfig. Разработчик должен вручную настраивать инфраструктурные компоненты (DataSource, TransactionManager и др.). -
Spring Boot: Использует автоматическую конфигурацию (
@EnableAutoConfiguration
), которая активирует бины на основе:- Наличия классов в classpath (например,
DataSource
→ автоматически настраиваетJdbcTemplate
) - Свойств в
application.properties
/application.yml
- Условий (
@ConditionalOnClass
,@ConditionalOnProperty
)
- Наличия классов в classpath (например,
-
Spring Framework: Требует явного объявления бинов через XML, аннотации (
-
Управление зависимостями
- Spring Framework: Разработчик самостоятельно подбирает совместимые версии библиотек (Spring MVC, Hibernate, Jackson и др.), что может приводить к конфликтам.
-
Spring Boot: Предоставляет starter-пакеты (например,
spring-boot-starter-web
,spring-boot-starter-data-jpa
), которые:- Автоматически подключают совместимые версии всех зависимостей
- Включают готовые настройки для типовых сценариев (например, embedded сервер + Spring MVC)
-
Запуск и развертывание
- Spring Framework: Приложение требует внешнего сервера приложений (Tomcat, WildFly) и деплоится как WAR.
-
Spring Boot:
- Создает самодостаточные (self-contained) JAR-файлы со встроенным сервером
- Предоставляет
SpringApplication
для удобного запуска (main()
метод) - Поддерживает executable JAR для production-развертывания
-
Работа с окружением
-
Spring Framework: Настройки окружения (профили, свойства) требуют ручной конфигурации (например, через
PropertyPlaceholderConfigurer
). -
Spring Boot:
- Упрощает управление конфигурацией через
application.properties
/application.yml
- Поддерживает профили (
spring.profiles.active
) - Позволяет переопределять настройки через внешние источники (env vars, VM options, config серверы)
- Упрощает управление конфигурацией через
-
Spring Framework: Настройки окружения (профили, свойства) требуют ручной конфигурации (например, через
-
Готовые решения для production
- Spring Framework: Нет встроенных инструментов для мониторинга и администрирования.
-
Spring Boot: Добавляет
Actuator
— REST-эндпоинты для:- Проверки здоровья приложения (
/actuator/health
) - Просмотра метрик (
/actuator/metrics
) - Анализа автоматических конфигураций (
/actuator/conditions
)
- Проверки здоровья приложения (
Опыт
При переходе с Spring Framework на Spring Boot в enterprise-проекте:
- Время настройки инфраструктуры сократилось на 60-70% за счет auto-configuration
- Количество конфигурационных файлов уменьшилось в 3 раза
- Упростился процесс CI/CD благодаря единому формату исполняемых JAR
- Стандартизировались подходы к мониторингу (Actuator + Prometheus/Grafana)
Вывод
Spring Boot не заменяет Spring Framework, а дополняет его, устраняя рутинные задачи и ускоряя разработку. Однако для сложных кастомизаций важно понимать, как работают его механизмы (например, @Conditional
, порядок загрузки автоконфигураций).
Как создать кастомный Starter?
https://struchkov.dev/blog/ru/create-spring-boot-starter
https://www.baeldung.com/spring-boot-custom-starter
Проблема
1. В распределенных системах с множеством микросервисов повторяются одинаковые интеграции
2. Ручная настройка в каждом сервисе приводит к:
- Несогласованным конфигурациям
- Тонким ошибкам
- Трудностям масштабирования
3. Изменения требований требуют правок во всех сервисах
4. Новые разработчики тратят время на изучение нюансов вместо бизнес-логики
Решение
1. Инкапсуляция всей конфигурации в reusable компонент
2. Автоматическая настройка по best practices
3. Единый подход для:
- Базовой конфигурации
- Retry-логики
- Circuit breakers
- Сбора метрик
4. Гибкая настройка через application.properties
5. Соответствие принципам Spring Boot AutoConfiguration
Практический опыт
1. Кейс: интеграция 30+ микросервисов с платежной системой
2. Реализация:
- Pre-configured REST client
- Resilience4j для retry/circuit breaker
- Кастомные Prometheus метрики
- Детализированное логирование
3. Результаты:
- Время подключения сократилось с дней до 20 минут
- Централизованное управление изменениями
- Устранение класса ошибок
- Экономия сотен часов работы
Ключевые преимущества
1. Стандартизация подходов
2. Ускорение разработки
3. Упрощение поддержки
4. Гибкость конфигурации
5. Соблюдение best practices
Что такое N+1 проблема? Как ее решить (@EntityGraph, FETCH JOIN)?
Как работают @Transactional, Propagation и Isolation? Как работает @Transactional под капотом?
Как работает аутентификация и авторизация в Spring Security?
Какие есть способы аутентификации (Basic, JWT, OAuth2)?
Как реализовать кастомный SecurityFilter?
Как реализовать distributed transactions (Saga, Outbox Pattern)?