我正在参加「启航计划」

我们好,这篇文章跟我们来聊下 Spring 中供应的常用扩展点、Spring SPI 机制、以及 SpringBoot 自动装置原理,要害介绍下 Spring 根据这些扩展点怎样跟配备中心(Apollo、Nacos、Zookeeper、Consul)等做集成。

写在前面

我们大多数 Java 程序员的日常作业底子都是在做业务开发,俗称 crudboy。

作为 crudboy 的你有没有这些烦恼呢?

  1. 随着业务的迭代,新功用的参加,代码变得越来越臃肿,可维护性越来越低,逐渐变成了屎山

  2. 遇到一些结构层的问题不知道怎样处理

  3. 面试被问到运用的结构、中间件原理、源码层东西,不知道怎样答复

  4. 写了 5 年代码了,感觉自己的技术没有理想的长进

假设你有上述这些烦恼,我想看优异结构的源码会是一个很好的提高方法。通过看源码,我们能学到业界大佬们优异的规划理念、编码风格、规划方式的运用、高效数据结构算法的运用、魔鬼细节的奇妙运用等等。这些东西都是助力我们成为一个优异工程师不可或缺的。

假设你打算要看源码了,优先引荐 Spring、Netty、Mybatis、JUC 包。

Spring 扩展

我们知道 Spring 供应了许多的扩展点,第三方结构整合 Spring 其实大多也都是根据这些扩展点来做的。所以娴熟的把握 Spring 扩展能让我们在阅读源码的时分能快速的找到进口,然后断点调试,一步步深化结构内核。

这些扩展包括但不限于以下接口:

BeanFactoryPostProcessor:在 Bean 实例化之前对 BeanDefinition 进行批改

BeanPostProcessor:在 Bean 初始化前后对 Bean 进行一些批改包装增强,比如回来代理方针

Aware:一个符号接口,结束该接口及子接口的类会收到 Spring 的告知回调,赋予某种 Spring 结构的才能,比如 ApplicationContextAware、EnvironmentAware 等

ApplicationContextInitializer:在上下文准备阶段,容器改写之前做一些初始化作业,比如我们常用的配备中心 client 底子都是继承该初始化器,在容器改写前将配备从长途拉到本地,然后封装成 PropertySource 放到 Environment 中供运用

ApplicationListener:Spring 工作机制,监听特定的运用工作(ApplicationEvent),观察者方式的一种结束

FactoryBean:用来自定义 Bean 的创建逻辑(Mybatis、Feign 等等)

ImportBeanDefinitionRegistrar:定义@EnableXXX 注解,在注解上 Import 了一个 ImportBeanDefinitionRegistrar,结束注册 BeanDefinition 到容器中

InitializingBean:在 Bean 初始化时会调用履行一些初始化逻辑

ApplicationRunner/CommandLineRunner:容器发动后回调,履行一些初始化作业

上述列出了几个比较常用的接口,可是 Spring 扩展远不于此,还有许多扩展接口我们可以自己去了解。

Spring SPI 机制

在讲接下来内容之前,我们先说下 Spring 中的 SPI 机制。Spring 中的 SPI 首要是运用 META-INF/spring.factories 文件来结束的,文件内容由多个 k = list(v) 的格局组成,比如:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.dtp.starter.adapter.dubbo.autoconfigure.ApacheDubboTpAutoConfiguration,\
  com.dtp.starter.adapter.dubbo.autoconfigure.AlibabaDubboTpAutoConfiguration
org.springframework.boot.env.EnvironmentPostProcessor=\
  com.dtp.starter.zookeeper.autoconfigure.ZkConfigEnvironmentProcessor

这些 spring.factories 文件可能是位于多个 jar 包中,Spring 容器发动时会通过 ClassLoader.getResources() 获取这些 spring.factories 文件的全途径。然后遍历途径以字节省的方式读取全部的 k = list(v) 封装到到一个 Map 中,key 为接口全约束类名,value 为全部结束类的全约束类名列表。

上述说的这些加载操作都封装在 SpringFactoriesLoader 类里。该类很简略,供应三个加载方法、一个实例化方法,还有一个 cache 特点,首次加载到的数据会保存在 cache 里,供后续运用。

这些不知道,甭说你了解 Spring

SpringBoot 中心要害

