Spring-@Conditional分析

Conditional分析

作用

表示这个component是否有资格,能不能添加到BeanFactory中。

在使用Conditional注解的时候,被Conditional标注的类,会调用到Conditional里面value里面的Condition的实现类,如果这些都为true,表示匹配,当前的bean就会添加到BeanFactory中,如果有一个为false,就不会添加进去。

因为它的这个特殊的功能,在使用他的时候就可以在bean注册到beanfactory之前,做检查。比如像ConditionalOnClass......

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
	Class<? extends Condition>[] value();

}
复制代码

使用方式

  1. 直接或者间接使用到的@Component。比如像@Configuration。(Configuration注解元注解就一个@Component,但是如果在一个类上面使用了它和Conditional注解,Conditional注解还是会起作用的)

  2. 作为一个元注解,可以自定义注解来使用。比如像Springboot里面的ConditionalOnClass注解。

    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(OnClassCondition.class)   //作为元注解
    public @interface ConditionalOnClass {
    	Class<?>[] value() default {};
    	String[] name() default {};
    
    }
    
    复制代码
  3. 还可以用在@Bean标注的方法上面。

    下面例子中ConditionalOnMissingBean和上面说的ConditionalOnClass差不多,Conditional注解都是他们的元注解,这也能说明问题。

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(MongoTemplate.class)
    @ConditionalOnBean(MongoTemplate.class)
    @ConditionalOnEnabledHealthIndicator("mongo")
    @AutoConfigureAfter({ MongoAutoConfiguration.class, MongoDataAutoConfiguration.class,
    		MongoReactiveHealthContributorAutoConfiguration.class })
    public class MongoHealthContributorAutoConfiguration
    		extends CompositeHealthContributorConfiguration<MongoHealthIndicator, MongoTemplate> {
    
    	@Bean
    	@ConditionalOnMissingBean(name = { "mongoHealthIndicator", "mongoHealthContributor" })
    	public HealthContributor mongoHealthContributor(Map<String, MongoTemplate> mongoTemplates) {
    		return createContributor(mongoTemplates);
    	}
    
    }
    复制代码

特殊情况:

  1. 如果一个@Configuration类上面标注了@Conditional注解,这个Bean里面所有的被@Bean标注的方法,@Import注解,@ComponentScan,都会关联到这个@Conditional上面。也就是说,都是由这个@Conditional来决定的。

Condition接口分析

Condition接口分析

这个接口是用来判断的,在@Conditional的value存放的是实现了Condition接口的类,这个类会被实例化,并且调用它的matches方法。

在matches方法里里面,需要传递两个参数。

  1. ConditionContext context:当前Condition的上下文对象。

    因为在Condition接口里面会由一些操作,一般操作基本都是直接操作BeanFactory或者Environment,当然,肯定还有ResourceLoader。所以,在ConditionContext的接口里面就定义了这些方法,在实现类里面,也是保留了这些的对象。

    public interface ConditionContext {
    	BeanDefinitionRegistry getRegistry();
    	@Nullable
    	ConfigurableListableBeanFactory getBeanFactory();
    	Environment getEnvironment();
    	ResourceLoader getResourceLoader();
    	@Nullable
    	ClassLoader getClassLoader();
    }
    复制代码
  2. AnnotatedTypeMetadata metadata

    类上面注解的的信息,这个信息被Spring包装了,包装成这个对象了。

返回值为true表示匹配,当前的bean可以添加到BeanFactory中,返回为false表示,当前的bean不需要添加到Beanfactory中。

@FunctionalInterface
public interface Condition {
   boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
复制代码

类图

image-20211122105800784.png

ConfigurationCondition接口分析

继承于Condition接口,比起Condition,他有更详细的控制@Configuration。

在不同的配置阶段,对Condition做处理,简而言之,它讲配置解析,分为了两个阶段,在不同的阶段可以做不同的事情,并且在不同的阶段允许特定的存在。

分为了下面两个阶段

  1. ConfigurationPhase.PARSE_CONFIGURATION

    在解析@Configuration 标注的类的时候Condition要计算。如果不匹配,就不会将这个@Configuration添加进来。

  2. ConfigurationPhase.REGISTER_BEAN

    在添加一个规则的bean,才会去做匹配。

public interface ConfigurationCondition extends Condition {
	ConfigurationPhase getConfigurationPhase();

	enum ConfigurationPhase {

		PARSE_CONFIGURATION,

