1. Default bean и его роль
Classpath, properties и наличие бинов уже дают Boot сигналы. Следующий вопрос практический: что происходит, когда платформа понимает, что типовой инфраструктурный компонент приложению нужен, а вы свой вариант ещё не объявили?
Когда вы впервые видите Spring Boot, появляется ощущение, что он «угадывает» то, что вы хотели написать. Как будто Boot сидит в углу, смотрит на ваш build.gradle.kts и такой: «Ага, вижу web — сейчас накину тебе Tomcat, JSON и ещё десяток вещей. Пожалуйста». Звучит подозрительно, но здесь логика довольно приземлённая и инженерная.
Default bean — это резервный (fallback) bean, который Spring Boot готов предоставить, когда есть понятный «типовой сценарий». То есть когда можно reasonably сказать: «В большинстве приложений это нужно, и почти всем подходит примерно одинаковая настройка». В этом месте Boot и говорит: «Окей, если пользователь не написал своё — я подложу стандартный вариант».
При этом важно не перепутать default bean с «обязательным». Он не обязателен и не священен. Его смысл — убрать повторяемый boilerplate. Вы не должны писать вручную одно и то же в каждом проекте: «создай объект X, положи в контейнер, настрой, свяжи». Boot делает это за вас, но (ключевой момент) только пока вы сами не сказали явно, как вам нужно.
Именно поэтому default beans почти всегда создаются по принципу «создавай только если…». И среди этих «если…» одно из самых важных — если в контексте ещё нет подходящего бина. То есть Boot не должен принести вам второй вариант и устроить хаос в DI.
2. Границы: что Boot создаёт сам
Граница здесь та же, что и раньше: reasonable default появляется у инфраструктуры, а не у домена. Сервер, JSON-mapping, conversion, task execution и другие типовые платформенные штуки Boot умеет привезти из коробки. CourseCatalogService или CourseCatalogRepository он за вас не придумывает, потому что тут уже начинается бизнес, а не общий infrastructure baseline.
И это хорошо. Как только платформа начинает «угадывать» доменную логику, она перестаёт быть предсказуемой. Boot действует прагматично: создаёт только то, для чего есть sensible default, и не лезет туда, где начинаются прикладные решения.
Механика “создавай только если нет”: @ConditionalOnMissingBean
Теперь к главному инструменту, который объясняет половину поведения Boot: @ConditionalOnMissingBean. Если вы запомните только одну аннотацию про default beans — пусть это будет она. Она буквально описывает философию Boot: «Я помогу, но не буду мешать».
Смысл простой. У вас есть @Bean-метод (обычный Spring), но он сработает только если в ApplicationContext ещё нет подходящего бина указанного типа (или с указанным именем). В мире Boot это и есть стандартный способ оформить «fallback».
При этом эта аннотация обычно живёт в auto-configuration классах внутри Spring Boot, а не в вашем проекте. Но мы можем использовать её в своём коде как учебный микроскоп: чтобы увидеть механику на маленьком примере, без необходимости читать исходники Boot.
Есть ещё один важный нюанс: missing-bean условие — это не «проверка на null». Оно про контейнер. То есть оно отвечает на вопрос: «Уже зарегистрирован bean definition, который подходит?» Если да — default не нужен.
И вот почему это важно: если Boot принёс бы вам второй bean того же типа, DI мог бы стать неоднозначным. А неоднозначный DI — это классика жанра «я просто хотел добавить одну зависимость, а теперь приложение не стартует, и я читаю stacktrace как древнее пророчество».
3. Мини-демо: default bean для старта
Чтобы прочувствовать идею default beans, давайте сделаем небольшой sandbox-фрагмент. Он нужен только для механики @ConditionalOnMissingBean и не задаёт финальную startup-цепочку catalog-service.
Интерфейс для demo-реализации
public interface DemoStartupReporter {
// Контракт максимально простой: возвращаем строку, которую покажем при старте приложения.
String buildMessage();
}
Это максимально честный контракт: «дай строку, которую мы покажем на старте». Он намеренно простой — потому что наша цель не построить систему репортинга, а увидеть механику default beans.
Конфигурация default bean
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class DemoStartupConfiguration {
@Bean
// Создаём fallback-реализацию только если в контексте ещё нет DemoStartupReporter.
@ConditionalOnMissingBean
DemoStartupReporter demoStartupReporter() {
// Default поведение: просто сообщаем, что сервис стартовал.
return () -> "catalog-service started";
}
}
Обратите внимание на ключевую мысль: мы не говорим “создай DemoStartupReporter всегда”. Мы говорим “создай, если его ещё нет”. Это и есть «платформенный» стиль.
Используем DemoStartupReporter в DemoStartupMessageRunner
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
class DemoStartupMessageRunner implements ApplicationRunner {
private final DemoStartupReporter reporter;
DemoStartupMessageRunner(DemoStartupReporter reporter) {
// Инъекция по интерфейсу: будет либо default bean, либо пользовательский.
this.reporter = reporter;
}
@Override
public void run(ApplicationArguments args) {
// На старте печатаем сообщение, которое предоставил DemoStartupReporter.
System.out.println(reporter.buildMessage()); // catalog-service started
}
}
Теперь наш runner не хардкодит строку. Он просто зависит от контракта. А конкретная реализация может прийти как default — или быть определена приложением явно.
И да, это тот самый момент, когда вы начинаете думать как Boot: «мой компонент не обязан знать, кто ему подложит реализацию; главное — чтобы в контексте был ровно один подходящий вариант».
4. Свой bean вместо default
Остаёмся в том же sandbox-фрагменте. Теперь представим, что приложению нужен другой текст. Например: «Courses loaded: 12, featured: 4». Или «Maintenance mode enabled, please don’t panic» (шутка, но вы поняли).
Вот тут и вступает в игру backoff-логика: если приложение определяет свой bean того же типа, default должен стать не нужен. В реальном Spring Boot это работает именно так: Boot-автоконфигурации подключаются так, чтобы «пользовательские» beans считались первыми.
Покажу это на маленьком фрагменте кода. Важно: сейчас это именно демонстрация идеи, не попытка “обязательно добавить ещё одну конфигурацию в проект”. Мы смотрим, как мыслит Boot.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class CustomDemoStartupReporterConfiguration {
@Bean
DemoStartupReporter demoStartupReporter() {
// Пользовательский bean: как только он появился, fallback из DemoStartupConfiguration не создастся.
return () -> "catalog-service is ready (custom reporter)";
}
}
Если в контексте появляется этот bean, то наш fallback из DemoStartupConfiguration уже не нужен: контейнер видит, что DemoStartupReporter «уже есть», и условие missing-bean перестаёт быть истинным.
Почему Boot так упирается в это правило? Потому что это лучший компромисс между «удобно» и «контролируемо». Boot помогает вам стартовать и не писать тонну кода, но при этом не отбирает у вас право сказать: «Нет, мне нужно иначе».
И ещё одна важная мысль для новичка: эта “уступка” Boot — не случайность. Это фундаментальный договор между платформой и приложением. Именно поэтому Boot обычно не требует от вас «выключать всю автоконфигурацию», чтобы заменить один маленький компонент.
Примеры default beans в реальном Boot
Чтобы не казалось, что default beans — это только наш учебный трюк с DemoStartupReporter, давайте накидаем пару «узнаваемых» примеров из реального Boot. Мы без погружения в детали — просто чтобы вы начали видеть знакомые паттерны в логах и в контексте.
Ниже таблица, где видно главное: есть ли разумный default и почему Boot может себе позволить его создать.
| Категория | Что нужно приложению | Почему есть разумный default | Как Boot обычно решает «создавать или нет» |
|---|---|---|---|
| Web runtime | Встроенный сервер (например, Tomcat) | Почти всем web-приложениям нужен сервер, и базовая конфигурация стандартна | По classpath (есть Tomcat-классы) и по missing-bean (нет другого web server factory) |
| JSON | JSON mapper | Сериализация/десериализация JSON повторяется постоянно | По classpath (есть Jackson) и по missing-bean (пользователь не дал свой mapper) |
| Конвертации типов | ConversionService для типовых преобразований | Типовые конвертации нужны почти всегда | Часто создаётся как default и может быть расширена |
| Потоки/задачи | TaskExecutor / пул потоков | «Куда-то» выполнять задачи нужно многим приложениям | Boot даёт разумный пул по умолчанию, но не мешает заменить |
| Logging | Logging setup и дефолтные настройки | Логировать нужно всем, а “просто чтобы было” — тоже полезно | Boot настраивает baseline через зависимости и свойства |
Общий рисунок всегда одинаковый: Boot создаёт инфраструктуру, когда она типовая, и старается не создавать «второй вариант» того же самого, чтобы не ломать DI.
И отдельно: если какого-то default bean нет — это не означает, что «Boot сломан». Это означает, что либо условия не совпали (о них мы говорили в предыдущей лекции), либо Boot считает, что «разумного дефолта тут нет».
5. Когда Boot пропускает bean
В реальности новичок чаще всего сталкивается с такой эмоцией: «Я ожидал, что Boot что-то создаст… а оно не создалось». И дальше начинается шаманство: очистка кеша, перезапуск IDE, молитва Gradle Wrapper’у и лёгкий торг со вселенной.
На самом деле «не создалось» почти всегда объясняется одним из трёх типов причин, и все они рациональные.
Первая причина — нет нужного сигнала на classpath. Boot не гадалка; если на classpath нет нужных классов, он не может создать инфраструктуру, которая от них зависит. Вы это уже видели в условиях @ConditionalOnClass.
Вторая причина — свойство (property) выключило поведение. Иногда default bean существует, но только если включён флаг. Это нормальный способ сделать «по умолчанию выключено, но легко включается». Такой подход встречается часто в диагностике и в dev-режиме.
Третья причина — missing-bean условие не прошло, потому что вы или какая-то другая конфигурация уже определили bean нужного типа. И это, вообще-то, хорошая новость: значит, Boot «не полез в драку», а уважил существующую конфигурацию.
Сегодня мы не разбираем, как именно увидеть, какие условия сработали и почему, но уже сейчас полезно держать в голове вопрос-«триггер», который экономит часы: «Какое условие могло выключить этот default? classpath? property? missing-bean?».
6. Типичные ошибки при работе с default beans
Ошибка №1: ожидание, что Boot создаст прикладные beans так же, как инфраструктурные.
Это происходит почти у всех: вы видите сотни beans в контексте и подсознательно ждёте, что CourseCatalogService тоже «как-нибудь появится». Но Boot создаёт только то, что можно стандартизировать. Доменный сервис и репозиторий — это решение вашего приложения, и оно должно быть явно описано в коде.
Ошибка №2: трактовать “не создался default bean” как поломку Boot.
Если default bean не появился, чаще всего Boot просто не увидел нужного набора условий. Это не исключение и не «ошибка конфигурации», а штатный режим работы. Главное — перестать угадывать и начать проверять: есть ли нужная зависимость на classpath, включено ли нужное property, не объявили ли вы сами bean такого типа.
Ошибка №3: случайно создать второй bean того же типа и получить неоднозначность при инъекции.
Иногда студент добавляет свой @Bean, не замечая, что Boot уже дал default. В результате контейнер видит два кандидата, и инъекция по типу становится неоднозначной. Да, это решается @Primary и @Qualifier, но гораздо полезнее понять первопричину: Boot старался не создавать второй bean, но вы сами добавили ещё один — и теперь контейнер честно не знает, кого выбрать.
Ошибка №4: путать “default bean” и “единственно правильный”.
Психологически легко попасть в ловушку: раз Boot так сделал, значит так «надо всегда». Но default — это компромисс для типового случая. Иногда он отлично подходит годами, а иногда — перестаёт подходить, и вы добавляете свой явный bean. Важно не воевать с defaults и не поклоняться им, а понимать их как стартовую площадку.
Ошибка №5: пытаться лечить поведение Boot отключениями и хаотичными правками, не поняв причины.
Это классический «cargo cult»: “не нравится — выключу”. Проблема в том, что выключить легко, а потом вы выясняете, что выключили ещё пять вещей «заодно». Гораздо здоровее сначала понять, почему default появился или не появился (условия), и только потом решать, нужен ли вам явный контроль.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