抽象环境的概念
在介绍Spring核心模块为运行环境管理提供的功能之前,咱们先得解释清楚“运行环境”是什么。
码砖早年,对上下文(Context)、环境(Environment)一直都是傻傻分不清楚,感觉2者都是放了一堆参数在里面,貌似并没有多大区别。后来才慢慢摸清楚这2个词的套路。上下文(Context)是用来处理分层传递的,不清楚的可以看看上下文与IoC一文关于ApplicationContext的介绍。
而环境(Environment)是指当前运行程序之外的各种“全局变量”,这些变量反映了当前软件运行的各种外部情况。例如我们执行System.getenv()方法,就会获取到当前包括操作系统、全局路径配置、磁盘、jdk版本等等信息。这些信息实际上与当前运行的程序是无关的——无论你是否启动JVM,这些环境变量都是客观存在的。
既然环境的作用是体现当前运行的各种外部情况,那么除了JVM启动时提供的固定参数,也可以指定我们需要的环境变量。例如我们最常见的环境——开发环境、测试环境、集成QA环境、仿真环境、生产环境等。
Profile特性
对于软件开发而言经常要控制的就是当前程序是在开发环境运行还是在生产环境运行。除了后面要介绍的Spring Profile功能,还有各种各样的方法来进行控制,比如Maven的profile标签。Spring Profile只是一种环境控制的参考手段,他的好处是可以在代码级别去控制,具体使用什么根据项目的需要去考量。
Spring的Profile特性使用起来并不复杂,而且同时支持Java注解和XML配置。我们通过几段代码来说明如何使用Profile。
纯Java常规使用
(以下案例的可执行代码请到gitee下载,)
定义一个servuce接口和三个service的实现类:
package chkui.springcore.example.hybrid.profile.service;
public interface Blizzard {
String getName();
}
package chkui.springcore.example.hybrid.profile.service.blizzard;
class Warcraft implements Blizzard {
public String getName() {
return "Warcraft";
}
}
class WorldOfWarcraft implements Blizzard {
public String getName() {
return "World of Warcraft";
}
}
class Overwatch implements Blizzard {
public String getName() {
return "Overwatch";
}
}
然后我们通过纯Java配置讲接口的每个实现添加到容器中:
@Configuration
public class EnvironmentApp {
public static void main(String[] args) {
//在启动容器之前,先指定环境中的profiles参数
System.setProperty("spring.profiles.active", "wow");
ApplicationContext ctx = new AnnotationConfigApplicationContext(EnvironmentApp.class);
//当前的profile值是wow,所以获取的实现类是worldOfWarcraft
Blizzard blizzard = ctx.getBean(Blizzard.class);
}
@Bean
@Profile("war")
public Blizzard warcraft() {
return new Warcraft();
}
@Bean
@Profile("wow")
public Blizzard worldOfWarcraft() {
return new WorldOfWarcraft();
}
@Bean
@Profile("default")
public Blizzard overwatch() {
return new Overwatch();
}
}
@Configuration类中每一个@Bean注解之后都有一个@Profile注解。@Profile中的字符串就标记了当前适配的环境变量,他配合System.setProperty("spring.profiles.active", "wow");这一行一起使用。当设定环境参数为wow时,标记了@Profile("wow")的方法会被启用,对应的Bean会添加到容器中。而其他标记的Bean不会被添加,当没有适配到任何Profile值时,@Profile("default")标记的Bean会被启用。
Spring Profile的功能就是根据在环境中指定参数的方法来控制@Bean的创建。
在@Configuration上配置Profile
@Profile注解除了在@Bean方法上使用,也可以用于@Configuration类上。这样使用可以一次性控制多个Bean的加载。例如下面的例子:
@Configuration
@Profile("cast")
class CastConfig {
@Bean
public Castlevania castlevania() {
return new Castlevania();
}
}
@Configuration
@Profile("pes")
class PESConfig {
@Bean
public ProEvolutionSoccer proEvolutionSoccer() {
return new ProEvolutionSoccer();
}
}
这样可以控制整个@Configuration类中的Bean是否加载。这个时候如果在@Configuration类上还标注了@Import注解,那么被@Import引入的类中的@Bean也不会添加到IoC容器中,那么这对统一配置环境是很有好处的。
需要注意的是,如果这个时候又在@Bean之上添加了@Profile注解,那么Spring最终会根据@Bean之上的标签来执行。例如:
@Configuration
@Profile("cast")
class CastConfig {
@Bean
public Castlevania castlevania() {
return new Castlevania();
}
@Bean
@Profile("pes")
public ProEvolutionSoccer proEvolutionSoccer() {
return new ProEvolutionSoccer();
}
}
当环境中的profile值包含"pes"时候,@Profile("pes")标注的这个Bean就会添加到IoC容器中。
Profile的XML配置
Profile特性也可以在XML配置。不过只能在<beans>标签上进行:
<beans ... >
<beans profile="ff">
<bean class="chkui.springcore.example.hybrid.profile.service.squareenix.FinalFantasy" />
</beans>
<beans profile="dog">
<bean class="chkui.springcore.example.hybrid.profile.service.squareenix.SleepingDogs" />
</beans>
</beans>
配置之后,<beans>中的多个<bean>都会被Profile控制。
环境变量的设置
Profile的环境变量可以包含多个值。例如:
System.setProperty("spring.profiles.active", "wow,pes");
这样环境中就包含了2个Profile的值。对用的@Profile或profile配置就会被启用。
除了例子中给出的System::setProperty方法,Spring还提供了多种方法来设置Profile的环境变量。
直接在Jvm启动参数中设置
-Dspring.profiles.active="wow,pes"
使用EnvironmentCapable接口来设置
ConfigurableApplicationContext继承了ConfigurableEnvironment接口我们可以通过ConfigurableEnvironment::getEnvironment方法获取到当前Spring中的环境对象——org.springframework.core.env.Environment,然后使用他来设置环境变量:
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(EnvironmentApp.class);
ConfigurableEnvironment env = ctx.getEnvironment();
//通过setActiveProfiles来设置。
env.setActiveProfiles("wow","pes","ff");
//必须重建容器
ctx.refresh();
需要注意的是,在继承关系中ConfigurableApplicationContext之后才实现ConfigurableEnvironment,如果这里使用ApplicationContext::getEnvironment方法得到的是Environment,它不提供set相关的方法。所以上面的例子使用了ConfigurableApplicationContext。由于ApplicationContext的所有实现类都实现了Configurable的功能,我们也可以像下面这样进行转型:
ApplicationContext ctx = new AnnotationConfigApplicationContext(EnvironmentApp.class);
Environment _e =ctx.getEnvironment();
ConfigurableEnvironment env = ConfigurableEnvironment.class.cast(_e);
@Profile的实现
Profile特性的实现也不复杂,其实就是实现了Conditional功能(Conditional功能见@Configuration与混合使用一文中关于Conditionally的介绍)。
首先@Profile注解继承实现了@Conditional:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {}
然后他的处理类实现了Condition接口:
class ProfileCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles((String[]) value)) {
return true;
}
}
return false;
}
return true;
}
}
处理过程也很简单,实际上就检查@Profile注解中的值,如果和环境中的一致则添加。