SpringBoot编程思想之不求甚解

在 Java 领域,SpringBoot 是目前最流行的微服务框架,将使用门槛大幅度的降低,达到开箱即用;那么这也必然使 SB 成为了一个黑盒,如果需要深度定制或者了解内部原理变得有点复杂,希望通过阅读本系列书籍来解开这个黑盒。
最重要的部分就是 SB 的主要特性部分,自动装配和自动配置方面的理解;从 SB2.x 开始架构逐步稳定,现在可以尝试大规模使用了。
微服务架构作为一种细粒度的 SOA,无论那种表达方式,只不过是名词之争,架构设计的好坏不在于理论和技术,而在于实施者对业务的理解和专业水平。
想了一下,直接写到这一篇里了,不再分篇,超长文预警。

初探SpringBoot

对于简单的使用没什么可说的,SB 是出了名的简单,这是对于使用来说,也有句话越简单的东西越复杂,关于基本的使用参考官方文档和 API 文档基本就可以了,我之前也写过两篇使用内容为主的:SpringBoot初尝试SpringBoot进阶
上面的两篇可能与本篇有重叠,但是总应该是互补的,侧重点不同。还可以参考我的一篇笔记深入SB
还有一篇未完成的 Spring核心编程概述 待填坑。

特性

官方列举了六大特性:

  1. 创建独立的 Spring 应用
  2. 嵌入 Tomcat, Jetty 或者 Undertow 等 Web 容器(不需要部署 War 文件)
  3. 提供固化的 starter 依赖,简化构建配置
  4. 当条件满足时,自动装配 Spring 或者第三方类库
  5. 提供运维特性,例如指标信息、健康检查、外部化配置
  6. 绝无代码生成,不需要 XML 配置

独立的Spring应用

SpringBoot 除了构建我们熟悉的 Web 应用,在非 Web 应用上也是非常好用的,例如服务提供、调度任务、消息处理等场景。
并且,在 Web 应用方面,除了传统的 Servlet 容器,2.x 版本实现了 Reactive Web 容器,也就是 Spring5.x 的 WebFlux。

按照一般的思路用 SB 构建的应用我们应该叫 SB 应用,然而实际上是 Spring 应用,因为在 SB 的构建过程中,主要的驱动核心是靠 SpringApplication 完成的,所以可以称为 Spring 应用。

Spring Web 时代是利用 ServletContext 生命周期构建 Web Root Spring 应用上下文(ContextLoaderListener);
结合 Servlet 生命周期构建 DispatcherServlet 的 Spring 应用上下文。

所以,在 Spring Web 时代都是被动的回调执行,没有完整的应用主导权,这在使用了嵌入式容器后才改善。

可执行JAR

用 SB 构建的应用我们可以非常方便的使用 java -jar 来运行,但是默认的 Maven 打包出来是不支持的,这是因为使用了一个 Maven 插件:spring-boot-maven-plugin,并且一般情况不需要指定版本,因为父工程已经配置好了。
可执行 JAR 又被称为 fat jars。
PS:开发环境使用 mvn spring-boot:run 命令运行也是同样效果。

我们知道 jar 文件其实就是一个 zip 包,解压可执行 jar 文件会得到一些目录和文件:

  • BOOT-INF/classes
    存放应用编译后的 class 文件
  • BOOT-INF/lib
    存放依赖的 jar 包
  • META-INF/
    存放相关元信息,例如 MANIFEST.MF
  • org/
    存放 SB 相关 Class 文件

接下来就是分析为什么这个 jar 可以直接运行了,依赖于 spring-boot-loader;
根据 Java 规范,如果使用 -jar 运行,引导配置必须放在 MANIFEST.MF 文件中,它必须在 META-INF 目录下;所以你查看这个文件基本就能了解了。

1
2
3
4
5
6
Main-Class: org.springframework.boot.loader.JarLauncher  # Main 函数
Start-Class: com.example.demo.DemoApplication # 启动类
Spring-Boot-Classes: BOOT-INF/classes/ # 编译之后的 class 文件目录
Spring-Boot-Lib: BOOT-INF/lib/ # 当前工程依赖的 jar 包目录
Build-Jdk-Spec: 1.8 # 指定的 JDK 版本
Spring-Boot-Version: 2.1.6.RELEASE # SpringBoot 版本

这里主要看 JarLauncher 这个类,相应的,打出来的可执行 war 包就是 WarLauncher;知道了核心类,那么我们就又了另一种启动方式,把 jar 包解压,在解压后的目录可以直接执行:

1
java org.springframework.boot.loader.JarLauncher

这样也是可以正常启动 Spring 应用,可以得出 JarLauncher 装载执行了我们的启动类,如果你尝试直接运行启动类,很遗憾是不行的,原因是 lib 依赖库的原因。
具体的原因可以查看 JarLauncher 的源码,很简单就不说了(深层次的例如对于 URL 协议的处理就很难理解),里面进行了处理所以才能 jar 或者解压后运行。
一句话概括就是:JarLauncher 实际上是同进程内调用 Start-Class 的 main 方法,并在启动前准备好 CP(classpath),war 包亦是如此,不过目录会有所差别,毕竟要兼容 servlet 容器(仅关注 WEB-INF/classes、lib)运行,根据容器的特性就可以做到忽略非规范目录的冲突的包,当然 WebFluex 不行。

嵌入式Web容器