上面讲的 SPI 其实就是我们 SpringBoot 自动装置的中心。

何为自动装置?

自动装置对应的就是手动装置,在没 SpringBoot 之前,我们运用 Spring 就是用的手动装置方式。在运用某项第三方功用时,我们需求引入该功用依托的全部包,并检验保证这些引入包版别兼容。然后在 XML 文件里进行许多标签配备,十分繁琐。后来 Spring4 里引入了 JavaConfig 功用,运用 @Configuration + @Bean 来替代 XML 配备,虽然对开发来说是友好了许多,可是这些模板式配备代码还是很繁琐,会糟蹋许多时间做配备。Java 重可能也就是这个时分给人留的一种形象。

在该背景下呈现了 SpringBoot,SpringBoot 可以说是稳住了 Java 的地位。SpringBoot 供应了自动装置功用,自动装置简略来说就是将某种功用(如 web 相关、redis 相关、logging 相关等)打包在一起,统一管理依托包版别,并且约定好相关功用 Bean 的装置规则,运用者只需引入一个依托,通过少数注解或简略配备就可以运用第三方组件供应的功用了。

在 SpringBoot 中这类功用组件有一个好听的姓名叫做 starter。比如 spring-boot-starter-web、spring-boot-starter-data-redis、spring-boot-starter-logging 等。starter 里会通过 @Configuration + @Bean + @ConditionalOnXXX 等注解定义要注入 Spring 中的 Bean,然后在 spring.factories 文件中配备为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的结束,就可以结束自动装置了。

详细装置流程怎样样的呢?

其实也很简略,底子都是 Spring 中的常识,没啥新颖的。首要依托于@EnableAutoConfiguration 注解,该注解上会 Import 一个 AutoConfigurationImportSelector,看下继承联系,该类继承于 DeferredImportSelector。

这些不知道,甭说你了解 Spring

首要方法为 getAutoConfigurationEntry()

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
      // 1
      if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
      }
      AnnotationAttributes attributes = getAttributes(annotationMetadata);
      // 2
      List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
      configurations = removeDuplicates(configurations);
      // 3
      Set<String> exclusions = getExclusions(annotationMetadata, attributes);
      checkExcludedClasses(configurations, exclusions);
      configurations.removeAll(exclusions);
      // 4
      configurations = getConfigurationClassFilter().filter(configurations);
      fireAutoConfigurationImportEvents(configurations, exclusions);
      return new AutoConfigurationEntry(configurations, exclusions);
}

方法解读

  1. 通过 spring.boot.enableautoconfiguration 配备项判别是否启用自动装置,默认为 true

  2. 运用上述说的 SpringFactoriesLoader.loadFactoryNames() 加载全部 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的结束类的全约束类名,凭借 HashSet 进行去重

  3. 获取 @EnableAutoConfiguration 注解上配备的要 exclude 的类,然后排除这些特定类

  4. 通过 @ConditionalOnXXX 进行过滤,满足条件的类才会留下,封装到 AutoConfigurationEntry 里回来

那 getAutoConfigurationEntry() 方法在哪儿调用呢?

public void refresh() throws BeansException, IllegalStateException {
    // Allows post-processing of the bean factory in context subclasses.
    postProcessBeanFactory(beanFactory);
    StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
    // Invoke factory processors registered as beans in the context.
    invokeBeanFactoryPostProcessors(beanFactory);
    // Register bean processors that intercept bean creation.
    registerBeanPostProcessors(beanFactory);
    beanPostProcess.end();
    // Initialize message source for this context.
    initMessageSource();
    // Initialize event multicaster for this context.
    initApplicationEventMulticaster();
    // Initialize other special beans in specific context subclasses.
    onRefresh();
    // Check for listener beans and register them.
    registerListeners();
    // Instantiate all remaining (non-lazy-init) singletons.
    finishBeanFactoryInitialization(beanFactory);
    // Last step: publish corresponding event.
    finishRefresh();
}

以上是 Spring 容器改写时的几个要害过程,在过程二 invokeBeanFactoryPostProcessors() 中会调用全部现已注册的 BeanFactoryPostProcessor 进行处理。此处调用也是有次序的,优先会调用全部 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(),BeanDefinitionRegistryPostProcessor 是一个特别的 BeanFactoryPostProcessor,然后再调用全部 BeanFactoryPostProcessor#postProcessBeanFactory()。

