JavaRush /Курсы /Spring Core /Профили Spring: active

Профили Spring: active/ default/ expr

Spring Core
14 уровень , 2 лекция
Открыта

1. Профили и момент refresh()

Если вы когда-нибудь пытались «переобуться на лету» и включить профиль после старта контекста, то уже знаете это ощущение: вроде бы профиль выставили, а бины — как были, так и остались. Здесь важно поймать правильную картинку. Профили влияют на регистрацию и фильтрацию BeanDefinition, а это происходит во время refresh() (или внутри конструктора контекста, который вызывает refresh() автоматически). После старта контейнер не обязан пересобирать себя заново — он не конструктор LEGO по вызову, он скорее «собранный робот», который уже побежал.

Чтобы увидеть этот момент «вживую», полезно держать в голове очень короткую схему старта AnnotationConfigApplicationContext:

flowchart TD
    A["Создали контекст"] --> B["Настроили Environment (profiles, properties)"]
    B --> C["Зарегистрировали конфигурацию (register)"]
    C --> D["refresh()"]
    D --> E["Фильтрация @Profile и регистрация BeanDefinitions"]
    E --> F["Создание singleton beans"]
    F --> G["Контекст готов"]

Ключевая мысль здесь простая: профили должны быть определены до шага refresh(). Если refresh() уже прошёл, ваши @Profile("demo")-бины либо уже попали в контекст, либо уже были выкинуты — и обратно «включить» их одним сеттером не получится.

Ниже — маленький пример типичной ошибки. Он особенно коварный, потому что на вид кажется логичным: «ну я же просто выставлю профиль в environment».

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class WrongProfileTiming {
    public static void main(String[] args) {
        // ВАЖНО: этот конструктор сам поднимает контекст и внутри вызывает refresh()
        try (AnnotationConfigApplicationContext ctx =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Ошибка: профиль меняем слишком поздно — BeanDefinition уже отфильтрованы по @Profile
            ctx.getEnvironment().setActiveProfiles("demo");
            // Проверяем наличие бина, который должен быть только в demo-профиле
            System.out.println(ctx.containsBean("fileAuditWriter")); // false
        }
    }
}

Комментарий к этому коду звучит почти как диагноз: AnnotationConfigApplicationContext(AppConfig.class) уже поднял контекст, внутри конструктора был выполнен refresh(). Да, Environment внутри объекта вы поменяли, но фильтрация @Profile уже случилась раньше. В результате fileAuditWriter (который вы регистрировали, скажем, в demo-профиле) так и не появится, даже если вы потом сто раз скажете «профиль demo, честно-честно».

2. Активация профилей

Программно через setActiveProfiles(...)

Когда мы учимся, самый понятный путь — включать профиль из кода. Это не «продакшен-стратегия на всю жизнь», но это великолепный способ убедиться, что вы понимаете механику. В чистом Spring (без Boot) самый прямой способ — создать контекст пустым, выставить активный профиль в Environment, зарегистрировать конфигурацию и только потом вызвать refresh(). То есть мы буквально руками делаем то, что Boot потом будет «делать за нас».

Вот рабочий минимальный шаблон. Обратите внимание: мы используем пустой конструктор и вызываем refresh() явно.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ProfiledStartup {
    public static void main(String[] args) {
        // Создаём контекст "пустым": он ещё не refreshed, бины ещё не отфильтрованы
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            // Сначала задаём активный профиль (это важно сделать ДО refresh)
            ctx.getEnvironment().setActiveProfiles("demo");
            // Регистрируем конфигурацию (классы @Configuration / @ComponentScan)
            ctx.register(AppConfig.class);
            // Теперь запускаем refresh() — именно здесь применяется @Profile-фильтрация
            ctx.refresh();

            // Проверяем, что бин из demo действительно попал в контекст
            System.out.println(ctx.containsBean("fileAuditWriter")); // true (в demo)
        }
    }
}

В этом примере всё происходит в «правильном» порядке. Профиль установлен до refresh(), поэтому @Profile("demo") действительно влияет на то, какие BeanDefinition попадут в контейнер.

Отдельно полезно помнить, что setActiveProfiles(...) принимает несколько значений. Spring это умеет, но для ContextFlow этого дня лучше держать один верхнеуровневый режим на запуск: dev или demo или test. Так сборка остаётся читаемой, а не превращается в комбинаторику из случайных комбинаций.