从 2.x 开始,Netty 也作为容器加入,不同容器无法共存,Undertow 作为 JBoss 社区推出的新一代兼容 Servlet3.1+ 的容器,用的还是不太广泛。
目前最新版本支持 HTTP/2 和 servlet4.0,核心 jar 体积只有 2.2MB,4.0 规范对应 Tomcat 和 Jetty 9.x,Undertow 2.x。
因为 SpringFlux 基于 Reactor 框架实现,因此 Netty 容器属于 Reactor 和 Netty 的整合实现;
其他的三种容器也能作为 Reactive Web Server,默认是 Netty,毕竟 Servlet3.1+ 也支持 Reactive 异步非阻塞的特性
另外需要说明的是,嵌入式容器并不是 SB 首创,各个容器早就支持,SB 不过是将整合做到了极致。

Apache Tomcat 官方很早就提供了相应的可执行 jar 的构建插件,但是运行时还是通过解压到临时目录的方式实现;
对于 SB,它使用了零压缩模式,所以可以不解压直接读取运行,也正是这个所以重写 JAR 协议的 URL 实现。

同样,Jetty 天然的可插拔 API 对嵌入式容器开发更加友好,也是 Google 的 GAE 弃用 Tomcat 转为 Jetty 的一个原因;不仅想如果当时有 SB 这种框架或许迁移难度会大幅下降。

在嵌入式 Reactive Web 容器方面,Undertow 用的还是蛮多的,它也对这两种情况各有一个实现。当依赖中存在 WebFlex 的时候,容器的 Starter 就自动装配为 Reactive 容器了。

自动装配

关于这点,一开始让我们感觉很神奇,文档也是轻描淡写『@EnableAutoConfiguration 和 @SpringBootApplication 两者选其一标注在 @Configuration 类上』,但是它没说明这个 @Configuration 是如何装配的。
对于 Spring,我们熟悉常见的三种装配方式:

  • <context:component-scan> 标签
  • @Import
  • @ComponentScan

以上三种手段均需要 Spring 上下文引导,前一个可以使用 ClassPathXmlApplicationContext 加载,后者就需要 AnnotationConfigApplicationContext 加载;
对于 SB 很显然是通过 SpringApplication 实现的,那么我们可以认为,主启动类承担了 @Configuration 的角色。

@SpringBootApplication

这个注解肯定不陌生,它是很有料的,如果你点进去看源码,相关的分析其实在『Spring进阶』中已经说的差不多了,这里不再重复,简单说它相当于开启自动装配、标注配置类、开启包扫描;在包扫描上,它排除了一些特定类型,例如同时标注配置类和开启自动装配的,这就避免了使用多次时的冲突问题。
PS:配置类源于 @Component 的派生,Spring 称为『模式注解』。

另外,在这个注解中,使用了大量的 @AliasFor 别名,这属于 JDK 的知识了,只是提醒一下。

@SpringBootApplication 也并不是一定要标注在引导类上,只要保证 run 方法传递的是标注这个注解的类即可,这一点的原因在进阶文章里也提到过。
具体的将也不是非 @SpringBootApplication 不可,官方的说法 @EnableAutoConfiguration 也是没问题的(即使它不是 @Configuration),这里去看进阶篇就足够了,点到为止,之后还会深入分析,这里还不到时候。


作为 @Configuration 的派生类,也继承了 CGLIB 提生的特性,官方文档中有说明,传统的 @Bean 属于轻量模式(Lite)在 @Configuration 或者 @Component 下的 @Bean 就属于完全模式(Full),会执行 CGLIB 提升。

@SpringBootConfiguration

这个注解如果你看的话跟 @Configuration 没什么不同,但是在 SB 中,官方建议多使用 @SpringBootConfiguration,在做注解元数据解析的时候会有帮助。

创建自动配置类

编写自动配置类也是有固定套路的,一般创建名为 xxxAutoConfiguration 的配置类,这个类一般被标注了 @Configuration 和 @Import 注解;
对于 @Import 注解,你可以选择导入一个配置类或者 ConfigurationSelector 的实现类。
接下来,就是配置执行入口,也就是在 META-INF/spring.factories 文件中配置这个类:

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.bfchengnuo.demo.xxxAutoConfiguration

因为都是注解,解析注解带来的时间成本肯定会影响应用的启动速度,所以在 Spring5.x 的时候加入了 @Indexed 注解,在编译的时候就会向 @Component 和其派生注解增加索引,从而减少运行时的消耗。

Production-Ready特性

也就是为生产准备的特性,例如指标、健康检查、外部化配置。
它是 DevOps 的立足点,当然这个词具体的定义就是仁者见仁智者见智了。
对于 SpringBootActuator 国内大部分都不重视,其实还是很有用的,它用于监控和管理 Spring 应用,可以通过 HTTP Endpoint 或者 JMX Bean 与其交互。
这里就不继续展开了,内容还是蛮多的,如果以后逐步都用起来了,估计还会单独写吧,个人感觉还是很有前途的。

下面说说外部化配置,在 SB 中有三种用途:

  1. Bean 的 @Value 注入
  2. Spring Environment 读取
  3. @ConfigurationProperties 的读取

上面说的是消费方,那么生产源呢,在官方文档中描述了 17 种 PropertySource 的顺序,然而我们平常用的并不多,也就是用用命令行指定来替代 application.yml 中的属性。
因为 SB 的规约大于配置思想,让我们省去了大量的配置。

后记

纵观 Spring 的一路演变,就是注解驱动之路,在 3.1 之后可谓大变化,增加了 @EnableXXX 模式,包扫描和 ConfigurationSelector(ImportSelector);到了 4.x 有了 Condition 条件注解,接下来会更加详细的探讨。
SB 的特性让其称为微服务的基础组件,也是 SC 的基础设施,或者将 SB 称为微服务的中间件,也是 SB 的出现让 Spring 社区焕发了第二春,简单再说 SC,对于分布式系统要解决的问题有:

  • 分布式配置
  • 服务注册和发现
  • 路由
  • 服务调用
  • 负载均衡
  • 熔断机制
  • 分布式消息

