原文:http://sivalabs.in/2016/03/how-springboot-autoconfiguration-magic/
作者:Siva
譯者:http://oopsguy.com
在之前的博文《為什么是 Spring Boot》中,我們已經介紹了如何創建一個 Spring Boot 應用程序。但是,你可能還不了解它幕后究竟發生了什么,你想了解 SpringBoot AutoConfiguration 背后的原理。但在此之前,你應該先了解 Spring @Conditional 的特性,所有 Spring Boot AutoConfiguration 魔法都是基於它。
@Conditional 的魔力
在開發基於 Spring 的應用程序時,我們可能需條件性地注冊 Bean。
例如,你希望在本地運行應用程序時注冊一個指向 DEV 數據庫的 DataSource bean,在生產中指向 PRODUCTION 數據庫。
你可以將數據庫連接參數外部化,放置在屬性(properties)文件中,並在適當的環境中使用該個文件。但是,當你需要指向不同的環境並構建應用程序時,你得更改配置。
為了解決這個問題,Spring 3.1 引入了 Profiles 概念。你可以注冊多個相同類型的 bean,並將它們與一個或多個配置文件相關聯。在運行應用程序時,你可以激活所需的配置文件,只有與配置文件相關聯的 bean 才被注冊。
@Configuration
public class AppConfig
{
@Bean
@Profile("DEV")
public DataSource devDataSource() {
...
}
@Bean
@Profile("PROD")
public DataSource prodDataSource() {
...
}
}
然后,可以使用系統屬性(System Property)來指定要激活配置文件 - Dspring.profiles.active = DEV
這種方法適用於啟用了基於 profiles 來啟用或禁用 bean 注冊的簡單場景。如果要根據一些條件邏輯來注冊 bean,profiles 方式是遠遠不夠的。
為了讓有條件性注冊 Spring bean 變得更加靈活,Spring 4 引入了 @Conditional 概念。通過使用 @Conditional 方式,你可以根據任意條件注冊一個 bean。
例如,你可能想在如下情況注冊一個 bean:
- classpath 中某個類
- 某種類型的 Spring bean,尚未在 ApplicationContext 中注冊
- 存放在某個位置的某個文件
- 一個存在於配置文件中的某個屬性值
- 一個存在/不存在的某個系統屬性
這里只是列舉了幾個例子,你可以根據自己的需要定義任何條件。
讓我們來看看 Spring 的 @Conditional
是如何工作的。
假設我們有一個 UserDAO 接口,其包含從數據存儲中獲取數據的方法。我們有兩個 UserDAO 接口的實現,即 JdbcUserDAO(與 MySQL 數據庫通信)和 MongoUserDAO(與 MongoDB 通信)。
我們可能想通過基於系統屬性 dbType 來只啟用 JdbcUserDAO 或 MongoUserDAO 其中一個。
如果應用程序使用了 java -jar myapp.jar -DdbType = MySQL
啟動,則啟用 JdbcUserDAO,如果啟動時使用了 java -jar myapp.jar -DdbType = MONGO
,則啟用 MongoUserDAO。
假設我們有 UserDAO 接口,以下是 JdbcUserDAO 和 MongoUserDAO 實現類:
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");
}
}
我們可以實現一個 MySQLDatabaseTypeCondition Condition 來檢查系統屬性是否是 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"));
}
}
現在我們可以使用 @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
來運行應用程序,則只有 JdbcUserDAO bean 被注冊。
但如果將系統屬性設置為 -DdbType = MONGODB
,則只會注冊 MongoUserDAO bean。
現在我們已經了解了如何基於系統屬性條件性地注冊一個 bean。
假設我們想當 MongoDB java 驅動程序類 com.mongodb.Server 在 classpath 上可用時注冊 MongoUserDAO bean,否則,注冊 JdbcUserDAO bean。
為了實現這點,我們可以創建 Condition 來檢查 MongoDB 的驅動程序類 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;
}
}
}
我們剛剛看到了如何根據 classpath 中的類是否存在來條件性地注冊 bean。
如果我們只想在沒有其他 UserDAO 類型的 Spring bean 注冊時才注冊 MongoUserDAO bean,該怎么辦?
我們同樣可以創建一個 Condition 來檢查是否存在現有的某個類型的 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 配置文件中設置屬性 app.dbType = MONGO,那該怎么注冊 MongoUserDAO bean 呢?
我們可以實現如下 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);
}
}
我們剛剛已經看到了如何實現各種 condition。
然而,使用注解實現 Condition 的方式更為優雅。而不是像為 MYSQL 和 MongoDB 創建 Condition 實現那樣做。我們可以創建一個 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));
}
}
現在我們可以將 @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 注解獲取元數據,並檢查系統屬性 dbType 的值以確定是否啟用或禁用 bean 注冊。
我們已經看了許多示例,了解了如何使用 @Conditional 注解條件性地注冊 bean。
SpringBoot 大量地使用了 @Conditional 特性,根據各種標准條件性地注冊 bean。
你可以在 Spring-boot-autoconfigure- {version} .jar 的 org.springframework.boot.autoconfigure 包中找到 SpringBoot 使用的各種 Condition 實現。
現在我們來了解一下 SpringBoot 如何使用 @Conditional 特性條件性地檢查是否注冊一個 bean。
究竟是什么觸發了自動配置(auto-configuration)機制?
這就是我們下一節的內容。
SpringBoot 的 AutoConfiguration
SpringBoot 自動配置魔法的關鍵在於 @EnableAutoConfiguration 注解。通常,我們使用 @SpringBootApplication 來注解應用程序入口類,如果我們要自定義默認值,我們可以使用以下注解:
Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application
{
}
@EnableAutoConfiguration 注解允許通過掃描 classpath 中的組件並注冊匹配各種條件的 bean 來自動配置 Spring ApplicationContext。
SpringBoot 在 spring-boot-autoconfigure- {version} .jar 中提供了各種 AutoConfiguration 類,它們負責注冊各種組件。
通常,AutoConfiguration 類使用 @Configuration 注解將其標記為一個 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 等)的內部類和 bean 定義方法。
只有當這些條件匹配時,這些 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
等等
我希望現在你已經對 “Spring Boot 自動配置如何通過 AutoConfiguration 類以及 @Conditional 工作”有了一定的認識。