ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JDBC数据库连接全解析:从Driver接口到DriverManager工作原理

JDBC数据库连接全解析:从Driver接口到DriverManager工作原理 1. JDBC到底是干什么的先想一个问题Java程序要操作数据库总不能直接去调MySQL、Oracle、PostgreSQL各自的私有API吧每换一个数据库就把代码重写一遍这谁顶得住。JDBCJava Database Connectivity就是Java官方为了解决这个问题定的一套统一数据库访问接口标准。说直白点JDBC就是Java程序和数据库之间的一座桥。它把数据库连接、SQL语句执行、结果集处理、事务管理这些动作全部抽象成标准接口底层具体怎么跟数据库交互由各个数据库厂商提供的驱动来实现。咱们写代码的人只需要面向JDBC接口编程不用管对面是MySQL还是PostgreSQL换数据库的时候只要换驱动jar包和连接URL就行核心代码基本不用动。这篇文章主要想聊JDBC里最基础也是最重要的一个环节——获取数据库连接尤其是通过Driver接口这条路径。很多刚学Java Web的同学会用conn DriverManager.getConnection(url, user, password)但DriverManager内部到底怎么跟Driver接口协作的如果绕过DriverManager直接用Driver行不行这两种方式有什么差别这些细节很多人是模糊的。我尽量用大白话加代码把这一块讲透。2. JDBC的核心组件和它们的分工2.1 Driver数据库驱动的代言人每个数据库厂商都会提供一个jar包里面有个核心类实现java.sql.Driver接口。比如MySQL的驱动类叫com.mysql.cj.jdbc.DriverPostgreSQL的叫org.postgresql.Driver。这个接口有五个方法要实现但真正核心的就两个connect(String url, Properties info)根据URL和连接参数建立真实数据库连接成功就返回Connection对象失败返回null或抛异常。acceptsURL(String url)判断这个驱动是否能处理给定的JDBC URL。你可以把Driver理解成“翻译官”Java标准说的那些话由它翻译成数据库能听懂的方言然后带着你的账号密码去敲门建立连接。值得一提的是新版MySQL驱动类名从旧的com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。很多人用了新版驱动还在填老类名结果ClassNotFoundException上来就怀疑环境有问题其实就这一个小区别。2.2 DriverManager驱动注册中心和连接分发器DriverManager是一个工具类它的职责有两个第一维护一个已注册驱动的列表。Driver接口的实现类在被加载时会主动调用DriverManager.registerDriver()把自己登记上去存在一个CopyOnWriteArrayList里。第二根据调用方传入的JDBC URL依次遍历已注册的所有驱动逐个调用acceptsURL()方法看哪个驱动认领这个URL认领成功就用它去connect()。用生活化的方式理解DriverManager就像一个快递分拣中转站你拿着写好的地址上门寄件它先帮你找哪个快递公司能送这个片区找到了再让这家公司的人去实际配送。2.3 Connection连接就是会话通道Connection接口代表Java程序与数据库之间的一条物理连接会话。它会创建Statement、PreparedStatement来执行SQL控制事务的提交commit()、回滚rollback()还能查到数据库的元数据。一条Connection在生命周期内可以执行多条SQL但默认情况下每一条SQL执行完是自动提交的这个后面聊到事务的时候再展开。2.4 Statement和ResultSetSQL执行与结果读取Statement负责把SQL字符串发给数据库执行ResultSet是查询结果集的迭代器接口。这里不多展开因为本篇文章重点是连接阶段但顺带提一句真实项目中几乎不用Statement因为存在SQL注入风险大家通常用PreparedStatement做参数化查询。后续如果有时间单独写一篇讲PreparedStatement的占位符机制和安全原理。3. 通过Driver接口获取连接的前世今生3.1 最古老的方式Class.forName手动加载驱动如果你是2010年左右接触JDBC的一定写过类似这样的代码Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/test?useSSLfalseserverTimezoneAsia/Shanghai; Connection conn DriverManager.getConnection(url, root, 123456);Class.forName()的作用是让JVM加载这个类到内存中执行它的静态初始化代码块。MySQL驱动类里有一段静态代码static { try { java.sql.DriverManager.registerDriver(new Driver()); } catch (SQLException E) { throw new RuntimeException(Cant register driver!); } }所以Class.forName()一执行驱动就自动完成注册了。然后DriverManager.getConnection()才能在你的已注册驱动列表里找到合适的驱动。这里有个很多人没注意的细节JDBC 4.0之后其实可以不用写Class.forName()这行了。因为Java引入了SPI机制驱动jar包里的META-INF/services/java.sql.Driver文件标注了驱动类全名DriverManager初始化时会自动扫描并加载这些类。也就是说新版JDBC只要把jar包放到classpath下驱动就会自动注册。但很多老代码还是习惯写上Class.forName()这没毛病只是要理解它不是强制必需的。我自己的习惯还是写上原因有两个一是让代码的意图更清晰读者一眼就知道用的是什么驱动二是在某些类加载器比较特殊的容器环境下比如复杂的Tomcat部署显式加载可以避免SPI扫描不到的情况。3.2 绕过DriverManager直接new Driver类前面都是通过DriverManager间接调用Driver接口。标题里说“通过Driver接口获取数据库连接”那就必须聊聊直接方式String url jdbc:mysql://localhost:3306/test?useSSLfalseserverTimezoneAsia/Shanghai; Properties info new Properties(); info.setProperty(user, root); info.setProperty(password, 123456); java.sql.Driver driver new com.mysql.cj.jdbc.Driver(); Connection conn driver.connect(url, info);这样写确实能拿到Connection。但问题来了——这里直接new了一个具体驱动类代码死死绑定在了MySQL上JDBC“面向接口编程、屏蔽数据库差异”的优势就没了。而且Com.mysql.cj.jdbc.Driver这个类每次new的时候都会尝试向DriverManager重复注册一遍虽然DriverManager本身做了去重处理但总归是多余的动作。所以结论很明确直接使用Driver接口获取连接适合在学习理解原理时用实际项目中基本不会这么干。DriverManager是官方推荐的门面它的存在就是让你不用关心具体Driver是谁。3.3 DriverManager中两个看起来很相似的方法日常写代码时DriverManager提供了三个重载的getConnection()方法签名分别如下方法签名使用场景getConnection(String url)连接参数全放在URL里适合测试环境快速验证getConnection(String url, String user, String password)最常用用户名密码显式传参getConnection(String url, Properties info)适合连接参数较多时统一管理比如SSL配置、连接超时时间等在实际中第二个方法最通用。第三个方法在写连接池框架或者工具类时比较常见因为Properties可以灵活扩展很多连接属性。第一个很少用因为把密码拼进URL既不好维护也容易泄露在日志中。一个容易踩坑的地方有个方法是DriverManager.getDriver(String url)它只是根据URL找到能认领它的Driver对象并不会建立真实连接。新手容易把getDriver和getConnection搞混一调getDriver返回了一个Driver实例就以为连接成功了实际上Connection还是null。这个务必留意。4. 手写一个完整的JDBC连接Demo4.1 环境准备在写代码之前先保证三样东西就位JDK 8及以上JDBC是标准APIJDK自带了java.sql包所以不需要额外引依赖MySQL数据库本地建好比如我建了一个库test_db用户root密码123456mysql-connector-java驱动包我用的是8.0.33版本。如果用的Maven在pom.xml里加dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency4.2 完整示例代码import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class JdbcConnectionDemo { public static void main(String[] args) throws ClassNotFoundException, SQLException { // 方式一显式加载驱动类JDBC 4.0之前必须写之后可省略但建议保留 Class.forName(com.mysql.cj.jdbc.Driver); // JDBC URL 格式jdbc:mysql://主机:端口/库名?参数键值对 String url jdbc:mysql://localhost:3306/test_db ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 connectTimeout5000 socketTimeout10000; // 方式二最常用直接传用户名密码 try (Connection conn DriverManager.getConnection(url, root, 123456)) { System.out.println(连接成功conn conn); } // 方式三Properties传参适合复杂场景 Properties props new Properties(); props.setProperty(user, root); props.setProperty(password, 123456); props.setProperty(connectTimeout, 5000); props.setProperty(socketTimeout, 10000); try (Connection conn DriverManager.getConnection(url, props)) { System.out.println(Properties方式连接成功); } } }注意我用的是try-with-resources语法Connection实现了AutoCloseable接口这样无论连接是否正常关闭代码块结束都会自动释放资源。这是Java 7引入的语法强烈建议大家在日常代码中使用比手动在finally里关闭干净得多也不会漏写。4.3 URL中的参数到底在干什么MySQL 8的JDBC URL中比较关键的参数如下useSSLfalse本地开发环境不走SSL加密避免SSL握手报错或性能损耗。生产环境视情况而定一般走内网且数据库层有安全策略时也可以不开但如果数据库支持SSL且链路涉及公网建议打开。serverTimezoneAsia/Shanghai指定服务器时区。MySQL 8驱动要求必须显式设置否则会抛异常The server time zone value... 因为驱动要用Java时区和数据库时区做转换。characterEncodingutf8指定字符集编码防止中文乱码。connectTimeout5000TCP连接超时时间单位毫秒。不设置的话数据库IP不可达时程序可能要卡几十秒才报错设了5秒就能快速失败。socketTimeout10000Socket读写超时时间防止SQL执行时网络异常导致线程无限等待。这里尤其想强调serverTimezone这是MySQL 8刚升级时最常见的坑。很多人从MySQL 5.7升到8.0代码一行没改突然报时区异常其实不是代码问题是新驱动强制要求显式指定时区。设置成Asia/Shanghai就对了别设置成UTC否则查询出来的时间会比北京时间慢8小时。4.4 连接到底什么时候真正建立从TCP层面看建立一条JDBC数据库连接大致经历这么几步JVM发起TCP三次握手与数据库服务器建立网络连接。连接建立后驱动和数据库服务器进行握手协议包括认证、权限校验、字符集协商。如果配置了SSL还会做TLS握手。握手完成驱动生成Connection对象返回给客户端。所以一次getConnection()背后不只是一个对象创建而是有实实在在的网络开销。这也是为什么生产环境几乎不会每次请求都新建连接而是用连接池复用连接。5. DriverManager是怎么找到Driver的5.1 源码视角看选路逻辑可能有人好奇DriverManager为什么知道用哪个驱动。这里简单看一下它内部的getConnection方法核心逻辑private static Connection getConnection(String url, java.util.Properties info, Class? caller) throws SQLException { ClassLoader callerCL caller ! null ? caller.getClassLoader() : null; synchronized (DriverManager.class) { // 如果调用者的类加载器为空用系统类加载器兜底 if (callerCL null) { callerCL Thread.currentThread().getContextClassLoader(); } } for (DriverInfo aDriver : registeredDrivers) { // 跳过与调用者类加载器不匹配的驱动 if (isDriverAllowed(aDriver.driver, callerCL)) { try { Connection con aDriver.driver.connect(url, info); if (con ! null) { return con; } } catch (SQLException e) { // 记录异常继续尝试下一个驱动 } } } throw new SQLException(No suitable driver found for url, ...); }这段逻辑里值得留意的点它不管理协议只做分发。找不到合适驱动就抛异常报错就是那句很经典的“No suitable driver found for jdbc:xxx”。遇到这个错90%的原因是jar包没引入或者URL前缀写错。比如MySQL的URL必须以jdbc:mysql:开头Oracle的以jdbc:oracle:开头写错前缀驱动acceptsURL返回false自然就抛这个错。遍历是有顺序的越早注册的驱动越先被尝试。如果同时引入了多个驱动且URL前缀容易混淆可能会有选错驱动的风险不过现实中数据库URL前缀区分度很高基本不会冲突。connect方法返回null表示这个驱动不认领这个URL。只有真正建立连接的驱动才返回Connection对象。5.2 驱动注册的SPI时机前面说过SPI会自动加载驱动具体过程是这样的DriverManager在类初始化的时候执行了一段静态代码static { loadInitialDrivers(); println(JDBC DriverManager initialized); }loadInitialDrivers()用ServiceLoader加载META-INF/services/java.sql.Driver文件中声明的类逐个实例化。实例化时类里面的静态代码块又调用registerDriver把自己注册进去。这就是为什么jar包丢进classpath就自动可用的原因。一般来说SPI自动加载已经足够。但在OSGi环境或者某些自定义类加载器场景下ServiceLoader可能扫不到驱动这时显式Class.forName()就是必要的兜底方案。理解这个机制排查“驱动没生效”的问题时会快很多。6. 连接阶段最常见的异常和排查思路6.1 异常速查表整理一下我用JDBC这么多年遇到最高频的几类异常以及处理建议异常信息原因分析解决办法ClassNotFoundException: com.mysql.jdbc.Driver用了新版本驱动却填了旧类名或者jar包没引入使用com.mysql.cj.jdbc.Driver确认jar包在classpathNo suitable driver found for jdbc:mysql://...URL前缀不对或驱动jar确实没加载检查URL前缀是否为jdbc:mysql:确认驱动已注册Access denied for user rootlocalhost用户名密码错误或该用户没有远程访问权限核对数据库账号密码检查用户授权Connection refused: connect目标IP端口不通或者服务没启动用telnet/命令行工具测试端口连通性The server time zone value xxx is unrecognizedMySQL 8驱动要求显式指定时区URL加serverTimezoneAsia/ShanghaiPublic Key Retrieval is not allowedMySQL 8默认使用caching_sha2_password认证驱动需要获取公钥URL添加allowPublicKeyRetrievaltrue6.2 定位问题的一般套路连接失败时最常见的场景就是报Communications link failure这个异常信息比较笼统定位时一般按下面这个顺序排查第一确认数据库服务活着。用命令行、图形化工具先手动连一下能连上说明服务没问题问题在代码侧或网络侧。第二确认网络通不通。在应用服务器上执行ping和telnet命令测试数据库主机和端口ping通但telnet不通多半是防火墙拦截或端口没监听。第三在代码里打印完整URL和用户名看看是不是配置拼接错了。曾经遇到同事把密码配置里带了空格肉眼根本看不出来打印出来才发现。第四把驱动的版本和数据库版本对照一下。MySQL 8.0.33的驱动连接MySQL 5.7没问题但反过来老驱动连新数据库可能报认证协议不兼容的错。驱动版本更新不及时有时会出很多莫名其妙的问题。6.3 一个典型的“5秒连不上”的故事有次帮朋友排查一个测试环境问题。现象是应用启动后首次查询数据库要卡20多秒然后报连接超时。我先ping数据库IP延迟正常再telnet 3306端口正常。最后发现是他在URL里没配connectTimeout导致驱动在TCP层无限重试直到系统默认超时才放弃。当时数据库服务器虽然能连通但负载很高TCP握手排队时间长。加上connectTimeout5000后问题立刻变成了快速失败配合日志能马上暴露数据库侧的性能瓶颈。所以连接超时参数不是锦上添花而是必须配置的兜底保护。7. 从Driver到连接池JDBC的进阶之路7.1 为什么不建议直接用DriverManager获取连接虽然本文是用DriverManager来教学但真实的项目中几乎没有人直接这么干。原因很直接每次getConnection都走一次完整TCP握手认证流程数据库连接创建成本很高。连接数不可控并发上来数据库连接数很容易被打满。没有连接复用性能浪费严重。所以Druid、HikariCP、Tomcat JDBC Pool这些连接池工具就是来解决这些问题的。它们的核心思想很简单提前创建一批连接放在池里谁要用谁借用完了还回来避免频繁创建和销毁。理解Driver和DriverManager的工作原理以后再看连接池源码会轻松很多——连接池本质上就是对DriverManager或Driver获取到的Connection做了一层管理包装。HikariCP之所以快很多优化也发生在获取连接和归还连接的路径上比如对Connection的代理类做了字节码层面的优化但这些不展开说了以后单独写一篇。7.2 JDBC事务与自动提交的坑最后顺带聊一个和Connection强相关的话题。Connection默认是自动提交模式也就是说你执行一条SQL它立马生效事务边界根本没法控制。要做事务必须先把自动提交关掉conn.setAutoCommit(false); try { // 执行多条SQL conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); // 恢复自动提交尤其是连接池场景下必须恢复 }这里有三个细节值得注意。第一在连接池场景里还回池子之前必须把autoCommit恢复成true否则下一个借到这条连接的线程就继承了false它的SQL可能不会按预期提交排查起来非常隐蔽。第二事务只对同一个Connection内执行的SQL有效。如果想在多个服务之间做分布式事务那就得引入消息队列、本地消息表、Seata之类的方案单靠JDBC事务管不了跨库跨服务的场景。第三有的数据库DDL语句自带隐式提交比如MySQL的CREATE TABLE、ALTER TABLE等执行这类语句会让当前事务强制提交。写事务代码时注意别在事务中间混入DDL。让我用一个实际场景总结这个连接获取的核心问题我刚入行时写过一个报表系统每天凌晨跑批每次跑批都新建一个Connection跑完不关结果到第三天连接数就爆了数据库直接拒绝服务。后来才慢慢理解了连接复用的重要性也理解了DriverManager自动注册驱动的机制。从那以后写JDBC代码我养成两个习惯第一是永远用try-with-resources或者连接池来管理连接生命周期第二是设置任何项目都会配置connectTimeout和socketTimeout不给数据库“拖死”应用的机会。如果你刚接触JDBC建议把Driver接口的五个方法和DriverManager的getConnection源码好好看一看再手写一遍直接使用Driver.connect()的代码你会发现后面的内容学起来都顺了。
返回列表