欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

精尽Spring Boot源码分析 - Condition 接口的扩展

程序员文章站 2022-06-19 11:46:09
该系列文章是笔者在学习 Spring Boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 Spring Boot 源码分析 GitHub 地址 进行阅读 Spring Boot 版本:2.2.x 最好对 Spring 源码有一定的了解,可以先查看我的 《死磕 Sp ......

该系列文章是笔者在学习 spring boot 过程中总结下来的,里面涉及到相关源码,可能对读者不太友好,请结合我的源码注释 spring boot 源码分析 github 地址 进行阅读

spring boot 版本:2.2.x

最好对 spring 源码有一定的了解,可以先查看我的 《死磕 spring 之 ioc 篇 - 文章导读》 系列文章

如果该篇内容对您有帮助,麻烦点击一下“推荐”,也可以关注博主,感激不尽~

该系列其他文章请查看:《精尽 spring boot 源码分析 - 文章导读》

概述

在上一篇《剖析 @springbootapplication 注解》文章分析了 spring boot 的自动配置功能,在通过 @enableautoconfiguration 注解驱动整个自动配置模块的过程中,并不是所有的自动配置类都需要被注入,不同的自动配置类需要满足一定条件后,才应该进行自动配置

那么 spring boot 怎么知道满足一定条件呢?spring boot 对 spring 的 condition 接口进行了扩展,然后结合自定义的注解,则可以判断自动配置类是否符合条件。

例如 @conditionalonclass 可以指定必须存在哪些 class 对象才注入这个 bean。

那么接下来,我们一起来看看 spring 的 condition 接口以及 spring boot 对其的扩展

condition 演进史

profile 的出场

在 spring 3.1 的版本,为了满足不同环境注册不同的 bean ,引入了 @profile 注解。例如:

@configuration
public class datasourceconfiguration {

    @bean
    @profile("dev")
    public datasource devdatasource() {
        // ... 单机 mysql
    }

    @bean
    @profile("prod")
    public datasource proddatasource() {
        // ... 集群 mysql
    }
}
  • 在测试环境下,我们注册单机 mysql 的 datasource bean
  • 在生产环境下,我们注册集群 mysql 的 datasource bean

spring 3.1.x 的 @profile 注解如下:

@retention(retentionpolicy.runtime)
@target(elementtype.type)
public @interface profile {

	/**
	 * the set of profiles for which this component should be registered.
	 */
	string[] value();
}

可以看到,最开始 @profile 注解并没有结合 @conditional 注解一起使用,而是在后续版本才引入的

condition 的出现

在 spring 4.0 的版本,出现了 condition 功能,体现在 org.springframework.context.annotation.condition 接口,如下:

@functionalinterface
public interface condition {

	/**
	 * determine if the condition matches.
	 * @param context the condition context
	 * @param metadata the metadata of the {@link org.springframework.core.type.annotationmetadata class}
	 * or {@link org.springframework.core.type.methodmetadata method} being checked
	 * @return {@code true} if the condition matches and the component can be registered,
	 * or {@code false} to veto the annotated component's registration
	 */
	boolean matches(conditioncontext context, annotatedtypemetadata metadata);
}

函数式接口,只有一个 matches(..) 方法,判断是否匹配,从入参中可以知道,它是和注解配合一起实现 condition 功能的,也就是 @conditional 注解,如下:

@target({elementtype.type, elementtype.method})
@retention(retentionpolicy.runtime)
@documented
public @interface conditional {

	/**
	 * all {@link condition} classes that must {@linkplain condition#matches match}
	 * in order for the component to be registered.
	 */
	class<? extends condition>[] value();
}

随之 @profile 注解也进行了修改,和 @conditional 注解配合使用

spring 5.1.x 的 @profile 注解如下:

@target({elementtype.type, elementtype.method})
@retention(retentionpolicy.runtime)
@documented
@conditional(profilecondition.class)
public @interface profile {