Spring 官方最大的优势就是 API 设计能力,也可以说是抽象能力,云平台的现在,Java 已经派生语系处于垄断地位,SC 也慢慢成长(虽然 Netflix 进入维护模式,不过 Alibaba 崛起了)SpringCloudStream 也可以了解一下。

走向自动装配

前面的自动装配只是简单的提了一下相当于是结果,这一部分讲述自动装配的发展过程以及其中涉及的一些编程模式。

注解驱动的发展

在 Spring2.5 的时候我们常用的 @Autowired、@Component、@Order、@Service、@Qualifier、SpringMVC 相关注解才被加入,之前基本还是 XML 的天下,即使这个版本提供了大量的注解,但是还是离不开 <context:component-scan><context:annotation-config> 的噩梦。

对于 @Qualifier 这个注解,还有一种『逻辑类型』限定的使用(不带参数,用来缩小匹配范围),例如在 SC 的 @LoadBalanced 中就如此使用。
即,你在 @Bean 的时候顺带加一个 @Qualifier,那么在 @Autowired 的时候也加一个 @Qualifier 它会自动找之前定义的时候也有 @Qualifier 的对象,就算有其他的相同的类型的 Bean 也会被过滤,并且这种方式不需要写值。
另外,Spring 还有一种缺省机制,也可以说容错机制,在没用 @Qualifier 注解下,即缺省情况下使用 Bean 标识符(方法名、字段名)注入。

到了 3.x 时代,进入了注解驱动的黄金时代,井喷式增长,当然也跟 Java5 的特性有关,通过派生性,有了 @Configuration 这类注解,@Import 也出现了,并且提供 @ImportResource 来解决 XML 遗留问题。
通过加入 @Bean 等一些列注解对标 XML 配置,决心替代 XML 了。那么最后的问题,谁来引导 SpringContextConfiguration 呢,不可能通过包扫描标签,那样又是依赖 XML,所以有了 AnnotationConfigApplicationContext,用这个来注册 @Configuration,并且通过前面的 @ImportResource 导入遗留的 XML 配置,虽然这种方式看起来有点别扭。
终于,在 3.1 的时候引入 @ComponentScan 替换了包扫描标签,全面进入注解驱动时代;
同时,也出现了 @Profile 这类条件化定义 Bean 能力的注解,虽然功能很弱;Web 方面也是突飞猛进,@RequestBody 之类的注解也都出现;
并且 3.x 时代还新增了 Environment 之类的 API 接口,@PropertySource 的出现为外部化配置奠定了基础,其他的还有很多,例如缓存的抽象、异步的支持(@Schedule、异步 Web 处理)、校验注解(JSR-303)、@Enable 模块驱动(@EnableWebMvc 等)。
所以,3.x 真的是黄金时代,看着都兴奋。


接下来的 4.x 进入完善时代,上面说过 @Profile 很鸡肋,所以加入了 @Conditional,通过编程的方式来解决 @Profile 灵活性不足的问题,SB 中的 @ConditionalOn* 就是这个的派生。
尽管不是强制使用 Java8,但也巧妙的兼容了新的时间 API、@Repeatable 和参数名称发现;也正是有了 @Repeatable 所以 @PropertySource 提升为了可重复标注的注解(Java8 之前可以配合 @PropertySources 使用),@ComponentScan 也是同理;
同时,4.2 新增了 @EventListener 作为 ApplicationListener 的另一个选择,@AliasFor 注解也解除了之前派生的一些限制(@GetMapping 一类注解就使用了这个特性),加入 @CrossOrigin 作为 CorsRegistration 的替换方案。
其他的一些例如依赖查找 @Lookup 处在比较边缘化的地位,不需要太多了解。


最后,也就是现在的 5.x 版本,新增的 @Indexed 用来构建索引,加快包扫描,但是存在一定的缺陷,要避免模式注解忽略的问题(例如项目引入两个 jar,一个使用了一个没使用,并且包名相同,那么你在项目里只能扫到使用了 @Indexed 的 Bean,因为没使用的不会存在于 META-INT/spring.components 索引文件中,存在这个文件 Spring 就不会扫描包,只会寻找索引文件对应的 CandidateComponentsIndex 对象);感兴趣的可以研究一下。
同时引入了 JSR-305 适配注解 @NonNull 之类(Spring 中已经大量使用),为 Java 和 Kotlin 之间提供技术杠杆。

注解编程模型

Java 语言规范规定,注解不能继承,没有派生子类的功能,所以 Spring 采用了元标注(注解上标注注解)的方式来实现『派生』;
需要注意,直到 Spring4.x 才实现了多层次派生性,之前都是深度有限,4.x 版本中的 AnnotationAttributesReadingVisitor 使用了递归方式查找元注解,这个问题才得以解决,不过 SB 最低版本都是依赖 4.x+,所以不用担心。
其他的,为了方便使用还添加了很多组合注解,例如 @TransactionalService 就是 @Transactional 和 @Service 的组合;那么如何感知是个问题,常规的思路肯定是反射手段解析元信息,但是 Spring 并没有这么做,使用的是抽象出 AnnotationMetadata 这个接口。
为了提高效率,Spring 的类加载机制是通过 ASM 实现的,例如 ClassReader,相对于 ClassLoader 体系,它直接操作字节码,也便于进行字节码提升;这方面的内容很复杂,是一个大的体系,这里不再详细说明。

