
简介这是一份基于Java Swing和MySQL的考试管理系统完整源码面向Java初学者及需要完成课程设计、毕业设计的学生也可作为桌面应用开发入门的实战参考。系统以Swing构建图形界面用JDBC连接MySQL数据库围绕试题库管理、考试安排、成绩统计等核心业务展开并应用了多线程并发处理、自定义异常处理、文件I/O导入导出、数据加密以及基于角色的权限控制等关键开发技术有助于理解桌面应用从需求分析、界面设计到数据持久化的完整流程。同时代码中体现了JDBC连接池的使用、SQL语句封装与分层设计思想对提升Java编程能力和项目整合能力很有帮助。资源压缩包大小2.21MB内含源码及相关文件可直接导入开发工具运行和二次修改。目前已有1576人学习下载适合正在探索Java GUI编程或准备快速搭建考试管理系统的开发者。1. JavaSwingMySQL 考试管理系统课设常客真正做扎实的却没几个很多人的课程设计、毕业设计清单里都有过这个组合JavaSwingMySQL 考试管理系统。但相当一部分交上去的版本登录界面做得像模像样题库、组卷、判分却只是摆设。这类系统最难的从来不是界面设计而是怎么用事务把一次交卷变成一条铁记录怎么让多选、判断、简答在不同题型下都不会判错分。这篇文章从数据库表设计讲到自动判分再落到连接失败、中文乱码、界面卡死、重复交卷这几类高频问题适合正在做课程设计、或者想用 Swing 复习一遍 JDBC 与 MySQL 联动的人。跟着把六张表建好、代码跑通这个项目就能稳稳交出去。2. 先把系统拆成能落地的六张表角色、试卷、答题记录的完整设计2.1 角色设计一张 sys_user 表怎么装下管理员、教师和学生考试管理系统必然有三类角色管理员管题库和试卷教师管出题和批改主观题学生在线考试、查成绩。很多新手第一反应是建三张表分别存三类用户。结果就是登录时要查三张表、权限判断要写三段代码加个“教师同时是管理员”的需求就直接改表结构。我一般只建一张sys_user表用一个role字段区分身份。值为 1 代表管理员2 代表教师3 代表学生。代码里定义好常量类写if (user.getRole() 1)这样的判断就足够不需要引入 Spring Security 这类重框架Swing 课程设计阶段手写权限判断完全够用。密码字段记得留足长度。明文 MD5 的摘要固定是 32 位SHA-256 是 64 位所以password VARCHAR(64)是最稳妥的选择。不要用VARCHAR(20)存明文密码那是在给数据库裸奔。用户名要加UNIQUE约束这是登录查询性能的保障也防止同一考号注册两次。2.2 题库与试卷表结构和字段类型怎么定后续才不返工题库表question_bank是整套系统的数据源。设计时要提前想清楚一道题属于哪个科目、什么题型、什么难度、哪个知识点、分值多少、答案是什么。字段类型上题干用TEXT选项用VARCHAR(255)判断题和填空题的选项字段允许为空。不要在数据库里把空选项设为NOT NULL否则填空题录入时每道都报错。exam_paper试卷表只存试卷的基本信息标题、科目、总分、时长、创建人、状态。试卷和题目的关系放在关联表exam_paper_question里上面存paper_id、question_id、question_order、score。同一道题可以在不同试卷里给不同分值这个需求只有关联表能接住。有个设计细节值得注意题目分值不要写死在question_bank表里。因为同一道题在模拟卷里可能是一分在正式考试里可能是两分。把分值放到关联表出卷时才决定这道题值多少分这才是考试系统的正确姿势。2.3 考试记录与答案表一张状态字段撑起交卷、批改、成绩发布exam_record考试记录表记录“谁在什么时间考了哪张卷”关键是status状态字段我把它拆成四个值未交、已交、待批改、批改完成。这里容易踩的坑是漏掉“待批改”因为客观题能自动判分主观题必须等教师人工批没有这个中间态成绩发布逻辑会非常别扭。exam_answer答案表是数据量增长最快的一张表一个学生考一张 50 题的卷子就产生 50 条记录。字段设计为record_id、question_id、student_answer、score其中score存这道题实际得分。这里有一个重要细节答案表里不存is_correct布尔字段因为主观题是部分给分一个布尔值表示不了“得了一半分”这种状态。整个系统的数据一致性核心就在最后两张表上。学生点击交卷那一刻要同时写exam_record和exam_answer一张表成功一张表失败分数和答题记录就对不上。这块后面用事务解决表结构设计阶段先把字段定义清楚别到写业务时才发现缺状态字段。3. 建库脚本与组卷 SQL从建表到抽出试卷的核心命令3.1 建库建表总脚本utf8mb4 字符集与索引设置数据库连接串里写characterEncodingutf8之前先保证建库这一步就用对字符集。MySQL 8 默认字符集已经是 utf8mb4但本地老版本 MySQL 5.7 装完库还是默认 latin1中文一进去就乱。建表脚本开头把字符集写死是最便宜的后悔药。CREATE DATABASE IF NOT EXISTS exam_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE exam_db; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL DEFAULT 3 COMMENT 1管理员 2教师 3学生, real_name VARCHAR(50) NOT NULL, class_name VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE question_bank ( id BIGINT PRIMARY KEY AUTO_INCREMENT, subject_name VARCHAR(50) NOT NULL, question_type TINYINT NOT NULL COMMENT 1单选 2多选 3判断 4填空 5简答, question_text TEXT NOT NULL, option_a VARCHAR(255), option_b VARCHAR(255), option_c VARCHAR(255), option_d VARCHAR(255), answer VARCHAR(255) NOT NULL, difficulty TINYINT DEFAULT 1 COMMENT 1容易 2中等 3困难, knowledge_point VARCHAR(50), created_by BIGINT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_subject_type (subject_name, question_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_paper ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, subject_name VARCHAR(50) NOT NULL, total_score INT NOT NULL DEFAULT 100, duration_minutes INT NOT NULL DEFAULT 90, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1发布, creator_id BIGINT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_paper_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, paper_id BIGINT NOT NULL, question_id BIGINT NOT NULL, question_order INT NOT NULL, score INT NOT NULL DEFAULT 1, UNIQUE KEY uk_paper_question (paper_id, question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, paper_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0未交 1已交 2待批改 3完成, score DECIMAL(6,2) DEFAULT 0, start_time DATETIME, submit_time DATETIME, INDEX idx_student_paper (student_id, paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_answer ( id BIGINT PRIMARY KEY AUTO_INCREMENT, record_id BIGINT NOT NULL, question_id BIGINT NOT NULL, student_answer TEXT, score DECIMAL(6,2) DEFAULT 0, INDEX idx_record (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;subject_name字段我放在题库和试卷表里而不是单独建科目表课程设计这个体量下没必要多一张关联表查询还得多一次JOIN。索引设计上给question_bank加(subject_name, question_type)联合索引因为组卷时最常用的过滤条件就是“某科目下某种题型”。exam_answer表按record_id建索引批改和成绩查询都靠它。3.2 登录校验 SQLPreparedStatement 防止注入才是正路登录查询是新手最爱写字符串拼接的地方也是最容易被注入攻击的地方。Swing 项目里看不到明显的安全问题但 SQL 注入一旦发生你的题库数据可能一夜之间全没了。别用Statement用PreparedStatement是把参数交给驱动处理数据库会区分 SQL 结构和参数值注入语句就失去了意义。SELECT id, username, password, role, real_name FROM sys_user WHERE username ? AND password ?;密码在前端不传明文。项目里应该有一个MD5Util登录时把用户输入的密码做一次摘要再传给 SQL 比较。数据库里存的本来也是摘要这样即使数据库备份被拖走也不会直接暴露密码原文。3.3 按知识点、难度随机抽题避开 ORDER BY RAND() 的性能坑自动组卷的核心是“从题库里随机抽取指定数量、指定难度、指定知识点的题”。新手最容易走的路是ORDER BY RAND() LIMIT n数据量在几百条时感觉不到问题题库过万后这条 SQL 会全表扫描、把每条记录标记随机值再排序一次组卷能卡好几秒。我的做法是分两步。第一步按条件把候选题目全部查出来数量少用Collections.shuffle()打乱然后从内存里取前 count 条。第二步是只对筛选后的数据做随机不碰大数据量的排序。当一次组卷需要多个题型分别抽题时循环调用这个方法按题型传入不同的题型代码和抽取数量。public ListQuestion pickRandomQuestions(String subject, int type, String knowledgePoint, int count) { StringBuilder sql new StringBuilder( SELECT * FROM question_bank WHERE subject_name ? AND question_type ?); ListObject params new ArrayList(); params.add(subject); params.add(type); if (knowledgePoint ! null !knowledgePoint.isBlank()) { sql.append( AND knowledge_point ?); params.add(knowledgePoint); } ListQuestion candidates queryList(sql.toString(), params); Collections.shuffle(candidates); return candidates.size() count ? candidates.subList(0, count) : candidates; }这个方法有个边界候选量很大时全量查出来也占内存。题库到几万条时我给外面套一层LIMIT 200的 SQL先用ORDER BY id ASC LIMIT 200捞出候选池再内存打乱。这个边界值是我在几万条数据下实测过的平衡点既能保证随机性又不会让内存被无谓的数据占满。3.4 查成绩与分数段统计一条统计 SQL 写完课设报表成绩列表、平均分、最高分、分数段统计是课设答辩时老师必看的功能。不要写循环查数据库一条GROUP BY加条件聚合就能完成。SELECT e.paper_id, p.title, COUNT(DISTINCT e.student_id) AS exam_count, ROUND(AVG(e.score), 2) AS avg_score, MAX(e.score) AS max_score, MIN(e.score) AS min_score FROM exam_record e JOIN exam_paper p ON e.paper_id p.id WHERE e.status 3 GROUP BY e.paper_id;分数段统计用SUM(CASE WHEN ...)一次查出优秀、及格、不及格三档人数SELECT SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS high_cnt, SUM(CASE WHEN score BETWEEN 60 AND 89 THEN 1 ELSE 0 END) AS mid_cnt, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS low_cnt FROM exam_record WHERE paper_id ? AND status 3;两个查询都不需要花里胡哨的存储过程课设阶段能跑、能解释清楚就是好方案。用Case When而不是在 Java 代码里循环统计是因为数据库做聚合比程序逐行判断快一个量级这个差距在成绩表几千行时就能感觉到。4. 从登录到自动判分跑通一条完整考试流程的 Java 代码4.1 数据库连接工具类 DBUtil连接串参数才是隐藏的坑一个DBUtil类管连接是 Swing 项目最常用的结构。连接串参数是这套系统最容易翻车的地方共五个参数每一个都对应一个真实踩过的坑。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/exam_db ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 connectTimeout5000 socketTimeout60000; 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(MySQL驱动没找到检查lib目录, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }useSSLfalse是给本地开发用的MySQL 8 默认开启 SSL不加这个参数会报 SSL 握手失败。serverTimezoneAsia/Shanghai解决日期字段差 8 小时的问题尤其是submit_time写入后看起来比当前时间早 8 小时十有八九是漏了时区。connectTimeout5000让数据库没启动时五秒内报错不加这个参数程序会傻等一两分钟。characterEncodingutf8写在连接串里配合建库时的 utf8mb4中文乱码问题基本能一次清干净。驱动类名com.mysql.cj.jdbc.Driver是 MySQL 8 的写法旧驱动用com.mysql.jdbc.Driver两个类名不一样混用会报ClassNotFoundException。单连接对课程设计够用不急着上连接池。等做到多台考试机同时交卷再考虑把DriverManager换成 HikariCP 也不迟。4.2 登录窗口的按钮事件Swing 监听器里的最小实现登录界面的按钮事件核心是把取输入、调服务、反馈结果三件事分开。有些新手把 SQL 直接写在按钮监听器里代码全挤在一块出问题根本没法定位。loginBtn.addActionListener(e - { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } User user authService.login(username, MD5Util.md5(password)); if (user null) { JOptionPane.showMessageDialog(this, 用户名或密码错误); return; } if (user.getRole() 1) { new AdminMainFrame(user).setVisible(true); } else { new StudentMainFrame(user).setVisible(true); } dispose(); });密码框用getPassword()返回char[]而不是getText()返回字符串这是 Java 安全编码的一个习惯——字符串会留在常量池里字符数组用完后可以手动清空。authService.login()内部用PreparedStatement执行第 3.2 节那条 SQL返回的User对象只携带页面需要展示的字段不背数据库连接这种重对象。4.3 组卷与交卷事务批量插入答案与 rollback 的正确姿势交卷是整套系统里事务边界最明确的地方。一次交卷要做三件事更新考试记录状态、批量插入所有答案、计算客观题分数并写入。这三件要么全成功要么全回滚否则就会出现“记录显示未交但答案表里已经有数据”这种查不出原因的数据不一致。public boolean submitExam(long recordId, ListStudentAnswer answers) throws SQLException { Connection conn DBUtil.getConnection(); String updateSql UPDATE exam_record SET status1, submit_timeNOW() WHERE id? AND status0; String insertSql INSERT INTO exam_answer(record_id, question_id, student_answer, score) VALUES(?,?,?,?); try { conn.setAutoCommit(false); try (PreparedStatement psUpdate conn.prepareStatement(updateSql)) { psUpdate.setLong(1, recordId); int affected psUpdate.executeUpdate(); if (affected 0) { conn.rollback(); return false; } } try (PreparedStatement psInsert conn.prepareStatement(insertSql)) { for (StudentAnswer answer : answers) { psInsert.setLong(1, recordId); psInsert.setLong(2, answer.getQuestionId()); psInsert.setString(3, answer.getAnswerContent()); psInsert.setBigDecimal(4, answer.getScore()); psInsert.addBatch(); } psInsert.executeBatch(); } conn.commit(); return true; } catch (SQLException ex) { conn.rollback(); throw ex; } finally { conn.setAutoCommit(true); } }这里最关键的不是commit而是那条带上AND status0的条件更新。学生双击交卷按钮、网络重试导致同一份试卷提交两次时第二次执行executeUpdate()影响行数为 0整个事务立刻回滚不会产生两条考试记录。这就是数据一致性在数据库层面的最后防线Java 代码再怎么写判断都赶不上这一句 SQL 起作用。4.4 客观题自动判分多选题乱序怎么比对才不错判自动判分只处理客观题也就是单选、多选、判断。这里有一个高频翻车场景考生选“B,D”标准答案存的是“D,B”直接比对字符串就判错了。选项顺序不是考生的错比对方式得先排序。public boolean judgeAnswer(String standardAnswer, String studentAnswer, int questionType) { if (studentAnswer null || studentAnswer.isBlank()) { return false; } if (questionType 3) { return standardAnswer.trim().equalsIgnoreCase(studentAnswer.trim()); } if (questionType 2) { SetString standardSet new TreeSet( Arrays.asList(standardAnswer.split(,))); SetString studentSet new TreeSet( Arrays.asList(studentAnswer.split(,))); return standardSet.equals(studentSet); } return standardAnswer.trim().equalsIgnoreCase(studentAnswer.trim()); }TreeSet会自动排序并去重标准答案是A,C考生写C,A甚至C, A解析成集合后都是[A, C]等价比较自然成立。答案入库前也建议统一格式大写字母、逗号分隔不要加中文逗号和空格。判分结果只表示“这题对没对”具体得多少分在写exam_answer表时用score字段乘上这题的配分。5. 避坑连接失败、中文乱码、界面卡死、重复交卷的排查手册5.1 现象ClassNotFoundException 与 No suitable driver foundClass.forName(com.mysql.cj.jdbc.Driver)抛ClassNotFoundException或者getConnection()抛No suitable driver found原因多数不是代码写错而是 jar 包没进入运行时的 classpath。Eclipse 或 IDEA 里项目能跑打包成 jar 后双击就报这个错十有八九是 jar 包只放进了编译路径没有打进发布目录。解决确认mysql-connector-java.jar确实存在于项目的 lib 目录在命令行用jar tf mysql-connector-java.jar | grep Driver.class查看驱动类是否在包内。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver8.0 之前的版本是com.mysql.jdbc.Driver别把旧驱动配到新类名上。5.2 现象登录后中文全是问号建库用了 utf8mb4连接串也加了characterEncodingutf8但界面上查出来的题库、用户名仍然是??。这个坑多半在导入 SQL 脚本那一步。Windows 下双击运行.sql文件时命令行客户端默认用系统编码解析文件GBK 编码的脚本里中文直接变成问号再存进表。解决用命令行导入时显式指定字符集mysql -uroot -p --default-character-setutf8mb4 exam_db exam_db.sql。或者用图形化工具导入前先把脚本另存为 UTF-8 编码。这个坑修起来容易但清掉已经变问号的脏数据很麻烦所以导入前就要盯住编码。5.3 现象点击交卷后窗口白屏十几秒Swing 是单线程模型所有界面刷新都跑在事件分发线程 EDT 上。如果在按钮监听器里直接执行查询题库、批量插入答案这些耗时操作EDT 被阻塞界面就僵在原地严重时系统直接提示“程序未响应”。解决耗时操作放进度条和后台线程里。Swing 内置的SwingWorker是最好的选择doInBackground里跑数据库操作done里更新界面。SwingWorkerListPaper, Void worker new SwingWorker() { Override protected ListPaper doInBackground() { return paperService.listPublishedPapers(); } Override protected void done() { try { tableModel.setRows(get()); progressBar.setVisible(false); } catch (Exception e) { JOptionPane.showMessageDialog(frame, 试卷加载失败); } } }; worker.execute();记住一条铁律SwingWorker的doInBackground里绝不碰任何 Swing 组件done里绝不做耗时操作。5.4 现象重复点击交卷出现两条考试记录前端防了双击后端没防网络重试这就是第 4.3 节条件更新的用武之地。没有AND status0的更新两个并发请求都查到记录处于未交状态于是都执行更新记录表里出现两条交卷时间不同的记录。解决后端更新语句必须带状态条件UPDATE exam_record SET status1 WHERE id? AND status0。影响行数为 0 时直接返回“试卷已提交请勿重复操作”。同时在交卷按钮的监听器里加setEnabled(false)双保险。5.5 现象MySQL 8 连接报时区或 SSL 错误MySQL 8 装好后Java 连接时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized或者SSL connection error这两个都是连接串参数缺失的典型症状。时区错误是没加serverTimezoneSSL 错误是没加useSSLfalse。数据库服务本身起不来的情况另当别论比如 Linux 下报ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock十有八九是 MySQL 服务没启动或者 socket 文件路径和客户端配置不一致。启动后用mysql -uroot -p -h127.0.0.1 -P3306从 TCP 方式连一次验证能连上说明服务正常问题在连接串参数。6. 从“能跑”到“跑好”批量提交、成绩导出、外部化配置6.1 答题记录批量插入批大小 500 时的内存矛盾第 4.3 节用了addBatch()和executeBatch()但新手往往忽略了批大小。一次考试五十道题看不出问题几百人同时交卷时所有答案一次性executeBatch驱动会把整个批次打包发送内存和网络包双双吃紧。我一般每攒 500 条执行一次批次把内存峰值控制在一个可控范围。连接串上再加一个参数rewriteBatchedStatementstrueMySQL 驱动会把多条 INSERT 重写为多值 INSERT批量插入性能能提升一个量级。500 这个值不是拍脑袋定死的它是我在普通笔记本上压出来的经验值超过 1000 时内存上涨明显但每个项目环境不一样这个参数也值得自己动手试试。6.2 用 Apache POI 导出成绩 Excel一个最小可跑的模板成绩统计页面只显示在 Swing 表格里不够老师要拿去存档就得导 Excel。Apache POI 是 Java 生态里最常见的 Office 文件生成方案它也能做 Word 图表但这里只讲导出成绩单的最小模板。try (Workbook wb new XSSFWorkbook(); FileOutputStream fos new FileOutputStream(scores.xlsx)) { Sheet sheet wb.createSheet(成绩单); Row header sheet.createRow(0); header.createCell(0).setCellValue(学号); header.createCell(1).setCellValue(姓名); header.createCell(2).setCellValue(试卷); header.createCell(3).setCellValue(分数); for (int i 0; i scoreList.size(); i) { Row row sheet.createRow(i 1); Score s scoreList.get(i); row.createCell(0).setCellValue(s.getStudentNo()); row.createCell(1).setCellValue(s.getStudentName()); row.createCell(2).setCellValue(s.getPaperTitle()); row.createCell(3).setCellValue(s.getScore()); } wb.write(fos); }XSSFWorkbook对应.xlsx格式支持 Excel 2007 以上版本不需要装 Office。字段多的报表可以动态生成列头核心逻辑都一样。注意Workbook实现了Closeable用 try-with-resources 保证文件流一定关闭不然下一次导出会报“文件被占用”。6.3 发布时把数据库配置从代码里挪出来DBUtil里写死root、密码、IP 地址开发时怎么改都行但交付出去换台电脑就傻眼。数据库配置应该是外部文件部署时改文件不用重新编译 jar。常见做法是在src/main/resources下放一个db.properties用Properties类加载。Properties props new Properties(); try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { props.load(in); } String url props.getProperty(db.url); String user props.getProperty(db.username); String password props.getProperty(db.password);打包时把这个db.properties放到 jar 同级目录程序启动时从外部文件加载换数据库只需要改文本文件。这是我从很久以前踩过的坑里总结出来的习惯凡是和环境相关的配置一律不进代码。那次就是把 IP 写死在DBUtil里考试机房换了个网段所有客户端全部连不上数据库最后重新编译了一次才解决。把数据库配置外部化之后再能做到连接串参数齐全、交卷条件更新、Swing 线程模型不踩雷这套系统就算站住了。希望帮到你。本文还有配套的精品资源点击获取