侧边栏壁纸
博主头像
张种恩的技术小栈博主等级

行动起来,活在当下

  • 累计撰写 747 篇文章
  • 累计创建 65 个标签
  • 累计收到 39 条评论

目 录CONTENT

文章目录

SpringBoot(22)之数据源的自动配置分析

zze
zze
2018-04-09 / 0 评论 / 0 点赞 / 772 阅读 / 22038 字

不定期更新相关视频,抖音点击左上角加号后扫一扫右方侧边栏二维码关注我~正在更新《Shell其实很简单》系列

在之前的使用中我们只是在配置文件中配置了数据库连接信息然后我们就可以直接获取到数据源,原因也是因为 SpringBoot 给我们做了大量的自动配置,对应的相关自动配置类在 org.springframework.boot.autoconfigure.jdbc 包下:

image.png

查看 DataSourceConfiguration 类:

// org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration
abstract class DataSourceConfiguration {

    @SuppressWarnings("unchecked")
    protected static <T> T createDataSource(DataSourceProperties properties,
            Class<? extends DataSource> type) {
        return (T) properties.initializeDataSourceBuilder().type(type).build();
    }

    @ConditionalOnClass(org.apache.tomcat.jdbc.pool.DataSource.class)
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.tomcat.jdbc.pool.DataSource", matchIfMissing = true)
    static class Tomcat {

        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.tomcat")
        public org.apache.tomcat.jdbc.pool.DataSource dataSource(
                DataSourceProperties properties) {
            org.apache.tomcat.jdbc.pool.DataSource dataSource = createDataSource(
                    properties, org.apache.tomcat.jdbc.pool.DataSource.class);
            DatabaseDriver databaseDriver = DatabaseDriver
                    .fromJdbcUrl(properties.determineUrl());
            String validationQuery = databaseDriver.getValidationQuery();
            if (validationQuery != null) {
                dataSource.setTestOnBorrow(true);
                dataSource.setValidationQuery(validationQuery);
            }
            return dataSource;
        }

    }

    @ConditionalOnClass(HikariDataSource.class)
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true)
    static class Hikari {

        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.hikari")
        public HikariDataSource dataSource(DataSourceProperties properties) {
            return createDataSource(properties, HikariDataSource.class);
        }

    }

    @ConditionalOnClass(org.apache.commons.dbcp.BasicDataSource.class)
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.commons.dbcp.BasicDataSource", matchIfMissing = true)
    @Deprecated
    static class Dbcp {

        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.dbcp")
        public org.apache.commons.dbcp.BasicDataSource dataSource(
                DataSourceProperties properties) {
            org.apache.commons.dbcp.BasicDataSource dataSource = createDataSource(
                    properties, org.apache.commons.dbcp.BasicDataSource.class);
            DatabaseDriver databaseDriver = DatabaseDriver
                    .fromJdbcUrl(properties.determineUrl());
            String validationQuery = databaseDriver.getValidationQuery();
            if (validationQuery != null) {
                dataSource.setTestOnBorrow(true);
                dataSource.setValidationQuery(validationQuery);
            }
            return dataSource;
        }

    }

    @ConditionalOnClass(org.apache.commons.dbcp2.BasicDataSource.class)
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type", havingValue = "org.apache.commons.dbcp2.BasicDataSource", matchIfMissing = true)
    static class Dbcp2 {

        @Bean
        @ConfigurationProperties(prefix = "spring.datasource.dbcp2")
        public org.apache.commons.dbcp2.BasicDataSource dataSource(
                DataSourceProperties properties) {
            return createDataSource(properties,
                    org.apache.commons.dbcp2.BasicDataSource.class);
        }

    }

    // <1 - begin>
    @ConditionalOnMissingBean(DataSource.class)
    @ConditionalOnProperty(name = "spring.datasource.type")
    static class Generic {

        @Bean
        public DataSource dataSource(DataSourceProperties properties) {
            // <2>
            return properties.initializeDataSourceBuilder().build();
        }

    }
    // <1 - end>
}

可以看到在当前工程引入不同数据源依赖时 SpringBoot 会给我们自动注册不同类型的数据源 bean,默认提供如下几个数据源的自动配置:

org.apache.tomcat.jdbc.pool.DataSource # 因 web 场景启动器默认引入了 tomcat 依赖,所以默认使用该数据源
com.zaxxer.hikari.HikariDataSource
org.apache.commons.dbcp.BasicDataSource
org.apache.commons.dbcp2.BasicDataSource

除了上面几个可自动配置的数据源,在 <1> 处还有一个 Generic 内部类,该内部类的作用是为我们提供定制其它数据源功能的支持。它是如何让我们实现自定义数据源的呢?

