Spring之@Conditional注解
@Conditional条件注解
一、概述
Spring中有一个条件注解@Conditional,想要探究一下这里的原理,看看是怎么来进行实现的。
二、准备工作
1、Conditional注解
@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();
}
看下注解信息意思是说:实现了Condition 接口的类,重写了其中的方法之后,如果匹配,会成为一个候选者。
2、Condition接口
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);
}
注释说明:
判断条件是否匹配。
参数:
context – 条件上下文
metadata – 被检查的类或方法的元数据
回报:
如果条件匹配并且可以注册组件,则为 true,否则为否决注解组件的注册为 false
3、元数据概念和使用
这个元数据信息真的很蛋疼,所以提前来说一下,Spring中的有好多元数据信息。描述类的、方法的、方法注解的
如:MetadataReader、ClassMetadata、AnnotationMetadata
在Spring中需要去解析类的信息,比如类名、类中的方法、类上的注解,这些都可以称之为类的元数据,所以Spring中对类的元数据做了抽象,并提供了一些工具类。
MetadataReader表示类的元数据读取器,默认实现类为SimpleMetadataReader。比如:
public class Test {
public static void main(String[] args) throws IOException {
SimpleMetadataReaderFactory simpleMetadataReaderFactory = new SimpleMetadataReaderFactory();
// 构造一个MetadataReader
MetadataReader metadataReader = simpleMetadataReaderFactory.getMetadataReader("com.guang.service.UserService");
// 得到一个ClassMetadata,并获取了类名
ClassMetadata classMetadata = metadataReader.getClassMetadata();
System.out.println(classMetadata.getClassName());
// 获取一个AnnotationMetadata,并获取类上的注解信息
AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
for (String annotationType : annotationMetadata.getAnnotationTypes()) {
System.out.println(annotationType);
}
}
}
需要注意的是,SimpleMetadataReader去解析类时,使用的ASM技术。
为什么要使用ASM技术,Spring启动的时候需要去扫描,如果指定的包路径比较宽泛,那么扫描的类是非常多的,那如果在Spring启动时就把这些类全部加载进JVM了,这样不太好,所以使用了ASM技术。
三、开始测试
需求说明:如果当前操作系统是Linux操作系统,那么就使用Linux服务;如果当前操作系统是Windows操作系统,那么就使用Windows服务
1、接口抽象
因为想获取得到当前的bean的名称,所以实现BeanNameAware接口
public interface Service extends BeanNameAware {
}
2、实现
因为两个类都需要来进行获取得到bean名称,所以抽象到父类中来
public abstract class AbstractService implements Service{
private String beanName;
@Override
public void setBeanName(String name) {
this.beanName = name;
}
public String getBeanName(){
return beanName;
}
}
LinuxService实现
public class LinuxService extends AbstractService {
}
WindowsService实现
public class WindowsService extends AbstractService {
}
3、自定义注解
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ConditionOnService {
String value() default "";
}
4、实现条件判断
因为需要对Linux和Windows来进行判断,所以各自实现Condition接口
public class ConditionOnServiceBeanLinux implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionOnService.class.getName());
String value = (String) annotationAttributes.get("value");
return "l".equals(value);
}
}
public class ConditionOnServiceBeanWindows implements Condition {
/**
* spring容器
* @param context the condition context spring容器
* @param metadata the metadata of the {@link org.springframework.core.type.AnnotationMetadata class} 对应的元信息注解
* or {@link org.springframework.core.type.MethodMetadata method} being checked
* @return
*/
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionOnService.class.getName());
String value = (String) annotationAttributes.get("value");
return "w".equals(value);
}
}
5、配置类
@Configuration
@ComponentScan("com.gunag.ioc.demo5condition")
public class AppConfig5 {
}
6、启动类
public class Test {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig5.class);
String[] beanNamesForType = context.getBeanNamesForType(Service.class);
for (String s : beanNamesForType) {
System.out.println(s);
}
}
}
四、添加@Component注解到类上测试
所以修改一下WindowsService和LinuxService实现类:
@Component
@Conditional(value = ConditionOnServiceBeanWindows.class)
@ConditionOnService(value = "w")
public class WindowsService extends AbstractService {
}
@Component
@Conditional(value = ConditionOnServiceBeanLinux.class)
@ConditionOnService(value = "l")
public class LinuxService extends AbstractService {
}
控制台输出:
linuxService
windowsService
实现原理
因为之前看到过spring对于@Component的解析,所以能够快速定位到对应的位置上。
// 符合includeFilters的会进行条件匹配,通过了才是Bean,也就是先看有没有@Component,再看是否符合@Conditional
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
在这里会根据元数据读取器利用ASM技术判断@Conditional是否匹配
private boolean isConditionMatch(MetadataReader metadataReader) {
if (this.conditionEvaluator == null) {
this.conditionEvaluator =
new ConditionEvaluator(getRegistry(), this.environment, this.resourcePatternResolver);
}
return !this.conditionEvaluator.shouldSkip(metadataReader.getAnnotationMetadata());
}
利用condition计算器俩进行计算。
这里的方法命名很有意思,shouldSkip:是否应该跳过(因为方法返回值是boolean,一般称之为是否)
那么什么样的应该跳过,不让其成为候选者呢?因为是不匹配的应该不会成为候选者,匹配的应该成为候选者。
那么继续看:
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();
}
// 循环调用集合中的对象来调用match方法
// 如果匹配,返回true,取反为true,表示不应该跳过,应该成为bean
// 如果matchs方法返回的是false,返回true,表示应该跳过
if ((requiredPhase == null || requiredPhase == phase) && !condition.matches(this.context, metadata)) {
return true;
}
}
return false;
}
在第一行判断中:判断类的元数据是否为空或者类上没有Conditional注解。
显然,元数据读取器不为空;判断后者的时候,如果有Conditional注解,那么if判断返回false;如果不存在Conditional注解,if判断返回false。
返回FALSE表示没有Conditional注解,表示的是应该跳过,因为取反,所以返回为true;
返回为true,表示的是有Conditional注解注解,那么在这里应该来进行判断,是否匹配的逻辑
然后来判断是否是一个配置类,根据判断,是一个配置类,然后继续下一轮判断,类似递归
第二次判断的时候就会获取得到@Conditional中value属性的值,放入到集合中来:
private List<String[]> getConditionClasses(AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(Conditional.class.getName(), true);
Object values = (attributes != null ? attributes.get("value") : null);
return (List<String[]>) (values != null ? values : Collections.emptyList());
}
然后在for循环中来加载对应的类并进行实例化:
private Condition getCondition(String conditionClassName, @Nullable ClassLoader classloader) {
Class<?> conditionClass = ClassUtils.resolveClassName(conditionClassName, classloader);
return (Condition) BeanUtils.instantiateClass(conditionClass);
}
将实例化后的对象直接放入到结合中来
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;
在排序之后,这里居然开始来使用实例化对象调用matches方法。
如果匹配方法返回的是true,取反之后,直接返回FALSE,表示不应该跳过;
如果匹配方法返回的是FALSE,取反之后,直接返回true,表示应该跳过;
这里看起来很简单。
五、添加@Bean测试
将原来加在WindowsService和LinuxService上的注解去掉。
public class LinuxService extends AbstractService {}
public class WindowsService extends AbstractService {}
改造一下:
ConditionOnServiceBean
public class ConditionOnServiceBean implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ConditionOnService.class.getName());
String value = (String) annotationAttributes.get("value");
return "w".equals(value);
}
}
注解:
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(value = ConditionOnServiceBean.class)
public @interface ConditionOnService {
String value();
}
然后在配置上添加:
@Configuration(proxyBeanMethods = false)
@ComponentScan("com.gunag.ioc.demo5condition")
public class AppConfig5 {
@Bean
@ConditionOnService(value = "l")
public Service linux() {
LinuxService linuxService = new LinuxService();
return linuxService;
}
@Bean
@ConditionOnService(value = "w")
public Service windows() {
WindowsService linuxService = new WindowsService();
return linuxService;
}
}
运行之后的结果是:
windows
实现原理
那么这里就涉及到了@Bean的解析流程,和之前@Component解析流程不同了。
这里很简单,直接来到解析@Bean的地方
// @Bean生成BeanDefinition并注册
for (BeanMethod beanMethod : configClass.getBeanMethods()) {
loadBeanDefinitionsForBeanMethod(beanMethod);
}
点进去之后,映入眼帘的就是一个判断:
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) {
configClass.skippedBeanMethods.add(methodName);
return;
}