		REGISTER_BEAN
	}

}
复制代码

PARSE_CONFIGURATION和REGISTER_BEAN阶段在Spring中具体是哪些地方。

为什么要分为两个阶段

按照常理来说,解析就是加载,因为解析完了,下一步直接就加载了,没什么问题。但是对于Spring的Configuration来说,不是这个样子的。

对于配置类来说,配置类里面是是可以导入普通的类,也是可以导入一个配置类,这就分为两个阶段了。在读取之后,需要加载。

  1. 读取@import,@Bean,@ComponentScan,等这种可以向容器中添加bean的注解。
  2. 加载,这里的加载是要解析那些普通的注解,比如,@role。@Scope,还有bean的那些init和destroy方法,等等。这些注解不用给在给Spring中添加具体的bean信息。

基于上面的原因,所以在分为了两个阶段。分成两个节点,就可以更加细粒的控制,比如对于一个配置类来说,getConfigurationPhase返回值是REGISTER_BEAN。那么在解析Configuration的时候,就会将这个bean添加进去。并不会调用它的Mathch方法,也就是在这个阶段不用管Match方法的结果。

就可以用这样的一个例子,比如,作为一个配置类来说,它需要在一个类存在的前提下才能用,但是这个类又是别的配置类导入的,那么,这种场景就很适合。

PARSE_CONFIGURATION阶段

在开始解析Configuration标注的类。这个阶段主要是解析@import,@Bean,@ComponentScan。

主要是在ConfigurationClassParser#parse方法里面,下面主要分析一些关键的代码。具体的代码太长了,这里就不写了。

下面只是列举了一个简单的代码,因为,解析的逻辑是一样的,所以,和下面REGISTER_BEAN阶段的分析写在一起了。

ConfigurationClassParser#processConfigurationClass
    // 调用了conditionEvaluator来计算当前的类是否是一个Condition,并且传递的阶段是PARSE_CONFIGURATION。
    if (this.conditionEvaluator来.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
			return;
	}
复制代码

REGISTER_BEAN阶段

在上面解析完之后,这个阶段会解析@role,这种不会向Spring中添加bean的注解,构建BeanDefinition,添加到BeanFactory中,这个时候才会真正的添加进来。

主要是在ConfigurationClassBeanDefinitionReader#loadBeanDefinitions方法里面,下面主要分析一些关键的代码。具体的代码太长了,这里就不写了

ConfigurationClassParser#processConfigurationClass

 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass
    	private void loadBeanDefinitionsForConfigurationClass(
			ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
        // 如果一个对象是被导入的,这里的返回值是通过导入的的类决定的
     // 这里也调用conditional判断
		if (trackedConditionEvaluator.shouldSkip(configClass)) {
			String beanName = configClass.getBeanName();
			if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
				this.registry.removeBeanDefinition(beanName);
			}
			this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
			return;
		}

		if (configClass.isImported()) {
			registerBeanDefinitionForImportedConfigurationClass(configClass);
		}
		for (BeanMethod beanMethod : configClass.getBeanMethods()) {
			loadBeanDefinitionsForBeanMethod(beanMethod);
		}

		loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
		loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
	}
复制代码

ConditionEvaluator分析

要注意,它不是public的,它是给内部用的,来处理Conditional注解。在使用它的时候需要一个context对象,ConditionContextImpl是它的静态内部类。这个context主要是为了封装常用的对象,传递给Condition的matches方法。

其中最主要的方法是shouldSkip方法,方法需要传递两个参数:

  1. AnnotatedTypeMetadata metadata:当前类的注解元信息。
  2. ConfigurationPhase phase:之前说的配置的阶段。

下面重点看看shouldSkip方法

方法的返回值:

true:表示当前的bean不需要添加进来。

false:表示当前的bean需要添加。

主要有以下几个步骤:

  1. 在这个方法里面会判断当前的类是否有Conditional注解。

  2. 如果ConfigurationPhase没有传递,会有一个阶段增强的过程,null->PARSE_CONFIGURATION->REGISTER_BEAN;

  3. 会先实例化Conditional注解里面的values的值(这些都是实现了Condition接口)。

  4. 排序。

  5. 循环调用。重点要看这个方法

    如果这个Condition是ConfigurationCondition,就会获取ConfigurationPhase。注意下面有一个比对ConfigurationPhase是否一致(从方法的入参和ConfigurationCondition返回值做比较,判断是否要起作用的阶段。)。

    for (Condition condition : conditions) {
    			ConfigurationPhase requiredPhase = null;
    			if (condition instanceof ConfigurationCondition) {
    				requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
    			}
        
    			if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
    				return true;
    			}
    		}
    复制代码
