本文介绍Spring中以
@ConditionalOn*
开头的注解是如何使用的,以及解析其原理,主要有以下内容
- 使用方式
- 实现原理
- 扩展
- 总结
使用方式
对于@ConditionalOn*
注解,很多都是是在SpringBoot中添加的,有以下内容:
org.springframework.boot.autoconfigure.condition.ConditionalOnBean
org.springframework.boot.autoconfigure.condition.ConditionalOnClass
org.springframework.boot.autoconfigure.condition.ConditionalOnCloudPlatform
org.springframework.boot.autoconfigure.condition.ConditionalOnExpression
org.springframework.boot.autoconfigure.condition.ConditionalOnJava
org.springframework.boot.autoconfigure.condition.ConditionalOnJndi
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass
org.springframework.boot.autoconfigure.condition.ConditionalOnNotWebApplication
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
org.springframework.boot.autoconfigure.condition.ConditionalOnResource
org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate
org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication
以上注解有各自的用途,以@ConditionalOnBean
为例,
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(
DispatcherServlet dispatcherServlet) {
ServletRegistrationBean<DispatcherServlet> registration = new ServletRegistrationBean<>(
dispatcherServlet,
this.serverProperties.getServlet().getServletMapping());
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(
this.webMvcProperties.getServlet().getLoadOnStartup());
if (this.multipartConfig != null) {
registration.setMultipartConfig(this.multipartConfig);
}
return registration;
}
上述代码表示若存在Bean DispatcherServlet
或 BeanName 为dispatcherServlet
时,将执行方法,注册 ServletRegistrationBean
为Bean。
实现原理
对比条件注解@ConditionalOn*
的各个注解,我们可以发现一个共同的现象,每个注解上都有 @Conditional
注解修饰,那我们看下 @Conditional
注解的定义。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
/**
* All {@link Condition}s that must {@linkplain Condition#matches match}
* in order for the component to be registered.
*/
Class<? extends Condition>[] value();
}
从@Conditional
定义中可以发现,具有一个属性 value,类型为Condition
接口的实现数组。对于Condition
接口,定义如下:
@FunctionalInterface
public interface Condition {
/**
* Determine if the condition matches.
* @param context the condition context
* @param metadata 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
接口被标注为函数式接口,定义了一个方法,返回boolean
类型,从注释中可以看出,这个方法决定条件是否匹配。那我们就可以确定,@ConditionalOn*
注解,是依靠Condition
接口的实现来完成功能的。那么问题来了,这个Condition
在何时被调用的呢。
按照这个思路,我们先确定这个Condition#matchers
方法在哪里被调用。我们从Condition
接口上也可以看出,这个接口是在spring-context中被引入的,所以这个功能不依赖于SpringBoot。结合以上内容,这个接口调用处只有一个org.springframework.context.annotation.ConditionEvaluator#shouldSkip(org.springframework.core.type.AnnotatedTypeMetadata, org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase)
,在这个方法中被调用,方法代码如下:
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;
}
通过代码调试,进一步确认此方法的调用逻辑,即可看出这个是在定义Bean的时候,确认是否跳过的过程。
扩展
了解了@ConditionalOn*
的实现原理,那我们就可以对其进行扩展了,我们定义一个基于系统属性判断是否加载Bean的@ConditionalOn*
注解 @ConditionalOnSystemProperty
代码如下:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnSystemPropertyCondition.class)
public @interface ConditionalOnSystemProperty {
/**
* Java 系统属性名称
* @return
*/
String name();
/**
* Java 系统属性值
* @return
*/
String value();
}
这里用了Condition
的实现类OnSystemPropertyCondition
,定义如下:
public class OnSystemPropertyCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnSystemProperty.class.getName());
String propertyName = String.valueOf(attributes.get("name"));
String propertyValue = String.valueOf(attributes.get("value"));
String javaPropertyValue = System.getProperty(propertyName);
return propertyValue.equals(javaPropertyValue);
}
}
在类中取出注解@ConditionalOnSystemProperty
的两个属性值,然后和系统属性进行对比,若相同,这返回true
,不同则放回 false
,写个测试类试下:
public class ConditionalOnSystemPropertyBootstrap {
@Bean
@ConditionalOnSystemProperty(name = "user.name", value = "xiaobai")
public String helloWorld() {
return "Hello World, 爱上编程的小白";
}
public static void main(String[] args) {
ConfigurableApplicationContext context = new SpringApplicationBuilder(ConditionalOnSystemPropertyBootstrap.class)
.web(WebApplicationType.NONE)
.run(args);
// 通过名称和类型获取 helloWorld Bean
String helloWorld = context.getBean("helloWorld", String.class);
System.out.println("helloWorld Bean : " + helloWorld);
// 关闭上下文
context.close();
}
}
测试类中确定系统属性user.name
是否为 xiaobai
,若是,则注册Bean helloWorld
,否则程序将报错,找不到对应的Bean。
总结
@ConditionalOn*
注解的实现有很多种,具有相同的注解@Conditional
。@Conditional
注解有一个value
属性,类型为Condition
接口实现的数组。Condition#match
方法是@ConditionalOn*
实现的关键。@ConditionOn*
注解是SpringBoot中添加的,但是其原理,在spring-context就已支持。