Spring Boot 是由 Pivotal 团队提供的全新框架,其设计目的是用来简化新 Spring 应用的初始搭建以及开发过程。该框架通过约定由于配置的原则,来进行简化配置。Spring Boot致力于在蓬勃发展的快速应用开发领域成为领导者。Spring Boot 目前广泛应用与各大互联网公司,有以下特点:
- 创建独立的 Spring 应用程序
- 嵌入的 Tomcat,无需部署 WAR 文件
- 简化 Maven 配置
- 自动配置 Spring
- 提供生产就绪型功能,如指标,健康检查和外部配置
- 绝对没有代码生成,对 XML 没有要求配置
并且 Spring Boot 可以与Spring Cloud、Docker完美集成,所以我们非常有必要学习 Spring Boot 。并且了解其内部实现原理。通过本次分享,您不仅可以学会如何使用 Spring Boot,还可以学习到其内部实现原理,并深入理解:
- Spring Boot 项目结构,starter 结构
- 常用注解分析
- Spring Boot 启动过程梳理(含:Spring 事件监听与广播;自定义事件; SpringFactoriesLoader 工厂加载机制等)
- 自定义 starter
- 自定义 condition
1. 项目初始化过程
springboot启动类
springboot启动类非常简单,就一句话:
要想了解springboot的启动过程,肯定要从这句代码开始了。
跟进去可以看到有两步,一个是初始化,一个是run方法的执行:
先看初始化方法:
SpringFactoriesLoader工厂加载机制
上面代码中 ,Initializers和Listeners的加载过程都是使用到了SpringFactoriesLoader
工厂加载机制。我们进入到getSpringFactoriesInstances
这个方法中:
配置文件中的内容:
了解过dubbo的同学会觉得这个非常熟悉,是的,没错,和dubbo中的扩展点有异曲同工之妙。
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
方法的执行内容入下,下图中展示了我们自定义的一个配置类的加载过程(后面自定义starter的实现一节中会讲):
ApplicationContextInitializer的类图:
下面是listener的类图(太多了,不全,只列出了部分)
总结初始化initialize过程
- 判断是否是web应用程序
- 从所有类中查找META-INF/spring.factories文件,加载其中的初始化类和监听类。
- 查找运行的主类 默认初始化Initializers都继承自ApplicationContextInitializer。
默认Listeners有:
run方法:
spring事件
自定义spring事件
spring事件为Bean与bean之间的通讯提供了支持。我们可以发送某个事件,然后通过监听器来处理该事件。
spring事件继承自
ApplicationEvent
, 监听器实现了接口ApplicationListener<E extends ApplicationEvent>
下面我们自定义一个事件和监听器
编写一个controller方法:
启动服务,访问testPublishMsg
接口,就可以在控制台看到日志打印:MyListener收到了MyEvent的消息:。。。。。
springboot启动过程中的事件广播
SpringApplicationRunListener类图:
上述run过程广泛应用了spring事件机制(主要是广播)。上述代码中首先获取SpringApplicationRunListeners。这就是在spring.factories
文件中配置的所有监听器。然后整个run 过程使用了listeners的5个方法,每个方法对应一个事件Event:
- starting() run方法执行的时候立马执行;对应事件的类型是
ApplicationStartedEvent
- environmentPrepared()
ApplicationContext
创建之前并且环境信息准备好的时候调用;对应事件的类型是ApplicationEnvironmentPreparedEvent
- contextPrepared()
ApplicationContext
创建好并且在source加载之前调用一次;没有具体的对应事件 - contextLoaded()
ApplicationContext
创建并加载之后并在refresh之前调用;对应事件的类型是ApplicationPreparedEvent
- finished() run方法结束之前调用;对应事件的类型是
ApplicationReadyEvent
或ApplicationFailedEven
SpringApplicationRunListeners
是SpringApplicationRunListener
的集合,SpringApplicationRunListener
只有一个实现类:EventPublishingRunListener
,在这个实现类中,有一个SimpleApplicationEventMulticaster
类型的属性initialMulticaster
,所有的事件都是通过这个属性的multicastEvent
方法广播出去的。以environmentPrepared()
方法为例,展示一下环境变了的加载过程:
上述广播了一个ApplicationEnvironmentPreparedEvent
事件。我们知道所有的事件都会被监听器捕获处理,spring的监听器都是ApplicationListener
的子类。根据事件的类型,找到处理这个时间的监听器类ConfigFileApplicationListener
:
ConfigFileApplicationListener
类中可以看到一些其他有用的信息:[DEFAULTSEARCHLOCATIONS = “classpath:/,classpath:/config/,file:./,file:./config/”][ACTIVEPROFILESPROPERTY = “spring.profiles.active”]等等。
OK,spring的事件机制以及springboot启动过程中的事件广播机制讲完了。
FailureAnalyzers错误分析器
查找并加载spring.factories中所有的org.springframework.boot.diagnostics.FailureAnalyzer。
FailureAnalyzers的类图:
当启动过程中发生错误,会广播事件并且执行分析器。 目前主要有analyze和report两个方法,用途是打印日志。
比如:当我们的启动类DemoApplication
不是放到最外层的时候,报错,这就是FailureAnalyzers
中打印出的结果:
refresh过程
比较复杂,主要是spring的加载过程,本文的目标是springboot而不是spring,所以不再深入,我会专门写篇文件介绍。现在只需要了解:
- spring启动过程
- bean的加载初始化都是在该方法中完成
afterRefresh与CommandLineRunner、ApplicationRunner
在springboot启动过程中的run方法的最后,有一句·afterRefresh(context, applicationArguments);·。主要是执行·ApplicationRunner·和·CommandLineRunner·两个接口的实现类,这两个类都是在springboot启动完成后执行的一点代码,类似于普通bean中的init方法,开机自启动。 两者唯一不同是获取参数方式不同,后面的小例子会讲。callRunners的源码:
如果有类似的需求:springboot启动完成后执行一点代码逻辑,则可以通过实现上述两个类来完成。下面写个小例子: 定义4个类:
执行结果:
结果分析:
CommandLineRunner
与ApplicationRunner
的实现类是在springboot初始化完成后执行的- 可以设置执行的顺序,数字越小,优先级越高
- 两者都可以从外界获取参数。唯一不同是:
CommandLineRunner
的参数类型是字符串数组。而ApplicationRunner
的参数类型是ApplicationArguments
。它可以解析--a=1 --b=2类型的参数为key-value形式。
@order
上述过程中,我们看到了好几个地方使用到了order,比如CommandLineRunner
与ApplicationRunner
,初始化器Initializer,监听器Listener。
order比较简单,主要是为了控制这些实现类的执行顺序。规则是数值越小,优先级越高。
在通过spring工厂模式,从spring.factories
中加载这些实现之后,会通过AnnotationAwareOrderComparator.sort(instances);
方法来进行排序。
总结启动run过程
- 注册一个StopWatch,用于监控启动过程
- 获取监听器SpringApplicationRunListener,用于springboot启动过程中的事件广播
- 设置环境变量environment
- 创建spring容器
- 创建FailureAnalyzers错误分析器,用于处理记录启动过程中的错误信息
- 调用所有初始化类的initialize方法
- 初始化spring容器
- 执行ApplicationRunner和CommandLineRunner的实现类
- 启动完成
springboot中重要的注解
前面讲到了@order等注解,下面看一下springboot中最重要的、最常用的几个注解。
@SpringBootApplication
他是一个组合注解:
@Inherited
Inherited作用是,使用此注解声明出来的自定义注解,在使用此自定义注解时,如果注解在类上面时,子类会自动继承此注解,否则的话,子类不会继承此注解。这里一定要记住,使用Inherited声明出来的注解,只有在类上使用时才会有效,对方法,属性等其他无效。
@SpringBootConfiguration
源码:
@SpringBootConfiguration
继承自@Configuration
,二者功能也一致,标注当前类是配置类,并会将当前类内声明的一个或多个以@Bean注解标记的方法的实例纳入到srping容器中,并且实例名就是方法名。
@EnableAutoConfiguration
@EnableAutoConfiguration
用于启动自动的配置,是springboot的核心注解。上面import了EnableAutoConfigurationImportSelector
,这个类继承自AutoConfigurationImportSelector.AutoConfigurationImportSelector
是关键类。
EnableAutoConfigurationImportSelector
类使用了Spring Core包的SpringFactoriesLoade
r类的loadFactoryNamesof()
方法。 SpringFactoriesLoader
会查询META-INF/spring.factories
文件中包含的JAR文件。
当找到spring.factories
文件后,SpringFactoriesLoader
将查询配置文件命名的属性org.springframework.boot.autoconfigure.EnableAutoConfiguration
的值。
然后在spring启动过程中的refresh方法中进行真正的bean加载。
@ComponentScan
@ComponentScan
,扫描当前包及其子包下被@Component
,@Controller
,@Service
,@Repository
注解标记的类并纳入到spring容器中进行管理。这也是为什么我们的启动类DemoApplication
要放到项目的最外层的原因。
springboot自动化配置原理及自定义starter
前面的文章已经讲了springboot的实现原理,无非就是通过spring的condition条件实现的,还是比较简单的(感谢spring设计的开放性与扩展性)。
在实际工作过程中会遇到需要自定义starter的需求,那么我们接下来就自己实现一个starter。
先看一下目录结构:
- MyConfig是自定义的配置类
- HelloService是自定义的bean
- HelloServiceProperties是自定义的类型安全的属性配置
- MEYA-INF/spring.factories文件是springboot的工厂配置文件
本项目就是自定义的starter。假设我们这里需要一些配置项,使用者在使用该starter时,需要在application.properties
文件中配置相关属性。这里我们使用了@ConfigurationProperties
来将属性配置到一个POJO类中。这样做的好处是:可以检测数据的类型,并且可以对数据值进行校验,详情请参考我的另一篇博客:
HelloServiceProperties
类内容如下:
这样使用这个starter的时候,就可以配置hello.a=**
来设置属性了。
HelloService
是我们的bean,这里实现比较简单,获取外部的属性配置,打印一下日志。内容如下:
接下来是比较重要的配置类MyConfig,前面已经讲过,springboot是通过条件注解来判断是否要加载bean的,这些内容都是在我们自定义的配置类中来实现:
@Configuration
表明这是一个配置类@EnableConfigurationProperties(HelloServiceProperties.class)
表示该类使用了属性配置类HelloServiceProperties
initHelloService()
方法就是实际加载初始化helloServicebean的方法了,它上面有三个注解:
@ConditionalOnProperty(prefix="hello", value="enabled", matchIfMissing = true): hello.enabled=true
时才加载这个bean,配置没有的话,默认为true,也就是只要引入了这个依赖,不做任何配置,这个bean默认会加载。@ConditionalOnClass(HelloService.class)
:当HelloService这个类存在时才加载bean。@Bean
:表明这是一个产生bean的方法,改方法生成一个HelloService的bean,交给spring容器管理。
好了,到这里,我们的代码已经写完。根据前面讲的springboot的原理我们知道,springboot是通过扫描MEYA-INF/spring.factories
这个工厂配置文件来加载配置类的, 所以我们还需要创建这个文件。其内容如下:
上面的\
是换行符,只是为了便于代码的阅读。通过这个文件,springboot就可以读取到我们自定义的配置类MyConfig。 接下来我们只需要打个jar包即可供另外一个项目使用了。下面贴一下pom.xml的内容:
上面定义了一个starter,下面我们将写一个新的工程,来引用我们自定义的starter。还是先看一下项目目录:
要想引用我们自定义的starter,自然是先引入依赖了:
然后再application.properties文件中添加如下配置:
好了,现在就可以在项目中注入HelloService使用了。是的,就是那么简单.DemoApplication主类如下:
启动项目,访问URLhttp://127.0.0.1:8080/?name=hhh:
后台打印日志为initVal=hahaha, name=hhh
其中hahaha
就是我们没在配置文件中配置的属性值,这句日志是我们上面starter项目中com.example.myservice.HelloService#hello
方法打印出来的。
自定义Conditional注解
前面已经讲过condition的原理。其实自定义一个condition很简单,只需要实现SpringBootCondition
类即可,并重写com.example.demo.condition.OnLblCondition#getMatchOutcome
方法。
下面我们简单写个实例:根据属性配置文件中的内容,来判断是否加载bean。
首先定义一个注解,有两个内容:一个是属性文件的KEY,一个是value。我们要实现的是,属性文件的value与指定的value一致,才创建bean。
自定义SpringBootCondition的实现类OnLblCondition:
然后随便定义一个类:
好了,condition已经自定义完成。接下来就是如何使用了:
要想加载MyConditionService
的bean到spring容器中,需要满足以下两个条件:
- 属性文件中配置了
com.lbl.mycondition=lbl
- 可以加载
MyConditionService
类
好了,让我们在application.properties
文件中添加配置com.lbl.mycondition=lbl
启动项目,可以看到日志: MyConditionService
已加载。 把application.properties
文件中的com.lbl.mycondition
去掉,或者更改个值,则上述日志不会打印,也就是不会创建MyConditionService
这个bean .
spring @Conditional注解
前面讲了springboot的实现基础是spring的@Conditional注解。介绍原理前我们来看看怎么用。后面介绍其原理。
我们实现这么一个小功能:**根据不同的环境,实例化不同的bean。 ** springboot通常都是通过-Dspring.profiles.active=dev来区分环境的,如果我们想实现线上的代码逻辑与开发或者测试环境不同,那么这是一个解决方案。
使用java的多态,先定义一个接口:
然后定义两个不同环境的实现类,内容比较简单,只是输出一句log。
好了,,准备工作已经做完,接下来编写配置类,使用spring的@Conditional注解来根据不同的环境配置加载不同的类:
官方的condition有很多: 这里我们使用的是@ConditionalOnExpression。它根据SPEL表达式返回的结果作为条件判断。 这里判断条件为:spring.profiles.active=dev时,创建DevService。为prod时,创建ProdService。
接下来看如何使用:
很简单,只需要注入EnvironmentService
即可。注意IDEA可能会提示错误,因为这个接口有两个实现类。不过不用去管他,因为我们通过condition只实例化了一个bean。
运行结果:
将配置修改为prod后:
使用非常简单,那么@Conditional
是怎么实现的呢?
官方文档的说明是“只有当所有指定的条件都满足是,组件才可以注册”。主要的用处是在创建bean时增加一系列限制条件。 他的核心类是:
所有的condition都是Condition接口的实现类,条件判断是通过matches方法返回的布尔值来判断的。
以@ConditionalOnExpression
注解为例:
ConditionalOnExpression
注解上添加了另一个注解Conditional,指明是哪个condition类处理改注解。 我们打开OnExpressionCondition
这个类:
有这么一个方法,获取到我们设置的EL表达式的值后,进行一些处理,比如渠道environment
中的值,然后计算整体的结果。 其中关键的是boolean result = (Boolean) resolver.evaluate(expression, expressionContext);
这么一行代码,通过springEL计算最终结果。
至此,spring的condition算是介绍完了,我们可以通过实现org.springframework.context.annotation.Condition#matches
来自定义condition。
当然抛开@Condition注解,实现不同环境加载不同的类,既可以使用ConditionalOnExpression
也可以使用@Profile
注解
两种都可以实现不同环境加载不同的类,写法不同,但是他们的实现原理都是一样的,都是获取环境变量
spring.profiles.active
的值,与value进行比较。读者可以去看一下ProfileCondition
源码
备注:springboot的核心注解
spring.factories文件里每一个xxxAutoConfiguration文件一般都会有下面的条件注解:
@ConditionalOnBean:当容器里有指定Bean的条件下
@ConditionalOnClass:当类路径下有指定类的条件下
@ConditionalOnExpression:基于SpEL表达式作为判断条件
@ConditionalOnJava:基于JV版本作为判断条件
@ConditionalOnJndi:在JNDI存在的条件下差在指定的位置
@ConditionalOnMissingBean:当容器里没有指定Bean的情况下
@ConditionalOnMissingClass:当类路径下没有指定类的条件下
@ConditionalOnNotWebApplication:当前项目不是Web项目的条件下
@ConditionalOnProperty:指定的属性是否有指定的值
@ConditionalOnResource:类路径是否有指定的值
@ConditionalOnSingleCandidate:当指定Bean在容器中只有一个,或者虽然有多个但是指定首选Bean
@ConditionalOnWebApplication:当前项目是Web项目的条件下。
上面@ConditionalOnXXX都是组合@Conditional元注解,使用了不同的条件Condition