Spring的 @Conditional 系列注解和 Spring Starter

Preface

@Conditional 是Spring4新提供的注解

和它类似的有 @DependsOn(“beanName”) 注解, 组件依赖注解, 也就是被依赖的组件会比自身先注册到IOC容器中。

@Conditional

@Conditional({Class, Class}) 的值为 一组 Condition接口 的实现类

  • 是否满足 Condition#matches 方法 (返回true/false)
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

值得注意的是, Spring本身也有很多实现类 (e.g. AllNestedConditions、AnyNestedCondition、NoneNestedConditions) 和扩展 (e.g. @ConditionalOnXxx)

XxxNestedConditions类 系列注解

这些类本身就是一个 Condition 类, 可直接作为注解 @Conditonal 的条件 (基础条件), 可用于创建复合条件 (目的).

复合条件使用静态内部类的方式实现, 条件可以是 @Conditional 也可以是 @ConditionalOnXxx;

  1. AllNestedConditions 全部匹配
  2. AnyNestedCondition 任意匹配
  3. NoneNestedConditions 全不匹配

e.g.

class DefaultWebSecurityCondition extends AllNestedConditions {
	DefaultWebSecurityCondition() {
		super(ConfigurationPhase.REGISTER_BEAN);
	}
	@ConditionalOnClass({ SecurityFilterChain.class, HttpSecurity.class })
	static class Classes {
	}
	@ConditionalOnMissingBean({ WebSecurityConfigurerAdapter.class, SecurityFilterChain.class })
	static class Beans {
	}
}

@ConditionalOnXxx 系列注解

  • @ConditionalOnBean
  • @ConditionalOnSingleCandidate
  • @ConditionalOnMissingBean

Spring 对 @Condition 做了拓展,提供了 Condition 接口的通用实现 (e.g. OnBeanCondition 类),并包装成对应的类似 @ConditonalOnBean @ConditonalOnProperty 等注解

  • @ConditionalOnProperty

配置文件 是否有对应值( havingValue, @ConditionalOnProperty 是通过 havingValue 与配置文件中的值进行判断来实现)

@ConditionalOnProperty(prefix = "spring.redis",name = "enabled", havingValue = "true")

可结合 @ConfigurationProperties(prefix=“spring.redis”) 来装配参数

@ConditionalOnXx 底层实现

  • SpringBootCondition抽象类
public abstract class SpringBootCondition implements Condition {
	public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		//	matches 方法
		// 通过 元数据信息 解析对应的 类、方法 名
		String classOrMethodName = getClassOrMethodName(metadata);
		try {
			/**
			 * getMatchOutcome 获取对应的 ConditionOutcome, 由子类实现
			 * ConditionOutcome 类封装了条件的匹配结果和匹配信息
			 */
			ConditionOutcome outcome = getMatchOutcome(context, metadata);
			// 匹配信息打印
			logOutcome(classOrMethodName, outcome);
			recordEvaluation(context, classOrMethodName, outcome);
			// 返回是否匹配
			return outcome.isMatch();
		} catch (NoClassDefFoundError ex) {
			// ...
		}
	}
}

SpringBootCondition 是个抽象类,实现了 Condition 接口,核心方法 match 的实现委托给了 getMatchOutcome 方法实现,其具体实现自然是由 Spring Boot 提供的各种条件类复写, 其返回的 ConditionOutcome 对象是对条件匹配结果和匹配信息的封装.

以下类仅关注 getMatchOutcome 方法实现, 他们都是SpringBootCondition子类

  • OnBeanCondition 类
  • OnPropertyCondition 类

ConditionalOnProperty

注意: ConditionalOnProperty 配置是从容器中获取的, 而不是在配置文件中获取, 有些时候配置文件没写, 依然存在于容器中, 这时 Condition 依然会判定为不加载

    @Autowired Environment env;

    @Bean
    public ApplicationRunner runner() {
        return r -> {
            Map<String, Object> map = new HashMap();
            for (Iterator it = ((AbstractEnvironment) env).getPropertySources().iterator();
                    it.hasNext(); ) {
                PropertySource propertySource = (PropertySource) it.next();
                if (propertySource instanceof MapPropertySource) {
                    map.putAll(((MapPropertySource) propertySource).getSource());
                }
            }
        };
    }

@Import 和 @ImportResource 注解

@Import

将外部类注入到spring容器中. 在4.2之前只支持导入配置类, 在4.2之后,@Import注解支持导入普通的 java类,并将其声明成一个bean.