AnnotationMetadata 有两个实现类:AnnotationMetadataReadingVisitor 和 StandardAnnotationMetadata;
前者使用 ASM 方式读取,涉及 AnnotationMetadata 和 ClassMetadata 等对象,丰富性肯定不如 Java 反射 API;
后者使用 Java 反射进行读取,用的也很多。

那么 Spring 为什么要两套实现?
除了效率的差距,还有一个是场景,如果使用 Java 反射 API,必然需要一个大前提,就是反射的 Class 必须被 ClassLoader 装载,但是在 Spring 的包扫描阶段,显然是不可能的,所以使用 ASM 的方式;之后装载之后就可以使用 Java API 了。
如果需要进行反射相关操作,不妨试试 Spring 提供的反射工具类:ReflectionUtils;类似的 AnnotationUtils、AnnotatedElementUtils 的工具类 Spring 中也大量使用。
同时,在使用元注解的时候要留意属性覆盖的情况,细分可以是显性覆盖(@AliasFor)和隐性覆盖;其中也有一些规则,这里不细说了。

注解驱动设计模式

从 @Enable 模式开始说起(模块装配),Spring 中就存在很多,例如 Web Mvc、缓存、JMX、Async 模块等,这都是来自 Spring,并不是 SB 的特性,当然在 SB 和 SC 中也有新增,例如开启自动装配。
这个模式简化了装配模式,做到了按需装配,但是缺点是必须手动开启。
而想要自定义 @Enable 也很简单,你可以随便拿一个来参考,主要就是利用 @Import 和 @Configuration,当然你也可以试试接口编程的 ImportSelector 接口,其他的 ImportBeanDefinitionRegistrar 用的相对少一点。
而原理,主要还是对这些注解的解析,因为这又是一个大的体系,在这也不想多说,简单提一提:

无论是 XML 还是注解驱动的场景,均是通过 AnnotationConfigUtils 的 registerAnnotationConfigProcessors 方法进行装载 ConfigurationClassPostProcessor 类,这个类是最高优先级的 BeanFactoryPostProcessor 实现。

解析 Spring BeanDefinition 的注解元信息最重要的组件是 ConfigurationClassParser,它的两个重载分为不同的实现,就是前面说过的 AnnotationMetadataReadingVisitor 和 StandardAnnotationMetadata;
这里也会进行递归调用解析,还记得前面说的轻量模式和完全模式么,就是根据这个来区分进行 CGLIB 提升。

SpringWeb自动装配

在 Spring 中除了模块装配,在 3.1 之后,也支持自动装配,仅限于 Web 场景;新引入的 WebApplicationInitializer 构建在 Servlet 3.0 的 ServletContainerInitializer 上,支持编程方式替代传统 web.xml。
在 SpringSecurity 也有类似的实现 AbstractSecurityWebApplicationInitializer。

实现原理还是依赖 Servlet 3.0 的特性,大部分开发人员对 Servlet 规范还是陌生的,新规范带来的运行时插拔可是极大的灵活性(拓展知识 SPI),也不能说新了,毕竟很多年了,现在 4.0 的异步技术又有多少人关注呢。
关于这一块的内容,如果感兴趣去稍微看下源码,其实还挺有意思的。

1
2
3
AbstractDispatcherServletInitializer
|-AbstractAnnotationConfigDispatcherServletInitializer
|-AbstractContextLoaderInitializer

在 Web Mvc 中,DispatcherServlet 有专属的 WebApplicationContext,它继承了来自 Root WebApplicationContext 的所有 Bean,也就是我们常说的父子容器。
无论哪一个容器,都是基于注解驱动的 Web 应用上下文实现的,一般情况我们选择最具体的就好。

条件装配

说的就是 @Profile 和 @Conditional,对于 Profile 只支持简单的 @Profile({"dev", "prod"})@Profile("!dev") 这种形式,具体的处理原理不多说了,还是分析注解元数据那一套。
由于 Profile 太过于局限性,现在基本都是 @Conditional 的天下,实现起来也不复杂。

1
2
3
4
@Conditional({ProfileCondition.class})
public @interface Profile {
String[] value();
}

看到这个源码,还有什么疑问,肯定是 @Conditional 的天下了。
当多个 Conditional 并存时,会使用 @Order 排序,需要注意一下;ConditionEvaluator 的评估有两个阶段,Bean 注册阶段和 Configuration Class 解析阶段。

SB自动装配

在 SB 中的自动装配相比上面所说,一个区别就是应用依赖的 Jar 存在变化的可能,因为所在包的路径不确定,所以很多手段都不太合适,或许会想到使用 @ComponentScan 来全局扫描,但是官方文档中明确表示不鼓励这样扫描默认包,因为它会读取所有 Jar 中的类。
SB 的自动装配是非侵占性的,对于失效自动装配有两种方式:代码配置(@EnableAutoConfiguration 的 exclude)和外部化配置(spring.autoconfigure.exclude),为了避免侵入性,外部化是优先选择。
如果你想看 SB 自动装配的原理,可以进入注解发现是通过 AutoConfigurationImportSelector 来实现的,读一下这个的源码就能猜个差不多,从字面意思:

  1. 加载自动装配的元信息
    至于为什么需要加载元信息,因为 @Conditional 之类的注解处理时机较晚,所以根据元信息来匹配就减少了自动装配的时间,参考:spring-autoconfigure-metadata.properties
  2. 获取 @EnableAutoConfiguration 标注类的元信息
  3. 获取自动装配的候选类名集合
    使用 SpringFactoriesLoader 进行读取,采用 Spring 工厂机制的加载器,简单说就是读取 spring.factories 中的配置,合并成一个 Map。
  4. 移除重复对象
    利用的是 Set 集合去重:return new ArrayList<>(new LinkedHashSet<>(list));
  5. 移除我们自己配的『失效自动装配』
  6. 再根据 autoConfigurationMetadata 过滤
    过滤 spring.factories 中那些当前 ClassLoader 不存在的 Class,可以说是检查是否合法
  7. 触发自动装配导入事件

