原文: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. swift3.0 CoreGraphics绘图-实现画板

    swift3.0对绘图的API进行了优化,看起来更swift了. 看下UI的构造.设置画笔粗细.清空面板和保存到本地 下面直接看画板文件 这里我做的比较复杂,记录触摸到的每个点,再连成路径,其实直接用 ...

  2. angularjs 动态显示内容适用于$modal

    1.创建指令 angular.module('app').directive('dynamicElement', ["$compile", function ($compile) ...

  3. webIDE 第二篇博文

    这是我做webIDE过程中的第二篇博文,之所以隔了这么长时间没更,因为确实是没有啥进度啊,没什么可写的,现在虽然依然没啥进度,但中途遇到很多坑,这些坑还是有记录下来的必要的. 因个人水平问题,可能有的 ...

  4. iOS设置圆角的常用方法

    //第一种方法:最常用的方法,但是性能最差 UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100 ...

  5. 最短路和次短路问题,dijkstra算法

    /*  *题目大意:  *在一个有向图中,求从s到t两个点之间的最短路和比最短路长1的次短路的条数之和;  *  *算法思想:  *用A*求第K短路,目测会超时,直接在dijkstra算法上求次短路; ...

  6. html5 mdn一些精彩的案例

    https://developer.mozilla.org/zh_CN/docs/Games/Examples

  7. [Usaco2015 Jan]Grass Cownoisseur Tarjan缩点+SPFA

    考试的时候忘了缩点,人为dfs模拟缩点,没想到竟然跑了30分,RB爆发... 边是可以重复走的,所以在同一个强连通分量里,无论从那个点进入从哪个点出,所有的点一定能被一条路走到. 要使用缩点. 然后我 ...

  8. sqlite 的基本使用2

    sqlite的运算符有好几种,算术运算符,比较运算符,逻辑运算符,位运算符 1,算术运算符 算术运算符主要有 + - * . % (取余)这个很简单,举一个例子就行,要达到这样的效果需要格式化行输出 ...

  9. Linux改变语言设置的命令

    --Linux语言设置--------------2013/09/22Linux中语言的设置和本地化设置真是一个很繁琐的事情,时不时的会出现乱码的情况,在这篇文章中讨论的是shell中出现乱码的一些解 ...

  10. 《Java从入门到放弃》入门篇:hibernate中的多表对应关系

    hibernate中的对应关系其实就是数据库中表的对应关系, 就跟某些电影中的某些场景是一样一样滴. 比如可以是一男一女,还可以是一男多女, 更可以是多男一女,最后最后最后还可以是多男多女!!! 有些 ...