使用Spring Boot
开发项目的时候,有时候也会遇到不同的条件加载不同的配置,但是这里的条件不一定是指不同的环境的配置,也可以是系统环境等等。接下来我们就探究一下Spring Boot
是给我们提供的@Profile
相关的实现,以及一个重要的注解@Conditional
的自定义实现。
演示环境
- IntelliJ IDEA 2018.2.1 (Community Edition)
- Maven 3.5.4
- Spring Boot 2.1.1.RELEASE
简单实现
使用Spring Boot
开发项目时,我们会根据开发、测试和生产等不同的环境来编写不同的application-xx.properties
配置文件。Spring Boot
给我们提供了一个@Profile
注解,可以用来标注我们根据不同的环境来配置的配置类或者javaBean
等等,达到控制它们的作用环境的作用,即实现条件配置。
接着我们编写一个bootstrap
来演示一下@Profile
的使用:
/**
* 基于{@link Profile}实现条件配置
*
* @author Jerome Zhu
*/
public class ProfilesBootstrap {
@Bean(name = "hello")
@Profile("prod")
public String prodHello() {
System.out.println("profiles: prod");
return "hello jerome, this is prod.";
}
@Bean(name = "hello")
@Profile("test")
public String testHello() {
System.out.println("profiles: test");
return "hello jerome, this is test.";
}
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(ProfilesBootstrap.class)
.web(WebApplicationType.NONE)
.profiles("prod")
.run(args);
String helloBean = context.getBean("hello", String.class);
System.out.println("hello Bean: " + helloBean);
context.close();
}
}
首先看代码,我们配置了两个javaBean
,它们的名字都是hello
,这里这两个javaBean
都被标注了@Prifile
,其中@Profile("prod")
表示当激活prod
环境时这个javaBean
才会被注册,而@Profile("test")
表示当激活test
环境时这个javaBean
会被注册到容器中。
接着通过SpringApplicationBuilder
来配置我们的Spring Boot
应用并启动,使用SpringApplicationBuilder
提供的profiles
方法来激活我们的环境。
并通过应用上下文根据javaBean
的名字hello
来获取当前环境中的javaBean
,并输出来验证。
当我们使用profiles("prod")
的时候,会发现控制台会有这样的输出:
profiles: prod
hello Bean: hello jerome, this is prod.
当我们使用profiles("test")
的时候,会发现控制台会有这样的输出:
profiles: test
hello Bean: hello jerome, this is test.
这里就表示我们的配置是成功的。这里@Profile
也可以与其他组件一起使用,比如:@Configuration
、@Service
等等。
走进源码
根据上面的一个简单的例子,我们实现了根据激活的不同的profiles
作为条件去加载不同的组件。接着进入源码一探究竟。首先进入@Profile
的代码:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ProfileCondition.class)
public @interface Profile {
/**
* The set of profiles for which the annotated component should be registered.
*/
String[] value();
}
根据注释可以看出,我们可以根据指定的profiles
来注入我们的组件。这里value
的类型是一个数组,说明我们可以同时指定多个。接着我们还可以看到@Conditional(ProfileCondition.class)
这个注解,我们先跟到@Conditional
里面看:
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* All {@link Condition Conditions} that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();
}
根据注释和代码,我们可以看出@Conditional
的value
值必须是一个Condition
类的子类的Class
,然后根据Condition#matches
方法来判断是否匹配条件,根据判决条件的结果觉得是否要注册这个组件。接着我们进入ProfileCondition.class
的代码中:
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(Profiles.of((String[]) value))) {
return true;
}
}
return false;
}
return true;
}
}
看到这儿我们就好像明白了写什么,首先ProfileCondition
实现了Condition
接口,并且重写了matches
方法来判断是否匹配我们的条件。接着根据注解元数据AnnotatedTypeMetadata
获取当前标注@Profile
的注解元数据,如果这里attrs
就为空直接返回true
;如果不为空就获取指定的@Profile
的值,在根据上下文获取环境,根据环境来判断当前环境是否与@Profile
指定的环境一样,如果一样就返回true
注册组件。
自定义实现
在Spring Boot
中有默认实现了很多@ConditionalOn***
,它们最后使用的还是@Conditional
和Condition
接口,接下来我们就自定义实现一个@ConditionalOnSystemProperty
体验以下条件配置的魅力。介绍以下@ConditionalOnSystemProperty
的作用,我们注解中传入两值,一个是系统环境中变量的key
和一个我们指定的这个key
的值,然后我们拿到这个key
去获取系统中对应的value
,判断是否符合我们的条件来决定是否注册组件。
首先看@ConditionalOnSystemProperty
代码:
/**
* 根据系统环境条件判断是否装配Bean
*
* @author Jerome Zhu
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnSystemPropertyCondition.class)
public @interface ConditionalOnSystemProperty {
/**
* system property key
*/
String name();
/**
* system property key
*/
String value();
}
这个注解需要指定两个值key
和value
,用于我们后面的判断系统属性。
接着看OnSystemPropertyCondition
代码的编写:
/**
* {@link Condition} 实现匹配系统配置的条件
*
* @author Jerome Zhu
*/
public class OnSystemPropertyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnSystemProperty.class.getName());
String propertyKey = String.valueOf(attributes.get("key"));
String propertyValue = String.valueOf(attributes.get("value"));
// 获取系统变量 propertyKey 对应的值
String sysPropertyValue = System.getProperty(propertyKey);
return propertyValue.equals(sysPropertyValue);
}
}
我们这里实现了Condition
接口,重写了matches
方法,根据我们的注解获取注解属性key
和value
的值。然后使用System#getProperty()
方法来获取系统中的值,最后返回匹配结果。
最后我们编写一个bootstrap
来验证我们自定义的条件配置的正确性:
/**
* 验证自定义基于编程{@link Condition}方式的条件装配
*
* @author Jerome Zhu
*/
public class ConditionBootstrap {
@Bean("hello")
@ConditionalOnSystemProperty(key = "user.name", value = "user")
public String userHello() {
return "hello user !";
}
@Bean("hello")
@ConditionalOnSystemProperty(key = "user.name", value = "jerome")
public String jeromeHello() {
return "hello jerome !";
}
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(ConditionBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
String helloBean = context.getBean("hello", String.class);
System.out.println("hello Bean: " + helloBean);
context.close();
}
}
我们这里还是使用系统属性中的user.name
作为判断条件,如果条件符合就注册以hello
为名字的Bean
。最后还是根据上下文获取该Bean
,判断是否成功。启动程序在控制台会看到这样的输出:
hello Bean: hello user !
这里说明我们的基于@Conditional
的条件配置完成了。
我当前电脑的
user.name=user
所有没有加载下面配置的那个Bean
。
总结
我们首先通过Spring Boot
提供的@Profile
来实现条件配置,接着又根据@Profile
一步一步去了解源码,直到发现了@Conditional
和Condition
。最后我们使用@Conditional
和Condition
实现了我们自定义的根据系统环境去判断的@ConditionalOnSystemProperty
。使用这个注解根据我们当前系统的某些参数决定否要注册我们的组件来。当然这种方式相对于@Enable
模块实现的***ImportSelector
接口更加的灵活,扩展性更高。
具体的代码可以参考GitHub:spring-boot-demo-autoconfigure小节。