Spring @Conditional简单使用 以及 使用时注意事项一点
- @Conditional注解在类的方法中
- @Conditional注解失效的一种原因
- @Conditional注解在类上
- 手写的低配版@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简单使用 以及 使用时注意事项一点的更多相关文章
- MySQL数据库使用时注意事项
MySQL数据库使用时注意事项 建表的角度上 1.合理安排表关系 2.尽量把固定长度的字段放在前面 3.尽量使用char 代替varchar 4.分表:水平分和垂直分 在使用sql语句的时候 1.尽量 ...
- oracle中sqlldr工具使用时注意事项
1.命令写在一行:如,sqlldr sh/&sh_pass@&connect_string control=&ctl_file data=&dat_file log=& ...
- angular js 上传插件 ng-file-upload 使用时注意事项
项目框架为angular js,需要用到文件上传,百度之后先选择了angular-file-upload,githuab上API文档很全,想要具体了解,可以仔细研究一下.在这里简单回顾一下自己使用的插 ...
- UITableViewCell使用时注意事项
1,注意使用重用机制(有利于提高效率) 2,做到通过改变模型去间接改变UI样式(做到永久改变,无论怎样拖动刷新,都不会恢复改变) 3,在通过传递模型给Cell控件布局时,记得完全覆盖(嗯,不好解释,主 ...
- grid++报表使用时注意事项
#开始使用:Grid++Report 可以在 Visual C#.Net 与 Visual Basic.Net 下的 WinForm 项目中使用.在项目中使用 Grid++Report 之前,首先必须 ...
- 守护线程以及要使用时注意的一点(Daemon Thread)
在Java中有两类线程:User Thread(用户线程).Daemon Thread(守护线程) Daemon的作用是为其他线程的运行提供便利服务,比如垃圾回收线程就是一个很称职的守护者.User和 ...
- spring security简单教程以及实现完全前后端分离
spring security是spring家族的一个安全框架,入门简单.对比shiro,它自带登录页面,自动完成登录操作.权限过滤时支持http方法过滤. 在新手入门使用时,只需要简单的配置,即可实 ...
- Spring的简单使用(1)
一:IOC(控制反转):它是由spring容器进行对象的创建和依赖注入,程序员使用时直接取出即可 正转:例如: Student stu=new Student(): stu.setname(" ...
- Spring cache简单使用guava cache
Spring cache简单使用 前言 spring有一套和各种缓存的集成方式.类似于sl4j,你可以选择log框架实现,也一样可以实现缓存实现,比如ehcache,guava cache. [TOC ...
随机推荐
- 马士兵 spring 视频笔记
课程内容 1. 面向接口(抽象)编程的概念与好处 2. IOC/DI的概念与好处 a) inversion of control b) dependen ...
- FastReport使用技巧
使用技巧篇 1.FastReport中如果访问报表中的对象? 可以使用FindObject方法. TfrxMemoView(frxReport1.FindObject('memo ...
- 什么是PAGELATCH和PAGEIOLATCH
在分析SQL server 性能的时候你可能经常看到 PAGELATCH和PAGEIOLATCH.比方说 Select * from sys.dm_os_wait_stats 的输出里面就有Latch ...
- 【译】AI 让科技公司变得更强大吗
机器学习可能是当今技术中最重要的基本趋势.由于机器学习的基础是数据 - 大量的数据 - 很常见的是,人们越来越担心已经拥有大量数据的公司会变得更强大.这有一定的道理,但是以相当狭窄的方式,同时ML也看 ...
- MVVM双向绑定实现之Object.defineProperty
随着web应用的发展,直接操作dom的应用已渐行渐远,取而代之的是时下越来越流行的MVVM框架,dom操作几乎绝迹,这里面自然是框架底层封装的结果.MVVM框架的双向数据绑定使开发效率大大提高:然后在 ...
- git如何忽略已经加入版本控制的文件
git移除已经追踪的文件 有时候新增一个文件,会自动追加到git的版本控制当中,但是又不想提交到仓库.可以按照下面的步骤: git status 查看管理状态: ml-py git:(master) ...
- 阿里云mysql数据库备份还原
1.下载备份包 在rds的备份恢复中点击下载,在弹出的窗口中复制内网下载地址(前提是目标服务器与rds内网互通,否则请复制外网地址) 在目标服务器中执行如下命令进行下载: wget -c '复制的地址 ...
- mybatis 全局配置文件
传送门:mybatis XML 映射配置文件官方文档 配置文件中的标签顺序不能颠倒,否则会报错. <?xml version="1.0" encoding="UTF ...
- DevOps - CI - Sonar
Sonar 官方信息 https://www.sonarqube.org/ https://www.sonarqube.org/downloads/ https://docs.sonarqube.or ...
- 5_Python OOP
1. 实例属性和类属性 (1) 实例属性在构造函数__init__中定义,定义时以self作为前缀,只能通过实例名访问 (2) 类属性在类中方法之外单独定义,还可以在程序中 ...