侧边栏壁纸
博主头像
孔子说JAVA博主等级

成功只是一只沦落在鸡窝里的鹰,成功永远属于自信且有毅力的人!

  • 累计撰写 352 篇文章
  • 累计创建 135 个标签
  • 累计收到 10 条评论

目 录CONTENT

文章目录

Spring SPI 对 Java SPI 的扩展及运用

孔子说JAVA
2022-10-31 / 0 评论 / 0 点赞 / 61 阅读 / 19,227 字 / 正在检测是否收录...
广告 广告

SPI机制(Service Provider Interface)是一种将服务接口与服务实现分离以达到解耦的机制,大大提升了程序的可扩展性。Spring SPI沿用了Java SPI的设计思想,Spring采用的是spring.factories方式实现SPI机制,可以在不修改Spring源码的前提下,提供Spring框架的扩展性。

1、Spring SPI 简介

Java SPI机制的原理及运用 中,我们可以看到,虽然java提供的SPI机制的思想非常好,但是也存在相应的弊端。具体如下:

  • Java内置的方法方式只能通过遍历来获取。
  • 服务提供接口必须放到META-INF/services/目录下。

Spring SPI沿用了Java SPI的设计思想,对其进行扩展和优化。Spring 通过 spring.handlers 和 spring.factories 两种方式实现SPI机制,可以在不修改Spring源码的前提下,做到对Spring框架的扩展开发。

  • Spring在3.0.x时便引入了spring.handlers,通过配置spring.handlers文件实现自定义标签并使用自定义标签解析类进行解析实现动态扩展。
  • Spring在3.2时引入spring.factories,加强版的SPI配置文件,为Spring的SPI机制的实现提供支撑。
  • Springboot也用到了spring的spi来实现自动装配。

2、使用示例

2.1 简单示例1

  1. 接口类
public interface DataBaseSPI
{
   void getConnection();
}
  1. 实现类
#DB2实现
public class DB2DataBase implements DataBaseSPI
{
    @Override
    public void getConnection()
    {
        System.out.println("this database is db2");
    }

}

#Mysql实现
public class MysqlDataBase implements DataBaseSPI
{
    @Override
    public void getConnection()
    {
       System.out.println("this is mysql database");
    }

}
  1. 在项目的META-INF目录下,新增spring.factories文件

11123

填写相关的接口信息,内容如下(多个实现采用逗号分隔):

com.skywares.fw.juc.springspi.DataBaseSPI = com.skywares.fw.juc.springspi.DB2DataBase, com.skywares.fw.juc.springspi.MysqlDataBase
  1. 测试类
public class SpringSPITest
{
    public static void main(String[] args)
    {
         List<DataBaseSPI> dataBaseSPIs =SpringFactoriesLoader.loadFactories(DataBaseSPI.class, 
                 Thread.currentThread().getContextClassLoader());
         
         for(DataBaseSPI datBaseSPI:dataBaseSPIs){
            datBaseSPI.getConnection();
         }
    }
}

11124

2.2 简单示例2

下面是一个SPI加载配置类的示例,通过SPI结合条件装配选择合适配置类加载;模拟两个数据库Oracle,MySQL根据配置加载合适的配置类;

  1. 配置文件
database.type=mysql
  1. spring.factories文件
org.example.factoryLoader.EnableDataBase=\
  org.example.factoryLoader.OracleConfig,\
  org.example.factoryLoader.MySQLConfig
  1. 配置类
@Configuration
@ConditionalOnDataBaseType("mysql")
public class MySQLConfig {

	@Bean
	public DataBaseType mysqlDataBaseType() {
		DataBaseType dataBaseType = new DataBaseType();
		dataBaseType.setDatabaseType("mysql");
		return dataBaseType;
	}
}
@Configuration
@ConditionalOnDataBaseType("oracle")
public class OracleConfig {

