上面章节介绍了 @ConditionalOnBean 注解的用法,本章将介绍 @ConditionalOnClass 注解的用法,以及该注解的实现原理。
@ConditionalOnBean 表示仅有你指定的类在类路径上时才匹配 @Conditional 注解。
你可以在 @Configuration 类上安全地指定 value(),因为在加载类之前通过使用 ASM 解析了注释元数据。
在对 @Bean 方法进行处理时,需要格外小心,考虑将条件隔离在单独的配置类中,特别是如果方法的返回类型与条件的目标匹配时。
利用 @ConditionalOnBean 注解的 name 属性,判断 classpath 下面是否存在 com.huangx.springboot.autoconfig.init.InitUser 类。如果存在该类,则实例化 UserService。
(1)先创建 UserService 和 OrderService 服务
a、UserService.java
package com.huangx.springboot.autoconfig.service; public class UserService { // 什么也不做 }
b、OrderService.java
package com.huangx.springboot.autoconfig.service; public class OrderService { // 什么也不做 }
(2)创建一个用户初始化类,后面 @ConditionalOnBean 注解将判断该类是否位于 classpath 中。
package com.huangx.springboot.autoconfig.init; public class InitUser { // 这里什么也不做,仅仅为了让 @ConditionalOnClass // 能够在 classpath 下面发现它,成功匹配 @Conditional }
(3)@Configuration 配置类
a、UserConfig.java
package com.huangx.springboot.autoconfig.config; import com.huangx.springboot.autoconfig.service.UserService; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration // 判断在 classpath 中是否存在 InitUser 类 // 如果存在,则匹配 @Conditional 注解,创建 UserService 实例 @ConditionalOnClass(name={"com.huangx.springboot.autoconfig.init.InitUser"}) public class UserConfig { // 只有 @ConditionalOnClass 条件匹配时才能匹配 @Conditional @Bean public UserService userService() { System.out.println("UserService -> userService()"); return new UserService(); } }
b、OrderConfig.java
package com.huangx.springboot.autoconfig.config; import com.huangx.springboot.autoconfig.service.OrderService; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration // 判断在 classpath 中是否存在 InitOrder 类 // 如果存在,则匹配 @Conditional 注解,创建 OrderService 实例 @ConditionalOnClass(name={"com.huangx.springboot.autoconfig.init.InitOrder"}) public class OrderConfig { // 只有 @ConditionalOnClass 条件匹配时才能匹配 @Conditional @Bean public OrderService orderService() { System.out.println("OrderConfig -> orderService()"); return new OrderService(); } }
(4)客户端代码,下面将在 index() 方法内部动态的使用 ApplicationContext 的 getBean() 方法获取 UserService 和 OrderService 类的实例。如下:
package com.huangx.springboot.autoconfig; import com.huangx.springboot.autoconfig.service.OrderService; import com.huangx.springboot.autoconfig.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController @SpringBootApplication public class Demo3Application { @Autowired private ApplicationContext applicationContext; public static void main(String[] args) { SpringApplication.run(Demo3Application.class, args); } @GetMapping("/") public String index() { UserService userService = null; try { userService = applicationContext.getBean(UserService.class); } catch (Exception e) { System.err.println(e.getMessage()); } OrderService orderService = null; try { orderService = applicationContext.getBean(OrderService.class); } catch (Exception e) { System.err.println(e.getMessage()); } return "userService=" + userService + "<br/>" + "orderService=" + orderService; } }
运行上面代码后,在浏览器中访问 http://localhost:8080 地址。如下图:
上面我们学会了怎样去使用 @ConditionalOnClass 注解,接下来我们先看看 @ConditionalOnClass 注解的源码,如下:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented @Conditional(OnClassCondition.class) public @interface ConditionalOnClass { // 通过 Class 类指定你期待在 classpath 中必须存在的类 // 注意:如果你指定的类不再你项目中,IDEA 可能会提示错误,找不到你需要的类 Class<?>[] value() default {}; // 指定你期待在 classpath 中的必须存在类的全限定名称 String[] name() default {}; }
上面源码中,@Conditional(OnClassCondition.class) 语句指定了 @ConditionalOnClass 注解的条件判断具体实现类。@ConditionalOnClass 的功能是由 OnClassCondition 类去实现的,我们可以通过分析 OnClassCondition 类了解 @ConditionalOnClass 是如何工作的?
@Order(Ordered.HIGHEST_PRECEDENCE) class OnClassCondition extends FilteringSpringBootCondition { //... }
OnClassCondition 类继承自 FilteringSpringBootCondition 类,而 FilteringSpringBootCondition 类又继承了 SpringBootCondition,SpringBootCondition 又实现了 Condition 接口。继承层次机构如下图:
Condition 接口仅仅提供了一个 boolean matches() 方法,该方法用来判断条件是否匹配。如果条件成立,则匹配 @Conditional 注解;如果条件不成立,则不匹配 @Conditional 注解。源码如下:
@FunctionalInterface public interface Condition { // 确定条件是否匹配 boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata); }
关于 @Conditional 的用法,请查看自定义 @Conditional 条件。
Condition 接口的 matches() 方法在 SpringBootCondition 类中提供了实现。在 FilteringSpringBootCondition 类中,又重写了 matches() 方法。代码如下:
@Override public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory); // 这里去判断指定的类是否在 classpath 下面 ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata); boolean[] match = new boolean[outcomes.length]; for (int i = 0; i < outcomes.length; i++) { match[i] = (outcomes[i] == null || outcomes[i].isMatch()); if (!match[i] && outcomes[i] != null) { logOutcome(autoConfigurationClasses[i], outcomes[i]); if (report != null) { report.recordConditionEvaluation(autoConfigurationClasses[i], this, outcomes[i]); } } } return match; }
上面方法中,getOutcomes() 方法将返回一个 ConditionOutcome[] 数组。每一个 ConditionOutcome 对象代表了一个类在 classpath 中是否得到匹配,包含匹配日志信息。如下图:
然而 getOutcomes() 方法是一个抽象方法:
protected abstract ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata);
该方法由子类 OnClassCondition 中实现,代码如下:
@Override protected final ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { // Split the work and perform half in a background thread if more than one // processor is available. Using a single additional thread seems to offer the // best performance. More threads make things worse. if (autoConfigurationClasses.length > 1 && Runtime.getRuntime().availableProcessors() > 1) { // 如果是多核CPU,则使用多线程完成解析 return resolveOutcomesThreaded(autoConfigurationClasses, autoConfigurationMetadata); } else { // 如果不是多核CPU,则不使用线程进行解析 OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, 0, autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader()); return outcomesResolver.resolveOutcomes(); } }
resolveOutcomesThreaded() 方法将会把待验证的类名称平均分配给两个线程去进行匹配。代码如下:
private ConditionOutcome[] resolveOutcomesThreaded(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) { int split = autoConfigurationClasses.length / 2; // 第一个解析器 OutcomesResolver firstHalfResolver = createOutcomesResolver(autoConfigurationClasses, 0, split, autoConfigurationMetadata); // 第二个解析器 OutcomesResolver secondHalfResolver = new StandardOutcomesResolver(autoConfigurationClasses, split, autoConfigurationClasses.length, autoConfigurationMetadata, getBeanClassLoader()); // 分别调用第一个和第二个解析器进行解析 ConditionOutcome[] secondHalf = secondHalfResolver.resolveOutcomes(); ConditionOutcome[] firstHalf = firstHalfResolver.resolveOutcomes(); // 将第一个和第二个解析器返回的结果进行合并 ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length]; System.arraycopy(firstHalf, 0, outcomes, 0, firstHalf.length); System.arraycopy(secondHalf, 0, outcomes, split, secondHalf.length); return outcomes; }
根据传递的开始地址和偏移量从目标类数组中取出子数组,然后创建 ThreadedOutcomesResolver 多线程解析器。如下:
private OutcomesResolver createOutcomesResolver(String[] autoConfigurationClasses, int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) { // 标准解析器 OutcomesResolver outcomesResolver = new StandardOutcomesResolver(autoConfigurationClasses, start, end, autoConfigurationMetadata, getBeanClassLoader()); try { // 多线程解析器 return new ThreadedOutcomesResolver(outcomesResolver); } catch (AccessControlException ex) { return outcomesResolver; } }
我们继续看看 ThreadedOutcomesResolver 类的实现,代码如下:
private static final class ThreadedOutcomesResolver implements OutcomesResolver { private final Thread thread; // 匹配结果 private volatile ConditionOutcome[] outcomes; private ThreadedOutcomesResolver(OutcomesResolver outcomesResolver) { // 启动线程去进行匹配 this.thread = new Thread(() -> this.outcomes = outcomesResolver.resolveOutcomes()); this.thread.start(); } @Override public ConditionOutcome[] resolveOutcomes() { try { // 等待线程结束 this.thread.join(); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } // 返回匹配结果 return this.outcomes; } }
从上面代码可以知道,在 ThreadedOutcomesResolver 类中实际还是调用 StandardOutcomesResolver 类的 resolveOutcomes() 方法去完成匹配的。resolveOutcomes() 方法实现如下:
@Override public ConditionOutcome[] resolveOutcomes() { return getOutcomes(this.autoConfigurationClasses, this.start, this.end, this.autoConfigurationMetadata); }
进入 getOutcomes() 方法,该方法将迭代我们需要匹配的类,进行逐个匹配。代码如下:
private ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses, int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) { ConditionOutcome[] outcomes = new ConditionOutcome[end - start]; for (int i = start; i < end; i++) { String autoConfigurationClass = autoConfigurationClasses[i]; if (autoConfigurationClass != null) { String candidates = autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnClass"); if (candidates != null) { // 重点:通过 getOutcome 去验证某个类 outcomes[i - start] = getOutcome(candidates); } } } return outcomes; }
getOutcome() 方法的源码如下:
private ConditionOutcome getOutcome(String candidates) { try { // 如果是单个类 if (!candidates.contains(",")) { return getOutcome(candidates, this.beanClassLoader); } // 如果多个类使用逗号分割,如:com.hxstrive.A,com.hxstrive.B,则只会匹配第一个类 // 如果第一个类不存在,继续匹配第二个,直到匹配最后一个或匹配到存在的类为止 for (String candidate : StringUtils.commaDelimitedListToStringArray(candidates)) { ConditionOutcome outcome = getOutcome(candidate, this.beanClassLoader); if (outcome != null) { return outcome; } } } catch (Exception ex) { // We'll get another chance later } return null; }
快了,我们继续进一步进入 getOutcome(String className, ClassLoader classLoader) 方法,代码如下:
private ConditionOutcome getOutcome(String className, ClassLoader classLoader) { // 关键代码来了,matches() 方法用来验证类是否在 classpath 下面 if (ClassNameFilter.MISSING.matches(className, classLoader)) { return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class) .didNotFind("required class").items(Style.QUOTE, className)); } return null; }
下面是 FilteringSpringBootCondition.ClassNameFilter 枚举的代码,如下:
protected enum ClassNameFilter { // 存在 PRESENT { @Override public boolean matches(String className, ClassLoader classLoader) { return isPresent(className, classLoader); } }, // 不存在 MISSING { @Override public boolean matches(String className, ClassLoader classLoader) { return !isPresent(className, classLoader); } }; abstract boolean matches(String className, ClassLoader classLoader); static boolean isPresent(String className, ClassLoader classLoader) { if (classLoader == null) { classLoader = ClassUtils.getDefaultClassLoader(); } try { // 实际解析类是否在classpath中,该方法位于 FilteringSpringBootCondition 类 resolve(className, classLoader); return true; } catch (Throwable ex) { return false; } } }
进入 FilteringSpringBootCondition 类,resolve() 方法代码如下:
/** * Slightly faster variant of {@link ClassUtils#forName(String, ClassLoader)} that * doesn't deal with primitives, arrays or inner types. * @param className the class name to resolve * @param classLoader the class loader to use * @return a resolved class * @throws ClassNotFoundException if the class cannot be found */ protected static Class<?> resolve(String className, ClassLoader classLoader) throws ClassNotFoundException { if (classLoader != null) { return Class.forName(className, false, classLoader); } return Class.forName(className); }
看到了吧!它实际上使用的是 Class.forName 来检查你指定的类是否在 classpath 下面,这也要求我们指定类的完全限定名。如果 Class.forName 时,类不存在会抛出 ClassNotFoundException 异常。如下图: