Java连接MySQL数据库
目录
Java连接MySQL数据库
<!-- 必须:MySQL驱动 -->
<!-- mysql-connector-java 是 MySQL 官方(现 Oracle)提供的 JDBC 驱动,不是 Java 标准库的一部分,但被广泛认可为连接 MySQL 的标准方式。 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.37</version>
<!-- <version>8.0.33</version> -->
</dependency>
Java数据库连接方式对比
性能:HikariCP > Druid > DBCP2 > C3P0 > JDBC原生。除了JDBC原生无连接池,其他都有连接池。
| 连接方式 | 类型 | 必需依赖 | 优点 | 缺点 | 生产推荐 |
|---|---|---|---|---|---|
| JDBC原生 | 无连接池 | mysql-connector-java | 简单易用、无额外依赖 | 性能差、无连接复用 | ❌ |
| HikariCP | 连接池 | mysql-connector-java HikariCP | 性能最优、轻量快速 | 监控功能相对简单 | ✅ |
| Druid | 连接池 | mysql-connector-java druid | 监控功能强大、防SQL注入 | 配置相对复杂 | ✅ |
| DBCP2 | 连接池 | mysql-connector-java commons-dbcp2 | 稳定成熟、Apache项目 | 性能中等、配置复杂 | ⚠️ |
| C3P0 | 连接池 | mysql-connector-java c3p0 | 历史悠久、稳定性好 | 性能最差、已过时 | ❌ |
选择建议
- 新项目: 首选 HikariCP (性能) 或 Druid (监控)
- 学习测试: 使用 JDBC原生
- 老项目: 维持现有连接池或迁移到HikariCP
所有方式都必须导入 mysql-connector-java 驱动
连接示例
JdbcExample
public class JdbcExample {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
// 1. 加载数据库驱动
// Class.forName("com.mysql.cj.jdbc.Driver");
// 2. 建立连接
String url = "jdbc:mysql://localhost:3306/test";
String username = "root";
String password = "root";
conn = DriverManager.getConnection(url, username, password);
// 3. 创建Statement对象
stmt = conn.createStatement();
// 4. 执行SQL查询
String sql = "SELECT * FROM users";
rs = stmt.executeQuery(sql);
// 5. 处理结果集
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
System.out.println("ID: " + id + ", Name: " + name + ", Email: " + email);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 关闭资源
try {
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
DruidExample
public class DruidExample {
private static DruidDataSource dataSource;
static {
// 初始化数据源
dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC");
dataSource.setUsername("root");
dataSource.setPassword("root");
// 配置连接池参数
dataSource.setInitialSize(5); // 初始连接数
dataSource.setMinIdle(5); // 最小空闲连接
dataSource.setMaxActive(20); // 最大连接数
dataSource.setMaxWait(60000); // 获取连接超时时间
dataSource.setTimeBetweenEvictionRunsMillis(60000); // 检查间隔
dataSource.setMinEvictableIdleTimeMillis(300000); // 最小生存时间
// DruidDataSourceFactory.createDataSource(props);
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void main(String[] args) {
try (Connection conn = getConnection()) {
String sql = "SELECT * FROM users WHERE id = ?";
try (PreparedStatement p = conn.prepareStatement(sql)) {
p.setInt(1, 1);
try (ResultSet rs = p.executeQuery()) {
while (rs.next()) {
System.out.println("ID: " + rs.getInt("id"));
System.out.println("Name: " + rs.getString("name"));
System.out.println("Email: " + rs.getString("email"));
}
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
驱动选择
// JDBC原生原生的 conn = DriverManager.getConnection(url, username, password);是从已注册的drive里面顺序建立连接,连接成功就直接返回使用
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// Druid 是通过配置driverClass或者this.jdbcUrl配置选择已经注册的driver
// DruidDataSource类里的方法
protected void resolveDriver() throws SQLException {
if (this.driver != null) {
if (this.driverClass == null) {
this.driverClass = this.driver.getClass().getName();
}
} else {
if (this.driverClass == null || this.driverClass.isEmpty()) {
this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl);
}
if (MockDriver.class.getName().equals(this.driverClass)) {
this.driver = MockDriver.instance;
} else if ("com.alibaba.druid.support.clickhouse.BalancedClickhouseDriver".equals(this.driverClass)) {
Properties info = new Properties();
info.put("user", this.username);
info.put("password", this.password);
info.putAll(this.connectProperties);
this.driver = new BalancedClickhouseDriver(this.jdbcUrl, info);
} else {
if (this.jdbcUrl == null && (this.driverClass == null || this.driverClass.length() == 0)) {
throw new SQLException("url not set");
}
this.driver = JdbcUtils.createDriver(this.driverClassLoader, this.driverClass);
}
}
}
核心区别总结
1. JDBC 原生的驱动选择机制
// DriverManager.getConnection() 的工作方式
for(DriverInfo aDriver : registeredDrivers) {
// 遍历所有已注册的驱动,顺序尝试
Connection con = aDriver.driver.connect(url, info);
if (con != null) { // 第一个能识别该URL的驱动返回非null连接,就使用它
return con;
}
}
特点:
- 顺序尝试:按驱动注册顺序逐个尝试
- 第一个匹配原则:第一个能识别 URL 的驱动获胜
- 性能开销:可能需要多次尝试才能找到正确的驱动
2. Druid 的驱动选择机制
// DruidDataSource.resolveDriver() 的工作方式
protected void resolveDriver() throws SQLException {
if (this.driverClass == null || this.driverClass.isEmpty()) {
// 关键:通过URL分析直接确定驱动类名
this.driverClass = JdbcUtils.getDriverClassName(this.jdbcUrl);
}
// 直接创建指定驱动类的实例
this.driver = JdbcUtils.createDriver(this.driverClassLoader, this.driverClass);
}
特点:
- 直接定位:通过 URL 分析直接确定需要的驱动类
- 精确创建:直接实例化特定驱动,无需遍历尝试
- 性能优化:避免不必要的驱动尝试
3. JdbcUtils.getDriverClassName() 的工作原理
这个方法通过 URL 前缀直接映射到驱动类:
// JdbcUtils.getDriverClassName() 简化逻辑
public static String getDriverClassName(String url) {
if (url.startsWith("jdbc:derby:")) {
return "org.apache.derby.jdbc.EmbeddedDriver";
} else if (url.startsWith("jdbc:mysql:")) {
return "com.mysql.cj.jdbc.Driver"; // 直接返回MySQL驱动类
} else if (url.startsWith("jdbc:oracle:")) {
return "oracle.jdbc.OracleDriver";
} else if (url.startsWith("jdbc:microsoft:")) {
return "com.microsoft.jdbc.sqlserver.SQLServerDriver";
}
// ... 其他数据库映射
}
4. 性能对比
原生 JDBC:
尝试驱动A → 失败
尝试驱动B → 失败
尝试驱动C → 成功 → 返回连接
Druid:
分析URL → 确定需要驱动C → 直接创建驱动C实例 → 返回连接
5. 配置方式的灵活性
Druid 提供了多种驱动指定方式:
// 方式1:通过URL自动推断(最常用)
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
// 方式2:显式指定驱动类名
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
// 方式3:直接设置驱动实例
dataSource.setDriver(DriverManager.getDriver("jdbc:mysql://localhost:3306/test"));
6. 实际执行流程对比
原生 JDBC:
Connection conn = DriverManager.getConnection(url, user, password);
// ↑ 内部遍历所有注册的驱动,找到能处理该URL的第一个驱动
Druid:
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
Connection conn = dataSource.getConnection();
// ↑ 内部:resolveDriver() → 直接创建MySQL驱动实例 → createPhysicalConnection()
7. 优势分析
Druid 的这种设计带来了几个优势:
- 性能更好:避免驱动遍历的开销
- 确定性更强:明确知道使用哪个驱动,避免驱动冲突
- 配置更灵活:可以强制指定使用特定驱动版本
- 问题排查更容易:驱动选择逻辑清晰
8. 实际验证代码
public class DriverSelectionTest {
public static void main(String[] args) throws SQLException {
// 测试原生JDBC的驱动选择
System.out.println("=== 原生JDBC驱动选择 ===");
Connection nativeConn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test", "root", "password");
System.out.println("原生连接类: " + nativeConn.getClass().getName());
nativeConn.close();
// 测试Druid的驱动选择
System.out.println("=== Druid驱动选择 ===");
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("password");
Connection druidConn = dataSource.getConnection();
System.out.println("Druid连接类: " + druidConn.getClass().getName());
System.out.println("Druid使用的驱动类: " + dataSource.getDriver().getClass().getName());
druidConn.close();
dataSource.close();
}
}
总结
Druid 通过主动解析 URL 并直接创建特定驱动实例,避免了原生 JDBC 的顺序遍历机制,这在性能、确定性和可配置性方面都带来了优势。这是 Druid 在设计上的一个巧妙优化。
连接过程
SPI加载驱动 + 建立数据库连接
应用启动
↓
DriverManager初始化 (调用DriverManager的任何方法)
↓
ServiceLoader.load(Driver.class)
↓
扫描所有jar包的 META-INF/services/java.sql.Driver文件 # (💡SPI加载驱动)
↓
发现驱动类名:com.mysql.cj.jdbc.Driver
↓
通过反射加载驱动类
↓
执行驱动类的静态代码块
↓
DriverManager.registerDriver(new Driver()) ← # 驱动注册完成!
↓
驱动加载完成 ✅(内存中已有驱动实例)
↓
应用调用 DriverManager.getConnection()
↓
DriverManager根据URL选择合适的已注册驱动
↓
建立数据库连接 ✅
建立连接详细描述
# DriverManager.getConnection()详细描述
应用调用 DriverManager.getConnection()
↓
Driver.connect()
↓
ConnectionImpl.getInstance()
↓
new ConnectionImpl() 构造函数
↓
createNewIO()
↓
new MysqlIO() ← 关键类!
↓
MysqlIO构造函数中建立Socket连接
↓
this.socketFactory.connect(this.host, this.port, props)
↓
this.rawSocket.connect(sockAddr, this.getRealTimeout(connectTimeout)); # new Socket().connect(); // ← 最底层TCP连接!
↓
MySQL协议握手和认证
↓
建立数据库连接 ✅
连接后JDBC VS Druid
JDBC连接后直接返回
// JDBC原生的 conn = DriverManager.getConnection(url, username, password);是从已注册的drive里面顺序建立连接,连接成功就直接返回使用
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
Druid连接后放入连接池
// 创建时:部分源码 DruidDataSource的init()方法
while(this.poolingCount < this.initialSize) {
try {
PhysicalConnectionInfo pyConnectInfo = this.createPhysicalConnection();
DruidConnectionHolder holder = new DruidConnectionHolder(this, pyConnectInfo);
this.connections[this.poolingCount++] = holder; // 创建好后放入connections
} catch (SQLException var18) {
LOG.error("init datasource error, url: " + this.getUrl(), var18);
if (this.initExceptionThrow) {
connectError = var18;
break;
}
Thread.sleep(3000L);
}
}
// 获取时:
// 部分源码 DruidDataSource的getConnectionInternal(long maxWait)方法
DruidConnectionHolder holder;
if (maxWait > 0L) {
holder = this.pollLast(nanos); // 从connections拿一个,connections会少一个
} else {
holder = this.takeLast(); // 从connections拿一个,connections会少一个
}
....
holder.incrementUseCount();
DruidPooledConnection poolalbeConnection = new DruidPooledConnection(holder);
return poolalbeConnection;
// 部分源码 DruidDataSource的getConnectionDirect(long maxWaitMillis)方法
if (this.removeAbandoned) {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
poolableConnection.connectStackTrace = stackTrace;
poolableConnection.setConnectedTimeNano();
poolableConnection.traceEnable = true;
this.activeConnectionLock.lock();
try {
this.activeConnections.put(poolableConnection, PRESENT); // 放入activeConnections
} finally {
this.activeConnectionLock.unlock();
}
}
// 空闲连接(可立即使用的连接)
private volatile DruidConnectionHolder[] connections;
// 活跃连接(正在被应用使用的连接)
private final Map<DruidPooledConnection, Object> activeConnections = new IdentityHashMap<>();
MySQL 协议
JDBC MySQL 驱动其实是基于 TCP 协议,类似于 HTTP 也是基于 TCP 的协议。
当使用 jdbc:mysql://localhost:3306/mydb 时:
// JDBC 驱动内部实际上建立的是 TCP 连接
Socket socket = new Socket("localhost", 3306); // MySQL 默认端口 3306
协议特点对比
| 特性 | HTTP 协议 | MySQL 协议 |
|---|---|---|
| 基于 | TCP | TCP |
| 默认端口 | 80/443 | 3306 |
| 协议格式 | 文本协议 | 二进制协议 |
| 连接方式 | 短连接/长连接 | 长连接 |
| 认证机制 | Basic Auth/OAuth 等 | MySQL 原生认证 |
可以通过网络工具观察到:
# 查看 MySQL 连接
netstat -an | grep 3306
# 输出:tcp4 0 0 192.168.1.100.54321 192.168.1.200.3306 ESTABLISHED
MySQL 5.7 vs 8.0 主要区别对比
| 特性类别 | MySQL 5.7 | MySQL 8.0 | 说明 |
|---|---|---|---|
| 性能优化 | 基础优化 | 窗口函数、CTE、直方图 | 8.0查询性能大幅提升 |
| JSON支持 | 基础JSON功能 | JSON增强、路径表达式 | 8.0的JSON处理更强大 |
| 字符集 | 默认latin1 | 默认utf8mb4 | 8.0更好支持中文和emoji |
| 数据字典 | 基于文件 | 事务性数据字典 | 8.0更稳定可靠 |
| 窗口函数 | 不支持 | 支持 | 8.0支持高级分析函数 |
| CTE | 不支持 | 支持递归和非递归CTE | 8.0支持复杂查询 |
| 索引 | 普通索引 | 隐藏索引、降序索引 | 8.0索引功能更丰富 |
| 安全性 | 基础安全 | 角色管理、密码策略增强 | 8.0安全性更高 |
| 复制功能 | 传统复制 | 增强型复制、组复制 | 8.0高可用性更好 |
| 版本支持 | 已停止官方支持 | 官方持续维护 | 建议使用8.0 |
总结:MySQL 8.0在性能、功能和安全性方面都有显著提升,是新项目的首选版本。
MySQL 驱动主要变化
| 驱动方面 | MySQL 5.7 | MySQL 8.0 | 关键影响 |
|---|---|---|---|
| 默认驱动 | mysql-connector-java 5.x | mysql-connector-java 8.x | 版本不兼容 |
| 驱动类名 | com.mysql.jdbc.Driver |
com.mysql.cj.jdbc.Driver |
必须更新 |
| 连接URL | jdbc:mysql://host:3306/db |
需添加参数: ?serverTimezone=Asia/Shanghai&useSSL=false |
时区必须配置 |
| SSL处理 | 默认不强制SSL | 默认要求SSL连接 | 需显式关闭或配置SSL |
| 认证插件 | mysql_native_password | caching_sha2_password | 8.0默认新认证方式 |
| 时区要求 | 可选 | 必须指定serverTimezone | 避免时区错误 |
连接示例对比
// MySQL 5.7
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test", "user", "pass");
// MySQL 8.0
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useSSL=false&requireSSL=false",
"user", "pass");
注意:升级到MySQL 8.0时,应用程序的驱动配置必须相应更新。
MySQL服务和驱动不一致时,可能会有兼容性问题,最好同步更新!!!
版本匹配建议:
| 服务端版本 | 推荐驱动版本 | 驱动类名 |
|---|---|---|
| MySQL 5.6 | 5.1.x | com.mysql.jdbc.Driver |
| MySQL 5.7 | 5.1.x 或 8.0.x | com.mysql.cj.jdbc.Driver |
| MySQL 8.0 | 8.0.x | com.mysql.cj.jdbc.Driver |
MySQL 8.0 SSL 验证机制
SSL验证的几种模式
| 验证模式 | 配置参数 | 适用场景 | 证书要求 |
|---|---|---|---|
| 不验证 | useSSL=false |
开发环境 | 不需要证书 |
| 不验证证书 | useSSL=true&verifyServerCertificate=false |
内网环境 | 只需要加密,不验证身份 |
| 完全验证 | useSSL=true&verifyServerCertificate=true |
生产环境 | 需要CA证书 |
为什么驱动能"验证通过"?
// 常见的开发环境配置:
String url = "jdbc:mysql://localhost:3306/db?" +
"useSSL=true&" + // 启用SSL加密
"verifyServerCertificate=false&" + // 关键:不验证证书有效性
"requireSSL=true";
// 这样配置时:
// 1. 连接会使用SSL加密
// 2. 但不会验证服务器证书是否可信
// 3. 即使使用自签名证书也能连接成功
生产环境正确做法
// 1. 获取服务器的CA证书
// 2. 在客户端配置信任库
// 3. 启用完整验证
String url = "jdbc:mysql://localhost:3306/db?" +
"useSSL=true&" +
"verifyServerCertificate=true&" +
"requireSSL=true&" +
"trustCertificateKeyStoreUrl=file:/path/to/truststore&" +
"trustCertificateKeyStorePassword=password";
开发环境中通常通过 verifyServerCertificate=false 来绕过证书验证,只使用SSL加密功能。
浙公网安备 33010602011771号