	@Bean
	public DataBaseType mysqlDataBaseType() {
		DataBaseType dataBaseType = new DataBaseType();
		dataBaseType.setDatabaseType("oracle");
		return dataBaseType;
	}
}
@Data
public class DataBaseType {
	private String databaseType;
}
  1. 定义一个条件装配的注解
public class OnDataBaseTypeConditional implements Condition {

	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		String annotationData = (String) Objects.requireNonNull(metadata
						.getAnnotationAttributes(ConditionalOnDataBaseType.class.getName()))
				.get("value");
		String dataBaseType = context.getEnvironment().getProperty("database.type");
		return dataBaseType.equalsIgnoreCase(annotationData);
	}

}
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@Conditional(OnDataBaseTypeConditional.class)
public @interface ConditionalOnDataBaseType {
    
    String value();
}
  1. 定义一个模块装配的注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(DataBaseConfigSelector.class)
public @interface EnableDataBase {
}

SPI根据配置文件的key加载对应的配置类实例; ImportSelector接口的实现类可以根据指定的筛选标准(通常是一个或者多个注解)来决定导入哪些配置类;但是ImportSelector也可以导入普通类;

image-1667050759062

image-1667050762813

selectImports方法根据导入的@Configuration类的 AnnotationMetadata选择并返回要导入的类的类名,即全限定类名;

public class DataBaseConfigSelector implements ImportSelector {

	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) {
		List<String> configClassNames = SpringFactoriesLoader
				.loadFactoryNames(EnableDataBase.class, this.getClass().getClassLoader());
		return configClassNames.toArray(new String[0]);
	}
}
  1. 测试
@Configuration
@EnableDataBase
@PropertySource("database.properties")
public class SpringFactoriesLoaderDemo {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
		ctx.register(SpringFactoriesLoaderDemo.class);
		ctx.refresh();
		System.out.println(ctx.getBean(DataBaseType.class));
	}
}

当前database.type的配置为mysql,运行结果如下:

image-1667050828566

当database.type的配置修改为oracle,运行结果如下:

image-1667050846838

3、Spring SPI实现原理

Spring 通过 spring.handlers 和 spring.factories 两种方式实现了SPI机制。Java SPI从 /META-INF/services 目录加载服务提供接口配置,而Spring默认从 META-INF/spring.handlers 和 META-INF/spring.factories 目录加载配置,其中 META-INF/spring.handlers 的路径可以通过创建实例时重新指定,而 META-INF/spring.factories 固定不可变。下面分别看一下它们的实现原理。

3.1 spring.handlers SPI

Spring在3.0.x时便引入了spring.handlers,通过配置spring.handlers文件实现自定义标签并使用自定义标签解析类进行解析实现动态扩展。spring.handlers SPI是基于xml的,而且在没有boot的年代,它几乎是所有三方框架跟spring整合的必选机制。

image-1667045188387

3.1.1 配置文件

自定义标签配置文件 spring.handlers 是以namespaceUri作为key,NamespaceHandler作为value,建立映射关系,在解析标签时通过 namespaceUri 获取相应的 NamespaceHandler 来解析,从而实现功能的动态扩展。内容配置如下:

http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.SimpleConstructorNamespaceHandler
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler

3.1.2 关键代码

在Spring里有个接口 NamespaceHandlerResolver,只有一个默认的实现类 DefaultNamespaceHandlerResolver,类似于Java SPI的ServiceLoader,而它的作用就是加载解析classpath下可能分散在各个jar包中的 META-INF/spring.handlers 文件,生成namespaceUri 和 NamespaceHandler名称的映射,并实例化NamespaceHandler。

  1. DefaultNamespaceHandlerResolver.resolve()方法本身是根据namespaceUri获取对应的namespaceHandler对标签进行解析,核心源码:
