1. Условия в Spring Boot
Теперь уже ясно, что Boot не тащит в приложение второй тайный контейнер: auto-configuration попадает в тот же ApplicationContext. Следующий вопрос неизбежный — по каким сигналам Boot вообще решает, что именно включать?
Давайте начнём с мотивации, потому что без неё @ConditionalOn... выглядит как очередная аннотация, которую надо «просто запомнить». Условия в Boot нужны не для красоты и не чтобы усложнить жизнь новичку, а чтобы платформе было как-то безопасно решать, какие куски инфраструктуры включать, а какие — не трогать, иначе приложение превратилось бы в комбайн, который при старте пытается поднять вообще всё.
Представьте, что Spring Boot — это охранник на входе в клуб “Runtime”. Ему нужно понять, кого пускать внутрь: нужен ли Tomcat, нужен ли JSON mapper, нужно ли включать веб-слой, нужно ли включать какие-то дополнительные фичи. Но он не может просто «включить всё подряд», потому что:
Во‑первых, не все библиотеки присутствуют. Если на classpath нет Tomcat — бессмысленно включать конфигурацию сервера Tomcat. Во‑вторых, у приложения могут быть собственные решения: вы уже создали какой-то бин, и Boot не должен подбрасывать второй «такой же» и устраивать драку за @Autowired. В‑третьих, часть вещей должна зависеть от настроек: например, “включить фичу только если свойство такое-то”.
Поэтому Boot повсюду использует conditions: маленькие «проверки на входе», которые отвечают на вопрос: имеем ли мы право и смысл включать эту конфигурацию?
Чтобы удержать картину в голове, полезно смотреть на это как на очень простую блок-схему:
flowchart TD
A["Конфигурация/auto-config хочет добавить bean"] --> B{"Класс есть на classpath?"}
B -->|нет| X["Пропустить"]
B -->|да| C{"Свойство разрешает?"}
C -->|нет| X
C -->|да| D{"Нужный bean уже существует?"}
D -->|да| X
D -->|нет| E["Зарегистрировать bean"]
Это не «внутренности Boot», это базовая логика принятия решений: сначала проверяем, что “вообще есть из чего собирать”, потом — что “это разрешено”, потом — что “не конфликтуем с приложением”.
2. Classpath и влияние зависимостей
Classpath — слово, которое сначала звучит как «что-то из скучной главы по JVM», но в Spring Boot оно становится почти прикладным инструментом. Когда вы добавляете starter или библиотеку в build.gradle.kts, вы не просто увеличиваете размер build/libs/*.jar. Вы меняете то, какие классы доступны приложению на старте, а значит — какие условия смогут сработать.
В Java (упрощённо) classpath — это список мест, где JVM может найти .class файлы. В Gradle-проекте это, по сути, результат сборки: ваш код плюс все зависимости (и зависимости зависимостей). Для Boot это “инвентарь” вашего приложения. И дальше начинает работать очень практическая логика: если на classpath есть определённый класс, значит, вы (скорее всего) хотите соответствующую подсистему.
Например, наличие jakarta.servlet.Servlet и классов Spring MVC — это жирный намёк: «мы web-приложение на servlet-стеке». Наличие Jackson — намёк: «мы хотим уметь превращать объекты в JSON и обратно». Наличие каких-то конкретных библиотек логирования — намёк: «вот какой logging stack будет работать».
И вот тут происходит психологическая ловушка новичка: вы не писали код для запуска сервера, для регистрации DispatcherServlet, для JSON-конвертеров — но они появляются. Потому что Boot видит, что эти классы доступны, и включает нужные конфигурации.
Чтобы почувствовать эту идею физически, полезно один раз увидеть «проверку наличия класса» в лоб, на уровне Java-кода (не как “правильный Boot-way”, а как демонстрацию мысли):
import org.springframework.util.ClassUtils;
// Проверяем наличие класса по имени: так делает и Boot внутри условий
boolean mvcPresent = ClassUtils.isPresent(
"org.springframework.web.servlet.DispatcherServlet",
ClassUtils.getDefaultClassLoader() // Берём дефолтный ClassLoader приложения
);
// Выводим результат, чтобы «пощупать» classpath как факт
System.out.println("Spring MVC present = " + mvcPresent); // Spring MVC present = true
Смешная деталь: Boot примерно так же и мыслит, просто делает это в более аккуратном и массовом виде, через @ConditionalOnClass и друзей.
3. Карта условий: classpath, properties, beans
Сейчас будет маленькая “карта местности”, чтобы вы не держали всё в голове как набор несвязанных аннотаций. Важно понимать, что Spring Boot conditions — это не один тип проверки, а несколько семейств проверок, каждая отвечает на свой вопрос. И да, сегодня главный герой — classpath, но он не один.
Ниже — компактная табличка, которую удобно вспоминать, когда вы видите странное поведение Boot и думаете: “а по какому признаку он решил так сделать?”
| Что проверяем | Какой это сигнал | Типичная аннотация Boot | Пример человеческим языком |
|---|---|---|---|
| Наличие класса | classpath | @ConditionalOnClass | “В проекте есть MVC классы — включаем MVC” |
| Отсутствие класса | classpath | @ConditionalOnMissingClass | “Нет servlet API — не включаем servlet web runtime” |
| Наличие свойства | configuration | @ConditionalOnProperty | “Фича включена только если свойство = true” |
| Наличие бина | контекст | @ConditionalOnBean | “Если уже есть X, можно включить Y, который от него зависит” |
| Отсутствие бина | контекст | @ConditionalOnMissingBean | “Если вы не создали бин сами — дам дефолтный” |
Сегодня мы будем в основном говорить о первых двух строках (classpath-driven behavior) и чуть-чуть о третьей (properties), потому что в реальном Boot решение почти всегда “составное”: зависимость + настройка.
4. @ConditionalOnClass: включение по классу
Теперь к самому вкусному: @ConditionalOnClass. Эта аннотация — один из главных “проводов” между вашим build.gradle.kts и тем, что реально собирается в ApplicationContext. И важно понять её не как «магическую штуку», а как очень простую мысль: если нужной библиотеки нет — не делай вид, что умеешь с ней работать.
Возьмём catalog-service. Представим, что мы хотим добавить маленький бин‑маркер, который появляется только если в приложении реально есть Spring MVC (то есть присутствует DispatcherServlet на classpath). Это не auto-configuration в смысле Boot-библиотеки, это просто наш учебный “индикатор”.
Сначала сделаем модель (она совсем простая):
// Простейшая модель-маркер: значение будем печатать при старте
record RuntimeMarker(String value) {
}
Теперь конфигурация, которая создаёт этот marker только при наличии класса:
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// Обычная @Configuration, но с условной регистрацией бина
@Configuration
class RuntimeMarkerConfiguration {
@Bean
// Условие: бин создаётся только если на classpath есть указанный класс
@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")
RuntimeMarker mvcMarker() {
// Значение — просто «индикатор», чтобы увидеть результат в логах
return new RuntimeMarker("spring-mvc");
}
}
Почему мы используем name = "...", а не value = DispatcherServlet.class? Потому что так конфигурация компилируется даже если этого класса нет. Это очень важная инженерная мелочь: условие должно быть проверяемым без обязательной компиляционной зависимости. Boot в своих auto-config классах часто делает так же.
Чтобы увидеть результат, добавим ещё один временный probe-компонент. Его задача — не стать частью постоянного startup-кода, а просто показать факт: бин появился или нет.
import java.util.Optional;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
class RuntimeMarkerRunner implements ApplicationRunner {
// Optional подчёркивает: бин может отсутствовать, и это нормально
private final Optional<RuntimeMarker> marker;
RuntimeMarkerRunner(Optional<RuntimeMarker> marker) {
this.marker = marker;
}
@Override
public void run(ApplicationArguments args) {
// Если бина нет — печатаем "none", чтобы поведение было наблюдаемым
System.out.println("Runtime marker = " +
marker.map(RuntimeMarker::value).orElse("none")); // Runtime marker = spring-mvc
}
}
Что здесь важно заметить, и это прям “модель мышления Boot”:
@ConditionalOnClass решает, будет ли бин вообще существовать. Это не if внутри метода, это решение на этапе сборки контекста. Поэтому ваш код дальше либо может инжектить этот бин, либо должен быть готов к его отсутствию (через Optional, ObjectProvider, разные профили и т.д.).
Если вы впервые это видите, оно ощущается странно: “почему мне надо думать о том, существует ли класс?”. Но именно это делает Boot устойчивым: он не включает то, что физически не может работать.
5. @ConditionalOnProperty: включение по настройкам
Classpath — мощный сигнал, но он не всегда достаточный. Иногда библиотека есть, но конкретная фича должна включаться по настройке. Например, у вас может быть библиотека для чего-то, но вы хотите иметь флаг “включить/выключить” без перекомпиляции. Именно здесь появляется @ConditionalOnProperty.
Здесь property нужен только как сигнал для условия. Нам не требуется большая конфигурационная модель приложения; достаточно увидеть сам принцип: строковый ключ и значение тоже могут участвовать в решении, создавать ли bean.
Сделаем в catalog-service маленькую фичу: “печать подсказки на старте”, но только если свойство включено.
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class StartupHintsConfiguration {
@Bean
// Включаем бин только если свойство явно равно "true"
@ConditionalOnProperty(name = "app.catalog.startup-hints-enabled", havingValue = "true")
ApplicationRunner startupHintsRunner() {
// Тут можно представить «опциональную фичу», завязанную на флаг
return args -> System.out.println("Hint: catalog-service started"); // Hint: catalog-service started
}
}
Чтобы не уводить разговор в полноценный config-layer, можно временно задать свойство прямо в main() как простой переключатель для условия:
import java.util.Map;
import org.springframework.boot.SpringApplication;
public class CatalogServiceApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(CatalogServiceApplication.class);
// Дефолтные свойства «как будто из application.yml», но прямо из кода
app.setDefaultProperties(Map.of("app.catalog.startup-hints-enabled", "true"));
// Запускаем приложение обычным образом
app.run(args);
}
}
Психологически полезно зафиксировать: @ConditionalOnProperty — это такой же “переключатель”, как и classpath, просто другой природы. Classpath говорит “у меня есть инструменты”, property говорит “мне разрешили/запросили включить режим”.
6. Комбинация условий
Одна из причин, почему новичку кажется, что Boot “делает как хочет”, в том, что решение часто зависит сразу от нескольких условий. Вы смотрите только на dependency и думаете “ну всё, должно включиться”, а Boot ещё проверяет свойство. Или наоборот: вы включили свойство, а нужной библиотеки нет. И вы получаете ощущение “оно меня игнорирует”, хотя на самом деле оно просто последовательно выполняет правила.
Давайте покажем это на нашем же catalog-service: сделаем бин CatalogDiagnostics, который появляется только если выполняются оба условия: мы реально web-приложение на MVC, и мы явно включили diagnostics флаг.
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class DiagnosticsConfiguration {
@Bean
// Условие №1: приложение реально в MVC/servlet-стеке (класс есть на classpath)
@ConditionalOnClass(name = "org.springframework.web.servlet.DispatcherServlet")
// Условие №2: фича включена настройкой
@ConditionalOnProperty(name = "app.catalog.diagnostics-enabled", havingValue = "true")
CatalogDiagnostics catalogDiagnostics() {
// Сам бин простой: главное — что он появляется/не появляется по условиям
return new CatalogDiagnostics();
}
}
// Пустой класс-«фича» для демонстрации: его наличие в контексте = фича включена
class CatalogDiagnostics {
}
Что мы этим примером “впечатываем в голову”:
Если diagnostics должны работать только в web-режиме, то проверка на classpath — честнее, чем пытаться где-то в коде ловить ошибки или проверять, поднялся ли сервер. Вы проверяете сигнал, который для Boot является объективным: “есть MVC классы” значит “смысл включать web-диагностику есть”. А property — второй сигнал: “да, я действительно этого хочу”.
Это очень Boot-стиль: не делать один огромный “if” в одном месте, а разбивать логику на маленькие проверяемые условия.
7. Starters как переключатели classpath
Теперь давайте соберём всё в одну практическую мысль, которая спасает от огромного количества путаницы. Starters в Spring Boot — это не просто “удобный набор библиотек”. В контексте conditions starter — это как большой рубильник: вы добавили зависимость, в classpath приехали новые классы, а значит сработали новые @ConditionalOnClass, и внезапно Boot включил целую подсистему.
В build.gradle.kts это выглядит почти невинно. Например, добавили web starter — и всё.
dependencies {
// Добавили webmvc starter → на classpath появились servlet/MVC классы → сработают условия автоконфигураций
implementation("org.springframework.boot:spring-boot-starter-webmvc")
// Тестовый starter: подтянет тестовую инфраструктуру (включая зависимости для тестов)
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
А в runtime это означает, что: приложение начинает восприниматься Boot как servlet-web приложение; появляется embedded server; появляются MVC инфраструктурные бины; появляется стандартная цепочка обработки HTTP запросов; появляются message converters и куча другого. Вы не обязаны всё это писать руками, потому что Boot уже написал за вас — но он включает это не “потому что Boot”, а потому что теперь на classpath есть нужные классы, и условия auto-configuration начинают совпадать.
Добавил dependency → изменил classpath → изменил результаты @ConditionalOnClass → изменился набор включённых конфигураций → изменился набор бинов → изменилось поведение приложения.
Это не философия. Это реально объясняет, почему один starter иногда “ломает” приложение: он не ломает, он включает новые куски, а вы неожиданно столкнулись с тем, что теперь ваше приложение стало другим (например, стало web-приложением, хотя вы думали, что оно просто “умеет HTTP когда-нибудь потом”).
### Мини-наблюдение в catalog-service
Полного отчёта нам для этого не нужно: можно сделать маленькое “рентген‑наблюдение”, чтобы не верить на слово ни себе, ни Boot. Идея простая: создать наблюдателя, который посмотрит, какие бины реально есть, и выведет понятное сообщение.
Сделаем небольшой runner, который попробует найти наш CatalogDiagnostics и, если он появился, скажет “да, diagnostics активны”. И это снова одноразовый probe, а не постоянный участник startup-кода.
import java.util.Optional;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
class DiagnosticsProbeRunner implements ApplicationRunner {
// Optional снова подчёркивает: бин может отсутствовать, и это штатная ситуация
private final Optional<CatalogDiagnostics> diagnostics;
DiagnosticsProbeRunner(Optional<CatalogDiagnostics> diagnostics) {
this.diagnostics = diagnostics;
}
@Override
public void run(ApplicationArguments args) {
// Наблюдаем «как факт»: включена фича или нет (по наличию бина)
System.out.println("Diagnostics enabled = " + diagnostics.isPresent());
// Diagnostics enabled = false
}
}
Почему это полезно начинающему? Потому что это снимает “ощущение магии”. Вы перестаёте думать “наверное, оно включилось” и начинаете видеть факт: бин либо существует, либо нет. Если нет — значит, какое-то условие не совпало. Если да — значит совпало.
А дальше уже можно идти по цепочке: какое условие могло не совпасть? Не хватает зависимости (classpath)? Не задано свойство? Приложение не в том режиме? И вот так вы постепенно приходите к инженерной диагностике, а не к шаманству.
8. Типичные ошибки при работе с условиями
Здесь будет не «страшилка», а набор очень жизненных ситуаций, в которые почти все попадают на старте. Условия Boot — штука простая, но у новичка мозг часто пытается применить к ним модель “if в коде”, и отсюда растут странные ожидания.
Ошибка №1: думать, что условие — это “проверка во время выполнения”, а не “решение при сборке контекста”.
Новички иногда ожидают, что если изменить свойство на лету, то бин “появится” или “исчезнет” прямо в работающем приложении. В большинстве случаев так не работает: @ConditionalOn... определяет, будет ли бин зарегистрирован при старте. Если вы хотите динамику в рантайме — это уже совсем другая архитектура, здесь она нам не нужна.
Ошибка №2: использовать @ConditionalOnClass(SomeClass.class) на классе, которого нет в compile classpath.
Если вы ссылаетесь на класс напрямую, компилятор потребует эту зависимость. Поэтому в условиях часто используют строку name = "...". Это выглядит чуть менее красиво, зато конфигурация становится универсальной: она может быть в проекте даже тогда, когда библиотека опциональна.
Ошибка №3: объяснять поведение Boot только одной причиной (“я же добавил starter!”).
Очень частая история: студент добавил зависимость и ждёт, что фича включится, но не учёл второе условие — property. Или наоборот, включил свойство, но не добавил библиотеку. В итоге возникает ощущение “Boot не слушается”. На практике почти всегда надо искать комбинацию сигналов: classpath + properties + (иногда) наличие/отсутствие бинов.
Ошибка №4: делать условную логику внутри бизнес-кода вместо условной регистрации бинов.
Когда начинающий разработчик понимает, что есть “фича включена/выключена”, он часто пишет if (enabled) { ... } в сервисе, а потом ещё в контроллере, а потом ещё где-нибудь. И получается размазанная логика по всему проекту. Гораздо чище, когда фича выражается как наличие/отсутствие бина, а зависимости на неё инжектятся как Optional — тогда wiring сам фиксирует, что фича опциональна, и это видно прямо в конструкторе.
Ошибка №5: путать “classpath-driven behavior” с “Boot сам придумал бизнес-логику”.
Boot может включить инфраструктуру (сервер, MVC, JSON-конвертеры, часть интеграций), но CourseCatalogService и ваш репозиторий он не придумает. Если у вас не работает доменная часть catalog-service, искать проблему в auto-configuration чаще всего бессмысленно: это ваш код и ваш wiring. Conditions здесь помогают только в том, чтобы отделять инфраструктурное “появилось из Boot” от прикладного “появилось из проекта”.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