干 Java 后端这行,每年帮新人排查数据库问题,听得最多的一句话就是“我照着教程写了,怎么就是连不上”。其实从 MySQL 的安装管理,到 JDBC 的操作步骤,再到用 JUnit 去验证这些代码,是一条完整的链路,任何一个环节理解不到位,后面全卡壳。这篇文章我就把这条链路从头到尾走一遍,内容包括:MySQL 数据库的安装与日常管理、JDBC 操作数据库的完整步骤、怎么用 JUnit 给 DAO 层写可回归的测试,最后再送一个实用小技巧——怎么把压缩包里解压出来的 exe 免安装程序,在桌面上放一个图标。
适合谁看?如果你是刚学 JavaWeb 的学生,或者正在复习数据库操作的后端新人,这篇文章能帮你把散落的知识点串起来。我不会讲太多理论空话,尽量按照实际干活儿的方式来说,每一步都直接给能跑、能用的方案。
1. MySQL数据库管理:先把“数据放哪里”这件事搞明白
1.1 MySQL在技术栈里的位置
MySQL 是市面上最常见的开源关系型数据库管理系统,很多中小型项目、学习项目、甚至一些大型互联网公司内部都在用它。你可以把数据库理解成一个“数据仓库”,Java 程序本身跑在内存里,关了机数据就没了,所以需要 MySQL 这样的人把数据持久化地存到磁盘。
为什么开源的管理系统会成为默认选择?原因很简单:免费、生态成熟、资料多。遇到问题一搜基本都有答案,这一点对新手来说比任何特性都重要。MySQL 的底层是 C/C++ 写的服务器,对外提供 SQL 接口,Java 程序通过 JDBC 驱动和它通信。记住这个分工:程序负责逻辑,MySQL 负责存储。
1.2 装完MySQL之后的三件必做之事
我见过太多人装完 MySQL 就开始写 JDBC,结果第一步就死在 连接不上 上。装完之后,请老老实实完成下面三件事:
第一,确认服务已经启动。Windows 下用命令net start mysql或者去“服务”窗口里看 MySQL 服务的状态;Linux 下用systemctl status mysqld(或mysql,看发行版)检查。服务没起来,后面所有操作都是白扯。
第二,拿到初始密码并改掉。MySQL 8.0 在初始化时会生成一个临时密码,一般写在启动日志里。Windows 安装版在安装目录的my.ini同目录日志中,Linux RPM 安装后通常看/var/log/mysqld.log。临时密码长这样:A temporary password is generated for root@localhost: xxxxxxxx。拿到后用mysql -uroot -p登录,立刻执行:
ALTER USER 'root'@'localhost' IDENTIFIED BY '你的新密码';注意 MySQL 8 的默认密码策略要求至少8位、包含大小写字母数字和特殊符号,如果就想用简单密码,得先降低校验策略,新手一般不建议折腾,直接用强密码最省事。
第三,确认端口和字符集。默认端口 3306,如果被占用会启动失败,这也是很多人“明明装好了却连不上”的原因。用select @@port, @@character_set_database;查看当前配置,字符集建议是utf8mb4,这个会直接关系到中文能不能正常入库。
1.3 建库建表时值得花心思的四个细节
建库很简单,CREATE DATABASE demo DEFAULT CHARSET utf8mb4;一行搞定。但建表时我建议你花点心思,等数据量大了再回头改表,成本高得吓人。我常用的建表语句长这样:
CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, email VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB;这里有几个细节想单独强调:
- 主键用
BIGINT AUTO_INCREMENT,不要用业务字段当主键。等以后做关联查询、分库分表时自增整数主键最省心。 - 字段能设 NOT NULL 就设 NOT NULL,避免代码里出现一堆
null判断。 - 表名和字段名我习惯加前缀
t_,没什么硬性规范,纯粹为了避免和 SQL 关键字冲突,比如你要建一张order表,order是关键字,不加反引号就是报错。 - 存储引擎用 InnoDB,支持事务、支持外键、崩溃恢复能力强。MyISAM 也就老项目里还能见到,新项目别碰。
2. JDBC操作数据库步骤:六步走通,但每一步都有讲究
2.1 JDBC背后的运行逻辑
JDBC 是 Java 访问数据库的一套标准接口,全称 Java Database Connectivity。你要记住一个关键点:JDBC 本身只是接口,真正干活的“实现”是数据库厂商提供的驱动 JAR 包。MySQL 的驱动叫mysql-connector-j,以前老版本叫mysql-connector-java。
打个比方,JDBC 就像充电接口标准,驱动就是充电头,MySQL 是充电桩。你写的代码只面向标准接口,底层换 Oracle、PostgreSQL,只要驱动和连接串换掉,业务代码基本不用动。这也是 JDBC 这么多年还值得学的原因——它是一切 ORM 框架(MyBatis、Hibernate)的地基。
2.2 标准六步与关键代码
不管业务多复杂,JDBC 操作数据库的核心步骤,我拆成了六步,你按这个模板写,基本不会乱:
第一步,注册驱动。MySQL 8 之后驱动类名变成了com.mysql.cj.jdbc.Driver,老教程里的com.mysql.jdbc.Driver已经过时了,虽然 MySQL Connector/J 8 为了兼容还保留着老类名,但控制台会打印警告。
Class.forName("com.mysql.cj.jdbc.Driver");第二步,获取连接。这里最容易出幺蛾子,连接串必须写对。
String url = "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&allowPublicKeyRetrieval=true"; Connection conn = DriverManager.getConnection(url, "root", "你的密码");URL 的格式是jdbc:mysql://ip:端口/数据库名?参数。serverTimezone=Asia/Shanghai是解决时区报错的;characterEncoding=UTF-8是解决中文乱码的;allowPublicKeyRetrieval=true是解决 MySQL 8 的认证加密问题的,三个参数新手建议直接都写上。
第三步,创建预编译语句对象。
String sql = "INSERT INTO t_user(username, password, email) VALUES(?, ?, ?)"; PreparedStatement ps = conn.prepareStatement(sql);第四步,给占位符赋值并执行。
ps.setString(1, "admin"); ps.setString(2, "123456"); ps.setString(3, "admin@example.com"); int rows = ps.executeUpdate();executeUpdate()用于 INSERT、UPDATE、DELETE,返回受影响行数;查询用executeQuery(),返回 ResultSet。
第五步,处理结果集。只有 SELECT 才需要这一步。
ResultSet rs = ps.executeQuery("SELECT * FROM t_user"); while (rs.next()) { Long id = rs.getLong("id"); String username = rs.getString("username"); }第六步,释放资源。记住一个原则:后开的先关。
if (rs != null) rs.close(); if (ps != null) ps.close(); if (conn != null) conn.close();2.3 Statement、PreparedStatement与事务
很多新手会问为什么不用Statement,非要PreparedStatement。两个原因:第一,安全。Statement做字符串拼接,用户输入' or '1'='1就能把你的 SQL 变成“永远为真”的查询,这就是经典的 SQL 注入。PreparedStatement用预编译 + 参数化查询,从根本上杜绝了拼接注入。第二,效率。同一条 SQL 重复执行时,数据库会缓存预编译的执行计划,性能更好。所以我的习惯是无论什么场景都只用PreparedStatement,没有例外。
再提一下 JDBC 的事务。MySQL 的 InnoDB 默认自动提交,但在“先扣钱、再减库存”这种需要原子性的场景里,必须手动控制:
conn.setAutoCommit(false); try { // 多条增删改 conn.commit(); } catch (Exception e) { conn.rollback(); } finally { // 恢复自动提交,释放连接 }事务是数据库正确性的最后一道防线,JDBC 层面虽然写得啰嗦,但这是概念基础。以后用 Spring 的@Transactional,说白了底层还是这一套。
3. JUnit测试:给JDBC代码加上安全网
3.1 JUnit5 的注解和断言
写完了 DAO 方法,怎么验证对不对?有人说,跑一遍 main 方法看看结果就行了。短期没问题,可一旦代码越来越复杂,你改了一个方法,影响到另一个方法,跑 main 是发现不了的。这时候就需要 JUnit。
JUnit 是 Java 生态用得非常广泛的单元测试框架,最新主流是 JUnit 5,Maven 依赖用:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.2</version> <scope>test</scope> </dependency>核心概念就三个:注解、断言、生命周期。
@Test:标记一个方法为测试方法,可以直接右键运行。assertEquals(期望值, 实际值):判断两个值是否相等。assertNotNull(对象):判断对象非空。assertThrows(异常类, 可执行代码):验证是否抛出了指定异常。@BeforeEach/@AfterEach:每个测试方法执行前/后运行,适合开/关资源。@BeforeAll/@AfterAll:所有测试运行前/后运行一次,需要是静态方法。
3.2 数据库测试的两个实操技巧
给 JDBC 代码写测试,最大的问题是怎么处理真实数据库。我建议初学阶段直接连本地 MySQL 做“集成测试”,不用 Mock,因为你要学的是连接和操作的真实链路,Mock 掉数据库反而学不到东西。实操中有两个超实用的技巧:
第一个技巧:测试前先清理数据。在@BeforeEach里先执行一条DELETE FROM t_user或删除你准备插入的特定测试数据,保证每个用例都是干净环境,不会出现“上一次跑的数据污染了这一次的断言”这种问题。
第二个技巧:用事务回滚做“无痕测试”。测试里把setAutoCommit(false),执行完断言后rollback(),数据库不会留下测试脏数据。这个技巧特别适合测试插入和更新逻辑:
@Test void testInsert() throws Exception { Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); try { // 执行数据库操作 // 断言结果 } finally { conn.rollback(); conn.close(); } }3.3 测试生命周期与资源释放
JUnit 测试方法里最容易出问题的不是断言,而是资源泄漏。每个@Test方法都会开连接、执行 SQL,如果不关闭连接,跑几百个用例后 MySQL 的连接数会被耗光,报Too many connections。
所以我强烈建议把获取连接和关闭连接放到生命周期方法里,而不是每个测试方法里重复写。结构大概是:
class UserDaoTest { private Connection conn; @BeforeEach void setUp() throws Exception { conn = DBUtil.getConnection(); } @AfterEach void tearDown() throws Exception { if (conn != null) conn.close(); } @Test void shouldInsertSuccess() throws Exception { // 用 this.conn 执行操作并断言 } }另外提醒一句:测试方法命名最好用“行为化”命名,比如shouldReturnUserWhenQueryByUsername,比test1、test2强一万倍。等过两个月回来看测试报告,一眼就知道哪个行为坏了。
4. 完整实操:从建库到DAO层测试的一次性走完
4.1 项目结构与依赖准备
光讲零散知识点,不如跑一个完整小案例。我以“用户管理”为例,把前面所有内容串起来。项目结构建议这样组织:
entity/User.java:实体类,字段对应t_user表。util/DBUtil.java:数据库连接工具。dao/UserDao.java:封装增删改查。test/dao/UserDaoTest.java:JUnit 测试。
依赖只需要两个:mysql-connector-j和junit-jupiter。如果你用的是 IDEA,方便的做法是创建 Maven 项目,把依赖写进pom.xml。如果是在普通 Java 项目里手动导 JAR,记得放到lib目录后,在 Project Structure 的 Libraries 里加进去,否则运行时一定会报ClassNotFoundException: com.mysql.cj.jdbc.Driver,这个问题我见得太多了。
4.2 DAO层的增删改查实现
DBUtil 工具类,静态代码块注册驱动,提供一个获取连接的方法:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }UserDao 里写两个方法作示范,一个是插入,一个是通过用户名查询:
public class UserDao { public boolean insert(User user) throws SQLException { String sql = "INSERT INTO t_user(username, password, email) VALUES(?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); return ps.executeUpdate() == 1; } } public User findByUsername(String username) throws SQLException { String sql = "SELECT id, username, password, email, created_at FROM t_user WHERE username = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setEmail(rs.getString("email")); return user; } return null; } } } }看到我用了try-with-resources语法吗?Java 7 之后推荐这么写,资源会自动关闭,代码干净不少,比手写finally里close()稳妥。
4.3 用JUnit验证DAO方法
测试类里做的事情很朴素:插入一条数据,然后查询它,断言用户名一致;再插入一条重复用户名,断言抛出异常。写出来大概是:
class UserDaoTest { private UserDao userDao; @BeforeEach void setUp() { userDao = new UserDao(); } @Test void testInsertAndQuery() throws Exception { User u = new User(); u.setUsername("zhangsan"); u.setPassword("pass123"); u.setEmail("zhangsan@example.com"); boolean inserted = userDao.insert(u); assertTrue(inserted); User found = userDao.findByUsername("zhangsan"); assertNotNull(found); assertEquals("zhangsan", found.getUsername()); assertEquals("pass123", found.getPassword()); } }跑一次测试,绿色通过,说明从建表到 JDBC 到查询逻辑,整条链路都通了。这比你在 main 里手动打印然后肉眼核对要可靠得多。以后每次改动UserDao,跑一遍测试立刻知道有没有改坏。
5. 常见问题排查:这些报错我帮你提前踩过
5.1 连接阶段报错速查表
新手 90% 的问题都出在连接阶段。下面这张表是我这些年帮人看代码时总结出来的高频问题,直接对着找:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
ClassNotFoundException: com.mysql.cj.jdbc.Driver | 驱动 JAR 没导入 | 检查依赖或 Project Structure 里是否添加了 mysql-connector-j |
No suitable driver found | URL 格式写错,或驱动没注册 | 确认 URL 以jdbc:mysql://开头 |
Access denied for user 'root'@'localhost' | 用户名或密码错误 | 检查密码;忘记密码需重置 |
Communications link failure | 服务没启动、防火墙拦截 | 查看 MySQL 服务状态,确认 3306 端口开放 |
Public Key Retrieval is not allowed | MySQL 8 缓存 SHA-2 认证问题 | URL 加allowPublicKeyRetrieval=true |
Unknown database 'demo' | 数据库不存在或名字写错 | 先执行CREATE DATABASE demo; |
The server time zone value is unrecognized | 时区未配置 | URL 加serverTimezone=Asia/Shanghai |
这些报错里有两个特别坑,我想展开说。第一个是No suitable driver found,新手容易以为是驱动没下载,其实经常是 URL 里把jdbc:mysql://写成了jdbc:mysql:/(少一层斜杠),或者数据库名写错了。第二个是Communications link failure,我碰过最隐晦的场景是 MySQL 装了两个版本,3306 端口被旧版本占着,新项目本来连的就是旧库,数据结构和密码全对不上。
5.2 数据和字符相关的坑
连接通了,数据又出幺蛾子。最常见的是中文乱码。乱码的原因通常是多层字符集不一致:MySQL 数据库是utf8mb4、连接 URL 没带characterEncoding=UTF-8、IDEA 文件编码又是 GBK。解决思路是把所有环节统一到 UTF-8,单改一处没用。
还有一类问题是Incorrect string value: '\xE4\xB8\xAD...'这种报错,看着像乱码,其实是表或字段的CHARSET不是 utf8,尤其创建表时没写DEFAULT CHARSET=utf8mb4。解决办法就是ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4;。
5.3 环境相关的隐蔽问题
最后说一个很多人忽略的点:mysql-connector-j版本要和 MySQL 服务器版本大致匹配。比如 MySQL 8 服务器,最好用 Connector/J 8.x,而不是 5.x。虽然旧驱动有时也能连,但在加密协议、认证方式上会撞出各种诡异错误,看起来像乱码,其实是握手协议不对。
另外,IDEA 里写 JUnit 测试时,如果右键测试方法没有“运行”选项,很可能是没在项目里正确配置 JUnit 测试库,或者测试类没有放在src/test/java目录下(Maven 约定)。这不算代码问题,但环境问题卡住人的次数真不比代码问题少。
6. 小技巧:压缩包里的exe程序怎么在桌面放一个图标
6.1 免安装程序创建桌面快捷方式
最后分享一个和数据库无关、但在实际工作中经常会用到的小技巧:从压缩包里解压出来的免安装 exe 程序,怎么让它出现在桌面。
先说一个最常见的错误操作:直接在压缩包内部双击 exe,或者把压缩包里单个 exe 文件直接拖到桌面运行。很多程序不是单文件的,它依赖压缩包里的 DLL、配置文件、资源文件,单独把 exe 拖出来就会闪退或者报“缺少 dll”。正确做法是:
第一步,把整个压缩包完整解压到固定目录。目录最好不含中文和空格,路径纯英文最稳,比如D:\Tools\MyApp。固定目录很重要,有些免安装程序使用相对路径读取配置,你移动了目录它反而跑不起来。
第二步,在解压目录里找到主程序 exe,右键点击它,选择“发送到” -> “桌面快捷方式”。
第三步,桌面就会自动出现一个快捷方式,双击即可打开。
如果你的右键菜单里“发送到”没有桌面快捷方式选项(Win11 有些右键精简版会隐藏),也可以用笨办法:右键 exe -> “创建快捷方式”,然后把这个快捷方式文件剪切到桌面上。效果一样,只是多一步操作。
6.2 让快捷方式更好用的三个小优化
快捷方式是生成了,但默认名字通常是“xxx.exe - 快捷方式”,图标也未必好看。我习惯再做三个小优化,这几招在很多同事机器上都用过,反馈都还不错。
第一,改名。桌面右键快捷方式 -> 重命名,把“ - 快捷方式”后缀删掉。Windows 不会因为你删掉这几个字就让快捷方式失效,放心改。
第二,改图标。有些 exe 自带资源里的图标很丑,或者干脆是空白图标。右键快捷方式 -> 属性 -> “快捷方式”选项卡 -> “更改图标”,可以浏览到可执行文件同目录下的.ico图标文件,或者直接从 exe 自带的图标组里选。
第三,如果这个程序需要管理员权限运行(比如它要读写系统目录、修改注册表),右键快捷方式 -> 属性 -> “兼容性”选项卡 -> 勾选“以管理员身份运行此程序”。不勾的话,双击可能会弹出 UAC 或者是无声失败。
如果你用的是 Windows 10 或 11,还有一个更快的路径:选中快捷方式后按Win + Shift + 方向键可以把它固定到其他显示器,但更常用的是右键快捷方式 -> “显示更多选项” -> “固定到任务栏”。固定到任务栏之后,桌面和任务栏各留一份,随用随点,特别适合那些不写桌面图标的绿色软件。
至于“从压缩包中”这个前提,我再多提醒一句:如果你是在压缩包里右键 exe 创建的快捷方式,解压之后请删掉重建,因为压缩包内的虚拟路径和解压后的真实路径不一致,双击很可能会提示找不到文件。这也是很多人反应“快捷方式没用”的根本原因。
顺便延伸一下:这种免安装程序和安装版程序的区别在于,安装版会帮你在“开始菜单”、注册表、桌面快捷方式里全部处理妥当,而免安装版一切都得手动来。理解了这个逻辑,以后遇到任何绿色软件,都知道怎么整理桌面了。
最后再分享一点个人体会:数据库相关的东西,报错信息永远是最好的老师。我见过太多人一看到英文报错就慌,其实慢慢把报错读完,80% 的问题自己就能定位,剩下的就是表格里那点事。JDBC 和 JUnit 这两样东西,刚开始觉得繁琐,一旦你把连接逻辑、测试套路固定成自己的模板,后面写业务代码会很顺手。把环境配置好、把测试跑起来、把常见错误记住,这三件事做扎实,比背多少面试题都管用。