	/**
	 * the set of profiles for which the annotated component should be registered.
	 */
	string[] value();
}

这里指定的的 condition 实现类是 profilecondition,如下:

class profilecondition implements condition {

	@override
	public boolean matches(conditioncontext context, annotatedtypemetadata metadata) {
		multivaluemap<string, object> attrs = metadata.getallannotationattributes(profile.class.getname());
		if (attrs != null) {
			for (object value : attrs.get("value")) {
				if (context.getenvironment().acceptsprofiles(profiles.of((string[]) value))) {
					return true;
				}
			}
			return false;
		}
		return true;
	}
}

逻辑很简答,从当前 spring 应用上下文的 environment 中判断 @profile 指定的环境是否被激活,被激活了表示匹配成功,则注入对应的 bean,否则,不进行操作

但是 spring 本身提供的 condition 实现类不多,只有一个 profilecondition 对象

springbootcondition 的进击

spring boot 为了满足更加丰富的 condition 场景,对 spring 的 condition 接口进行了扩展,提供更多的实现类,如下:

精尽Spring Boot源码分析 - Condition 接口的扩展

上面仅列出了部分 springbootcondition 的子类,同时这些子类与对应的注解配置一起使用

  • @conditionalonclass:必须都存在指定的 class 对象们
  • @conditionalonmissingclass:指定的 class 对象们必须都不存在
  • @conditionalonbean:必须都存在指定的 bean 们
  • @conditionalonmissingbean:指定的 bean 们必须都不存在
  • @conditionalonsinglecandidate:必须存在指定的 bean
  • @conditionalonproperty:指定的属性是否有指定的值
  • @conditionalonwebapplication:当前的 web 应用类型是否在指定的范围内(any、servlet、reactive)
  • @conditionalonnotwebapplication:不是 web 应用类型

上面列出了 spring boot 中常见的几种 @conditionxxx 注解,他们都配合 @conditional 注解与对应的 condition 实现类一起使用,提供了非常丰富的 condition 场景

condition 在哪生效?

spring 提供了 condition 接口以及 @conditional 注解,那么在 spring 中哪里体现,或者说是哪里进行判断的呢?

其实我在 《死磕spring之ioc篇 - @bean 等注解的实现原理》 这篇文章中有提到过,我们稍微回顾一下,有两种情况:

  • 通过 @component 注解(及派生注解)标注的 bean
  • @configuration 标注的配置类中的 @bean 标注的方法 bean

普通 bean

第一种情况是在 spring 扫描指定路径下的 .class 文件解析成对应的 beandefinition(bean 的前身)时,会根据 @conditional 注解判断是否符合条件,如下:

// classpathbeandefinitionscanner.java
public int scan(string... basepackages) {
    // <1> 获取扫描前的 beandefinition 数量
    int beancountatscanstart = this.registry.getbeandefinitioncount();

    // <2> 进行扫描,将过滤出来的所有的 .class 文件生成对应的 beandefinition 并注册
    doscan(basepackages);

    // register annotation config processors, if necessary.
    // <3> 如果 `includeannotationconfig` 为 `true`(默认),则注册几个关于注解的 postprocessor 处理器(关键)
    // 在其他地方也会注册,内部会进行判断,已注册的处理器不会再注册
    if (this.includeannotationconfig) {
        annotationconfigutils.registerannotationconfigprocessors(this.registry);
    }

    // <4> 返回本次扫描注册的 beandefinition 数量
    return (this.registry.getbeandefinitioncount() - beancountatscanstart);
}
// classpathscanningcandidatecomponentprovider.java
private boolean isconditionmatch(metadatareader metadatareader) {
    if (this.conditionevaluator == null) {
        this.conditionevaluator =
                new conditionevaluator(getregistry(), this.environment, this.resourcepatternresolver);
    }
    return !this.conditionevaluator.shouldskip(metadatareader.getannotationmetadata());
}

