原文:http://sivalabs.in/2016/03/how-springboot-autoconfiguration-magic/
作者:Siva
譯者:http://oopsguy.com
在之前的博文《為什么是 Spring Boot》中兼贸,我們已經(jīng)介紹了如何創(chuàng)建一個 Spring Boot 應(yīng)用程序吃溅。但是,你可能還不了解它幕后究竟發(fā)生了什么很澄,你想了解 SpringBoot AutoConfiguration 背后的原理颜及。但在此之前蹂楣,你應(yīng)該先了解 Spring @Conditional 的特性,所有 Spring Boot AutoConfiguration 魔法都是基于它肄扎。
@Conditional 的魔力
在開發(fā)基于 Spring 的應(yīng)用程序時,我們可能需條件性地注冊 Bean犯祠。
例如,你希望在本地運(yùn)行應(yīng)用程序時注冊一個指向 DEV 數(shù)據(jù)庫的 DataSource bean搔耕,在生產(chǎn)中指向 PRODUCTION 數(shù)據(jù)庫痰娱。
你可以將數(shù)據(jù)庫連接參數(shù)外部化,放置在屬性(properties)文件中鲸睛,并在適當(dāng)?shù)沫h(huán)境中使用該個文件坡贺。但是,當(dāng)你需要指向不同的環(huán)境并構(gòu)建應(yīng)用程序時遍坟,你得更改配置。
為了解決這個問題风瘦,Spring 3.1 引入了 Profiles 概念公般。你可以注冊多個相同類型的 bean,并將它們與一個或多個配置文件相關(guān)聯(lián)瞬雹。在運(yùn)行應(yīng)用程序時,你可以激活所需的配置文件酗捌,只有與配置文件相關(guān)聯(lián)的 bean 才被注冊涌哲。
@Configuration
public class AppConfig
{
@Bean
@Profile("DEV")
public DataSource devDataSource() {
...
}
@Bean
@Profile("PROD")
public DataSource prodDataSource() {
...
}
}
然后,可以使用系統(tǒng)屬性(System Property)來指定要激活配置文件 - Dspring.profiles.active = DEV
這種方法適用于啟用了基于 profiles 來啟用或禁用 bean 注冊的簡單場景哪廓。如果要根據(jù)一些條件邏輯來注冊 bean初烘,profiles 方式是遠(yuǎn)遠(yuǎn)不夠的分俯。
為了讓有條件性注冊 Spring bean 變得更加靈活哆料,Spring 4 引入了 @Conditional 概念东亦。通過使用 @Conditional 方式,你可以根據(jù)任意條件注冊一個 bean讥此。
例如,你可能想在如下情況注冊一個 bean:
- classpath 中某個類
- 某種類型的 Spring bean卒稳,尚未在 ApplicationContext 中注冊
- 存放在某個位置的某個文件
- 一個存在于配置文件中的某個屬性值
- 一個存在/不存在的某個系統(tǒng)屬性
這里只是列舉了幾個例子他巨,你可以根據(jù)自己的需要定義任何條件。
讓我們來看看 Spring 的 @Conditional
是如何工作的捻爷。
假設(shè)我們有一個 UserDAO 接口份企,其包含從數(shù)據(jù)存儲中獲取數(shù)據(jù)的方法。我們有兩個 UserDAO 接口的實現(xiàn)甜紫,即 JdbcUserDAO(與 MySQL 數(shù)據(jù)庫通信)和 MongoUserDAO(與 MongoDB 通信)骂远。
我們可能想通過基于系統(tǒng)屬性 dbType 來只啟用 JdbcUserDAO 或 MongoUserDAO 其中一個激才。
如果應(yīng)用程序使用了 java -jar myapp.jar -DdbType = MySQL
啟動,則啟用 JdbcUserDAO瘸恼,如果啟動時使用了 java -jar myapp.jar -DdbType = MONGO
钞脂,則啟用 MongoUserDAO。
假設(shè)我們有 UserDAO 接口冰啃,以下是 JdbcUserDAO 和 MongoUserDAO 實現(xiàn)類:
public interface UserDAO
{
List<String> getAllUserNames();
}
public class JdbcUserDAO implements UserDAO
{
@Override
public List<String> getAllUserNames()
{
System.out.println("**** Getting usernames from RDBMS *****");
return Arrays.asList("Siva","Prasad","Reddy");
}
}
public class MongoUserDAO implements UserDAO
{
@Override
public List<String> getAllUserNames()
{
System.out.println("**** Getting usernames from MongoDB *****");
return Arrays.asList("Bond","James","Bond");
}
}
我們可以實現(xiàn)一個 MySQLDatabaseTypeCondition Condition 來檢查系統(tǒng)屬性是否是 MYSQL
:
public class MySQLDatabaseTypeCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
{
String enabledDBType = System.getProperty("dbType");
return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MYSQL"));
}
}
現(xiàn)在我們可以使用 @Conditional 條件性地配置 JdbcUserDAO 和 MongoUserDAO bean阎毅,如下所示:
@Configuration
public class AppConfig
{
@Bean
@Conditional(MySQLDatabaseTypeCondition.class)
public UserDAO jdbcUserDAO(){
return new JdbcUserDAO();
}
@Bean
@Conditional(MongoDBDatabaseTypeCondition.class)
public UserDAO mongoUserDAO(){
return new MongoUserDAO();
}
}
如果我們使用 java -jar myapp.jar -DdbType = MYSQL
來運(yùn)行應(yīng)用程序扇调,則只有 JdbcUserDAO bean 被注冊。
但如果將系統(tǒng)屬性設(shè)置為 -DdbType = MONGODB
狼钮,則只會注冊 MongoUserDAO bean。
現(xiàn)在我們已經(jīng)了解了如何基于系統(tǒng)屬性條件性地注冊一個 bean莲镣。
假設(shè)我們想當(dāng) MongoDB java 驅(qū)動程序類 com.mongodb.Server 在 classpath 上可用時注冊 MongoUserDAO bean涎拉,否則,注冊 JdbcUserDAO bean半火。
為了實現(xiàn)這點(diǎn)季俩,我們可以創(chuàng)建 Condition 來檢查 MongoDB 的驅(qū)動程序類 com.mongodb.Server 是否存在:
public class MongoDriverPresentsCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext,AnnotatedTypeMetadata metadata)
{
try {
Class.forName("com.mongodb.Server");
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
public class MongoDriverNotPresentsCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
{
try {
Class.forName("com.mongodb.Server");
return false;
} catch (ClassNotFoundException e) {
return true;
}
}
}
我們剛剛看到了如何根據(jù) classpath 中的類是否存在來條件性地注冊 bean酌住。
如果我們只想在沒有其他 UserDAO 類型的 Spring bean 注冊時才注冊 MongoUserDAO bean,該怎么辦娱节?
我們同樣可以創(chuàng)建一個 Condition 來檢查是否存在現(xiàn)有的某個類型的 bean祭示,如下所示:
public class UserDAOBeanNotPresentsCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
{
UserDAO userDAO = conditionContext.getBeanFactory().getBean(UserDAO.class);
return (userDAO == null);
}
}
如果我們只想在 properties 配置文件中設(shè)置屬性 app.dbType = MONGO,那該怎么注冊 MongoUserDAO bean 呢稠歉?
我們可以實現(xiàn)如下 Condition:
public class MongoDbTypePropertyCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata metadata)
{
String dbType = conditionContext.getEnvironment()
.getProperty("app.dbType");
return "MONGO".equalsIgnoreCase(dbType);
}
}
我們剛剛已經(jīng)看到了如何實現(xiàn)各種 condition汇陆。
然而,使用注解實現(xiàn) Condition 的方式更為優(yōu)雅阅羹。而不是像為 MYSQL 和 MongoDB 創(chuàng)建 Condition 實現(xiàn)那樣做。我們可以創(chuàng)建一個 DatabaseType 注解执庐,如下所示:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(DatabaseTypeCondition.class)
public @interface DatabaseType
{
String value();
}
然后可以通過編寫 DatabaseTypeCondition 使用 DatabaseType 的值來確定是否啟用或禁用 bean 注冊导梆,如下所示:
public class DatabaseTypeCondition implements Condition
{
@Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata metadata)
{
Map<String, Object> attributes = metadata.getAnnotationAttributes(DatabaseType.class.getName());
String type = (String) attributes.get("value");
String enabledDBType = System.getProperty("dbType","MYSQL");
return (enabledDBType != null && type != null && enabledDBType.equalsIgnoreCase(type));
}
}
現(xiàn)在我們可以將 @DatabaseType 注解放置在 bean 定義上:
@Configuration
@ComponentScan
public class AppConfig
{
@DatabaseType("MYSQL")
public UserDAO jdbcUserDAO(){
return new JdbcUserDAO();
}
@Bean
@DatabaseType("MONGO")
public UserDAO mongoUserDAO(){
return new MongoUserDAO();
}
}
此處看尼,我們從 DatabaseType 注解獲取元數(shù)據(jù),并檢查系統(tǒng)屬性 dbType 的值以確定是否啟用或禁用 bean 注冊藏斩。
我們已經(jīng)看了許多示例灾茁,了解了如何使用 @Conditional 注解條件性地注冊 bean。
SpringBoot 大量地使用了 @Conditional 特性北专,根據(jù)各種標(biāo)準(zhǔn)條件性地注冊 bean拓颓。
你可以在 Spring-boot-autoconfigure- {version} .jar 的 org.springframework.boot.autoconfigure 包中找到 SpringBoot 使用的各種 Condition 實現(xiàn)。
現(xiàn)在我們來了解一下 SpringBoot 如何使用 @Conditional 特性條件性地檢查是否注冊一個 bean驶睦。
究竟是什么觸發(fā)了自動配置(auto-configuration)機(jī)制场航?
這就是我們下一節(jié)的內(nèi)容。
SpringBoot 的 AutoConfiguration
SpringBoot 自動配置魔法的關(guān)鍵在于 @EnableAutoConfiguration 注解溉痢。通常,我們使用 @SpringBootApplication 來注解應(yīng)用程序入口類髓削,如果我們要自定義默認(rèn)值镀娶,我們可以使用以下注解:
Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application
{
}
@EnableAutoConfiguration 注解允許通過掃描 classpath 中的組件并注冊匹配各種條件的 bean 來自動配置 Spring ApplicationContext。
SpringBoot 在 spring-boot-autoconfigure- {version} .jar 中提供了各種 AutoConfiguration 類宝泵,它們負(fù)責(zé)注冊各種組件。
通常坎怪,AutoConfiguration 類使用 @Configuration 注解將其標(biāo)記為一個 Spring 配置類廓握,并用 @EnableConfigurationProperties 注解來綁定自定義屬性和一個或多個條件的 bean 注冊方法嘁酿。
例如,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration 類娱仔。
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration
{
...
...
@Conditional(DataSourceAutoConfiguration.EmbeddedDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedConfiguration {
}
@Configuration
@ConditionalOnMissingBean(DataSourceInitializer.class)
protected static class DataSourceInitializerConfiguration {
@Bean
public DataSourceInitializer dataSourceInitializer() {
return new DataSourceInitializer();
}
}
@Conditional(DataSourceAutoConfiguration.NonEmbeddedDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
protected static class NonEmbeddedConfiguration {
@Autowired
private DataSourceProperties properties;
@Bean
@ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
public DataSource dataSource() {
DataSourceBuilder factory = DataSourceBuilder
.create(this.properties.getClassLoader())
.driverClassName(this.properties.getDriverClassName())
.url(this.properties.getUrl()).username(this.properties.getUsername())
.password(this.properties.getPassword());
if (this.properties.getType() != null) {
factory.type(this.properties.getType());
}
return factory.build();
}
}
...
...
@Configuration
@ConditionalOnProperty(prefix = "spring.datasource", name = "jmx-enabled")
@ConditionalOnClass(name = "org.apache.tomcat.jdbc.pool.DataSourceProxy")
@Conditional(DataSourceAutoConfiguration.DataSourceAvailableCondition.class)
@ConditionalOnMissingBean(name = "dataSourceMBean")
protected static class TomcatDataSourceJmxConfiguration {
@Bean
public Object dataSourceMBean(DataSource dataSource) {
....
....
}
}
...
...
}
這里的 DataSourceAutoConfiguration 使用了 @ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class}) 注解,這意味著只有 DataSource.class 和 EmbeddedDatabaseType.class 類在 classpath 上可用時才會考慮 DataSourceAutoConfiguration 中的 bean 的自動配置借卧。
該類也使用 @EnableConfigurationProperties(DataSourceProperties.class) 注解,它可以將 application.properties 中的屬性自動綁定到 DataSourceProperties 類的屬性上陪每。
@ConfigurationProperties(prefix = DataSourceProperties.PREFIX)
public class DataSourceProperties implements BeanClassLoaderAware, EnvironmentAware, InitializingBean {
public static final String PREFIX = "spring.datasource";
...
...
private String driverClassName;
private String url;
private String username;
private String password;
...
//setters and getters
}
由于使用了此配置镰吵,所有以 spring.datasource.* 開頭的屬性將自動綁定到 DataSourceProperties 對象。
spring.datasource.url=jdbc:mysql://localhost:3306/test
spring.datasource.username=root
spring.datasource.password=secret
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
你還可以看到一些使用了 SpringBoot 的 Condition 注解(如 @ConditionalOnMissingBean盼产、@ConditionalOnClass 和 @ConditionalOnProperty 等)的內(nèi)部類和 bean 定義方法勺馆。
只有當(dāng)這些條件匹配時,這些 bean 定義才被注冊到 ApplicationContext 中蜈项。
你可以在 spring-boot-autoconfigure-{version}.jar 中瀏覽更多其他 AutoConfiguration 類:
- org.springframework.boot.autoconfigure.web.DispatcherServletAutoConfiguration
- org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
- org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
- org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration
等等
我希望現(xiàn)在你已經(jīng)對 “Spring Boot 自動配置如何通過 AutoConfiguration 類以及 @Conditional 工作”有了一定的認(rèn)識续挟。