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

今天给大家带来的是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的@ConditionalOnClass注解的更多相关文章

  1. springboot的@ConditionalOnBean注解

      上篇文章中分析了springboot的自动注入的原理,可在文章后面的推荐阅读中温习哦.在自动注入的原理那篇文章中提到了@ConditionalOnXX注解,今天来看下springboot中的@Co ...

  2. springboot整合mybaits注解开发

    springboot整合mybaits注解开发时,返回json或者map对象时,如果一个字段的value为空,需要更改springboot的配置文件 mybatis: configuration: c ...

  3. SpringBoot 中常用注解

    本篇博文将介绍几种SpringBoot 中常用注解 其中,各注解的作用为: @PathVaribale 获取url中的数据 @RequestParam 获取请求参数的值 @GetMapping 组合注 ...

  4. SpringBoot 中常用注解@PathVaribale/@RequestParam/@GetMapping介绍

    SpringBoot 中常用注解@PathVaribale/@RequestParam/@GetMapping介绍 本篇博文将介绍几种如何处理url中的参数的注解@PathVaribale/@Requ ...

  5. springboot整合redis(注解形式)

    springboot整合redis(注解形式) 准备工作 springboot通常整合redis,采用的是RedisTemplate的形式,除了这种形式以外,还有另外一种形式去整合,即采用spring ...

  6. SpringBoot整合Mybatis注解版---update出现org.apache.ibatis.binding.BindingException: Parameter 'XXX' not found. Available parameters are [arg1, arg0, param1, param2]

    SpringBoot整合Mybatis注解版---update时出现的问题 问题描述: 1.sql建表语句 DROP TABLE IF EXISTS `department`; CREATE TABL ...

  7. SpringBoot使用Mybatis注解进行一对多和多对多查询(2)

    SpringBoot使用Mybatis注解进行一对多和多对多查询 GitHub的完整示例项目地址kingboy-springboot-data 一.模拟的业务查询 系统中的用户user都有唯一对应的地 ...

  8. SpringBoot 中常用注解@Controller/@RestController/@RequestMapping的区别

    SpringBoot中常用注解@Controller/@RestController/@RequestMapping的区别 @Controller 处理http请求 @Controller //@Re ...

  9. SpringBoot 中常用注解@Controller/@RestController/@RequestMapping介绍

    原文 SpringBoot 中常用注解 @Controller/@RestController/@RequestMapping介绍 @Controller 处理http请求 @Controller / ...

随机推荐

  1. [codeforces] 暑期训练之打卡题(三)

    每个标题都做了题目原网址的超链接 Day21<Alphabetic Removals> 题意: 给定一个字符串,要求按照字典序按照出现的前后顺序删除 k 个字母 题解: 记录字符串中各个字 ...

  2. 【摸鱼神器】一次搞定 vue3的 路由 + 菜单 + tabs

    做一个管理后台,首先要设置路由,然后配置菜单(有时候还需要导航),再来一个动态tabs,最后加上权限判断. 这个是不是有点繁琐?尤其是路由的设置和菜单的配置,是不是很雷同?那么能不能简单一点呢?如果可 ...

  3. 使用Rclone将Onedirve挂载到Linux本地

    1. centos挂载onedrive时, 需要安装fuse. # 安装fuse yum -y install fuse 2. 安装完fuse后使用rclone进行挂载 #创建挂载目录 mkdir - ...

  4. “极简”创建 github page 并设置域名

    最简单最详细的,创建 github page 并设置域名,没有多余的步骤,并且多图,对新手特别友好 尝试用 github page 创建博客,并设置独立域名.网上找了许多教程,都太复杂.自己的创建过程 ...

  5. YAML在线验证

    推荐一个网站:YAML在线验证https://www.bejson.com/validators/yaml_editor/

  6. SCI论文写作注意事项

    1. 先写结论:(划定范围,以防添加无效的内容)     并非一开始就把整个结论都写出来,而是把

  7. SpringCloudAlibaba分布式流量控制组件Sentinel实战与源码分析(上)

    概述 定义 Sentinel官网地址 https://sentinelguard.io/zh-cn/index.html 最新版本v1.8.4 Sentinel官网文档地址 https://senti ...

  8. MySQL-3-DML

    DML 数据操作语言 插入insert 语法一:insert into 表名(列名,...)values(值1,...): 语法二:insert into 表名 set 列名=值,列名=值,... 插 ...

  9. 前端下载图片的N种方法

    前几天一个简单的下载图片的需求折腾了我后端大佬好几天,最终还是需要前端来搞,开始说不行的笔者最后又行了,所以趁着这个机会来总结一下下载图片到底有多少种方法. 先起个服务 使用expressjs起个简单 ...

  10. Mybatis中@select注解联合查询

    前言 在项目中经常会使用到一些简单的联合查询获取对应的数据信息,我们常规都是会根据对应的mapper接口写对应的mapper.xml的来通过对应的业务方法来调用获取,针对这一点本人感觉有点繁琐,就对@ ...