上面只是简单的提一下,可以看到会通过 conditionevaluator 计算器进行计算,判断是否满足条件

配置类

第二种情况是 spring 会对 配置类进行处理,扫描到带有 @bean 注解的方法,尝试解析成 beandefinition(bean 的前身)时,会根据 @conditional 注解判断是否符合条件,如下:

// configurationclassbeandefinitionreader.java
private void loadbeandefinitionsforconfigurationclass(
        configurationclass configclass, trackedconditionevaluator trackedconditionevaluator) {

    // <1> 如果不符合 @conditional 注解的条件,则跳过
    if (trackedconditionevaluator.shouldskip(configclass)) {
        string beanname = configclass.getbeanname();
        if (stringutils.haslength(beanname) && this.registry.containsbeandefinition(beanname)) {
            this.registry.removebeandefinition(beanname);
        }
        this.importregistry.removeimportingclass(configclass.getmetadata().getclassname());
        return;
    }

    // <2> 如果当前 configurationclass 是通过 @import 注解被导入的
    if (configclass.isimported()) {
        // <2.1> 根据该 configurationclass 对象生成一个 beandefinition 并注册
        registerbeandefinitionforimportedconfigurationclass(configclass);
    }
    // <3> 遍历当前 configurationclass 中所有的 @bean 注解标注的方法
    for (beanmethod beanmethod : configclass.getbeanmethods()) {
        // <3.1> 根据该 beanmethod 对象生成一个 beandefinition 并注册(注意这里有无 static 修饰会有不同的配置)
        loadbeandefinitionsforbeanmethod(beanmethod);
    }

    // <4> 对 @importresource 注解配置的资源进行处理,对里面的配置进行解析并注册 beandefinition
    loadbeandefinitionsfromimportedresources(configclass.getimportedresources());
    // <5> 通过 @import 注解导入的 importbeandefinitionregistrar 实现类往 beandefinitionregistry 注册 beandefinition
    // mybatis 集成 spring 就是基于这个实现的,可查看 mybatis-spring 项目中的 mapperscannerregistrar 这个类
    // https://github.com/liu844869663/mybatis-spring/blob/master/src/main/java/org/mybatis/spring/annotation/mapperscannerregistrar.java
    loadbeandefinitionsfromregistrars(configclass.getimportbeandefinitionregistrars());
}

上面只是简单的提一下,可以看到会通过 trackedconditionevaluator 计算器进行计算,判断是否满足条件

这里提一下,对于 @bean 标注的方法,会得到 cglib 的提升,也就是返回的是一个代理对象,设置一个拦截器专门对 @bean 方法进行拦截处理,通过依赖查找的方式从 ioc 容器中获取 bean 对象,如果是单例 bean,那么每次都是返回同一个对象,所以当主动调用这个方法时获取到的都是同一个对象。

springbootcondition

org.springframework.boot.autoconfigure.condition.springbootcondition 抽象类,实现了 condition 接口,spring boot 扩展 condition 的抽象基类,主要用于打印相应的日志,并记录每次的匹配结果,如下:

/**
 * base of all {@link condition} implementations used with spring boot. provides sensible
 * logging to help the user diagnose what classes are loaded.
 *
 * @author phillip webb
 * @author greg turnquist
 * @since 1.0.0
 */
public abstract class springbootcondition implements condition {

	private final log logger = logfactory.getlog(getclass());

	@override
	public final boolean matches(conditioncontext context, annotatedtypemetadata metadata) {
		// <1> 从注解元信息中获取所标注的`类名`(或者`类名#方法名`)
		string classormethodname = getclassormethodname(metadata);
		try {
			// <2> 获取匹配结果(包含匹配消息),抽象方法,交由子类实现
			conditionoutcome outcome = getmatchoutcome(context, metadata);
			// <3> 打印匹配日志
			logoutcome(classormethodname, outcome);
			// <4> 向 conditionevaluationreport 中记录本次的匹配结果
			recordevaluation(context, classormethodname, outcome);
			// <5> 返回匹配结果
			return outcome.ismatch();
		}
		catch (noclassdeffounderror ex) {
			// 抛出异常
		} catch (runtimeexception ex) {
			// 抛出异常
		}
	}
}