ConfigurationClassPostProcessor 是 BeanDefinitionRegistryPostProcessor 的一个结束类,该类首要用来处理 @Configuration 注解标示的类。我们用 @Configuration 标示的类会被 ConfigurationClassParser 解析包装成 ConfigurationClass 方针,然后再调用 ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForConfigurationClass() 进行 BeanDefination 的注册。

其间 ConfigurationClassParser 解析时会递归处理源配备类上的注解(@PropertySource、@ComponentScan、@Import、@ImportResource)、 @Bean 标示的方法、接口上的 default 方法,进行 ConfigurationClass 类的补全填充,一起假设该配备类有父类,相同会递归进行处理。详细代码请看 ConfigurationClassParser#doProcessConfigurationClass() 方法

protected final SourceClass doProcessConfigurationClass(
			ConfigurationClass configClass, SourceClass sourceClass, Predicate<String> filter)
			throws IOException {
    // Process any @PropertySource annotations
    // Process any @ComponentScan annotations
    // Process any @Import annotations
    processImports(configClass, sourceClass, getImports(sourceClass), filter, true);
    // Process any @ImportResource annotations
    // Process individual @Bean methods
    Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
    for (MethodMetadata methodMetadata : beanMethods) {
             configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
    }
    // Process default methods on interfaces
    processInterfaces(configClass, sourceClass);
    // Process superclass, if any
    if (sourceClass.getMetadata().hasSuperClass()) {
             String superclass = sourceClass.getMetadata().getSuperClassName();
             if (superclass != null && !superclass.startsWith("java") &&
                            !this.knownSuperclasses.containsKey(superclass)) {
                      this.knownSuperclasses.put(superclass, configClass);
                    // Superclass found, return its annotation metadata and recurse
                      return sourceClass.getSuperClass();
            }
    }
    // No superclass -> processing is complete
    return null;
}

1)parser.parse(candidates) 解析得到完好的 ConfigurationClass 方针,首要填充下图框中的四部分。

这些不知道,甭说你了解 Spring

这些不知道,甭说你了解 Spring

2)this.reader.loadBeanDefinitions(configClasses) 根据框中的四部分进行 BeanDefination 的注册。

这些不知道,甭说你了解 Spring

在上述 processImports() 过程中会将 DeferredImportSelector 的结束类放在 deferredImportSelectorHandler 中以便延迟到全部的解析作业结束后进行处理。deferredImportSelectorHandler 中就存放了 AutoConfigurationImportSelector 类的实例。process() 方法里通过几步走会调用到 AutoConfigurationImportSelector#getAutoConfigurationEntry() 方法上获取到自动装置需求的类,然后进行与上述相同的 ConfigurationClass 解析封装作业。

这些不知道,甭说你了解 Spring

这些不知道,甭说你了解 Spring

代码层次太深,调用太杂乱,主张自己断点调试源码跟一遍形象会更深化。

ApplicationContextInitializer 调用机遇

我们就以 SpringBoot 项目为例来看,在 SpringApplication 的构造函数中会进行 ApplicationContextInitializer 的初始化。

这些不知道,甭说你了解 Spring

上图中的 getSpringFactoriesInstances 方法内部其实就是调用 SpringFactoriesLoader.loadFactoryNames 获取全部 ApplicationContextInitializer 接口的结束类,然后反射创建方针,并对这些方针进行排序(结束了 Ordered 接口或者加了 @Order 注解)。

 private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
      ClassLoader classLoader = getClassLoader();
      // Use names and ensure unique to protect against duplicates
      Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
      List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
      AnnotationAwareOrderComparator.sort(instances);
      return instances;
}

至此,项目中全部 ApplicationContextInitializer 的结束现已加载并且创建好了。在 prepareContext 阶段会进行全部已注册的 ApplicationContextInitializer#initialize() 方法的调用。在此之前prepareEnvironment 阶段现已准备好了环境信息,此处接入配备中心就可以拉到长途配备信息然后填充到 Spring 环境中供运用运用。

这些不知道,甭说你了解 Spring

SpringBoot 集成 Apollo

ApolloApplicationContextInitializer 结束 ApplicationContextInitializer 接口,并且在 spring.factories 文件中配备如下

