Spring @Conditional简单使用 以及 使用时注意事项一点

  1. @Conditional注解在类的方法中
  2. @Conditional注解失效的一种原因
  3. @Conditional注解在类上
  4. 手写的低配版@ConditionalOnClass

 

Spring  @Conditional注解出现自 4.0 版本 ,注解的声明如下,其中可以看出几点:

  1.可以标注在类上、方法上;

  2.只有一个属性,value值,可以传入class数组,且需要实现Condition接口;

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

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

}

 javaDoc上说明了一点,所有的条件匹配了才会注册该bean,意味着Condition接口的match方法返回true才会注册该bean对象;

 * Indicates that a component is only eligible for registration when all
 * {@linkplain #value specified conditions} match.

 

 一。简单使用例子,与注解配置类的@Bean注解一起使用:

简单的两个对象,男孩和女孩;

public class Girl {

}

public class Boy {

}

 

 男孩和女孩条件类:暂时都返回 true,(实际中这样没有什么意义)

public class BoyCondition implements Condition{
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return true;
    }
}

public class GirlCondition implements Condition{

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return true;
    }
}

 

 测试类(社会测试类,假设现在有小明和小芳,条件类都是返回true)

public class SocialTests {

    @Bean
    @Conditional({BoyCondition.class})
    public Boy xiaoming() {
        return new Boy();
    }
    
    @Bean
    @Conditional({GirlCondition.class})  
    public Girl xiaofang() {
        return new Girl();
    }
    
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SocialTests.class);
        String[] names = context.getBeanDefinitionNames();
        for (String string : names) {
            System.out.println(string+" , "+context.getBean(string));
        }
    }
}

 

查看测试结果:

 

二。假设现在条件复杂了,Boy和Girl要组成一个家庭,要先有男孩,才能有女生(先有男生还是先有女生,这个具体就不考虑了);就像是jdbcTemplate需要有一个dataSource;

我以为的家庭条件类简单大概是这样:

public class FamilyCondition implements Condition{

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        ConfigurableListableBeanFactory bf = context.getBeanFactory();
        String[] names = bf.getBeanNamesForType(Boy.class);          //把女生加入Spring容器之前先判断 这个容器里有没有男生对象,这就是条件
        if(names !=null && names.length>=1) {
            return true;
        }
        return false;
    }
}

 

家庭测试类:

public class FamilyTests {

    @Bean
    public Boy xiaoming() {
        return new Boy();
    }
    @Bean
    @Conditional({FamilyCondition.class})   
    public Girl xiaofang() {
        return new Girl();
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(FamilyTests.class);
        String[] names = context.getBeanDefinitionNames();
        for (String string : names) {
            System.out.println(string+" , "+context.getBean(string));
        }
    }
}

 

 查看测试结果: 男生和女生都加入到Spring容器中了;

 

下面就是我要说的坑点了:@Condition需要考虑加入容器的顺序,可能存在当前判断条件时候对象还没有加入的容器的情况;比如说,代码稍微换个顺序,下面模拟最简单的加载顺序不同引起的@Conditional失效的情况

public class FamilyTests {

    @Bean
    @Conditional({FamilyCondition.class})   //鸡还是先有蛋的关系
    public Girl xiaofang() {
        return new Girl();
    }
    @Bean
    public Boy xiaoming() {
        return new Boy();
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(FamilyTests.class);
        String[] names = context.getBeanDefinitionNames();
        for (String string : names) {
            System.out.println(string+" , "+context.getBean(string));
        }
    }
}

 

可能上面的代码看起来几乎一样,只是两个@Bean的顺序不一样;测试结果就完全不同了,女生没有加入到容器中,可是代码看起来容器中明明就有男生啊,这就是最简单的@Conditional失效的情况;

 

简单分析下这个@Bean加入到容器的顺序:

 ConfigurationClassBeanDefinitionReaderloadBeanDefinitionsForBeanMethod方法,  这个方法在Spring容器初始化时候,调用BeanDefinitionRegistryPostProcessor类型的实例对象ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry中;具体作用是在每个配置类@Configuration读取完成以后,所有的@Bean注解注册到容器时的判断逻辑;

