2.3 Spring Framework的条件装配
在实际开发中我们可能遇到以下场景:测试环境用8080端口,生产环境用9999端口;测试环境需要将某个组件注册到IOC容器,但生产环境又不需要。
为了解决在不同场景/条件/环境下满足不同组件的装配,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.
如果一个标注了@Configuration的配置类被标注为@Profile,那么与该类关联的所有@Bean方法和@Import}注释将被绕过,除非一个或多个指定的配置文件处于活动状态。
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启动参数、环境变量、web.xml配置
简单概括,Profile提供了一种“基于环境的配置”,根据当前项目的不同运行时环境,可以动态地注册与当前运行环境匹配的组件。
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);
}
}
执行结果(已省略一些内部组件打印):
=========分割线=========
=========分割线=========
控制台没有打印zhangsan和lisi。
因为在默认情况下,ApplicationContext中的Profile为“default”,与配置的@Profile("city")不匹配,所以BartenderConfiguration不会生效,@Bean也就不会注册到IOC容器中。
要想zhangsan和lisi注册到IOC容器中,则需要给ApplicationContext设置一下激活的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("=========分割线=========");
}
}
执行结果(已省略一些内部组件打印):
=========分割线=========
bartenderConfiguration
zhangsan
lisi
=========分割线=========
zhangsan和lisi已注册到IOC容器。
注意:这里AnnotationConfigApplicationContext在创建对象时,没有传入配置类,则内部不会执行初始化逻辑,而是等到手动调用其refresh方法后才会初始化IOC容器(如果传入了会立即初始化IOC容器),在初始化过程中,一并处理环境配置。
(3)命令行参数配置Profile
上面使用的编程式配置Profile存在硬编码问题,如果需要切换Profile,则需要修改代码并重新编译。为此,SpringFramework还支持命令行参数配置Profile。
在IDEA中配置启动选项:
在main方法中改回原来的构造方法传入配置类的形式:
public class TavernProfileApplication {
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(BartenderConfiguration.class);
Stream.of(ctx.getBeanDefinitionNames()).forEach(System.out::println);
}
}
执行结果(已省略一些内部组件打印):
=========分割线=========
bartenderConfiguration
zhangsan
lisi
=========分割线=========
zhangsan和lisi成功注册到IOC容器。
3.Profile运用于实际开发
application.properties文件可以通过加profile后缀来区分不同环境下的配置文件(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控制的是整个项目的运行环境,无法根据单个Bean的因素决定是否装配。这种情况要用第二种条件装配的方式:基于@Conditional注解。
2.3.2 基于Conditional的装配
Conditional,意为条件,可以使Bean的装配基于一些指定的条件。
换句话说,被标注@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标注的类上作为类级别注,包括@Configuration类
- 作为元注解,用于组合自定义构造型注解
- 作为任何@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配置类标注了@Conditional,那么与之相关联的@Bean方法,@Import导入,@ComponentScan注解都将适用于这些条件。
Class<? extends Condition>[] value();
@Conditional注解需要传入一个Condition接口实现类数组,说明在使用时还需要定义一个条件判断类作为匹配依据,实现Condition接口。
2.@Conditional使用
(1)创建判断Boss是否存在的条件判断类
public class Boss {
}
public class BossExistCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 使用BeanDefinition而不是Bean做判断,这是因为考虑到:
// 当进行匹配时Boss对象可能尚未创建,使用BeanDefinition
// 可以确保不会出现偏差
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的内容详见:SpringBoot源码解读与原理分析(二)组件装配
场景一:@EnableTavern只导入BarConfiguration,不导入Boss
@Documented
@Retention(RetentionPolicy.RUNTIME) //
@Target(ElementType.TYPE) // 该注解只能标注到类上
@Import({BarConfiguration.class})
public @interface EnableTavern {
}
执行结果(已省略一些内部组件打印):
=========分割线=========
tavernConfiguration
com.star.springboot.conditional.BarConfiguration
=========分割线=========
Boss和bbBar均没有注册到IOC容器中。
场景二:@EnableTavern导入BarConfiguration和Boss
@Documented
@Retention(RetentionPolicy.RUNTIME) //
@Target(ElementType.TYPE) // 该注解只能标注到类上
@Import({Boss.class, BarConfiguration.class})
public @interface EnableTavern {
}
执行结果(已省略一些内部组件打印):
=========分割线=========
tavernConfiguration
com.star.springboot.ioc.Boss
com.star.springboot.conditional.BarConfiguration
bbBar
=========分割线=========
Boss和bbBar均注册到IOC容器中,说明@Conditional已经起了作用。
3.ConditionalOnXXX系列注解
SpringBoot针对@Conditional注解扩展了一系列条件注解。
- @ConditionalOnClass & @ConditionalOnMissingClass :检查当前项目的类路径下是否包含/缺少指定类。
- @ConditionalOnBean & @ConditionalOnMissingBean :检查当前容器中是否注册/缺少指定Bean。
- @ConditionalOnProperty :检查当前应用的属性配置。
- @ConditionalOnWebApplication & @ConditionalOnNotWebApplication :检查当前应用是否为Web应用。
- @ConditionalOnExpression :根据指定的SqEL表达式确定条件是否满足。
注意,@ConditionalOnXXX注解通常都用在自动配置类中,对于普通的配置类最好避免使用,以免出现判断偏差。
网友评论