这些不知道,甭说你了解 Spring

org.springframework.context.ApplicationContextInitializer=\
com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer

initialize() 方法中会根据 apollo.bootstrap.namespaces 配备的 namespaces 进行配备的拉去,拉去到的配备会封装成 ConfigPropertySource 添加到 Spring 环境 ConfigurableEnvironment 中。详细的拉去流程就不翻开讲了,感兴趣的可以自己去阅读源码了解。

SpringCloud 集成 Nacos、Zk、Consul

在 SpringCloud 场景下,SpringCloud 标准中供应了 PropertySourceBootstrapConfiguration 继承 ApplicationContextInitializer,别的还供应了个 PropertySourceLocator,二者合作结束配备中心的接入。

这些不知道,甭说你了解 Spring

initialize 方法根据注入的 PropertySourceLocator 进行配备的定位获取,获取到的配备封装成 PropertySource 方针,然后添加到 Spring 环境 Environment 中。

这些不知道,甭说你了解 Spring

Nacos、Zookeeper、Consul 都有供应相应 PropertySourceLocator 的结束

这些不知道,甭说你了解 Spring

我们来剖析下 Nacos 供应的 NacosPropertySourceLocator,locate 方法只提取了首要流程代码,可以看到 Nacos 发动会加载以下三种配备文件,也就是我们在 bootstrap.yml 文件里配备的扩展配备 extension-configs、同享配备 shared-configs 以及运用自己的配备,加载到配备文件后会封装成 NacosPropertySource 放到 Spring 的 Environment 中。

这些不知道,甭说你了解 Spring

public PropertySource<?> locate(Environment env) {
     loadSharedConfiguration(composite);
     loadExtConfiguration(composite);
     loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);
     return composite;
}

loadApplicationConfiguration 加载运用配备时,一起会加载以下三种配备,分别是

  1. 不带扩展名后缀,application

  2. 带扩展名后缀,application.yml

  3. 带环境,带扩展名后缀,application-prod.yml

并且从上到下,优先级依次增高

private void loadApplicationConfiguration(
			CompositePropertySource compositePropertySource, String dataIdPrefix,
			NacosConfigProperties properties, Environment environment) {
    String fileExtension = properties.getFileExtension();
    String nacosGroup = properties.getGroup();
    // load directly once by default
    loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
                    fileExtension, true);
    // load with suffix, which have a higher priority than the default
    loadNacosDataIfPresent(compositePropertySource,
                    dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
    // Loaded with profile, which have a higher priority than the suffix
    for (String profile : environment.getActiveProfiles()) {
            String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
            loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
                            fileExtension, true);
    }
}

加载过程中,通过 namespace, dataId, group 仅有定位一个配备文件

  1. 首要获取本地缓存的配备,假设有直接回来

  2. 假设过程1从本地没找到相应配备文件,开始从远处拉去,Nacos 2.0 以上版别运用 Grpc 协议进行长途通讯,1.0 及以下运用 Http 协议进行长途通讯

  3. 对拉去到的字符串进行解析,封装成 NacosPropertySource 回来

详细细节就不翻开讲了,可以自己看源码了解

Zookeeper、Consul 的接入也是十分简略,可以自己剖析一遍。假设我们有自研的配备中心,需求在 SpringCloud 环境下运用,可以根据 SpringCloud 供应的这些扩展参看以上几种结束快速的写个 starter 进行接入。

总结

本篇文章首要讲了下 Spring SPI 机制、SpringBoot 自动装置原理,以及扩展点 ApplicationContextInitializer 在集成配备中心时的运用。篇幅有限,一些详细代码细节就没翻开讲了,今后会出些文章针对某一个点进行详细讲解。

个人开源项目

DynamicTp 是一个根据配备中心结束的轻量级动态线程池管理工具,首要功用可以总结为动态调参、告知报警、工作监控、三方包线程池管理等几大类。

这些不知道,甭说你了解 Spring

现在累计 2k star,代码高雅,运用了许多规划方式,假设你觉得看这些大型结构源码吃力,那么可以检验从 DynamicTp 源码下手,欢迎我们了解试用

官网:dynamictp.cn

gitee地址:gitee.com/dromara/dyn…

github地址:github.com/dromara/dyn…