SpringFactoriesLoader 在 SB 中大量的使用,这一块的内容确实不少,如果是做基础架构的,还是要深入了解,我这种打酱油的就先点到为止。
关于事件,这里贴一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 依赖于 spring.factories 配置文件:
// org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
// thinking.in.spring.boot.samples.auto.configuration.listener.DefaultAutoConfigurationImportListener
public class DefaultAutoConfigurationImportListener implements AutoConfigurationImportListener {
@Override
public void onAutoConfigurationImportEvent(AutoConfigurationImportEvent event) {
// 获取当前 ClassLoader
ClassLoader classLoader = event.getClass().getClassLoader();
// 候选的自动装配类名单
List<String> candidates =
SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class, classLoader);
// 实际的自动装配类名单
List<String> configurations = event.getCandidateConfigurations();
// 排除的自动装配类名单
Set<String> exclusions = event.getExclusions();
// 输出各自数量
System.out.printf("自动装配类名单 - 候选数量:%d,实际数量:%d,排除数量:%s\n",
candidates.size(), configurations.size(), exclusions.size());
// 输出实际和排除的自动装配类名单
System.out.println("实际的自动装配类名单:");
event.getCandidateConfigurations().forEach(System.out::println);
System.out.println("排除的自动装配类名单:");
event.getExclusions().forEach(System.out::println);
}
}

SB 的事件分发相关在后面会说,事件的分发是一个非常重点的内容。

从生命周期来说,因为 AutoConfigurationImportSelector 实现了 DeferredImportSelector,从名字看这是延迟的(会有一个待处理队列),它在 @Configuration Bean 处理完毕后才会运作,Order 来看也是优先级接近最低的。
拓展阅读:ConfigurationClassParser(实际执行 ImportSelector 的地方)

在顺序方面,可选择的有两种,固定和相对,例如 @AutoConfigureOrder 和 @AutoConfigureBefore,他们两个之间的顺序是先固定然后再根据相对顺序调整。
PS:尽量使用 name 属性,别用 value,还是因为 SB 升级导致的破坏性 API 变更,每次升级或多或少都有些。

关于自动装配的 BasePackages 参考嵌套类:AutoConfigurationPackages.Registrar,为了避免重复导入,会根据名字判断 IoC 有没有,也就是它只会注册一次。
因为方法执行在 Bean 初始化阶段,其 BeanDefinition 还有调整的机会,所以可以追加,借此达到夸大搜索范围的目的。

自定义SB自动装配

从 spring.factories 这种加载机制可以看出与 SPI 非常像,那么接下来正式开始,首先从命名来看,遵循 SB 的套路使用 *AutoConfiguration 这样的规则,也要看一下他们的包结构,模仿着写;
Spring 官方建议自动装配的代码写在 autoconfigure 模块下,starter 模块依赖该模块,并且附加其他需要的依赖,当然官方并没有坚持要分包(jar),开发者完全可以合并到一个模块(jar)。

Starter 的命名,官方建议是使用 ${module}-spring-boot-starter 的命名模式(或者拆分出一个 autoconfigure 模块),使用 spring-boot-configuration-processor 可以帮助 @ConfigurationProperties Bean 生成 IDE 辅助信息。
PS:在设置 Key 命名空间的时候,注意不要跟官方冲突,要不然,可能会有奇奇怪怪的问题。

总体来说跟『创建自动配置类』差不多,毕竟那是基础,编写完自动配置类后将其加入 spring.factories 文件,然后就可以构建 Starter,其中你所依赖的 SB 相关依赖都要设置为 <optional>true</optional> 不要传递依赖,因为用户使用的 SB 版本可能会与之发生冲突,由于不同环境的 ClassLoader 不确定性,最终导致 Class 文件二进制不兼容的情况,可能表现为 IDE 中正常线上不正常或者反之,例如常见的 NoClassDefFoundError、NoSuchMechanismException 等。

其中可以使用 @ConditionalOn* 之类条件注解来实现,由于 SB 的设计问题,在 2.0 时代真的是改来改去,各种破坏性升级,让人怀疑人生,一般情况为,name 属性是用于第三方库或者高低版本兼容的场景;value 属性用于物理路径非常稳定的情况,一般情况下,还是 name 用的多。

因为 ConditionEvaluator 在注册 Bean 阶段进行评估,所以 @ConditionalOnBean 和 @ConditionalOnMissingBean 的 Java doc 强烈建议开发人员仅在自动装配中使用该条件注解。
这一对注解主要用来判断当前的 Spring 应用上下文是否存在该 Bean,如果存在就直接 autowired,如果不存在就 new。
通常,它会和 OnClass 连用,先判断 CP 中是否存在,然后才有后来的容器中是否存在,还要考虑其他依赖是否装配了该 Bean,因为有个顺序问题。

