Перейти к содержанию

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():

  1. Чтение определений бинов. ConfigurationClassPostProcessor разбирает @Configuration, @ComponentScan, @Import, @Bean и регистрирует BeanDefinition. Объектов бинов ещё нет.
  2. BeanFactoryPostProcessor - правят определения до создания бинов. Пример: подстановка ${...} в свойства.
  3. Регистрация BeanPostProcessor - обработчики, которые будут оборачивать каждый бин: @Autowired, @PostConstruct, AOP-прокси для @Transactional, @Async, @Cacheable.
  4. Инфраструктура контекста - MessageSource, рассылка событий.
  5. onRefresh() - в Spring Boot здесь создаётся встроенный Tomcat.
  6. Создание всех не-lazy singleton-бинов - на каждом проходит жизненный цикл бина.
  7. 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 - устаревший способ, встречается в старых проектах.

<bean id="myService" class="com.example.MyServiceImpl"/>

Стереотипы

Все они по сути @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:

  1. Создание - вызов конструктора, внедрение зависимостей через конструктор.
  2. Внедрение остального - сеттеры и поля с @Autowired, @Value.
  3. *Aware-интерфейсы - BeanNameAware, ApplicationContextAware и т.п.
  4. BeanPostProcessor.postProcessBeforeInitialization - здесь вызывается @PostConstruct.
  5. Инициализация - InitializingBean.afterPropertiesSet(), потом initMethod из @Bean(initMethod = ...).
  6. BeanPostProcessor.postProcessAfterInitialization - здесь бин может быть заменён прокси (AOP, @Transactional, @Async). В контейнер кладётся уже прокси.
  7. Работа.
  8. Уничтожение при закрытии контекста - @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 (выше - приоритетнее):

  1. ServletConfig - параметры сервлета (только web-контекст)
  2. ServletContext - <context-param> в web.xml
  3. JNDI
  4. JVM-параметры (-Dkey=value)
  5. переменные окружения
  6. файлы из @PropertySource

Note

В исходной версии заметки «пользовательские источники» стояли первыми. На деле @PropertySource добавляется в конец списка и проигрывает JVM-параметрам и переменным окружения. Чтобы источник перебивал остальные, его добавляют программно через ConfigurableEnvironment.getPropertySources().addFirst(...). В Spring Boot свой, более длинный порядок источников.