Через property: spring.profiles.active

Профили — штука про окружение, а окружение чаще всего хочется задавать снаружи, а не править код и не перекомпилировать приложение. Spring это понимает и поэтому умеет читать активные профили из Environment по ключу spring.profiles.active. Для нас это удобно: можно запускать одно и то же приложение в разных режимах, просто меняя VM option или переменную окружения. И да, это работает и в чистом Spring, не только в Spring Boot.

С практической точки зрения существует несколько «точек входа», откуда значение может попасть в Environment. Их удобно увидеть в небольшой таблице:

Способ Пример Когда удобно
JVM system property -Dspring.profiles.active=demo локальный запуск из IDE, скрипты, Gradle run
Программно через System.setProperty(...) System.setProperty("spring.profiles.active", "dev"); учебные примеры и демо (но осторожно)
Переменная окружения SPRING_PROFILES_ACTIVE=demo запуск в CI, контейнеры, «внешний» конфиг ОС

Самый «честный» демонстрационный пример — выставить system property до создания контекста. Тогда можно даже использовать «удобный» конструктор AnnotationConfigApplicationContext(AppConfig.class), потому что во время его работы значение уже будет доступно:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class PropertyDrivenProfileStartup {
    public static void main(String[] args) {
        // ВАЖНО: выставляем system property до создания контекста,
        // чтобы оно попало в Environment ещё до refresh()
        System.setProperty("spring.profiles.active", "dev");

        // Этот конструктор сам вызовет refresh(), и профиль уже будет виден контейнеру
        try (AnnotationConfigApplicationContext ctx =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // В этом примере держим один верхнеуровневый режим, поэтому спокойно печатаем первый элемент
            System.out.println(ctx.getEnvironment().getActiveProfiles()[0]); // dev
        }
    }
}

Здесь есть важный нюанс: System.setProperty(...) меняет состояние всей JVM. В маленьком консольном приложении это простительно, но в тестах или в сложном запуске можно получить забавный эффект «первый тест включил demo — остальные внезапно тоже demo». Поэтому как только вы почувствуете, что у вас появляется больше одного сценария запуска, программный setActiveProfiles(...) в локальном контексте становится безопаснее и предсказуемее, чем глобальное System.setProperty(...).

3. Профиль по умолчанию

Теперь разберём штуку, которая часто путает новичков: профиль default. Он не означает «профиль для продакшена» и не означает «режим по умолчанию, который всегда вместе со всеми». По смыслу это скорее «режим запасного парашюта»: он используется только если активные профили вообще не заданы. Как только вы выставили хотя бы один active profile — default перестаёт участвовать в игре.

Это удобно проверить очень коротким фрагментом кода. Он показывает то, что многие не ожидают: активных профилей может быть ноль, но default профили при этом всё равно существуют.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;

public class DefaultProfilesDemo {
    public static void main(String[] args) {
        // Контекст создаём без профилей и без конфигурации: просто смотрим, что в Environment по умолчанию
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            // Active профили — пусто, потому что мы ничего не задавали
            System.out.println(Arrays.toString(ctx.getEnvironment().getActiveProfiles()));  // []
            // Default профили существуют всегда, если вы их явно не переопределили
            System.out.println(Arrays.toString(ctx.getEnvironment().getDefaultProfiles())); // [default]
        }
    }
}

То есть «активные профили» и «default профили» — это две разные сущности. Механика примерно такая: если active не заданы, Spring при проверке @Profile ориентируется на default.

Зачем тогда spring.profiles.default? Чтобы переименовать или заменить набор профилей, которые Spring будет считать «дефолтными», если активный профиль не выбран. Например, если вы хотите, чтобы локальный запуск без дополнительных настроек автоматически становился dev, можно сделать так:

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class CustomDefaultProfileStartup {
    public static void main(String[] args) {
        // Переопределяем набор default-профилей на уровне JVM (для демо)
        System.setProperty("spring.profiles.default", "dev");

        // Контекст создаём "удобным" конструктором: он поднимется сразу,
        // и контейнер увидит default-профиль при refresh()
        try (AnnotationConfigApplicationContext ctx =
                     new AnnotationConfigApplicationContext(AppConfig.class)) {

            // Проверяем, что профиль dev считается подходящим для матчинг-логики
            System.out.println(ctx.getEnvironment().matchesProfiles("dev")); // true
        }
    }
}

