自定义ConditionalOnXX注解

导读:本篇文章讲解 自定义ConditionalOnXX注解,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

一、Conditional注解介绍

对SpringBoot有足够了解的小伙伴应该都用过Conditional系列注解,该注解可用在类或者方法上用于控制Bean的初始化。

常用的Conditional注解有以下几种:

  1. @ConditionalOnBean:如果存在对应的Bean,则进行当前Bean的初始化。

  2. @ConditionalOnClass:如果项目的classpath下存在对应的类文件,则进行当前Bean的初始化。

  3. @ConditionalOnExpression:如果满足SpEL表达式,则进行当前Bean的初始化。

  4. @ConditionalOnMissingBean:如果不存在对应的Bean,则进行当前Bean的初始化。

  5. @ConditionalOnMissingClass:如果项目的classpath下不存在对应的类文件,则进行当前Bean的初始化。

  6. @ConditionalOnProperty:如果配置文件上的属性值符合预期值,则进行当前Bean的初始化。

注意如果存在多个Conditional注解,只有都满足条件时才会生效。这里只作简单介绍,更多用法可以搜索其他文章。

二、源码分析

我们先以@ConditionalOnBean为例,分析SpringBoot是如何实现该功能的?

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {

	/**
	 * The class types of beans that should be checked. The condition matches when beans
	 * of all classes specified are contained in the {@link BeanFactory}.
	 * @return the class types of beans to check
	 */
	Class<?>[] value() default {};

	/**
	 * The class type names of beans that should be checked. The condition matches when
	 * beans of all classes specified are contained in the {@link BeanFactory}.
	 * @return the class type names of beans to check
	 */
	String[] type() default {};

	/**
	 * The annotation type decorating a bean that should be checked. The condition matches
	 * when all of the annotations specified are defined on beans in the
	 * {@link BeanFactory}.
	 * @return the class-level annotation types to check
	 */
	Class<? extends Annotation>[] annotation() default {};

	/**
	 * The names of beans to check. The condition matches when all of the bean names
	 * specified are contained in the {@link BeanFactory}.
	 * @return the names of beans to check
	 */
	String[] name() default {};

	/**
	 * Strategy to decide if the application context hierarchy (parent contexts) should be
	 * considered.
	 * @return the search strategy
	 */
	SearchStrategy search() default SearchStrategy.ALL;

	/**
	 * Additional classes that may contain the specified bean types within their generic
	 * parameters. For example, an annotation declaring {@code value=Name.class} and
	 * {@code parameterizedContainer=NameRegistration.class} would detect both
	 * {@code Name} and {@code NameRegistration<Name>}.
	 * @return the container types
	 * @since 2.1.0
	 */
	Class<?>[] parameterizedContainer() default {};

}

首先,查看@ConditionalOnBean注解的源码,发现该注解上有个元数据注解 @Conditional,那么这个注解是干嘛的呢?

点进去查看其注释,发现有如下一行话。

/**
 *
 * <p>The {@code @Conditional} annotation may be used in any of the following ways:
 * <ul>
 * <li>as a type-level annotation on any class directly or indirectly annotated with
 * {@code @Component}, including {@link Configuration @Configuration} classes</li>
 * <li>as a meta-annotation, for the purpose of composing custom stereotype
 * annotations</li>// 作为元注解,用于编写自定义构造型注解
 * <li>as a method-level annotation on any {@link Bean @Bean} method</li>
 * </ul>
 * ....
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

	/**
	 * All {@link Condition} classes that must {@linkplain Condition#matches match}
	 * in order for the component to be registered.
	 */
	Class<? extends Condition>[] value();

}

由此可知,@Conditional有三种用法,当作为元注解时可以用来自定义条件注解,其核心逻辑是由其value指定的Condition接口的实现类来完成的。

@FunctionalInterface
public interface Condition {

	/**
	 * Determine if the condition matches.
	 * @param context the condition context
	 * @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class}
	 * or {@link org.springframework.core.type.MethodMetadata method} being checked
	 * @return {@code true} if the condition matches and the component can be registered,
	 * or {@code false} to veto the annotated component's registration
	 */
	boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

}

Condition是个函数式接口,里面只有一个mathes方法,如果返回true则进行Bean的创建。其中matches方法有两个参数ConditionContext和AnnotatedTypeMetadata,其作用如下:

