@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框架中的应用。