摘要

本博文主要介绍SpringBoot SPI原理,帮助大家都更好的理解SPI机制在Springboot中应用。

一、双亲委派机制

java中的类加载器负载加载来自文件系统、网络或者其他来源的类文件。jvm的类加载器默认使用的是双亲委派模式。三种默认的类加载器Bootstrap ClassLoader、Extension ClassLoader和System ClassLoader(Application ClassLoader)每一个中类加载器都确定了从哪一些位置加载文件。于此同时我们也可以通过继承java.lang.classloader实现自己的类加载器。

  • Bootstrap ClassLoader:负责加载JDK自带的rt.jar包中的类文件,是所有类加载的父类
  • Extension ClassLoader:负责加载java的扩展类库从jre/lib/ect目录或者java.ext.dirs系统属性指定的目录下加载类,是System ClassLoader的父类加载器
  • System ClassLoader:负责从classpath环境变量中加载类文件

springboot使用istio_spring

1.1 双亲委派模型

当一个类加载器收到类加载任务时,会先交给自己的父加载器去完成,因此最终加载任务都会传递到最顶层的BootstrapClassLoader,只有当父加载器无法完成加载任务时,才会尝试自己来加载。

具体:根据双亲委派模式,在加载类文件的时候,子类加载器首先将加载请求委托给它的父加载器,父加载器会检测自己是否已经加载过类,如果已经加载则加载过程结束,如果没有加载的话则请求继续向上传递直Bootstrap ClassLoader。如果请求向上委托过程中,如果始终没有检测到该类已经加载,则Bootstrap ClassLoader开始尝试从其对应路劲中加载该类文件,如果失败则由子类加载器继续尝试加载,直至发起加载请求的子加载器为止。

采用双亲委派模式可以保证类型加载的安全性,不管是哪个加载器加载这个类,最终都是委托给顶层的BootstrapClassLoader来加载的,只有父类无法加载自己猜尝试加载,这样就可以保证任何的类加载器最终得到的都是同样一个Object对象

protected Class<?> loadClass(String name, boolean resolve) {
    synchronized (getClassLoadingLock(name)) {
    // 首先,检查该类是否已经被加载,如果从JVM缓存中找到该类,则直接返回
    Class<?> c = findLoadedClass(name);
    if (c == null) {
        try {
            // 遵循双亲委派的模型,首先会通过递归从父加载器开始找,
            // 直到父类加载器是BootstrapClassLoader为止
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } catch (ClassNotFoundException e) {}
        if (c == null) {
            // 如果还找不到,尝试通过findClass方法去寻找
            // findClass是留给开发者自己实现的,也就是说
            // 自定义类加载器时,重写此方法即可
           c = findClass(name);
        }
    }
    if (resolve) {
        resolveClass(c);
    }
    return c;
    }
}

1.2 双亲委派模型缺陷

在双亲委派模型中,子类加载器可以使用父类加载器已经加载的类,而父类加载器无法使用子类加载器已经加载的。这就导致了双亲委派模型并不能解决所有的类加载器问题。

案例:Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JNDI、JAXP 等,这些SPI的接口由核心类库提供,却由第三方实现,这样就存在一个问题:SPI 的接口是 Java 核心库的一部分,是由BootstrapClassLoader加载的;SPI实现的Java类一般是由AppClassLoader来加载的。BootstrapClassLoader是无法找到 SPI 的实现类的,因为它只加载Java的核心库。它也不能代理给AppClassLoader,因为它是最顶层的类加载器。也就是说,双亲委派模型并不能解决这个问题。

使用线程上下文类加载器(ContextClassLoader)加载

如果不做任何的设置,Java应用的线程的上下文类加载器默认就是AppClassLoader。在核心类库使用SPI接口时,传递的类加载器使用线程上下文类加载器,就可以成功的加载到SPI实现的类。线程上下文类加载器在很多SPI的实现中都会用到。

通常我们可以通过Thread.currentThread().getClassLoader()和Thread.currentThread().getContextClassLoader()获取线程上下文类加载器。

