@Conditional注解是用来判断是否满足指定的条件来决定是否进行Bean的实例化及装配,下面通过示例演示@ConditionalOnMissingClass、@ConditionalOnMissingBean、@ConditionalOnClass和@ConditionalOnBean的用法。
1、定义基础类,根据这些类或实例判断指定的类是否被加载
@Component
public class BeanOne {
public void run(){
System.out.println("BeanOne.run()方法。");
}
}
@Component
public class BeanTwo {
public void run(){
System.out.println("BeanTwo.run()方法。");
}
}
public class BeanThree {
public void run(){
System.out.println("BeanThree.run()方法。");
}
}
这三个类,前两个通过@Component注解,会被加载到Spring IOC容器中,第三个类没有被注解,所以在Spring IOC容器中不存在该类的实例。
2、配置测试类
注解@ConditionalOnBean:
主要使用了注解@ConditionalOnBean,主要是根据Spring IOC容器中是否有该类的实例对象来进行判断。根据前面的配置,可以知道BeanOne、BeanTwo两个类的实例会被加载到Spring IOC容器中,而BeanThree类的实例不会被加载到容器中,所以第一种情况下,该类不会被加载,第二种情况该类会被加载。
@Configuration
//第一种情况:不会被加载,BeanThree不在IOC容器中
@ConditionalOnBean({
BeanOne.class, BeanThree.class})
//第二种情况:会被加载,都在容器内
//@ConditionalOnBean({BeanOne.class, BeanTwo.class})
public class TestOnBean {
}
注解@ConditionalOnClass:
主要使用了注解@ConditionalOnClass,主要是根据类路径下是否有该类来进行判断。根据前面的配置,可以知道BeanOne、BeanThree两个类的都存在(BeanThree在类路径存在,但是不在Spring IOC容器中),而BeanFour类在类路径下不存在,所以第一种情况下,该类会被加载,第二种情况该类不会被加载。
@Configuration
//第一中情况:存在这两个类,所以会被加载
//@ConditionalOnClass({BeanOne.class, BeanThree.class})
//第二种情况:不会被加载,因为不存在BeanFour
@ConditionalOnClass(name={
"com.qriver.spring.conditional.bean.base.BeanOne", "com.qriver.spring.conditional.bean.base.BeanFour"})
public class TestOnClass {
}
注解@ConditionalOnMissingBean:
主要使用了注解@ConditionalOnMissingBean,主要是根据Spring IOC容器中是否有该类的实例对象来进行判断,和注解@ConditionalOnBean正好相反。根据前面的配置,可以知道BeanOne类的实例会被加载到Spring IOC容器中,而BeanThree类的实例不会被加载到容器中,所以第一种情况下,该类会被加载,第二种情况该类不会被加载。
@Configuration
//第一种情况:BeanThree不在IOC容器中,所以会被加载
//@ConditionalOnMissingBean(BeanThree.class)
//第二种情况:BeanOne在IOC容器中,所以不会被加载
@ConditionalOnMissingBean(BeanOne.class)
public class TestOnMissingBean {
}
注解@ConditionalOnMissingClass:
主要使用了注解@ConditionalOnMissingClass,主要是根据类路径下是否有该类来进行判断,正好跟注解@ConditionalOnClass相反。根据前面的配置,可以知道BeanOne、BeanThree两个类的都存在(BeanThree在类路径存在,但是不在Spring IOC容器中),而BeanFour类在类路径下不存在,所以第一种情况下,该类不会被加载,第二种情况该类会被加载。
@Configuration
//第一中情况:存在这两个类,所以会被加载
//@ConditionalOnClass({BeanOne.class, BeanThree.class})
//第二种情况:不会被加载,因为不存在BeanFour
@ConditionalOnClass(name={
"com.qriver.spring.conditional.bean.base.BeanFour"})
public class TestOnClass {
}
3、测试运行类
@Configuration
@ComponentScan("com.qriver.spring.conditional")
public class MainConfig {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(MainConfig.class);
String[] beanNames = context.getBeanDefinitionNames();
for(int i=0;i<beanNames.length;i++){
System.out.println("bean名称:"+beanNames[i]);
}
}
}
运行上述代码,会根据注解条件,打印相应的被加载到Spring IOC容器中的类。这里不在展示打印结果了。
@Conditional注解是由Spring 4.0版本引入的新特性,可根据是否满足指定的条件来决定是否进行Bean的实例化及装配,比如,设定当类路径下包含某个jar包的时候才会对注解的类进行实例化操作。总之,就是根据一些特定条件来控制Bean实例化的行为。
@Conditional注解的源码如下:
@Target({
ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[] value();
}
@Conditional注解及其衍生的注解结构如下图所示,其中大部分衍生的注解都是在在Spring Boot的autoconfigure项目中定义的,即在包org.springframework.boot.autoconfigure.condition下定义,大都是为了适应Spring Boot自动装配过程中,各个组件模块在不同应用 场景下的实例加载。
@Conditional注解唯一的元素属性是接口Condition的数组,只有在数组中指定的所有Condition的matches方法都返回true的情况下,被注解的类才会被加载。在条件注解中,注解@Conditional及其衍生类是定义了注解的用法,而Condition 及其子类实现了该用法的具体逻辑判断,两者配合实现了条件注解的功能。
Condition的源码如下:
@FunctionalInterface
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}
其中,matches方法的第一个参数为ConditionContext,可通过该接口提供的方法来获得Spring应用的上下文信息。第二个参数为AnnotatedTypeMetadata,该接口提供了访问特定类或方法的注解功能,可以用来检查带有@Bean注解的方法上是否还有其他注解。
Condition接口的层级结构, 如下所示:
其中抽象类SpringBootCondition是Condition接口的子类,主要是Spring Boot为自动装配各个组件定义的条件注解匹配判断条件的基类。
注解@ConditionalOnWebApplication主要用来判断当前环境是否是Web环境,其中Web环境又细分了ANY、SERVLET、REACTIVE三类。该注解主要是通过组合上述提到的@Conditional注解而实现的,其中对应的Condition实现类是OnWebApplicationCondition。源码如下所示:
@Target({
ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnWebApplicationCondition.class)
public @interface ConditionalOnWebApplication {
Type type() default Type.ANY;
enum Type {
ANY,
SERVLET
REACTIVE
}
}
通过源码可以发现,注解@ConditionalOnWebApplication再细分Web环境类型时,是由定义的枚举类型Type标识的。其中,让注解真正能够实现判断是否是Web环境,是什么类型的Web环境,是由OnWebApplicationCondition实现类来进行的。下面我们将详细分析OnWebApplicationCondition类,首先我们需要了解该类的继承关系,如下所示:
通过OnWebApplicationCondition类的继承关系图,我们知道该类实现FilteringSpringBootCondition抽象类,而FilteringSpringBootCondition抽象类又继承了SpringBootCondition类,并实现了AutoConfigurationImportFilter接口,而SpringBootCondition抽象类在前面也提到了,是Spring Boot为自动装配各个组件定义的条件注解判断的基类。
SpringBootCondition抽象类
SpringBootCondition抽象类实现了Condition接口,实现了接口中的matches(ConditionContext context, AnnotatedTypeMetadata metadata)方法,在该方法中,判断是否符合条件的核心逻辑是通过抽象方法getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata)实现,该方法交由子类具体实现。在实现的matches()方法中,主要是完成了抽象方法getMatchOutcome()的调用,然后就是相关日志信息的记录。
//SpringBootCondition.java类
@Override
public final boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//根据注解的元数据,获取该注解的类或方法名称,其中方法名称字符串的格式:权限类名+“#”+方法名
String classOrMethodName = getClassOrMethodName(metadata);
try {
//调用抽象方法,实现是否匹配的逻辑判断,交由子类实现
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);
}
}
AutoConfigurationImportFilter接口
@FunctionalInterface
public interface AutoConfigurationImportFilter {
boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata);
}
该接口主要是判断autoConfigurationClasses对应的类,是否符合可以被加载的条件。在Spring Boot自动加载各个组件的过程中,用于快速排除不符合条件的类。这里暂时只分析该接口实现的功能,等梳理Spring Boot加载过程的时候,再详细了解该接口再Spring Boot框架中的应用。
FilteringSpringBootCondition抽象类
FilteringSpringBootCondition抽象类继承了前面提到的SpringBootCondition类,并实现了AutoConfigurationImportFilter接口。FilteringSpringBootCondition抽象类主要实现了AutoConfigurationImportFilter接口的match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata)方法,其中判断是否符合加载条件的逻辑,又通过定义的抽象方法getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata)交由子类进行了实现。同时还定义了一个内部类ClassNameFilter,用来判断指定类(字符串形式)是否存在该类加载器中。
AutoConfigurationImportFilter接口的match()方法在FilteringSpringBootCondition中的实现,代码如下:
@Override
public boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata) {
//获取ConditionEvaluationReport实例,用来记录条件判断的详细信息
ConditionEvaluationReport report = ConditionEvaluationReport.find(this.beanFactory);
//判断autoConfigurationClasses数组中的类,是否符合加载条件,通过抽象方法getOutcomes()实现,该抽象方法交由子类进行实现
ConditionOutcome[] outcomes = getOutcomes(autoConfigurationClasses, autoConfigurationMetadata);
boolean[] match = new boolean[outcomes.length];
//把ConditionOutcome[]数组转成了boolean[],记录autoConfigurationClasses数组中对应的类是否符合加载条件
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;
}
内部类实现的用来判断指定类(字符串形式)是否存在该类加载器中的判断,由下面方法实现,共
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) {
//调用内部类,判断candidate是否存在于当前的类加载器中
if (classNameFilter.matches(candidate, classLoader)) {
matches.add(candidate);
}
}
return matches;
}
判断类是否存在的方法,通过调用resolve()方法,尝试使用类加载器根据className去加载该类,如果抛出ClassNotFoundException异常,说明该类不存在,否则认为该类存在。
static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
try {
resolve(className, classLoader);
return true;
}
catch (Throwable ex) {
return false;
}
}
protected static Class<?> resolve(String className, ClassLoader classLoader) throws ClassNotFoundException {
if (classLoader != null) {
return classLoader.loadClass(className);
}
return Class.forName(className);
}
OnWebApplicationCondition类
该类继承FilteringSpringBootCondition抽象类,主要用来判断项目是否是Web环境。通过前面的分析,我们可以知道,OnWebApplicationCondition类间接的继承了SpringBootCondition抽象类,所以需要实现SpringBootCondition抽象类中定义的getMatchOutcome()方法。同时,OnWebApplicationCondition类继承的FilteringSpringBootCondition抽象类,也有一个抽象方法getOutcomes()方法需要实现。
首先,我们来分析OnWebApplicationCondition类是如何FilteringSpringBootCondition抽象类中的getOutcomes()方法的。
@Override
protected ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
//定义保存是否匹配结果的变量
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
//循环,遍历每一个autoConfigurationClass是否匹配,并把匹配结果存到上面定义的变量中,具体的判断交由getOutcome()方法实现
for (int i = 0; i < outcomes.length; i++) {
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
outcomes[i] = getOutcome(
autoConfigurationMetadata.get(autoConfigurationClass, "ConditionalOnWebApplication"));
}
}
return outcomes;
}
通过上面的源码,我们可以知道,首先由工具类AutoConfigurationMetadataLoader定义的内部类PropertiesAutoConfigurationMetadata(AutoConfigurationMetadata的实现类)来获取autoConfigurationClass."ConditionalOnWebApplication"对应的type(对应的type是在Spring Boot初始化时加载的元数据配置)。
这里提到的type是在@EnableAutoConfiguration注解上通过@Import注解导入的AutoConfigurationImportSelector实现类(ImportSelector实现类)的selectImports()方法中进行加载的。加载元数据的配置用到了AutoConfigurationMetadataLoader类提供的loadMetaData方法,该方法会默认加载类路径下META-INF/spring-autoconfigure-metadata.properties内的配置。spring-autoconfigure-metadata.properties文件内的配置格式:自动配置类的全限定名.注解名称=值。
代码如下:
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
……
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
……
}
通过上述在初始化时,加载的元数据,就是为了这里过滤自动配置使用。通过调用新定义的getOutcome(type)方法来进行是否匹配的判断,具体实现如下:
private ConditionOutcome getOutcome(String type) {
if (type == null) {
return null;
}
//消息构建类
ConditionMessage.Builder message = ConditionMessage.forCondition(ConditionalOnWebApplication.class);
//判断SERVLET类型的Web应用
if (ConditionalOnWebApplication.Type.SERVLET.name().equals(type)) {
//判断org.springframework.web.context.support.GenericWebApplicationContext类是否存在类加载器中,如果不存在说明不是SERVLET类型的Web应用
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("servlet web application classes").atAll());
}
}
//判断REACTIVE类型的Web应用
if (ConditionalOnWebApplication.Type.REACTIVE.name().equals(type)) {
//判断org.springframework.web.reactive.HandlerResult类是否存在类加载器中,如果不存在说明不是REACTIVE类型的Web应用
if (!ClassNameFilter.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("reactive web application classes").atAll());
}
}
//如果org.springframework.web.context.support.GenericWebApplicationContext类和org.springframework.web.reactive.HandlerResult类都不在类加载器中,说明既不是SERVLET类型的Web应用也不是REACTIVE类型的Web应用
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())
&& !ClassUtils.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("reactive or servlet web application classes").atAll());
}
return null;
}
然后,我们来分析OnWebApplicationCondition类如何实现SpringBootCondition抽象类中定义的getMatchOutcome()方法。具体实现如下:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
//首先判断ConditionalOnWebApplication直接是否存在
boolean required = metadata.isAnnotated(ConditionalOnWebApplication.class.getName());
//判断是否是Web应用
ConditionOutcome outcome = isWebApplication(context, metadata, required);
if (required && !outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
if (!required && outcome.isMatch()) {
return ConditionOutcome.noMatch(outcome.getConditionMessage());
}
return ConditionOutcome.match(outcome.getConditionMessage());
}
根据上面的代码,我们可以知道,判断是否是Web应用的逻辑是由isWebApplication()方法实现的,首先由deduceType()方法判断具体的Web类型,然后再交给具体的方法进行判断,具体实现如下:
private ConditionOutcome isWebApplication(ConditionContext context, AnnotatedTypeMetadata metadata,
boolean required) {
//判断Web类型
switch (deduceType(metadata)) {
case SERVLET:
return isServletWebApplication(context);
case REACTIVE:
return isReactiveWebApplication(context);
default:
return isAnyWebApplication(context, required);
}
}
private Type deduceType(AnnotatedTypeMetadata metadata) {
Map<String, Object> attributes = metadata.getAnnotationAttributes(ConditionalOnWebApplication.class.getName());
if (attributes != null) {
return (Type) attributes.get("type");
}
return Type.ANY;
}
三种Web类型的判断的逻辑,基本上是一样的,这里我们以SERVLET类型的Web应用的为例,来分析具体的实现,代码如下:
private ConditionOutcome isServletWebApplication(ConditionContext context) {
//日志消息构建实例
ConditionMessage.Builder message = ConditionMessage.forCondition("");
//存在对应的GenericWebApplicationContext类,所以不是SERVLET类型Web应用
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, context.getClassLoader())) {
return ConditionOutcome.noMatch(message.didNotFind("servlet web application classes").atAll());
}
if (context.getBeanFactory() != null) {
//存在session的Scope则是SERVLET类型Web应用(后续需要详细了解该条件的判断依据(待处理))
String[] scopes = context.getBeanFactory().getRegisteredScopeNames();
if (ObjectUtils.containsElement(scopes, "session")) {
return ConditionOutcome.match(message.foundExactly("'session' scope"));
}
}
//如果是ConfigurableWebEnvironment的上下文,判断是SERVLET类型Web应用
if (context.getEnvironment() instanceof ConfigurableWebEnvironment) {
return ConditionOutcome.match(message.foundExactly("ConfigurableWebEnvironment"));
}
//如果是资源加载器是WebApplicationContext类型,判断是SERVLET类型Web应用
if (context.getResourceLoader() instanceof WebApplicationContext) {
return ConditionOutcome.match(message.foundExactly("WebApplicationContext"));
}
//其他情况,则都不是SERVLET类型Web应用
return ConditionOutcome.noMatch(message.because("not a servlet web application"));
}
注解@ConditionalOnWebApplication在Spring Boot环境初始化的过程中,用来判断当前项目是否是Web应用,具体是什么类型的Web应用,会根据不同的结果进行相应的初始化过程,后续在分析Spring Boot初始化过程中在分析注解@ConditionalOnWebApplication在Spring Boot框架中的应用。