SpringBoot自動配置的魔法是怎么實現的


SpringBoot 最重要的功能就是自動配置,幫我們省去繁瑣重復地配置工作。相信用過SpringBoot的人,都會被它簡潔的步驟所驚訝。那么 SpringBoot 是如何實現自動配置的呢?

在這之前,我們需要了解Spring的@Conditional注解特性,SpringBoot的自動配置魔法正是基於此實現的。

探尋@Conditional的魔力

當開發基於Spring的應用時,我們可能會選擇性的注冊Bean。

比如說,當程序運行在本地的時候,你可能會注冊一個DataSource Bean指向dev數據庫。 當程序運行在生產環境時,將DataSource Bean指向一個生產庫。

你可以將數據庫連接參數抽取到properties文件中,並在恰當的環境中使用這個properties文件。但是當你想連接另外一個數據庫環境時,你需要修改properties文件配置。

為了處理這個問題,Spring 3.1提出來Profiles概念。你可以注冊多個相同類型的Bean,並用一個或多個profiles文件關聯。當你運行程序時,你可以激活需要的profiles文件以及與激活的profiles文件關聯的beans,並且只有這些profiles會被激活。


@Configuration
public class AppConfig
{
 @Bean
 @Profile("DEV")
 public DataSource devDataSource() {
 ...
 }
 @Bean
 @Profile("PROD")
 public DataSource prodDataSource() {
 ...
 }
}

通過系統參數 -Dspring.profiles.active=DEV,你可以指定需要激活的profile。

對於簡單的情況,比如通過激活的profile開啟或者關閉bean的注冊,這種方式很好用。但是如果你想通過一些邏輯判斷來注冊bean,那么這種方式就不那么有效了。

為了更靈活地注冊Spring beans,Spring 4提出了@Conditional概念。通過使用@Conditional,你可以根據任何條件選擇性地注冊bean。

例如,你可以根據下面這些條件來注冊bean:

  • 當classpath中存在這個指定的類時
  • 當ApplicationContext中不存在這個指定的Spring bean時
  • 當一個指定的文件存在時
  • 當配置文件中一個指定的屬性配置了時
  • 當一個指定的系統屬性存在或者不存在時

上面僅僅只是一個很小的例子,你可以制定任何你想要的規則。

我們一起來看看Spring的@Conditional究竟是怎么工作的。

假設我們有一個UserDAO接口,並有一個從數據庫獲取數據的方法。我們有兩個UserDAO接口實現類,一個叫JdbcUserDAO,連接MySQL 數據庫。另一個叫MongoUserDAO,連接MongoDB

我們可能啟用JdbcUserDAOMongoUserDAO中的一個接口,通過系統屬性dbType

如果應用通過java -jar myapp.jar -DdbType=MySQL命令啟動,那么將啟用JdbcUserDAO。否則,應用通過java -jar myapp.jar -DdbType=MONGO命令啟動,那么將啟用MongoUserDAO

UserDAOJdbcUserDAOMongoUserDAO的代碼如下:

public interface UserDAO {
    List<String> getAllUserNames();
}

public class JdbcUserDAOImpl implements UserDAO {
    @Override
    public List<String> getAllUserNames() {
        System.out.println("**** Getting usernames from RDBMS *****");
        return Arrays.asList("Siva","Prasad","Reddy");
    }
}

public class MongoUserDAOImpl implements UserDAO {
    @Override
    public List<String> getAllUserNames() {
        System.out.println("**** Getting usernames from MongoDB *****");
        return Arrays.asList("Bond","James","Bond");
    }
}

實現Condition接口的MySQLDatabaseTypeCondition類,用來檢查系統屬性dbTypeMySQL,代碼如下:

public class MySQLDatabaseTypeCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext,
                           AnnotatedTypeMetadata annotatedTypeMetadata) {
        String enabledDBType = System.getProperty("dbType");
        return "MySQL".equalsIgnoreCase(enabledDBType);
    }
}

同樣地,為了檢查系統屬性dbTypeMongoDBMongoDBDatabaseTypeCondition類的實現如下:

public class MongoDBDatabaseTypeCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext,
                           AnnotatedTypeMetadata annotatedTypeMetadata) {
        String enabledDBType = System.getProperty("dbType");
        return "MongoDB".equalsIgnoreCase(enabledDBType);
    }
}

現在我們可以通過@Conditional選擇性地配置JdbcUserDAOMongoUserDAO。如下:

@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會被注冊。如果運行程序類似於java -jar myapp.jar -DdbType=MONGODB這樣,那么只有MongoUserDAO會被注冊。

到目前為止,我們知道了如何通過System Property屬性選擇性地注冊bean。

假設我們想只有當MongoDB的驅動類"com.mongodb.Server"可以在類路徑下獲取時,才注冊MongoUserDAO,否則注冊JdbcUserDAO

為了實現這個目的,我們可以創建一個類去檢查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;
        }
    }
}

