Spring Boot自动配置是如何实现的

本贴最后更新于 3028 天前,其中的信息可能已经时移世易

Spring Boot习惯优于配置的理念可以让项目快速的运行起来,使用它可以不用或者只需要很少的Spring配置,因为自动配置AutoConfiguration已经为我们做了很多工作。那么自动配置是如何实现的呢?

1 Spring中的Profiles特性

在开发Spring应用的时候,可能会根据不同的情况注册不同的bean。例如,在开发环境和生产环境会使用不同的数据源。常见的思路则是,把数据源配置写在配置文件中,但这样的话,不同的环境就需要更改相应的配置,并重新打包生成应用。

为解决这样的问题,从Spring3.1开始引入了Profiles的概念。我们可以注册多个相同类型的bean,并将它们关联到一个或者多个profiles。应用运行的时候,则可以激活期望的profiles,同时跟这些profiles相关联的beans就可以自动被注册。

对于刚才所提到的开发环境和生产环境使用不同的数据源的情况,则可以这样来做:

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

}

然后通过启动选项-Dspring.profiles.active=DEV便可注册不同的数据源了。

上面通过指定不同profiles来注册不同beans的方法,往往只适用于一些很简单的业务场景,如果注册beans的逻辑复杂起来,profiles的方式则显得力不从心了。

为了提供更多灵活的通过不同条件判断来注册beans的方式,Spring 4引入了@Conditional条件注解,可在多种条件判断下选择性的注册beans。

2 @Conditional条件注解

我们可能在如下某种情况下才需要注册某个bean,而通过Spring的@Conditional都能够实现这些既定条件下的bean注册:

  • classpath中存在某个class的时候
  • 某个指定类型的bean在ApplicationContext中不存在的时候
  • 某个文件在指定路径中存在的时候
  • 某个配置项在配置文件中存在的时候

下面就来看看Spring的@Conditional是如何工作的,以及通过它怎么实现不同条件下bean的注册。

假设有一个UserDAO接口,通过接口中的方法可从数据库中得到数据。这个接口有两个实现:JdbcUserDAO和MongoUserDAO,分别用来操作MySQL数据库和MongoDB数据库。

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");
    }
}

接下来将通过使用@Conditional条件注解,展示几种在不同条件下注册不同bean的例子。

2.1 通过启动参数注册不同的bean

我们希望通过dbType这样一个启动参数来选择性地使用其中一个实现:

(1)java -jar myapp.jar -DdbType=MySQL,使用JdbcUserDAO

(2)java -jar myapp.jar -DdbType=MONGO,使用MongoUserDAO

下面,分别实现Condition接口,用于判断dbType系统属性:

public class MySQLDatabaseTypeCondition implements Condition{
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    {
        String enabledDBType = System.getProperty("dbType");
        return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MYSQL"));
    }
}
public class MongoDBDatabaseTypeCondition implements Condition{
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    {
        String enabledDBType = System.getProperty("dbType");
        return (enabledDBType != null && enabledDBType.equalsIgnoreCase("MONGODB"));
    }
}

好了,接下来就可以使用@Conditional注解来对beans进行配置了:

@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会被注册。

2.2 通过判断classpath中的class存在与否注册不同的bean

假设如果MongoDB数据库驱动类“com.mongodb.Server”在classpath中存在,则注册MongoUserDAO这个bean,否则则注册JdbcUserDAO bean。

要完成这一需求,我们可以通过如下方式来检查驱动是否存在,同样需要实现Condition接口:

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;
        }
    }
}

如果没有类型为UserDAO的bean,则注册MongoUserDAO bean:

public class UserDAOBeanNotPresentsCondition implements Condition
{
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata metadata)
    {
        UserDAO userDAO = conditionContext.getBeanFactory().getBean(UserDAO.class);
        return (userDAO == null);
    }
}

2.3 通过配置项注册不同的bean

如果配置文件中,存在配置app.dbType=MONGO,则注册MongoUserDAO bean:

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

2.4 通过自定义注解注册不同的bean

以上我们已经看到了多种通过不同条件注册不同bean的例子,接下来展示一种更加优雅的方法,那就是通过自定义注解来完成。

首先,定义一个DatabaseType注解:

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

接下来,实现Condition接口,通过比较DatabaseType注解的值,和dbType系统参数的值,来判断使用哪个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));
    }
}

然后,则可以在获得bean的方法上使用@DatabaseType注解了:

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

}

在DatabaseTypeCondition的matches方法中,通过比较DatabaseType注解的元数据与系统属性dbType,最终确定哪个bean被注册。

以上则是几个使用@Conditional条件注解按照不同的业务逻辑注册bean的例子。在看了这么多例子之后,接下来该进入我们的主题,Spring Boot自动配置了。

3 Spring Boot自动配置

在Spring Boot中大量使用了@Conditional特性,在不同的情况下注册不同的bean。

在spring-boot-autoconfigure-{version}.jar这个文件的Org.springframework.boot.

autoconfigure包中,可以看到许多Condition接口的实现类。那么Spring Boot中的自动配置机制是如何触发的呢?

下面的代码段,大家应该都不陌生:

@Configuration
@EnableAutoConfiguration
@ComponentScan
public class Application {

}

在Spring Boot应用程序入口类中,我们一般都会使用@Configuration,@EnableAutoConfiguration,@ComponentScan三个注解,或者使用@SpringBootApplication来代替。@EnableAutoConfiguration注解则是Spring Boot中,自动配置的关键。该注解打开Spring ApplicationContext的自动配置功能,扫描classpath下的components,并根据不同的条件注册beans。

在spring-boot-autoconfigure-{version}.jar中这个jar包中,Spring Boot提供了许多AutoConfiguration类,他们负责注册不同的components。

典型的AutoConfiguration类有@Configuration和@EnableConfigurationProperties两个注解。@Configuration说明这个来是一个Spring的配置类,@EnableConfigurationProperties用来绑定自定义属性以及一个或多个bean的条件注册方法。
下面来看一下源码中的AutoDataSourceAutoConfiguration这个类,源码太长,不在这里贴出来了,完整源码请搭梯子去github看。传送门

在这个类中:

(1)@ConditionalOnClass({ DataSource.class,EmbeddedDatabaseType.class }) 注解说明,只有DataSource.class,EmbeddedDatabaseType.class在classpath中存在的情况下,类中bean的自动配置才会生效

(2)@EnableConfigurationProperties(DataSourceProperties.class) 注解说明,application.properties配置文件中的属性会与DataSourceProperties中的相应字段进行绑定。

让我们在看看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.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://192.168.249.130/jbase
spring.datasource.username=postgres
spring.datasource.password=postgres

在源码中,在许多内部类和bean定义方法上,均能看到Spring Boot的条件注解,例如:

  • @ConditionalOnMissingBean
  • @ConditionalOnClass
  • @ConditionalOnProperty

等等。只有满足条件,那些bean才会被注册。

4 小结

通过开发过程中切换不同数据源的例子,分别使用Spring中的Profiles特性以及Condition条件注解来解决这一问题,并最终简单说明了Spring Boot的自动配置。

点击下载PDF阅读版

  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    944 引用 • 1459 回帖 • 17 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...