瞭解Spring Boot AutoConfiguration

探索 @Conditional 的力量

在開發基於 Spring 的應用程序時,我們可能需要有條件地註冊 Bean。

例如,您希望在本地運行應用程序時註冊一個指向 DEV 數據庫的 DataSource bean,在生產中運行時指向 PRODUCTION 數據庫。

您可以將數據庫連接參數外部化,放置在屬性(properties)文件中,並在適合的環境中使用這個文件。但是,當您需要指向不同的環境並構建應用程序時,您得更改配置。

為了解決這個問題,Spring 3.1 引入了 Profiles 概念。您可以註冊多個相同類型的 bean,並將它們與一個或多個配置文件相關聯。在運行應用程序時,您可以激活所需的配置文件,只有與配置文件相關聯的 bean 才被註冊。

@Configurationpublic 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 來只啟用 JdbcUserDAOMongoUserDAO 中的一個。

如果應用程序使用了 java -jar myapp.jar -DdbType = MySQL 啟動,則啟用 JdbcUserDAO,如果啟動時使用了 java -jar myapp.jar -DdbType = MONGO,則啟用 MongoUserDAO

假設我們有 UserDAO 接口,以下是 JdbcUserDAOMongoUserDAO 實現類:

public interface UserDAO{ List getAllUserNames();}public class JdbcUserDAO implements UserDAO{ @Override public List getAllUserNames() { System.out.println("**** Getting usernames from RDBMS *****"); return Arrays.asList("Siva","Prasad","Reddy"); }}public class MongoUserDAO implements UserDAO{ @Override public List getAllUserNames() { System.out.println("**** Getting usernames from MongoDB *****"); return Arrays.asList("Bond","James","Bond"); }} 

我們可以實現一個 Condition MySQLDatabaseTypeCondition 來檢查系統屬性是否是 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 有條件地配置 JdbcUserDAOMongoUserDAO beans,如下所示:

@Configurationpublic 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 運行應用程序,那麼只有 JdbcUserDAObean 被註冊。

但是如果將系統屬性設置為 -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 註冊時才註冊 MongoUserDAObean,該怎麼辦?

我們同樣可以創建一個 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 placeholder)配置文件中設置屬性 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 的方式更為優雅。而不是為 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 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@ComponentScanpublic 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} .jarorg.springframework.boot.autoconfigure 包中找到 SpringBoot 使用的各種 Condition 實現。

現在我們來了解一下 SpringBoot 如何使用 @Conditional 功能有條件地檢查是否註冊一個 bean。

究竟是什麼觸發了自動配置(auto-configuration)機制?

這就是我們下一節的內容。

SpringBoot 的 AutoConfiguration

SpringBoot 自動配置魔法的關鍵在於 @EnableAutoConfiguration 註解。通常,我們使用 @SpringBootApplication 來註解我們的應用程序入口類,如果我們要自定義默認值,我們可以使用以下註解:

Configuration@EnableAutoConfiguration@ComponentScanpublic 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.classEmbeddedDatabaseType.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/testspring.datasource.username=rootspring.datasource.password=secretspring.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功能如何工作”有了一定的認識。


分享到:


相關文章: