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加入到容器的顺序:

ConfigurationClassBeanDefinitionReader的loadBeanDefinitionsForBeanMethod方法,  这个方法在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实际解决和我想的千差万别,不过以后会了再说;

      

Spring @Conditional简单使用 以及 使用时注意事项一点的更多相关文章

  1. MySQL数据库使用时注意事项

    MySQL数据库使用时注意事项 建表的角度上 1.合理安排表关系 2.尽量把固定长度的字段放在前面 3.尽量使用char 代替varchar 4.分表:水平分和垂直分 在使用sql语句的时候 1.尽量 ...

  2. oracle中sqlldr工具使用时注意事项

    1.命令写在一行:如,sqlldr sh/&sh_pass@&connect_string control=&ctl_file data=&dat_file log=& ...

  3. angular js 上传插件 ng-file-upload 使用时注意事项

    项目框架为angular js,需要用到文件上传,百度之后先选择了angular-file-upload,githuab上API文档很全,想要具体了解,可以仔细研究一下.在这里简单回顾一下自己使用的插 ...

  4. UITableViewCell使用时注意事项

    1,注意使用重用机制(有利于提高效率) 2,做到通过改变模型去间接改变UI样式(做到永久改变,无论怎样拖动刷新,都不会恢复改变) 3,在通过传递模型给Cell控件布局时,记得完全覆盖(嗯,不好解释,主 ...

  5. grid++报表使用时注意事项

    #开始使用:Grid++Report 可以在 Visual C#.Net 与 Visual Basic.Net 下的 WinForm 项目中使用.在项目中使用 Grid++Report 之前,首先必须 ...

  6. 守护线程以及要使用时注意的一点(Daemon Thread)

    在Java中有两类线程:User Thread(用户线程).Daemon Thread(守护线程) Daemon的作用是为其他线程的运行提供便利服务,比如垃圾回收线程就是一个很称职的守护者.User和 ...

  7. spring security简单教程以及实现完全前后端分离

    spring security是spring家族的一个安全框架,入门简单.对比shiro,它自带登录页面,自动完成登录操作.权限过滤时支持http方法过滤. 在新手入门使用时,只需要简单的配置,即可实 ...

  8. Spring的简单使用(1)

    一:IOC(控制反转):它是由spring容器进行对象的创建和依赖注入,程序员使用时直接取出即可 正转:例如: Student stu=new Student(): stu.setname(" ...

  9. Spring cache简单使用guava cache

    Spring cache简单使用 前言 spring有一套和各种缓存的集成方式.类似于sl4j,你可以选择log框架实现,也一样可以实现缓存实现,比如ehcache,guava cache. [TOC ...

随机推荐

  1. hdu 2642 Stars 【二维树状数组】

    题目 题目大意:Yifenfei是一个浪漫的人,他喜欢数天上的星星.为了使问题变得更容易,我们假设天空是一个二维平面,上面的星星有时会亮,有时会发暗.最开始,没有明亮的星星在天空中,然后将给出一些信息 ...

  2. Ubuntu12.04 root用户登录设置

    ubuntu12.04默认是不允许root登录的,在登录窗口只能看到普通用户和访客登录.以普通身份登录Ubuntu后我们需要做一些修改. 1.普通用户登录后,修改系统配置文件需要切换到超级用户模式,在 ...

  3. hive函数 parse_url的使用

    hive提供了直接处理url的函数 parse_url desc funtion 的解释是: parse_url(url, partToExtract[, key]) - extracts a par ...

  4. ASP.NET MVC5 高级编程-学习日记-第三章 视图

    开发人员之所以花费大量时间来重点设计控制器和模型对象,是因为在这些领域中,精心编写的整洁代码是开发一个可维护Web应用程序的基础. 3.1 视图的作用 视图的职责是向用户提供用户界面.当控制器针对被请 ...

  5. Lily-一个埋点管理工具

    本文来自网易云社区 前言 在很多项目中,埋点数据使用表格来统计的,随着项目的进行,数据量越来越复杂,越来越难以维护.所以很多公司都已经开发了一整套系统,从埋点的录入到代码的输出. 我们项目中iOS和A ...

  6. XSS 跨站脚本攻击 的防御解决方案

    虽然说在某些特殊情况下依然可能会产生XSS,但是如果严格按照此解决方案则能避免大部分XSS攻击. 原则:宁死也不让数据变成可执行的代码,不信任任何用户的数据,严格区数据和代码. XSS的演示 Exam ...

  7. re模块 模块

    import re findall()  烦的奥 import re # 1. findall 查找所有结果,数据不是特别庞大 lst = re.findall('a','abcsdfasdfa') ...

  8. Yii2 Apache + Nginx 路由重写

    一.什么是路由重写 原本的HTTP访问地址: www.test.com/index.php?r=post/view&id=100 表示这个请求将由PostController 的 action ...

  9. Java内存溢出问题总结

    使用Java那么久,在此总结一下Java中常见的内存溢出问题以及对应的解决思路 堆溢出 报错信息 java.lang.OutOfMemoryError: Java heap space 报错原因 堆中 ...

  10. java后端导入excel将数据写入数据库

    参考:https://www.cnblogs.com/hanfeihanfei/p/7079210.html @RequestMapping("/importExcel.do") ...