private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) {
        ConfigurationClass configClass = beanMethod.getConfigurationClass();
        MethodMetadata metadata = beanMethod.getMetadata();
        String methodName = metadata.getMethodName();

        // Do we need to mark the bean as skipped by its condition?
     //@Bean方法有@Conditional注解才有机会进入判断返回true;没有Conditional注解就直接返回false了
if (this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)) { configClass.skippedBeanMethods.add(methodName); //Conditional接口返回false就标记为跳过;加入到ConfigurationClass的skippedBeanMethods中 return; } if (configClass.skippedBeanMethods.contains(methodName)) { return; }
    //省略代码... }

 

查看this.conditionEvaluator.shouldSkip(metadata, ConfigurationPhase.REGISTER_BEAN)方法,conditionEvaluator对象为ConditionEvaluator;

    public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
        if (metadata == null || !metadata.isAnnotated(Conditional.class.getName())) {         // 没有@Conditional注解直接返回false
            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)) {                //@Conditional注解的value属性
            for (String conditionClass : conditionClasses) {
                Condition condition = getCondition(conditionClass, this.context.getClassLoader());    //遍历 然后实例化加入到条件集合中conditions
                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)) { //这里就调用了condition实现类的matches方法判断 返回true代表这个@Bean不应该注册
                return true;
            }
        }

        return false;
    }

 

根据上面分析,条件Condition实现类中条件不满足的时候 shouldSkip返回 true, loadBeanDefinitionsForBeanMethod本来是注册bean的,这里就直接返回了,根本没有注册女孩对象;

总结原因:加载@Bean的时候,按照顺序读取@Bean,然后按照顺序遍历,比如判断女孩的时候,男孩还没有执行loadBeanDefinitionsForBeanMethod,容器中没有男孩类型的,这个时候FamilyCondition的match返回false ,女孩就没有加入到容器中  ;这还只是最基本的@Bean的加载顺序导致的@Conditional失效问题,此外比如@Import、@ComponentScan等组合起来,导致的失效问题会更难以寻找原因; SpringBoot中的@ConditionalOnBean、@ConditionalOnClass类似的注解也会存在这个bean加载顺序导致失效的问题;      一种猜想的排查思路,String[] names = context.getBeanDefinitionNames();存储beanDefinitionNames是有序集合,通过查看集合可以看到bean定义注册的顺序,可能会有一点帮助;或者在条件里面通过ConditionContext对象获取BeanFactory,可以来排查bean是否注册了;

 

三。@Conditional注解在类上,那根据条件判断这个配置类中的@Bean是否全部加入Spring容器还是 全不加入Spring容器;

同样以男孩、女孩为例子  ;下面例子做个基本说明,@Import代表引入一个类作为bean    Spring Import注解

                                                                                      @PropertySource代表导入配置文件,保存到environment对象中,可以通过多种方式读取到 Spring 注解方式引入配置文件

@Import({SecondFamily.class})
@PropertySource(value= {"com/lvbinbin/day0121/config.properties"})
public class FamilyTests {
    @Bean
    public Girl xiaofang() {
        return new Girl();
    }
    @Bean
    public Boy xiaoming() {
        return new Boy();
    }
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(FamilyTests.class);
        String[] names = context.getBeanDefinitionNames();
        for (String string : names) {
            System.out.println(string+" , "+context.getBean(string));
        }
    }
}

引入的配置类信息:

@Conditional({SecondFamilyCondition.class})
public class SecondFamily {
    
    @Bean
    public Girl xiangrikui() {
        return new Girl();
    }
    @Bean
    public Boy boren() {
        return new Boy();
    }
}

 

 

public class SecondFamilyCondition implements Condition{
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment env = context.getEnvironment();
        String flag=env.getProperty("married");
        System.out.println(flag);
        Boolean b = Boolean.valueOf(flag);
        return b;
    }
}

 

 配置文件:

