• SpringBoot自动配置的魔法是怎么实现的


    SpringBoot 最重要的功能就是自动配置,帮我们省去繁琐重复地配置工作。相信用过SpringBoot的人,都会被它简洁的步骤所惊讶。那么 SpringBoot 是如何实现自动配置的呢?

    在这之前,我们需要了解Spring的@Conditional注解特性,SpringBoot的自动配置魔法正是基于此实现的。

    探寻@Conditional的魔力

    当开发基于Spring的应用时,我们可能会选择性的注册Bean。

    比如说,当程序运行在本地的时候,你可能会注册一个DataSource Bean指向dev数据库。 当程序运行在生产环境时,将DataSource Bean指向一个生产库。

    你可以将数据库连接参数抽取到properties文件中,并在恰当的环境中使用这个properties文件。但是当你想连接另外一个数据库环境时,你需要修改properties文件配置。

    为了处理这个问题,Spring 3.1提出来Profiles概念。你可以注册多个相同类型的Bean,并用一个或多个profiles文件关联。当你运行程序时,你可以激活需要的profiles文件以及与激活的profiles文件关联的beans,并且只有这些profiles会被激活。

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

    通过系统参数 -Dspring.profiles.active=DEV,你可以指定需要激活的profile。

    对于简单的情况,比如通过激活的profile开启或者关闭bean的注册,这种方式很好用。但是如果你想通过一些逻辑判断来注册bean,那么这种方式就不那么有效了。

    为了更灵活地注册Spring beans,Spring 4提出了@Conditional概念。通过使用@Conditional,你可以根据任何条件选择性地注册bean。

    例如,你可以根据下面这些条件来注册bean:

    • 当classpath中存在这个指定的类时
    • 当ApplicationContext中不存在这个指定的Spring bean时
    • 当一个指定的文件存在时
    • 当配置文件中一个指定的属性配置了时
    • 当一个指定的系统属性存在或者不存在时

    上面仅仅只是一个很小的例子,你可以制定任何你想要的规则。

    我们一起来看看Spring的@Conditional究竟是怎么工作的。

    假设我们有一个UserDAO接口,并有一个从数据库获取数据的方法。我们有两个UserDAO接口实现类,一个叫JdbcUserDAO,连接MySQL 数据库。另一个叫MongoUserDAO,连接MongoDB

    我们可能启用JdbcUserDAOMongoUserDAO中的一个接口,通过系统属性dbType

    如果应用通过java -jar myapp.jar -DdbType=MySQL命令启动,那么将启用JdbcUserDAO。否则,应用通过java -jar myapp.jar -DdbType=MONGO命令启动,那么将启用MongoUserDAO

    UserDAOJdbcUserDAOMongoUserDAO的代码如下:

    public interface UserDAO {
        List<String> getAllUserNames();
    }
    
    public class JdbcUserDAOImpl implements UserDAO {
        @Override
        public List<String> getAllUserNames() {
            System.out.println("**** Getting usernames from RDBMS *****");
            return Arrays.asList("Siva","Prasad","Reddy");
        }
    }
    
    public class MongoUserDAOImpl implements UserDAO {
        @Override
        public List<String> getAllUserNames() {
            System.out.println("**** Getting usernames from MongoDB *****");
            return Arrays.asList("Bond","James","Bond");
        }
    }
    

    实现Condition接口的MySQLDatabaseTypeCondition类,用来检查系统属性dbTypeMySQL,代码如下:

    public class MySQLDatabaseTypeCondition implements Condition {
        @Override
        public boolean matches(ConditionContext conditionContext,
                               AnnotatedTypeMetadata annotatedTypeMetadata) {
            String enabledDBType = System.getProperty("dbType");
            return "MySQL".equalsIgnoreCase(enabledDBType);
        }
    }
    

    同样地,为了检查系统属性dbTypeMongoDBMongoDBDatabaseTypeCondition类的实现如下:

    public class MongoDBDatabaseTypeCondition implements Condition {
        @Override
        public boolean matches(ConditionContext conditionContext,
                               AnnotatedTypeMetadata annotatedTypeMetadata) {
            String enabledDBType = System.getProperty("dbType");
            return "MongoDB".equalsIgnoreCase(enabledDBType);
        }
    }
    

    现在我们可以通过@Conditional选择性地配置JdbcUserDAOMongoUserDAO。如下:

    @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会被注册。如果运行程序类似于java -jar myapp.jar -DdbType=MONGODB这样,那么只有MongoUserDAO会被注册。

    到目前为止,我们知道了如何通过System Property属性选择性地注册bean。

    假设我们想只有当MongoDB的驱动类"com.mongodb.Server"可以在类路径下获取时,才注册MongoUserDAO,否则注册JdbcUserDAO

    为了实现这个目的,我们可以创建一个类去检查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;
            }
        }
    }
    

    刚刚我们实现了基于是否一个类在类路径中来注册bean的方法。

    如果要实现只有Spring中没有UserDAO类型的bean时,才注册MongoUserDAO,要该怎么做?

    我们可以创建一个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);
        }
    }
    

    如果要实现只有当属性app.dbType=MONGO在配置文件中被设置时,才注册MongoUserDAO,要该怎么做?

    我们可以这样实现:

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

    我们已经举了很多例子去实现条件注册。但是,通过使用注解还有一种更优雅地方式去实现条件注册。我们创建一个 @DatabaseType 注解,而不用为MySQL和MongoDB都实现Condition类,如下:

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

    然后我们可以实现DatabaseTypeCondition类,使用 @DatabaseType 的value值来判断是否注册。代码如下:

    public class DatabaseTypeCondition implements Condition {
        @Override
        public boolean matches(ConditionContext conditionContext,
                               AnnotatedTypeMetadata annotatedTypeMetadata) {
            Map<String, Object> annotationAttributes = annotatedTypeMetadata
                    .getAnnotationAttributes(DatabaseType.class.getCanonicalName());
            if (annotationAttributes == null) {
                return false;
            }
            String type = (String) annotationAttributes.get("value");
            String enabledType = System.getProperty("dbType", "MySQL");
            return type != null && type.equalsIgnoreCase(enabledType);
        }
    }
    

    现在我们可以使用 @DatabaseType 来配置我们的bean,具体如下:

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

    这里我们从 @DatabaseType 注解中获取元数据,并与系统属性dbType比较,从而决定是否注册bean。

    我们已经通过许多例子来理解如何通过 @Conditional 来控制bean的注册。

    SpringBoot中广泛地使用 @Conditional 特性来进行条件注册。

    你可以在spring-boot-autoconfigure-{version}.jar的org.springframework.boot.autoconfigure包下找到各种各样的Condition实现类。

    至此,我们知道了SpringBoot如何使用 @Conditional 特性来选择性地注册bean,但是自动配置机制是如何触发的呢?

    我们接着往下看。

    Spring Boot 自动配置

    Spring Boot 自动配置魔法的关键是 @EnableAutoConfiguration 注解。

    通常我们使用 @SpringBootApplication 来注解一个应用的入口类,也可以使用以下注解来定义:

    @Configuration
    @EnableAutoConfiguration
    @ComponentScan
    public class Application{
    
    }
    

    @EnableAutoConfiguration注解能够开启Spring ApplicationContext 的自动配置功能,通过扫描类路径下的组件并注册符合条件的bean。

    SpringBoot 在spring-boot-autoconfigure-{version}.jar中提供了各种各样的AutoConfiguration类,负责注册各种各样的组件。

    通常,AutoConfiguration 类被 @Configuration 注解,表明这是一个Spring 的配置类。如果被 @EnableConfigurationProperties 注解,则可以绑定自定义的属性。

    例如,org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration类:

    @Configuration
    @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
    @EnableConfigurationProperties(DataSourceProperties.class)
    @Import({ DataSourcePoolMetadataProvidersConfiguration.class,
    		DataSourceInitializationConfiguration.class })
    public class DataSourceAutoConfiguration {
    
    	@Configuration
    	@Conditional(EmbeddedDatabaseCondition.class)
    	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    	@Import(EmbeddedDataSourceConfiguration.class)
    	protected static class EmbeddedDatabaseConfiguration {
    
    	}
    
    	@Configuration
    	@Conditional(PooledDataSourceCondition.class)
    	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
    	@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
    			DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
    			DataSourceJmxConfiguration.class })
    	protected static class PooledDataSourceConfiguration {
    
    	}
    	
    	...
    	...
    }
    

    DataSourceAutoConfiguration上注解着 @ConditionalOnClass({ DataSource.class,EmbeddedDatabaseType.class }) ,说明只有在DataSource.classEmbeddedDatabaseType.class类在类路径下可获得的情况下,自动配置才会生效。

    同时,这个类上面还注解着 @EnableConfigurationProperties(DataSourceProperties.class),也就是说它能够自动地将application.properties中的属性绑定到DataSourceProperties类上的属性。

    @ConfigurationProperties(prefix = "spring.datasource")
    public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
        ...
        ...
        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
    

    你可能还会看到一些其他的条件注解,例如 @ConditionalOnMissingBean@ConditionalOnClass@ConditionalOnProperty 等等。

    只有这些条件满足时,bean才会被注册到ApplicationContext

    spring-boot-autoconfigure-{version}.jar中能找到许多其他的自动配置类,例如:

    • 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.JacksonAutoConfigurationetc 等等。

    一句话总结下来就是,SpringBoot通过 @Conditional 以及各种各样的自动配置类实现SpringBoot的自动配置机制。

  • 相关阅读:
    网页制作--标签,表格,表单,框架
    sql数据库小结
    数据库的触发器
    数据库的复制与附加
    sql数据库随笔
    Web窗体--控件
    练习--学生信息录入
    HTML常用数据类型
    javascript-表单验证
    javasc-正则表达式
  • 原文地址:https://www.cnblogs.com/bluemilk/p/10569720.html
Copyright © 2020-2023  润新知