百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

这些不知道,别说你熟悉 Spring :spring

lipiwang 2024-10-31 15:24 12 浏览 0 评论

大家好,这篇文章跟大家来聊下 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 里,供后续使用。

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);
}
复制代码

方法解读

  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 对象,主要填充下图框中的四部分。

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 在集成配置中心时的应用。篇幅有限,一些具体代码细节就没展开讲了,以后会出些文章针对某一个点进行详细讲解。

个人开源项目

DynamicTp 是一个基于配置中心实现的轻量级动态线程池管理工具,主要功能可以总结为动态调参、通知报警、运行监控、三方包线程池管理等几大类。

目前累计 2k star,代码优雅,使用了大量设计模式,如果你觉得看这些大型框架源码费劲,那么可以尝试从 DynamicTp 源码入手,欢迎大家了解试用

官网:dynamictp.cn

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

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


作者:CodeFox
链接:https://juejin.cn/post/7152369961120301069
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐

想减少Windows 11内存占用?请取消固定Teams

如果你想要提高Windows11系统的运行速度,那么可以禁用某些默认启用的功能和设置。如果你的Windows11是安装在已经停止支持的设备或者内存容量不高的旧设备,那么应该立即限制或禁用固...

Windows查看端口占用、查看PID对应的进程、并终止进程

Windows下:查看端口占用netstat-ano|findstr"端口号"获取到pid查看PID对应的进程tasklist|findstr"进程ID"...

计算机组成原理(36): 分时之一——进程

建立一个虚拟机VM目标:给每个程序一个自己的虚拟机“VirtualMachine”,程序并不知道其他的虚拟机。1.1进程(Process)为了捕获正在运行的程序,我们创建一个称为“进程(Proce...

window系统如何停止端口被占用的进程(高手版)

如上图1,作为开发人员是不是经常遇到这个问题?(Webserverfailedtostart.Port9527wasalreadyinuse.)当然,如果在你知道确实有某个进程正占...

电脑的文件无法删除咋回事?你需要这款神兵利器

很多朋友用电脑的时候,都遇到过文件无法删除的情况。这往往是由于文件被某个软件、进程所调用所引发的——在Windows中,某个文件如果被使用,这个文件可能就没法进行删除、重命名之类的操作了。想要进一步操...

Windows日志分析(windows 日志文件)

1.Windows日志文件简介1.1Windows日志核心分类1.系统日志系统日志包含由Windows系统组件记录的事件,记录系统进程和设备驱动程序的活动。由它审核的系统事件包括启动失败的设备驱动程...

电脑软件崩溃、闪退不用慌!DJS Tech 教你几招轻松解决

当你正全神贯注用电脑处理重要文件、沉浸在精彩的游戏世界,或是观看喜欢的视频时,软件突然崩溃、闪退,那一刻的烦躁简直难以言喻。别着急,DJSTech作为深耕计算机领域多年的专业团队,为你带来一系列超...

微软Win11推进淘汰控制面板,时间服务器配置迁移至设置应用

IT之家5月29日消息,科技媒体Winaero昨日(5月28日)发布博文,报道称微软在Windows11系统中,继续推进“淘汰控制面板”进程,配置时间服务器地址选项迁移到设置应...

微软 PowerToys更新,可帮你找出 Win11上哪些进程正在占用该文件

IT之家11月3日消息,微软针对Windows11和Windows10的PowerToys已经更新到了最新的0.64.0版本,并上线了一个名为“文件锁匠FileLock...

Windows基础操作 认识任务管理器(windows任务管理器的使用)

Windows基础操作:认识任务管理器任务管理器(TaskManager)是Windows系统中一个功能强大的实用工具,它为用户提供了实时监控系统资源、管理正在运行的程序和服务的能力。掌握任务管理器...

windows——netstat过滤(终止进程)

windows——netstat过滤(终止进程)在Windows操作系统中,使用netstat命令可以查看网络连接的状态。要过滤特定协议或端口的连接,可以使用以下命令:查看所有连接:netstat-...

只要这么做 Windows Defender与第三方就能和平共存啦

无论大家是否喜欢WindowsDefender,伴随着Windows10的不断升级,它已经成为系统的底层必备组件之一。虽然我们有各种各样的方法去关闭它,换用顺手的第三方,但只要更新打补丁,噩梦就来...

Win10如何彻底关闭wsappx进程(win10 wsappx怎么关闭)

win10如何彻底关闭wsappx进程?wsappx进程是什么?wsappx进程是Windows10系统的一部分,这个进程是WindowsStore和微软通用应用程序平台(UWP)的依赖进程。...

Windows环境黑客入侵应急与排查(黑客入侵电脑原理)

1文件分析1.1临时目录排查黑客往往可能将病毒放在临时目录(tmp/temp),或者将病毒相关文件释放到临时目录,因此需要检查临时目录是否存在异常文件。假设系统盘在C盘,则通常情况下的临时目录如下...

Windows 11 24H2 KB5044384出现大面积安装失败、任务管理器0进程等问题

Windows11KB5044384更新由于出现大量错误而无法在Windows1124H2上安装、其中包括一个奇怪的错误,即由于0x800f0922、0x800736b3和0x8...

取消回复欢迎 发表评论: