zoukankan      html  css  js  c++  java
  • 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实际解决和我想的千差万别,不过以后会了再说;

          

  • 相关阅读:
    mysql数据库常用命令
    二维码的生成--后台版
    软件构建--目录
    软件构建--项目总结
    软件构建--产品测试
    软件构建--产品研发
    软件构建--系统设计
    百度分享代码
    JS定时跳转URL并输出剩余秒数
    c#生成word文档
  • 原文地址:https://www.cnblogs.com/lvbinbin2yujie/p/10298224.html
Copyright © 2011-2022 走看看