public NamespaceHandler resolve(String namespaceUri) {
    // 1、核心逻辑之一:获取namespaceUri和namespaceHandler映射关系
    Map<String, Object> handlerMappings = getHandlerMappings();
    // 根据namespaceUri参数取对应的namespaceHandler全限定类名or NamespaceHandler实例
    Object handlerOrClassName = handlerMappings.get(namespaceUri);
    if (handlerOrClassName == null) {
        return null;
    }
    // 2、handlerOrClassName是已初始化过的实例则直接返回
    else if (handlerOrClassName instanceof NamespaceHandler) {
        return (NamespaceHandler) handlerOrClassName;
    }else {
        String className = (String) handlerOrClassName;
        try {
            ///3、使用反射根据namespaceHandler全限定类名加载实现类
            Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
            if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
                throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
                        "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
            }
            // 3.1、初始化namespaceHandler实例
            NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
            // 3.2、 初始化,不同的namespaceHandler实现类初始化方法逻辑有差异
            namespaceHandler.init();
            // 4、将初始化好的实例放入内存缓存中,下次解析到相同namespaceUri标签时直接返回,避免再次初始化
            handlerMappings.put(namespaceUri, namespaceHandler);
            return namespaceHandler;
        }catch (ClassNotFoundException ex) {
            throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
                    namespaceUri + "] not found", ex);
        }catch (LinkageError err) {
            throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
                    namespaceUri + "]: problem with handler class file or dependent class", err);
        }
    }
}

第1步:源码中getHandlerMappings()是比较核心的一个方法,通过懒加载的方式解析spring.handlers并返回namespaceUri和NamespaceHandler的映射关系。

第2步:根据namespaceUri返回对应的NamespaceHandler全限定名或者具体的实例(是名称还是实例取决于是否被初始化过,若是初始化过的实例会直接返回)

第3步:是NamespaceHandler实现类的全限定名,通过上述源码中的第3步,使用反射进行初始化。

第4步:将初始化后的实例放到handlerMappings内存缓存中,这也是第2步为什么可能是NamespaceHandler类型的原因。

看完resolve方法的源码,再看下resolve方法在Spring中调用场景,大致可以了解spring.handlers的使用场景:

image-1667047975881

可以看到resolve()主要用在标签解析过程中,主要被在BeanDefinitionParserDelegate的parseCustomElement和decorateIfRequired方法中调用。

  1. resolve()源码中核心逻辑之一便是调用的getHandlerMappings(),在getHandlerMappings()中实现对各个jar包中的META-INF/spring.handlers文件的解析,如:
private Map<String, Object> getHandlerMappings() {
    Map<String, Object> handlerMappings = this.handlerMappings;
    // 使用线程安全的解析逻辑,避免在并发场景下重复的解析,没必要重复解析
    // 这里在同步代码块的内外对handlerMappings == null作两次判断很有必要,采用懒汉式初始化
    if (handlerMappings == null) {
        synchronized (this) {
            handlerMappings = this.handlerMappings;
            // duble check
            if (handlerMappings == null) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
                }
                try {
                    // 加载handlerMappingsLocation目录文件,handlerMappingsLocation路径值可变,默认是META-INF/spring.handlers
                    Properties mappings =
                            PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
                    if (logger.isDebugEnabled()) {
                        logger.debug("Loaded NamespaceHandler mappings: " + mappings);
                    }
                    // 初始化内存缓存
                    handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
                    // 将加载到的属性合并到handlerMappings中
                    CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
                    // 赋值内存缓存
                    this.handlerMappings = handlerMappings;
                }catch (IOException ex) {
                    throw new IllegalStateException(
                            "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
                }
            }
        }
    }
    return handlerMappings;
}

源码中this.handlerMappings是一个Map类型的内存缓存,存放解析到的namespaceUri以及NameSpaceHandler实例。

getHandlerMappings()方法体中的实现使用了线程安全方式,增加了同步逻辑。

