原文地址: www.marcobehler.com/guides/spri…
基本简介
似乎每个人和他的奶奶都在使用SpringBoot进行项目构建. 但只有少数人能回答这个问题: "SpringBoot到底是什么?"
简短的回答:
SpringBoot官网有如下说明: "SpringBoot使用一种固执己见的方式看待Spring平台和第三方库, (只要你熟悉了这种方式, )你就能非常轻松地入门."
Wow, 这说了等于没说啊, 不是吗?
译者注: 这里翻译成
固执已见, 看似贬义, 但能够很好地表达Spring文档的意思. 毕竟这种方式就是Spring独创的.
幸好, 还有更详细的回答:
就是这篇文章.
小警告
我向你99.99%保证, 你会更想先了解什么是Spring框架?, 如果你...:
- 你完全是SpringBoot或Java新手
- 你认为"Spring框架, Spring Web MVC和SpringBoot是一个东西".
- 你认为"少说些废话, 我就只想学SpringBoot!".
- (尤其是你认为"少说些废话").
您最好是花些时间先行搞懂"什么是Spring框架?", 然后再回来这里.
这篇文章的目标
- 让你深入理解SpringBoot和它的
自动配置功能. - 向你展示, 当你运行main()方法时, SpringBoot是怎样自动启动一个Tomcat服务器?
- 向你展示, SpringBoot的
properties魔法是怎么工作的? 你指定了一些database.properties, 然后你的应用突然就能够访问数据库了?
SpringBoot基础: @Conditionals
在你成为SpringBoot大佬之前, 你得先理解仅仅一个非常重要的概念: Spring框架的@Conditional注解.
爱的建议: 不要跳过这一节, 它是SpringBoot的核心基础. 同时, 我会尽量让它看起来有趣一些.
如何共享你的ApplicationContextConfigurations?
想象一下, 如果你在一家超级巨无霸公司工作, 公司内部成百上千个技术开发组各自使用着Spring平台中不同的项目和产品, 注意, 你不是在使用SpringBoot, 仅仅是Spring框架.
再想象一下, 突然有一天, 你的一个同事有了一个伟大的点子: 从所有公司项目中封装并抽取一个通用的ApplicationContextConfiguration, 因为她注意到公司里所有的项目都在使用着一些通用的Beans.
@Configuration
public class ReallyBigCompanySharedContextConfiguration { // (1)
@Bean
public ReallyBigCompanyProprietaryFlywayClone flywayClone() {
return new ReallyBigCompanyProprietaryFlywayClone(); // (2)
}
}
SharedContextConfiguration会被封装进专属于它的项目中, 然后这个项目最终会被发布到专属于它的jar文件中. 因此你公司内所有的Spring项目都可以通过Maven/Gradle,import这个依赖.ReallyBigCompanyProprietaryFlywayClone封装了一些操作: 例如初始化和管理数据库. 正如上述说明一样, 你公司内所有的项目都在使用关系型数据库. 那么, 将这一系列操作封装并抽取成一个SharedContextConfiguration是非常合理的.
接着, 在你实际负责的项目里, ApplicationContextConfigurations会像这样:
@Configuration
@Import(ReallyBigCompanySharedConfiguration.class) // (1)
public class EarlyExitUnicornProjectContextConfiguration {
// (2)
}
- 从现在起, 所有公司项目都会
@Import(ReallyBigCompanySharedConfiguration.class), 它里面封装了非常多的数据库管理代码. (SharedConfiguration类中只有一个@Bean, 但你应该懂我的意思). - 你可以继续指定一些你的
项目专属@Bean, 正如你正常使用Spring时一样.
SharedConfiguration有什么问题?
SharedConfiguration大获成功, 从今起你司所有项目都不再需要手动构建Database Bean了, 你老板给你的同事升职加薪了. 可喜可贺, 普天同庆! BUT!! 问题来了.
如果你老板让你做一个新项目, 这个新项目需要SharedConfiguration里面所有的@Bean, 但就是不能引入ReallyBigCompanyProprietaryFlywayClone, 因为新项目不用关系型数据库!
这时! 你需要通过一种办法, 告诉Spring: @Configuration相当棒, 但请不要构建那个RBCPFClone, 麻烦您忽略掉它, 帮我构建其余所有的@Beans吧.
而这种办法, 就是Spring的@Conditional.
@Conditional是什么?
从4.0版本(发布于2013)开始, Spirng框架提供了@Conditional注解, 它可以放在@Bean methods/@Components/@Configurations等注解上.
@Conditional(SomeCondition.class) // (1)
- 它有一个
Condition参数,Condition中存在方法matches, 返回值是一个简单的Boolean.- True: (继续 解析/注册)构建相应的
@Bean/@Component/@Configuration - False: (停止 解析/注册)不要构建相应的
@Bean/@Component/@Configuration
- True: (继续 解析/注册)构建相应的
简单来说: 即使ApplicationContextConfiguration中定义了许多@Bean, 但作为终端用户的你, 仍然可以通过@Conditional决定某个bean是否需要构建.
在SharedConfiguration中使用@Conditional
那对SharedConfiguration来说, 有什么意义呢? 我们重构一下它:
@Configuration
public class ReallyBigCompanySharedContextConfiguration {
@Bean
@Conditional(IsRelationalDatabaseCondition.class) // (1)
public ReallyBigCompanyProprietaryFlywayClone flywayClone() {
return new ReallyBigCompanyProprietaryFlywayClone();
}
}
- 啥都没变, 就只是多了个@Conditional注解
那IsRelationalDatabaseCondition又长什么样?
实现Spring的Condition
package com.marcobehler;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class IsRelationalDatabaseCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // (1)
return oracleJdbcDriverOnClassPath() && databaseUrlSet(context); // (2)
}
private boolean databaseUrlSet(ConditionContext context) { // (3)
return context.getEnvironment().containsProperty("spring.datasource.url");
}
private boolean oracleJdbcDriverOnClassPath() { // (4)
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
Condition接口:matchs方法, 返回true/false, 标识这个条件究竟是否被满足?- 实际上这里用到了两个条件:首先检查Oracle driver类是否在Classpath里, 然后再检查用户(就是你)是否在
.properties文件中设置了数据库的url. - 检查property很简单, 只要通过Spring的environment类, 它会遍历所有的@PropertySources(例如: application.properties), 只为了找到一个property.
- 通过
Class.forName("...")查找依赖, 抛出异常当然就是不存在该依赖了.
在实际开发中, 你很可能会把IsRelationalDatabaseCondition分成两个Condition, 它强调了两个非常重要的条件:
- 如果存在具体的有效的properties, 则构建该@Bean
- 如果classpath中存在指定的jar包, 则构建该@Bean
半场总结: SpringBoot是什么?
我们仍未探索任何Springboot的源代码, 但到这里已经阐述了一种新的洞见:
SpringBoot, 是否仅仅就是一个
SharedContextConfiguration, 只不过它包含了好几吨@Conditional?
- 只要你指定了特定的properties, @Conditional就帮你构建
DataSource? - 只要你在classpath里放进了Tomcat库, @Conditional就帮你自动启动内嵌的Tomcat服务器?
是的, 妹有错(再没有别的什么了), 这就是SpringBoot. 不信我? 那我们来看一些源码.
SpringBoot: 自动配置
当你启动了SpringBoot应用时, 发生了什么?
把玩过SpringBoot的都知道, 最终需要执行main()方法来触发SpringBoot的魔法:
package com.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class MySpringBootApp {
public static void main(String[] args) { // (1)
SpringApplication.run(MySpringBootApp.class, args);
}
}
那SpringApplication.run()是怎么工作的?
SpringBoot: 内部核心机制
当SpringApplication启动时, 有许多机制会被触发, 我们来看看其中最重要的三点, 它们并没有什么共同之处, 但组合在一起则构成了SpringBoot最核心的基础
1.自动注册的@PropertySources
基本上, 你可以让Spring框架去读取任何位置的.properties文件, 通过@PropertySource注解即可.
@PropertySource(value = "classpath:application.properties", ignoreResourceNotFound = true)
当你执行main()方法时, SpringBoot会自动从17处位置读取PropertySources(即使不是很正确, 但你可以当成17个被自动加载到你项目的注解)
完整的PropertySource列表在这里, 以下是简略版:
4. Command line arguments.
...
10. OS environment variables.
...
15. Application properties packaged inside your jar (application.properties and YAML variants).
所以, SpringBoot仅仅只是有一系列配置源位置, 它会一直尝试从这些位置读取property, 例如在你的jar包里的command line arguments/application.properties.
现在的问题是, SpringBoot究竟对这些.properties做了什么? 在阅读源码之前, 我们先来看看: 当执行main()方法时, 第二个重要的点.
2. 读入 META-INF/spring.factories
所有的SpringBoot项目都有如下这个依赖: org.springframework.boot:spring-boot-autoconfigure, 它是一个包含了几乎所有SpringBoot的魔法的简单的jar包.
如下所示, 在它的MATA-INF文件夹下, 有一个spring.factories文件.

打开这个文件, 标着"#Auto Configure"的那一段, 上下跨屏了一百多行.
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
// 100+ more lines
这些AutoConfigurations(下文会详细介绍), 就是一个正常的Spring @Configurations, 并配上了好几吨@Conditionals, 如前文所述, SpringBoot应用在每一次启动时, 都会读取并解析这些@Conditionals.
不过, 在我们深入到AutoConfigurations之前, 我们再最后瞟一眼某些额外的SpringBoot功能.
3. 增强的Conditional支持
在好几个段落之前, 我们知道了Spring框架就已经有@Conditional注解了, 不过它是个底层注解.
SpringBoot对@Conditional进行了一系列增强, 这使开发者的活儿更好了. (注意以下@Conditional的参数仅仅是个例子)
@ConditionalOnBean(DataSource.class), 这个条件为真, 当且仅当用户在@Configuration中指定了DateSource @Bean.@ConditionalOnClass(DataSource.class), 如果Classpath中存在DateSource.class, 则此条件为真.@ConditionalOnCloudPlatform(CloudPlatform.Heroku), 如果CloudPlatform被设置为Heroku, 则此条件为真.@ConditionalOnExpression("someSpELExpression), 如果这个SpEL表达式为真, 则此条件为真.@ConditionalOnJava(JavaVersion.EIGHT), 如果当前Java版本是8, 则此条件为真.@ConditionalOnJndi("java:comp/env/ejb/myEJB"), 如果指定的JNDI环境存在, 则此条件为真.@ConditionalOnMissingBean(DataSource.class), 如果用户所有的@Configuration中都没有DataSource @Bean, 则此条件为真.@ConditionalOnMissingClass(DataSource.class), 如果Classpath中不存在DataSource.class, 则此条件为真.@ConditionalOnWebApplication, 如果当前应用是Web应用, 则此条件为真.@ConditionalOnNotWebApplication, 如果当前应用不是Web应用, 则此条件为真.@ConditionalOnProperty("my.property"), 如果已设置了"my.property", 则此条件为真.@ConditionalOnResource("classpath:my.properties"), 如果my.properties文件存在, 则此条件为真.@ConditionalOnSingleCandidate(DataSource.class), 如果当前应用中只有一个@Primary DataSource, 则此条件为真.
好了, 简单来说, 拥抱SpringBoot, 你不再需要写那么多通用的conditions了(例如查询获取一些property), 你可以直接使用其相应的增强的@ConditionalOnXXX注解了.
总结
当SpringBoot启动时:
- 它尝试从17个硬编码的资源位置读入.properties
- 它同时也从你的自动配置模块中读入spring.factories, 并找出它应该引入的
AutoConfiguration. - 相比于Spring, 它有@ConditionalOnXXX等更增强更顺手的工具.
以上这些知识已经足够帮助我们去看看那些AutoConfigurations了.
一个AutoConfiguration的例子: 分析DataSourceAutoConfiguration
在spring-boot-autoconfigure模块中, 你会找到一个子包, 它包含了许多AutoConfiguration, 这些AutoConfig都是SpringBoot可集成的所有的Spring或者第三方库的相关配置操作.
我们先来看看DataSourceAutoConfiguration, 当你填入"spring.datasource.url"等相关properties时, 它就是那个真正协助你构建DataSource Bean的好人.
@Configuration(proxyBeanMethods = false) // (1)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) // (2)
@EnableConfigurationProperties(DataSourceProperties.class) // (3)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class }) // (3)
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
}
@Configuration(proxyBeanMethods = false) // (4)
@Conditional(PooledDataSourceCondition.class) // (5)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) // (6)
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, // (7)
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
}
// some more
}
哇偶.., 但这活生生的注池解林, 别虚, 我们一行一行走下去.
- DataSourceAutoConfiguration是一个正常的Spring @Configuration.
- 如果你想要DataSourceAutoConfiguration这个@Configuration生效, 那你就必须把两个.class放入你的Classpath: DataSource.class和EmbeddedDatabaseType.class. 如前文所述, 如果这个@Conditional返回了false, 那整个DataSourceAutoConfiguration将不会被解析.
- 暂时可以忽略这两行, 不过可以做个快速笔记: @EnableConfigurationProperties可以使你放入.properties文件中的properties们, 自动转换成一个object, 例如上面代码里的DataSourceProperties.
- 这里还有两个内部类, 他们同时都是@Configuration, 其中的PooledDataSourceConfiguration会根据@Conditional的返回值, 来决定是否为你创建一个数据库连接池.
- 如果跟踪代码进入PooledDataSourceCondition的@Conditional, 会发现里面又是一个@ConditionalOnProperty. 啊! 递归!
- 当且仅当你没有指定DataSource bean, 也没有指定XADataSource bean时, PooledDataSourceConfiguration才会实际生效.
- 这里引入了许多别的@Configuration, 具体来说, 每个configuration负责其对应的连接池库(Hikari, Tomcat, Dbcp2, 等等...)
虽然上述有许多代码仍待消化, 但它终究只是一个被@Conditional修饰的普通的Spring @Configuration. 为了更清晰地解构它, 我们来看看被import的DataSourceConfiguration.Hikari.class.
/**
* Hikari DataSource configuration.
*/
@Configuration(proxyBeanMethods = false) // (1)
@ConditionalOnClass(HikariDataSource.class) // (2)
@ConditionalOnMissingBean(DataSource.class) // (3)
@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", // (4)
matchIfMissing = true)
static class Hikari {
@Bean // (5)
@ConfigurationProperties(prefix = "spring.datasource.hikari")
HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
注解更多了!!!!!
- 又一个普通的Spring @Configuration
- HikariDataSource.class一定要在你的Classpath里面(其实就是在你的pom.xml;里引入hikari-cp)
- 你必须没有自定义过哪怕一个DataSource bean.
- 要么你不要配置spring.datasoure.type, 要么它的值必须等于"com.zaxxer.hikari.HikariDataSource".
- 如果上面的条件都满足了, 那Spring才会构建这个HikariDataSource bean. 其效果完全等同于你看着HikariCP的官方文档并自己手动构建的bean.
所以, 最终而言, 所有的DataSourceAutoConfiguration做得都是查询Classpath中是否存在相应的第三方库, 同时看看相应的一些配置是否已经配置好了. 然后它就会帮你构建那些相应的本来你需要自己手动构建的bean.
针对数据库这个例子来说:
- 如果你的Classpath里存在HikariCP库, 那你就会自动地拿到HikariDatasource @Bean,
- 如果你的Classpath里存在DBCP2库(并且excluded了Hikari), 那你就会拿到另一个不同的连接池. 这些事情, 你只要更换了依赖包, SpringBoot就自动帮你完成了.
这! 就是@Conditionals的力量!
练习: 可以自行分析下TomcatAutoConfiguration.
最终回答: 什么是SpringBoot?
SpringBoot仅仅是一堆普通的Spring @Configuration, 只不过它们带着好几吨@Conditionals. 而上面这句话, 大概也就是SpringBoot首页的"固执己见的默认方式"的深层含义吧.