剛剛我們實現了基於是否一個類在類路徑中來注冊bean的方法。

如果要實現只有Spring中沒有UserDAO類型的bean時,才注冊MongoUserDAO,要該怎么做?

我們可以創建一個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);
    }
}

如果要實現只有當屬性app.dbType=MONGO在配置文件中被設置時,才注冊MongoUserDAO,要該怎么做?

我們可以這樣實現:

public class MongoDbTypePropertyCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata) {
        String dbType = conditionContext.getEnvironment().getProperty("app.dbType");
        return "MONGO".equalsIgnoreCase(dbType);
    }
}

我們已經舉了很多例子去實現條件注冊。但是,通過使用注解還有一種更優雅地方式去實現條件注冊。我們創建一個 @DatabaseType 注解,而不用為MySQL和MongoDB都實現Condition類,如下:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(DatabaseTypeCondition.class)
public @interface DatabaseType {
    String value();
}

然后我們可以實現DatabaseTypeCondition類,使用 @DatabaseType 的value值來判斷是否注冊。代碼如下:

public class DatabaseTypeCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext,
                           AnnotatedTypeMetadata annotatedTypeMetadata) {
        Map<String, Object> annotationAttributes = annotatedTypeMetadata
                .getAnnotationAttributes(DatabaseType.class.getCanonicalName());
        if (annotationAttributes == null) {
            return false;
        }
        String type = (String) annotationAttributes.get("value");
        String enabledType = System.getProperty("dbType", "MySQL");
        return type != null && type.equalsIgnoreCase(enabledType);
    }
}

現在我們可以使用 @DatabaseType 來配置我們的bean,具體如下:

@Configuration
@ComponentScan
public class AppConfig {
    @Bean
    @DatabaseType("MYSQL")
    public UserDAO jdbcUserDAO(){
        return new JdbcUserDAOImpl();
    }
    @Bean
    @DatabaseType("MONGO")
    public UserDAO mongoUserDAO(){
        return new MongoUserDAOImpl();
    }
}

這里我們從 @DatabaseType 注解中獲取元數據,並與系統屬性dbType比較,從而決定是否注冊bean。

我們已經通過許多例子來理解如何通過 @Conditional 來控制bean的注冊。

SpringBoot中廣泛地使用 @Conditional 特性來進行條件注冊。

你可以在spring-boot-autoconfigure-{version}.jar的org.springframework.boot.autoconfigure包下找到各種各樣的Condition實現類。

至此,我們知道了SpringBoot如何使用 @Conditional 特性來選擇性地注冊bean,但是自動配置機制是如何觸發的呢?

我們接着往下看。

Spring Boot 自動配置

Spring Boot 自動配置魔法的關鍵是 @EnableAutoConfiguration 注解。

通常我們使用 @SpringBootApplication 來注解一個應用的入口類,也可以使用以下注解來定義:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application{

}

@EnableAutoConfiguration注解能夠開啟Spring ApplicationContext 的自動配置功能,通過掃描類路徑下的組件並注冊符合條件的bean。

SpringBoot 在spring-boot-autoconfigure-{version}.jar中提供了各種各樣的AutoConfiguration類,負責注冊各種各樣的組件。

通常,AutoConfiguration 類被 @Configuration 注解,表明這是一個Spring 的配置類。如果被 @EnableConfigurationProperties 注解,則可以綁定自定義的屬性。

例如,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration類:

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
		DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {

	@Configuration
	@Conditional(EmbeddedDatabaseCondition.class)
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import(EmbeddedDataSourceConfiguration.class)
	protected static class EmbeddedDatabaseConfiguration {

	}

	@Configuration
	@Conditional(PooledDataSourceCondition.class)
	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
	@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
			DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
			DataSourceJmxConfiguration.class })
	protected static class PooledDataSourceConfiguration {

	}
	
	...
	...
}

DataSourceAutoConfiguration上注解着 @ConditionalOnClass({ DataSource.class,EmbeddedDatabaseType.class }) ,說明只有在DataSource.classEmbeddedDatabaseType.class類在類路徑下可獲得的情況下,自動配置才會生效。

同時,這個類上面還注解着 @EnableConfigurationProperties(DataSourceProperties.class),也就是說它能夠自動地將application.properties中的屬性綁定到DataSourceProperties類上的屬性。

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    ...
    ...
    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

你可能還會看到一些其他的條件注解,例如 @ConditionalOnMissingBean@ConditionalOnClass@ConditionalOnProperty 等等。

只有這些條件滿足時,bean才會被注冊到ApplicationContext

spring-boot-autoconfigure-{version}.jar中能找到許多其他的自動配置類,例如:

  • 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.JacksonAutoConfigurationetc 等等。

一句話總結下來就是,SpringBoot通過 @Conditional 以及各種各樣的自動配置類實現SpringBoot的自動配置機制。


免責聲明!

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



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