实现的 condition 接口方法处理过程如下:

  1. 从注解元信息中获取所标注的类名(或者类名#方法名

    private static string getclassormethodname(annotatedtypemetadata metadata) {
        if (metadata instanceof classmetadata) {
            classmetadata classmetadata = (classmetadata) metadata;
            return classmetadata.getclassname();
        }
        methodmetadata methodmetadata = (methodmetadata) metadata;
        return methodmetadata.getdeclaringclassname() + "#" + methodmetadata.getmethodname();
    }
    
  2. 调用 getmatchoutcome(..) 方法,获取匹配结果(包含匹配消息),抽象方法,交由子类实现

  3. 打印匹配日志

    protected final void logoutcome(string classormethodname, conditionoutcome outcome) {
        if (this.logger.istraceenabled()) {
            this.logger.trace(getlogmessage(classormethodname, outcome));
        }
    }
    
  4. 向 conditionevaluationreport 中记录本次的匹配结果

    private void recordevaluation(conditioncontext context, string classormethodname, conditionoutcome outcome) {
        if (context.getbeanfactory() != null) {
            conditionevaluationreport.get(context.getbeanfactory()).recordconditionevaluation(classormethodname, this, outcome);
        }
    }
    
  5. 返回匹配结果

springbootcondition 的实现类

onclasscondition

org.springframework.boot.autoconfigure.condition.onclasscondition,继承 springbootcondition 抽象类,如下:

@order(ordered.highest_precedence)
class onclasscondition extends filteringspringbootcondition {
    /**
	 * 该方法来自 {@link springbootcondition} 判断某个 bean 是否符合注入条件(`@conditionalonclass` 和 `conditionalonmissingclass`)
	 */
	@override
	public conditionoutcome getmatchoutcome(conditioncontext context, annotatedtypemetadata metadata) {
		classloader classloader = context.getclassloader();
		conditionmessage matchmessage = conditionmessage.empty();
		// <1> 获取这个类上面的 `@conditionalonclass` 注解的值
		// 也就是哪些 class 对象必须存在
		list<string> onclasses = getcandidates(metadata, conditionalonclass.class);
		if (onclasses != null) {
			// <1.1> 找到这些 class 对象中哪些是不存在的
			list<string> missing = filter(onclasses, classnamefilter.missing, classloader);
			// <1.2> 如果存在不存在的,那么不符合条件,返回不匹配
			if (!missing.isempty()) {
				return conditionoutcome.nomatch(conditionmessage.forcondition(conditionalonclass.class)
						.didnotfind("required class", "required classes").items(style.quote, missing));
			}
			// <1.3> 添加 `@conditionalonclass` 满足条件的匹配信息
			matchmessage = matchmessage.andcondition(conditionalonclass.class)
					.found("required class", "required classes")
					.items(style.quote, filter(onclasses, classnamefilter.present, classloader));
		}
		// <2> 获取这个类上面的 `@conditionalonmissingclass` 注解的值
		// 也就是这些 class 对象必须都不存在
		list<string> onmissingclasses = getcandidates(metadata, conditionalonmissingclass.class);
		if (onmissingclasses != null) {
			// <2.1> 找到这些 class 对象中哪些是存在的
			list<string> present = filter(onmissingclasses, classnamefilter.present, classloader);
			// <2.2> 如果有一个存在,那么不符合条件,返回不匹配
			if (!present.isempty()) {
				return conditionoutcome.nomatch(conditionmessage.forcondition(conditionalonmissingclass.class)
						.found("unwanted class", "unwanted classes").items(style.quote, present));
			}
			// <2.3> 添加 `@conditionalonmissingclass` 满足条件的匹配信息
			matchmessage = matchmessage.andcondition(conditionalonmissingclass.class)
					.didnotfind("unwanted class", "unwanted classes")
					.items(style.quote, filter(onmissingclasses, classnamefilter.missing, classloader));
		}
		// <3> 返回符合条件的结果
		return conditionoutcome.match(matchmessage);
	}
}

判断是否匹配的过程如下:

  1. 获取这个类上面的 @conditionalonclass 注解的值,也就是哪些 class 对象必须存在

    1. 找到这些 class 对象中哪些是不存在的

      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());
          // 使用 `classnamefilter` 对 `classnames` 进行过滤
          for (string candidate : classnames) {
              if (classnamefilter.matches(candidate, classloader)) {
                  matches.add(candidate);
              }
          }
          // 返回匹配成功的 `classname` 们
          return matches;
      }
      
    2. 如果存在不存在的,那么不符合条件,返回不匹配

    3. 添加 @conditionalonclass 满足条件的匹配信息

  2. 获取这个类上面的 @conditionalonmissingclass 注解的值,也就是这些 class 对象必须都不存在

    1. 找到这些 class 对象中哪些是存在的,和上面的 1.1 差不多,只不过这里传的是 classnamefilter.present 过滤器
    2. 如果有一个存在,那么不符合条件,返回不匹配
    3. 添加 @conditionalonmissingclass 满足条件的匹配信息
  3. 返回符合条件的结果

