Интерфейс Environment – это абстракция, интегрированная в контейнер, которая моделирует два ключевых аспекта окружения приложения: profiles и properties.

Профиль (profile) – это именованная логическая группа определений бинов, которые будут зарегистрированы в контейнере только в том случае, если данный профиль активен. Бины могут быть отнесены к профилю, определенному в XML или с помощью аннотаций. Роль объекта Environment по отношению к профилям заключается в определении того, какие профили (если таковые имеются) в текущий момент активны, а какие (если таковые имеются) должны быть активны по умолчанию.

Свойства (properties) играют важную роль почти во всех приложениях и могут происходить из различных источников: файлы свойств, системные свойства JVM, переменные системного окружения, JNDI, параметры контекста сервлетов, специально подобранные объекты Properties, объекты Map и так далее. Роль объекта Environment по отношению к свойствам заключается в предоставлении пользователю удобного служебного интерфейса для настройки источников свойств и выполнения разрешения свойств из них.

Профили определения бинов

Профили определения бинов предоставляют собой механизм в основном контейнере, который позволяет регистрировать различные бины в различных окружениях. Слово "окружение" может означать разные вещи для разных пользователей, и эта функция может помочь во многих случаях, включая:

  • Работу с источником данных в памяти в процессе разработки по сравнению с поиском этого же источника данных из JNDI на этапе контроля качества (QA) или производства.

  • Регистрацию инфраструктуры мониторинга исключительно при развертывании приложения в эксплуатационной среде.

  • Регистрацию специальных реализаций бинов для развертывания клиента А по сравнению с развертыванием клиента B.

Рассмотрим первый случай использования в практическом приложении, для которого требуется источник данных DataSource. В тестовой среде конфигурация может выглядеть следующим образом:

Java
@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}
Kotlin
@Bean
fun dataSource(): DataSource {
    return EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("my-schema.sql")
            .addScript("my-test-data.sql")
            .build()
}

Теперь рассмотрим, каким образом данное приложение можно развернуть в QA-окружении или производственном окружении, условившись, что источник данных для приложения зарегистрирован в каталоге JNDI производственного сервера приложений. Теперь наш бин dataSource выглядит следующим образом:

Java
@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
Kotlin
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
    val ctx = InitialContext()
    return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}

Проблема заключается в том, каким образом переключаться между этими двумя вариантами в зависимости от текущего окружения. Со временем пользователи Spring придумали несколько способов решить эту проблему, обычно используя комбинацию системных переменных окружения и инструкции <import/> на XML, содержащие маркеры ${placeholder}, которые разрешаются в правильный путь к файлу конфигурации в зависимости от значения переменной окружения. Профили определения бинов – это функция основного контейнера, которая обеспечивает решение этой проблемы.

Если обобщить показанный в предыдущем примере случай использования специфичных для окружения определений бинов, то в итоге получаем необходимость регистрировать конкретные определения бинов в одних контекстах, но не в других. Допустим, вам нужно зарегистрировать определенный профиль определений бина в ситуации A и другой профиль в ситуации B. Начнем с обновления нашей конфигурации, чтобы отразить эту потребность.

Использование @Profile

Аннотация @Profile позволяет указывать, что компонент может быть зарегистрирован, если активен один или несколько указанных профилей. Используя наш предыдущий пример, мы можем переписать конфигурацию dataSource следующим образом:

Java
@Configuration
@Profile("development")
public class StandaloneDataConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}
Kotlin
@Configuration
@Profile("development")
class StandaloneDataConfig {
    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .addScript("classpath:com/bank/config/sql/test-data.sql")
                .build()
    }
}
Java
@Configuration
@Profile("production")
public class JndiDataConfig {
    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
Kotlin
@Configuration
@Profile("production")
class JndiDataConfig {
    @Bean(destroyMethod = "")
    fun dataSource(): DataSource {
        val ctx = InitialContext()
        return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
    }
}
Как упоминалось ранее, при использовании методов, аннотированных @Bean, обычно предпочтительным является задействование программного поиска JND, ли пользуясь вспомогательными классами JndiTemplate/JndiLocatorDelegate из Spring, либо путем прямого задействования InitialContext, как было продемонстрировано ранее, но не варианта JndiObjectFactoryBean из JNDI, что создаст необходимость объявить возвращаемый тип как тип FactoryBean.

Строка профиля может содержать простое имя профиля (например, production) или выражение профиля. Выражение профиля позволяет выразить более сложную логику профиля (например, production & us-east). В выражениях профиля поддерживаются следующие операторы:

  • !: Логическое "not" профиля

  • &: Логическое "and" профилей