И снова — это решение «про окружение». В реальной жизни вы чаще зададите SPRING_PROFILES_DEFAULT=dev или VM option, чем будете хардкодить в main(). Но для понимания механики такой пример отлично работает: вы видите, что «дефолтный» профиль живёт отдельно от «активного», и включается только при отсутствии active.

Ещё один нюанс, который стоит проговорить словами. Аннотация @Profile("default") означает: «этот бин будет зарегистрирован, если активные профили не заданы и среди default-профилей есть default». Это хороший способ держать «базовую» конфигурацию без специальных режимов, но не надо превращать default в «ещё один нормальный профиль» наравне с dev/demo/test. Для проекта куда читаемее, когда вы явно запускаете dev, чем когда пытаетесь угадать, что же там в default.

4. Profile expressions

Профили быстро упираются в бытовую задачу: «этот бин нужен везде, кроме тестов» или «этот бин нужен в dev или demo». Если делать это только через перечисление профилей, можно начать плодить копии. Поэтому Spring поддерживает profile expressions — небольшие логические выражения внутри @Profile, например !test. Это не магия и не отдельный язык программирования, а просто компактная логика выбора.

Чтобы не превращать тему в зубрёжку, достаточно помнить три оператора и скобки. Я специально свёл их в таблицу, потому что мозг новичка любит таблицы больше, чем длинные абзацы (и я его понимаю):

Оператор Смысл Пример
! НЕ !test — «всё, кроме test»
& И demo & debug — «только когда включены оба»
| ИЛИ dev | demo — «когда включён хотя бы один»

Spring умеет матчить и более сложные комбинации, но для ContextFlow верхнеуровневый режим остаётся один: dev или demo или test. Expressions здесь полезны главным образом для точечных условий вроде !test, а не для свободного смешивания всех трёх режимов.

Самый частый и самый полезный пример для нашего ContextFlow: в test-режиме мы хотим отключить реальные побочные эффекты, например «настоящие» уведомления. Тогда бин, отвечающий за «боевые» уведомления, можно пометить так:

import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Component;

// Означает: этот компонент будет создан во всех профилях, КРОМЕ test
@Profile("!test")
@Component
class ConsoleNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        // Здесь мы специально оставляем "боочное" действие: вывод/уведомление,
        // которое в тестах обычно хотят отключить
        System.out.println("NOTIFY: " + message); // NOTIFY: ...
    }
}

Выглядит понятно: тесты — это тесты, там уведомления не нужны. Во всех остальных режимах — пожалуйста.

Ещё один пример — «dev или demo». Его часто пишут либо как @Profile({"dev", "demo"}), либо как выражение @Profile("dev | demo"). Для новичка первый вариант обычно читабельнее, потому что меньше символов, которые похожи на «математику из старшей школы». Но выражение полезно, когда вы хотите добавить отрицание или сочетания.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
class NonTestAuditConfig {
    @Bean
    // Выражение: бин будет зарегистрирован, если активен dev ИЛИ demo
    @Profile("dev | demo")
    AuditWriter consoleAuditWriter() {
        // Возвращаем конкретную реализацию, которую хотим включать только в этих режимах
        return new ConsoleAuditWriter();
    }
}

Здесь важно не обманывать себя: profile expression по-прежнему оценивается при старте контейнера, а не «на каждом вызове метода». То есть это не runtime-ветвление, это всё та же conditional registration. Если вы включили профиль — бин либо будет, либо не будет. Никаких «сегодня я dev, а через минуту demo» внутри одного контекста. И слава богу, иначе debugging превратился бы в хоррор.

5. Печать active и default профилей

Когда профили уже начали жить в конфигурации, очень хочется быстрой самопроверки: «а я точно запустился в том режиме, в котором думаю?» Это особенно полезно в учебном проекте, где вы часто переключаете dev/demo/test и хотите увидеть, что контейнер действительно собрал разные реализации. Самый простой способ — напечатать профили в main() после refresh().

