原文: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 工作”有了一定的认识。

最新文章

  1. C#的委托
  2. sh脚本异常:/bin/sh^M:bad interpreter: No such file or directory
  3. 选择Android还是选择JavaEE?
  4. 射频识别技术漫谈(9)&mdash;&mdash;动物标签HDX【worldsing笔记】
  5. c#中字符串显示上标和下标解决办法
  6. 【课程分享】Oracle数据库系统project师
  7. 解决办法:CMake编译时出现“error in configuration process project files may be invalid”
  8. TOGAF架构能力框架之架构合同、成熟度模型和架构技能框架
  9. Sublime Text 2
  10. ural 1352. Mersenne Primes
  11. 快速排序的C语言实现
  12. 【面向对象设计原则】之依赖倒置原则(DIP)
  13. Interrupt中断线程注意点
  14. multipart upload
  15. HR_Counting Valleys
  16. Mac Mojave(10.14.1)执行Matlab的mex报错
  17. HTML动画 request animation frame
  18. Linux基础知识之用户和用户组以及 Linux 权限管理
  19. Python socket文件传送md5校验
  20. Eclipse频繁崩溃问题待解决

热门文章

  1. 将java对象转成json字符串
  2. (转)上传jar包到nexus私服
  3. ES6中的Symbol类型
  4. HDU1113 Word Amalgamation
  5. [COGS 0014][网络流24题] 搭配飞行员
  6. JVM学习笔记二:垃圾收集算法
  7. Vue组件基础用法
  8. Java学生成绩
  9. 如何通过binlog获取我们想要的MySql语句?
  10. js存款计算器原生小demo