Главное свойство Spring Boot — он почти ничего не заставляет настраивать руками. Добавили одну зависимость для работы с базой — и JdbcTemplate уже есть. Добавили зависимость для веба — и приложение само поднимает встроенный сервер. Разберём, как это устроено и откуда приложение берёт настройки.
Каждый класс настройки из стартера проходит проверку условий. Нет библиотеки — кандидат пропускается; ваш бин уже объявлен — настройка отступает. В контекст попадает только то, что прошло условия.
Стартеры — наборы зависимостей одним пунктом
Раньше, чтобы подключить, скажем, веб-слой, приходилось вручную перечислять десяток библиотек: сам Spring MVC, сервер, библиотеку для JSON, валидацию — и следить, чтобы их версии были совместимы. Одна несовместимая версия — и приложение падает на старте с непонятной ошибкой. Это долго и хрупко.
Стартер (starter) — это готовый набор зависимостей под одну задачу, собранный за вас. Подключаете один пункт — получаете всё нужное в согласованных версиях.
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web") // веб целиком
implementation("org.springframework.boot:spring-boot-starter-data-jpa") // работа с БД
}
Стартеры команды Spring легко узнать по имени: они начинаются с spring-boot-starter-. У сторонних библиотек порядок слов обратный — <имя>-spring-boot-starter: так договорились, чтобы чужой стартер нельзя было принять за официальный. Внутри стартера нет почти никакого кода — это просто список других библиотек.
Как Spring Boot сам настраивает бины
Подключили стартер для базы — и в приложении откуда-то появился готовый DataSource и JdbcTemplate, хотя вы их не создавали. Раньше каждый такой объект пришлось бы собирать руками: прочитать адрес базы, логин, пароль, создать пул соединений, связать всё вместе. Десятки строк однотипной настройки в каждом проекте.
Auto-configuration — это механизм, которым Spring Boot делает эту настройку за вас. Идея простая: «посмотри, какие библиотеки лежат в проекте, и настрой их разумным образом по умолчанию».
Включается всё одной аннотацией на главном классе:
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@SpringBootApplication — это три аннотации в одной: пометка класса как конфигурации, сканирование ваших пакетов на @Component и @Service, и @EnableAutoConfiguration — та самая команда «настрой всё, что найдёшь в проекте».
Внутри стартеров лежат заранее написанные классы настройки. При старте Spring Boot перебирает их и для каждого решает: применять или нет.
Условные аннотации — настройка «если есть»
Откуда Spring Boot знает, что именно настраивать? Ведь в одном проекте есть база, а в другом — нет, и настройка базы там только мешала бы.
Ответ — условные аннотации. Каждый класс настройки помечен условиями, и применяется он, только если условия выполнены.
@AutoConfiguration
@ConditionalOnClass(DataSource.class) // только если класс DataSource есть в проекте
public class DataSourceAutoConfiguration {
@Bean
@ConditionalOnMissingBean // только если вы не создали свой DataSource
@ConditionalOnProperty(name = "spring.datasource.url") // только если задан адрес базы
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder().build();
}
}
Листинг упрощён: у настоящего DataSourceAutoConfiguration условий больше, а пул соединений собирается во вложенных классах настройки. Но устроен он ровно так — набор условий над @Bean-методом.
Самые частые условия и их смысл:
@ConditionalOnClass— применить, только если нужный класс есть в проекте. Нет библиотеки для базы — настройка базы просто пропускается.@ConditionalOnMissingBean— применить, только если такого бина ещё нет. Это значит ваши настройки всегда главнее: объявили свойDataSource— Spring Boot отступает и оставляет ваш.@ConditionalOnProperty— применить, только если в настройках стоит определённое значение. Удобно, чтобы включать и выключать что-то одной строкой.
Сам механизм легко повторить на чистой Java: карта уже созданных бинов и три проверки на каждого кандидата.
живой пример
import java.util.LinkedHashMap;
import java.util.Map;
public class MiniAutoConfig {
static final Map<String, String> context = new LinkedHashMap<>();
static final Map<String, String> props = Map.of("spring.datasource.url", "jdbc:postgresql://localhost/shop");
static boolean onClasspath(String className) {
try {
Class.forName(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
static void apply(String config, String bean, String onClass, String onProperty) {
if (!onClasspath(onClass)) {
System.out.println(config + " — пропущен: нет класса " + onClass);
} else if (context.containsKey(bean)) {
System.out.println(config + " — пропущен: бин " + bean + " уже есть");
} else if (onProperty != null && !props.containsKey(onProperty)) {
System.out.println(config + " — пропущен: не задано " + onProperty);
} else {
context.put(bean, config);
System.out.println(config + " — применён: создан " + bean);
}
}
public static void main(String[] args) {
context.put("JdbcTemplate", "ваш @Bean");
apply("DataSourceAutoConfiguration", "DataSource", "java.sql.DriverManager", "spring.datasource.url");
apply("KafkaAutoConfiguration", "KafkaTemplate", "org.apache.kafka.clients.producer.KafkaProducer", null);
apply("JdbcTemplateAutoConfiguration", "JdbcTemplate", "java.sql.DriverManager", null);
System.out.println("в контексте: " + context.keySet());
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Первая проходит, вторая пропускается из-за отсутствующей библиотеки, третья отступает перед вашим бином. У Spring Boot так же, только кандидатов сотни.
Почему бин не создался
Когда «должен был появиться бин, а его нет», не нужно гадать. Включите режим отладки:
debug=true
В логах при старте появится отчёт о принятых решениях: что подошло (совпавшие условия) и что отклонено и почему. Это первое место, куда стоит смотреть. Тот же режим включается аргументом запуска --debug, без правки файла настроек.
В проде логи старта обычно уже уехали, и перечитывать их неудобно. Там тот же отчёт отдаёт Actuator: включите management.endpoints.web.exposure.include=conditions и откройте /actuator/conditions — придёт JSON с двумя списками, positiveMatches и negativeMatches, с той же причиной у каждой строки. Рядом полезны /actuator/beans (что в контексте на самом деле) и /actuator/configprops (какие значения получили группы настроек).
Настройки снаружи кода
Адрес базы, пароли, номер порта нельзя зашивать в код: для разработки одно значение, для боевого сервера другое, а пароли вообще не должны лежать в репозитории. Менять код и пересобирать приложение ради смены порта — плохой путь.
Spring Boot позволяет держать все такие значения снаружи — в файле настроек или в переменных окружения. Основной файл — application.yml (или application.properties) в проекте:
server:
port: 8080
spring:
datasource:
url: jdbc:postgresql://localhost:5432/shop
Одно и то же значение можно задать в нескольких местах, и у них есть приоритет. Запоминать весь список не нужно, достаточно главного правила: чем «ближе» к запуску задано значение, тем оно главнее. Аргумент командной строки перебивает переменную окружения, а та — файл application.yml.
Spring идёт по источникам сверху вниз и берёт первое найденное значение: заданное ближе к запуску перебивает всё, что ниже.
java -jar app.jar --server.port=9000 # перебьёт порт из application.yml
Внутри это просто упорядоченный список источников: Spring идёт по нему сверху вниз и берёт первое найденное значение.
живой пример
import java.util.List;
import java.util.Map;
public class PropertyOrder {
public static void main(String[] args) {
Map<String, String> commandLine = Map.of("server.port", "9000");
Map<String, String> env = Map.of("spring.profiles.active", "prod");
Map<String, String> yaml = Map.of("server.port", "8080");
for (String key : List.of("server.port", "spring.profiles.active")) {
for (Map<String, String> source : List.of(commandLine, env, yaml)) {
if (source.containsKey(key)) {
System.out.println(key + " = " + source.get(key));
break;
}
}
}
}
}
Запустить
Запуск примеров доступен в платном доступе. Там этот же код выполняется прямо в статье: редактор, запуск и проверка рядом с абзацем. Три дня бесплатно →
Одно место в этом списке ловит почти каждого: application.yml, лежащий рядом с jar-файлом (или в папке config/ рядом с ним), перебивает тот, что упакован внутрь jar. Правите файл в репозитории, а на сервере значение другое — смотрите, не лежит ли рядом с jar ещё один.
На практике обычно так: общие настройки лежат в application.yml в репозитории; пароли и адреса для боевого сервера приходят через переменные окружения; а личные локальные правки — в application-local.yml, который не попадает в репозиторий.
Подтянуть ещё один файл: spring.config.import
Файл application.yml не обязан быть единственным. Свойство spring.config.import подключает к нему другие источники, и они встают в тот же список приоритетов:
spring:
config:
import:
- optional:file:./config/local.yml # если файла нет, старт не падает
- configtree:/run/secrets/ # каталог, где имя файла это ключ
Первая форма — обычный файл рядом с приложением: так подкладывают настройки стенда, не трогая образ. Вторая, configtree, читает каталог, где каждый файл это одно свойство, а его содержимое — значение: ровно так монтируются секреты в Kubernetes и Docker, и приложение получает пароль как spring.datasource.password, не зная, что он приехал файлом. Есть и формы для внешних хранилищ настроек (configserver:, vault:). Префикс optional: означает «нет источника — не падать»; без него отсутствующий файл роняет старт, и это правильное поведение для обязательных секретов.
@ConfigurationProperties и @Value
Настройки из файла нужно как-то прочитать в коде, и способ зависит от масштаба: одно-два значения читают через @Value, группу связанных настроек через @ConfigurationProperties.
@Value для одного значения:
@Value("${app.timeout:5000}") // 5000 — значение по умолчанию, если в настройках пусто
private long timeoutMs;
Но когда связанных значений много, @Value приходится повторять у каждого поля, и легко ошибиться в имени.
@ConfigurationProperties связывает целую группу настроек с объектом — одним махом и с проверкой типов:
app:
retries:
max-attempts: 3
backoff-ms: 1000
timeout-ms: 5000
@ConfigurationProperties(prefix = "app.retries")
public record RetryProperties(int maxAttempts, long backoffMs, long timeoutMs) {}
Без регистрации бина не будет. Аннотация описывает, откуда брать значения, но бином класс не делает, и приложение упадёт на старте с жалобой, что бин RetryProperties не найден. Регистрируют либо @EnableConfigurationProperties(RetryProperties.class) рядом с конфигурацией, либо один раз на главном классе:
@SpringBootApplication
@ConfigurationPropertiesScan
public class App { }
Со сканированием все такие записи находятся сами.
После этого RetryProperties внедряется как обычная зависимость, и все три значения уже на месте. Имена в YAML записываются через дефис (max-attempts), а в коде — как обычные поля (maxAttempts); Spring сам сопоставляет одно с другим.
Проверка настроек на старте
К группе настроек можно добавить правила, и тогда Spring проверит их при запуске:
@ConfigurationProperties(prefix = "app.retries")
@Validated
public record RetryProperties(
@Min(1) @Max(10) int maxAttempts,
@Positive long backoffMs,
@Positive long timeoutMs
) {}
Сами проверки (@Min, @Positive и остальные) приезжают со стартером spring-boot-starter-validation — без него их просто нет в проекте, а @Validated останется пустой пометкой.
Если кто-то задаст max-attempts: 0, приложение не запустится и сразу скажет, что не так. Это лучше, чем поймать ошибку позже, когда приложение уже работает и падает в неожиданном месте.
Профили — разные настройки для разных окружений
Для разработки вы хотите отправлять письма «понарошку» и писать подробные логи. На боевом сервере — отправлять настоящие письма и логировать сдержанно. Держать для этого разные сборки приложения неудобно и опасно.
Профиль — это пометка-режим. Бин или настройку можно привязать к профилю, и работать она будет только тогда, когда этот профиль включён.
@Service
@Profile("prod")
public class RealEmailService implements EmailService { } // только на боевом
@Service
@Profile({"dev", "test"})
public class FakeEmailService implements EmailService { } // в разработке и в тестах
Включить профиль можно настройкой spring.profiles.active:
spring:
profiles:
active: prod
Или через переменную окружения SPRING_PROFILES_ACTIVE=prod, или аргументом запуска --spring.profiles.active=prod. Можно включить сразу несколько через запятую.
Под профили есть и отдельные файлы настроек. Файл application-prod.yml накладывается поверх общего application.yml, когда активен профиль prod. Так общие значения лежат в одном месте, а различия между окружениями — в файлах по профилям.
Три вещи про профили, на которых спотыкаются почти все.
spring.profiles.active нельзя задавать в профильном файле. Строка spring.profiles.active: prod внутри application-prod.yml — это попытка включить профиль из файла, который читается, только если профиль уже включён. Spring Boot такую запись отвергает с ошибкой на старте. Активный профиль задают снаружи: переменной окружения, аргументом запуска или в общем application.yml.
Профили объединяют в группы. Когда стенду нужно сразу три профиля, их перечисляют один раз:
spring:
profiles:
group:
prod: [prod-db, prod-mq, metrics]
Теперь --spring.profiles.active=prod включает все четыре.
Один файл может содержать несколько профилей. В YAML документы разделяются строкой ---, и каждому куску можно назначить условие:
server:
port: 8080
---
spring:
config:
activate:
on-profile: prod
server:
port: 80
Старое написание spring.profiles: prod внутри документа в Boot 2.4 заменили на spring.config.activate.on-profile; в чужих примерах из интернета попадается и то и другое.
Опечатка в имени свойства ничего не сломает
Написали в application.yml spring.datasoure.url вместо spring.datasource.url — приложение спокойно стартует и идёт к базе по адресу по умолчанию. Незнакомые ключи Spring Boot просто игнорирует: файл настроек это карта строк, и проверить её на опечатки без списка допустимых ключей нельзя.
Список этот можно сгенерировать для своих настроек. Зависимость spring-boot-configuration-processor при сборке читает классы с @ConfigurationProperties и кладёт в jar файл META-INF/spring-configuration-metadata.json: после этого IDE подсказывает ваши ключи, подсвечивает опечатки и показывает типы и значения по умолчанию.
annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
Для чужих ключей (spring.*, management.*) метаданные уже лежат в их jar-файлах, и подсказки работают сразу — поэтому опечатку в spring.datasoure IDE обычно подчёркивает, а в своём app.retires без процессора нет.
Обратная сторона той же терпимости: у @ConfigurationProperties есть ignoreUnknownFields = false, и тогда лишний ключ внутри вашего префикса роняет старт. Это полезно для настроек, где опечатка меняет поведение молча, но включать его повсеместно не стоит: любой временный ключ в том же префиксе начнёт ронять приложение.
Своя auto-configuration в библиотеке
Допустим, у вас есть переиспользуемый кусок инфраструктуры — обёртка над метриками или общий набор фильтров. Чтобы подключать его одной зависимостью и без копирования настроек, оформите его как собственный стартер: он сам себя настроит у того, кто его подключил.
Принцип ровно тот же, что у встроенных стартеров: класс настройки с условными аннотациями плюс один служебный файл, который говорит Spring Boot «вот мой класс настройки, примени его».
@AutoConfiguration
@ConditionalOnClass(MyService.class)
public class MyAutoConfiguration {
@Bean
@ConditionalOnMissingBean // проверка бина — на @Bean-методе, а не на классе
public MyService myService() {
return new MyService();
}
}
@ConditionalOnMissingBean стоит именно на методе: на классе оно отменило бы весь класс настройки — вместе с бинами, которые вы туда добавите позже.
Второй обязательный вопрос своего стартера — порядок. @ConditionalOnMissingBean смотрит на бины, которые уже определены к моменту проверки, поэтому результат зависит от того, чья настройка применилась раньше. Если ваш стартер объявляет ObjectMapper, а рядом лежит встроенный JacksonAutoConfiguration, без указания порядка вы получите то одно, то другое между запусками и версиями.
@AutoConfiguration(after = JacksonAutoConfiguration.class,
before = WebMvcAutoConfiguration.class)
public class MyAutoConfiguration { }
after означает «нас применяют после названных», before — «до». Есть и грубая форма, @AutoConfigureOrder с числом, но в новом коде предпочитают ссылку на конкретный класс: она говорит, от кого вы зависите, а не в каком месте безымянной очереди стоите. Правило простое: если ваш класс настройки объявляет бин, который может объявить кто-то ещё, порядок задают явно.
Файл-указатель кладётся в src/main/resources по фиксированному пути META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, и в нём — просто имя вашего класса:
com.example.mystarter.MyAutoConfiguration
После этого любой, кто добавит вашу библиотеку, получит MyService в контексте автоматически — и точно так же сможет переопределить его своим бином благодаря @ConditionalOnMissingBean. Именно так устроена, например, библиотека usecase-pattern: одна зависимость — и нужные бины уже на месте.
Глубже: старт приложения: события, раннеры и fail-fastрасширенное
Между main() и первым обработанным запросом проходит цепочка шагов, и у каждого есть имя. SpringApplication.run публикует события по порядку: ApplicationStartingEvent до всего, ApplicationEnvironmentPreparedEvent, когда настройки прочитаны, но бинов ещё нет, ApplicationContextInitializedEvent и ApplicationPreparedEvent до создания бинов, ContextRefreshedEvent, когда все синглтоны собраны, ApplicationStartedEvent и последним ApplicationReadyEvent: приложение приняло бы запрос. Слушать их можно обычным @EventListener, но ранние события до контекста так не поймать, для них есть SpringApplication.addListeners.
Для кода «выполнить после старта» есть два интерфейса. CommandLineRunner получает аргументы строки запуска как есть, ApplicationRunner разобранными (getOptionValues("--import")); оба вызываются после ApplicationStartedEvent и до ApplicationReadyEvent, поэтому проверка готовности в Kubernetes подождёт, пока они отработают. Прогрев кэша и проверка обязательных настроек живут здесь.
@Component
class StartupChecks implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
if (props.paymentUrl() == null) {
throw new IllegalStateException("app.payment-url не задан"); // приложение не стартует
}
}
}
Исключение из раннера останавливает старт, и это правильно: сервис, который поднялся без адреса платёжного шлюза, упадёт на первом заказе, лучше пусть не поднимется вовсе. Тот же принцип встроен в @ConfigurationProperties с @Validated: неверная настройка роняет приложение на старте, а не в проде через час. И про скорость: spring.main.lazy-initialization=true откладывает создание бинов до первого обращения и ускоряет старт, но переносит ошибки конфигурации с запуска на первый запрос; для прода это плохая сделка, для локальной разработки и тестов нормальная.
Глубже: логирование: уровни, logback-spring.xml и JSONрасширенное
Логирование в Spring Boot настроено из коробки: Logback, вывод в консоль, уровень INFO. Дальше три вещи, которые меняют в каждом проекте.
Уровни по пакетам задают в настройках, и по профилям они разные:
logging:
level:
root: INFO
ru.shop.orders: DEBUG
org.hibernate.SQL: DEBUG # только в профиле local
Формат вывода задаёт logback-spring.xml в resources (именно -spring: тогда внутри работают <springProfile name="prod"> и <springProperty>, а файл читается после настроек Boot). В локальной разработке нужен читаемый текст, в проде однострочный JSON: сборщик логов разбирает его по полям, и traceId из MDC становится колонкой, по которой ищут. С Boot 3.4 структурный вывод включается одной строкой logging.structured.format.console=ecs (или logstash), раньше подключали logstash-logback-encoder.
Что писать: событие и его идентификаторы («заказ 42 подтверждён», а не «вошли в метод confirm»), ошибки вместе с исключением (log.error("...", e), а не e.getMessage(), иначе теряется стек). Что не писать: персональные данные, токены и пароли, тела запросов целиком, и ничего на уровне INFO в цикле по тысяче элементов. Уровень DEBUG для того и существует, чтобы включать его точечно по пакету на время разбора, а не держать всегда.
Коротко
- Стартер — готовый набор зависимостей под одну задачу в согласованных версиях; официальные зовутся
spring-boot-starter-*, сторонние — наоборот,*-spring-boot-starter. - Auto-configuration сама настраивает бины, глядя на подключённые библиотеки; включается через
@SpringBootApplication. - Условные аннотации решают, применять ли настройку:
@ConditionalOnClass(есть ли класс),@ConditionalOnMissingBean(нет ли уже бина),@ConditionalOnProperty(стоит ли значение). - Ваши бины всегда главнее автоматических; не создался бин — поставьте
debug=trueи посмотрите отчёт о решениях при старте. - Настройки держат снаружи кода; чем ближе значение к запуску, тем оно главнее.
@Value— одно значение,@ConfigurationProperties— группа,@Validatedроняет старт при неверном. - Профили дают разные настройки для окружений;
application-<профиль>.ymlнакладывается поверх общего файла. - Старт это цепочка событий до
ApplicationReadyEvent; проверки и прогрев живут вApplicationRunner, неверная настройка обязана ронять запуск. - Логи: уровни по пакетам и профилям, формат в
logback-spring.xml, в проде JSON; в лог идут события с идентификаторами и исключения, не персональные данные. - Свой стартер объявляет порядок через
@AutoConfiguration(after =, before =), иначе@ConditionalOnMissingBeanсрабатывает случайно; почему бина нет, показываетdebug=true,--debugили/actuator/conditionsв проде. - Внешние файлы и секреты подтягивает
spring.config.import(optional:file:,configtree:); профили объединяют вspring.profiles.group, условие документа этоspring.config.activate.on-profile, аspring.profiles.activeв профильном файле запрещено; незнакомый ключ игнорируется молча, подсказки даётspring-boot-configuration-processor.
Что почитать дальше
- DI/IoC, bean lifecycle, scopes — как Spring создаёт бины, на которых строится auto-configuration.
- Spring AOP —
@EnableAsync,@EnableCaching,@EnableTransactionManagementтоже включаются через auto-configuration. - Spring Testing — как проверять
@ConfigurationPropertiesи профили в тестах. - Библиотека usecase-pattern — пример реального стартера со своей auto-configuration.