married=true

 

这时候测试结果为:

修改married为false: 可以看到 不仅配置类中的@Bean没有加入进来,配置类也没有加入Spring容器;

           证明了加载顺序 主配置类A @Configuration --- 引入配置类B @Configuration --引入配置类B @Bean  --- 主配置类A @Bean

 

四、

想用@ConditionalOnClass发现这是Spring Boot才有的功能,其实知道@Conditional的注解,@ConditionalOnXXXX的基本怎么实现也大致了解了;因为 @ConditionalOnXXXX 也有没有解决Bean加载顺序的问题???

简单写个低配版实现ConditionalOnClass  :(

4.1声明注解:

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target({ElementType.TYPE,ElementType.METHOD})
@Conditional({BeanCondition.class})
public @interface SimpleConditionalOnClass {
    Class[] value();
}

 

 4.2条件类BeanCondition,代码可能不规范也会存在我考虑不全的地方,不过只是自己动手下,有BUG地方欢迎指正 

简单说下,我考虑的,遍历容器当前已有的BeanDefinition对象,然后取出所有的className加入到集合;但是有的beanDefinition是没有beanClass,或者说这个时候class类型还不确定,运行时才知道的;考虑了另外一种情况factory-method方式的,那我取这个bean定义的类,以及方法名,这样可能会存在方法重载,但是返回值不同不构成重载,我把返回值类型作为className一起存进去(这又有问题,返回值类型是个接口呢?这种情况已经超出一个类能解决的了,就不考虑这种情况,因为难点获取这个配置类的时机、获取入参信息、假如方法重载等等);

public class BeanCondition implements Condition{

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        ConfigurableListableBeanFactory bf = context.getBeanFactory();
        String[] currBeanNames = bf.getBeanDefinitionNames();
        Set<String> nameList = new HashSet();
        for (String string : currBeanNames) {
            BeanDefinition bd = bf.getBeanDefinition(string);
            if(bd instanceof AbstractBeanDefinition) {
                if(bd.getBeanClassName()!=null) {
                    nameList.add(bd.getBeanClassName());
                }else if(bd.getBeanClassName()==null && bd.getFactoryMethodName()!=null) {
                    AnnotatedBeanDefinition abd=(AnnotatedBeanDefinition) bd;
                    if(abd.getMetadata() instanceof StandardAnnotationMetadata) {
                    StandardAnnotationMetadata smm=(StandardAnnotationMetadata)abd.getMetadata();
                    Class<?> clazz = smm.getIntrospectedClass();
                    String methodName = bd.getFactoryMethodName();
                    Method[] methods = clazz.getDeclaredMethods();
                    for (Method m : methods) {
                        if(m.getName().equals(methodName)) {
                            Class<?> returnType = m.getReturnType();
                            nameList.add(returnType.getName());
                            break;
                            }
                        }
                    }
                }
            }
        }
        
        if(metadata instanceof StandardMethodMetadata) {
            StandardMethodMetadata metadataToUse=(StandardMethodMetadata) metadata;
            Method method = metadataToUse.getIntrospectedMethod();
            SimpleConditionalOnClass[] annotationsByType = method.getAnnotationsByType(SimpleConditionalOnClass.class);
            if(annotationsByType!=null && annotationsByType.length==1) {
                SimpleConditionalOnClass scob=annotationsByType[0];
                Class[] requiredBeanClassType = scob.value();
                for (Class var1 : requiredBeanClassType) {
                    if(!nameList.contains(var1.getName())) {
                        throw new RuntimeException("required Type: "+var1 +" is missing in "+bf);
                    }
                }
                return true;
            }
        }
        return false;
    }

}

 

贴一下测试结果:

左图为测试成功,右图为缺少bean时候的测试结果,也算完成了低配版 @ConditionalOnClass  虽然Spring Boot实际解决和我想的千差万别,不过以后会了再说;

      

 

posted @ 2019-01-22 13:50  喜欢日向雏田一样的女子啊  阅读(1709)  评论(0编辑  收藏  举报