- @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实际解决和我想的千差万别,不过以后会了再说;