自定义ConditionalOnXX注解
一、Conditional注解介绍
对SpringBoot有足够了解的小伙伴应该都用过Conditional系列注解,该注解可用在类或者方法上用于控制Bean的初始化。
常用的Conditional注解有以下几种:
-
@ConditionalOnBean:如果存在对应的Bean,则进行当前Bean的初始化。
-
@ConditionalOnClass:如果项目的classpath下存在对应的类文件,则进行当前Bean的初始化。
-
@ConditionalOnExpression:如果满足SpEL表达式,则进行当前Bean的初始化。
-
@ConditionalOnMissingBean:如果不存在对应的Bean,则进行当前Bean的初始化。
-
@ConditionalOnMissingClass:如果项目的classpath下不存在对应的类文件,则进行当前Bean的初始化。
-
@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注解大概分为如下几步:
- 创建一个Condition类并实现Condition接口
- 根据自己的需求写一个ConditionalOnXX注解,并指定Condition类
- 完善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,不胜感激🙏。
本文作者:烟味i
本文链接:https://www.cnblogs.com/2YSP/p/15431073.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步