• 了解 Spring Boot AutoConfiguration


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

  • 相关阅读:
    毕设2020.02.02
    架构师眼中的高并发架构
    云时代架构读后感二
    以《淘宝网》为例,描述质量属性六个常见场景
    《架构漫谈》读后感
    云时代架构读后感一
    暑假周总结报告08
    暑假周总结报告07
    暑假周总结报告06
    假期周总结报告05
  • 原文地址:https://www.cnblogs.com/oopsguy/p/7484398.html
Copyright © 2020-2023  润新知