Day55——SpringBoot2.x版本的jdbc&自动配置原理

一. 问题背景

前面学习了Day54——数据访问简介以及准备工程环境,今天来学习jdbc和自动配置原理

二. jdbc

2.1 简单的jdbc

工程环境在Day54——数据访问简介以及准备工程环境已经搭好了,要使用jdbc,只需在配置文件做配置即可。

application.yaml配置如下:

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.32.103:3306/jdbc
    driver-class-name: com.mysql.jdbc.Driver

注意:这里mysql使用的是Docker里面的mysql容器,因此url是使用虚拟机的ip地址,不是localhost。jdbc是数据库

SpringBoot的test类里面做测试,如下:

@SpringBootTest
class SpringBoot06DataJdbcApplicationTests {

    @Autowired
    DataSource dataSource;

    //SrpingBoot默认使用了HiKari数据源
    @Test
    void contextLoads() throws SQLException {
        //数据源使用的是:class com.zaxxer.hikari.HikariDataSource
        System.out.println("dataSorce.class:" + dataSource.getClass());
        Connection connection = dataSource.getConnection();
        //连接使用的是:HikariProxyConnection@1071245351 wrapping com.mysql.cj.jdbc.ConnectionImpl@4e682398
        System.out.println("connection:" + connection);
        connection.close();
    }

}

启动测试方法,如果启动报错,详细解决方案看关于启动SpringBoot的单元测试junit报错Failed to resolve org.junit.platform:junit-platform-launcher

测试结果:

SpringBoot2.x版本及以后,默认使用的DataSource是HiKari,而不是Tomcat。因为HiKari的性能远比Tomcat高。

总结:创建工程是只需添加jdbc模块、MySQL模块,在配置文件中配置数据库连接信息,就可以获取到数据库的连接。并且数据源默认使用的是HiKari。数据源信息是从DataSourceProperties获取的

2.2 设置SpringBoot启动时,能自动执行sql脚本文件

首先在application.yaml配置文件配置,如下:

spring:
  datasource:
    username: root
    password: 123456
    url: jdbc:mysql://192.168.32.103:3306/jdbc
    driver-class-name: com.mysql.jdbc.Driver
    schema:
      - classpath:department.sql
    initialization-mode: always

这里关键的是配置initialization-mode: always,否则不能启动时自动执行脚本文件,原理后面会详细讲述。schema如果不配置的话,sql脚本文件需要命名为schema-all.sql或者schema.sql

添加脚本文件:
在这里插入图片描述
添加controller,如下:

@Controller
public class HelloController {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @ResponseBody
    @GetMapping("/query")
    public Map<String, Object> map(){
        List<Map<String, Object>> list = jdbcTemplate.queryForList("select * from department");
        return list.get(0);
    }
}

SpringBoot是自动配置了jdbcTamplate的(后面会详细讲原理),这里使用jdbcTamplate操作数据查询。

由于设置了SpringBoot启动后会自动执行脚本文件,因此在SpringBoot启动后,我们再去给表添加数据,如下:
在这里插入图片描述

测试结果:
在这里插入图片描述

三. 自动配置原理

3.1 DataSourceConfiguration

涉及到自动配置,首先想到的就是xxxAutoConfiguration。这里涉及的是jdbc,所以我们去autoconfig下查看jdbc的xxxAutoConfiguration。如下:

在这里插入图片描述
我们先开DataSourceConfiguration,列出部分代码,如下:

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

        @Bean
        @ConfigurationProperties(
            prefix = "spring.datasource.hikari"
        )
        HikariDataSource dataSource(DataSourceProperties properties) {
            HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(properties, HikariDataSource.class);
            if (StringUtils.hasText(properties.getName())) {
                dataSource.setPoolName(properties.getName());
            }

            return dataSource;
        }
    }

DataSourceConfiguration里面有与上面类似的代码,分别可以配置dbcp2.BasicDataSourcehikari.HikariDataSourcedatasource.tomcat

除了以上这些数据源,还可以自定义数据源,如下:

 @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnMissingBean({DataSource.class})
    @ConditionalOnProperty(
        name = {"spring.datasource.type"}
    )
    static class Generic {
        Generic() {
        }

        @Bean
        DataSource dataSource(DataSourceProperties properties) {
            return properties.initializeDataSourceBuilder().build();
        }
    }

可以使用spring.datasource.type来指定数据源。 它关键是调用一个DataSourceBuilder的build()方法。build()方法如下:

