大家好,这篇文章跟大家来聊下 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 里,供后续应用。
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。
次要办法为 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 对象,次要填充下图框中的四局部。
2)this.reader.loadBeanDefinitions(configClasses) 依据框中的四局部进行 BeanDefination 的注册。
在上述 processImports() 过程中会将 DeferredImportSelector 的实现类放在 deferredImportSelectorHandler 中以便提早到所有的解析工作实现后进行解决。deferredImportSelectorHandler 中就寄存了 AutoConfigurationImportSelector 类的实例。process() 办法里通过几步走会调用到 AutoConfigurationImportSelector#getAutoConfigurationEntry() 办法上获取到主动拆卸须要的类,而后进行与上述同样的 ConfigurationClass 解析封装工作。
代码档次太深,调用太简单,倡议本人断点调试源码跟一遍印象会更粗浅。
ApplicationContextInitializer 调用机会
咱们就以 SpringBoot 我的项目为例来看,在 SpringApplication 的构造函数中会进行 ApplicationContextInitializer 的初始化。
上图中的 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 环境中供给用应用。
SpringBoot 集成 Apollo
ApolloApplicationContextInitializer 实现 ApplicationContextInitializer 接口,并且在 spring.factories 文件中配置如下
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,二者配合实现配置核心的接入。
initialize 办法依据注入的 PropertySourceLocator 进行配置的定位获取,获取到的配置封装成 PropertySource 对象,而后增加到 Spring 环境 Environment 中。
Nacos、Zookeeper、Consul 都有提供相应 PropertySourceLocator 的实现
咱们来剖析下 Nacos 提供的 NacosPropertySourceLocator,locate 办法只提取了次要流程代码,能够看到 Nacos 启动会加载以下三种配置文件,也就是咱们在 bootstrap.yml 文件里配置的扩大配置 extension-configs、共享配置 shared-configs 以及利用本人的配置,加载到配置文件后会封装成 NacosPropertySource 放到 Spring 的 Environment 中。
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 在集成配置核心时的利用。篇幅无限,一些具体代码细节就没开展讲了,当前会出些文章针对某一个点进行具体解说。