环境抽象
Environment
接口是集成在容器中的一个抽象,模拟应用环境的两个关键方面:profiles和properties。
Profile是一个命名的逻辑bean定义组,仅在给定profile处于活动状态时才向容器注册。无论是在XML中定义还是使用注解,都可以将bean分配给一个profile。Environment对象与profiles的关系在于确定当前活动的profiles(如果有的话),以及默认情况下应该活动的profiles(如果有的话)。
Properties在几乎所有应用程序中都扮演着重要角色,可能来自各种来源:属性文件、JVM系统属性、系统环境变量、JNDI、servlet上下文参数、临时Properties对象、Map对象等等。Environment对象与properties的关系在于为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。
Bean定义配置文件
Bean定义配置文件提供了一个在核心容器中的机制,允许在不同环境中注册不同的bean。 “环境”一词对不同的用户可能有不同的含义,这个特性可以帮助解决许多用例,包括:
-
在开发中使用内存数据源与在QA或生产环境中从JNDI查找相同数据源的区别。
-
仅在将应用部署到性能环境时注册监控基础设施。
-
为A客户和B客户部署注册定制的bean实现。
考虑一个需要DataSource
的实际应用中的第一个用例。在测试环境中,配置可能如下所示:
-
Java
-
Kotlin
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build();
}
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("my-schema.sql")
.addScript("my-test-data.sql")
.build()
}
现在考虑如何将此应用部署到QA或生产环境中,假设应用的数据源已在生产应用服务器的JNDI目录中注册。我们的dataSource
bean现在如下所示:
-
Java
-
Kotlin
@Bean(destroyMethod = "")
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
@Bean(destroyMethod = "")
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
问题在于如何根据当前环境在这两种变体之间进行切换。随着时间的推移,Spring用户已经想出了许多方法来解决这个问题,通常依赖于一组系统环境变量和包含${placeholder}
标记的XML <import/>
语句,这些标记根据环境变量的值解析为正确的配置文件路径。 Bean定义配置文件是一个核心容器功能,提供了解决这个问题的解决方案。
如果我们概括前面示例中显示的特定环境bean定义的用例,我们最终需要在某些上下文中注册某些bean定义,而在其他上下文中不注册。您可以说您希望在情况A中注册一组特定的bean定义配置文件,在情况B中注册另一组不同的配置文件。我们首先更新我们的配置以反映这种需求。
Using @Profile
The @Profile
annotation lets you indicate that a component is eligible for registration when one or more specified profiles are active. Using our preceding example, we can rewrite the dataSource
configuration as follows:
-
Java
-
Kotlin
@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();
}
}
@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
-
Kotlin
@Configuration
@Profile("production")
public class JndiDataConfig {
@Bean(destroyMethod = "") (1)
public DataSource dataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
1 | @Bean(destroyMethod = "") disables default destroy method inference. |
@Configuration
@Profile("production")
class JndiDataConfig {
@Bean(destroyMethod = "") (1)
fun dataSource(): DataSource {
val ctx = InitialContext()
return ctx.lookup("java:comp/env/jdbc/datasource") as DataSource
}
}
1 | @Bean(destroyMethod = "") disables default destroy method inference. |
As mentioned earlier, with @Bean methods, you typically choose to use programmatic JNDI lookups, by using either Spring’s JndiTemplate /JndiLocatorDelegate helpers or the straight JNDI InitialContext usage shown earlier but not the JndiObjectFactoryBean variant, which would force you to declare the return type as the FactoryBean type. |
The profile string may contain a simple profile name (for example, production
) or a profile expression. A profile expression allows for more complicated profile logic to be expressed (for example, production & us-east
). The following operators are supported in profile expressions:
-
!
: A logicalNOT
of the profile -
&
: A logicalAND
of the profiles -
|
: A logicalOR
of the profiles
You cannot mix the & and | operators without using parentheses. For example, production & us-east | eu-central is not a valid expression. It must be expressed as production & (us-east | eu-central) . |
You can use @Profile
as a meta-annotation for the purpose of creating a custom composed annotation. The following example defines a custom @Production
annotation that you can use as a drop-in replacement for @Profile("production")
:
-
Java
-
Kotlin
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@Profile("production")
annotation class Production
If a @Configuration class is marked with @Profile , all of the @Bean methods and @Import annotations associated with that class are bypassed unless one or more of the specified profiles are active. If a @Component or @Configuration class is marked with @Profile({"p1", "p2"}) , that class is not registered or processed unless profiles 'p1' or 'p2' have been activated. If a given profile is prefixed with the NOT operator (! ), the annotated element is registered only if the profile is not active. For example, given @Profile({"p1", "!p2"}) , registration will occur if profile 'p1' is active or if profile 'p2' is not active. |
@Profile
can also be declared at the method level to include only one particular bean of a configuration class (for example, for alternative variants of a particular bean), as the following example shows:
-
Java
-
Kotlin
@Configuration
public class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
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") (2)
public DataSource jndiDataSource() throws Exception {
Context ctx = new InitialContext();
return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}
}
1 | The standaloneDataSource method is available only in the development profile. |
2 | The jndiDataSource method is available only in the production profile. |
@Configuration
class AppConfig {
@Bean("dataSource")
@Profile("development") (1)
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") (2)
fun jndiDataSource() =
InitialContext().lookup("java:comp/env/jdbc/datasource") as DataSource
}
1 | The standaloneDataSource method is available only in the development profile. |
2 | The jndiDataSource method is available only in the production profile. |
在 如果要使用不同的配置条件定义替代bean,请使用指向相同bean名称的不同Java方法名称,并使用 |
XML Bean Definition Profiles
XML Bean Definition Profiles(XML Bean定义配置文件)的对应部分是<beans>
元素的profile
属性。我们之前的示例配置可以重写为两个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="...">
<!-- 其他bean定义 -->
<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对应部分不支持之前描述的profile表达式。但是,可以通过使用
在上面的示例中,如果 |
激活配置文件
现在我们已经更新了我们的配置,我们仍然需要告诉Spring哪个配置文件是活动的。如果我们现在启动我们的示例应用程序,会看到一个NoSuchBeanDefinitionException
异常被抛出,因为容器找不到名为dataSource
的Spring bean。
激活配置文件可以通过多种方式完成,但最直接的方法是通过ApplicationContext
中可通过Environment
API进行编程。以下示例展示了如何实现:
-
Java
-
Kotlin
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();
val ctx = AnnotationConfigApplicationContext().apply {
environment.setActiveProfiles("development")
register(SomeConfig::class.java, StandaloneDataConfig::class.java, JndiDataConfig::class.java)
refresh()
}
此外,还可以通过spring.profiles.active
属性声明性地激活配置文件,该属性可以通过系统环境变量、JVM系统属性、web.xml
中的servlet上下文参数,甚至作为JNDI中的条目来指定。在集成测试中,可以使用spring-test
模块中的@ActiveProfiles
注解来声明活动配置文件(参见带环境配置文件的上下文配置)。
请注意,配置文件不是“要么...要么”的命题。您可以同时激活多个配置文件。在编程方式下,可以向setActiveProfiles()
方法提供多个配置文件名称,该方法接受String…
可变参数。以下示例激活多个配置文件:
-
Java
-
Kotlin
ctx.getEnvironment().setActiveProfiles("profile1", "profile2");
ctx.getEnvironment().setActiveProfiles("profile1", "profile2")
在声明方式下,spring.profiles.active
可以接受一个逗号分隔的配置文件名称列表,如下例所示:
-Dspring.profiles.active="profile1,profile2"
默认配置文件
默认配置文件代表如果没有配置文件处于活动状态,则启用的配置文件。考虑以下示例:
-
Java
-
Kotlin
@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();
}
}
@Configuration
@Profile("default")
class DefaultDataConfig {
@Bean
fun dataSource(): DataSource {
return EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.HSQL)
.addScript("classpath:com/bank/config/sql/schema.sql")
.build()
}
}
如果没有配置文件处于活动状态,将创建dataSource
。您可以将其视为为一个或多个bean提供默认定义的一种方式。如果启用了任何配置文件,则默认配置文件不适用。
默认配置文件的名称是default
。您可以通过在Environment
上使用setDefaultProfiles()
或通过spring.profiles.default
属性在声明方式下更改默认配置文件的名称。
PropertySource
抽象
Spring的Environment
抽象提供了对可配置属性源层次结构的搜索操作。考虑以下清单:
-
Java
-
Kotlin
ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("当前环境是否包含'my-property'属性? " + containsMyProperty);
val ctx = GenericApplicationContext()
val env = ctx.environment
val containsMyProperty = env.containsProperty("my-property")
println("当前环境是否包含'my-property'属性? $containsMyProperty")
在上面的代码片段中,我们看到了一种高级的方式询问Spring当前环境是否定义了my-property
属性。为了回答这个问题,Environment
对象在一组PropertySource
对象上执行搜索。一个PropertySource
是对任何键值对源的简单抽象,Spring的StandardEnvironment
配置了两个PropertySource对象,一个代表JVM系统属性的集合(System.getProperties()
),另一个代表系统环境变量的集合(System.getenv()
)。
这些默认属性源适用于StandardEnvironment ,用于独立应用程序。StandardServletEnvironment 填充了额外的默认属性源,包括servlet配置、servlet上下文参数,以及如果JNDI可用,则包括一个JndiPropertySource 。 |
具体来说,当您使用StandardEnvironment
时,调用env.containsProperty("my-property")
如果在运行时存在my-property
系统属性或my-property
环境变量,则返回true。
执行的搜索是分层的。默认情况下,系统属性优先于环境变量。因此,如果在调用 对于常见的
|
最重要的是,整个机制是可配置的。也许您有一个自定义的属性源要集成到这个搜索中。为此,实现并实例化自己的PropertySource
,并将其添加到当前Environment
的PropertySources
集合中。以下示例显示了如何实现:
-
Java
-
Kotlin
ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());
val ctx = GenericApplicationContext()
val sources = ctx.environment.propertySources
sources.addFirst(MyPropertySource())
在上述代码中,MyPropertySource
已经以最高优先级添加到搜索中。如果它包含一个my-property
属性,则检测并返回该属性,优先于任何其他PropertySource
中的my-property
属性。MutablePropertySources
API公开了许多方法,允许对属性源集合进行精确操作。
使用 @PropertySource
@PropertySource
注解提供了一种方便且声明性的机制,用于向 Spring 的 Environment
添加一个 PropertySource
。
假设有一个名为 app.properties
的文件,其中包含键值对 testbean.name=myTestBean
,以下的 @Configuration
类使用 @PropertySource
的方式,使得调用 testBean.getName()
返回 myTestBean
:
-
Java
-
Kotlin
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
@Configuration
@PropertySource("classpath:/com/myco/app.properties")
class AppConfig {
@Autowired
private lateinit var env: Environment
@Bean
fun testBean() = TestBean().apply {
name = env.getProperty("testbean.name")!!
}
}
在 @PropertySource
资源位置中存在任何 ${…}
占位符时,这些占位符将根据已经注册到环境中的属性源集进行解析,如下例所示:
-
Java
-
Kotlin
@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {
@Autowired
Environment env;
@Bean
public TestBean testBean() {
TestBean testBean = new TestBean();
testBean.setName(env.getProperty("testbean.name"));
return testBean;
}
}
@Configuration
@PropertySource("classpath:/com/\${my.placeholder:default/path}/app.properties")
class AppConfig {
@Autowired
private lateinit var env: Environment
@Bean
fun testBean() = TestBean().apply {
name = env.getProperty("testbean.name")!!
}
}
假设 my.placeholder
已经存在于已注册的属性源之一(例如系统属性或环境变量)中,则该占位符将解析为相应的值。如果没有,则使用 default/path
作为默认值。如果未指定默认值且无法解析属性,则会抛出 IllegalArgumentException
。
@PropertySource 可以作为可重复注解使用。 @PropertySource 也可以作为元注解使用,以创建具有属性覆盖的自定义组合注解。 |
语句中的占位符解析
在历史上,元素中的占位符值只能根据 JVM 系统属性或环境变量解析。但现在不再是这样了。由于 Environment
抽象已经整合到容器中,因此可以通过它轻松地路由占位符的解析。这意味着您可以根据需要配置解析过程。您可以更改通过系统属性和环境变量搜索的优先级,或者完全删除它们。您还可以根据需要向混合中添加自己的属性源。
具体来说,以下语句无论 customer
属性定义在何处,只要在 Environment
中可用,都可以正常工作:
<beans>
<import resource="com/bank/service/${customer}-config.xml"/>
</beans>