通过阅读源码可以了解到Spring基于spring.handlers实现SPI逻辑相对比较简单,但应用却比较灵活,对自定义标签的支持很方便,在不修改Spring源码的前提下轻松实现接入。spring.handlers 为Spring的动态扩展提供更多的入口和手段,为自定义标签的实现提供了强力支撑。

3.2 spring.factories SPI

工具类:Spring中使用的类是SpringFactoriesLoader,在org.springframework.core.io.support包中
文件路径:文件路径不同 spring配置放在 META-INF/spring.factories中

Spring在3.2.x时又新引入了spring.factories,它的实现和JDK的SPI基本相似,对其进行了拓展,可以按类加载。使用方式如下:

// 获取某个已定义接口的实现类,跟JDK的ServiceLoader SPI相似度为90%
List<Animal> animals = SpringFactoriesLoader.loadFactories(Animal.class, classLoader);
Animal animal = animals.get(1);
animal.say();

输出结果:

image-1667046989137

org.springframework.core.io.support.SpringFactoriesLoader就是Spring框架中的“ServiceLoader”,该类提供了下列功能:

  • 类静态成员常量 final String FACTORIES_RESOURCE_LOCATION = “META-INF/spring.factories” 此常量定义了该工具类要从每个jar包中提取的工厂类定义属性文件的相对路径。
  • 类静态方法 <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) 此方法会读取classpath上所有的jar包中的所有 META-INF/spring.factories 属性文件,找出其中定义的匹配类型 factoryClass 的工厂类,然后创建每个工厂类的对象/实例,并返回这些工厂类对象/实例的列表。
  • 类静态方法 List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) 此方法会读取classpath上所有的jar包中的所有 META-INF/spring.factories 属性文件,找出其中定义的匹配类型 factoryClass 的工厂类,然后并返回这些工厂类的名字列表,注意是包含包名的全限定名。、

3.2.1 配置文件

上述代码获取 Animal 这个接口的所有实现类,查看源码可以发现,它指定的配置文件是有要求的,必须放在 META-INF/spring.factories 中(该配置文件指定接口及实现类的映射关系)。

  • spring.factories文件的格式为:key=value1,value2,value3

image-1667046543982

配置文件的内容为key=value1,value2,value3,其中 key 为接口全限定名,value 为实现类全限定名,多个之间以 , 分隔,换行符使用 \

image-1667046888871

配置内容示例:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
 
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\org.springframework.boot.context.event.EventPublishingRunListener
 
spring.factories实现的SPI是以接口的全限定名作为key,接口实现类作为value,多个实现类用逗号隔开,最终返回的结果是该接口所有实现类的实例集合

3.2.2 关键代码

loadFactories 的作用是从所有jar文件中找到MET-INF/spring.factories文件(注意是:classpath下的所有jar包,所以可插拔、扩展性超强),从中加载配置的那些类,看下 loadFactories 的关键代码:

public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
    Assert.notNull(factoryClass, "'factoryClass' must not be null");
    ClassLoader classLoaderToUse = classLoader;
    // 1.确定类加载器
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    // 2.核心逻辑之一:解析各jar包中META-INF/spring.factories文件中factoryClass的实现类全限定名
    List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
    if (logger.isTraceEnabled()) {
        logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
    }
    List<T> result = new ArrayList<T>(factoryNames.size());
    // 3.遍历实现类的全限定名并进行实例化
    for (String factoryName : factoryNames) {
        result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
    }
    // 排序
    AnnotationAwareOrderComparator.sort(result);
    // 4.返回实例化后的结果集
    return result;
}
  • 源码中loadFactoryNames() 是另外一个比较核心的方法,解析spring.factories文件中指定接口的实现类的全限定名。经过源码中第2步解析得到实现类的全限定名后,在第3步通过instantiateFactory()方法逐个实例化实现类。
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    // 1.接口全限定名
    String factoryClassName = factoryClass.getName();
    try {
        // 2.加载META-INF/spring.factories文件路径(分布在各个不同jar包里,所以这里会是多个文件路径,枚举返回)
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
        // 3.遍历枚举集合,逐个解析spring.factories文件
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            String propertyValue = properties.getProperty(factoryClassName);
            // 4.spring.factories文件中一个接口的实现类有多个时会用逗号隔开,这里拆开获取实现类全限定名
            for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {
                result.add(factoryName.trim());
            }
        }
        return result;
    }catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}
  • 上述第2步获取所有jar包中META-INF/spring.factories文件路径,以枚举值返回。
  • 上述第3步开始遍历spring.factories文件路径,逐个加载解析,整合factoryClass类型的实现类名称。

