SpringBoot编程思想
第一部分、总览Spring Boot
第一章、初览Spring Boot
Spring Framework对传统Java EE的颠覆方式为新的概念,即两种技术IoC和DI
其实Spring Framework也是在重复造轮子,放眼JSR规范,IoC的实现方式为JNDI(Java命名和目录接口),而DI则是EJB容器注入
Spring Framework的局限性:由于自身并非容器,基本上不得不随着Java EE容器启动而装载,但SpringBoot的出现改变了这一局面
SpringBoot具备开发(Dev)和运维(Ops)双重特性
第二章、理解独立的Spring应用
SpringBoot采用了嵌入式容器,独立于外部容器,对应用生命周期拥有完全自主的控制,从此改变了寄人篱下的境地
误区:很多人都以为SpringBoot嵌入式Web容器启动时间少于传统的Servlet容器,实际上没有证据表明,只是一种更快捷的启动方式,提升了开发效率而已
口头上习惯将Spring Boot开发的应用称为SpringBoot应用,实际上是Spring应用,对应着标题。
在原来Web容器外部化的时候,Spring应用上下文是依附于Web容器,Spring应用上下文的操作执行例如初始化操作destroy操作都需要结合回掉函数来达到,但是随着SpringBoot的出现,Web容器从属于Spring应用上下文,此时驱动Spring应用上下文启动的核心组件是SpringBoot核心API SpringApplication,所以是Spring应用,也可以称为SpringBoot应用。
接下来讲述了在非图形化环境下完整开发SpringBoot的HelloWorld例子,非常厉害了
windows集成了tree命令,可以直接展示文件树
图形化界面开发SpringBoot程序
只有在创建可执行JAR的时候才需要使用到Spring-boot-maven-plugin到pom中,如果直接使用maven命令启动则无需此插件
java -jar方式运行的jar文件和mvn spring-boot:run命令基本无异,只是前者是生产环境后者是开发环境
打包后生成两个文件:
.jar.original:Maven原始打包文件,不包含第三方依赖。
.jar:加工后的文件,包含第三方依赖,springboot-plugin帮的忙
JAR文件有时也被称为“Fat JAR”,采用zip压缩格式存储,可以解压zip的也可以解压JAR
其中文件的路径:
使用Java -jar命令运行JAR文件时候,就是标准的JAR执行操作了,会读取MANIFEST.MF文件作为启动引导类。
使用java -jar
命令启动程序是通过引导类JarLauncher来启动主类Start-Class
手动引入spring-boot-loader项目查看JarLauncher
调用Java -jar命令会执行JarLauncher中的main方法,实际上调用的方法:
MainMethodRunner#run:
这里的mainClassName指的就是MANIFEST.MF中的Start-Class属性,然后通过反射调用main方法
WAR实现方式与此类似
**打包WAR文件是一种兼容措施,既能被WarLauncher启动,又能兼容Servlet容器环境,换言之,WarLauncher和JarLauncher并无本质差别,所以建议SpringBoot应用使用非传统方式部署时,尽可能地使用JAR归档方式
第三章、理解固化的Maven依赖
SpringBoot为我们提供的固化依赖如:spring-boot-starter-parent和spring-boot-dependencies都提供了如版本信息管理等的固化依赖,如果项目有自己的parent就需要使用到后者了
这里有介绍后者的使用:https://docs.spring.io/spring-boot/docs/2.3.2.RELEASE/maven-plugin/reference/html/#using-import
spring-boot-dependencies是spring-boot-starter-parent的parent,其中包含着大量的dependencyManagement指定的版本号信息,甚至还包含maven plugin的管理信息。
就dependency两者好像并没有实质性的差别,但是在plugin上,spring-boot-starter-parent好像对plugin进行了定制,尽量还是使用parent吧
第四章、理解嵌入式Web容器
SpringBoot应用直接嵌入Tomcat、Jetty 和Undertow作为核心特性,在2.0版本还嵌入了Netty来做异步请求处理。
Java对HTTP请求的处理仅有两种选择:Servlet和其他。
前者几乎垄断了Java Web的开发,Tomcat和Jetty作为Servlet的经典实现,后来的Undertow作为JBoss社区推出的新一代Servlet3.1+规范的容器(其实就是Servlet3.1规范推出的时候的实现产品),成为嵌入式Servlet容器的新选择,嵌入式容器不是Spring自家推出的功能,而是Tomcat和Jetty在很久以前就已经支持嵌入式容器了。
至于Reactive Web容器,默认实现为Netty Web Server,与业内其他基于Netty实现的Web Server类似,如Eclipse vert.x。在Spring Boot Webflux中也是使用前者作为默认嵌入式容器实现,也可以通过spring-boot-starter-reactive-netty引入,也支持嵌入式容器。
热门的Web容器均支持嵌入式容器,并非Spring独创
2.2.6的web-starter的Web容器默认实现是9.0.33的Tomcat
接下来是对Tomcat、Jetty和Undertow作为嵌入式Servlet Web容器的讨论
Tomcat:
点进官网选择最老的版本7,可以看到
即使是7版本也对嵌入式容器有支持
实际上Tomcat自己发行了Maven插件,可以不用部署直接打包成可运行的JAR或WAR文件,估计SpringBoot有偷哦。
但不是将Tomcat作为嵌入式容器打包进项目当中,依然是以Tomcat容器为主体,这点和SPringBoot有所不同
不过Tomcat官方只是将该插件维护到了Tomcat7
Jetty:
切换到Jetty相当简单,在Web-starter exclusion掉tomcat-starter,再引入Jetty-starter即可
undertow也是类似
嵌入式Reactive Web容器
嵌入式Reactive Web容器作为SpringBoot2.0的新特性,处于被动激活的状态,只有检测到starter-webflux之后才会激活,并且当web和webflux同时存在的情况下后者会被忽略
可以使用SpringBoot2.0引入的一种ApplicationContext实现——WebServerApplicationContext来完成获取当前WebServer类型:
上面是通过让主类来实现ApplicationRunner接口来实现的,等效于下面的代码:
注册进IoC中的ApplicationRunner会在应用启动后回掉
另一种查看WebServer的方法:WebServerInitializedEvent(弥补非Web应用类型的场景,感觉 没咋读懂,既然引入了WebServer就必然是Web应用类型了呀,在非web情况下注入WebServerApplicationContext会失败)
如今的Tomcat、jetty、Undertow都支持Reactive,可以直接在webflux的starter外直接加入对应的Servlet容器依赖,而不用再去排除掉默认的实现了,估计是SpringBoot2.0推出的webflux的starter支持
一旦引入了这些依赖到classpath路径,我们就可以利用SpringBoot的自动装配特性来完成后续的配置工作。
第五章、理解自动装配
SpringBoot自动装配的对象是Spring Bean,比如通过XML方式和Java配置类方式组装Bean
启用自动装配的方式:在@Configuration类上标注@EnableAutoConfiguration或者是@SpringBootApplication,至于如何装配@Configuration类这里并没有说明,依赖于Spring Framework的装配规则:XML方式、@Import注解和@ComponentScan注解都可以完成自动装配,前者需要ClassPathXmlApplicationContext加载,后者需要AnnotationConfigApplicationContext进行注册。
由此看来,SpringBoot程序的自动装配来源于@SpringBootApplication注解的支持
SpringBoot官方给出的
@SpringBootApplication = @Configuration + @EnableAutoConfiguration + @ComponentScan注解,且他们都是用默认配置
实际上并没有这么简单,SpringBoot在1.3的文档中即是这样描述的,SpringBoot2.0的文档并没有真实的反映SpringBootApplication的全部作用
上述文档中描述的ComponentScan使用默认值,不准确,这里排除了FilterType的两个实现:
TypeExcludeFilter:在SpringBoot1.4中引入,用于查找BeanFactory中已经注册的TypeExcludeFilter Bean作为代理执行对象
AutoConfigurationExcludeFilter:在1.5的时候引入,用于排除其他同时标注@Configuration和@EnableAutoConfiguration的类
而在SpringBootApplication注解中使用了这两个Filter
不过还有一个不是很大的区别:SpringBootApplication标注的不是@Configuration而是@SpringBootConfiguration,不过在运行上的行为没有差异,这种类似于对象之间的继承关系好似Component和Configuration,Controller,Service之间的关系一样。
官方还提到了SpringBootApplication上面使用了EnableAutoConfiguration和ComponentScan的属性别名来实现定制化
这里跟着源码走就行了,实践通过
具体是通过Spring Framework4.2,SpringBoot1.2引入的注解@AliasFor
从这里可以看到SpringBootApplication是一个聚合注解,类似于@RestController
如果SpringBootApplication标注于非引导类之上,仍然可以通过SpringApplication的run方法来指定到被SpringBootApplication标注的类上,被run方法指定的类需要具有自动装配的特性,即至少需要@EnableAutoConfiguration注解标注才可执行。
对于Configuration则不强制要求,发现即使run指定的类没有指定@Configuration注解也可以将内部声明的bean加载到容器当中去,就相当于是SpringBoot自动将EnableAutoConfiguration类注册到容器当中去了吧。
@SpringBootApplication作为@Configuration的继承注解的特性:
在普通的Component中声明的@Bean对象被称为“轻量模式”的Bean,而在@Configuration 中声明的Bean是“完全模式”的Bean,后者会执行CGLIB提升的操作,前者只是简单的将对象注册到IoC容器当中来。
可以通过查看容器中Bean的ClassName来得出结论
这里的CGLIB的提升并非是@Bean对象提供的,而是为@Configuration类准备的
实例代码:
EnableAutoConfiguration千万不能省略掉,要不然项目启动会出现问题
没有加@Configuration的输出:
加了@Configuration的输出:
明显的看出代理,Configuration类是被CGLIB增强了的。
自动装配机制
SpringBoot的自动装配机制是依赖于Spring Framework的Bean生命周期管理和Spring编程模型,单Spring Framework自身是不支持@Configuration的自动装配了,SpringBoot1.0便添加了约定配置化导入@Configuration类的方式
实现自动导入配置类的条件判断是依赖于注解:@ConditionalOnClass和@ConditionalOnMissingBean(标注于@Configuration之上),可以指定在特定条件下(当依赖的类找到了,但没有声明配置类)的时候起效。进而实现配置类上@Import注解的导入到更多的依赖类和其他的自动配置
那么最初始的一批配置该如何导入呢?不可能一个个去Import吧,实际上在spring-boot-autoconfigure项目的spring.factories文件中都写着了初始化容器时候加载的Configuration
每个starter都会提供一个autoconfigure包来,可以查看这个包中的spring.factories文件来实现该starter自己的自动装配
实践:创建自己的starter:
实现环境搭建:
非常简单,让引导类处于三级包下,让另一个配置类也处于三级包下,由于默认只会加载引导类的目录下的所有类,因此配置类不会被加载到,我们通过自动配置类来实现自动加载配置类的功能:
在resources目录下创建META-INF/spring.factories文件,其中应该指定EnableAutoConfiguration注解作为配置类的key,因为自动配置一定会开启(引导类必须指定@EnableAutoConfiguration)也就相当于我们指定的配置类一定会被加载到,
/只是换行符,如果有多个类需要加载,中间用逗号隔开,如下
需要注意的一点:类命名均要以AutoConfiguration作为后缀
从而加载到WebAutoConfiguration配置类(只是写着是AutoConfiguration而已):
这里又是Web环境,因此会引入WebConfiguration类,可以在WebConfiguration中做一些输出来验证是否自动配置成功。
从而可以完全实现基于注解的Bean组装。
不可否认的是,随着 越来越多starter的引入,大量的Spring Boot装配变成黑盒,并且搭配条件注解之后显得更加复杂,为了支持以配置化的方式调整应用行为,如Web服务器端口等,Spring Boot提供了Production-Ready特性
第六章、理解Production-Ready特性
官方的解释为:Provide production-ready features such as metrics,health check and externalized configuration
production-ready 的一般性定义:
https://github.com/mitodl/handbook/blob/master/production-ready.md
主要是通过Spring-Boot-actuator来实现的
提供HTTP和JMX两种方式来监控和管理Spring
需要手动添加依赖到pom文件,通过暴露的endpoint来实现对程序情况的访问,默认情况下仅仅暴露health和info,要想增加其他,得使用management.endpoints.web.exposure.include=*配置到配置文件或者启动参数当中,这类配置被称为外部化配置
现在只剩下Production-Ready的最后 一个特性——externalized Configuration
官方给出的解释:
Spring Boot allows you to externalize your configuration so you can work with the same application code in different environments,You cna use properties file,YAML files,environment variables and command-line arguments to externalize configuration
外部化配置的三个用途:
Bean的@Value注入
Spring Environment读取
@ConfigurationProperties绑定到结构化对象
PropertySource之间存在顺序,顺序高的配置可以覆盖掉顺序低的配置(就相当于是配置覆盖了)
感觉SpringBoot的核心文档在Spring Boot Features中
外部化配置的概念:与外部化配置相对应的是内部化配置,如各种set方法就是内部化配置的典型代表,配置数据是来源于应用内部实现的,缺少相应的弹性
SpringBoot采取的外部化配置类似于使用properties文件,命令行参数,系统参数来达到不干预内部代码的情况下实现定制化。
理解约定大于配置:
结合SpringBoot官方给出的最后一个特性:
Absolutely no code generation and no requirement for XML configuration
前半句话说明绝无中间代码生成,从而影响SpringBoot应用运行时行为,后半句则是耳熟能详的约定大于配置了。
从技术的角度看,大多数人以为注解驱动是SpringBoot提供的,其实是Spring Framework自身提供的,因此SpringBoot依赖于Spring Framework
Spring Framework的注解在2.5版本时候便开始推广,那时候推荐的是在XML中配置Componentscan,然后在Java代码中使用一系列DI注解实现,依然依赖于XML
到了Spring3.0提供了@Configuration来代替XML,Bean标签也可以被@Bean注解代替,框架还提供了@Import注解来导入@Configuration class并将其注册为Spring Bean,尽管在Bean的装配上有所提升,但仍然需要以硬编码的方式指定范围
Spring3.1发布了@ComponentScan注解来取代了XML的ComponentScan标签,实现了XML的完全可替代性,还提供了部分激活注解@EnableXXX来将指定类装配的 方法,但是被@EnableXXX标注的类需要自己被IoC容器感知到,这就成为了一个先有鸡还是先有蛋的问题了。
例如你声明了一个@ComponentScan注解到一个类上,但是在当时脱离了XML后IoC是加载不到该类的,自然无法扫描到@ComponentScan注解了,仍然无法实现自动配置
Spring4.0提出的Conditional注解使得自动配置成为可能,因为官方给出的描述——“whenever possible”。
SpringBoot官方给出的六大特征如第三点是maven提供的,第六点是Spring Framework3.1就已经完全支持的,综上 ,Spring Boot的主要五大特征为:
SpringApplication
自动装配
外部化配置
Spring Boot Actuator
嵌入式Web容器
这五大特征构成了SpringBoot作为微服务中间件的基础,又提供了SpringCloud的基础设施
其实微服务开发完全没有技术的限制,传统的Java EE容器也可以实现微服务
只不过由于Spring社区十几年的开源策略和技术演进使得Spring Boot在微服务的世界中独占鳌头
虽然Spring Boot提供了丰富的功能特性,但是其天生缺少快速构建分布式系统的能力,为此,Spring官方在SpringBoot的基础上开发出Spring Cloud,致力于为开发人员提供一些快速的通道构建通用的分布式系统,核心特性包含:
Distributed/versioned Configuration(分布式配置)
Service registration and discovery(服务注册与发现)
Routing(路由)
Service-to-service calls(服务调用)
Load Balancing(负载均衡)
Circuit Breaker(熔断机制 )
Distributed messaging(分布式消息)
Spring Cloud想提供的这些特性被大多数互联网公司所实现了,Spring官方的最大优势在于其强大的API设计能力(统一抽象,屏蔽技术实现细节),在 云平台Java语言(及其派生语言)处于垄断地位,Spring Cloud高度抽象的接口对于开发人员而言,底层实现是透明的,当需要更换底层时候只需要更换一个starter即可,就类似于Tomcat换Jetty,无需过多的业务回归测试。
Spring Cloud的第二大优势在于Spring Cloud Stream的整合,通过Stream编程模式达到数据传输的目的,感觉是有偷Java语言层面设计的。
SpringBoot的成功使得Spring社区焕发出了第二春,主要是因为SpringBoot的自动装配机制,但自动装配底层依赖的是Spring Framework的注解支持。
第二部分、走向自动装配
随着微服务的发展,开发人员开始更加重视SpringBoot了
殊不知Spring Framework是SpringBoot的核心,Java规范才是他们的基石
对Java EE来说,SpringBoot这种优秀的技术架构遵从着“兼容并包,继往开来”的原则,兼容旧的技术实现,发展新的技术理念
第七章、走向注解驱动编程(Annotation-Driven)
在Spring Framework第一个版本时候,Java Annotation尚未发布,结合J2EE(Java EE的前身,当时称之为J2EE)的传统,通过XML文件的方式管理Bean之间的依赖关系
注解驱动发展史
2003年发布了Spring Framework1.0版本
Spring1.2.0版本开启了Spring Framework对Annotation的支持
主要也是当时注解的流行导致Java在语言层面开启了对Annotation的支持,随后Spring也做出了相应的支持。
框架层面已经支持了@ManagedResource和@Transactional注解,但是被注解标注的SpringBean对象仍然需要以XML的方式进行装配,对于Spring Framework1.0来说,XML是唯一的选择
2006年Spring2.0正式发布
完全兼容1.0框架,且添加了如数据相关的Repository和AOP相关的Aspect注解,同时支持扩展XML编写,这为XML配置的价值提升了一个阶段
重要版本还是2.5,引入了骨架式的Annotation :
依赖注入Annotation:@Autowired
依赖查找Annotation:@Qualifier
组件声明Annotation:@Component,@Service
Spring MVC Annotation:@Controller,@RequestMapping,@ModelAttribute
@Autowired支持注入SpringBean集合
不建议使用Qualifier,建议使用Resource注解
@Qualifier还可以用于“逻辑类型”限定,例如以下的两个注解都被@Qualifier标注
@LoadBalanced和@ConfigurationPropertiesBinding,可以先经过@Qualifier筛选
支持JSR-250的@Resource注入,当然还支持JSR250的生命周期回掉函数@PostConstruct和@PreDestroy,可以被XML替换掉
尽管2.5版本提供的注解不少,但是仍然摆脱不了XML,主要是因为仍然需要在XML中使用如下标签:用于注册Annotation处理器,还需要使用< context:component-scan>用于寻求需要注册成Spring Bean的类
且在2.0的时候提供了@order注解代替Ordered接口来进行对多个Spring Bean进行排序
虽然Spring2.0时期被称为注解的过渡时代,但是在这个版本Spring MVC却完成了蜕变,官方推荐使用注解的方式代替XML的 方式完成编码
2009年Spring Framework3.0正式发布
被称为注解驱动的黄金时代,Spring Annotation雨后春笋般的出现,体现了Spring官方对替换XML配置的决心
就我感觉这是非常伟大的,因为Spring在XML配置的PropertyEditor上是花了大心思的,并且Spring为了提供与产品的整合刚开始也都是使用的XML配置方式,现在却自己要推倒自己最强势的地方了,有远见的公司
这个阶段引入了@Configuration注解,@Component的另一个“派生”注解
遗憾的是这时候并未直接替代XML元素ComponentScan而是采用了过度注解的方式——@ImportResource和@Import,前者允许导入遗留的XML配置文件,后者允许导入类作为Spring Bean,通常这些类无需标注Spring模式注解如Service等等。
通常@Import和@ImportResource需要和@Configuration注解一起使用
但是被标注的类有谁来引导呢?提出了定制的ApplicationContext——AnnotationConfigApplicationContext注册@Configuration class,然后通过这个@Configuration class上面标注的Inport注解来实现对依赖的导入。
用起来还是感觉非常的别扭
于是在Spring 3.1提出了@ComponentScan注解实现了对XML元素的替换,且这时候就出现了初步的条件注解@profile,如以下用法:
实现对非生产环境下Configuration的注册
在Web方面更是开启了全面的支持,请求处理注解@RequestHeader、@CookieValue和@RequestPart。但是更重要的是开启了REST开发,提供@PathVariable,@RequestBody反序列化请求体,@ResponseBody将处理方法返回对象序列化为REST主体内容,并且@ResponseStattus补充HTTP响应状态
主要还抽象了一套全新并统一的配置属性API,包括配置属性存储接口Environment,配置源抽象PropertySources,奠定了Spring外部化配置的基础,Spring为了简化获取外部化配置 ,提供了@PropertySource简化实现。
其次还支持了缓存抽象,异步支持,检验方面的支持。
即使是这样,Spring作者仍然继续添加“@Enable模块驱动”来实现模块化的Bean装配,例如@EnableWebMVC被标注在Spring Bean上后,RequestMappingHandlerMapping、RequestMappingHandlerAdaptor,HandlerExceptionResolver等Bean就被装配上了,当然这需要手动声明在配置类上,只能算作手动配置,也离自动配置更近了一步
仍然存在缺陷如:@Profile条件注解仍然功能单一太过简单等等
2013年的Spring4.0
注解驱动完善时代
不像Spring3.0版本中注解的大爆发侵入,有的只是完善的注解体系补充
提升装配能力的条件判断,引入了@Conditional注解,以至于曾经的@Profile注解都以Conditional注解的方式实现了一遍
从条件判断注解的完善标志着SpringBoot项目的基础正式打牢
Spring在4.2提出了EventListener作为ApplicationListener的备选方案
使用注解@AliasFor使得注解属性可以使用别名
2017年的Spring5.0
已经作为SpringBoot2.0的核心框架了,还没发行完,万一后面引入了什么新的特性了呢
Spring Framework个个版本引入的核心注解可以查看7.2节
深度展开:
元注解
Spring模式注解
Spring组合注解
Spring注解属性别名和覆盖
元注解:能声明在其他注解上的注解,如@Documented注解可以成为任何注解的元注解,
在Spring世界中@Component就是标准的元注解
Spring注解模式就是特定场景下的注解,如Service之于 Component,因为Java语言层面是不允许注解之间的继承,因此需要通过给元注解特殊化的方式实现注解之间的派生
使用Spring Version模拟自定义@Component派生类:
剩下的就是简单的验证过程了,单有一个地方非常有趣:
在引导类 中出现了如下代码:
来实现Spring对Java8的兼容,测试用例:
主要为了证明派生注解也拥有元注解的作用。
派生性原理:初始化一个Bean定义解析器
componentScan标签的Bean定义解析器为:ComponentScanBeanDefinitionParser
于是开始解析出需要装配的BeanDefinitionHolder,方法摘要如下:
于是对BeanDefinition的加载还要归属到ClassPathBeanDefinitionScanner#doScan方法上去:
还有一次调用:findCandidateComponents的调用栈
然后归结到方法:PathMatchingResourcePatternResolver#getResources方法得到资源集合,这个类在Spring揭秘中见过 ,是Spring提供的资源操作类,解析出Resource[]对象
根据传入的basepackage,获取到所有的Resource然后进行candidate进行筛选
至于筛选的标准,则需要看最开始的ComponentScanBeanDefinitionParser#parse的第八行
关注ClassPathBeanDefinitionScanner对象,其中是含有includeFilters和excludeFilters的
查看他父类的方法ClassPathScanningCandidateComponentProvider:
只需要关注第一行即可,后面是对JSR的支持,默认通过AnnotationTypeFilter指定添加包含@Component的AnnotationTypeFilter实例,而excludeFilters字段为空。
归根结底还是由AnnotationTypeFilter的识别功能来判断是否注册此BeanDefinition到容器中
ClassPathBeanDefinitionScanner允许我们自定义过滤规则,从而实现与Spring的解耦
只需要注入ClassPathBeanDefinitionScanner,他的父类中实现了addIncludeFilter、addExcludeFilter等方法可以直接使用,达到指定注解添加到IoC的目的
Spring并没有将ClassPathBeanDefinitionScanner注入到IoC当中去
自己尝试了下,最新版本的Spring并未使用ClassPathBeanDefinitionScanner
可以参考SpringBootApplication中的ComponentScan注解使用,如下:
因为SpringBootApplication没有提供includeFilter这一属性,我们需要手动代替掉SpringBootApplication注解,然后通过给@ComponentScan加上一个includeFilter属性即可
IDEA也会给出提示的,只要你注入到容器当中就不会报错了。
Spring对多重派生的支持:从3开始支持两层结构的派生,到4支持任意深度的派生,因为SpringBoot1.0是基于Spring4.0的,因此SpringBoot完美继承Spring的多重派生
Spring组合注解:
Spring组合注解中的元注解允许是Spring模式注解与其他Spring功能性注解的任意结合
常见的如:@RestController就是由元注解@Controller和功能注解@ResponseBody组合而成,又如@TransactionService就是由元注解@Service和功能注解@Transactional组合而来
书上举的例子,但是在引入MyBatis的starter的时候并未发现存在该注解
Class对象是类的元信息载体,承载着其成员的元信息对象,包括:字段、方法、构造器以及注解等,于是提供了获取当前类标记的注解 的方法:AnnotationMetadata#getAnnotationAttributes(String)
该方法的底层实现是由Java的反射编程模型作为基础,Java语言默认所有注解实现Annotation接口,被标注的对象用AnnotatedElement表示,通过AnnotatedElement#getAnnotation(Class)方法返回指定类型的注解对象
使用方法:
别想着直接通过ThinkingInSpringBootSamplesApplication去获取@@ComponentScan注解,会找不到的,注解之间不仅仅是相互堆叠的关系,一个注解标注在另一个注解上在语法层面是没有任何意义的,但是Spring的类加载器会递归所有的注解这才导致了内层注解会生效。
上面终归还是指定了获取SpringBootApplication中的哪个属性,完全的反射使用如下:
以上方法调用了所有无参method,但因为其中混杂着Annotation的方法,需要将这些方法排除方可看到SpringBootApplication的方法,将最后一个筛选方法的条件替换掉。
使用Spring提供的注解递归实现:
详细看下第三种方法,使用Spring提供的Utils来进行的操作:
将注解属性抽象成为了AnnotationAttributes,通过这种方式可以直接获取到元注解的AnnotationAttributes,Spring提供的最大便利了。
AnnotationAttributes直接扩展子:LinkedHashMap<String,Object>
获取AnnotationAttributes的时候存在一个覆盖问题,因为使用了@AliasFor注解,那么如果对于RestController来说,是Controller的value属性作数,还是Component的Value属性作数呢 ?因为都有默认值“”,结果发现是派生注解的属性作数,可以覆盖元注解的字段。
存在着显性覆盖和隐性覆盖两种原则:
隐性覆盖:派生注解能够覆盖元注解的同名属性
显性覆盖:标注@AliasFor注解的字段有权利进行覆盖
这都是Spring提供的特性,在Java语言层级是不会出现派生注解,元注解的关系,元注解在Java语言层面是无效的。
当然也会存在同一注解中两个属性相互的AliasFor 情况,要求这两个值必须相等,只要别同时赋值就行了。另外,如果Value属性显式得使用了AliasFor注解去别名其他注解,他的隐式覆盖规则仍然 生效。
第八章、Spring注解驱动设计模式
前面主要集中讨论在单一Annotation例如Component和少数组件上,这一章讲解“@Enable模块驱动”部分,系统地介绍这部分内容
Spring Framework在3.1开始支持@Enable模块驱动
这里的模块是指具备相同领域的功能组件集合,所形成的一个独立的单元,比如Web MVC模块,AspectJ代理模块,Cache模块,JMX模块,Async模块等等
Spring提供的@Enable模块驱动这种设计模式有别于传统的23种设计模式
提供的@Enable注解模块如下图:
这样使用就可以屏蔽组件中集合装配的细节,开发者只需要将Annotation标注在某个Bean上,即可实现装配过程,但是实现该模式的成本较高,也就是用起来爽,但是实现和理解这个功能费劲
依赖于Spring Framework提供的注解@Import,该注解在3.1的时候做出了调整,在3.0的时候仅限导入一个被@Configuration标注的类,但是在3.1及以后增加了导入@Bean方法的类,以及ImportSelector或ImportBeanDefinitionRegistrar
实际测试中直接导入一个普通的类也是可以的
于是将@Configuration类和@Bean方法(如果按照我测试的,应该是任意被Import的类,除去ImportSelector和ImportBeanDefinitionRegistrar)归类为“注解驱动”,而ImportSelector和ImportBeanDefinitionRegistrar的实现类则归类于”接口编程“
注解驱动可以观察@EnableWebMvc注解,模仿他我写一个类似的(以下是基于注解驱动的Spring,感觉那时候老师交Spring都是用XML,反倒忽略了Spring注解驱动带来的便利):
1、@Enable模块:
全部搬运@EnableWebMvc,除了Import,来看看Import
具体的HelloWorld代码:
主测试代码,使用Spring启动的:
这种启动方式即使引入了starter他们的自动配置也无法生效
上面展示的注解驱动,下面的接口编程可能比较复杂
ImportSelector比ImportBeanDefinitionRegistrar容易
使用ImportSelector可以参考@EnableCaching,具体就是要实现:
这个方法,弹性较注解驱动较大,但是Spring实现较少
至于ImportBeanDefinitionRegistrar和ImportSelector类似
实现这个方法即可:
这全部都要依赖于Spring的@Import的自动装配功能
注解驱动模式下的实现:使用XML方式的<Context:component-scan/>
开启的基于注解的自动配置还是使用AnnotationConfigApplicationContext开启的基于注解的配置都会帮我们向容器中注入一个对象——ConfigurationClassPostProcessor帮助我们装载@Configuration和@Bean,他是最高优先级的BeanFactoryPostProcessor实现
在postProcessBeanFactory中的processConfigBeanDefinitions进行了检查
@Component也可以被视为是@Configuration,只是在筛选的时候排除去@Order注解默认的排序是在@Configuration后面的
基于注解的@ImportSelector和@BeanDefinitionRegistrar也是在这个类当中
模块装配类似于汽车的手动挡,自动装配类似于汽车的自动档,两者并未完全排除,而是相互兼容,当然自动装配是简历在模块装配之上的。
来看看Spring Framework提供的自动装配
很多人都以为自动装配是SpringBoot独有的功能
Spring3.1提供了Web自动装配的功能
在3.1引入了WebApplicationInitializer接口用于实现Servlet3.0中的Initializer,后者可用于替换web.xml文件的,这是Servlet3.0所提供的特性,然而Spring在3.1版本将该特性使用Spring的方式进行了封装,使得开发人员更容易去实现,直接实现该接口即可。
当然如果直接使用WebApplicationInitializer接口较为困难,那么可以使用他的抽象实现类:AbstractDispatcherServletInitializer,或者可以再具体一点,去实现AbstractDispatcherServletInitializer的抽象子类:AbstractAnnotationConfigDispatcherServletInitializer。
一般都不会直接使用到WebApplicationInitializer接口,而是使用两个抽象实现类:
具体用例:
AbstractAnnotationConfigDispatcherServletInitializer:
这种配置类似于基于Java Config配置驱动
AbstractDispatcherServletInitializer:
基于XML的配置驱动
全都是向DispatcherServlet中注册信息
基于对Spring Configuration的配置驱动,深入研究这个即可
完全可以在AbstractAnnotationConfigDispatcherServletInitializer#getServletConfigClasses方法中注册进一个包含了@EnableWebMvc和@ComponentScan(Basepackages)的配置类,从而实现自动开启WebMvc和自动开启扫描的功能,从而实现自动装配。
当然单独依靠Spring Framework3.2(两个抽象子类是3.2提出的)是不足以Web自动装配的能力,因为Spring Framework3.1中提出的接口是基于Servlet3.0的规范,从而达到在开启Servlet容器的时候自动配置进我们的配置类
Servlet3.0打破了原来的各种Servlet,filter,listener必须注册进web.xml文件的传统,实现了更高的灵活性,因为web.xml不支持占位符也不支持条件运算,绝对的静态文件。
在Servlet3.0支持以编程的方式配置Servlet,Filter,Listener,通过操作ServletContext对象来完成,且方法摘要非常简单:
当然这还远远达不到自动装配的目的,但是给了第三方框架例如Spring MVC的很大机会,Spring MVC允许通过注解的方式标识Servlet,Filter,Listener,然后通过反射获取类信息,调用ServletContext的add方法将其注册进Servlet中。这些方法只会在容器初始化时候被调用,Spring实现了Servlet提供的Initialization接口。
Spring的条件装配,在前面已经提到过,有3.1开始逐步引入的@Profile和@Conditional注解
在没有这个注解之前,XML也是没有提出使用Profile属性的,那么那时候是如何对不同的生产环境实现配置的单独处理的呢?
就好似现在的SpringBoot项目,默认是读取application.properties,也可以读取appliation-{env}.properties,从而实现不同环境下的配置文件需求,只不过当时是XML文件而不是properties文件
而在Profile注解出来之后,容器会根据Profile注解来决定该配置类(也有可能是Component的其他派生注解标注的类)是否会加载到IoC中来,判断条件也是十分简单,只要实现简单的Profile比对即可。
已知的Bean注册方式有两大类:注解驱动和传统XML配置驱动,对应的配置进行方式分别是@Profile和<beans profile="">
具体的加载方式在8.3.3,P277
里面有全程的预加载BeanDefinition过程,有多种处理方式
XML的Profile判断则相对单一
Spring4.0提出的@Candidational具有更大的弹性,前面的Profile像是静态的激活,而Conditional更倾向于运行时候的动态选择
其中允许指定一个或者多个Condition,当所有condition都满足的时候才会匹配成功
可以自定义实现Conditional注解,参照283页,也可以参照Profile的实现
在Spring4.0也就是Conditional注解出来的时候使用了@Conditional作为其元注解来实现的。
具体的内部实现使用过ConditionEvaluator#shouldSkip方法来判断是否应该跳过当前被标注的类,返回true表示遇到了不匹配的实例,应该跳过
然后再去覆盖原来Profile的判断逻辑,改变成对这个方法的调用,实现条件判断的统一
当然尽管Spring Framework付出了如此多的努力,但是仍然不是特别理想,如存在以下问题:
需要将@Enable注解标注在配置类上,并且配置类需要注册进容器中去
Web自动装配是依赖于外部Servlet3.0+容器中,如果换了容器就失去了该功能
正因为这些原因,驱动着SpringBoot项目的出现,最终被SpringBoot的自动装配和嵌入式Web容器所解决。
虽然自动装配是SpringBoot的特性,但是其在Spring Framework中的部分场景中还是有用的。
第九章、Spring Boot自动装配
当然其中可能还有一系列问题如:需要整合 Spring注解编程模型,@Enable模块驱动手动配置 及条件装配等Spring Framework原生特性,这种技术即是Spring Boot自动装配。
Spring Boot的自动装配可以从@SpringBootApplication注解说起:
通过该注解可以激活@EnableAutoConfiguration,该类应该是使用 了Spring的@Enable特性,但是这个注解绝对是Spring Boot独有的
另外还激活了@SpringBootConfiguration,相当于就是一个@Configuration的简单派生类
至于@ComponentScan,则是Spring Framework4.0所提供的注解,没有什么区别
理解@EnableAutoConfiguration
Spring Boot项目会根据你 应用的依赖来尝试自动配置,如果要开启这种尝试,可以在配置类上面加上@EnableAutoConfiguration
实际上完全可以使用@EnableAutoConfiguration在启动类上就可以开启自动配置了,完全不依赖于 @Configuration,至于为什么推荐 使用@SpringBootApplication,估计是因为免得再去指定一个ComponentScan注解,尽量减轻 开发人员的记忆负担。
并且在随后官方文档指出Spring Boot自动装配不是侵入式的,开发人员可以定义自己的配置类来覆盖掉自动配置,可以理解为自定义的配置优先级高
实际上会使用@Conditional来判断你是否进行了配置,如果没有进行配置则Conditiona为true,自动配置生效,否则自动配置失效
排除自动配置:
内部配置:@EnableAutoConfiguration.exclude()或者excludeName(使用场景:If the class is not on the classpath)
外部配置:spring.autoconfigure.exclude
可以简单猜测下实现原理:@EnableAutoConfiguration通过ImportSelector实现选择性的导入,通过@Value注解可以获取到外部配置的exclude,内部配置可以通过AnnotationMetadada获取
内部实现就得看@EnableAutoConfiguration所Import的AutoConfigurationImportSelector了
ImportSelector的核心方法:
将核心代码都封装到了getAutoConfigurationEntry方法中,再进去:
可以根据方法名猜个大概了
装配哪些组件可以查看:getCandidateConfigurations方法
排除哪些组件的自动配置:getExclusions方法
getCandidateConfigurations内部最终依赖的是SpringFactoriesLoader#loadFactoryNames方法,该方法内部的加载逻辑:
1、搜索ClassLoader下所有的META-INF/spring.factories资源内容
2、读取spring.factories文件,以Map方式格式化
3、解析2中返回的Map,将Value,也就是权限定类名所确定的类注册进IoC容器当中去
在官方文档的4.29.2. Locating Auto-configuration Candidates当中
至于getExclusions就非常简单的,结合AnnotationMetadata来实现
继续探讨AutoConfigurationImportSelector的筛选后续,在读取完所有的Class之后,调用了fireAutoConfigurationImportEvents方法用于事件的自动装配
剩下的@EnableAutoConfiguration 也是讲的排序等的组件装配
排序分为绝对排序和相对排序,绝对排序提供了AutoConfigureOrder(注意:并不是Order的派生注解),相对排序提供AutoConfigureBefore和AutoConfigureAfter注解,都是元注解。
在使用注解的时候尽量不要使用Value属性,而是使用其更加具体的属性,因为Value太过于通用,说不定以后就指定成别的属性的别名了。
自定义SpringBoot自动装配
其中有很多不成文的规则,无论 是Spring还是第三方如Mybatis都在共同遵守着:
自动装配Class命名 均以 AutoConfiguration结尾
包命名规则均以:
知道了命名规则后便可以开始自定义starter
4.29.5. Creating Your Own Starter可以 参考
建议民间使用
Spring官方则采用以下命名方法:
当然使用starter很简单,但是专业性较强的starter却要求Configuration类需要标注一系列的@Conditional用来保证运行环境的正确,Spring官方提供的条件注解有:
可以查看4.29.3. Condition Annotations
条件注解不仅可以标注在类上,还可以标注在@Bean方法上用于判断是否将该值注册IoC当中去
Class Conditions
Bean Conditions
Property Conditions
Resource Conditions
Web Application Conditions
SpEL Expression Conditions
Class Conditions:
主要有一对反义注解——@ConditionalOnClass和@ConditionalOnMissingClass,分别表述在指定类存在时和在指定类缺失时的语义
前面推荐不要使用注解中的Value属性,这里就出现了,@ConditionalOnMissingClass的Value属性变化多样,在SpringBoot版本 升级的过程当中,且存在破坏性升级
主要是避免这种情况的发生: 因为SpringBoot中间铲平的Maven依赖都有可能声明成optional为true的形式,这样是为了避免冲突,也进一步减少包的体积,然而这就会的熬制一个问题,如果SpringBoot最终产品没有包含这个Jar包,就会出现Class找不到的情况,这个注解就是用于判断Class是否存在,从在给指定场景来自动配置的。
非常容易理解,autoconfig项目中在原编码中肯定都包含了各种starter的AutoConfiguration,最后他会根据我们SpringBoot最终产品来决定执行哪些自动配置
难怪有时候看依赖的第三方库里面好多红色的,原来是通过指定optional为true来实现的
Bean Conditions:
也是成对出现的——@ConditionalOnBean和@ConditionalOnMissingBean
仅仅匹配BeanDefinition中的Bean种类和名称
底层依旧是基于@Conditional+SpringBootCondition实现类OnBeanCondition
这里主要是防止自动配置覆盖了由开发人员主导的外部化配置
当外部化配置存在的时候,该自动配置就不应该生效了。
也等价于当外部配置不存在的时候自动配置才会生效
完美契合@ConditionalOnMissingBean的语义,当然这里还需要结合@ConditionalOnClass来实现,避免因为依赖的关系导致运行过程中报错。
只整合这两个注解就会成倍地增加项目的复杂度,当引入更多注解时候只会更加复杂,当然是先的功能也会更加丰富
Property Conditions:
主要就是@CoditionalOnProperty注解,配置来源于Java系统变量,环境变量和application.properties都是PropertySource的来源,也是该注解基于的环境配置
该注解摘要:
对应的配置:
这样就可以根据application.properties来决定是否要开启自动配置了,如:@ConditionalOnProperty(prefix="git-config",name="enabled",havingValue="true")
这样只有在声明git-config.enabled=true 的时候才会满足配置条件
当然这样就会非常麻烦,每次都要去手动开启,如何省略掉这一步骤呢?使用其提供的matchIfMissing属性,将其设置成true,这样当只要该PropertySource不存在时候即可开启自动配置,需要关闭的时候也只需要手动声明enabled=false即可,然后提供自己的配置,这也正是很多框架所使用的方法。
最常见的例子就是Spring AutoConfiguration包下提供的:
Resource Conditions:
核心租借@ConditionalOnResource,该注解摘要:
这部分实现的Condition接口还是比较复杂的,先跳过了
默认使用的ResourceLoader就是ApplicationContext,如果传入了的话就是用传入的ResourceLoader
这里需要扩展Resource用来对classpath路径下的资源扫描,即 Spring提供的ClassPathContextResource
我们使用的时候就可以这样使用该注解@ConditionalOnResource(resource="META-INF/spring.factories")
,当这个资源存在的时候,被标注的 类才会生效
Web Application Conditions:
提供一对注解:@ConditionalOnWebApplication和@ConditionalOnNotWebApplication
由于Spring5.0对Web Flux的支持,@ConditionalOnWebApplication增加了TYPE字段用于对Web容器的筛选,有:ANY、SERVLET、REACTIVE。默认选择ANY
内部的判断逻辑实现就跳过了
SpEL Expression Conditions:
Spring默认提供的@Conditional注解还是相对单一,虽然实现Condition接口可以实现在自定义的@Conditional注解,非常的实用,但是成本有点高,Spring为了降低使用成本,整合了SpELl表达式的Conditional方式,SpELL适用于Spring旗下的所有产品
具体的文档需要在Spring Framework的Core章节中
核心注解:@ConditionalOnExpression注解中的Value字段,该字段支持使用SpEL表达式并进行真伪的评判 ,默认是true
由于刚开始并不支持,所以 SpringBoot内部大部分都是使用的是@ConditionalOnProperty来代替了@ConditionalOnExpression,但是我们在实际使用情况中往往可能是SpEL表达式更加的 便利。例如前面的jmx的enabled属性,可以使用如下注解来表示:@ConditionalOnExpression("${spring.jmx.enabled:true}")
但是也间接的存在一个问题:无法达到不指定enabled默认是启用的情况,然而却支持复杂条件的交并补运算,扩展性不强,不怎么建议使用。
总结
在本部分花了大量时间讲解了 Spring Framework对自动装配所做出的努力,尤其是注解驱动方面的努力。是Spring Boot自动装配的基石。
Spring Framework框架的兼容性比SpringBoot框架优秀太多,主要是因为SpringBoot框架在2.0版本额API破坏性升级导致很多原来的项目无法复用,降低了对Spring社区的信任度,当然这样做都是有有原因的,仍然无法否认SpringBoot是一款优秀的框架,AutoConfiguration的特性更是使得开发变得简单。
Spring Framework启动时将当前应用通过将ClassPathXmlApplicationContext装配到Servlet容器中进而完成Servlet的启动,在Spring Boot时代使用SpringApplication#run()或者是SpringApplicationBuilder#run()来启动,(通过后者启动可以夹带一些配置进去,但通常不建议这么做)配合@SpringBootApplication或者EnableAutoConfiguration注解完成。
那么SpringApplication和SpringApplicationBuilder底层的实现原理是什么呢?这就是第三部分理解SpringApplication的重点。
第三部分、理解SpringApplication
在Spring Boot中使用的SpringApplication是全新的Spring应用API
因此从本章开始才算是对Spring Boot的功能特性的讨论。
本部分抛开官方文档中SpringApplication部分(4.1),因为里面都是使用方法的介绍,完全没有深入到如何启动,于是本章采用:
Spring Application初始化阶段
SpringApplication运行阶段
SpringApplication结束阶段
SpringBoot应用退出
的逻辑展开
第十章、SpringApplication初始化阶段
初始化阶段属于运行前的准备阶段,可以在run方法之前指定该容器是web容器还是非web容器,还可以进行Banner输出的调整,配置默认属性的方式。
大体上的准备阶段由两部分组成:构造阶段和配置阶段。
Spring Application构造阶段:
当然一般开发者都不会与构造器打交道,一般使用SpringApplication.run方法来启动程序
实际上是在内部构建了一个SpringApplication对象,然后调用了该对象的run方法
primarySource(主配置类),在Spring Boot2.0引入的,通常这些类不是被标注@SpringBootApplication就是标注@EnableAutoConfiguration
先分析核心代码的构造阶段
再继续看SpringApplication的初始化过程:
然后就是对上述方法的底层描述展开:
存储推断出的Web类型方法实现:
通过ClassUtils#isPresent方法判断类的存在性情况从而判断出Web应用类型
加载Spring应用上下文初始化器:
其中先委派给了方法getSpringFactoriesInstances:
其中SpringFactoriesLoader.loadFactoryNames会读取META-INF中的以ApplicationContextInitializer为key的内容:
如在spring-boot-2.3.3中
根据Order排序后返回
最终在装配上下文初始化器的时候:
是直接实现的覆盖性装配,意思是在Application.run方法之前装配的上下文初始化器会直接被覆盖掉
然后就是:
装配应用程序监听器Listener
装载逻辑与上面的Initializer十分类似,也是获取spring.factories中的Listener:
并也实行覆盖性装配:
于是到了SpringApplication构造的最后一步:
推断应用的引导类。
我们传入的类仅仅是作为参数primarySource,并不是引导类,引导类是SpringBoot内部代码实现判断的
实现起来非常简单,是对当前线程执行栈进行递归调用,看哪个类包含main方法便返回该类。
这样我们就可以在run的时候不用传递引导类了,可以传入核心配置类。
再来回顾下核心代码摘要:return new SpringApplication(primarySources).run(args);
既然构造阶段加载完成了,那么该进入运行阶段了,但是这个逻辑是建立在我们使用如下代码:
的情况下的。
如果我们也是这样在main函数中使用:
于是在创建SpringApplication和启动SpringApplication中间便可以多了很多步对SpringApplication的定制,这是Spring提供给我们的。
具体的定制内容可以看4.1.4
或许new SpringApplication过于繁琐,Spring在4.1.5中提供了SpringApplicationBuilder来提升API的便携性
对SpringApplication的简单定制:
使用SpringApplicationBuilder来完成:
使用:
定制Spring Boot提供的配置源:主要是@Configuration class,XML配置文件和package
摘要:
至于source的解析过程以后再看
调整外部化配置:实际上就是通过方法:
来添加 application.properties之外的配置
当这些配置完成后,Spring Boot应用进入下一阶段
第十一章、SpringApplication运行阶段
依旧围绕着核心代码:
上一章已经说了SpringApplication的初始化阶段和Spring提供给我们的自定义配置阶段。
现在来看SpringApplication的运行阶段
属于Spring Boot的绝对核心,该过程是结合这SpringApplication初始化完成的状态,进一步完善运行时候需要准备的资源,随后启动Spring应用上下文,在此期间伴随着Spring Boot和Spring事件的触发,形成完整的SpringApplication生命周期。
于是又可以粗略的划分成三个阶段:
SpringApplication准备阶段
ApplicationContext启动阶段
ApplicationContext启动后阶段
先展示完整的运行代码:
SpringApplication准备阶段从第一行开始,到第十八行refreshContext方法调用的前
看前面书上的例子都是使用context.refresh方法来启动容器的
可以先从SpringApplicationRunListeners开始,其中持有很多个SpringApplicationRunListener的引用,从而实现对一系列的SpringApplicationRunnerListener的管理,对SpringApplicationRunListeners的理解于是递归到了SpringApplicationRunListener的理解
可以简单得理解为Spring Boot的运行时监听器,内部方法说明(监听方法的返回值都是void,因为不可能靠返回参数记录信息):
至于注册进这些SpringApplicationRunListeners的SpringApplicationRunListener来源于哪里,则可以进入方法getRunListeners:
方法getSpringFactoriesInstances结合SpringFactoriesLoader机制,SpringApplicationRunListener内建实现可以轻松被定为到:
确实,SpringApplicationRunListener接口只有唯一实现类EventPublishingRunListener
这里的SpringApplicationRunListener是SpringBoot提供的事件监听机制,与Spring提供的ApplicationListener有所区别
Spring底层依赖的事件/监听机制在JDK1.0中就已经实现了,也可以视为是观察者模式的一种特例,观察者模式允许Observable和Observer收发消息,但是现在已经不支持使用了,允许携带对象Object,但是事件监听模式只允许携带EventObject实现类,于是ApplicationEvent可以扩展EventObject接口,同时事件的监听者也必须是EventListener实例,当然其中使用非常不方便,于是Spring提供了ApplicationListener来对特性的Event进行监听(通过泛型的方式指定),但是通过泛型监听事件就会出现一个局限——无法同时对两种事件进行监听。
于是Spring3.0提出了SmartApplicationListener接口,通过supportsEventType方法过滤出满足条件的Event了。
如:监听application.properties的事件监听器ConfigFileApplicationListener就实现了SmartApplicationListener接口。
然而目前讨论的范围仅限于ApplicationEvent和ApplicationListener。
就目前来讲使用ApplicationEventPublisher和使用SimpleApplicationEventMulticaster来发布ApplicationEvent没有发现任何的区别,且在使用后者时候IDEA还会自动报错【误报】。
但是后者有一个前者没有的特性:增删ApplicationListener
当然不怎么好用,增加进的Listener好多Event都没有扫描出来,建议还是使用@EventListener注解。
实际上ApplicationContext就实现了ApplicationEventPublisher接口。
还为编程式Spring提供了ApplicationEventPublisherAware接口用于对ApplicationEventPublisher实现在生命周期回掉时候的赋值操作。
实际上ApplicationContext内部的事件发布就是由依赖于ApplicationEventMulticaster的multicastEvent方法
实际上前面提到的ApplicationEventMulticaster的注册ApplicationListener实例的方式也被ConfigurableApplicationContext中的addApplicationListener方法复用。
这个ConfigurableApplicationContext是ApplicationContext的子类,于是我分别注入ApplicationContext和ConfigurableApplicationContext,发现他们两个是一个东西。
最后为了弄清楚程序中使用的ApplicationContext的继承关系,我获取了使用的是AnnotationConfigApplicationContext类,其类继承图如下所示:
所以到此为止完全可以使用该ApplicationContext去代替SimpleApplicationEventMulticaster。
于是我们可以仅关注ApplicationListener和ApplicationEvent即可(当然要注意顺序问题,需要保证事件Listener注册操作在事件发布操作之前)。但是这样做比使用@EventListener注解监听来的事件要少一些:
这么做的监听结果:
@EventListener监听结果:
第二行是输出的INFO日志信息。
建议以第二个为准,至于发布信息可以与ApplicationContext打交道
应该是ApplicationRunner的调用时机是需要容器完全准备好才能执行的
其实第二个输出也不全面,因为被EventListener标注的方法也不是在容器启动之初就作为Listener就注册到IoC当中的,最全面的方法应该是这样:
明确指定在run之前就加载进Listener,也就是在初始化阶段进行配置
Spring内建事件:
ContextRefreshedEvent:Spring ApplicationContext就绪事件
ContextStartedEvent:Spring ApplicationContext启动事件
ContextStoppedEvent:Spring ApplicationContext停止事件
ContextClosedEvent:Spring ApplicationContext关闭事件
最常见的就是Spring ApplicationContext就绪事件
下面开始逐一分析:
ContextRefreshedEvent:
具体的发布时间需要追溯到AnnotationConfigApplicationContext的refresh方法,而此方法是由其父类AbstractApplicationContext默认实现的:
随着容器的启动,到达第39行的时候,会在里面调用publishEvent方法来发布一个ContextRefreshedEvent事件,并把自身作为 Source传入进去。
到此时Spring ApplicationContext中的Bean已经完成了初始化,并能够投入使用。
ContextStartedEvent和ContextStopedEvent:Spring应用上下文的启动停止事件
非常罕见,只会在AbstractApplicationContext中的start方法和stop方法中发布
出现了问题,在Spring-framework-5.2.8中我在start方法第一行打上了端点,然而程序并没有执行到那儿去,反倒是输出了对应的ContextStartedEvent,先按照书上的来。(后面给了解释)
假设当start方法被调用后,其关联的LifeCycleProcessor实例将触发所有LifeCycle Bean的start方法调用,stop方法同理。这两个方法不仅仅是简单的发布Event那么简单,这里面涉及到LifeCycle Bean方法的回调。
书上继续说道:AbstractApplicationContext尽管是LifeCycle实现类,但其并不是LifeCycle Bean,因此他的start和stop方法不会被调用。
并且ContextStartedEvent和ContextStoppedEvent的传播必然在ContextRefreshedEvent之后
最后一个事件:ContextClosedEvent
与refresh正好相反,他对应的是AbstractApplicationContext的close方法,内部调用了被protected修饰的doClose方法:
可以明显的看到事件的发布是在destroyBeans之前的,destroyBeans会回掉LifeCycle的onClose方法,并销毁当前Spring ApplicationContext。
这四个Spring提供的内购事件都是ApplicationContextEvent的子类,更是ApplicationEvent的子类ApplicationEvent
构建自己的事件非常简单,实现ApplicationEvent接口,将该类交给ApplicationEventPublisher即可,其实AbstractApplicationContext就是ApplicationEventPublisher的直接子类,于是可以直接和ApplicationContext打交道了
接下来讲述了Spring Framework在4.2引入的@EventListener注解实现对Event的监听功能。
@EventListener官方没有非常详细的在文档给出说明,以下直接给结论:@EventListener方法必须是Spring Boot中的public方法,并支持返回类型为非void的情况(价值几乎为零),当他监听一个或者多个ApplicationEvent的时候,其参数可以为零个或者是一个ApplicationEvent。
@EventListener方法异步化的支持:只需要在当前注解上再加入一个@Async注解即可。
记得要开启Async
验证方法非常简单:
基于注解监听和基于编程监听的对比:
接下来讲述@EventListener方法的实现原理。
在AnnotationConfigUtils#registerAnnotationConfigProcessors(BeanDefinitionRegistry,Object)方法中存在着@EventListener方法处理的相关Bean注册
完整的EventListener的装载过程:
1、使用AnnotatedElementUtils.findMergedAnnotation(method,EventListener,class)方法筛选出所有被@EventListener标注的方法,当然@Async标注是被AOP增强的,于是可以进行异步增强
2、随后EventListenerMethodProcessor完成对所有扫描出来的@EventListener的装配,是将@EventListener转换成为ApplicationListener然后完成注册
Spring部分事件发布完结
Spring Boot事件部分。
在1.4版本Spring Boot事件处理机制发生了改变
在这之前是依赖的Spring Framework的事件处理机制
浏览一遍吧,现在还没有达到熟练使用Spring Boot的地步,就开始要分析Spring Boot事件原理了
讲着讲着又讲到了Spring中去了,有点晕,到底有多少个阶段呢?有点没有理清楚
执行CommandLineRunner和ApplicationRunner
在4.1.10中有详细的介绍
他们两的run方法会在SpringApplication.run方法完成之前执行
当出现多个ApplicationRunner或者CommandLineRunner时候,需要手动通过@Order注解来控制他们的执行顺序,默认应该是最后执行的(仅仅是猜测,取值为Integer.MAX_VALUE)
就形式上来看,两者除了核心方法的传入参数不同其他都基本相同,那有什么引进的必要呢?
其实也就是时代产物吧,CommandLineRunner在Spring Boot1.0的时候就引入了,然而在SpringBoot2.0ApplicationRunner才引入,为了尽可能向前兼容就没有移除CommandLineRunner
第十二章、Spring Application结束阶段
SpringApplication的结束阶段在SpringBoot1.0和2.0的版本的实现逻辑是相对稳定的
再次回到SpringApplication中的run方法:
在第33行进入结束阶段,如在执行running方法的时候抛出异常便会异常结束
至于异常分析,主要是依靠FailureAnalyzer和FailureAnalysisReporter
当SpringApplication执行完成后便进入了退出阶段,该阶段还是依赖于JVM提供的特性
第十四章、SpringBoot应用退出
对应文档章节:4.1.11. Application Exit
主要是通过JVM的shutdown hook去关闭ApplicationContext,并且保证Bean生命周期正确执行销毁操作,如标注的@PreDestroy或者是实现了DisposableBean接口的Bean
主要是借助着AbstractApplicationContext的registerShutdownHook方法来完成的
可以定制退出码,如以下程序所示:
但是在实践中几乎用不到,因为不会手动调用Context的exit方法,从而得到返回码
本小节可以作为ApplicationContext的一个LifeCycle来看待
本部分(第三部分)是围绕着SpringApplication来展开论述的,主要分为:“初始化”,“运行”和“结束”三个阶段,主要包括的核心特性为SpringApplicationRunnerListener,Spring Boot事件和Spring应用上下文的生命周期管理等
还有一个疑问:为什么要提出SpringApplication,直接使AnnotationConfigApplicationContext启动ApplicationContext不好吗?
小马哥给出的解释是对Spring Framework的应用上下文的补充。
传统的Spring应用上下文起源于ConfigurableApplicationContext对象的创建,运行则由refresh方法,终止于close方法,在refresh方法调用过程中伴随着BeanFactory,Environment,ApplicationEventMulticaster(事件广播使用底层)和ApplicationListener的创建,不过在SpringBoot中SpringApplication让其在refresh方法执行之前就执行了
完结~
最后更新于