class ConditionEvaluator {
	private final ConditionContextImpl context;
	
	public boolean shouldSkip(AnnotatedTypeMetadata metadata) {
		return shouldSkip(metadata, null);
	}
 
	public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
		if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {
			return false;
		}

		if (phase == null) {
			if (metadata instanceof AnnotationMetadata &&
					ConfigurationClassUtils.isConfigurationCandidate((AnnotationMetadata) metadata)) {
				return shouldSkip(metadata, ConfigurationPhase.PARSE_CONFIGURATION);
			}
			return shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN);
		}

		List<Condition> conditions = new ArrayList<>();
		for (String[] conditionClasses : getConditionClasses(metadata)) {
			for (String conditionClass : conditionClasses) {
				Condition condition = getCondition(conditionClass, this.context.getClassLoader());
				conditions.add(condition);
			}
		}

		AnnotationAwareOrderComparator.sort(conditions);

		for (Condition condition : conditions) {
			ConfigurationPhase requiredPhase = null;
			if (condition instanceof ConfigurationCondition) {
				requiredPhase = ((ConfigurationCondition) condition).getConfigurationPhase();
			}
			if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
				return true;
			}
		}
		return false;
	}


	private static class ConditionContextImpl implements ConditionContext {

		@Nullable
		private final BeanDefinitionRegistry registry;

		@Nullable
		private final ConfigurableListableBeanFactory beanFactory;

		private final Environment environment;

		private final ResourceLoader resourceLoader;

		@Nullable
		private final ClassLoader classLoader;

    }
    }
复制代码

TrackedConditionEvaluator分析

TrackedConditionEvaluator是ConfigurationClassBeanDefinitionReader的内部类,主要是用了装饰者设计模式.在正常的conditionEvaluator调用之前,增加了一些操作.

增加了对Import的处理,这里实现了Configuration注解上面的@Conditional上面的级联操作,也就是说,一个配置类上的Conditional注解会关联到由这个配置类导入的一些bean。

这是怎么做的?

  1. 首先这是一个递归操作。这个方法的本质是判断当前的bean是否不要添加到beanfactory中。(记住这是本质)。
  2. 假如A是由B导入的。现在是A调用到了这个方法。
    1. 一上来先看缓存中没有,然后再看A是否是导入的,在拿到A是被谁导入的。
    2. 开始遍历集合,这里就是B。
    3. B就开始走上面的方法。又来了一次。但是这里发现B不是被导入的。这个时候B就继续往下走了
    4. B就走到了conditionEvaluator.shouldSkip()里面了。假设现在B返回了true。表示B不需要添加到BeanFactory中。同时B也会在缓存中
    5. 方法就回到了2了。继续走,allSkipped还是它的默认值true了,skip为true。就不会走到下面的matches的判断里面,直接跳过。放到缓存里面。直接返回 skip。
  3. 通过上面的步骤分析,因为B的不满足,导致A不会添加进来。
private class TrackedConditionEvaluator {
       // 这就是一个缓存,key是类,value表示当前的这个bea是否要跳过(也就是不添加到beanFactory中)
		private final Map<ConfigurationClass, Boolean> skipped = new HashMap<>();

		public boolean shouldSkip(ConfigurationClass configClass) {
            // 先从缓存中捞。
			Boolean skip = this.skipped.get(configClass);
			if (skip == null) {
                 // 当前的bean是是否是导入的,ConfigurationClass的importedBy属性,存放的是导入当前bean的类。
				if (configClass.isImported()) {
                    
					boolean allSkipped = true;
					for (ConfigurationClass importedBy : configClass.getImportedBy()) {
                        // 开始判断,importedBy。
						if (!shouldSkip(importedBy)) {
							allSkipped = false;
							break;
						}
					}
					if (allSkipped) {
						// allSkipped只有在shouldSkip为false的情况下,allSkipped才会变为false
						skip = true;
					}
				}
				if (skip == null) {
                    // 这就会调用上面的判断
					skip = conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN);
				}
                // 放缓存
				this.skipped.put(configClass, skip);
			}
			return skip;
		}
	}
复制代码

使用