对于属性条件注解 @ConditionalOnProperty,属性来源于 Spring Environment,典型代表就是 Java 系统属性和环境变量,application.yml 也是,都属于 PropertySource。
剩下的 Resource 条件注解感兴趣的可以看一下,在书中进行了大量的源码分析,关键是要搞明白 ConditionEvaluator 和 ResourceLoader(ConditionEvaluator 关联的 ResourceLoader 来自 Spring 应用上下文),最终会发现 DefaultResourceLoader 实际上是 Spring 中的唯一 ResourceLoader 实现。
个人感觉涉及 Resource 的东西,不说难吧就是很绕,很烦人,I/O 是个折磨人的东西,涉及 Stream、各种 URL 协议之类,Handle 和 Factory。

SB 自定义的 Condition 基本都是扩展的 SpringBootCondition 而不是直接实现 Condition 接口,可以借鉴下。

示例工程:Github

理解SpringApplication

这一部分从 Spring 应用的生命周期来看,分为初始化阶段、运行阶段、结束阶段。也是很硬核的内容。

SpringApplication初始化阶段

初始化阶段主要分为构造阶段和配置阶段,构造阶段当然是通过构造器来完成的,不过一般情况我们都用 run 这个静态方法了,它也是走的构造。
无论那种,最终都需要传递一个 primarySource,也可以理解成标注了 @EnableAutoConfiguration 的类,最终会被 SpringApplication 的 primarySources 属性保存,接下来会依次执行 WebApplicationType.deduceFromClasspath、setInitializers、setListeners、deduceMainApplicationClass,可以理解为:

  • 推断 Web 应用类型
    在此阶段上下文还没有准备,所以使用的是检查当前 ClassLoader 下的基准 Class 的存在性来判断;
    当 DispatcherHandler 存在,DispatcherServlet 不存在,也就是依赖 WebFlux 下,判断为 Reactive Web 应用;
    当 Servlet 和 ConfigurableWebApplicationContext 不存在时,非 Web 应用;
    当 Spring WebFlux 和 Spring Mvc 同时存在,按 Servlet Web 处理。
  • 加载 Spring 应用上下文初始化器
    该方法返回所有 spring.factories 资源配置中的 ApplicationContextInitializer实现类名单。
    并不强制要求实现 Ordered 排序,排序后保存到 initializers 属性中;
    PS:在调用 run 之前,允许你使用 setter 方法进行覆盖性更新。
  • 加载 Spring 应用事件监听器
  • 推断应用引导类

构造阶段就算到此完成,接下来是配置阶段,该阶段是可选的,主要用于调整或者补充构造阶段的状态,以 SpringApplication 的 setter 方法为代表,是用于调整的相关;补充行为则以 add 方法为代表。推荐使用 SpringApplicationBuilder。
大多数情况开发人员无需调整 SpringApplication 的默认状态,作为拓展可以看看 SC 或者 SC Data Flow。

1
2
3
4
5
6
new SpringApplicationBuilder(DemoApplication.class)
.bannerMode(Banner.Mode.CONSOLE)
.web(WebApplicationType.NONE)
.profiles("dev")
.headless(false)
.run(args);

用的最多的也许是 Banner 相关吧。

关于配置源,1.x 和 2.x 差别较大,2.x 中构造函数由 Object 改为 Class,所以 XML 和 packages 就无法作为参数传递,只能通过 setSources 方法传递。
PS:注意 sources 和 primarySources 属性。

其中,sources 属性使用的是 LinkedHashSet 说明具有去重功能,并且有序。

SpringApplication运行阶段

这个阶段属于核心过程,围绕 run 方法展开,它会进一步完善所需要的资源准备,随后启动 Spring 应用上下文,伴随 SB 和 Spring 的事件分发,形成完整的 SpringApplication 生命周期。
它可以再进行细分,准备、启动、启动后。

上下文准备阶段