Ниже — минимальный пример для запуска ContextFlow с профилем из аргументов командной строки. Он не превращает проект в CLI-комбайн, но даёт честный контроль над сборкой.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ContextFlowMain {
    public static void main(String[] args) {
        // Берём профиль из аргументов командной строки, иначе используем dev как "понятный дефолт" для учебного запуска
        String profile = (args.length > 0) ? args[0] : "dev";

        // Создаём контекст пустым, чтобы успеть выставить профиль ДО refresh()
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
            // Профиль должен быть задан до регистрации/refresh, чтобы @Profile успел отфильтровать бины
            ctx.getEnvironment().setActiveProfiles(profile);
            // Регистрируем конфигурацию приложения
            ctx.register(AppConfig.class);
            // Поднимаем контекст
            ctx.refresh();

            // Быстрая самопроверка: в каком режиме мы реально запускались
            System.out.println("Active profile = " + profile); // Active profile = dev
            // Запускаем сценарий, который использует собранный набор бинов
            ctx.getBean(ScenarioRunner.class).run();
        }
    }
}

Обратите внимание на один методический момент: мы печатаем то, что выбрали, а не пытаемся «догадаться» по косвенным признакам. Да, можно вывести ctx.getEnvironment().getActiveProfiles(), но в учебном коде иногда даже проще распечатать переменную profile, чтобы глаз сразу зацепился. Когда приложение станет крупнее, вы, конечно, будете логировать это аккуратнее, но сейчас мы просто делаем контейнерное поведение наблюдаемым.

Если хочется чуть более формально, можно распечатать оба массива — active и default. Только не пугайтесь, когда active будет пустым: это нормально, если вы запускаете без явного профиля и полагаетесь на default.

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import java.util.Arrays;

public class ProfilesPrint {
    public static void main(String[] args) {
        // Здесь используем "удобный" конструктор — он сразу поднимет контекст
        try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            // Active профили: то, что явно включили (может быть пусто)
            System.out.println(Arrays.toString(ctx.getEnvironment().getActiveProfiles()));  // []
            // Default профили: fallback-набор, который используется, если active пуст
            System.out.println(Arrays.toString(ctx.getEnvironment().getDefaultProfiles())); // [default]
        }
    }
}

6. Типичные ошибки при работе с профилями

Ошибка №1: активировать профиль после refresh() и ожидать, что контекст «пересоберётся».
Это самая популярная ловушка. Профиль влияет на регистрацию бинов, а регистрация происходит до создания объектов. Когда контекст уже поднят, менять profile в Environment всё равно что менять план дома после того, как вы его построили. Можно, конечно, снести и построить заново, но это уже другой контекст, а не «переключение режима».

Ошибка №2: думать, что default работает одновременно с любым активным профилем.
Механика default — это fallback при отсутствии active. Как только вы выставили dev, demo или test, профиль default перестаёт учитываться при @Profile-проверках. Из-за этого бин с @Profile("default") внезапно исчезает, и новичок думает: «Spring сломался». Нет, просто так задумано.

Ошибка №3: ставить System.setProperty("spring.profiles.active"...) в тестах и забывать очищать.
System properties — глобальны на JVM. Один тест выставил demo, другой тест рассчитывал на test или на пустой active — и вы получаете flaky tests без единой строчки многопоточности. В учебных примерах System.setProperty допустим, но в реальности лучше управлять профилями через Environment конкретного контекста или через настройки запуска тестового контекста.

Ошибка №4: превращать профили в «настройки для каждой мелочи».
Профиль должен описывать режим работы приложения, а не значение одной цифры. Если вы сделали профили limit-10, limit-20, limit-30, то вы построили систему, которую невозможно поддерживать: комбинаций слишком много, смысл профилей размывается. Для цифр и строк у нас есть properties; профили — для выбора набора реализаций.

Ошибка №5: писать выражения профилей так, что их понимает только автор и его кот (и то не всегда).
@Profile("dev & (demo | !test) & (blue | green)") выглядит умно ровно до первого code review. Profile expressions полезны, но лучше держать их простыми: !test, dev | demo, максимум что-то вроде demo & debug. Если выражение сложнее, обычно выигрывает разбиение конфигурации на понятные модули и явные профили.

1
Задача
Spring Core, 14 уровень, 2 лекция
Недоступна
Профиль по умолчанию через `spring.profiles.default`
Профиль по умолчанию через `spring.profiles.default`
1
Задача
Spring Core, 14 уровень, 2 лекция
Недоступна
Profile expression `!test` и активация через `spring.profiles.active`
Profile expression `!test` и активация через `spring.profiles.active`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