  • |: Логическое "or" профилей

Нельзя смешивать операторы & и | без использования круглых скобок. Например, production & us-east | eu-central не является допустимым выражением. Оно должно иметь вид production & (us-east | eu-central).

Вы можете использовать @Profile в качестве мета-аннотации для создания специальной составной аннотации. Следующий пример определяет специальную аннотацию @Production, которую можно использовать в качестве замены @Profile("production"):

Java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
Kotlin
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
Если класс, аннотированный @Configuration, помечен @Profile, все методы, аннотированные @Bean, и аннотации @Import, связанные с этим классом, обходятся, если только один или несколько указанных профилей не активны. Если класс, аннотированный @Component или @Configuration, помечен @Profile({"p1", "p2"}), то этот класс не регистрируется и не обрабатывается, если не активированы профили 'p1' или 'p2'. Если данный профиль имеет префикс с оператором NOT (!), аннотируемый элемент регистрируется только в том случае, если профиль не активен. Например, если взять @Profile({"p1", "!p2"}), регистрация произойдет, если профиль 'p1' активен или если профиль 'p2' не активен.

@Profile также можно объявить на уровне метода для включения только одного конкретного бина конфигурационного класса (например, для альтернативных вариантов конкретного бина), как показано в следующем примере:

Java
@Configuration
public class AppConfig {
    @Bean("dataSource")
    @Profile("development") 
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
    @Bean("dataSource")
    @Profile("production") 
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
  1. Метод standaloneDataSource доступен только в профиле development.
  2. Метод jndiDataSource доступен только в профиле production.
Kotlin
@Configuration
class AppConfig {
    @Bean("dataSource")
    @Profile("development") 
    fun standaloneDataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .addScript("classpath:com/bank/config/sql/test-data.sql")
                .build()
    }
    @Bean("dataSource")
    @Profile("production") 
    fun jndiDataSource() =
        InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
  1. Метод standaloneDataSource доступен только в профиле development.
  2. Метод jndiDataSource доступен только в профиле production.

При использовании @Profile в методах, помеченных аннотацией @Bean, может иметь место особый сценарий: В случае перегруженных методов, аннотированных @Bean, с одинаковым именем метода Java (аналогично перегрузке конструктора), условие @Profile должно быть последовательно объявлено для всех перегруженных методов. Если условия несовместимы, то имеет значение только условие первого объявления среди перегруженных методов. Поэтому @Profile нельзя использовать для выбора перегруженного метода с определенной сигнатурой аргумента вместо другого. Разрешение между всеми фабричными методами для одного и того же бина следует алгоритму разрешения конструкторов Spring во время создания.

Если вы хотите определить альтернативные бины с различными условиями профиля, используйте разные имена методов Java, которые указывают на одно и то же имя бина с помощью атрибута имени с аннотацией @Bean , как показано в предыдущем примере. Если сигнатуры аргументов одинаковы (например, все варианты имеют фабричные методы без аргументов), то это единственный способ представить такую организацию в допустимом классе Java (поскольку может существовать только один метод с определенным именем и сигнатурой аргументов).

XML-профили определения бинов

Аналогом на XML является атрибут profile элемента <beans>. Наш предыдущий пример конфигурации может быть переписан в два XML-файла следующим образом:

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">
    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<beans profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">
    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

Можно также избежать этого разделения и вложить элементы <beans/> в один файл, как показано в следующем примере:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">
    <!-- другие определения бинов -->
    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>
    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

В файле spring-bean.xsd разрешены только такие элементы, как последние в файле. Это должно помочь обеспечить гибкость, не создавая нагромождения в XML-файлах.

XML-аналог не поддерживает выражения профиля, описанные ранее. Однако можно выполнить логическую операцию отрицания профиля с помощью оператора ! Можно также применить логическое "and" путем вложения профилей, как показано в следующем примере:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">
    <!-- другие определения бинов -->
    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>

В предыдущем примере бин dataSource открывается, если активны оба профиля - production и us-east.

Активация профиля

Теперь, когда наша конфигурация обновлена, нам все еще требуется указать Spring, какой профиль является активным. Если бы мы запустили наш пример приложения прямо сейчас, мы бы увидели, что возникла ошибка NoSuchBeanDefinitionException, потому что контейнер не смог найти бин Spring с именем dataSource.

Активация профиля может быть выполнена несколькими способами, но самый простой - сделать это программно с помощью API-интерфейса Environment, который доступен через интерфейс ApplicationContext. В следующем примере показано, как это сделать:

Java
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
Kotlin
val ctx = AnnotationConfigApplicationContext().apply {
    environment.setActiveProfiles("development")
    register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
    refresh()
}

Кроме того, можно декларативно активировать профили через свойство spring.profiles.active, которое можно задать через переменные системного окружения, системные свойства JVM, параметры контекста сервлетов в web.xml или даже как запись в JNDI. В комплексных (интеграционных) тестированиях активные профили могут быть объявлены с помощью аннотации @ActiveProfiles в модуле spring-test.

Обратите внимание, что профили не являются высказыванием "either-or". Можно активировать несколько профилей одновременно. Программно можно указать несколько имен профилей методу setActiveProfiles(), который принимает String… varargs. В следующем примере активируется несколько профилей:

Java
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
Kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")

Декларативно spring.profiles.active может принимать список имен профилей, разделенных запятыми, как показано в следующем примере:

    -Dspring.profiles.active="profile1,profile2"

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

Профиль по умолчанию представляет собой профиль, который активирован по умолчанию. Рассмотрим следующий пример:

Java
@Configuration
@Profile("default")
public class DefaultDataConfig {
    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}
Kotlin
@Configuration
@Profile("default")
class DefaultDataConfig {
    @Bean
    fun dataSource(): DataSource {
        return EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.HSQL)
                .addScript("classpath:com/bank/config/sql/schema.sql")
                .build()
    }
}

Если профиль не активен, создается DataSource. Вы можете считать это способом указания определения по умолчанию для одного или нескольких бинов. Если какой-либо профиль активирован, профиль по умолчанию не применяется.

Вы можете изменить имя профиля по умолчанию с помощью setDefaultProfiles() в Environment или декларативно с помощью свойства spring.profiles.default.