ConditionContext: 可以拿到上下文信息,包括beanFactory、environment和resourceLoader等。

AnnotatedTypeMetadata: 可以获取被@Conditional标记的注解信息。

public interface ConditionContext {


	BeanDefinitionRegistry getRegistry();

	@Nullable
	ConfigurableListableBeanFactory getBeanFactory();


	Environment getEnvironment();

	
	ResourceLoader getResourceLoader();

	@Nullable
	ClassLoader getClassLoader();

}

那么问题来了,上面的@Conditional的value属性可以指定多个Condition类A和B,如何控制Bean的初始化呢?

其实这里是与的逻辑,只有当所以Condition实现类的mathes方法都返回true时,才会进行Bean的初始化,否则不生效。具体原因,这里不扩展了。

由此可知@ConditionalOnBean的核心逻辑就在OnBeanCondition类里,OnBeanCondition也实现了Condition接口,但是其mathes()方法是在SpringBootCondition中实现的。

/**
 * 所有Condition实现的基类
 * Base of all {@link Condition} implementations used with Spring Boot. Provides sensible
 * logging to help the user diagnose what classes are loaded.
 *
 * @author Phillip Webb
 * @author Greg Turnquist
 * @since 1.0.0
 */
public abstract class SpringBootCondition implements Condition {

	private final Log logger = LogFactory.getLog(getClass());

	@Override
	public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		String classOrMethodName = getClassOrMethodName(metadata);
		try {
      // getMatchOutcome是个抽象方法,由各个子类去实现
			ConditionOutcome outcome = getMatchOutcome(context, metadata);
			logOutcome(classOrMethodName, outcome);
			recordEvaluation(context, classOrMethodName, outcome);
			return outcome.isMatch();
		}
		catch (NoClassDefFoundError ex) {
			throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
					+ ex.getMessage() + " not found. Make sure your own configuration does not rely on "
					+ "that class. This can also happen if you are "
					+ "@ComponentScanning a springframework package (e.g. if you "
					+ "put a @ComponentScan in the default package by mistake)", ex);
		}
		catch (RuntimeException ex) {
			throw new IllegalStateException("Error processing condition on " + getName(metadata), ex);
		}
	}
	// 省略其他代码...
}


public class ConditionOutcome {

	private final boolean match;

	private final ConditionMessage message;
  
  // 省略其他代码...
}

ConditionOutcome是个匹配结果类,里面只有两个属性字段,匹配逻辑在getMatchOutcome方法由子类实现,刚好OnBeanCondition.java中有该方法的实现,代码如下。

@Override
	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
		ConditionMessage matchMessage = ConditionMessage.empty();
		MergedAnnotations annotations = metadata.getAnnotations();
		if (annotations.isPresent(ConditionalOnBean.class)) {
      // 首先包装成Spec类,Spec里的属性与ConditionalOnBean注解的基本一致
			Spec<ConditionalOnBean> spec = new Spec<>(context, metadata, annotations, ConditionalOnBean.class);
      // 获取所有的匹配的bean
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (!matchResult.isAllMatched()) {
        // 由于@ConditionalOnBean可以指定多个bean,所以这里要求全匹配,否则返回不匹配的原因
				String reason = createOnBeanNoMatchReason(matchResult);
				return ConditionOutcome.noMatch(spec.message().because(reason));
			}
			matchMessage = spec.message(matchMessage).found("bean", "beans").items(Style.QUOTE,
					matchResult.getNamesOfAllMatches());
		}
		if (metadata.isAnnotated(ConditionalOnSingleCandidate.class.getName())) {
			Spec<ConditionalOnSingleCandidate> spec = new SingleCandidateSpec(context, metadata, annotations);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (!matchResult.isAllMatched()) {
				return ConditionOutcome.noMatch(spec.message().didNotFind("any beans").atAll());
			}
			else if (!hasSingleAutowireCandidate(context.getBeanFactory(), matchResult.getNamesOfAllMatches(),
					spec.getStrategy() == SearchStrategy.ALL)) {
				return ConditionOutcome.noMatch(spec.message().didNotFind("a primary bean from beans")
						.items(Style.QUOTE, matchResult.getNamesOfAllMatches()));
			}
			matchMessage = spec.message(matchMessage).found("a primary bean from beans").items(Style.QUOTE,
					matchResult.getNamesOfAllMatches());
		}
		if (metadata.isAnnotated(ConditionalOnMissingBean.class.getName())) {
			Spec<ConditionalOnMissingBean> spec = new Spec<>(context, metadata, annotations,
					ConditionalOnMissingBean.class);
			MatchResult matchResult = getMatchingBeans(context, spec);
			if (matchResult.isAnyMatched()) {
				String reason = createOnMissingBeanNoMatchReason(matchResult);
				return ConditionOutcome.noMatch(spec.message().because(reason));
			}
			matchMessage = spec.message(matchMessage).didNotFind("any beans").atAll();
		}
		return ConditionOutcome.match(matchMessage);
	}

