美文网首页@IT·互联网
SpringBoot源码解读与原理分析(三)条件装配

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

作者: 灰色孤星 | 来源:发表于2024-01-01 18:07 被阅读0次

    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中配置启动选项:


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

    SpringBoot源码解读与原理分析(合集)

    相关文章

      网友评论

        本文标题:SpringBoot源码解读与原理分析(三)条件装配

        本文链接:https://www.haomeiwen.com/subject/sobundtx.html