Spring Core¶
Spring Core - это IoC-контейнер: объект в памяти JVM, который создаёт бины, внедряет в них зависимости и управляет их жизненным циклом. Всё остальное в Spring (MVC, Data, транзакции, Security) - бины и пост-процессоры поверх него.
IoC-контейнер¶
IoC (Inversion of Control) - объекты не создают свои зависимости сами, их создаёт и связывает контейнер. DI (Dependency Injection) - способ IoC: контейнер передаёт зависимости через конструктор, сеттер или поле.
BeanFactory - базовый минимальный интерфейс контейнера.
- хранит метаданные бинов (
BeanDefinition) и создаёт экземпляры - делает DI (конструктор, сеттер, поле), если зарегистрирован нужный обработчик
- управляет скоупами и уничтожением бинов, умеет lazy-инициализацию
- сам почти ничего не делает: аннотации, автосвязывание, прокси и транзакции работают, только если вручную подключить соответствующие пост-процессоры
Важные подинтерфейсы и реализации:
ListableBeanFactory- перечисление бинов по типу и имениHierarchicalBeanFactory- поддержка родительской фабрикиAutowireCapableBeanFactory- программное автосвязывание, пост-процессорыDefaultListableBeanFactory- рабочая реализация, которой Spring пользуется под капотом
Напрямую с BeanFactory обычно не работают, но любой ApplicationContext содержит внутри DefaultListableBeanFactory.
ApplicationContext - контейнер Spring, который используют в реальных приложениях, включая Spring Boot.
Он наследует BeanFactory и одновременно реализует ApplicationEventPublisher, MessageSource,
ResourcePatternResolver и EnvironmentCapable. Это главный runtime-объект приложения, а не просто хранилище бинов.
Что добавляет поверх BeanFactory:
- Автоподключение инфраструктуры - сам находит и регистрирует все
BeanFactoryPostProcessorиBeanPostProcessor, поэтому@Autowired,@Configuration, AOP и транзакции работают без ручной настройки - События - публикация и подписка:
ApplicationEventPublisher,@EventListener - Ресурсы -
Resource,ResourcePatternResolverс префиксамиclasspath:,file:,classpath*: - i18n -
MessageSource, строки изmessages.properties - Окружение -
Environment,PropertySources, профили (см. Environment) - Pre-instantiate singletons - на
refresh()заранее создаёт все singleton-бины, кроме@Lazy. Ошибка конфигурации падает сразу при старте (fail-fast), а не на первом запросе
Итого: контейнер = BeanFactory, контекст = настроенный и запущенный контейнер + окружение приложения.
// ctx - и есть IoC-контейнер
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService service = ctx.getBean(MyService.class);
Реализации контекста - разные реализации ApplicationContext под разные задачи.
| Реализация | Когда |
|---|---|
AnnotationConfigApplicationContext |
Java- и аннотационный конфиг без веба: консольные утилиты, standalone |
ClassPathXmlApplicationContext, FileSystemXmlApplicationContext |
XML-конфиг, старые проекты |
WebApplicationContext (интерфейс) |
Контекст, привязанный к ServletContext: веб-скоупы, сервлет-окружение, работает вместе с DispatcherServlet |
AnnotationConfigServletWebServerApplicationContext |
Что поднимает Spring Boot для servlet-приложения. Руками его не создают, SpringApplication.run(...) возвращает ConfigurableApplicationContext |
Классическая Spring MVC без Boot держит два контекста:
- Root WebApplicationContext - уровень приложения: репозитории, сервисы, безопасность
- Контекст DispatcherServlet - отдельный
WebApplicationContextдля веб-слоя: контроллеры, view resolvers. Root для него - parent, поэтому контроллеры видят сервисы, а сервисы контроллеры нет
В Spring Boot контекст один.
Как стартует контекст¶
Контейнер создают одним из способов:
// 1) Java-конфиг
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// 2) XML (наследие)
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
// 3) Spring Boot - контекст создаётся и запускается сам
SpringApplication.
run(Application .class, args);
Дальше всё делает AbstractApplicationContext.refresh():
- Чтение определений бинов.
ConfigurationClassPostProcessorразбирает@Configuration,@ComponentScan,@Import,@Beanи регистрируетBeanDefinition. Объектов бинов ещё нет. BeanFactoryPostProcessor- правят определения до создания бинов. Пример: подстановка${...}в свойства.- Регистрация
BeanPostProcessor- обработчики, которые будут оборачивать каждый бин:@Autowired,@PostConstruct, AOP-прокси для@Transactional,@Async,@Cacheable. - Инфраструктура контекста -
MessageSource, рассылка событий. onRefresh()- в Spring Boot здесь создаётся встроенный Tomcat.- Создание всех не-lazy singleton-бинов - на каждом проходит жизненный цикл бина.
finishRefresh()- публикуетсяContextRefreshedEvent, Boot стартует веб-сервер и шлётApplicationReadyEvent.
Итого: сначала определения, потом пост-процессоры, потом сами бины. Поэтому BeanFactoryPostProcessor не должен
зависеть от обычных бинов: их ещё нет.
Бины¶
Бин - объект, который создал контейнер и которым он управляет.
Как объявить бин¶
Component scan - класс помечают стереотипом, контейнер находит его при сканировании пакетов.
@Configuration
@ComponentScan(basePackages = "com.example") // где искать @Component и производные
public class AppConfig {
}
Java config - метод с @Bean возвращает объект, контейнер регистрирует результат как бин.
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
XML - устаревший способ, встречается в старых проектах.
Стереотипы¶
Все они по сути @Component. Для контейнера это одна сущность, метки нужны для читаемости, а у части есть своё
поведение.
@Component- обычный бин@Service- сервисный слой, бизнес-логика@Repository- DAO и репозитории. Дополнительно: исключения JDBC/JPA переводятся вDataAccessException(черезPersistenceExceptionTranslationPostProcessor)@Controller- MVC-контроллер, возвращает view и модель@RestController-@Controller+@ResponseBody, возвращает JSON/XML (см. Контроллеры)@Configuration- класс с@Bean-методами
@Configuration и @Bean¶
@Configuration - класс с конфигурацией, он заменяет XML. Каждый @Bean-метод внутри объявляет бин.
@Bean - метод-фабрика бина. Контейнер вызывает его при старте, а результат регистрирует как бин. Дальше этот
бин внедряется в другие через конструктор, @Autowired или сеттер.
- метод обязан что-то вернуть: возвращённый объект и есть бин
- параметры метода контейнер внедряет сам:
@Bean DataSource ds(Environment env) - по умолчанию бин - singleton, метод отрабатывает один раз
- имя бина - имя метода
Full и lite режим. @Bean можно объявить и в обычном @Component, но поведение отличается:
- В
@Configuration(full, по умолчаниюproxyBeanMethods = true) класс оборачивается CGLIB-прокси. Вызов одного@Bean-метода из другого возвращает тот же singleton из контейнера. - В
@Componentили в@Configuration(proxyBeanMethods = false)(lite) прокси нет. Прямой вызов@Bean-метода - обычный вызов Java, он создаст новый объект мимо контейнера.
@Configuration
public class AppConfig {
@Bean
public Repo repo() {
return new Repo();
}
@Bean
public Service service() {
return new Service(repo()); // в full-режиме - тот же бин repo, в lite - новый Repo
}
}
Итого: @Bean-методы, которые зовут друг друга, держи в @Configuration. В lite-режиме зависимость бери
параметром метода.
Внедрение зависимостей¶
Через конструктор - способ по умолчанию. С Spring 4.3 при единственном конструкторе @Autowired писать не надо.
Поля можно сделать final, бин нельзя создать без зависимостей, его легко тестировать без контейнера.
@Service
public class OrderService {
private final OrderRepository repository;
public OrderService(OrderRepository repository) { // @Autowired не нужен
this.repository = repository;
}
}
Через сеттер - для необязательных зависимостей, метод помечают @Autowired.
Через поле - @Autowired на поле. Работает, но поле нельзя сделать final, зависимость скрыта, без контейнера
объект не собрать. В новом коде не используют.
@Autowired - внедрение бина по типу.
- работает на полях, конструкторах и сеттерах классов, которыми управляет Spring
- если подходящих бинов несколько, берётся помеченный
@Primary, иначе нужно уточнить@Qualifier("имя") - если не нашлось ни одного - ошибка старта. Для необязательной зависимости:
@Autowired(required = false),Optional<T>илиObjectProvider<T>
@Primary - бин по умолчанию среди нескольких бинов одного типа.
@Qualifier - выбор конкретного бина по имени или по своей аннотации-квалификатору.
@Resource (Jakarta) - внедрение сначала по имени (по умолчанию имя поля), потом по типу. Поддерживает JNDI-ресурсы сервера приложений.
@Resource(name = "myBean")
private MyService service;
@Resource // JNDI-ресурс, если приложение развёрнуто на Jakarta EE сервере
private DataSource dataSource;
@Inject, @Named (JSR-330, jakarta.inject) - стандартные аналоги @Autowired и @Qualifier, Spring их тоже
понимает.
Scope¶
Scope - сколько экземпляров бина создаёт контейнер и как долго они живут. Задаётся @Scope.
| Scope | Экземпляр | Где |
|---|---|---|
singleton |
Один на весь ApplicationContext. По умолчанию, так живут 99% бинов |
Везде |
prototype |
Новый при каждом getBean() и каждом внедрении. Удобно для одноразовых объектов |
Везде |
request |
Один на HTTP-запрос | Web |
session |
Один на HTTP-сессию | Web |
application |
Один на ServletContext, то есть на всё веб-приложение |
Web |
websocket |
Один на WebSocket-сессию | WebSocket |
Можно объявить свой scope через CustomScopeConfigurer.
Warning
Prototype внутри singleton создаётся один раз - при создании singleton. Чтобы получать новый экземпляр на каждый
вызов, внедряй ObjectProvider<T> и вызывай getObject(). Request- и session-бины в singleton внедряют через
scoped proxy: @RequestScope и @SessionScope уже включают его.
У контейнера нет доступа к уничтожению prototype-бина: он создал объект и отдал, @PreDestroy не вызовется.
Жизненный цикл бина¶
Контейнер ведёт бин от создания до уничтожения. Шаги для singleton:
- Создание - вызов конструктора, внедрение зависимостей через конструктор.
- Внедрение остального - сеттеры и поля с
@Autowired,@Value. *Aware-интерфейсы -BeanNameAware,ApplicationContextAwareи т.п.BeanPostProcessor.postProcessBeforeInitialization- здесь вызывается@PostConstruct.- Инициализация -
InitializingBean.afterPropertiesSet(), потомinitMethodиз@Bean(initMethod = ...). BeanPostProcessor.postProcessAfterInitialization- здесь бин может быть заменён прокси (AOP,@Transactional,@Async). В контейнер кладётся уже прокси.- Работа.
- Уничтожение при закрытии контекста -
@PreDestroy,DisposableBean.destroy(),destroyMethod.
@Component
public class Cache {
@PostConstruct
void warmUp() { /* зависимости уже внедрены, можно грузить данные */ }
@PreDestroy
void flush() { /* контекст закрывается */ }
}
Итого: логику инициализации ставь в @PostConstruct, а не в конструктор, если ей нужны зависимости из полей.
Прокси появляется только на шаге 6, поэтому вызов @Transactional-метода изнутри @PostConstruct идёт мимо прокси
(см. транзакции).
Environment, профили и свойства¶
Environment - абстракция окружения в Spring Core. Через неё читают свойства из всех источников: переменных окружения, JVM-параметров, файлов свойств.
Профиль - именованный набор бинов и настроек. Бины с @Profile("dev") попадают в контекст, только если профиль
dev активен (spring.profiles.active=dev).
@PropertySource - добавляет файл свойств в Environment. Ставится на @Configuration-класс. Значения потом читают
через Environment или @Value.
@Configuration
@PropertySource("classpath:mail.properties")
public class MailConfig {
private final Environment environment;
public MailConfig(Environment environment) {
this.environment = environment;
}
public String mailDebug() {
return environment.getProperty("mail.debug");
}
}
@Value - внедрение одного свойства: @Value("${application.security.jwt.secret-key}") String secretKey. Для группы
свойств в Spring Boot удобнее @ConfigurationProperties (см. Spring Boot).
Порядок источников в веб-приложении без Boot (выше - приоритетнее):
ServletConfig- параметры сервлета (только web-контекст)ServletContext-<context-param>вweb.xml- JNDI
- JVM-параметры (
-Dkey=value) - переменные окружения
- файлы из
@PropertySource
Note
В исходной версии заметки «пользовательские источники» стояли первыми. На деле @PropertySource добавляется в конец
списка и проигрывает JVM-параметрам и переменным окружения. Чтобы источник перебивал остальные, его добавляют
программно через ConfigurableEnvironment.getPropertySources().addFirst(...). В Spring Boot свой, более длинный
порядок источников.