ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JDBC连接MySQL完整教程:从驱动加载到数据库连接池实战详解

JDBC连接MySQL完整教程:从驱动加载到数据库连接池实战详解 1. 先说清楚JDBC连接MySQL到底在解决什么问题不少刚接触Java后端的朋友上来就用框架MyBatis、Hibernate一套组合拳打完反而把最底层的JDBC给忽略了。等到有一天框架报错、连接池失效、驱动冲突的时候才回头翻JDBC的文档结果发现连最基础的连接都没搞明白。换个角度说JDBC就是Java和数据库之间的一座桥而且是唯一那座官方指定的桥。不管是MyBatis还是Hibernate底层跑的都是JDBC。你把它搞懂了后面看框架源码、排查连接异常、理解连接池参数都会顺很多。这个教程适合三类人刚学完Java基础准备进入Web开发阶段的初学者工作中需要写数据层代码但一直用框架、没系统看过JDBC的后端开发做运维或测试需要写一些临时脚本批量操作MySQL数据的同学先说结论JDBC连接MySQL核心就那么几步——加载驱动、获取连接、执行SQL、处理结果、关闭资源。但每一步往深了挖都有坑。这篇文章我就按自己的实操习惯把从环境准备到最终代码的完整链路走一遍顺手把踩过的坑都标出来。2. 环境准备JDK、MySQL、IDEA的版本搭配心得2.1 JDK版本怎么选JDBC本身对JDK版本要求不苛刻JDK 8到JDK 21都能跑。但如果你的项目后续要接Spring Boot我建议直接用JDK 8或JDK 11这两个版本在大多数公司的存量项目里最常用而且跟各种中间件的兼容性最好。我自己日常开发用的是JDK 8虽然有点老但胜在稳定。新学JDBC的同学没必要在这上面花太多时间纠结一个能用的JDK就够了。2.2 MySQL 5.7还是8.0这个选择直接影响驱动版本和URL参数所以单独拎出来说。MySQL 5.7老项目的主力驱动用com.mysql.jdbc.DriverURL里一般不用加时区参数也能跑。MySQL 8.0现在新项目的主流驱动类名改成了com.mysql.cj.jdbc.DriverURL必须带serverTimezone参数否则会报时区错误。我在实操中遇到过最典型的场景本地装的是MySQL 8.0公司测试环境是5.7结果代码在本地跑得好好的一部署到测试环境就报驱动类找不到。原因就是两个版本驱动类名不一致代码里写死了。所以写代码的时候最好把驱动类名提出来做成配置别硬编码在代码里。2.3 MySQL安装的几个提醒MySQL的安装分两种一种是装到本机当开发库另一种是用Docker跑一个临时实例。两者各有优劣。本机安装适合长期开发但安装过程容易踩坑。以Windows为例MySQL 8.0的安装包下载后一路Next基本没问题但有两个地方要注意选Server Only还是Full建议选Server Only开发机没必要装一堆额外组件。设置密码时选Use Strong Password Encryption记住自己的密码后面连接要用。装完以后MySQL默认端口3306一般不需要改但如果本机3306被占用可以改成3307对应修改代码里的URL端口即可。Docker方式适合临时测试一条命令搞定docker run --name mysql-test -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -d mysql:8.0注意容器里的MySQL默认字符集是latin1连接中文数据会出现乱码。建议加一个启动参数docker run --name mysql-test -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -d mysql:8.0 --character-set-serverutf8mb4 --collation-serverutf8mb4_unicode_ci这是因为MySQL 8.0的容器镜像默认字符集不是utf8mb4插入中文数据后查询出来的内容全是问号排查半天才发现是字符集的问题。2.4 IDEA版本和连接数据库的功能说明很多人在搜IDEA社区版能不能连接数据库这里统一说下。IDEA的社区版Community Edition确实砍掉了Database Tools功能只有一个基础的数据查看插件只能看个大概。如果你要可视化操作数据库有两条路用IDEA Ultimate版付费内置Database面板功能接近Navicat的轻量版用Navicat、DBeaver、MySQL Workbench等独立工具比较务实的选择是开发用DBeaver免费且跨平台社区活跃度也够高。如果你坚持用IDEA社区版至少装一个MySQL Workbench兜底。3. 打通第一个JDBC连接驱动加载的底层逻辑3.1 驱动下载与Maven依赖的经典坑JDBC驱动本身是一个JAR包有两种方式引入项目。方式一Maven依赖dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency这是最推荐的方式版本管理交给Maven就行。8.0.33是目前8.0分支的最新版稳定性和兼容性都验证过。方式二手动下载JAR包去MySQL官网下载mysql-connector-j-8.0.33.jar然后放到项目的lib目录下。这种方式适合不用Maven的老项目。这里必须说一下新手常踩的坑IDEA里首次引入Maven依赖时经常卡在Download from Maven failed。出现这个报错的原因主要有两个本地Maven仓库里没有这个依赖需要从中央仓库下载但网络不稳定导致超时用的是IDEA自带的Maven它的中央仓库地址在某些环境访问不了解决办法找到Maven的settings.xml把中央仓库镜像换成阿里云镜像mirror idaliyun/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror改完以后记得在IDEA里执行Reload All Maven Projects或者直接在Maven工具窗口点刷新按钮。3.2 加载驱动到底发生了什么先看最基础的代码Class.forName(com.mysql.cj.jdbc.Driver);这一行看似简单实际干了两件事通过反射机制把Driver类加载到JVM中触发Driver类的静态代码块自动注册一个驱动实例到DriverManager这里有个细节JDBC 4.0以后其实不需要手动Class.forName了只要驱动JAR包在classpath中DriverManager会自动发现驱动。但为了兼容老版本和明确表达意图建议还是保留这行。如果你用的是MySQL 5.7的驱动这行代码要改成Class.forName(com.mysql.jdbc.Driver);两个驱动类名的区别是后缀多了cj很容易被忽略报错却是实打实的ClassNotFoundException。3.3 第一个连接测试的完整代码直接上代码每一步带注释import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class JdbcFirstDemo { public static void main(String[] args) { // 数据库连接四要素 String url jdbc:mysql://localhost:3306/test_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; String user root; String password 123456; String driverClass com.mysql.cj.jdbc.Driver; Connection conn null; try { // 1. 加载驱动 Class.forName(driverClass); // 2. 获取连接 conn DriverManager.getConnection(url, user, password); System.out.println(数据库连接成功conn conn); } catch (ClassNotFoundException e) { System.out.println(驱动类未找到请检查驱动依赖是否引入); e.printStackTrace(); } catch (SQLException e) { System.out.println(数据库连接失败请检查URL、用户名、密码); e.printStackTrace(); } finally { // 3. 关闭连接 if (conn ! null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }跑一下如果控制台输出数据库连接成功说明JDBC通路已经打通。这一步成功了后面CRUD就是在这个连接基础上做文章。4. JDBC参数详解URL里的每个参数都是什么意思4.1 URL的拆解与含义JDBC的URL是一个固定格式拆开来看其实很好理解jdbc:mysql://localhost:3306/test_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8逐段解释jdbc:mysql://协议头告诉DriverManager使用MySQL驱动localhost数据库服务器主机名本地就用localhost远程就填IP或域名3306MySQL服务端口默认就是3306test_db要连接的数据库名?useSSLfalse是否使用SSL加密连接。本地开发用false就行因为自签名证书会导致连接报错serverTimezoneAsia/Shanghai时区参数MySQL 8.0驱动必带不加会报The server time zone value йʱ is unrecognized错误characterEncodingutf8指定字符集防止中文乱码4.2 useSSL参数到底要不要关新手最常见的误区是看到useSSLtrue就认为更安全于是坚持用true。实际在一个没有配置证书的MySQL上强制SSL反而会触发证书校验失败报错信息形如java.sql.SQLException: The server key is invalid开发环境直接用useSSLfalse即可等真正上了生产环境再通过网络层或数据库层的TLS方案解决。4.3 serverTimezone参数的选择MySQL 8.0驱动在连接时会做时区转换如果你没有在URL里指定时区驱动会读取JVM默认时区但和数据库的时区对不上就会报错。建议统一用Asia/Shanghai避免用CTT这种老写法。另外如果服务器用的是UTC那你要么改URL参数要么改数据库的全局时区设置两边保持一致才不会有时间偏差问题。4.4 顺带说下targetServerType是什么这个词很多人搜因为第一次见根本不知道是什么意思。它其实不是MySQL的参数是微软SQL Server JDBC驱动里的连接参数用于指定连接目标服务器类型可选值有any、master、preferSlave等。如果你不在SQL Server项目里基本不用关心这个参数。之所以有人搜大概率是写多数据源配置的时候在某个配置文件里见过它。这里提一嘴避免大家混淆。5. JDBC增删改查实战完整可复用的代码模板5.1 建表语句与准备数据先准备一张用户表作为演示载体CREATE DATABASE IF NOT EXISTS test_db DEFAULT CHARACTER SET utf8mb4; USE test_db; CREATE TABLE IF NOT EXISTS t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(50) NOT NULL, age INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这张表设计的核心点是id自增主键方便演示插入后获取主键username和password设置了NOT NULL避免脏数据create_time有默认值插入时不用手动填5.2 插入数据三种方式的对比JDBC插入数据有三种写法从简到繁分别是方式一直接拼SQL不推荐用于生产String sql INSERT INTO t_user (username, password, age) VALUES ( username , password , age );这种写法有严重的SQL注入风险而且每次拼接字符串容易出错新手学的时候理解一下就行实际项目别这么干。方式二PreparedStatement预编译推荐public void insertUser(String username, String password, Integer age) { String sql INSERT INTO t_user (username, password, age) VALUES (?, ?, ?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ps.setInt(3, age); int rows ps.executeUpdate(); System.out.println(受影响行数 rows); } catch (SQLException e) { e.printStackTrace(); } }方式三插入后获取自增主键实战场景常用比如注册场景插入用户后马上想拿用户ID做下一步操作就要用Statement.RETURN_GENERATED_KEYSpublic int insertUserAndGetId(String username, String password, Integer age) { String sql INSERT INTO t_user (username, password, age) VALUES (?, ?, ?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, username); ps.setString(2, password); ps.setInt(3, age); ps.executeUpdate(); try (ResultSet rs ps.getGeneratedKeys()) { if (rs.next()) { return rs.getInt(1); } } } catch (SQLException e) { e.printStackTrace(); } return -1; }这里说明一下枚举Statement.RETURN_GENERATED_KEYS的作用它告诉驱动执行SQL后要返回数据库生成的自增键值。如果SQL不是INSERT类型返回值是无效的。5.3 查询数据ResultSet使用要点查询和更新的最大区别在于多了ResultSet结果集这个对象需要仔细处理public void queryUsers() { String sql SELECT id, username, password, age FROM t_user; try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { int id rs.getInt(id); String username rs.getString(username); String password rs.getString(password); int age rs.getInt(age); System.out.println(id id , username username , password password , age age); } } catch (SQLException e) { e.printStackTrace(); } }几个必须记住的点rs.next()返回boolean同时把游标移动到下一行。游标初始位置在第一行之前所以要先调用next()再取值getXxx(列名)按列名取getXxx(1)按列序号取推荐按列名因为SQL调整后不会被序号打乱每一列在ResultSet里都要求有个对应的getter类型不对会报SQLException5.4 更新和删除executeUpdate的返回值public void updateUserAge(int id, int newAge) { String sql UPDATE t_user SET age ? WHERE id ?; try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, newAge); ps.setInt(2, id); int rows ps.executeUpdate(); System.out.println(更新受影响行数 rows); } catch (SQLException e) { e.printStackTrace(); } }删除的写法和更新几乎一样把SQL换成DELETE即可。这里有个实战经验要提executeUpdate()的返回值是受影响的行数不是所有数据库都会正确返回这个值MySQL是支持的。你可以根据返回值判断操作是否成功比如更新影响行数为0说明可能传入的id本身就不存在。6. SQL注入的攻防演练为什么必须用PreparedStatement6.1 一个能突破登录的SQL假设登录查询用字符串拼接String sql SELECT * FROM t_user WHERE username username AND password password ;如果用户名输入的是admin OR 11那拼接出来的SQL变成了SELECT * FROM t_user WHERE username admin OR 11 AND password xxx由于OR 11恒为真这条SQL直接就返回了用户表里的第一条记录等于绕过了密码校验。这就是SQL注入的经典示例。原理并不高深本质上是用户输入的内容被当成了SQL代码的一部分来执行而不是单纯的数据。6.2 PreparedStatement如何防护PreparedStatement使用预编译方式SQL骨架提前发送给数据库做语法解析参数通过?占位符单独传递。数据库在执行时把参数当纯数据来处理不会重新解析其中的SQL结构。也就是说即使你在用户名里输入admin OR 11数据库也只会把它当成一个普通的字符串值和字段比较而不会触发OR逻辑。6.3 效率对比预编译带来的副产品是效率提升。同一段SQL用PreparedStatement多次执行时数据库只需要解析一次SQL后续直接复用执行计划。而Statement每次都要重新解析。如果你的业务有批量插入需求比如一次插入1万条用户数据用PreparedStatement配合addBatch()性能差距能拉到几倍。public void batchInsertUsers(ListUser users) { String sql INSERT INTO t_user (username, password, age) VALUES (?, ?, ?); try (Connection conn DriverManager.getConnection(url, user, password); PreparedStatement ps conn.prepareStatement(sql)) { for (User u : users) { ps.setString(1, u.getUsername()); ps.setString(2, u.getPassword()); ps.setInt(3, u.getAge()); ps.addBatch(); } ps.executeBatch(); } catch (SQLException e) { e.printStackTrace(); } }7. 资源释放与工具类封装让代码告别重复7.1 JDBC资源释放的规范JDBC中需要手动关闭的资源有三个Connection、PreparedStatement/Statement、ResultSet。关闭顺序有讲究先开的后关。或者说先关ResultSet再关Statement最后关Connection。为什么顺序重要因为Statement依赖ConnectionResultSet依赖Statement。如果先把Connection关了它的依赖对象可能就直接失效了在某些场景下会抛异常。从Java 7开始可以用try-with-resources语法自动关闭这也是我在代码里一直用的写法try (Connection conn DriverManager.getConnection(...); PreparedStatement ps conn.prepareStatement(...); ResultSet rs ps.executeQuery()) { ... }这个语法的好处是代码更简洁不用写finally块关资源自动按声明顺序逆序关闭关闭时如果抛异常也不影响主流程7.2 抽取一个JDBC工具类实际项目不可能每次连接都写一遍四要素和驱动加载抽一个工具类吧public class JdbcUtils { private static final String URL jdbc:mysql://localhost:3306/test_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; private static final String DRIVER com.mysql.cj.jdbc.Driver; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }静态代码块只执行一次驱动类也只加载一次。后续所有DAO类里直接JdbcUtils.getConnection()代码会清爽很多。7.3 配置外置的进阶做法工具类里的用户名密码硬编码虽然简单但维护差。一个提高可维护性的做法是把配置放到jdbc.properties文件里用Properties读取public class JdbcUtils { private static String url; private static String user; private static String password; private static String driverClass; static { try (InputStream is JdbcUtils.class.getClassLoader().getResourceAsStream(jdbc.properties)) { Properties props new Properties(); props.load(is); url props.getProperty(jdbc.url); user props.getProperty(jdbc.username); password props.getProperty(jdbc.password); driverClass props.getProperty(jdbc.driver); Class.forName(driverClass); } catch (IOException | ClassNotFoundException e) { throw new RuntimeException(初始化JDBC配置失败, e); } } }jdbc.properties对应的内容jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/test_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456这样换环境就只改文件不用动代码。8. 经典问题排查按下这些坑我都替你们填了8.1 ClassNotFoundException驱动类找不到报错全文类似java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver排查顺序确认Maven依赖有没有下载成功IDEA的External Libraries里搜一下mysql-connector如果搜不到就是依赖没引入确认依赖版本和驱动类名是否匹配。5.x的驱动类名是com.mysql.jdbc.Driver8.x是com.mysql.cj.jdbc.Driver写反了必报这个错如果用的是手动JAR包方案确认JAR是否已经被Add as Library8.2 Communications link failure连不上MySQL报错信息com.mysql.cj.jdbc.exceptions.CommunicationsException: Communications link failure最常见的三种原因MySQL服务没启动。Windows下到服务里看MySQL80服务状态Linux下用systemctl status mysqld端口不对。URL里写的端口和MySQL实际监听端口不一致网络不通。连接的远程MySQL要确认防火墙是否放行了3306端口这里特别说一下MySQL 8.0本机连接时报错Cant connect to local MySQL server through socket /tmp/mysql.sock的情况这是Linux下的经典问题。它表示MySQL客户端尝试通过Unix Socket去连服务但找不到socket文件。解决思路确认MySQL服务真的在运行ps -ef | grep mysqld如果服务没起来查错误日志一般路径是/var/log/mysql/error.log如果服务起来了还是没有socket文件大概率是mysql.sock的路径配置不一致检查/etc/mysql/my.cnf里的socket配置8.3 SSL连接错误与Public Key Retrieval错误MySQL 8.0驱动连接时报java.sql.SQLException: The server key is invalid或者Public Key Retrieval is not allowed这两个问题都是SSL和加密插件导致的第一个是因为服务端SSL证书和驱动校验不匹配本地开发直接URL加useSSLfalse第二个是因为MySQL 8.0默认用caching_sha2_password插件驱动需要先取服务端的公钥。本地开发可以加allowPublicKeyRetrievaltrue也可以把用户的插件改成mysql_native_password8.4 Unknown database数据库不存在java.sql.SQLException: Unknown database test_db原因很直白URL里写的库名在MySQL里不存在。用客户端连上MySQL确认一下数据库列表SHOW DATABASES;另外一个容易忽略的点MySQL库名严格区分大小写。在Linux上如果你建的库名是小写URL里写大写一样报错。8.5 中文乱码问题插入中文后查询出来是???或者乱码优先检查三处URL有没有加characterEncodingutf8数据库、表的字符集是不是utf8mb4JDBC驱动版本是不是太老老版本对utf8mb4支持不全创建一个测试表验证字符集CREATE TABLE test_charset ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) ) DEFAULT CHARSETutf8mb4;用JDBC插入一条中文数据再查询如果正常了基本就锁定是字符集配置问题。8.6 驱动版本兼容性不只是MySQL需要注意最后补一个容易被忽略的细节JDBC驱动的jar包版本不仅MySQL要注意兼容性其他数据库也一样。比如Elasticsearch的JDBC驱动如果版本和ES服务端不匹配启动会报this version of the JDBC driver is only compatible with Elasticsearch version ...这类兼容性错误在数据库连接领域普遍存在。所以一个通用习惯是引入第三方JDBC驱动时第一时间确认它的大版本号要和目标服务端匹配别闭眼依赖最新版。9. 从JDBC走向工程化数据库连接池衔接写到这里你手里的代码虽然能跑但如果真用到生产环境直接用DriverManager获取连接是有问题的。问题出在哪DriverManager每次获取连接都会建立一个物理TCP连接操作完再断开频繁创建销毁连接的开销非常大在高并发下很快就把数据库的线程资源耗尽了。业界通用方案是引入数据库连接池它维护一批连接常驻请求来了从池里取一个用完再还回去。目前主流的有HikariCP、Druid、Tomcat JDBC Pool等。这里简单演示HikariCP的配置方式因为Spring Boot 2默认集成的就是它HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/test_db?useSSLfalseserverTimezoneAsia/Shanghai); config.setUsername(root); config.setPassword(123456); config.setDriverClassName(com.mysql.cj.jdbc.Driver); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); HikariDataSource dataSource new HikariDataSource(config); Connection conn dataSource.getConnection();连接池的参数配置里maximumPoolSize不是越大越好。MySQL默认的连接数是151你如果每个服务实例都开200个连接两个实例就把数据库打满了。一个比较务实的起始值是10到20压测后再调。从JDBC到连接池代码接口是兼容的你甚至只需要把工具类的getConnection()方法内部实现从DriverManager换成dataSource.getConnection()上层DAO代码一行都不用改。这也是JDBC设计的高明之处——它把连接管理抽象成了一层稳定的接口。10. 我的实操体会与建议JDBC这块知识点看着不起眼却是所有Java数据访问技术的底座。你后面学的MyBatis本质上是帮你封装了JDBC的样板代码你调的连接池本质上是优化了JDBC连接获取的性能你用的Spring事务本质上是管理了JDBC事务的提交和回滚。把这些底片看明白了遇到上层框架报错就不会一头雾水。我个人建议是所有学Java的同学至少手写一遍完整的JDBC增删改查不要依赖任何框架。这个过程会把预编译、结果集、事务边界、资源释放这些概念全部过一遍是性价比很高的基础投入。如果你是想把JDBC用到实际项目中哪怕只是写个小型工具也建议至少引入一个连接池组件再配一个简单的DAO层。别嫌麻烦生产环境里直接用裸JDBC连接遇到一次高峰流量就能让你明白连接池的含金量。以后遇到连接相关的报错先按这个顺序排查驱动在不在、URL对不对、版本匹不匹配、SSL参数有没有问题、字符集是否一致。八成问题都出在这五步里面。
返回列表