使用类加载器加载资源文件,比如jar包

类加载器除了加载class外,还有一个非常重要功能,就是加载资源,它可以从jar包中读取任何资源文件,比如,ClassLoader.getResources(String name)方法就是用于读取jar包中的资源文件

//获取资源的方法
public Enumeration<URL> getResources(String name) throws IOException {
    Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
    if (parent != null) {
        tmp[0] = parent.getResources(name);
    } else {
        tmp[0] = getBootstrapResources(name);
    }
    tmp[1] = findResources(name);
    return new CompoundEnumeration<>(tmp);
}

它的逻辑其实跟类加载的逻辑是一样的,首先判断父类加载器是否为空,不为空则委托父类加载器执行资源查找任务,直到BootstrapClassLoader,最后才轮到自己查找。而不同的类加载器负责扫描不同路径下的jar包,就如同加载class一样,最后会扫描所有的jar包,找到符合条件资源文件。

// 使用线程上下文类加载器加载资源
public static void main(String[] args) throws Exception{
    // Array.class的完整路径
    String name = "java/sql/Array.class";
    Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources(name);
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        System.out.println(url.toString());
    }
}

二、SPI原理

2.1 Java SPI

SPI全称是Service Provider Interface ,它是JDK内置的一种动态扩展点的实现。简单来说,就是我们可以定义一个标准的接口,然后第三方的库里面可以实现这个接口。 那么,程序在运行的时候,会根据配置信息动态加载第三方实现的类,从而完成功能的动态扩展机制。

springboot使用istio_加载_02

在Java里面,SPI机制有一个非常典型的实现案例,就是数据库驱动java.jdbc.Driver,JDK里面定义了数据库驱动类Driver,它是一个接口,JDK并没有提供实现。 具体的实现是由第三方数据库厂商来完成的。在程序运行的时候,会根据我们声明的驱动类型,来动态加载对应的扩展实现,从而完成数据库的连接。

springboot使用istio_springboot使用istio_03

除此之外,在很多开源框架里面都借鉴了Java SPI的思想,提供了自己的SPI框架,比如Dubbo定义了ExtensionLoader,实现功能的扩展。Spring提供了SpringFactoriesLoader,实现外部功能的集成。

当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。通过这个约定,就不需要把服务放在代码中了,通过模块被装配的时候就可以发现服务类了。

2.2 SpringBoot SPI

springboot中的类SPI扩展机制

在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    // 取得资源文件的URL
    Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    List<String> result = new ArrayList<String>();
    // 遍历所有的URL
    while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        // 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
        Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
        String factoryClassNames = properties.getProperty(factoryClassName);
        // 组装数据,并返回
        result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
    }
    return result;
}

springboot使用istio_加载_04

三、Springboot SPI原理

在Spring Boot中,SPI机制允许开发者通过定义接口和实现类的方式,实现对应的功能扩展。通过在META-INF/spring.factories配置文件中列出实现类, Spring Boot能够自动加载并使用这些扩展点,提供了灵活的定制和扩展能力。

Springboot的SPI机制是怎么实现的?

  1. 程序启动,注册配置类处理器
  2. spring刷新上下文,执行配置类处理器
  3. 扫描spring.factories将得到的BeanDefinition注册到容器
  4. spring实例化/初始化这些BeanDefinition

3.1 启动SpringBoot程序,创建应用上下文ApplicationContext

我们会调用SpringApplication.run启动程序,通常情况下我们会在程序的主启动类上加一个 @SpringBootApplication 注解,追踪这个注解会发现它是一个复杂的聚合注解, 其中内部包含了@Configuration、 @EnableAutoConfiguration ,而 @EnableAutoConfiguration 有集成了 @Import 注解,这个注解对于springboot非常重要!

// 用户定义的程序主启动类,启动程序
@SpringBootApplication
public class SpringbootExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootExampleApplication.class, args);
    }
}


// Springboot的主启动类,创建spring上下文
public class SpringApplication {
    public ConfigurableApplicationContext run(String... args) {
        ConfigurableApplicationContext context = null;
        try {
            ...
            context = createApplicationContext();
            ...
        } catch (Throwable ex) {
            ...
        }
    }
}

3.2 注册BeanFactory后置处理器用于处理配置类上的注解

springboot启动创建应用上下文后,会注册一系列的内置的后置处理器,如ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、 CommonAnnotationBeanPostProcessor,其中 ConfigurationClassPostProcessor 实现了BeanFactoryPostProcessor是一个BeanFactory级别的处理器用于处理配置类

注册PostProcessor代码如下:

public class AnnotatedBeanDefinitionReader {
    public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
        ...
        // 注册各种后置处理器
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    } 
}

// 注册各种后置处理器
public abstract class AnnotationConfigUtils {
    public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(BeanDefinitionRegistry registry, @Nullable Object source) {
        ...
        if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
            // 注册<配置类>处理器
            RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
            def.setSource(source);
            beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
        }
        ...
        return beanDefs;
    }
}

3.3 刷新应用上下文,执行注册的后置处理器

springboot执行refreshContext刷新上下文,本质还是spring上下文的refresh方法,这个方法是spring生命周期的关键核心代码, spring生命周期的大部分扩展点都是在这里执行的。其中在执行容器中未实例化初始化的bean定义之前会执行内置的、用户自定义的所有注册的beanFactory后置处理器 ,这里会执行到ConfigurationClassPostProcessor这个配置类处理器。

刷新上下文代码如下:

// Springboot调用refreshContext来执行父类的refresh方法
public class SpringApplication {
    public ConfigurableApplicationContext run(String... args) {
        ...
        try {
            ...
            // 刷新上下文
            refreshContext(context);
            ...
        } catch (Throwable ex) {
            throw new IllegalStateException(ex);
        }
        ...
        return context;
    }
}


// 调用spring的AbstractApplicationContext的refresh()方法刷新
public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
    public void refresh() throws BeansException, IllegalStateException {
        ...
        try {
            ...
            // 执行所有注册的BeanFactory后置处理器
            invokeBeanFactoryPostProcessors(beanFactory);
            ...
            // 实例化、初始化所有的单例Bean
            finishBeanFactoryInitialization(beanFactory);
        } catch (BeansException ex) {
            ...
        }
    }
}

3.4 执行配置类后置处理器后置处理器(SPI核心逻辑)

spring在执行到BeanFactory处理器的生命周期时会执行到ConfigurationClassPostProcessor这个处理器, 执行具体处理逻辑,如配置类上的@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean等注解都会有相应的处理,目的就是将定义的BeanDefinition注册到BeanFactory容器中以便于最终的初始化。

由于用@SpringBootApplication这个注解标记的类即SpringbootExampleApplication.class本身也是个配置类, 这里的话就会处理这个主启动类。

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor {
    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        // 这里会有我们程序的主启动配置类,也就是SpringbootExampleApplication.class
        List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
        ...
        // 转换每个配置类
        ConfigurationClassParser parser = new ConfigurationClassParser();
        ...
        do {
            ...
            // 处理配置类
            parser.parse(candidates);
            ...
        } while (!candidates.isEmpty());
    }
}


// 调用parse方法后会执行到doProcessConfigurationClass
class ConfigurationClassParser {
    protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter) {
        // 处理配置类的@PropertySource注解
        ...
        processPropertySource(propertySource);
        ...
        // 处理配置类的@ComponentScan注解
        ...
        Set<BeanDefinitionHolder> scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
        ...
        // 处理配置类的@Import注解,这里就是SPI机制核心实现逻辑了
        processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
        // 处理配置类的@ImportResource注解
        ...
        String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
        configClass.addImportedResource(resolvedResource, readerClass);
        ...
        // 处理配置类的@Bean注解
        ...
        Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
        ...
    }
}