和@Bean的区别是:

  1. @Import 的 id 为全类名, @Bean的 id 是方法名

  2. @Import 更简单, 相比于@Bean 使用无参构造器直接 new() 更简单;

  3. 能使用 ImportSelector, 批量导入

public class MyImportSeletor implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.bidu.A","com.bidu.B"};
    }
}
@ImportSelector
  • 获取注解上的参数
  1. 我们给 @EnableA 注解添加一个参数 enabled,当 enabled = true 时导入 AConfiguration.class,当 enabled = false时不导入任何类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AConfiguration.class)
public @interface EnableA {
    boolean enabled() default true;
}
  1. 接着我们实现一个 ImportSelector.class
public class AImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata metadata) {
        Map<String, Object> attributes = metadata
            .getAnnotationAttributes(EnableA.class.getName());

        boolean enabled = (boolean) attributes.get("enabled");
        if (enabled) {
            return new String[]{AConfiguration.class.getName()};
        } else {
            return new String[]{};
        }
    }
}

关键两步:

  1. metadata.getAnnotationAttributes(EnableA.class.getName()) 获取注解信息, 得到一个 Map
  2. return new String[] {} 返回要加载的类的全限定名
  1. 我们可以通过 ImportSelector 中提供给我们的 AnnotationMetadata 来获得 EnableA 中的属性enabled

  2. 当 enabled 为 true 时,我们返回 AConfiguration.class 的全限定名;当 enabled 为 false 时,返回空数组即可

  3. 最后我们将 ImportSelector 子类写到 @Import(AImportSelector.class) 就行了

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AImportSelector.class)
public @interface EnableA {

    boolean enabled() default true;
}
  1. 当我们将enabled设置为false时,就不会配置AConfiguration.class了
@EnableA(enabled = false)
@SpringBootApplication
public class SampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SampleApplication.class, args);
    }
}
@ImportBeanDefinitionRegistrar

还有另一种方式也可以拿到注解的属性,那就是 ImportBeanDefinitionRegistrar

ImportSelector 不同的是,ImportBeanDefinitionRegistrar 可以直接注册 BeanDefinition

如果我们用 ImportBeanDefinitionRegistrar 来实现上面的功能大概就是这个样子

public class AImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        Map<String, Object> attributes = metadata
            .getAnnotationAttributes(EnableA.class.getName());
        boolean enabled = (boolean) attributes.get("enabled");
        if (enabled) {
            registry.registerBeanDefinition("a", new RootBeanDefinition(A.class));
        }
    }
}

关键一步: registry.registerBeanDefinition("beanName", new RootBeanDefinition(A.class))

然后同样的把 @Import(AConfiguration.class) 改为 @Import(AImportBeanDefinitionRegistrar.class) 就行了

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AImportBeanDefinitionRegistrar.class)
public @interface EnableA {
    boolean enabled() default true;
}
@ImportResource

导入 xml 一类的配置文件:

@ImportResource(locations="classpath:applicationContext.xml")

spring.factories

用法

一般在一个项目中也会有一些标记了@Configuration 的配置类, 只要Spring能够扫描到这个类,A实例就能被注入.

如果这个配置类是写在同一个包下,那么 Spring 默认的扫描路径就能扫到, 但如果做成一个 Starter,对应的包名可能就扫不到了, 所以我们需要用另外的方式来导入这个配置类: spring.factories 或 自定义注解 @EnableXXX

使用 spring.factories 来导入配置(注解和spring.factories选择一种就可以啦)

我们需要在resources目录下新建一个META-INF目录,然后在META-INF目录下创建spring.factories文件

接着我们需要在spring.factories中将AConfiguration.class配置上去

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.xxx.AConfiguration

一般情况下,如果是配置在spring.factories中的配置类都会取名xxxAutoConfiguration,所以我们在这里修改名称为AAutoConfiguration

最后在spring.factories中的配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.xxx.AAutoConfiguration

这样当你的项目启动后,Spring就会自动读取spring.factories将AAutoConfiguration(AConfiguration)扫描进去了

Spring boot 2.7.X 之后的新版配置为:

  1. 新建文件夹 META-INF/spring
  2. 新建文件 org.springframework.boot.autoconfigure.AutoConfiguration.imports
  3. 文件内写入: com.xxx.xxx.AAutoConfiguration, 多个可换行

扩展阅读

https://reflectoring.io/spring-boot-conditionals/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值