首先该内部类起作用的前提是我们在 IoC 容器中没有注册数据源,并且还在配置中通过 spring.datasource.type 指定了数据源类型。满足这两个条件后才会做后续操作。

dataSource 方法是用来想容器中注册一个数据源 bean,而这个 bean 的是由 <2> 处通过 properties.initializeDataSourceBuilder() 初始化的一个数据源构建器的 build() 生成的,查看该方法:

// org.springframework.boot.autoconfigure.jdbc.DataSourceProperties#initializeDataSourceBuilder
public DataSourceBuilder initializeDataSourceBuilder() {
    return DataSourceBuilder.create(getClassLoader()).type(getType())
            .driverClassName(determineDriverClassName()).url(determineUrl())
            .username(determineUsername()).password(determinePassword());
}

该方法创建了一个数据源构建器,接着将数据库连接信息绑定到该构建器,而这些数据库连接信息的值正是我们在配置文件中配置的 spring.datasource 节下的属性值,最后返回该构建器的实例,接着调用该构建器的 build() 方法:

// org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder#build
public DataSource build() {
    Class<? extends DataSource> type = getType();
    DataSource result = BeanUtils.instantiate(type);
    maybeGetDriverClassName();
    bind(result);
    return result;
}

最终利用反射创建对应类型数据源的实例,绑定数据库连接信息,返回了数据源。

再查看 DataSourceAutoConfiguration 类:

// org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration {

    private static final Log logger = LogFactory
            .getLog(DataSourceAutoConfiguration.class);

    @Bean
    @ConditionalOnMissingBean
    public DataSourceInitializer dataSourceInitializer(DataSourceProperties properties,
            ApplicationContext applicationContext) {
        // <3>
        return new DataSourceInitializer(properties, applicationContext);
    }

    public static boolean containsAutoConfiguredDataSource(
            ConfigurableListableBeanFactory beanFactory) {
        try {
            BeanDefinition beanDefinition = beanFactory.getBeanDefinition("dataSource");
            return EmbeddedDataSourceConfiguration.class.getName()
                    .equals(beanDefinition.getFactoryBeanName());
        }
        catch (NoSuchBeanDefinitionException ex) {
            return false;
        }
    }

    @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.Tomcat.class, DataSourceConfiguration.Hikari.class,
            DataSourceConfiguration.Dbcp.class, DataSourceConfiguration.Dbcp2.class,
            DataSourceConfiguration.Generic.class })
    @SuppressWarnings("deprecation")
    protected static class PooledDataSourceConfiguration {

    }

    @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) {
            if (dataSource instanceof DataSourceProxy) {
                try {
                    return ((DataSourceProxy) dataSource).createPool().getJmxPool();
                }
                catch (SQLException ex) {
                    logger.warn("Cannot expose DataSource to JMX (could not connect)");
                }
            }
            return null;
        }

    }

    static class PooledDataSourceCondition extends AnyNestedCondition {

        PooledDataSourceCondition() {
            super(ConfigurationPhase.PARSE_CONFIGURATION);
        }

        @ConditionalOnProperty(prefix = "spring.datasource", name = "type")
        static class ExplicitType {

        }

        @Conditional(PooledDataSourceAvailableCondition.class)
        static class PooledDataSourceAvailable {

        }

    }

    static class PooledDataSourceAvailableCondition extends SpringBootCondition {

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context,
                AnnotatedTypeMetadata metadata) {
            ConditionMessage.Builder message = ConditionMessage
                    .forCondition("PooledDataSource");
            if (getDataSourceClassLoader(context) != null) {
                return ConditionOutcome
                        .match(message.foundExactly("supported DataSource"));
            }
            return ConditionOutcome
                    .noMatch(message.didNotFind("supported DataSource").atAll());
        }

        private ClassLoader getDataSourceClassLoader(ConditionContext context) {
            Class<?> dataSourceClass = new DataSourceBuilder(context.getClassLoader())
                    .findType();
            return (dataSourceClass != null) ? dataSourceClass.getClassLoader() : null;
        }

    }

    static class EmbeddedDatabaseCondition extends SpringBootCondition {

        private final SpringBootCondition pooledCondition = new PooledDataSourceCondition();

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context,
                AnnotatedTypeMetadata metadata) {
            ConditionMessage.Builder message = ConditionMessage
                    .forCondition("EmbeddedDataSource");
            if (anyMatches(context, metadata, this.pooledCondition)) {
                return ConditionOutcome
                        .noMatch(message.foundExactly("supported pooled data source"));
            }
            EmbeddedDatabaseType type = EmbeddedDatabaseConnection
                    .get(context.getClassLoader()).getType();
            if (type == null) {
                return ConditionOutcome
                        .noMatch(message.didNotFind("embedded database").atAll());
            }
            return ConditionOutcome.match(message.found("embedded database").items(type));
        }

    }

    @Order(Ordered.LOWEST_PRECEDENCE - 10)
    static class DataSourceAvailableCondition extends SpringBootCondition {

        private final SpringBootCondition pooledCondition = new PooledDataSourceCondition();

        private final SpringBootCondition embeddedCondition = new EmbeddedDatabaseCondition();

        @Override
        public ConditionOutcome getMatchOutcome(ConditionContext context,
                AnnotatedTypeMetadata metadata) {
            ConditionMessage.Builder message = ConditionMessage
                    .forCondition("DataSourceAvailable");
            if (hasBean(context, DataSource.class)
                    || hasBean(context, XADataSource.class)) {
                return ConditionOutcome
                        .match(message.foundExactly("existing data source bean"));
            }
            if (anyMatches(context, metadata, this.pooledCondition,
                    this.embeddedCondition)) {
                return ConditionOutcome.match(message
                        .foundExactly("existing auto-configured data source bean"));
            }
            return ConditionOutcome
                    .noMatch(message.didNotFind("any existing data source bean").atAll());
        }

        private boolean hasBean(ConditionContext context, Class<?> type) {
            return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                    context.getBeanFactory(), type, true, false).length > 0;
        }

    }

}