上面使用到的 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 {
            // 加载指定类,加载成功表示存在这个类
            resolve(classname, classloader);
            return true;
        }
        catch (throwable ex) {
            // 加载失败表示不存在这个类
            return false;
        }
    }
}

逻辑很简单,就是判断 class 对象是否存在或者不存在

其它实现类

关于 springbootcondition 其他的实现类逻辑都差不多,感兴趣的可以去看看

回顾自动配置

在上一篇《剖析 @springbootapplication 注解》 文章分析通过 @enableautoconfiguration 注解驱动整个自动配置模块的过程中,会通过指定的 autoconfigurationimportfilter 对所有的自动配置类进行过滤,满足条件才进行自动配置

可以回顾一下上一篇文章的 2. getautoconfigurationentry 方法 小节和 3. filter 方法 小节

protected autoconfigurationentry getautoconfigurationentry(autoconfigurationmetadata autoconfigurationmetadata,
        annotationmetadata annotationmetadata) {
    // <1> 如果通过 `spring.boot.enableautoconfiguration` 配置关闭了自动配置功能
    if (!isenabled(annotationmetadata)) {
        // 则返回一个“空”的对象
        return empty_entry;
    }
    // <2> 获取 `@enableautoconfiguration` 注解的配置信息
    annotationattributes attributes = getattributes(annotationmetadata);
    // <3> 从所有的 `meta-inf/spring.factories` 文件中找到 `@enableautoconfiguration` 注解对应的类(需要自动配置的类)
    list<string> configurations = getcandidateconfigurations(annotationmetadata, attributes);
    // <4> 对所有的自动配置类进行去重
    configurations = removeduplicates(configurations);
    // <5> 获取需要排除的自动配置类
    // 可通过 `@enableautoconfiguration` 注解的 `exclude` 和 `excludename` 配置
    // 也可以通过 `spring.autoconfigure.exclude` 配置
    set<string> exclusions = getexclusions(annotationmetadata, attributes);
    // <6> 处理 `exclusions` 中特殊的类名称,保证能够排除它
    checkexcludedclasses(configurations, exclusions);
    // <7> 从 `configurations` 中将 `exclusions` 需要排除的自动配置类移除
    configurations.removeall(exclusions);
    /**
     * <8> 从 `meta-inf/spring.factories` 找到所有的 {@link autoconfigurationimportfilter} 对 `configurations` 进行过滤处理
     * 例如 spring boot 中配置了 {@link org.springframework.boot.autoconfigure.condition.onclasscondition}
     * 在这里提前过滤掉一些不满足条件的自动配置类,在 spring 注入 bean 的时候也会判断哦~
     */
    configurations = filter(configurations, autoconfigurationmetadata);
    fireautoconfigurationimportevents(configurations, exclusions);
    // <10> 将所有的自动配置类封装成一个 autoconfigurationentry 对象,并返回
    return new autoconfigurationentry(configurations, exclusions);
}

