随着SpringBoot日渐流行,有关SpringBoot
的各类“心经”、“秘籍”、“从入门到精通”之类的文章或者书籍琳琅满目、层出不穷,本文无意与这些典籍争锋,而是从实际应用出发,结合官方文档,收集并整理一些不常见却很有用的知识点,同时蹭一下You Don’t Know JS Yet的热度,姑且就叫它你不知道的SpringBoot
。
创建项目
start.spring.io可以说是最好的SpringBoot
项目创建工具,没有之一。start.spring.io
可以满足绝大多数创建由SpringBoot
驱动的应用程序的场景,它具备以下特征:
- 生成的工程开箱即用,几乎不做任何修改就可以运行;
- IDE友好,可以快速导入进主流IDE中;
- 依赖版本准确,不会引发依赖不匹配而导致的各类诡异问题,例如
SpringBoot
与Spring Cloud
大版本的匹配都是准确的; - 多构建工具支持,可以自由选择
Maven
或Gradle
; - 多语言支持,可以自由选择
Java
、Groovy
或Kotlin
; - 多Java版本支持,可以自由选择
LTS
或最新版的JDK; - 可以通过
Explore
功能拷贝需要的依赖代码片段而不需要下载整个项目; - 可以通过
Share
功能把创建项目的属性以URL的方式分享给其他人。
除以上特征之外,start.spring.io
本身提供了restful API
,可以通过简单的http
调用来创建项目而不需要访问他的web UI,例如下面的例子,通过curl
来创建一个新的项目并下载到本地:
1 | $ curl https://start.spring.io/starter.zip -d dependencies=web,devtools \ |
这种API调用的方式也是大多数IDE生成SpringBoot
项目的底层手段。除使用curl
外,还可以通过Spring Boot CLI的方式创建项目,与curl
方式是非类似,
1 | $ spring init --build=gradle \ |
Spring Boot CLI
的安装可以参考官方文档。
以上start.spring.io
以及其衍生工具的使用,除此之外,我们甚至可以搭建自己的Spring Initializr Reference
,原理类似与自己运行一个start.spring.io
,这里我们就不展开讨论了。
更多详情请参考官方文档,
内置的构建工具
如果采用上述start.spring.io
生成Maven
或Gradle
项目时,项目中会自带Maven Wrapper或Grade Wrapper,以Maven
项目为例,我们会在项目下看到下面的目录结构,
1 | ├── .mvn |
其中的.mvn
、mvnw
就是项目自带Maven Wrapper
。一般我们在使用Maven
时,用到的都是提前安装好的全局的Maven
,所有需要构建的程序也都会共享这一个全局的Maven
。但是对于某些项目来说,构建是可能需要用到特定的Maven
版本,这时做全局安装费时费力,还要污染全局,而使用Maven Wrapper
就可以避免类似问题,进而高效地使用特定版本的Maven
,Maven Wrapper
的工作流程是这样的:
- 在执行
mvnw
命令时,会检查/.m2/wrapper
目录下是否有对应版本的Maven
安装,如果有,则继续构建; - 如果没有,则根据当前目录下
.mvn/wrapper/maven-wrapper.properties
中的的distributionUrl
属性值来下载Maven
,下载后再完成构建;
有很多同学在生成SpringBoot
项目后,直接就把Maven Wrapper
相关的文件删除掉了,这里还是建议保留,并且也应该把这些文件一并提交到代码仓库里,这样对于其他开发人员和CI/CD
工具也是友好的,大家都可以使用一致的环境来构建项目。
特有的转换器
Converter SPI是Spring Framework
中非常重要的基本概念,Spring Framework
也内置转换器可以实现从String
到其他常用数据类型的转换。SpringBoot
进一步强化了转换器,引入了3个特有的转换器,可以方便的处理Duration、Peroid以及DataSize,详情如下表所示:
Java类型 | 单位 | 示例 |
---|---|---|
java.time.Duration | ns us ms s m h d |
50s 3d |
java.time.Period | y m w d |
2m 1y3d |
org.springframework.util.unit.DataSize | B KB MB GB TB |
512B 2MB |
完整的示例代码:
1 |
|
更多详情请参考官方文档。
特有的事件
Spring Framework
本身实现了一整套事件体系,并定义了一组内置的事件,但这些事件
基本上是Context
级别的,而不是Application
级别的。SpringBoot
拓展了Spring Framework
的事件体系,并引入了Application Level
的一组事件:
- 应用程序启动时会触发
ApplicationStartingEvent
; - 应用程序运行的
Environment
准备好后,在创建Applicatoin Context
之前,会触发ApplicationEnvironmentPreparedEvent
; - 在
ApplicationContext
准备好后,所有ApplicationContextInitializers
被执行后,但在所有的Bean
定义加载之前,会触发ApplicationContextInitializedEvent
事件; Bean
定义加载后,会触发ApplicationPreparedEvent
事件;- 在
Application Context
刷新后,command-line runner
被调用之前,会触发ApplicationStartedEvent
事件; - 在
LivenessState.CORRECT
被检测到之后,换言之,应用程序已经被认为处于活跃状态,会触发AvailabilityChangeEvent
事件; - 在所有的
command-line runner
被执行后,会触发ApplicationReadyEvent
事件。(原文是“any”而不是“all”,表述不是非常准确,特意提了这个issue); - 在
ReadinessState.ACCEPTING_TRAFFIC
被简则之后,换言之,应用程序已被认为可以处理外部请求,会再触发AvailabilityChangeEvent
事件; - 在应用程序启动过程中产生任何异常,会触发
ApplicationFailedEvent
事件。
以上Application Level
的各类事件中,最常用的监听ApplicationReadyEvent
事件来做其他初始化操作,因为这个时间点,所有的Bean
及其依赖都已经创建,这些Bean
已经可以使用,例如下面的代码所示:
1 |
|
更多详情请参考官方文档。
运行初始化代码
CommandLineRunner和ApplicationRunner都可以用来运行初始化代码,CommandLineRunner
通过一个字符串数组来访问命令行参数,而ApplicationRunner
是通过ApplicationArguments来访问。除此之外,可以配置多个CommandLineRunner
或者ApplicationRunner
,其优先级和执行顺序遵循一下规则:
ApplicationRunner
会优先于CommandLineRunner
执行;- 多个同类型的
Runner
可以通过@Order
注解来指定运行顺序,但@Order
只有放在class
上才生效。
考虑下面的代码,
1 | // incorrect code, don't use it |
其输出结果为,
1 | m.d.y.YouDontKnowSpringbootApplication : I'm application runner 3 |
可以看到applicationRunner3
最先被运行,但commandLineRunner1
与commandLineRunner2
并没有按照@Order
指定的顺序运行,其中的原理可以参考这里。只有按照下面的方式才能做到有序,
1 | 2) ( |
其输出结果为,
1 | me.danielpf.ydtk.CommandLineRunner2 : I'm commandline runner 2 |
上面已经提到过,
ApplicationReadyEvent
会在所有的Runner
运行之后才会触发,所以尽量避免既在Runner
中执行过长、过慢的逻辑又要依赖于监听ApplicationReadyEvent
,考虑下面的代码,其输出结果为,
1
2
3
4
5
6
7
8
9
10
11
pulic CommandLineRunner commandLineRunner() {
return args -> {
log.info("start commandline runner...");
TimeUnit.SECONDS.sleep(10);
};
.class) (ApplicationReadyEvent
pulic void ready() {
log.info("application ready...");
}
1
2 2021-04-21 15:57:32.379 INFO 56531 --- [ main] m.d.y.YouDontKnowSpringbootApplication : start commandline runner...
2021-04-21 15:57:42.386 INFO 56531 --- [ main] m.d.y.YouDontKnowSpringbootApplication : application ready...
更多详情请参考官方文档。
Web Application类型
SpringBoot
会根据依赖尝试创建合适的Web Environment
,其默认规则可以参考下面的表格,
Starter | Application Conext | Web Application Type | Web Container |
---|---|---|---|
spring-boot-starter-web only |
AnnotationConfigServletWebServer | SERVLET |
Tomcat |
spring-boot-starter-webflux only |
AnnotationConfigReactiveWebServer | REACTIVE |
Netty |
spring-boot-starter-web spring-boot-starter-webflux |
AnnotationConfigServletWebServer | SERVLET |
Tomcat |
spring-boot-starter |
AnnotationConfig | NONE |
N/A |
需要注意的是,
- 如果
spring-boot-starter-web
与spring-boot-starter-webflux
混用,那么Web Environment
还是会被设置为SERVLET
,这种情况是为了兼容在SERVLET
应用中使用Reactive API
,例如WebClient; - 可以通过编程的方式,手动设置
WebApplicationType
,甚至关闭Web Environment
,例如下面的代码是通过flunt-builder
的方式关闭Web Environment
,
1 | public static void main(String[] args) { |
更多详情请参考官方文档。
使用JSON配置应用
大多数场景下,我们一般会使用properties
文件或者YAML
文件,结合命令行参数来配置SpringBoot
应用程序,除此之外,我们还可以通过JSON
来配置应用程序,我们可以把基于JSON
的配置理解成为一个增强版的命令行。我们写在JSON
里面的配置,都会被merge进当前的Environment
。
JSON
配置可以通过以下几种方式传递给应用程序,这里借官方文档的几个示例说明,
- 通过
UN*X shell
的环境变量传递:1
$ SPRING_APPLICATION_JSON='{"acme":{"name":"test"}}' java -jar myapp.jar
- 通过
system property
传递:1
$ java -Dspring.application.json='{"acme":{"name":"test"}}' -jar myapp.jar
- 通过命令行参数传递:
1
$ java -jar myapp.jar --spring.application.json='{"acme":{"name":"test"}}'
- 如果应用程序部署在传统的web中间件中,可以通过
JNDI
传递,变量名称为:1
java:comp/env/spring.application.json
更多详情请参考官方文档。
基于Profile的Logback
在SpringBoot
中使用Logback
也能享受到Spring Profile
所带来的便利,基于Profile
的Logback
配置文件为logback-spring.xml
,放置在classpath
下会被SpringBoot
自动加载。
我们可以使用springProfile
来控制不同Profile
下有那些appender
会生效,例如这样,
1 | <springProfile name="dev"> |
也可以使用springProperty
来定义一些属性,并在后面的配置引用这些属性,
1 | <springProperty scope="context" name="mailSubjectPrefix" source="mail.subject.prefix" defaultValue=""/> |
更多详情请参考官方文档。
探活
Spring Boot Actuator提供了非常强大的系统监控功能。在K8s等容器环境部署SpringBoot
应用时,我们可以可以借助Spring Boot Actuator
内置的Health Indicator
来实现探活,一般情况下借助/actuator/health/liveness
与/actuator/health/readiness
这两个endpoint,
1 | livenessProbe: |
我们还可以实现自己的HealthIndicators
,
1 | import org.springframework.boot.actuate.health.Health; |
更多详情请参考官方文档。
多数据源
在检测到有数据库访问相关的依赖后,SpringBoot
会尝试Auto Config
数据源,我们只需要设定数据源的各类属性即可。但在某些业务场景下,我们需要多个数据源,我们可以借助SpringBoot
内置的一些工具,比较方便地配置多个数据源。下面的代码展示了从配置上彻底分开的两个不同数据源,
"app.datasource.first")
public DataSourceProperties firstDataSourceProperties() {
return new DataSourceProperties();
}
("app.datasource.first.configuration")
public HikariDataSource firstDataSource() {
return firstDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
("app.datasource.second")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
("app.datasource.second.configuration")
public BasicDataSource secondDataSource() {
return secondDataSourceProperties().initializeDataSourceBuilder().type(BasicDataSource.class).build();
}
(
上述代码的要点如下:
- 可以借助DataSourceProperties来绑定特定前缀的数据源属性(这些属性仍为标准属性,只是前缀不同),并通过
initializeDataSourceBuilder()
和type()
创建特定类型的数据源; - 可以借助
@ConfigurationProperties
再次将自定义的属性绑定到已经在上一步创建的数据源对象上; - 一定要通过
@Primary
来指定默认的数据源。