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。
我們可能啟用JdbcUserDAO和MongoUserDAO中的一個接口,通過系統屬性dbType。
如果應用通過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 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類,用來檢查系統屬性dbType是MySQL,代碼如下:
public class MySQLDatabaseTypeCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata annotatedTypeMetadata) {
String enabledDBType = System.getProperty("dbType");
return "MySQL".equalsIgnoreCase(enabledDBType);
}
}
同樣地,為了檢查系統屬性dbType是MongoDB,MongoDBDatabaseTypeCondition類的實現如下:
public class MongoDBDatabaseTypeCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata annotatedTypeMetadata) {
String enabledDBType = System.getProperty("dbType");
return "MongoDB".equalsIgnoreCase(enabledDBType);
}
}
現在我們可以通過@Conditional選擇性地配置JdbcUserDAO和MongoUserDAO。如下:
@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.class和EmbeddedDatabaseType.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的自動配置機制。