<3> 处, dataSourceInitializer() 方法给容器中注册了一个数据源初始化器,查看初始化器类:

// org.springframework.boot.autoconfigure.jdbc.DataSourceInitializer
class DataSourceInitializer implements ApplicationListener<DataSourceInitializedEvent> {

    private static final Log logger = LogFactory.getLog(DataSourceInitializer.class);

    private final DataSourceProperties properties;

    private final ApplicationContext applicationContext;

    private DataSource dataSource;

    private boolean initialized = false;

    DataSourceInitializer(DataSourceProperties properties,
            ApplicationContext applicationContext) {
        this.properties = properties;
        this.applicationContext = applicationContext;
    }

    // <4>
    @PostConstruct
    public void init() {
        if (!this.properties.isInitialize()) {
            logger.debug("Initialization disabled (not running DDL scripts)");
            return;
        }
        if (this.applicationContext.getBeanNamesForType(DataSource.class, false,
                false).length > 0) {
            this.dataSource = this.applicationContext.getBean(DataSource.class);
        }
        if (this.dataSource == null) {
            logger.debug("No DataSource found so not initializing");
            return;
        }
        // <5>
        runSchemaScripts();
    }

    private void runSchemaScripts() {
        // <6>
        List<Resource> scripts = getScripts("spring.datasource.schema",
                this.properties.getSchema(), "schema");
        if (!scripts.isEmpty()) {
            String username = this.properties.getSchemaUsername();
            String password = this.properties.getSchemaPassword();
            runScripts(scripts, username, password);
            try {
                this.applicationContext
                        .publishEvent(new DataSourceInitializedEvent(this.dataSource));
                if (!this.initialized) {
                    runDataScripts();
                    this.initialized = true;
                }
            }
            catch (IllegalStateException ex) {
                logger.warn("Could not send event to complete DataSource initialization ("
                        + ex.getMessage() + ")");
            }
        }
    }

    @Override
    public void onApplicationEvent(DataSourceInitializedEvent event) {
        if (!this.properties.isInitialize()) {
            logger.debug("Initialization disabled (not running data scripts)");
            return;
        }
        if (!this.initialized) {
            runDataScripts();
            this.initialized = true;
        }
    }

    // <8>
    private void runDataScripts() {
            // <9>
        List<Resource> scripts = getScripts("spring.datasource.data",
                this.properties.getData(), "data");
        String username = this.properties.getDataUsername();
        String password = this.properties.getDataPassword();
        runScripts(scripts, username, password);
    }

    private List<Resource> getScripts(String propertyName, List<String> resources,
            String fallback) {
        if (resources != null) {
            return getResources(propertyName, resources, true);
        }
        String platform = this.properties.getPlatform();
        List<String> fallbackResources = new ArrayList<String>();
        fallbackResources.add("classpath*:" + fallback + "-" + platform + ".sql");
        fallbackResources.add("classpath*:" + fallback + ".sql");
        return getResources(propertyName, fallbackResources, false);
    }

