乐趣区

关于java:原理分析Spring-Boot启动时基于springfactories自动读取远端Environment实现的原理源码分析

采纳 Spring 规范的事件 / 监听器模型,通过 Spring SPI 的形式,在 Spring Boot 启动时,主动读取远端「近程服务器、本地硬盘等」Environment 配置,不便在 Spring Boot 启动前,对配置进行灵便调整,减少灵活性,缩小硬编码。

本文先从原理进行剖析,表明其可行性,下一篇文章再展现具体的代码实现。首先从 SPI 的根底开始讲起。

1. 服务发现的根底:SPI

注:此大节内容形容次要参考此文章 spring.factories
在 Spring Boot 中有一种十分解耦的扩大机制:Spring Factories。这种扩大机制实际上是仿照 Java 中的 SPI 扩大机制来实现的。

1.1 背景形容

零碎中各个模块,往往有很多不同的实现计划,如日志组件、JDBC 驱动、XML 解析组件等。面向对象的程序设计中,举荐应用面向接口编程,业务程序中如需应用某项性能,须依赖通用的标准接口。基于可拔插的设计准则,此时如需更换功能模块的底层实现,间接予以替换即可「如替换 Jar、替换 maven 依赖等」,业务代码无需任何改变。上述想法很美妙,然而程序应用依赖的功能模块时,必须进行指明,不然程序运行时可能找不到相应的实现类,然而为理解耦,咱们不想在业务代码中申明具体的实现,有什么解决办法吗?

这就须要一种服务发现机制。Java SPI 就是提供这样的一个机制。Java SPI 机制「Service Provider Interface」次要用于插件等,如需具体理解可参考 java.util.ServiceLoader 的文档。

1.2 Java SPI 约定

Java SPI 的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在 jar 包的 META-INF/services/ 目录里同时创立一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当内部程序拆卸这个模块的时候,就能通过该 jar 包 META-INF/services/ 里的配置文件找到具体的实现类名,并装载实例化,实现模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不须要再在代码里指定。JDK 中提供了服务实现查找的一个工具类:java.util.ServiceLoader

1.3 Spring Boot 中的 SPI 机制

在 Spring 中也有一种相似与 Java SPI 的加载机制。它在 META-INF/spring.factories 文件中配置接口的实现类名称,而后在程序中读取这些配置文件并实例化。这种自定义的 SPI 机制是 Spring Boot Starter 实现的根底。

2. Spring Boot 实现 SPI 的源码剖析

上面就依据 Spring Boot 利用的启动过程,对源码进行简要剖析。当然 Spring Boot 实质是对 Spring 的再封装,故以下内容实用于 Spring,只是局部源码是 Spring Boot 专属的。要留神的是,为了节俭篇幅,防止喧宾夺主,会对理论源码进行精简,以突出要表述的内容。

首先展现最经典的 Spring Boot 启动代码,本节从此处讲起,如下:

public class Application {public static void main(String[] args) {SpringApplication application = new SpringApplication(Application.class);
        application.run(args);
    }
}

2.1 实例化 SpringApplication 对象

在实例化 SpringApplication 对象时,能够看到程序调用了如下构造方法。在执行到 setInitializers((Collection)getSpringFactoriesInstances(ApplicationContextInitializer.class)); 时,即触发了 Spring 实现的 SPI。

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();
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();}

持续深刻看该办法的具体实现,定位到该办法:org.springframework.boot.SpringApplication#getSpringFactoriesInstances,该办法的源码如下:

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

Spring-Core 包里定义了 SpringFactoriesLoader 类,该类实现了检索 META-INF/spring.factories 文件,并获取指定接口的配置的性能。在这个类中定义了两个对外的办法:
loadFactories:依据接口类获取其实现类的实例,这个办法返回的是对象列表。
loadFactoryNames:依据接口获取其接口类的名称,这个办法返回的是类名的列表。
下面的两个办法的要害都是从指定的 ClassLoader 中获取 spring.factories 文件,并解析失去类名列表,

此处应用的是 loadFactoryNames 办法。持续深刻发现理论调用的是 loadSpringFactories 办法:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {return result;}
    try {
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                List<String> factoryClassNames = Arrays.asList(StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
                result.addAll((String) entry.getKey(), factoryClassNames);
            }
        }
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

其中动态常量FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"

2.2 加载 factories 文件

java.lang.ClassLoader#getResources办法会遍历整个我的项目「蕴含依赖」的 META-INF/spring.factories 文件,获得其绝对路径,如 /Users/bitkylin/.m2/repository/cn/bitkylin/test/1.0.0/test.jar/META-INF/spring.factories,应用PropertiesLoaderUtils#loadProperties 办法从门路加载,并最终将接口和其实现类的全名缓存在 cache 对象中。cache对象的构造如下:

一颗多叉树。将 spring.factories 中配置的所有接口和其实现类的全名都读取了进去。此接口将接口 org.springframework.context.ApplicationListener 的实现类的类名的汇合作为后果返回,而后 org.springframework.boot.SpringApplication#createSpringFactoriesInstances 办法将上述实现类均进行实例化,此时监听器就都创立好并注册了。

spring.factories 是通过 Properties 解析失去的,咱们能够依照如下规定编写:

com.xxx.interface=com.xxx.classname

key 是接口,value 是实现类。零碎会主动将其初始化为如图所示的构造,方便使用。

2.3 Spring Boot 启动

调用 org.springframework.boot.SpringApplication#run 办法,开始启动 Spring Boot。在启动最开始阶段,程序就会调用到 org.springframework.boot.SpringApplication#prepareEnvironment 办法,并最终调用到经典的 org.springframework.context.event.SimpleApplicationEventMulticaster#invokeListener 办法「典型的观察者模式,规范的 Spring 事件 / 监听器模型」,源码如下:

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {ErrorHandler errorHandler = getErrorHandler();
    if (errorHandler != null) {
        try {doInvokeListener(listener, event);
        }
        catch (Throwable err) {errorHandler.handleError(err);
        }
    }
    else {doInvokeListener(listener, event);
    }
}

通过该办法,将事件 ApplicationEnvironmentPreparedEvent 传递到所有已注册的监听器,能够借此实现 Spring Boot 启动时主动读取远端 Environment。具体做法下节再讲述。

参考链接

spring.factories

退出移动版