文章目录
一. 问题背景
前面学习了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.BasicDataSource
、hikari.HikariDataSource
、datasource.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
即可。
再来看回DataSourceInitializerInvoker
的afterPropertiesSet()
,如下:
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
有问题的伙伴可留言