前阵子有个朋友想自己写一个员工管理系统问我的第一句话说“我该先做什么”我说你先别急着写页面、别急着设计表先把数据库连上。他有点不理解连接数据库不是很简单吗加载驱动、写个URL不就完事了确实看着简单但真到了业务系统里连接这个环节埋着无数的坑。时区、编码、驱动版本、连接池参数、异常上下文……每一样都能让你排查半天。这篇就是这系列的第一篇主题就一个把员工管理系统和数据库之间的通道打通。我尽量用最踏实的方式把连接数据库这件事从原理讲到落地包含完整的工程结构和可以直接抄走的代码。适合第一次用JDBC连接MySQL的初学者也适合想把自己零散的连接代码整理规范的老手。读完你应该能独立回答三个问题为什么连接要这么做、连接串里每个参数是什么含义、连接报错时到底去哪里找线索。1. 项目整体设计与数据库接入思路1.1 为什么第一步选择连接数据库员工管理系统的核心动作无非是增删改查员工信息本质上每一次操作都是在和数据打交道。无论是内存里的对象、前端表单里的字段最终都要落到数据库表里。所以整个系统的地基就是数据库连接连不上数据库后面所有的CRUD、事务、权限都无从谈起。我见过不少从零开始学习的开发者一上来就写Controller、写页面把数据库连接放到最后一步。结果项目做到一半发现字段类型对不上、数据读不出来又要回头改数据库设计等于返工。反过来先把连接通道打通再把数据表设计好后面的业务逻辑都变成顺着水管流水的活会顺畅得多。这一步的本质是解决“程序如何找到数据库、如何建立可信赖的会话”。Java访问数据库的标准方式是JDBC它是一套统一接口具体由数据库厂商提供驱动实现。只要代码面向JDBC写换数据库就只换驱动和连接串业务层几乎不用动这是Java生态这么多年沉淀下来的做法。1.2 技术选型MySQL、JDBC与连接池的取舍选型上没有太多花哨我用的是最常见的组合Java 8 MySQL 8 JDBC原生驱动 HikariCP连接池。如果你用Java 11或17也完全没问题本篇文章代码不依赖太高端的语法。MySQL选8.x是因为它默认字符集已经比较省心同时驱动类名、连接串参数和旧版有了明显不同很多教程还在用5.x时代的写法容易踩坑。有人会问为什么不直接上MyBatis或Spring Data JPA原因很简单框架会掩盖连接细节。第一次做系统应该先用JDBC手动建立一次连接、执行一条查询亲眼看看驱动加载、连接创建、资源关闭的完整生命周期。有了这个基础再去看MyBatis里的DataSource配置你会一目了然。另外本篇后面用的HikariCP也是Spring Boot默认的连接池现在熟悉了后面工程化会非常省力。1.3 连接层在系统里的位置我们做的员工管理系统后续大概会长成这样的分层Web层接收HTTP请求解析参数Service层写业务逻辑比如计算工资、校验部门归属DAO层负责拼接SQL并执行返回结果数据库连接层又被叫做DataSource或DbUtil是所有DAO操作的数据来源这一篇先单独把最底下的连接层做扎实也是分治思想。先让连接层能独立工作测试通过之后后续的DAO层就是流水线作业了。2. 环境准备数据库与工程骨架2.1 安装并初始化MySQL如果你机器上还没有MySQL先装一个8.x版本。安装过程多数是下一步下一步唯一要留意的是设置root密码时别随意后面连接串里要用的。装好后打开命令行先确认服务是否启动然后登录mysql -u root -p登录成功后执行以下SQL创建一个专属于员工系统的数据库和专用账号。个人开发时用root也能跑但团队协作或生产环境绝不能这样权限最小化是个好习惯。CREATE DATABASE employee_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER emp_userlocalhost IDENTIFIED BY emp_pass_2024; GRANT ALL PRIVILEGES ON employee_system.* TO emp_userlocalhost; FLUSH PRIVILEGES;注意这里指定了utf8mb4字符集。如果漏掉数据库会继承MySQL默认字符集插入Emoji比如员工评价表情时直接报错中文倒是可能正常。utf8mb4能覆盖完整Unicode是现在MySQL 8的推荐配置没有理由不用。2.2 创建员工表为连接测试提供对象连接是虚的你得有一张真实存在的表才能验证连接之后能不能干活。员工表要包含哪些字段可以按最小可用原则来编号、姓名、部门、入职日期、薪资后续需要再扩展。USE employee_system; CREATE TABLE employee ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 员工姓名, department VARCHAR(50) NOT NULL COMMENT 部门, hire_date DATE NOT NULL COMMENT 入职日期, salary DECIMAL(10, 2) NOT NULL COMMENT 月薪, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 员工基础信息表; INSERT INTO employee(name, department, hire_date, salary) VALUES (张三, 技术部, 2023-06-01, 15000.00), (李四, 人事部, 2022-03-15, 9000.00);建表SQL里写两个字段注释是不错的做法长期维护下来别人接手时看表结构就能懂业务。另外InnoDB是必须的它支持事务和外键员工系统的工资发放很可能涉及事务操作。2.3 Maven工程与驱动依赖接下去建工程骨架。推荐用Maven依赖管理方便后续加日志、加JSON解析库都是改一行pom。在pom.xml里加入MySQL驱动依赖dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency注意组名和版本。很多老教程写的是mysql:mysql-connector-java那是MySQL 5.x时代的坐标虽然也能用但官方已经迁移到新坐标了。驱动版本要与MySQL服务端版本大版本一致MySQL 8就用8.x驱动跨大版本容易出兼容问题。还有一个细节驱动包是runtime还是compile范围因为我们会在工具类里显式加载驱动Class所以默认的compile范围就够了。如果你只是靠SPI机制加载驱动用runtime也可以但新手阶段显式写Class.forName有利于理解机制。3. JDBC数据库连接核心实现3.1 加载驱动与获取Connection的完整代码网上JDBC连接代码五花八门核心其实是三步加载驱动类、拿到Connection、关闭资源。在MySQL 8里驱动类全名是com.mysql.cj.jdbc.Driver旧版是com.mysql.jdbc.Driver类名变了。下面这段是最早期的连接方式用来理解原理非常合适import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class RawConnectionDemo { public static void main(String[] args) throws SQLException { // 1. 加载驱动 try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } // 2. 获取连接 String url jdbc:mysql://localhost:3306/employee_system ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/Shanghai; String user emp_user; String password emp_pass_2024; Connection conn DriverManager.getConnection(url, user, password); System.out.println(数据库连接成功 conn); conn.close(); } }运行成功后会打印一长串对象信息里面能看到连接ID、用户名、库名和隔离级别这代表你打通了第一道关卡。想验证连接是不是真的能用还可以顺手执行一句SELECT 1比打印对象更实在。3.2 连接串里的那些隐藏参数逐个拆给你看连接字符串是这整篇最容易出问题的地方我把常用参数逐个解释一下useUnicodetruecharacterEncodingutf8告诉驱动字符按Unicode处理传输用UTF-8。在MySQL 8里驱动默认已经处理了不少但我依然建议显式写出来避免某些环境继承到奇怪编码。serverTimezoneAsia/ShanghaiMySQL 8的时区问题很普遍。驱动拿到服务器的timestamp值后要按指定时区做转换不设置的话经常报The server time zone value XXX is unrecognized。直接设成Asia/Shanghai最省事别用GMT%2B8这种写法虽然有部分版本支持但可读性太差。useSSLfalse本地开发时建议显式关闭SSL。如果服务器开启了SSL而驱动默认尝试连接日志会很啰嗦。生产环境再根据实际网络安全性决定是否开启。allowPublicKeyRetrievaltrueMySQL 8默认加密密码传输。某些环境连不上并报Public Key Retrieval is not allowed就是因为它需要去取公钥。开发环境可以加这个参数生产环境建议更严格处理。把这些参数串在一起就形成了一个比较可靠的连接串模板。顺序无关紧要但务必确认每个键值对的名称没拼错。3.3 封装一个数据库连接工具类每次都在业务代码里写DriverManager.getConnection下次会有几十处重复连接代码。与其这样不如第一步就封装成工具类。工具类要解决两个小问题加载驱动只执行一次连接复用一个统一入口。import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DbUtil { private static final String URL jdbc:mysql://localhost:3306/employee_system ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER emp_user; private static final String PASSWORD emp_pass_2024; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败检查依赖是否引入); } } private DbUtil() {} public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }静态代码块里完成驱动加载类加载一次就够了不会重复。关闭资源写在工具类里是为了养成释放资源的习惯。连接、语句、结果集三个都得关顺序从里到外。提示MySQL驱动从8.0开始支持SPI自动注册驱动理论上不写Class.forName也能获取连接。但建议保留显式加载因为工程里可能出现多个数据库驱动并存的情况自动注册的优先级未必符合预期。4. 从原生JDBC走向连接池4.1 为什么生产环境不能用DriverManager用DriverManager获取连接的方式每次都会经历TCP握手、MySQL认证、分配连接资源等开销整个过程可能几十到几百毫秒。如果一个系统每秒要处理几十个请求每个请求都重新建连数据库瞬间就会被连接请求淹没。更恶劣的问题是如果某次获取连接后程序抛了异常导致连接没关这条连接就成了“僵尸连接”占着数据库的连接数上限。MySQL默认max_connections是151超过就拒绝服务整个系统会表现出诡异的间歇性瘫痪。这也是我在实战中遇到过的最常见的员工系统生产事故之一。连接池的意义在于预先创建一批连接用的时候借用完还。归还后连接不会真正断开而是给下一个请求继续用。这样连接创建开销被摊薄连接数量也被池子牢牢控制住。4.2 HikariCP接入实操HikariCP是目前口碑最好的连接池Spring Boot 2.x之后默认就是它。它最大的特点是快这来自于字节码级别的优化。接入方式非常简单pom.xml增加依赖dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version4.0.3/version /dependency然后把DbUtil改成基于HikariDataSourceimport com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DbPoolUtil { private static final HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/employee_system ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse); config.setUsername(emp_user); config.setPassword(emp_pass_2024); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(3000); config.setMaxLifetime(1800000); config.setIdleTimeout(600000); config.setPoolName(EmployeeSystemPool); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这段配置里我特意加上了几个关键参数。maximumPoolSize20是连接池上限员工管理系统如果是中小型规模20个并发连接完全够了。connectionTimeout3000表示等不到连接时最多等3秒抛异常避免请求无限挂起。maxLifetime1800000是连接最长存活时间防止MySQL服务端断开后池里还存着失效连接。注意连接池的参数不是越大越好。每条连接都会占用MySQL内存如果池子设置成100数据库可能先扛不住。你的数据库配置、机器内存、业务并发量决定最终数字建议初期就从20起步压测后再调。4.3 健康检查与连接有效性验证连接池最大的隐患是“过期连接”。比如MySQL设置了wait_timeout为8小时池里的连接超过8小时没活动会被服务端掐断。池子不知道下次借出去给请求用第一条SQL就报错。解决思路是让池子周期性检查连接有效性。HikariCP提供了connectionTestQuery参数MySQL可以设为SELECT 1或者更推荐的方式是配置jdbc4ConnectionTest让驱动用自带的ping能力验证。HikariCP默认有连接存活检测机制配合maxLifetime小于数据库的wait_timeout就能基本避免拿到死连接的问题。这里有一个经验值maxLifetime一定要比MySQL的wait_timeout小一般设置成等值的一半或三分之二。MySQL默认8小时那池子里连接最长就给4到6小时保证连接被池子主动重建而不是被服务器掐断。5. 常见问题与排查实录5.1 ClassNotFoundException与No suitable driver如果你运行时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver先检查依赖坐标。很多情况是Maven依赖根本没有下载成功或者pom里写的groupId不正确。再检查一下mvn dependency:tree看mysql-connector-j是不是真的在依赖树里。如果报的是No suitable driver found for jdbc:mysql://...说明DriverManager在注册表里没找到能处理这个URL的驱动。常见原因有两种驱动类确实没加载或者连接串前缀写错了比如少写了jdbc:。把URL完整字符串贴出来仔细比对多半能找到问题。5.2 Communications link failure最折腾人的报错Communications link failure这个错在连接MySQL时非常常见但它背后有一堆可能原因。我第一次碰到时还以为是代码问题折腾了半天才发现防火墙没放行3306端口。按概率排序通常原因如下MySQL服务没启动检查systemctl status mysql或Windows服务管理器端口不对连接串写成3307或别的端口连到不存在的服务防火墙拦截本地开发最容易被忽略密码错误MySQL 8默认认证插件对密码策略更严格排查时先做两件事一是用命令行客户端mysql -u emp_user -p -h 127.0.0.1 -P 3306去连能连上说明数据库侧没问题二是用telnet 127.0.0.1 3306确认端口通不通。把网络层面先排除掉再回到代码层找原因。这里有个专属经验如果你用的是云服务器MySQL的bind-address默认可能只是127.0.0.1外部应用怎么都连不上。这时需要在配置文件里把绑定地址改成0.0.0.0或者指定内网IP然后重启MySQL服务。5.3 中文乱码与时区报错往员工表插入中文后读取出来全是问号这种问题几乎都是字符集不一致造成的。检查三层数据库字符集是不是utf8mb4连接串里有没有characterEncodingutf8表本身字段是不是utf8mb4。三层对齐乱码基本消失。要注意驱动参数里的UTF8在某些驱动版本里会被映射成MySQL的utf8mb3所以我们显式用utf8mb4更稳妥。时区报错则集中在MySQL 8。The server time zone value XXX is unrecognized的解决方案我就写了两次了连接串加serverTimezoneAsia/Shanghai。在代码里也可以用TimeZone.setDefault(TimeZone.getTimeZone(Asia/Shanghai))辅助不过最干净还是改连接串。5.4 连接泄漏finally与关闭顺序员工系统跑了一阵子后数据库报Too many connections这种情况大概率是连接泄漏。忘记close或者提前返回导致close没执行。排查技巧是把连接超时调小再配合数据库的SHOW PROCESSLIST能看到大量Sleep状态的连接堆积。修复的经验法则是关于连接、语句、结果集这类的资源释放逻辑一律放进finally或者直接用Java 7的try-with-resources。后者最省心代码也不容易漏try (Connection conn DbPoolUtil.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(SELECT ...)) { while (rs.next()) { // 业务处理 } }try-with-resources会用逆序自动关闭所有实现了AutoCloseable的资源和手动关finally等价还更不容易出错。后续写DAO层时我都建议用这个模式连接池里的连接也会被正确归还而不是关闭。关于权限的提醒也放这里给应用创建的数据库账号只要够用就行。员工管理系统的账号只操作employee_system这一个库的增删改查没必要给全局权限。权限过大等于把整个数据库暴露给应用一旦代码被注入后果就很难收拾。还有一个细节值得一说生产环境不要把数据库连接配置硬编码在Java类里。至少拆到properties或yaml配置文件里再进一步用环境变量覆盖。后续如果迁移到容器配置文件挂载出来就能改不需要重新编译打包。我个人在实际操作中的体会是数据库连接这件事第一遍写通容易真正麻烦的是“连接生命周期的管理”。驱动加载、连接创建、资源关闭、连接池参数每个环节都理解了员工系统的数据链路才算真正稳固。这篇做完你已经有了一个可以随时获取的连接源下一篇我们就能用这个连接源写出第一条“新增员工”的SQL操作。先别急去把本篇的测试跑通再继续走下去。