SpringBoot源码解读与原理分析(三)条件装配

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中配置启动选项:
在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只导入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注解通常都用在自动配置类中,对于普通的配置类最好避免使用,以免出现判断偏差。

本节完,更多内容请查阅分类专栏:SpringBoot源码解读与原理分析

  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值