我们看到第 8 步,调用 filter(..) 方法, 目的就是过滤掉一些不符合 condition 条件的自动配置类

private list<string> filter(list<string> configurations, autoconfigurationmetadata autoconfigurationmetadata) {
    long starttime = system.nanotime();
    // <1> 将自动配置类保存至 `candidates` 数组中
    string[] candidates = stringutils.tostringarray(configurations);
    boolean[] skip = new boolean[candidates.length];
    boolean skipped = false;
    /*
     * <2> 从 `meta-inf/spring.factories` 找到所有的 autoconfigurationimportfilter 对 `candidates` 进行过滤处理
     * 有 onclasscondition、onbeancondition、onwebapplicationcondition
     */
    for (autoconfigurationimportfilter filter : getautoconfigurationimportfilters()) {
        // <2.1> aware 回调
        invokeawaremethods(filter);
        // <2.2> 对 `candidates` 进行匹配处理,获取所有的匹配结果
        boolean[] match = filter.match(candidates, autoconfigurationmetadata);
        // <2.3> 遍历匹配结果,将不匹配的自动配置类至空
        for (int i = 0; i < match.length; i++) {
            if (!match[i]) {
                skip[i] = true;
                candidates[i] = null;
                skipped = true;
            }
        }
    }
    // <3> 如果没有不匹配的结果则全部返回
    if (!skipped) {
        return configurations;
    }
    // <4> 获取到所有匹配的自动配置类,并返回
    list<string> result = new arraylist<>(candidates.length);
    for (int i = 0; i < candidates.length; i++) {
        if (!skip[i]) {
            result.add(candidates[i]);
        }
    }
    return new arraylist<>(result);
}

可以看到第 2 步,会从 meta-inf/spring.factories 中找到对应的 autoconfigurationimportfilter 实现类对所有的自动配置类进行过滤

# auto configuration import filters
org.springframework.boot.autoconfigure.autoconfigurationimportfilter=\
org.springframework.boot.autoconfigure.condition.onbeancondition,\
org.springframework.boot.autoconfigure.condition.onclasscondition,\
org.springframework.boot.autoconfigure.condition.onwebapplicationcondition

这里,我们一定要注意到入参中的 autoconfigurationmetadata 对象,它里面保存了 meta-inf/spring-autoconfigure-metadata.properties 文件中 spring boot 的自动配置类的注解元信息(sprng boot 编译时生成的),如何来的请回顾上一篇文章

autoconfigurationimportfilter

org.springframework.boot.autoconfigure.autoconfigurationimportfilter 接口,用于过滤掉无需自动引入的自动配置类

/**
 * filter that can be registered in {@code spring.factories} to limit the
 * auto-configuration classes considered. this interface is designed to allow fast removal
 * of auto-configuration classes before their bytecode is even read.
 *
 * @author phillip webb
 * @since 1.5.0
 */
@functionalinterface
public interface autoconfigurationimportfilter {

	boolean[] match(string[] autoconfigurationclasses, autoconfigurationmetadata autoconfigurationmetadata);
}

可以看到它的注释,因为自动配置类会很多,如果无需使用,而创建对应的 bean(字节码)到 jvm 内存中,将是一种浪费

精尽Spring Boot源码分析 - Condition 接口的扩展

可以看到它的最终实现类,都是构建在 springbootcondition 之上。