获取到实现类的全限定名集合后,便根据实现类的名称逐个实例化,继续看下instantiateFactory()方法的源码:

private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
    try {
        // 1.使用classLoader类加载器加载instanceClassName类
        Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
        if (!factoryClass.isAssignableFrom(instanceClass)) {
            throw new IllegalArgumentException(
                    "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
        }
        // 2.instanceClassName类中的构造方法
        Constructor<?> constructor = instanceClass.getDeclaredConstructor();
        ReflectionUtils.makeAccessible(constructor);
        // 3.实例化
        return (T) constructor.newInstance();
    }
    catch (Throwable ex) {
        throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
    }
}
  • 实例化方法是私有型(private)静态方法,这个有别于loadFactories和loadFactoryNames。实例化逻辑整体使用了反射实现,比较通用的实现方式。

3.2.3 Spring框架中的使用

Spring在3.2便已引入spring.factories,那spring.factories在Spring框架中又是如何使用的呢?先看下loadFactories方法的调用情况:

image-1667048754223

从调用情况看Spring自3.2引入spring.factories SPI后并没有真正的利用起来,使用的地方比较少,然而真正把spring.factories发扬光大的,是在Spring Boot中, 简单了解下SpringBoot中的调用。

  • getSpringFactoriesInstances()、getSpringFactoriesInstances()并不是Spring框架中的方法,而是SpringBoot中SpringApplication类里定义的私有型(private)方法,很多地方都有调用,源码如下:
// 单个参数getSpringFactoriesInstances方法
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    // 默认调用多参的重载方法
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}
// 多个参数的getSpringFactoriesInstances方法
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // 调用SpringFactoriesLoader中的loadFactoryNames方法加载接口实现类的全限定名
    Set<String> names = new LinkedHashSet<>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 实例化
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

在getSpringFactoriesInstances()中调用了SpringFactoriesLoader.loadFactoryNames()来加载接口实现类的全限定名集合,然后进行初始化。除了这些,springboot在其他逻辑中也广泛运用着SpringFactoriesLoader中的方法来实现动态扩展,这里就不一一列举了。

3.3 与java spi比较

Spring 采用spring.factories实现SPI与java实现SPI非常相似,但是spring的spi方式针对java的spi进行的相关优化具体内容如下:

  • Java SPI是一个服务提供接口对应一个配置文件,配置文件中存放当前接口的所有实现类,多个服务提供接口对应多个配置文件,所有配置都在services目录下;
  • Java SPI使用了懒加载模式,即在调用ServiceLoader.load()时仅是返回了ServiceLoader实例,尚未解析接口对应的配置文件,在使用时即循环遍历时才正式解析返回服务提供接口的实现类实例;
  • Spring factories SPI是一个spring.factories配置文件存放多个接口及对应的实现类,以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开,仅spring.factories一个配置文件。
  • Spring factories SPI在调用SpringFactoriesLoader.loadFactories()时便已解析spring.facotries文件返回接口实现类的实例(实现细节在源码分析中详解)。
  • 对比java spi不难发现,SpringSPI使用起来更方便,实现类的实例都存放在List集合中,调用方可以任意选择。在项目中,只需要修改调用方的spring.factories文件和引入实现类的jar包,就可以快速选择适合的实现了。

