原文:http://sivalabs.in/2016/03/how-springboot-autoconfiguration-magic/
做者:Siva
譯者:http://oopsguy.comjava
在以前的博文《爲何是 Spring Boot》中,咱們已經介紹瞭如何建立一個 Spring Boot 應用程序。可是,你可能還不瞭解它幕後究竟發生了什麼,你想了解 SpringBoot AutoConfiguration 背後的原理。但在此以前,你應該先了解 Spring @Conditional 的特性,全部 Spring Boot AutoConfiguration 魔法都是基於它。mysql
在開發基於 Spring 的應用程序時,咱們可能需條件性地註冊 Bean。web
例如,你但願在本地運行應用程序時註冊一個指向 DEV 數據庫的 DataSource bean,在生產中指向 PRODUCTION 數據庫。spring
你能夠將數據庫鏈接參數外部化,放置在屬性(properties)文件中,並在適當的環境中使用該個文件。可是,當你須要指向不一樣的環境並構建應用程序時,你得更改配置。sql
爲了解決這個問題,Spring 3.1 引入了 Profiles 概念。你能夠註冊多個相同類型的 bean,並將它們與一個或多個配置文件相關聯。在運行應用程序時,你能夠激活所需的配置文件,只有與配置文件相關聯的 bean 才被註冊。mongodb
@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 方式是遠遠不夠的。apache
爲了讓有條件性註冊 Spring bean 變得更加靈活,Spring 4 引入了 @Conditional 概念。經過使用 @Conditional 方式,你能夠根據任意條件註冊一個 bean。tomcat
例如,你可能想在以下狀況註冊一個 bean:springboot
這裏只是列舉了幾個例子,你能夠根據本身的須要定義任何條件。
讓咱們來看看 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 自動配置魔法的關鍵在於 @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 類:
等等
我但願如今你已經對 「Spring Boot 自動配置如何經過 AutoConfiguration 類以及 @Conditional 工做」有了必定的認識。