processImports这个方法会专门处理 @Import 这个注解, 进入方法发现会执行getImports方法获得@Import导入的类AutoConfigurationImportSelector.class,由于它实现了DeferredImportSelector,所以程序会执行 deferredImportSelectorHandler.handle去处理这个Selector;其实这里也还没有真正开始处理,最终的处理是在调用deferredImportSelectorHandler.process()发起的。

class ConfigurationClassParser {
    private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates,...) {
        ...
        for (SourceClass candidate : importCandidates) {
            if (candidate.isAssignable(ImportSelector.class)) {
                ...
                // 判断当前的导入的类是不是DeferredImportSelector的子类
                if (selector instanceof DeferredImportSelector) {
                    // 是DeferredImportSelector的子类的话由deferredImportSelectorHandler去处理
                    this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
                } 
                ...
            }
            ...
        }
    }
    public void parse(Set<BeanDefinitionHolder> configCandidates) {
        ...
        // 这里发起真正处理Selector的逻辑
        this.deferredImportSelectorHandler.process();
    }
}

处理AutoConfigurationImportSelector的具体实现步骤如下:

3.4.1 读取项目路径下所有依赖中的spring.factories文件,加载需要自动配置的类

跟踪代码发现,执行process()后会执行handler.processGroupImports(),进而会执行grouping.getImports(),这个方法会用类加载器扫码程序classpath下所有jar中的spring.factories 文件,然后会得到所有用EnableAutoConfiguration属性标记的自动配置类

class ConfigurationClassParser {
    public Iterable<Group.Entry> getImports() {
        for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
            // 会扫描jar下的spring.factories中的自动配置类
            this.group.process(deferredImport.getConfigurationClass().getMetadata(),
                    deferredImport.getImportSelector());
        }
        return this.group.selectImports();
    }
}

// 具体扫描spring.factories的实现在AutoConfigurationImportSelector.process
public class AutoConfigurationImportSelector implements DeferredImportSelector,... {
    @Override
    public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
        // getAutoConfigurationEntry这个方法执行
        AutoConfigurationEntry autoConfigurationEntry = (deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
        ...
    }
    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        ...
        // 这里会调用SpringFactoriesLoader.loadFactoryNames读取spring.factories文件
        List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        ...
    }
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        // 读取key为EnableAutoConfiguration.class的所有自动配置类
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
    }
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }
}

3.4.2 针对所有扫描到的自动配置类再次执行配置类处理逻辑

上面已经从spring.factories中扫描到了所有配置类,紧接着程序同样会执行配置类处理逻辑,具体处理逻辑同第4节开始,同样也是处理这个配置类中的各种注解/方法, 目的就是为了将这些BeanDefinition加入到spring容器中以便于最终初始化。

3.4.3 将所有通过配置类处理过符合条件的BeanDefinition注册到spring容器中

执行完针对配置类的处理转换后,也就是执行完parse方法后,会再调用reader.loadBeanDefinitions用来把配置类处理后符合条件的BeanDefinition注册到spring容器。

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor {
    public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        ...
        do {
            ...
            // 处理配置类
            parser.parse(candidates);
            ...
            // parse转换处理完配置类后,然后会把所有处理过符合条件的注册到spring容器
            this.reader.loadBeanDefinitions(configClasses);
            ...
        } while (!candidates.isEmpty());
    }
}

3.4.5 Spring将所有单例BeanDefinition进行实例化、初始化了

这里spring会把通过各种方式注册的单例BeanDefinition,如通过注解@Component、@Service标记的Bean,以及我们本文探讨的重点通过SPI机制的导入的Bean进行最终的实例化/初始化。

public abstract class AbstractApplicationContext extends DefaultResourceLoader implements ConfigurableApplicationContext {
    public void refresh() throws BeansException, IllegalStateException {
        ...
        try {
            ...
            // 执行所有注册的BeanFactory后置处理器
            invokeBeanFactoryPostProcessors(beanFactory);
            ...
            // 实例化、初始化所有的单例Bean
            finishBeanFactoryInitialization(beanFactory);
        } catch (BeansException ex) {
            ...
        }
    }
}