这些不知道,别说你熟悉 Spring
这篇文章跟大家来聊下 Spring 中提供的常用扩展点、Spring SPI 机制、以及 SpringBoot 自动装配原理,重点介绍下 Spring 基于这些扩展点怎么跟配置中心(Apollo、Nacos、Zookeeper、Consul)等做集成。
写在前面
我们大多数 Java 程序员的日常工作基本都是在做业务开发,俗称 crudboy。
作为 crudboy 的你有没有这些烦恼呢?
-
随着业务的迭代,新功能的加入,代码变得越来越臃肿,可维护性越来越低,慢慢变成了屎山
-
遇到一些框架层的问题不知道怎么解决
-
面试被问到使用的框架、中间件原理、源码层东西,不知道怎么回答
-
写了 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 里,供后续使用。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142867976/28070583-a520ee0a2dfbee68.png)
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。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868107/28070583-816beb2c1e848a3e.png)
主要方法为 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); }
方法解读
-
通过 spring.boot.enableautoconfiguration 配置项判断是否启用自动装配,默认为 true
-
使用上述说的 SpringFactoriesLoader.loadFactoryNames() 加载所有 org.springframework.boot.autoconfigure.EnableAutoConfiguration 的实现类的全限定类名,借助 HashSet 进行去重
-
获取 @EnableAutoConfiguration 注解上配置的要 exclude 的类,然后排除这些特定类
-
通过 @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 对象,主要填充下图框中的四部分。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868191/28070583-9b84c2bcd62d5fd1.png)
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868160/28070583-e4a5aae78dd8c001.png)
2)this.reader.loadBeanDefinitions(configClasses) 根据框中的四部分进行 BeanDefination 的注册。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868174/28070583-577db61d3ca4ccc6.png)
在上述 processImports() 过程中会将 DeferredImportSelector 的实现类放在 deferredImportSelectorHandler 中以便延迟到所有的解析工作完成后进行处理。deferredImportSelectorHandler 中就存放了 AutoConfigurationImportSelector 类的实例。process() 方法里经过几步走会调用到 AutoConfigurationImportSelector#getAutoConfigurationEntry() 方法上获取到自动装配需要的类,然后进行与上述同样的 ConfigurationClass 解析封装工作。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868201/28070583-3241f378bdb1d582.png)
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868226/28070583-e03f10fda951b5e7.png)
代码层次太深,调用太复杂,建议自己断点调试源码跟一遍印象会更深刻。
ApplicationContextInitializer 调用时机
我们就以 SpringBoot 项目为例来看,在 SpringApplication 的构造函数中会进行 ApplicationContextInitializer 的初始化。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868204/28070583-45cdfc79c9978021.png)
上图中的 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 环境中供应用使用。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142867985/28070583-785bc50548095f75.png)
SpringBoot 集成 Apollo
ApolloApplicationContextInitializer 实现 ApplicationContextInitializer 接口,并且在 spring.factories 文件中配置如下
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868178/28070583-1518c79c9a9f6b21.png)
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,二者配合完成配置中心的接入。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142867944/28070583-5e11213c45a9b45a.png)
initialize 方法根据注入的 PropertySourceLocator 进行配置的定位获取,获取到的配置封装成 PropertySource 对象,然后添加到 Spring 环境 Environment 中。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142867986/28070583-f92e831b423101c2.png)
Nacos、Zookeeper、Consul 都有提供相应 PropertySourceLocator 的实现
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142867993/28070583-261c08f1282435d8.png)
我们来分析下 Nacos 提供的 NacosPropertySourceLocator,locate 方法只提取了主要流程代码,可以看到 Nacos 启动会加载以下三种配置文件,也就是我们在 bootstrap.yml 文件里配置的扩展配置 extension-configs、共享配置 shared-configs 以及应用自己的配置,加载到配置文件后会封装成 NacosPropertySource 放到 Spring 的 Environment 中。
![](https://uploadfiles.nowcoder.com/files/20221019/882557124_1666142868230/28070583-cf33cb1acc7a5dec.png)
public PropertySource<?> locate(Environment env) { loadSharedConfiguration(composite); loadExtConfiguration(composite); loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env); return composite; }
loadApplicationConfiguration 加载应用配置时,同时会加载以下三种配置,分别是
- 不带扩展名后缀,application
- 带扩展名后缀,application.yml
- 带环境,带扩展名后缀,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从本地没找到相应配置文件,开始从远处拉去,Nacos 2.0 以上版本使用 Grpc 协议进行远程通信,1.0 及以下使用 Http 协议进行远程通信
- 对拉去到的字符串进行解析,封装成 NacosPropertySource 返回
具体细节就不展开讲了,可以自己看源码了解
Zookeeper、Consul 的接入也是非常简单,可以自己分析一遍。如果我们有自研的配置中心,需要在 SpringCloud 环境下使用,可以根据 SpringCloud 提供的这些扩展参考以上几种实现快速的写个 starter 进行接入。
总结
本篇文章主要讲了下 Spring SPI 机制、SpringBoot 自动装配原理,以及扩展点 ApplicationContextInitializer 在集成配置中心时的应用。篇幅有限,一些具体代码细节就没展开讲了
#Java##Spring##程序员#