2.3 Spring Framework的條件裝配
在實際開發(fā)中我們可能遇到以下場景:測試環(huán)境用8080端口券盅,生產(chǎn)環(huán)境用9999端口;測試環(huán)境需要將某個組件注冊到IOC容器芝硬,但生產(chǎn)環(huán)境又不需要蛮拔。
為了解決在不同場景/條件/環(huán)境下滿足不同組件的裝配明场,Spring Framework提供了兩種條件裝配的方式:基于Profile和基于Conditional诅福。
2.3.1 基于Profile的裝配
1.Profile源碼解讀
If a {@code @Configuration} class is marked with {@code @Profile}, all of the {@code @Bean} methods and {@link Import @Import} annotations associated with that class will be bypassed unless one or more of the specified profiles are active.
如果一個標(biāo)注了@Configuration的配置類被標(biāo)注為@Profile朦拖,那么與該類關(guān)聯(lián)的所有@Bean方法和@Import}注釋將被繞過刃鳄,除非一個或多個指定的配置文件處于活動狀態(tài)古拴。
A profile is a named logical grouping that may be activated programmatically via {@link ConfigurableEnvironment#setActiveProfiles} or declaratively by setting the {@link AbstractEnvironment#ACTIVE_PROFILES_PROPERTY_NAME spring.profiles.active} property as a JVM system property, as an environment variable, or as a Servlet context parameter in {@code web.xml} for web applications.
這里描述激活Profile的三種方式:JVM啟動參數(shù)箩帚、環(huán)境變量、web.xml配置
簡單概括黄痪,Profile提供了一種“基于環(huán)境的配置”紧帕,根據(jù)當(dāng)前項目的不同運行時環(huán)境,可以動態(tài)地注冊與當(dāng)前運行環(huán)境匹配的組件桅打。
2.使用@Profile注解
(1)BartenderConfiguration類添加@Profile注解
public class Bartender {
private String name;
public Bartender(String name) {
this.name = name;
}
// gettter setter
}
@Configuration
@Profile("city")
public class BartenderConfiguration {
@Bean
public Bartender zhangsan() {
return new Bartender("張三");
}
@Bean
public Bartender lisi() {
return new Bartender("李四");
}
}
(2)編程式配置Profile
public class TavernProfileApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BartenderConfiguration.class);
Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
}
}
執(zhí)行結(jié)果(已省略一些內(nèi)部組件打邮鞘取):
=========分割線=========
=========分割線=========
控制臺沒有打印zhangsan和lisi。
因為在默認(rèn)情況下挺尾,ApplicationContext中的Profile為“default”鹅搪,與配置的@Profile("city")不匹配,所以BartenderConfiguration不會生效遭铺,@Bean也就不會注冊到IOC容器中丽柿。
要想zhangsan和lisi注冊到IOC容器中恢准,則需要給ApplicationContext設(shè)置一下激活的Profile。
public class TavernProfileApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("city");
ctx.register(BartenderConfiguration.class);
ctx.refresh();
System.out.println("=========分割線=========");
Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
System.out.println("=========分割線=========");
}
}
執(zhí)行結(jié)果(已省略一些內(nèi)部組件打痈μ狻):
=========分割線=========
bartenderConfiguration
zhangsan
lisi
=========分割線=========
zhangsan和lisi已注冊到IOC容器馁筐。
注意:這里AnnotationConfigApplicationContext在創(chuàng)建對象時,沒有傳入配置類坠非,則內(nèi)部不會執(zhí)行初始化邏輯敏沉,而是等到手動調(diào)用其refresh方法后才會初始化IOC容器(如果傳入了會立即初始化IOC容器),在初始化過程中炎码,一并處理環(huán)境配置盟迟。
(3)命令行參數(shù)配置Profile
上面使用的編程式配置Profile存在硬編碼問題,如果需要切換Profile潦闲,則需要修改代碼并重新編譯攒菠。為此,SpringFramework還支持命令行參數(shù)配置Profile矫钓。
在IDEA中配置啟動選項:
在main方法中改回原來的構(gòu)造方法傳入配置類的形式:
public class TavernProfileApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BartenderConfiguration.class);
Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
}
}
執(zhí)行結(jié)果(已省略一些內(nèi)部組件打印):
=========分割線=========
bartenderConfiguration
zhangsan
lisi
=========分割線=========
zhangsan和lisi成功注冊到IOC容器舍杜。
3.Profile運用于實際開發(fā)
application.properties文件可以通過加profile后綴來區(qū)分不同環(huán)境下的配置文件(application-dev.properties新娜、application-test.properties、application-prod.properties)
# application-dev.properties
server.port=8787
# application-prod.properties
server.port=8989
# application.properties
spring.profiles.active=dev #激活dev的配置
4.Profile的不足
Profile控制的是整個項目的運行環(huán)境既绩,無法根據(jù)單個Bean的因素決定是否裝配概龄。這種情況要用第二種條件裝配的方式:基于@Conditional注解。
2.3.2 基于Conditional的裝配
Conditional饲握,意為條件私杜,可以使Bean的裝配基于一些指定的條件。
換句話說救欧,被標(biāo)注@Conditional注解的Bean要注冊到IOC容器時衰粹,必須滿足@Conditional上指定的所有條件才允許注冊。
1.@Conditional源碼解讀
The {@code @Conditional} annotation may be used in any of the following ways:
- as a type-level annotation on any class directly or indirectly annotated with {@code @Component}, including {@link Configuration @Configuration} classes
- as a meta-annotation, for the purpose of composing custom stereotype annotations
- as a method-level annotation on any {@link Bean @Bean} method
@Conditional的三種使用方式:
- 在任何直接或間接用@Component標(biāo)注的類上作為類級別注笆怠,包括@Configuration類
- 作為元注解铝耻,用于組合自定義構(gòu)造型注解
- 作為任何@Bean方法上的方法級注解
If a {@code @Configuration} class is marked with {@code @Conditional}, all of the {@code @Bean} methods, {@link Import @Import} annotations, and {@link ComponentScan @ComponentScan} annotations associated with that class will be subject to the conditions.
如果一個@Configuration配置類標(biāo)注了@Conditional,那么與之相關(guān)聯(lián)的@Bean方法蹬刷,@Import導(dǎo)入瓢捉,@ComponentScan注解都將適用于這些條件。
Class<? extends Condition>[] value();
@Conditional注解需要傳入一個Condition接口實現(xiàn)類數(shù)組办成,說明在使用時還需要定義一個條件判斷類作為匹配依據(jù)泡态,實現(xiàn)Condition接口。
2.@Conditional使用
(1)創(chuàng)建判斷Boss是否存在的條件判斷類
public class Boss {
}
public class BossExistCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 使用BeanDefinition而不是Bean做判斷迂卢,這是因為考慮到:
// 當(dāng)進行匹配時Boss對象可能尚未創(chuàng)建某弦,使用BeanDefinition
// 可以確保不會出現(xiàn)偏差
return context.getBeanFactory().containsBeanDefinition(Boss.class.getName());
}
}
(2)吧臺配置類使用@Conditional桐汤,并傳入BossExistCondition
public class Bar {
}
@Configuration
public class BarConfiguration {
@Bean
@Conditional(BossExistCondition.class)
public Bar bbBar() {
return new Bar();
}
}
(3)測試
@EnableTavern的內(nèi)容詳見:SpringBoot源碼解讀與原理分析(二)組件裝配
場景一:@EnableTavern只導(dǎo)入BarConfiguration,不導(dǎo)入Boss
@Documented
@Retention(RetentionPolicy.RUNTIME) //
@Target(ElementType.TYPE) // 該注解只能標(biāo)注到類上
@Import({BarConfiguration.class})
public @interface EnableTavern {
}
執(zhí)行結(jié)果(已省略一些內(nèi)部組件打拥堆隆):
=========分割線=========
tavernConfiguration
com.star.springboot.conditional.BarConfiguration
=========分割線=========
Boss和bbBar均沒有注冊到IOC容器中惊科。
場景二:@EnableTavern導(dǎo)入BarConfiguration和Boss
@Documented
@Retention(RetentionPolicy.RUNTIME) //
@Target(ElementType.TYPE) // 該注解只能標(biāo)注到類上
@Import({Boss.class, BarConfiguration.class})
public @interface EnableTavern {
}
執(zhí)行結(jié)果(已省略一些內(nèi)部組件打印):
=========分割線=========
tavernConfiguration
com.star.springboot.ioc.Boss
com.star.springboot.conditional.BarConfiguration
bbBar
=========分割線=========
Boss和bbBar均注冊到IOC容器中亮钦,說明@Conditional已經(jīng)起了作用馆截。
3.ConditionalOnXXX系列注解
SpringBoot針對@Conditional注解擴展了一系列條件注解。
- @ConditionalOnClass & @ConditionalOnMissingClass :檢查當(dāng)前項目的類路徑下是否包含/缺少指定類蜂莉。
- @ConditionalOnBean & @ConditionalOnMissingBean :檢查當(dāng)前容器中是否注冊/缺少指定Bean蜡娶。
- @ConditionalOnProperty :檢查當(dāng)前應(yīng)用的屬性配置。
- @ConditionalOnWebApplication & @ConditionalOnNotWebApplication :檢查當(dāng)前應(yīng)用是否為Web應(yīng)用映穗。
- @ConditionalOnExpression :根據(jù)指定的SqEL表達式確定條件是否滿足窖张。
注意,@ConditionalOnXXX注解通常都用在自動配置類中蚁滋,對于普通的配置類最好避免使用宿接,以免出現(xiàn)判斷偏差。