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

了解 Spring Boot AutoConfiguration的更多相关文章

  1. How Spring Boot Autoconfiguration Magic Works--转

    原文地址:https://dzone.com/articles/how-springboot-autoconfiguration-magic-works In my previous post &qu ...

  2. Spring Boot AutoConfiguration注解@ConditionalXXXX之前生今世

    1.注解@Conditional的定义 @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHO ...

  3. Spring boot 官网学习笔记 - Auto-configuration(@SpringBootApplication、@EnableAutoConfiguration、@Configuration)

    Spring Boot auto-configuration attempts to automatically configure your Spring application based on ...

  4. Spring Boot文档阅读

    原因之初 最初习惯百度各种博客教程,然后跟着操作,因为觉得跟着别人走过的路走可以少走很多弯路,省时间.然而,很多博客的内容并不够完整,甚至错误,看多了的博客甚至有千篇一律的感觉.此外,博客毕竟是记载博 ...

  5. Spring boot 内存优化

    转自:https://dzone.com/articles/spring-boot-memory-performance It has sometimes been suggested that Sp ...

  6. Spring Boot Memory Performance

    The Performance Zone is brought to you in partnership with New Relic. Quickly learn how to use Docke ...

  7. Spring Boot面试题

    Spring Boot 是微服务中最好的 Java 框架. 我们建议你能够成为一名 Spring Boot 的专家. 问题一 Spring Boot.Spring MVC 和 Spring 有什么区别 ...

  8. spring boot常见问题

    1.什么是springboot 用来简化spring应用的初始搭建以及开发过程 使用特定的方式来进行配置(properties或yml文件) 创建独立的spring引用程序 main方法运行 嵌入的T ...

  9. spring boot多数据源配置(mysql,redis,mongodb)实战

    使用Spring Boot Starter提升效率 虽然不同的starter实现起来各有差异,但是他们基本上都会使用到两个相同的内容:ConfigurationProperties和AutoConfi ...

随机推荐

  1. php apache phpmyadmin mysql环境安装

    文件下载: Apache: http://httpd.apache.org/download.cgi PHP,phpMyAdmin,mysql,API下载:http://pan.baidu.com/s ...

  2. Angular4.0学习笔记 从入门到实战打造在线竞拍网站学习笔记之二--路由

    Angular4.0基础知识见上一篇博客 路由 简介 接下来学习路由的相关知识 本来是不准备写下去的,因为当时看视频学的时候感觉自己掌握的不错 ( 这是一个灰常不好的想法 ) ,过了一段时间才发现An ...

  3. webpack模块解析

    前面的话 在web存在多种支持JavaScript模块化的工具(如requirejs和r.js),这些工具各有优势和限制.webpack基于从这些系统获得的经验教训,并将模块的概念应用于项目中的任何文 ...

  4. RobotFramework自动化测试框架的基础关键字(五)

    1.1.1        Run Keyword If判断的使用 Run Keyword If是一个常用的用来做逻辑判断的关键字,意思是如果满足了某一个判断条件,然后就会执行关键字,我们对list3中 ...

  5. 处理Oracle数据中的无效对象

    今天还原了一份数据库(在服务器上没有无效对象),还原在本地之后有三十几个无效对象,当时很是郁闷,然后我发现还原之后的数据库中缺少表! 开始我怀疑Oracle数据库的还原功能,但是在我创建表的时候发现, ...

  6. RabbitMQ --- Work Queues(工作队列)

    目录 RabbitMQ --- Hello Mr.Tua 前言 Work Queues 即工作队列,它表示一个 Producer 对应多个 Consumer,包括两种分发模式:轮循分发(Round-r ...

  7. css中的几个小tip(二)

    margin的塌陷现象 (一)在标准文档流中, 垂直方向存在margin的塌陷现象 先上段代码: <style type="text/css"> .box{ width ...

  8. 安徽省2016“京胜杯”程序设计大赛_K_纸上谈兵

    纸上谈兵 Time Limit: 1000 MS Memory Limit: 65536 KB Total Submissions: 3 Accepted: 1 Description     战国时 ...

  9. 【有意思的BUG】浏览器的Title和Icon

    序:Icon在术语上也称为"ico",比如豆瓣的ICON地址:http://img3.douban.com/favicon.ico 先贴一张图,有助于理解这篇文档的标题--< ...

  10. HTML5 新点总结-持续

    H5新的表单元素:datalist datalist的表现形式和原先的select标签相似,但是datalist想要发挥作用需要input标签的帮助:这样就可以在input标签中显示类似select下 ...