← назад к разделу

Главное свойство Spring Boot — он почти ничего не заставляет настраивать руками. Добавили одну зависимость для работы с базой — и JdbcTemplate уже есть. Добавили зависимость для веба — и приложение само поднимает встроенный сервер. Разберём, как это устроено и откуда приложение берёт настройки.

классы настройки из стартеров условия контекст DataSource AutoConfiguration Kafka AutoConfiguration JdbcTemplate AutoConfiguration @ConditionalOnClass ✓@ConditionalOnProperty ✓DataSource @ConditionalOnClass ✗библиотеки нет в проектепропущено @ConditionalOnMissingBean ✗вы объявили свой бинваш JdbcTemplate debug=true печатает этот же разбор при старте

Каждый класс настройки из стартера проходит проверку условий. Нет библиотеки — кандидат пропускается; ваш бин уже объявлен — настройка отступает. В контекст попадает только то, что прошло условия.

Обязательно

Стартеры — наборы зависимостей одним пунктом

Раньше, чтобы подключить, скажем, веб-слой, приходилось вручную перечислять десяток библиотек: сам 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.

--server.port=9000 аргумент запуска нет значения SERVER_PORT переменная окружения нет значения application-prod.yml файл профиля нет значения application.yml рядом лежит рядом с jar нет значения application.yml в jar упакован в сборку

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.