4、SpringBoot SPI应用

Spring Boot提供的一些JAR包,里面会带有文件META-INF/spring.factories。Spring Boot应用启动的时候,根据启动阶段不同的需求,框架就会调用SpringFactoriesLoader加载相应的工厂配置信息。

  • 比如SpringBoot应用使用了注解@EnableAutoConfiguration时,就会触发对SpringFactoriesLoader.loadFactoryNames()的调用。

除此之外,SpringBoot还会对外提供其它的扩展点,下面列举了部分常见的扩展点

// SpringApplication.initialize 
// => SpringApplication.getSpringFactoriesInstances()
SpringFactoriesLoader.loadFactoryNames(org.springframework.context.ApplicationContextInitializer)

// SpringApplication.initialize 
// => SpringApplication.getSpringFactoriesInstances()
SpringFactoriesLoader.loadFactoryNames(org.springframework.context.ApplicationListenr)

// SpringApplication.run 
// => getRunListeners 
// => SpringApplication.getSpringFactoriesInstances()
SpringFactoriesLoader.loadFactoryNames(org.springframework.boot.SpringApplicationRunListener)

// SpringApplication.run 
// => prepareEnvironment 
// => SpringApplication.getSpringFactoriesInstances()
// => ConfigFileApplicationListener.onApplicationEnvironmentPreparedEvent() //事件处理
// => loadPostProcessors()
SpringFactoriesLoader.loadFactoryNames(org.springframework.boot.env.EnvironmentPostProcessor)

以 ApplicationContextInitializer 为例,分析一下在SpringBoot中 ApplicationContextInitializer 扩展点的加载时机。

SpringBoot启动是直接使用 main 方法启动,程序入口一目了然,对源码阅读比较友好,下面是一个SpringBoot应用标准的启动代码:

SpringApplication.run(SpringBootTestApplication.class, args);

我们从 run 方法入手:

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    // <1> 调用重载run方法
	return run(new Class<?>[] { primarySource }, args);
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    // <2> 创建,并初始化SpringApplication实例,并调用run方法,启动容器
	return new SpringApplication(primarySources).run(args);
}

首先是连续两个重载的静态run方法,静态run方法内部会调用构造方法实例化SpringApplication对象,再调用一个成员方法run()来正式启动。

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must not be null");
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	this.bootstrapRegistryInitializers = new ArrayList<>(
			getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    // <3> 通过SPI机制,加载classpath环境中配置的ApplicationContextInitializer实现类,并实例化
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // <4> 通过SPI机制,加载classpath环境中配置的ApplicationListener实现类,并实例化
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	this.mainApplicationClass = deduceMainApplicationClass();
}

在调用构造器实例化SpringApplication时,会通过SPI机制,加载classpath环境中配置的ApplicationContextInitializer实现类,并实例化:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
	return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
	// 获取当前线程的classLoader
	ClassLoader classLoader = getClassLoader();
	// Use names and ensure unique to protect against duplicates
	// <1> 加载指定类型对应的,在 `META-INF/spring.factories` 里的类名的数组
	Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
	// <2> 创建对象
	List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
	// <3> 排序对象
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}

5、SpringBoot2.7自动配置新模式

SpringBoot2.7.0 自动配置将不推荐使用spring.factories

SpringBoot 2.7 中,不再推荐使用/META-INF/spring.factories文件作为自动配置类的配置文件,所以对于有自定义Starter的开发者来说,有时间要抓紧把这一变化改起来了,因为在SpringBoot 3开始将移除对/META-INF/spring.factories的支持。

image-1667049516575

如果您已经创建了自动配置,那么应该将注册从META-INF/spring.factories转移到一个名为META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports的新文件。每一行都包含自动配置的完全限定名。请参考:the included auto-configurations

为了向后兼容,spring.factories 的功能仍将暂时支持。

0

评论区