了解 Spring Boot AutoConfiguration


原文: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 來只啟用 JdbcUserDAOMongoUserDAO 其中一個。

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

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

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 條件性地配置 JdbcUserDAOMongoUserDAO 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} .jarorg.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.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/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 工作”有了一定的認識。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM