Spring построен на двух идеях: Inversion of Control (вы не создаёте объекты сами, это делает контейнер) и Dependency Injection (контейнер связывает объекты, передавая зависимости в конструктор/поле/сеттер). Всё остальное в Spring — надстройки над этим.
ApplicationContext и BeanFactory
BeanFactory — базовый интерфейс контейнера, читает определения бинов и создаёт их по запросу. На практике с ним напрямую не работают.
ApplicationContext — это BeanFactory плюс события, ресурсы (i18n), Environment (свойства), MessageSource. Это то, что Spring Boot создаёт при старте.
@SpringBootApplication
public class App {
public static void main(String[] args) {
ApplicationContext ctx = SpringApplication.run(App.class, args);
OrderService orderService = ctx.getBean(OrderService.class);
}
}
Под капотом SpringApplication.run создаёт AnnotationConfigServletWebServerApplicationContext (или reactive-вариант), сканирует пакеты на @Component, регистрирует BeanDefinition-ы, инстанцирует синглтоны, прогоняет жизненный цикл.
Жизненный цикл бина — краткий обзор
Восемь фаз от создания до уничтожения: instantiation → DI → Aware-колбэки → BeanPostProcessor.before → initialization (@PostConstruct, afterPropertiesSet, init-method) → BeanPostProcessor.after (создание AOP-прокси) → ready → destruction.
Главное практическое следствие: в конструкторе нельзя пользоваться @Autowired-полями — они ещё не внедрены. В @PostConstruct — можно. Поэтому правило — внедряем через конструктор (final, без @Autowired). Это нативно для immutability, для тестов (можно создать вручную) и убирает race conditions при инициализации.
Детальный разбор каждой фазы с рабочим демо-кодом, выводом в консоль, ловушками (NPE в конструкторе, @Transactional в @PostConstruct, graceful shutdown) — в отдельной статье: Жизненный цикл Spring-бина с примерами.
Скоупы
Шесть встроенных, чаще всего применяются два.
singleton (default) — один экземпляр на весь ApplicationContext. Этим живут 95% бинов: @Service, @Repository, @Controller.
prototype — новый экземпляр на каждый getBean() / каждое внедрение. Spring не управляет их жизненным циклом после создания: @PreDestroy не вызывается. Применяется, когда нужны короткоживущие объекты с дополнительной инфраструктурой Spring (например, JmsListenerEndpointRegistrar).
Особенность: prototype-бин внутри singleton-бина по умолчанию внедряется один раз (при создании singleton'а). Чтобы каждый вызов получал свежий экземпляр — нужен ObjectProvider или @Lookup:
@Service
public class OrderService {
private final ObjectProvider<RequestContext> contextProvider; // prototype
public OrderService(ObjectProvider<RequestContext> p) { this.contextProvider = p; }
public void process() {
RequestContext ctx = contextProvider.getObject(); // новый экземпляр каждый вызов
}
}
request / session / application / websocket — для веб-приложений, привязка к веб-области.
@Configuration vs @Component
Внешне похожи — оба регистрируют бины. Под капотом разные.
@Component (а также @Service, @Repository, @Controller) — простой класс, который Spring инстанцирует. Между методами этого класса Spring не вмешивается.
@Configuration — класс с @Bean-методами. Spring проксирует его через CGLIB, чтобы при вызове одного @Bean-метода из другого возвращался уже созданный синглтон, а не новый объект:
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(...);
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // тот же DataSource, не новый
}
}
Если заменить @Configuration на @Component, jdbcTemplate() будет вызывать dataSource() напрямую, получая новый объект каждый раз. Для baseline-инфраструктуры это критично.
Альтернатива — lite mode через proxyBeanMethods = false:
@Configuration(proxyBeanMethods = false)
public class AppConfig {
@Bean DataSource dataSource() { ... }
@Bean JdbcTemplate jdbcTemplate(DataSource ds) { return new JdbcTemplate(ds); } // принимать через параметры
}
CGLIB-прокси не создаётся, но между @Bean-методами нужно передавать зависимости через параметры. Быстрее старт, проще GraalVM-native.
Условные бины
@Conditional*-аннотации управляют тем, регистрируется бин или нет — на основе свойств, классов в classpath, других бинов.
@Bean
@ConditionalOnProperty(name = "feature.metrics.enabled", havingValue = "true")
public MetricsCollector metricsCollector() { return new MetricsCollector(); }
@Bean
@ConditionalOnMissingBean // только если никто не объявил свой
@ConditionalOnClass(name = "redis.clients.jedis.Jedis")
public CacheManager defaultCacheManager() { return new RedisCacheManager(...); }
Это механика, на которой работает вся auto-configuration Spring Boot.
BeanPostProcessor
Точка расширения для всего, что хочет вмешаться в жизненный цикл каждого бина. Spring AOP, transaction proxy, кэширование — реализованы через BeanPostProcessor.
@Component
public class LoggingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String name) {
log.info("Bean ready: {} of type {}", name, bean.getClass().getName());
return bean; // или прокси-обёртка, как делает AOP
}
}
Применяется редко (обычно проще обойтись @EventListener(ApplicationReadyEvent.class)), но знание его существования отвечает на вопрос «как @Transactional превращает мой класс в прокси».
Циклические зависимости
A → B → A — Spring пытается разрешить через раннюю экспозицию (singleton'ы помещаются в кэш сразу после создания, до DI). С Spring Boot 2.6+ это отключено по умолчанию, цикл вызовет ошибку при старте.
Включить можно через spring.main.allow-circular-references=true, но это техдолг: цикл означает, что один из бинов делает слишком много. Правильный фикс — выделить третий бин, через который и идёт связь.
Что почитать дальше
- Auto-configuration, properties, profiles — как Spring Boot собирает контекст автоматически.
- Spring AOP — что именно делают
BeanPostProcessor, создающие прокси. - Spring Events — как использовать
ApplicationContextдля publish/subscribe внутри приложения.