part1-datasource-interface
约 3739 字大约 12 分钟
2026-05-24
系列文章:《深入Spring事务与数据源:从注解到数据库连接的全链路解析》
系列二第1篇:DataSource接口规范与连接池设计思想
基于版本:JDK 8+、Druid 1.2.x、Spring Framework 5.3.x
前置阅读:系列一第5篇:TransactionSynchronizationManager资源管理揭秘
前言
系列一咱们把Spring事务管理翻了个底朝天,最后追到了dataSource.getConnection()这行代码。
但是问题来了——这个getConnection()到底是怎么拿到连接的?
你可能脱口而出:"从连接池里拿的啊!"
没错,但是连接池是怎么工作的?为什么要用连接池?DataSource接口是个啥?Druid、HikariCP这些连接池是怎么实现这个接口的?
别急,这个系列咱们就来把数据源和连接池的源码整个扒一遍。而第一篇文章,咱们得先从最基础的讲起——DataSource接口规范。
说实话,很多开发者用了好几年数据库连接,都不清楚DataSource是个啥,就知道配置个URL、用户名、密码,然后就能用。但是如果你搞懂了DataSource的设计思想,再去看Druid、HikariCP的源码,那简直就是降维打击!
所以,坐稳了,咱们开始!
一、回顾一下:Spring Boot是怎么自动配置DataSource的?
老规矩,先看看自动装配的入口。毕竟这是咱们系列的传统,对吧?
核心入口1:DataSourceAutoConfiguration
文件位置:spring-boot-autoconfigure/DataSourceAutoConfiguration.java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) // 检查classpath是否有DataSource
@ConditionalOnMissingBean(DataSource.class) // 如果没有手动配置DataSource
@EnableConfigurationProperties(DataSourceProperties.class) // 绑定配置属性
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@Conditional(EmbeddedDatabaseCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import(EmbeddedDataSourceConfiguration.class)
protected static class EmbeddedDatabaseConfiguration {
// 嵌入式数据库配置(H2、HSQLDB等)
}
@Configuration(proxyBeanMethods = false)
@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 {
// 连接池配置
}
}看到没?这里根据条件导入了不同的连接池配置。Spring Boot 2.x默认使用的是HikariCP,所以会导入DataSourceConfiguration.Hikari。
核心入口2:DataSourceConfiguration.Hikari
文件位置:spring-boot-autoconfigure/DataSourceConfiguration.java
static class Hikari {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari") // 绑定配置
public HikariDataSource dataSource(DataSourceProperties properties) {
// 创建HikariDataSource
HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}如果你用的是Druid,就得自己配置:
扩展场景1:如何在Spring Boot中使用Druid?
@Configuration
public class DruidDataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSource dataSource() {
return new DruidDataSource();
}
}然后在application.yml中配置:
spring:
datasource:
druid:
url: jdbc:mysql://localhost:3306/test
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 5 # 初始连接数
max-active: 20 # 最大连接数
min-idle: 5 # 最小空闲连接数
max-wait: 60000 # 获取连接最大等待时间(毫秒)好了,自动装配的部分搞清楚了。现在问题来了——DataSource到底是个啥?
二、DataSource接口:Java EE的标准规范
DataSource可不是Spring发明的,它是Java EE的标准规范,定义在JDK的javax.sql包中。
核心接口1:DataSource
文件位置:JDK - javax/sql/DataSource.java
package javax.sql;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Wrapper;
/**
* 数据库连接的数据源接口
* 这是获取数据库连接的首选方式,比DriverManager更好
*
* 主要优势:
* 1. 可以配置连接池
* 2. 可以配置分布式事务
* 3. 可以通过JNDI查找
* 4. 更适合企业级应用
*/
public interface DataSource extends CommonDataSource, Wrapper {
/**
* 从数据源获取一个数据库连接
*
* @return 数据库连接对象
* @throws SQLException 如果获取连接失败
*/
Connection getConnection() throws SQLException;
/**
* 使用指定的用户名和密码获取数据库连接
*
* @param username 用户名
* @param password 密码
* @return 数据库连接对象
* @throws SQLException 如果获取连接失败
*/
Connection getConnection(String username, String password) throws SQLException;
/**
* 检查当前线程是否有访问数据源的权限
*
* @return 如果有权限返回true
* @throws SQLException 如果检查失败
*/
boolean isWrapperFor(Class<?> iface) throws SQLException;
}看到这个接口,你可能会说:"就这?就两个方法?"
没错!接口设计就是这样——简单、明确、职责单一。
DataSource的核心职责就是提供数据库连接,至于怎么提供、从哪提供,那是实现类的事儿。
扩展场景2:为什么不用DriverManager,而要用DataSource?
// 方式1:使用DriverManager(不推荐)
Connection con = DriverManager.getConnection(url, username, password);
// 问题:
// 1. 每次都要创建新连接,很慢
// 2. 没法做连接池
// 3. 没法做连接复用
// 方式2:使用DataSource(推荐)
Connection con = dataSource.getConnection();
// 优势:
// 1. 可以从连接池获取,很快
// 2. 连接复用,性能好
// 3. 支持事务管理扩展场景3:DataSource的继承体系
javax.sql.CommonDataSource (接口)
↓
javax.sql.DataSource (接口)
↓
javax.sql.XADataSource (接口,支持分布式事务)
↓
com.alibaba.druid.pool.DruidDataSource (实现类)
com.zaxxer.hikari.HikariDataSource (实现类)
org.apache.commons.dbcp2.BasicDataSource (实现类)三、CommonDataSource:公共的数据源接口
DataSource继承了CommonDataSource,这个接口定义了所有数据源的公共属性:
核心接口2:CommonDataSource
文件位置:JDK - javax/sql/CommonDataSource.java
package javax.sql;
import java.io.PrintWriter;
import java.sql.SQLException;
import java.util.logging.Logger;
/**
* 所有数据源的公共接口
* 定义了数据源的通用配置方法
*/
public interface CommonDataSource {
/**
* 设置日志输出流
*/
void setLogWriter(PrintWriter out) throws SQLException;
/**
* 获取日志输出流
*/
PrintWriter getLogWriter() throws SQLException;
/**
* 设置超时时间(秒)
*/
void setLoginTimeout(int seconds) throws SQLException;
/**
* 获取超时时间
*/
int getLoginTimeout() throws SQLException;
/**
* 获取父类日志
*/
Logger getParentLogger() throws SQLFeatureNotSupportedException;
/**
* 检查是否是指定接口的实现
*/
<T> T unwrap(Class<T> iface) throws SQLException;
boolean isWrapperFor(Class<?> iface) throws SQLException;
}这些方法主要是一些通用配置,比如日志、超时时间等。
四、SPI机制:ServiceLoader如何发现数据源实现?
说到DataSource,就不得不提Java的SPI(Service Provider Interface)机制。
SPI是Java提供的一种服务发现机制,它允许第三方实现标准接口,然后通过配置文件自动被发现和加载。
核心机制:ServiceLoader
文件位置:JDK - java/util/ServiceLoader.java
package java.util;
import java.util.Iterator;
import java.util.NoSuchElementException;
/**
* 服务加载器
* 用于发现和加载接口的实现类
*
* SPI的核心工作原理:
* 1. 在META-INF/services/目录下创建一个以接口全限定名命名的文件
* 2. 文件内容为实现类的全限定名
* 3. ServiceLoader读取配置文件,加载实现类
*/
public final class ServiceLoader<S> implements Iterable<S> {
// 服务配置文件目录
private static final String PREFIX = "META-INF/services/";
// 加载的服务类或接口
private final Class<S> service;
// 类加载器
private final ClassLoader loader;
// 已加载的服务实例缓存
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 延迟加载迭代器
private LazyIterator lookupIterator;
/**
* 私有构造方法
*/
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
reload();
}
/**
* 重新加载服务
*/
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
/**
* 静态工厂方法
*/
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
return new ServiceLoader<>(service, loader);
}
/**
* 静态工厂方法
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
/**
* 迭代器实现
*/
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pendingIterator = null;
public LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
@Override
public boolean hasNext() {
if (configs == null) {
try {
// 读取配置文件
String fullName = PREFIX + service.getName();
configs = loader.getResources(fullName);
} catch (IOException e) {
return false;
}
}
return pendingIterator != null && pendingIterator.hasNext();
}
@Override
public S next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
String cn = pendingIterator.next();
try {
// ★★★★★ 核心:通过反射加载并实例化类 ★★★★★
S service = Service.this.service.cast(
Class.forName(cn, true, loader).newInstance());
providers.put(cn, service);
return service;
} catch (Exception e) {
return null;
}
}
}
@Override
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator();
@Override
public boolean hasNext() {
if (knownProviders.hasNext()) return true;
return lookupIterator.hasNext();
}
@Override
public S next() {
if (knownProviders.hasNext()) {
return knownProviders.next().getValue();
}
return lookupIterator.next();
}
};
}
}SPI的工作原理:
- 在
META-INF/services/javax.sql.DataSource文件中写入实现类的全限定名 - 使用
ServiceLoader.load(DataSource.class)加载所有实现 - 遍历并实例化这些实现类
扩展场景4:Druid的SPI配置
文件位置:druid/src/main/resources/META-INF/services/java.sql.Driver
com.alibaba.druid.proxy.DruidDriver
com.alibaba.druid.mock.MockDriver这个配置文件告诉ServiceLoader,Druid实现了JDBC Driver接口。
扩展场景5:如何自己实现一个SPI?
// 1. 定义接口
public interface DatabasePool {
Connection getConnection();
void releaseConnection(Connection conn);
}
// 2. 实现接口
public class MyConnectionPool implements DatabasePool {
@Override
public Connection getConnection() {
// 实现逻辑
}
@Override
public void releaseConnection(Connection conn) {
// 实现逻辑
}
}
// 3. 在META-INF/services/com.example.DatabasePool文件中写入
com.example.MyConnectionPool
// 4. 使用ServiceLoader加载
ServiceLoader<DatabasePool> loader = ServiceLoader.load(DatabasePool.class);
for (DatabasePool pool : loader) {
Connection conn = pool.getConnection();
// 使用连接
}五、为什么需要连接池?
好了,DataSource接口搞清楚了,现在来聊聊一个更本质的问题——为什么需要连接池?
你可能觉得:"直接DriverManager.getConnection()不就完了吗?搞什么连接池,多此一举!"
那我问你一个问题:创建一个数据库连接需要多长时间?
答案是:大约10-50毫秒。
看着不多,但是你想啊,如果你的系统每秒要处理1000个请求,每个请求都要创建连接,那就是:
1000次/秒 × 30毫秒 = 30000毫秒 = 30秒也就是说,光是创建连接就要花30秒!这谁受得了?
而且,数据库连接是重量级资源:
- 需要建立TCP连接(三次握手)
- 需要进行身份验证
- 需要分配内存和线程资源
如果频繁创建和销毁连接,不仅性能差,还可能把数据库拖垮!
连接池的解决方案:
不用连接池:
请求1 → 创建连接 → 执行SQL → 销毁连接
请求2 → 创建连接 → 执行SQL → 销毁连接
请求3 → 创建连接 → 执行SQL → 销毁连接
使用连接池:
初始化 → 创建10个连接放入池中
请求1 → 从池中获取连接 → 执行SQL → 归还连接
请求2 → 从池中获取连接 → 执行SQL → 归还连接
请求3 → 从池中获取连接 → 执行SQL → 归还连接连接池的核心思想就是:提前创建好连接,用完后不销毁,而是归还到池中复用。
扩展场景6:连接池能提升多少性能?
一般来说:
- 不使用连接池:每次获取连接需要10-50毫秒
- 使用连接池:每次获取连接只需要0.1-1毫秒
性能提升了10-500倍!
六、连接池的核心设计思想
连接池虽然听起来高大上,但核心思想其实特别简单:
1. 初始化时创建一批连接(初始连接数)
2. 需要连接时,从池中获取
3. 用完连接后,归还到池中
4. 如果池中没有空闲连接,就创建新连接(但不超过最大连接数)
5. 定期检查连接的有效性,移除无效连接就这么简单!
核心数据结构:
public class SimpleConnectionPool {
// 连接池(用双端队列存储空闲连接)
private Deque<Connection> idleConnections = new ArrayDeque<>();
// 已使用的连接数
private int activeCount = 0;
// 最大连接数
private int maxPoolSize = 20;
// 最小空闲连接数
private int minIdle = 5;
/**
* 获取连接
*/
public synchronized Connection getConnection() throws SQLException {
// 1. 如果有空闲连接,直接返回
if (!idleConnections.isEmpty()) {
return idleConnections.poll();
}
// 2. 如果没有空闲连接,且未超过最大连接数,创建新连接
if (activeCount < maxPoolSize) {
Connection conn = createConnection();
activeCount++;
return conn;
}
// 3. 如果超过最大连接数,等待
throw new SQLException("连接池已满");
}
/**
* 归还连接
*/
public synchronized void releaseConnection(Connection conn) {
// 将连接放回池中
idleConnections.offer(conn);
}
/**
* 创建连接
*/
private Connection createConnection() throws SQLException {
return DriverManager.getConnection(url, username, password);
}
}看到没?这就是连接池的核心逻辑!当然,实际的连接池(比如Druid、HikariCP)要比这个复杂得多,包括:
- 连接的异步创建
- 连接的有效性检测
- 连接的超时管理
- 连接池的监控统计
- 连接的泄漏检测
但核心思想都是一样的。
扩展场景7:如果连接池满了怎么办?
有三种策略:
// 策略1:抛出异常(默认)
throw new SQLException("连接池已满");
// 策略2:阻塞等待(Druid默认)
long startTime = System.currentTimeMillis();
while (activeCount < maxPoolSize) {
if (System.currentTimeMillis() - startTime > maxWait) {
throw new SQLException("获取连接超时");
}
Thread.sleep(100);
}
// 策略3:创建临时连接(超出maxPoolSize)
Connection conn = createConnection();
// 注意:这种策略需要谨慎使用,可能导致连接数过多七、DataSource与Connection的关系
最后,咱们来理一理DataSource和Connection的关系:
DataSource(数据源)
↓ 管理
ConnectionPool(连接池)
↓ 包含多个
Connection(数据库连接)
↓ 用于执行
Statement(SQL语句)
↓ 返回
ResultSet(结果集)完整的使用流程:
// 1. 创建数据源
DataSource dataSource = new DruidDataSource();
druidDataSource.setUrl("jdbc:mysql://localhost:3306/test");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
druidDataSource.setInitialSize(5);
druidDataSource.setMaxActive(20);
// 2. 从数据源获取连接
Connection conn = dataSource.getConnection();
// 3. 创建Statement
Statement stmt = conn.createStatement();
// 4. 执行SQL
ResultSet rs = stmt.executeQuery("SELECT * FROM user");
// 5. 处理结果集
while (rs.next()) {
System.out.println(rs.getString("name"));
}
// 6. 关闭资源(重要!)
rs.close();
stmt.close();
conn.close(); // 注意:这里不是真的关闭,而是归还到连接池!关键点:conn.close()不是真的关闭连接,而是归还到连接池!
这是连接池的常见做法,通过代理模式拦截close()方法,改为调用释放逻辑。
扩展场景8:为什么不使用try-with-resources?
// 推荐做法:使用try-with-resources自动关闭
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM user")) {
while (rs.next()) {
System.out.println(rs.getString("name"));
}
} // 自动调用close(),归还连接到连接池这样写更安全,即使发生异常,资源也会被正确关闭。
八、系列二全文预览
好了,DataSource接口和连接池的基础概念咱们搞清楚了。接下来这个系列,咱们会深入到Druid的源码,看看它是怎么实现连接池的:
系列二第2篇:Druid连接池架构与核心流程
- DruidDataSource的整体架构
- DruidAbstractDataSource抽象类
- 连接池的初始化流程
- 核心配置参数详解
系列二第3篇:连接获取与回收的完整链路
getConnection()的完整流程- 连接的异步创建机制
- 连接的归还与回收
- 连接泄漏检测
系列二第4篇:Druid Filter链机制
- Filter接口设计
- Filter链的执行流程
- StatFilter统计监控
- WallFilter SQL防火墙
系列二第5篇:连接有效性检测与性能调优
- 连接有效性检测机制
- 心跳检测与保活
- Druid监控页面
- 性能调优最佳实践
九、总结
通过这篇文章,咱们搞清楚了:
- DataSource接口规范:Java EE的标准,定义了两个核心方法
- SPI机制:Java的服务发现机制,通过配置文件自动加载实现
- 为什么需要连接池:避免频繁创建连接,提升性能10-500倍
- 连接池的核心思想:提前创建、复用连接、归还不销毁
- DataSource与Connection的关系:DataSource管理连接池,Connection执行SQL
下一篇文章,咱们就正式进入Druid的源码,看看这个国产最强连接池是怎么实现的!
敬请期待!
参考资料
- JDK 8 源码
javax/sql/DataSource.javajavax/sql/CommonDataSource.javajava/util/ServiceLoader.java
- Spring Boot 2.7.x 源码
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java
- Druid 1.2.x 源码
src/main/java/com/alibaba/druid/pool/DruidDataSource.java
- 《Java核心技术卷Ⅱ》- JDBC章节
- 《深入理解Java虚拟机》- SPI机制