Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说
spring到底是什么_Spring Web Flow,希望能够帮助你!!!。
大家好,这篇文章跟大家来聊下 Spring 中提供的常用扩展点、Spring SPI 机制、以及 SpringBoot 自动装配原理,重点介绍下 Spring 基于这些扩展点怎么跟配置中心(Apollo、Nacos、Zookeeper、Consul)等做集成。
我们大多数 Java 程序员的日常工作基本都是在做业务开发,俗称 crudboy。
作为 crudboy 的你有没有这些烦恼呢?
如果你有上述这些烦恼,我想看优秀框架的源码会是一个很好的提升方式。通过看源码,我们能学到业界大佬们优秀的设计理念、编码风格、设计模式的使用、高效数据结构算法的使用、魔鬼细节的巧妙应用等等。这些东西都是助力我们成为一个优秀工程师不可或缺的。
如果你打算要看源码了,优先推荐 Spring、Netty、Mybatis、JUC 包。
我们知道 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 主要是利用 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 里,供后续使用。
上面讲的 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); } 复制代码
方法解读
那 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 解析封装工作。
代码层次太深,调用太复杂,建议自己断点调试源码跟一遍印象会更深刻。
我们就以 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 环境中供应用使用。
ApolloApplicationContextInitializer 实现 ApplicationContextInitializer 接口,并且在 spring.factories 文件中配置如下
org.springframework.context.ApplicationContextInitializer=\ com.ctrip.framework.apollo.spring.boot.ApolloApplicationContextInitializer 复制代码
initialize() 方法中会根据 apollo.bootstrap.namespaces 配置的 namespaces 进行配置的拉去,拉去到的配置会封装成 ConfigPropertySource 添加到 Spring 环境 ConfigurableEnvironment 中。具体的拉去流程就不展开讲了,感兴趣的可以自己去阅读源码了解。
在 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的设计
那么,在Spring中又是由哪些"角色"构成的呢?
1、Bean: Spring作为一个IoC容器,最重要的当然是Bean咯
2、BeanFactory: 生产与管理Bean的工厂
3、BeanDefinition: Bean的定义,也就是我们方案中的Class,Spring对它进行了封装
4、BeanDefinitionRegistry: 类似于Bean与BeanFactory的关系,BeanDefinitionRegistry用于管理BeanDefinition
5、BeanDefinitionRegistryPostProcessor: 用于在解析配置类时的处理器,类似于我们方案中的ClassProcessor
6、BeanFactoryPostProcessor: BeanDefinitionRegistryPostProcessor父类,让我们可以再解析配置类之后进行后置处理
7、BeanPostProcessor: Bean的后置处理器,用于在生产Bean的过程中进行一些处理,比如依赖注入,类似我们的AutowiredAnnotationBeanProcessor
8、ApplicationContext: 如果说以上的角色都是在工厂中生产Bean的工人,那么ApplicationContext就是我们Spring的门面,ApplicationContext与BeanFactory是一种组合的关系,所以它完全扩展了BeanFactory的功能,并在其基础上添加了更多特定于企业的功能,比如我们熟知的ApplicationListener(事件监听器)
以上说的类似其实有一些本末倒置了,因为实际上应该是我们方案中的实现类似于Spring中的实现,这样说只是为了让大家更好的理解
我们在经历了自己方案的设计与优化后,对这些角色其实是非常容易理解的
接下来,我们就一个一个的详细了解一下
BeanFactory是Spring中的一个顶级接口,它定义了获取Bean的方式,Spring中还有另一个接口叫SingletonBeanRegistry,它定义的是操作单例Bean的方式,这里我将这两个放在一起进行介绍,因为它们大体相同,SingletonBeanRegistry的注释上也写了可以与BeanFactory接口一起实现,方便统一管理。
1、ListableBeanFactory:接口,定义了获取Bean/BeanDefinition列表相关的方法,如getBeansOfType(Class type)
2、AutowireCapableBeanFactory:接口,定义了Bean生命周期相关的方法,如创建bean, 依赖注入,初始化
3、AbstractBeanFactory:抽象类,基本上实现了所有有关Bean操作的方法,定义了Bean生命周期相关的抽象方法
4、AbstractAutowireCapableBeanFactory:抽象类,继承了AbstractBeanFactory,实现了Bean生命周期相关的内容,虽然是个抽象类,但它没有抽象方法
5、DefaultListableBeanFactory:继承与实现以上所有类和接口,是为Spring中最底层的BeanFactory, 自身实现了ListableBeanFactory接口
6、ApplicationContext:也是一个接口,我们会在下面有专门对它的介绍
1、DefaultSingletonBeanRegistry: 定义了Bean的缓存池,类似于我们的BeanMap,实现了有关单例的操作,比如getSingleton(面试常问的三级缓存就在这里)
2、FactoryBeanRegistrySupport:提供了对FactoryBean的支持,比如从FactoryBean中获取Bean
BeanDefinition其实也是个接口(想不到吧),这里定义了许多和类信息相关的操作方法,方便在生产Bean的时候直接使用,比如getBeanClassName
它的大概结构如下(这里举例RootBeanDefinition子类):
里面的各种属性想必大家也绝不陌生
同样的,它也有许多实现类:
1、AnnotatedGenericBeanDefinition:解析配置类与解析Import注解带入的类时,就会使用它进行封装
2、ScannedGenericBeanDefinition:封装通过@ComponentScan扫描包所得到的类信息
3、ConfigurationClassBeanDefinition:封装通过@Bean注解所得到的类信息
4、RootBeanDefinition:ConfigurationClassBeanDefinition父类,一般在Spring内部使用,将其他的BeanDefition转化成该类
定义了与BeanDefiniton相关的操作,如registerBeanDefinition,getBeanDefinition,在BeanFactory中,实现类就是DefaultListableBeanFactory
插话:讲到这里,有没有发现Spring的命名极其规范,Spring团队曾言Spring中的类名都是反复推敲才确认的,真是名副其实呀,所以看Spring源码真的是一件很舒服的事情,看看类名方法名就能猜出它们的功能了。
该接口只定义了一个功能:处理BeanDefinitonRegistry,也就是解析配置类中的Import、Component、ComponentScan等注解进行相应的处理,处理完毕后将这些类注册成对应的BeanDefinition
在Spring内部中,只有一个实现:ConfigurationClassPostProcessor
所谓BeanFactory的后置处理器,它定义了在解析完配置类后可以调用的处理逻辑,类似于一个插槽,如果我们想在配置类解析完后做点什么,就可以实现该接口。
在Spring内部中,同样只有ConfigurationClassPostProcessor实现了它:用于专门处理加了Configuration注解的类
这里串场一个小问题,如知以下代码:
@Configuraiton public class MyConfiguration{ @Bean public Car car(){ return new Car(wheel()); } @Bean public Wheel wheel(){ return new Wheel(); } } 复制代码
问:Wheel对象在Spring启动时,被new了几次?为什么?
江湖翻译:Bean的后置处理器
该后置处理器贯穿了Bean的生命周期整个过程,在Bean的创建过程中,一共被调用了9次,至于哪9次我们下次再来探究,以下介绍它的实现类以及作用
1、AutowiredAnnotationBeanPostProcessor:用于推断构造器进行实例化,以及处理Autowired和Value注解
2、CommonAnnotationBeanPostProcessor:处理Java规范中的注解,如Resource、PostConstruct
3、ApplicationListenerDetector: 在Bean的初始化后使用,将实现了ApplicationListener接口的bean添加到事件监听器列表中
4、ApplicationContextAwareProcessor:用于回调实现了Aware接口的Bean
5、ImportAwareBeanPostProcessor: 用于回调实现了ImportAware接口的Bean
ApplicationContext作为Spring的核心,以门面模式隔离了BeanFactory,以模板方法模式定义了Spring启动流程的骨架,又以策略模式调用了各式各样的Processor......实在是错综复杂又精妙绝伦!
它的实现类如下:
1、ConfigurableApplicationContext:接口,定义了配置与生命周期相关操作,如refresh
2、AbstractApplicationContext: 抽象类,实现了refresh方法,refresh方法作为Spring核心中的核心,可以说整个Spring皆在refresh之中,所有子类都通过refresh方法启动,在调用该方法之后,将实例化所有单例
3、AnnotationConfigApplicationContext: 在启动时使用相关的注解读取器与扫描器,往Spring容器中注册需要用的处理器,而后在refresh方法在被主流程调用即可
4、AnnotationConfigWebApplicationContext:实现loadBeanDefinitions方法,以期在refresh流程中被调用,从而加载BeanDefintion
5、ClassPathXmlApplicationContext: 同上
从子类的情况可以看出,子类的不同之处在于如何加载BeanDefiniton, AnnotationConfigApplicationContext是通过配置类处理器(ConfigurationClassPostProcessor)加载的,而AnnotationConfigWebApplicationContext与ClassPathXmlApplicationContext则是通过自己实现loadBeanDefinitions方法,其他流程则完全一致
以上,我们已经清楚了Spring中的主要角色以及作用,现在我们尝试把它们组合起来,构建一个Spring的启动流程
同样以我们常用的AnnotationConfigApplicationContext为例
图中只画出了Spring中的部分大概流程,详细内容我们会在后面的章节展开
所谓万事开头难,本文初衷就是能让大家以由浅入深的方式认识Spring,初步建立Spring的认知体系,明白Spring的内部架构,对Spring的认知不再浮于表面。
现在头已经开了,相信后面内容的学习也将水到渠来。
本篇文章既讲是Spring的架构设计,也希望能成为我们以后复习Spring整体内容时使用的手册。
最后,看完文章之后,相信对以下面试常问的问题回答起来也是轻而易举
1、什么是BeanDefinition?
2、BeanFactory与ApplicationContext的关系?
3、后置处理器的分类与作用?
4、Spring的主要流程是怎么样的?
如果小伙伴觉得没办法很好回答上来的话就再看看文章,或者在评论区留下自己的见解吧
本篇文章主要讲了下 Spring SPI 机制、SpringBoot 自动装配原理,以及扩展点 ApplicationContextInitializer 在集成配置中心时的应用。篇幅有限,一些具体代码细节就没展开讲了,以后会出些文章针对某一个点进行详细讲解。
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
上一篇
已是最后文章
下一篇
已是最新文章