springboot的@ConditionalOnClass注解

大家好,我是“良工说技术”。

今天给大家带来的是springboot中的@ConditionalOnClass注解的用法。上次的@ConditionalOnBean注解还记得吗?

一、@ConditionalOnClass注解初始

看下@CodidtionalOnClass注解的定义,

需要注意的有两点,

  1. 该注解可以用在类及方法上;类指的是标有@Configuration的类,方法是标有@Bean的方法
  2. 该注解使用了@Conditional注解标记;这是重点

看到这里,有小伙伴会疑惑,讲了那么多@Conditional注解的作用是什么,不急,作用马上来。

@ConditionalOnClass注解的作用是当项目中存在某个类时才会使标有该注解的类或方法生效;

这句话有点拗口,通俗的讲,@ConditionalOnClass标识在@Configuration类上,只有存在@ConditionalOnClass中value/name配置的类该Configuration类才会生效;@ConditionalOnClass标识在@Bean方法上,只有只有存在@ConditionalOnClass中value/name配置的类方法才会生效。看具体的实例更容易理解些

二、@ConditionalOnClass注解用法

从上面@ConditionalOnClass注解的定义中我们知道该注解可以配置两个属性,分别是value和name,其中value和name都是数组,只不过内容不一样,

value是Class的数组,name是全限类名的字符串。

1、使用value属性

开始,我一直使用value属性进行配置,但是总是报错,比如我配置

@Configuration
@ConditionalOnClass(value = {Client.class})
public class MyAutoConfig {
    public MyAutoConfig(){
        System.out.println("constructor MyAutoConfig");
    }
}

该Client是下面的类,

org.springframework.boot.autoconfigure.data.elasticsearch.Client

它是ES中的一个类,我本身配置的含义是只有在Client存在的时候MyAutoConfig才会生效,但是总是不成功。你知道为什么不成功吗?

这是因为我没有引ES的依赖,导致在我的classpath中没有这个类,按照@ConditionalOnClass的理解,应该是不存在则不会生效,但是由于没有这个类,导致的问题是:无法编译,提示下面的错误

java: 找不到符号
  符号: 类 Client

这是可以理解的,因为没有这个类,而我要引用这个类肯定是引用不到的,所以编译是失败的,也就程序跑不起来。那么存在一个问题,@ConditionalOnClass注解的value属性要在什么情况下使用?

这里有一个mybatisplus的配置类,

其配置类上标识了@ConditionalOnClass注解,该注解中配置了value属性,且配置了sqlSessionFactory和sqlSessionfactorybean两个类,

MyBatisPlusAutoConfiguration是在mybatis-plus-boot-starter的jar包

sqlSessionFactory是在mybatis的jar包

sqlSessionfactorybean是在mybaits-spring的jar包

这三个类分属于不同的jar包,如果我在一个项目中引入了mybatis-plus-boot-starter的jar包,没有引入mybatis的jar包那么MybatisPlusAutoConfiguration不会生效,也就是只有mybatis和mybatis-spring的jar包都引入了,MybatisPlusAutoConfiguration才会生效,才会被纳入spring容器的管理。

需要注意一点:为了防止少引包,在mybatis-plus-boot-starter中会依赖mybatis和mybatis-spring,这也是starter的好处,不会少引包,需要哪些依赖它都引好了。

那么再回到问题的开始,为什么,我配置了一个不存在的类就没成功,那是因为java的源文件需要编译,在编译时会检查类是否存在,不存在肯定是编译不通过的;而如果引用的是jar包中的文件引用另外一个jar的,则是因为jar包经过了编译,已经打包成功了,故不存在问题。

通过value属性需要结合jar包的方式,这里就不演示了,感兴趣的小伙伴可以自己尝试。通过name属性来指定。

2、使用name属性

@ConditionalOnClass注解还有name属性name属性指定的是全限类名,也就是包含包名+类名。看下我的配置,

@Configuration
@ConditionalOnClass(name = {"com.my.template.config.ClassA"})
public class MyAutoConfig {
    public MyAutoConfig(){
        System.out.println("constructor MyAutoConfig");
    }
}

这里配置了“com.my.template.config.ClassA”,ClassA是我的一个存在的类,

下面启动,看下在启动日志中是否有“constructor MyConfig”打印,

constructor MyAutoConfig
constructor MyAutoConfig2
constructor classA
2022-07-30 17:18:54.113 

看到了,日志说明name配置是生效的,也就是存在ClassA则MyAutoConfig注册到spring的容器中。作为对比,下面配置一个不存在的类ClassD,

@Configuration
@ConditionalOnClass(name = {"com.my.template.config.ClassD"})
public class MyAutoConfig {
    public MyAutoConfig(){
        System.out.println("constructor MyAutoConfig");
    }
}

看下启动日志

