JDBC是Java语言操作数据库的一套接口,也就是规范,具体的实现需要各个数据库厂商去实现。比如我们在使用JDBC去连接mySql数据库的时候,我们必须要依赖一个叫做mysql-connector-java的jar包,这里面封装的就是mySql对于JDBC的实现。
首先引入mysql-connector-java依赖,比如maven项目的pom.xml文件中添加:
mysql mysql-connector-java 8.0.27
然后添加代码(一个简单的例子):
package org.example; import java.sql.*; public class JDBCTest { public static void main(String[] args) throws ClassNotFoundException, SQLException { //Driver类加载,执行其静态代码块,完成驱动注册 Class.forName("com.mysql.cj.jdbc.Driver"); //准备用户名、密码、数据库地址 String name = "root"; String password = "root"; String url = "jdbc:mysql://localhost:3306/book"; //创建数据库连接 Connection connection = DriverManager.getConnection(url, name, password); //创建一个PreparedStatement,字面意思"准备好的语句",用来执行具体的sql字符串 String sql = "select * from book"; PreparedStatement statement = connection.prepareStatement(sql); //执行sql,返回结果集 ResultSet resultSet = statement.executeQuery(); //处理结果集,逐行打印某一列数据 //ResultSet的光标每次只能指向一条记录,初始定位在在第一行之前,需要调用其next()方法移动光标才能访问记录 while (resultSet.next()) { System.out.println(resultSet.getString("book_name")); } //释放资源 resultSet.close(); statement.close(); connection.close(); } }
Spring框架对JDBC的支持,暴露在用户层面,最常见的是一个叫做JdbcTemplate的类,封装了各种对数据库的操作。
JdbcTemplate的使用:
1、确保pom.xml中有以下两个依赖(这里由于添加了spring-boot-starter-parent这种标签,可以不写版本号)
org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java
点开这个spring-boot-starter-jdbc,可以看到主要是添加了HikariCP这个数据源,以及spring-jdbc的代码库。
2、数据源属性的配置,application.yml配置文件中添加如下配置:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/book username: root password: root
3、在bean中引入JdbcTemplate依赖,业务代码中使用JdbcTemplate执行相关操作,示例如下:
package org.example.service.impl; import org.example.domain.Book; import org.example.service.BookService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Service; import java.sql.ResultSet; import java.sql.SQLException; import java.util.List; @Service public class BookServiceImpl implements BookService { @Autowired private JdbcTemplate jdbcTemplate; /** * 查询全部图书 * @return */ @Override public ListgetAllBooks() { List books = jdbcTemplate.query("select * from book", new RowMapper () { @Override public Book mapRow(ResultSet rs, int rowNum) throws SQLException { Book book = new Book(); book.setBookId(rs.getInt("book_id")); book.setBookName(rs.getString("book_name")); book.setBookDesc(rs.getString("book_desc")); book.setPublishTime(rs.getDate("publish_time")); return book; } }); return books; } /** * 更新某个图书 * @param book * @return */ @Override public Boolean updateBookMessage(Book book) { String sql = "update book set book_desc = ? where book_id = ?"; Object[] params = new Object[]{book.getBookDesc(), book.getBookId()}; jdbcTemplate.update(sql, params); return true; } }
到这里简单的JdbcTemplate的使用就完成了。
既然JdbcTemplate能用@Autowired自动注入,但我们又没有手动配置这个bean,那么它是如何自动注册成为bean,然后实例化的呢?
1、spring boot在启动过程中,在bean的实例化之前,会对配置类进行解析和注册bean definition。
首先在解析带有@SpringBootApplication的启动类时,会处理里面的@Import(AutoConfigurationImportSelector.class)这个注解(由于是DeferredImportSelector,暂存起来等启动类解析完后再处理);
处理过程中会读取META-INF目录下的spring.factories文件,其中包含了很多启用自动配置(EnableAutoConfiguration)的目标类,这里面就有和JdbcTemplate相关的JdbcTemplateAutoConfiguration类,于是我们找到了线索。
2、接下来我们找到JdbcTemplateAutoConfiguration类的源码:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass({ DataSource.class, JdbcTemplate.class }) @ConditionalOnSingleCandidate(DataSource.class) @AutoConfigureAfter(DataSourceAutoConfiguration.class) @EnableConfigurationProperties(JdbcProperties.class) @Import({ DatabaseInitializationDependencyConfigurer.class, JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class }) public class JdbcTemplateAutoConfiguration { }
这个类没有做什么实际操作,只是约束了一些自动配置的依赖关系,然后使用@Import注解引入了JdbcTemplateConfiguration这个类。
解析当前类时就会解析JdbcTemplateConfiguration类。
3、接下来看看JdbcTemplateConfiguration类:
@Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean(JdbcOperations.class) class JdbcTemplateConfiguration { @Bean @Primary JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) { JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); JdbcProperties.Template template = properties.getTemplate(); jdbcTemplate.setFetchSize(template.getFetchSize()); jdbcTemplate.setMaxRows(template.getMaxRows()); if (template.getQueryTimeout() != null) { jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds()); } return jdbcTemplate; } }
条件注解@ConditionalOnMissingBean(JdbcOperations.class):在没有JdbcOperations类型的bean时才加载当前类。
@Bean方法,返回值类型为JdbcTemplate,而JdbcTemplate恰好就实现了JdbcOperations接口,所以这里@ConditionalOnMissingBean注解的作用,就是当用户自己配置了JdbcOperations接口实现类的bean,并且已经注册时,spring就不会再自动注册jdbcTemplate了。
配置类解析完,会进行集中处理,注册bean definition。当处理到JdbcTemplateConfiguration这个配置类时,会解析@Bean方法(spring将其视为生成bean对象的工厂方法),检测到@Bean注解没有name属性,则使用方法名"jdbcTemplate"注册一个bean definition。后续在推断jdbcTemplate的类型时,会使用其工厂方法的返回类型JdbcTemplate作为目标类型。
4、现在jdbcTemplate这个bean已经注册上了,并且目标类型为JdbcTemplate。接下来在使用@Autowired注解注入jdbcTemplate实例时,就会使用到bean factory的getBeanNamesForType方法,根据类型找到jdbcTemplate这个bean,然后使用工厂方法,也就是带@Bean注解的jdbcTemplate(DataSource dataSource, JdbcProperties properties)方法进行实例化。
5、这时候只要保证 DataSource 和 JdbcProperties 这两个bean实例化完成,就能完成jdbcTemplate的实例化,我们就能用了。
实际上这个时候DataSource这个参数已经实例化完成了。
1)DataSource一个数据源接口,那么应该有一个实现类注册了这个bean;或者类似 jdbcTemplate 那样有个配置类通过@Bean方法生成这个bean。
经过debug发现,datasource这个bean,是由org.springframework.boot.autoconfigure.jdbc.DataSourceConfiguration$Hikari类的dataSource方法生成的:
@Configuration(proxyBeanMethods = false) @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") HikariDataSource dataSource(DataSourceProperties properties) { HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class); if (StringUtils.hasText(properties.getName())) { dataSource.setPoolName(properties.getName()); } return dataSource; } }
由于spring-boot-starter-jdbc默认引入了Hikari的jar包,所以spring能够加载到这个配置类,并根据@Bean方法注册了一个名为"dataSource"的bean definition。
2)实例化dataSource时,要先确保DataSourceProperties参数的实例化。
先看看DataSourceProperties的代码:
@ConfigurationProperties(prefix = "spring.datasource") public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean { private ClassLoader classLoader;
这个类不像常见的配置类那样使用@Configuration注解,而是使用了@ConfigurationProperties注解。
@ConfigurationProperties是用来绑定外部配置属性的注解,我们在applicaition.yml文件中配置的数据源属性,如下图:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/book username: root password: root
这里前缀正是对应了prefix = "spring.datasource"这个属性,包括后面的driver-class-name、url等属性,都是和DataSourceProperties类的成员变量一一对应的,后续在DataSourceProperties实例初始化的时候会进行解析和绑定。
DataSourceProperties这个bean是如何注册上的呢?
在DataSource的顶层自动配置类DataSourceAutoConfiguration中,使用了一个注解@EnableConfigurationProperties(DataSourceProperties.class),表示启用了DataSourceProperties这个类作为一个配置类,来支持外部配置的属性绑定。
在处理DataSourceAutoConfiguration并注册bean definition的过程中,调用了loadBeanDefinitionsFromRegistrars方法。这里使用了EnableConfigurationPropertiesRegistrar,顾名思义,它解析了@EnableConfigurationProperties的属性,然后注册了对应的bean definition。
DataSourceProperties这个bean又是如何绑定上我们定义的数据源配置的呢?
在DataSourceProperties实例化之后,在初始化过程中调用了initializeBean方法,允许一些BeanPostProcessor执行其postProcessBeforeInitialization方法,做特定的初始化工作;其中就有一个ConfigurationPropertiesBindingPostProcessor,通过bind方法对DataSourceProperties这个bean和配置文件中的属性进行了绑定。
还有一个问题,为什么在我们需要的jdbcTemplate实例化之前,DataSource就已经实例化了呢?
实际上在解析配置类的过程中,还有两个相关的bean被注册上了。
1)dataSourceScriptDatabaseInitializer,
有一个配置类叫做DataSourceInitializationConfiguration,它有一个@Bean方法,用来生成一个SqlDataSourceScriptDatabaseInitializer对象,这是一个数据库初始化器,用来获取数据库初始化相关的配置(比如sql脚本位置、用户名、密码等)。源码如下:
@Configuration(proxyBeanMethods = false) @ConditionalOnMissingBean({ SqlDataSourceScriptDatabaseInitializer.class, SqlR2dbcScriptDatabaseInitializer.class }) @ConditionalOnSingleCandidate(DataSource.class) @ConditionalOnClass(DatabasePopulator.class) class DataSourceInitializationConfiguration { @Bean SqlDataSourceScriptDatabaseInitializer dataSourceScriptDatabaseInitializer(DataSource dataSource, SqlInitializationProperties properties) { return new SqlDataSourceScriptDatabaseInitializer( determineDataSource(dataSource, properties.getUsername(), properties.getPassword()), properties); } private static DataSource determineDataSource(DataSource dataSource, String username, String password) { if (StringUtils.hasText(username) && StringUtils.hasText(password)) { return DataSourceBuilder.derivedFrom(dataSource).username(username).password(password) .type(SimpleDriverDataSource.class).build(); } return dataSource; } }
显然dataSourceScriptDatabaseInitializer 和我们的 jdbcTemplate 一样,也依赖 dataSource 这个 bean。
2)DependsOnDatabaseInitializationPostProcessor
它是org.springframework.boot.sql.init.dependency.DatabaseInitializationDependencyConfigurer的静态内部类,实现了BeanFactoryPostProcessor接口,部分源码如下:
public class DatabaseInitializationDependencyConfigurer implements ImportBeanDefinitionRegistrar { private final Environment environment; DatabaseInitializationDependencyConfigurer(Environment environment) { this.environment = environment; } @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { String name = DependsOnDatabaseInitializationPostProcessor.class.getName(); if (!registry.containsBeanDefinition(name)) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition( DependsOnDatabaseInitializationPostProcessor.class, this::createDependsOnDatabaseInitializationPostProcessor); registry.registerBeanDefinition(name, builder.getBeanDefinition()); } } private DependsOnDatabaseInitializationPostProcessor createDependsOnDatabaseInitializationPostProcessor() { return new DependsOnDatabaseInitializationPostProcessor(this.environment); } /** * {@link BeanFactoryPostProcessor} used to configure database initialization * dependency relationships. */ static class DependsOnDatabaseInitializationPostProcessor implements BeanFactoryPostProcessor, Ordered { private final Environment environment; DependsOnDatabaseInitializationPostProcessor(Environment environment) { this.environment = environment; }
其实DatabaseInitializationDependencyConfigurer这个类在JdbcTemplateAutoConfiguration配置类中已经使用@Import注解引入了。
由于它实现了ImportBeanDefinitionRegistrar接口,重写了registerBeanDefinitions方法,所以在解析的时候会实例化一个对象作为注册器registrar,归属到配置类JdbcTemplateAutoConfiguration中。
在处理配置类JdbcTemplateAutoConfiguration的时候,最终也会调用loadBeanDefinitionsFromRegistrars方法,然后走到DatabaseInitializationDependencyConfigurer重写的registerBeanDefinitions方法,注册了一个DependsOnDatabaseInitializationPostProcessor。
3)配置类的解析和处理,包括注册bean definition的过程,其实是spring容器启动过程中,调用BeanDefinitionRegistryPostProcessor的过程;在此之后,还有一个调用BeanFactoryPostProcessor(前者的父接口)的过程,其中就会实例化DependsOnDatabaseInitializationPostProcessor并调用其postProcessBeanFactory方法。
DependsOnDatabaseInitializationPostProcessor会使用相关的探测器detector去探测到dataSourceScriptDatabaseInitializer这个bean,
然后再使用相关探测器探测到依赖数据库初始化的bean(其中一个探测器的探测方法是寻找实现 JdbcOperations 或者 NamedParameterJdbcOperations 接口的bean),最终找到了我们的 jdbcTemplate,
然后将dataSourceScriptDatabaseInitializer设置成jdbcTemplate的依赖。所以在jdbcTemplate实例化前,已经先实例化了dataSourceScriptDatabaseInitializer以及他们依赖的DataSource。
JdbcProperties类源码:
@ConfigurationProperties(prefix = "spring.jdbc") public class JdbcProperties { private final Template template = new Template(); public Template getTemplate() { return this.template; }
JdbcProperties 和 刚才分析的 DataSourceProperties非常相似,都使用@ConfigurationProperties注解,bean注册和实例化的方式也和DataSourceProperties一样,这里不再分析。
该bean绑定的是以 spring.jdbc 为前缀的,提供给jdbcTemplate使用的配置,比如spring.jdbc.template.max-rows。