本阶段的范围是从 run 方法开始到 refreshContext 调用之前;挑主要的说。
SpringApplicationRunListeners 属于组合模式的实现,内部关联了 SpringApplicationRunListener 的集合,按照字面意思,应该是 SB 运行时监听器(此处应有监听方法与运行阶段对应表

结合 SpringFactoriesLoader 机制,可以从 spring.factories 中快速定位其内建实现;
对于普通开发者,只需要根据 SpringFactoriesLoader 机制和 SpringApplicationRunListener 的要求就能对该接口进行扩展。

然后,可以得出,EventPublishingRunListener 是 SB 中的唯一内建实现,可以得出,根据 SpringApplication 所关联的 ApplicationListener 实例列表,动态的添加到 SimpleApplicationEventMulticaster 中;
SimpleApplicationEventMulticaster 是 Spring 中实现 ApplicationEventMulticaster 接口的类,用于发布 Spring 应用事件(ApplicationEvent),所以可以看出它也充当了 SB 中事件发布者的角色。
(此处应有 SB 事件与监听方法对照表)
虽然 SB 与 Spring 事件有很大的关联性,但是他们还是有差异性的,主要体现再顺序和时机上,官方文档中也有提及。

拓展-理解Spring事件

Spring 事件是 Spring 应用上下文 ApplicationContext 对象触发的,SB 事件的发布者则为 SpringApplication 关联的 SimpleApplicationEventMulticaster 类型,虽然它也是来自 Spring。
Spring 中的事件也采用了 JDK 的观察者模式规范,不过进行了一定的扩展或者说增强。

对于如何监听具体的 ApplicationEvent 类型,在 3.0 得到改善,ApplicationListener 支持泛型监听,不再监听所有事件靠 instanceof 筛选,但是由于泛型的限制,无法同时监听不同的事件类型,如果继续使用 ApplicationEvent 做泛型,这就又回到了之前。
所以,3.0 中引入了 SmartApplicationListener 接口,它通过 supports* 方法来过滤监听的事件类型和事件源类型,例如 ConfigFileApplicationListener。

上面说过 SB 的事件发布者 SimpleApplicationEventMulticaster 也是来自 Spring,并且是 ApplicationEventMulticaster 接口的实现类,该接口主要承担两个职责:关联 ApplicationListener 和广播 ApplicationEvent。
PS:SB 的事件监听器都是经过排序了的。

看源码的朋友们,Spring 4.0 引入的 ResolvableType 是 Spring 为了简化 Java 反射 API 提供的组件,它能够轻松的获取泛型类型等。

SimpleApplicationEventMulticaster 虽然允许事件广播时 ApplicationListener 异步监听事件,但是无论时 Spring 还是 SB 均没有使用其来提升为异步执行,由于 EventPublishingRunListener 的封装,SB 事件监听器也无法异步执行。

关于 ApplicationEventMulticaster 与 ApplicationContext 的关系,官方文档提到过可以使用 ApplicationEventPublisher 发布 ApplicationEvent;
查看 ApplicationContext 可以看到它扩展了 ApplicationEventPublisher,也就是说,无论那种 Spring 应用上下文,都具备发布 ApplicationEvent 的能力
PS:获取 ApplicationEventPublisher 可以通过 Aware 方式。仔细看还会发现拓展了 ResourceLoader,所以 ApplicationContext 也是 setResourceLoader 方法的常客。

我们可以简单的得出结论,ApplicationEventPublisher 的实例就是当前 ApplicationContext。
SimpleApplicationEventMulticaster 既是 SB 事件广播的实现,又是 Spring 事件发布的实现。
SimpleApplicationEventMulticaster 作为 Spring 中唯一的 ApplicationEventMulticaster 实现,无论是 Spring 还是 SB,都充当同步广播事件对象的角色,开发人员主要关注 ApplicationEvent 的类型和对应的 ApplicationListener 实现即可。
此处应有 Spring 内建事件一览表

拓展-Spring内建事件

当 ConfigurableApplicationContext 的 refresh 方法执行到 finishRefresh 方法时,Spring 应用上下文就会发布 ContextRefreshedEvent (上下文就绪)事件;
此时应用上下文中的 Bean 已经完成初始化,并能投入使用,通常会使用 ApplicationListener<ContextRefreshedEvent> 监听,获取所需要的 Bean,防止出现 Bean 提早初始化。

剩下的 Spring 应用上下文启停事件不多说,SC 中还用了下,SB 不常见。
只需要正确的理解上下文的 start、stop、close 方法之间的区别。相关类:Lifecycle;

因为事件源都是用的 ApplicationContext,所以称之为 Spring 上下文事件

拓展-Spring应用上下文事件

应用上下文 ApplicationContextEvent 与 Spring 事件 ApplicationEvent 的关系嘛,直接看就是继承关系(extends ApplicationEvent);
对于 Spring 事件的监听,4.2 开始可以使用 @EventListener 注解,毕竟开始进军注解驱动编程。

使用 @EventListener 的时候,注意要标注在 IoC 中的 Bean 上,并且需要 public 权限;
单一类型监听中,虽然规范要求不能有返回值,但是即使返回值不为 void 也可以执行;
在多类型监听中,需要特别处理,不能有返回值,也需要手动进行多个 ApplicationEvent 的过滤。

异步支持:需要先使用 @EnableAsync 开启,然后使用 @Async 注解。
可以使用 @Order 控制顺序。
对泛型 ApplicationEvent 支持方面,需要事件实现 ResolvableTypeProvider 接口。
相关源码:EventListenerMethodProcessor(生命周期相关)、DefaultEventListenerFactory(适配相关)

因为关联了 ApplicationEventMulticaster 属性,在 close 的时候也没有进行销毁关系,所以即使在 close 后,依然可以发布 Spring 事件,但是因为关联的 ApplicationListener 已经被销毁,所以最终无法被监听。
广播实现也很简单,就是通过 ApplicationEvent 找到关联的 ApplicationListener 列表,异步或者同步的调用即可。
此处应当有这几种方式的对照表

Spring事件小结

主要包括的是 Spring 事件、事件监听器、事件广播器,以及它们与 Spring 应用上下文的关系。
涉及到的事件分为 Spring 事件与 Spring 应用事件,主要以事件源区分,并且开发者可以自定义 ApplicationEvent。
进一步是泛型化与注解化。注解化依赖于适配器将其转换为 ApplicationListener,其中有 AOP 相关知识。
其中我略去了大量的源码级讲解,没办法,源码看的我头大,目前真的不想再多弄了 (/▽\)。

拓展-SB事件

有了 Spring 的事件基础,再来看 SB 的事件就简单多了,毕竟都是一套体系,如果 SB 仅仅是复用 Spring 相关的 API 那么就更好了,他们各自为政互不干涉,对我们理解很友好。
但是再 1.4 之前的版本,其实内部还是很混乱的,主要因为直接复用了 SimpleApplicationEventMulticaster,可能存在重复添加的情况,核心问题在于 spring.factories 中的实例列表是否有必要关联到 Spring 应用上下文中,这就导致 ApplicationListener 也可以监听到 SB 的事件,这其实并不好。

从 1.4 之后,SB 就进行了微调,SB 事件与 Spring 事件开始独立,互不干扰,也是从这开始核心 API 开始趋于稳定。
具体改动可参考 EventPublishingRunListener 的 contextPrepared 方法,不再直接关联 SimpleApplicationEventMulticaster,但是实例依然会加入到 Spring 应用上下文中。
也正是因为 SpringApplication 使用了独立的 ApplicationEventMulticaster 对象,虽然 SpringApplication 和 ApplicationContext 都还是使用的 SimpleApplicationEventMulticaster 实例,但不再是同一个对象
到这里就完成了隔离,SB 中可以监听 Spring 事件,反之不可,但是多数情况监听的还是 SB 事件。

在 SB 中,大量使用了 SmartApplicationListener,SB 中事件的事件源多用 SpringApplication。

拓展-SB内建事件

在 SB 中,无论你监听 SB 事件还是 Spring 事件,都是通过在 spring.factories 中配置实现的(属性为 ApplicationListener 的实现类)。
此处应当有 SB 内建事件一览表
最熟悉的可能是 ConfigFileApplicationListener 和 LoggingApplicationListener,分别负责 SB 应用配置文件的加载和日志系统的初始化(日志框架识别和日志配置文件加载等)。
SB 的内建事件根据 EventPublishingRunListener 的生命周期回调方法依次发布。
可以理解为 SB 的事件/监听机制是继承于 Spring 的事件/监听机制,例如 SpringApplicationEvent 就继承自 ApplicationEvent。

其他的类似 ApplicationArguments、ConfigurableEnvironment (对应不同的 Web 类型)就战略性略过了(Spring 应用上下文运行前准备)。

上下文启动阶段

本阶段有 refreshContext 方法实现,它首先调用 refresh,执行 ApplicationContext 的启动,然后注册 shutdownHook 线程(JVM shutdown hook 机制),实现优雅的 Spring Bean 销毁生命周期回调。
在 1.4 版本之后,重构了 refreshContext 方法,随着这个方法的执行,Spring 应用上下文正式进入 Spring 生命周期,SB 的核心也随之启动,例如自动装配、嵌入式容器等特性,紧接着分发 ContextRefreshedEvent 事件。

上下文启动后阶段

这里主要是 afterRefresh 方法,具体的实现在 2.0 也有略微调整,主要为不再具有执行 ApplicationRunner 或者 CommandLineRunner 的能力,不过并不会影响它们的执行,只是执行时机相对延后了,最后分发 ApplicationStartedEvent 事件。

同时 ApplicationStartedEvent 的语义也有所变化,1.5 中加入了 ApplicationStartingEvent 事件,虽然 ApplicationStartedEvent 标注为 2.0 加入,其实在 1.x 就存在,2.0 对其进行了调整,关联了 ConfigurableApplicationContext 对象。
简单说就是,ApplicationStartingEvent 充当了之前 ApplicationStartedEvent 的角色,ApplicationStartedEvent 被延后触发。

SpringApplication结束阶段

各个版本中此阶段的实现比较稳定,可分为正常结束和异常结束。
当 ApplicationReadyEvent 事件触发后 SpringApplication 的生命周期进入尾声,除非发生异常,进入异常分支,这其中在不同版本中的实现都有细微变化,不过影响不大。
而如果进入异常分支,基本就意味着 Spring 应用运行失败,可参考 SpringApplicationRunListener。

对于 SB 的异常处理,1.1 开始就替换为 Throwable,同时拥有故障分析器:FailureAnalysis,它会在上下文关闭之前执行错误分析并输出报告。
其中 FailureAnalysis 仅分析故障,报告则由 FailureAnalysisReporter 负责(也是由工厂机制加载排序,默认仅存在一个)。
当然你也可以自定义这两个的实现,一样配置到 spring.factories 中,1.x 与 2.x 有所不同,SC Data Flow 用户关注一下。

应用退出阶段

这里主要是 ShutdownHook 线程,在 JVM 退出时能够确保完成 Spring 生命周期回调,进行资源释放,例如 JDBC 连接、会话状态等。
这里也可以细分正常退出和异常退出,还有个退出码,不知道有没有人关注,用 IDEA 的时候会打印出来,非 0 就是异常退出,相关源码:ExitCodeGenerator。
因为 IDEA 可以获取子进程的退出码,但是真实环境下基本不可用,除非你是用 SC Data Flow 之类。
异常退出由 SpringApplication 的 handleRunFailure 方法负责,由于用的不多,不多展开了。

其他

Spring IO Platform 项目是为了统一 Maven 管理的项目,2019 年后不再维护,被 spring-boot-dependencies 和 spring-boot-starter-parent 取代。

当相应的 starter 添加到 ClassPath 后,其关联的特性随应用的启动而自动装载,这种机制称为自动装配(AutomaticallyConfigure)。

使用 @HandlesTypes 来进行过滤,选择出自己关系的类型。

SB 根据 Web 类型推断来创建对应的 Spring 应用上下文,我们最常用的 Servlet 类型就是用 AnnotationConfigWebApplicationContext。
WebApplicationType 还可以作为 ConfigurableEnvironment 对象具体类型的条件,所以 applicationContextClass 的属性设定后还需要对 webApplicationType 设置。

在 spring.factories 中声明的资源,可能存在重复执行的情况,所以建议凡是使用 Spring 工厂加载机制的场景,建议覆盖 hashCode 和 equals。

SB 的事件监听器均由 Spring 工厂加载机制加载并初始化,它们并非 Spring Bean,因此无法享受注解驱动和 Bean 生命周期管理回调接口的『福利』,不过这并不影响他们获得 Spring Bean,因为有关联的 ConfigurableApplicationContext 对象。
这个要对比 ApplicationRunner 和 CommandLineRunner 看。

SB 引入 SpringApplication 大概是对 Spring 应用上下文的生命周期的补充。

喜欢就请我吃包辣条吧!

评论框加载失败,无法访问 Disqus

你可能需要魔法上网~~