constructor MyAutoConfig2
constructor classA
2022-07-30 21:43:30.550  INFO 13116 --- [  

从上面的日志可以看到,没有打印出来想要的日志,说明MyAutoConfig没有注册到spring的容器中。

我们知道name属性一个数组,上面仅仅配置了一个类,如果配置多个会是什么样子,感兴趣的可以自己尝试,这里这直接给出答案,只有name属性中配置的全部满足相应的配置类才会生效。

不知道,你是否对@ConditionalOnClass是怎么实现的感兴趣吗,继续往下看,大揭秘了。

三、@ConditionalOnClass是怎么实现的

要理解@ConditionalOnClass是怎么实现的还是要回到该注解的定义上,前边提到该注解被

@Conditional(OnClassCondition.class)

注解标识,@Conditional注解的含有是要满足条件才会生效,该注解后边再看。今天的主角是OnClassCondition类,看下其继承关系

重点关注XXCondition即可,可以看到最终实现了Condition接口,@Conditional注解的本质就是考查是否满足Codition接口的matches()方法,所以这看SpringBootCondition中matches方法的实现,

@Override
	public final boolean matches(ConditionContext context, AnnotatedTypeMetadata Metadata) {
        //获得该注解标准的类或方法
		String classOrMethodName = getClassOrMethodName(Metadata);
		try {
            //模板方法,该实现在OnClassCodition类中
			ConditionOutcome outcome = getMatchOutcome(context, Metadata);
			logoutcome(classOrMethodName, outcome);
			recordEvaluation(context, classOrMethodName, outcome);
            //返回是否符合条件
			return outcome.ismatch();
		}
		catch (NoClassDefFoundError ex) {
			throw new IllegalStateException("Could not evaluate condition on " + classOrMethodName + " due to "
					+ ex.getMessage() + " not found. Make sure your own configuration does not rely on "
					+ "that class. This can also happen if you are "
					+ "@ComponentScanning a springframework package (e.g. if you "
					+ "put a @ComponentScan in the default package by mistake)", ex);
		}
		catch (RuntimeException ex) {
			throw new IllegalStateException("Error processing condition on " + getName(Metadata), ex);
		}
	}

getMatchOutCome()方法使用了模板方法,实现在OnClassCondition类中,这是最要的方法

@Override
	public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata Metadata) {
		ClassLoader classLoader = context.getClassLoader();
		ConditionMessage matchMessage = ConditionMessage.empty();
         //获得@ConditionalOnClass注解中配置的value和name属性的值
		List<String> onClasses = getCandidates(Metadata, ConditionalOnClass.class);
		if (onClasses != null) {
            //判断@ConditionOnClass注解配置的类是否都可以加载到,如有加载不到的则放到missing中
			List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
			if (!missing.isEmpty()) {
                //有加载不到的,则返回ConditionOutcome对下,其中属性match为false
				return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
						.didNotFind("required class", "required classes").items(Style.QUOTE, missing));
			}
			matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
					.found("required class", "required classes")
					.items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
		}
        //@ConditionalOnMissingClass的处理逻辑
		List<String> onMissingClasses = getCandidates(Metadata, ConditionalOnMissingClass.class);
		if (onMissingClasses != null) {
			List<String> present = filter(onMissingClasses, ClassNameFilter.PRESENT, classLoader);
			if (!present.isEmpty()) {
				return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnMissingClass.class)
						.found("unwanted class", "unwanted classes").items(Style.QUOTE, present));
			}
			matchMessage = matchMessage.andCondition(ConditionalOnMissingClass.class)
					.didNotFind("unwanted class", "unwanted classes")
					.items(Style.QUOTE, filter(onMissingClasses, ClassNameFilter.MISSING, classLoader));
		}
        //返回ConditionOutCome对下,其match属性为true
		return ConditionOutcome.match(matchMessage);
	}

上面的代码已经给出了注释,对应@ConditionalOnClass注解的处理就是解析器配置的value和name属性,判断配置的类是否加载到,如有未加载到的则直接返回属性match=false的ConditionOutcome对象,那么是如何判断是否加载到的,是通过FilteringSpringBootCondition中的filter方法

protected final List<String> filter(Collection<String> classNames, ClassNameFilter classNameFilter,
			ClassLoader classLoader) {
		if (CollectionUtils.isEmpty(classNames)) {
			return Collections.emptyList();
		}
		List<String> matches = new ArrayList<>(classNames.size());
		for (String candidate : classNames) {
            //循环调用matches方法
			if (classNameFilter.matches(candidate, classLoader)) {
				matches.add(candidate);
			}
		}
		return matches;
	}

对于@CoditionalOnClass的处理该方法传入的参数为

List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);

那么也就是调用ClassNameFilter.MISSING的matches方法,其方法如下

可以看到调用的是!isPresent方法,看下该方法的实现,

static boolean isPresent(String className, ClassLoader classLoader) {
            if (classLoader == null) {
                classLoader = ClassUtils.getDefaultClassLoader();
            }

            try {
                //具体实现逻辑
                FilteringSpringBootCondition.resolve(className, classLoader);
                return true;
            } catch (Throwable var3) {
                return false;
            }
        }

具体的实现在resolve方法中,且该方法被try catch包住了,如果加载不到,直接返回false。

protected static Class<?> resolve(String className, ClassLoader classLoader) throws ClassNotFoundException {
        return classLoader != null ? Class.forName(className, false, classLoader) : Class.forName(className);
    }

看到这里,大家明白了,@ConditionalOnClass注解中判断配置的类是否存在使用的方法是Class.forName,类加载。

四、总结

本文主要认识了@ConditionalOnClass注解,分析了其注解的原理,如何判断配置的类是否存在。

  1. @ConditionalOnClass注解有两个属性,分别是value和name,注意其配置方式;
  2. @ConditionalOnClass注解判断配置的类是否存在的方式是通过Class.forName的方式;

 

推荐阅读

springboot的@ConditionalOnBean注解

深入理解springboot的自动注入

我的第一个springboot  starter

 

相关文章

今天小编给大家分享的是Springboot下使用Redis管道(pipeline...
本篇文章和大家了解一下springBoot项目常用目录有哪些。有一...
本篇文章和大家了解一下Springboot自带线程池怎么实现。有一...
这篇文章主要介绍了SpringBoot读取yml文件有哪几种方式,具有...
今天小编给大家分享的是SpringBoot配置Controller实现Web请求...
本篇文章和大家了解一下SpringBoot实现PDF添加水印的方法。有...