Spring Boot 教程

@ConditionalOnClass 注解

上面章节介绍了 @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 异常。如下图:

说说我的看法
全部评论(
没有评论
关于
本网站专注于 Java、数据库(MySQL、Oracle)、Linux、软件架构及大数据等多领域技术知识分享。涵盖丰富的原创与精选技术文章,助力技术传播与交流。无论是技术新手渴望入门,还是资深开发者寻求进阶,这里都能为您提供深度见解与实用经验,让复杂编码变得轻松易懂,携手共赴技术提升新高度。如有侵权,请来信告知:hxstrive@outlook.com
公众号