    // <7>
    private List<Resource> getResources(String propertyName, List<String> locations,
            boolean validate) {
        List<Resource> resources = new ArrayList<Resource>();
        for (String location : locations) {
            for (Resource resource : doGetResources(location)) {
                if (resource.exists()) {
                    resources.add(resource);
                }
                else if (validate) {
                    throw new ResourceNotFoundException(propertyName, resource);
                }
            }
        }
        return resources;
    }

    private Resource[] doGetResources(String location) {
        try {
            SortedResourcesFactoryBean factory = new SortedResourcesFactoryBean(
                    this.applicationContext, Collections.singletonList(location));
            factory.afterPropertiesSet();
            return factory.getObject();
        }
        catch (Exception ex) {
            throw new IllegalStateException("Unable to load resources from " + location,
                    ex);
        }
    }

    private void runScripts(List<Resource> resources, String username, String password) {
        if (resources.isEmpty()) {
            return;
        }
        ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
        populator.setContinueOnError(this.properties.isContinueOnError());
        populator.setSeparator(this.properties.getSeparator());
        if (this.properties.getSqlScriptEncoding() != null) {
            populator.setSqlScriptEncoding(this.properties.getSqlScriptEncoding().name());
        }
        for (Resource resource : resources) {
            populator.addScript(resource);
        }
        DataSource dataSource = this.dataSource;
        if (StringUtils.hasText(username) && StringUtils.hasText(password)) {
            dataSource = DataSourceBuilder.create(this.properties.getClassLoader())
                    .driverClassName(this.properties.determineDriverClassName())
                    .url(this.properties.determineUrl()).username(username)
                    .password(password).build();
        }
        DatabasePopulatorUtils.execute(populator, dataSource);
    }
}

<4> 处有一个初始化方法,该方法会在当前类实例创建完成之后执行,在 <5> 处执行了 runSchemaScripts() 方法,这里直接说明该方法的作用,该方法是使用来执行指定位置存放的 sql 文件中的 DDL 语句。

接着在 <6> 处通过 getScripts("spring.datasource.schema", this.properties.getSchema(), "schema") 方法获取一个 DDL 脚本资源列表。接着来到 <7> 处的 getScripts 方法,如果我们没有在配置文件中通过 spring.datasource.schema 属性指定 DDL sql 文件路径列表,那么将默认使用 classpath*:schema-all.sqlclasspath*:schema.sql 位置的资源,即会执行该 sql 资源文件中的 DDL 语句。也可以通过配置 spring.datasource.schema 属性来指定一个存放有 DDL 语句的 sql 文件资源路径列表。

可以看到该类还实现了 ApplicationListener 监听器接口,即应用程序启动完成后会调用该类实例的 onApplicationEvent 方法,在该方法中执行了 runDataScripts() 方法,而该方法的作用是用来执行指定位置存放的 sql 文件中的 DML 语句。

接着在 <8> 处的 runDataScripts() 方法中执行了 getScripts("spring.datasource.data", this.properties.getData(), "data") 来获取 DML 脚本资源列表,然后在 <9> 处执行 getScripts("spring.datasource.data", this.properties.getData(), "data") 方法,与之前的 runSchemaScripts() 类似,如果我们没有在配置文件中通过 spring.datasource.data 属性指定 DML sql 文件的路径列表,那么将默认使用 classpath*:data-all.sqlclasspath*:data.sql 位置的资源,即会执行该 sql 资源文件中的 DML 语句。也可以通过配置 spring.datasource.data 属性指定 DML sql 文件路径列表。

小结

可以在项目的 classpath 下放置存放有 DDL 语句的 schema-all.sqlschema.sql sql 脚本文件,也可以通过配置 spring.datasource.schema 属性自定义 DDL 语句 sql 文件的存放路径列表,SpringBoot 将会在项目启动时执行这些文件。

可以在项目的 classpath 下放置存放有 DML 语句的 data-all.sqldata.sql sql 脚本文件,也可以通过配置 spring.datasource.data 属性自定义 DML 语句 sql 文件的存放路径列表,SpringBoot 将会在项目启动时执行这些文件。

例:

# application.yml
spring:
  datasource:
    username: root
    password: root
    url: jdbc:mysql://192.168.202.135:3306/springboot_jdbc
    driver-class-name: com.mysql.jdbc.Driver
    schema:
      - classpath:myschema.sql
    data:
      - classpath:mydata.sql
-- myschema.sql
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
-- mydata.sql
insert into user(name) values('张三');

项目启动时将会执行 myschema.sql 创建 user 表,并会执行 mydata.sqluser 表中添加一条数据。

0

评论区