获取bean的逻辑在getMatchingBeans方法中,大家可以自己去看下(我才不会说是我懒😏)。

至此通过上面的分析总结下,自定义一个ConditionalOnXX注解大概分为如下几步

  1. 创建一个Condition类并实现Condition接口
  2. 根据自己的需求写一个ConditionalOnXX注解,并指定Condition类
  3. 完善Condition类的matches方法逻辑

三、自定义ConditionalOnXX注解

需求背景: 自定义ConditionalOnXX注解,实现设置的多个配置项中,任意一个配置匹配中即可完成Bean的初始化。

1.创建Conditional注解

/**
 * @Author: Ship
 * @Description: 任意name-value匹配即可
 * @Date: Created in 2021/10/18
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(value = OnAnyMatchCondition.class)
public @interface ConditionalOnAnyMatch {

    String[] name() default {};

    String[] value() default {};
}

2.实现Condition类

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2021/10/18
 */
public class OnAnyMatchCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        MergedAnnotations annotations = metadata.getAnnotations();
        if (!annotations.isPresent(ConditionalOnAnyMatch.class)) {
            return true;
        }
        MergedAnnotation<ConditionalOnAnyMatch> annotation = annotations.get(ConditionalOnAnyMatch.class);
      	// 获取注解的属性值
        String[] names = annotation.getValue("name", String[].class).orElse(new String[]{});
        String[] values = annotation.getValue("value", String[].class).orElse(new String[]{});
        for (int i = 0; i < names.length; i++) {
            // 通过环境变量拿到项目配置信息
            String property = environment.getProperty(names[i]);
            String value = values[i];
            if (value != null && value.equals(property)) {
                // 任意一个匹配则返回true
                return true;
            }
        }
        return false;
    }
}

四、测试

测试前需要写一些测试代码,首先创建一个用于测试的Bean类TestBean

public class TestBean {


}

其次,为了分别测试@ConditionalOnAnyMatch用于类上和方法上的效果,分别创建TestClassBean和TestConfiguration。

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2021/10/18
 */
@ConditionalOnAnyMatch(name = {"test.aa", "test.bb"}, value = {"1", "2"})
@Component
public class TestClassBean {

    @PostConstruct
    public void init(){
        System.out.println("Initialized bean:testClassBean...");
    }
}

TestConfiguration.java

/**
 * @Author: Ship
 * @Description:
 * @Date: Created in 2021/10/18
 */
@Configuration
public class TestConfiguration {

    @Bean
    @ConditionalOnAnyMatch(name = {"test.aa", "test.bb"}, value = {"1", "2"})
    public TestBean testBean() {
        System.out.println("Initialized bean:testBean...");
        return new TestBean();
    }
}

通过代码可以看到,test.aa=1或者test.bb=2任意一个条件成立,就会创建Bean。

  • 测试不符合的场景

    配置文件application.properties添加如下配置

    test.aa=3
    test.bb=4
    

    然后启动项目,可以看到控制台日志没有打印任何信息,说明testBean和testClassBean都没被创建。

  • 测试符合一个的场景

    修改配置如下

    test.aa=1
    test.bb=4
    

    再重启项目,可以看到控制台日志打印了如下信息,说明testBean和testClassBean都被创建了

    Initialized bean:testClassBean...
    Initialized bean:testBean...
    2021-10-20 21:37:50.319  INFO 7488 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8081 (http) with context path ''
    2021-10-20 21:37:50.328  INFO 7488 --- [           main] cn.sp.SpringExtensionApplication         : Started SpringExtensionApplication in 1.725 seconds (JVM running for 2.325)
    
    

说明@ConditionalOnAnyMatch成功的控制了Bean的初始化,本文代码已经上传至github,如果对你有用希望能点个star,不胜感激🙏。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/13197.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!