public T build() {
   Class<? extends DataSource> type = this.getType();
   //使用反射new 一个DataSource
   DataSource result = (DataSource)BeanUtils.instantiateClass(type);
   this.maybeGetDriverClassName();
   this.bind(result);
   return result;
}

从上面看到build()里面是使用反射new 一个DataSource。

3.2 DataSourceAutoConfiguration

我们再来了解DataSourceAutoConfiguration,如下:

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(
    type = {"io.r2dbc.spi.ConnectionFactory"}
)
@EnableConfigurationProperties({DataSourceProperties.class})//从DataSourceProperties获取数据源
//导入了DataSourcePoolMetadataProvidersConfiguration以及DataSourceInitializationConfiguration
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {

可以看到它从DataSourceProperties获取数据源,导入了DataSourcePoolMetadataProvidersConfiguration以及DataSourceInitializationConfiguration

我们打开DataSourceInitializationConfiguration,如下:

@Configuration(
    proxyBeanMethods = false
)
//导入了DataSourceInitalizerInvoker、DataSourceInitializationConfiguration
@Import({DataSourceInitializerInvoker.class, DataSourceInitializationConfiguration.Registrar.class})
class DataSourceInitializationConfiguration {

点击看看DataSourceInitalizerInvoker,如下:

class DataSourceInitializerInvoker implements ApplicationListener<DataSourceSchemaCreatedEvent>, InitializingBean {

它实现了ApplicationListener,监听有关事件

DataSourceInitializerInvoker中有一个关键方法afterPropertiesSet(),如下:

 public void afterPropertiesSet() {
   //1.获取Initializer
   DataSourceInitializer initializer = this.getDataSourceInitializer();
   if (initializer != null) {//2.initializer不为空,才进入
       //3.调用createSchema(),这里很关键
       boolean schemaCreated = this.dataSourceInitializer.createSchema();
       if (schemaCreated) {
           this.initialize(initializer);
       }
   }

}

我们再来看看createSchema(),如下:(注意代码中的注释,这里解释了为什么要配置initialization-mode=always

boolean createSchema() {
        /**1.获取所有的sql脚本封装成List,传入schema的值.如果没有在配置文件配置schema,
        *默认是null的
        **/
        List<Resource> scripts = this.getScripts("spring.datasource.schema", this.properties.getSchema(), "schema");
        //2.脚本不为空
        if (!scripts.isEmpty()) {
            /**3. isEnabled()返回为false就会进入if,这里很关键。获取完SQL脚本资源后,
            *会先进行判断获取的脚本是否为空,不为空还会继续调用!isEnabled()来判断,
            *如果这里isEnabled为false,那么它将会进入if,if里面会直接return,
            *那么后面的getSchemaUsername()以及getSchemaPassword()都不会执行。
            *因此要使isEnabled()返回true,那就需要配置initialization-mode=always
            **/
            if (!this.isEnabled()) {
                logger.debug("Initialization disabled (not running DDL scripts)");
                return false;
            }
           //3. 读取数据源信息
            String username = this.properties.getSchemaUsername();
            String password = this.properties.getSchemaPassword();
            //4. 运行SQL脚本
            this.runScripts(scripts, username, password);
        }

        return !scripts.isEmpty();
    }

总结1:获取脚本会根据shema的值去获取,如果配置文件没有配置schema,那么它默认值为null。

总结2:createSchema()中获取完SQL脚本资源后,会先进行判断获取的脚本是否为空,不为空还会继续调用!isEnabled()来判断,如果这里isEnabled为false,那么它将会进入if,if里面会直接return,那么后面的getSchemaUsername()以及getSchemaPassword()都不会执行。

我们再看看获取所有的脚本是怎么获取的,点击this.getScripts(),如下:

 private List<Resource> getScripts(String propertyName, List<String> resources, String fallback) {
        //前面传入了DataSourceProperties的schema值,不为空就先获取schema指定的资源
        if (resources != null) {
            return this.getResources(propertyName, resources, true);
        } else {//没有配置schema的值,则来到else
            //1. 从数据源获取platform,默认是“all”
            String platform = this.properties.getPlatform();
            List<String> fallbackResources = new ArrayList();
            /**2.回调执行,添加相应的脚本,所以我们创建表的脚本命名就需要按照以下2种规则命名
            * schema-all.sql或者schema.sql
            **/
            fallbackResources.add("classpath*:" + fallback + "-" + platform + ".sql");
            fallbackResources.add("classpath*:" + fallback + ".sql");
            return this.getResources(propertyName, fallbackResources, false);
        }
    }

总结1:paltform的默认值使all,如果没有配置schema,那么sql脚本文件的命名要按照schema-all.sql或者schema.sql格式

获取完脚本资源后,再来看看这个isEnabled()方法,前面说过了它要为true,才能获取到username那些信息,才能执行脚本文件,如下:(注意代码中的注释

 private boolean isEnabled() {
        //1.从DataSourceProperties获取mode,而它默认是EMBEDDED
        DataSourceInitializationMode mode = this.properties.getInitializationMode();
        if (mode == DataSourceInitializationMode.NEVER) {//不成立
            return false;//因为要是isEnabled()返回true,所以不能值配置initialization-mode为NEVER
        } else {
            /**2.要返会true,将initialization-mode设为不是NEVER也不是EMBEDDED即可,
            *也就是设为always
            **/
            return mode != DataSourceInitializationMode.EMBEDDED || this.isEmbedded();
        }
    }
public DataSourceProperties() {
    //默认值为EMBEDDED
    this.initializationMode = DataSourceInitializationMode.EMBEDDED;
    ...
    }

从上面看到,initializationMdoe默认是EMBEDDED,要想isEnabled()返回true,需要将它设置为always。这就是为什么要在application.yaml中配置initialization-mode=always

总结1:要使SpringBoot启动后自动运行sql脚本,只需配置initialization-mode=always即可。

再来看回DataSourceInitializerInvokerafterPropertiesSet(),如下:

 public void afterPropertiesSet() {
        DataSourceInitializer initializer = this.getDataSourceInitializer();
        if (initializer != null) {
            boolean schemaCreated = this.dataSourceInitializer.createSchema();
            if (schemaCreated) {
                this.initialize(initializer);
            }
        }

    }

执行完createSchema()方法,就会执行initialize()方法,如下:

private void initialize(DataSourceInitializer initializer) {
        try {
            this.applicationContext.publishEvent(new DataSourceSchemaCreatedEvent(initializer.getDataSource()));
            if (!this.initialized) {
                this.dataSourceInitializer.initSchema();
                this.initialized = true;
            }
        } catch (IllegalStateException var3) {
            logger.warn(LogMessage.format("Could not send event to complete DataSource initialization (%s)", var3.getMessage()));
        }

    }

其中关键的使initSchema()方法,如下:

 void initSchema() {
        List<Resource> scripts = this.getScripts("spring.datasource.data", this.properties.getData(), "data");
        if (!scripts.isEmpty()) {
            if (!this.isEnabled()) {
                logger.debug("Initialization disabled (not running data scripts)");
                return;
            }

            String username = this.properties.getDataUsername();
            String password = this.properties.getDataPassword();
            this.runScripts(scripts, username, password);
        }

    }

可以看到,initSchema和createSchema()大同小异,区别就是在于调用getScripts()方法传入的参数不同。由传入的参数可以知道,initSchema()是执行插入数据的脚本文件。而createSchema()是执行建表的脚本文件。

总结:initSchema()是执行插入数据的脚本文件。而createSchema()是执行建表的脚本文件。

3.3 JdbcTemplate自动配置原理

SpringBoot自动配置了JdbcTemplate来操作数据库。

在autoconfig/jdbc/下,有一个JdbcTemplateAutoConfiguration,来详细看看,如下:

@AutoConfigureAfter({DataSourceAutoConfiguration.class})
@EnableConfigurationProperties({JdbcProperties.class})
@Import({JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class})
public class JdbcTemplateAutoConfiguration {

可以看到它Import了JdbcTemplateConfiguration以及NamedParameterJdbcTemplateConfiguration

先来看看JdbcTemplateConfiguration,它有一个关键方法jdbcTemplate(),如下:

 @Bean
 @Primary
 JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
     JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
     Template template = properties.getTemplate();
     jdbcTemplate.setFetchSize(template.getFetchSize());
     jdbcTemplate.setMaxRows(template.getMaxRows());
     if (template.getQueryTimeout() != null) {
         jdbcTemplate.setQueryTimeout((int)template.getQueryTimeout().getSeconds());
     }

     return jdbcTemplate;
 }

它会注册JdbcTemplate。

再来看看NamedParameterJdbcTemplateConfiguration,里面有一个关键方法namedParameterJdbcTemplate(),如下:

@Bean
@Primary
NamedParameterJdbcTemplate namedParameterJdbcTemplate(JdbcTemplate jdbcTemplate) {
    return new NamedParameterJdbcTemplate(jdbcTemplate);
}

这是注册一个实名参数的bean

有问题的伙伴可留言

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值