通过上面的分析,举一个简单的例子。模拟一个@conditionOnBean的功能。只不过,这里是beanName

  1. 自定义注解

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Conditional(SimpleCondition.class)
    @Order(Ordered.LOWEST_PRECEDENCE)
    public @interface LcConditionalOnBeanName {
    	String beanName();
    }
    复制代码
  2. 两个简单的实体类

    public class A  {
    	private String name;
    	private Integer age;
    
    
    
    	public A(String name, Integer age) {
    		this.name = name;
    		this.age = age;
    	}
    
    	@Override
    	public String toString() {
    		return "A{" +
    				"name='" + name + '\'' +
    				", age=" + age +
    				'}';
    	}
    }
    
    public class B {
    	private String name;
    	private Integer age;
    
    
    	public B(String name, Integer age) {
    		this.name = name;
    		this.age = age;
    	}
    
    
    	@Override
    	public String toString() {
    		return "A{" +
    				"name='" + name + '\'' +
    				", age=" + age +
    				'}';
    	}
    }
    复制代码
  3. 两个相互作用的配置类

    @Configuration(proxyBeanMethods = false)
    @LcConditionalOnBeanName(beanName = "b")
    public class SimpleConfig {
    	@Bean
    	public A a(){
    		return new A("a",12);
    	}
    }
    
    @Configuration(proxyBeanMethods = false)
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public class SimpleConfig2 {
    
    	@Bean
    	public B b (){
    		return new B("a",12);
    	}
    }
    复制代码
  4. ConfigurationCondition的实现类

    public class SimpleCondition implements ConfigurationCondition {
    	@Override
    	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    		Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(LcConditionalOnBeanName.class.getName());
    		if (Objects.isNull(annotationAttributes)) {
    			return false;
    		}
    
    		String beanName = (String) annotationAttributes.get("beanName");
    		Assert.notNull(beanName, "beanName not null");
    		boolean contains = Arrays.asList(context.getRegistry().getBeanDefinitionNames()).contains(beanName);
    		return contains;
    	}
    
    	@Override
    	public ConfigurationPhase getConfigurationPhase() {
    		return ConfigurationPhase.REGISTER_BEAN;
    	}
    }
    复制代码
  5. 主启动类

    	public static void test() {
    		try {
    			AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SimpleConfig.class, SimpleConfig2.class);
    			A bean = context.getBean(A.class);
    			printLog(bean);
    			context.close();
    		} catch (Throwable e) {
    			e.printStackTrace();
    		}
    	}
    
    	public static void printLog(Object s) {
    		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    		System.out.println(Thread.currentThread().getName() + "-" + simpleDateFormat.format(new Date()) + ":" + s);
    	}
    复制代码
  6. 结果

image-20211123183221174.png

普普通通一个获取bean,答应bean信息

说明

在上面的例子中,有两个配置类,

SimpleConfig上面有我自定义的注解,表示,当前的找个bean要是想起作用,就必须需要一个bean名字为b的bean存在,才能起作用。

SimpleConfig2里面无脑往里面注入一个名字问b,类型为B的bean。

SimpleCondition说明

获取这个bean上面的LcConditionalOnBeanName注解,获取值,从beanFactory中获取bean。

LcConditionalOnBeanName说明

@Conditional和@Order注解是它的元注解信息。

问题?

  1. 为什么需要@order注解

    这是一个投巧的方法,因为通过order注解来控制Configuration类的加载顺序,我得在保证在解析加载SimpleConfig之前,SimpleConfig里面的B会被加载到BeanFactory中。

  2. Ordered接口可以不可以?

    不可以。

    具体的代码逻辑在 ConfigurationClassUtils#checkConfigurationClassCandidate 里面。这里面会有下面的代码

    会获取Bean上面标注的Order注解,并且获取value值。会将这个值放在属性里面(ORDER_ATTRIBUTE = Conventions.getQualifiedAttributeName(ConfigurationClassPostProcessor.class, "order");)

    public static Integer getOrder(AnnotationMetadata metadata) {
    		Map<String, Object> orderAttributes = metadata.getAnnotationAttributes(Order.class.getName());
    		return (orderAttributes != null ? ((Integer) orderAttributes.get(AnnotationUtils.VALUE)) : null);
    	}
    复制代码

    在解析Configuration的时候,会先排序,如果没有,默认是最小。所以,这也是为什么在两个配置类上标注的原因。

    	configCandidates.sort((bd1, bd2) -> {
    			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
    			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
    			return Integer.compare(i1, i2);
    		});
    复制代码

おすすめ

転載: juejin.im/post/7033724274405802020