生選課成績(jī)管理系統(tǒng)完整實(shí)現(xiàn)與避坑指南)
簡(jiǎn)介這是一套基于Java Swing與MySQL實(shí)現(xiàn)的學(xué)生選課及成績(jī)管理系統(tǒng)課程設(shè)計(jì)源碼包適合Java初學(xué)者、畢業(yè)設(shè)計(jì)及課設(shè)學(xué)生使用。系統(tǒng)覆蓋登錄注冊(cè)、管理員/教師/學(xué)生三類(lèi)角色權(quán)限包含學(xué)生信息管理、課程管理、選課操作、成績(jī)錄入與查詢等完整業(yè)務(wù)模塊數(shù)據(jù)庫(kù)腳本和初始賬號(hào)管理員admin、教師tch、學(xué)生stu密碼均為123已內(nèi)置下載后修改JdbcConfig即可運(yùn)行。壓縮包共53個(gè)文件以16個(gè)Java源碼文件為主輔以MySQL建表SQL腳本、IDEA配置、外部Jar驅(qū)動(dòng)以及20張界面運(yùn)行截圖和E-R圖整體大小8.63MB結(jié)構(gòu)清晰便于對(duì)照開(kāi)發(fā)。目前已有219人學(xué)習(xí)下載適合作為課程設(shè)計(jì)、大作業(yè)或初期項(xiàng)目立項(xiàng)的參考模板能夠幫助理解Swing界面編程與JDBC數(shù)據(jù)庫(kù)操作的完整流程。1. 基于 Java Swing MySQL 的學(xué)生選課及成績(jī)管理系統(tǒng)先別急著說(shuō)它老很多人一聽(tīng)“基于 Java Swing MySQL 的學(xué)生選課及成績(jī)管理系統(tǒng)”就皺眉覺(jué)得 Swing 是十幾年前的老古董。但課程設(shè)計(jì)答辯現(xiàn)場(chǎng)恰恰是這種組合最穩(wěn)界面用 Swing 組件拖出來(lái)數(shù)據(jù)落在 MySQL 里中間用 JDBC 串起來(lái)邏輯清楚評(píng)委提問(wèn)也有抓手。這個(gè)系統(tǒng)要解決的是學(xué)生登錄、瀏覽課程、選課退課、查成績(jī)以及老師錄入成績(jī)這些典型管理場(chǎng)景。它適合第一次做完整工程的人也適合想在短時(shí)間內(nèi)交付可演示結(jié)果的在職讀者。下面這套落地步驟是我?guī)н^(guò)三輪課程設(shè)計(jì)輔導(dǎo)以后沉淀下來(lái)的照著走新手能跑通熟手能避坑。2. 先把地基打牢環(huán)境變量、建表設(shè)計(jì)和項(xiàng)目骨架2.1 Java 環(huán)境變量配置與 MySQL 安裝裝完先驗(yàn)證三條命令課程設(shè)計(jì)翻車(chē)最多的地方往往不是代碼而是環(huán)境。JDK 裝了一半MySQL root 密碼忘了驅(qū)動(dòng) JAR 沒(méi)放進(jìn) lib這些問(wèn)題在教室演示時(shí)一碰一個(gè)準(zhǔn)。網(wǎng)上很多 java 環(huán)境變量配置和 mysql 安裝教程都寫(xiě)得很詳細(xì)但我要提醒的是裝完以后一定要用命令行驗(yàn)證一遍別等雙擊 Main 才報(bào)錯(cuò)。java -version javac -version mysql --version邏輯說(shuō)明第一條驗(yàn)證 JRE第二條驗(yàn)證 JDK 編譯器第三條驗(yàn)證 MySQL 客戶端是否在 PATH 中。前兩條命令都來(lái)自 JDK如果 javac 提示找不到命令說(shuō)明環(huán)境變量沒(méi)配好而不是 JDK 沒(méi)裝上。參數(shù)說(shuō)明JAVA_HOME 必須指向 JDK 安裝根目錄比如C:\Program Files\Java\jdk-17不能只配到 bin 目錄。Windows 的 Path 里追加%JAVA_HOME%\binLinux 或 macOS 追加$JAVA_HOME/bin。MySQL 8.0 安裝時(shí)默認(rèn)選 caching_sha2_password 插件如果后面 JDBC 連接報(bào)錯(cuò)再回來(lái)改配置或換舊版驅(qū)動(dòng)。提示本地課程設(shè)計(jì)用 MySQL 5.7 或 8.0 都可以但連接驅(qū)動(dòng) JAR 要和 MySQL 版本對(duì)應(yīng)別拿 5.1 的驅(qū)動(dòng)連 8.0 的庫(kù)否則大概率報(bào) “Unsupported major.minor version”。數(shù)據(jù)庫(kù)連接測(cè)試建議用 MySQL Workbench。mysql workbench 使用教程里經(jīng)常有人卡在連不上 localhost其實(shí)先檢查服務(wù)有沒(méi)有啟動(dòng)就行。Windows 下 Win R 輸入 services.msc找到 MySQL80確認(rèn)狀態(tài)是“正在運(yùn)行”。Workbench 里連接 root 的時(shí)候如果提示 Access denied就是密碼不對(duì)沒(méi)有快捷辦法只能重置密碼。2.2 建庫(kù)建表學(xué)生、課程、選課成績(jī)?nèi)龔埍碜侄尉驮撨@樣定表結(jié)構(gòu)是系統(tǒng)的地基。我的建議是三張表student、course、student_course。student_course 同時(shí)承擔(dān)“選課關(guān)系”和“成績(jī)表”兩個(gè)職責(zé)成績(jī)字段直接掛在選課記錄上這是最省事也最容易被評(píng)委接受的做法。CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE course_system; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT 學(xué)號(hào), sname VARCHAR(50) NOT NULL COMMENT 姓名, password VARCHAR(64) NOT NULL DEFAULT 123456 COMMENT 登錄密碼, class_name VARCHAR(50) COMMENT 班級(jí) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE COMMENT 課程編號(hào), cname VARCHAR(50) NOT NULL COMMENT 課程名, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT 學(xué)分, capacity INT NOT NULL DEFAULT 30 COMMENT 選課容量 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) DEFAULT NULL COMMENT 成績(jī)未錄入為 NULL, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;邏輯說(shuō)明student 表的 id 是代理主鍵sno 是業(yè)務(wù)主鍵登錄用 sno。course 表同樣用 id 做主鍵cno 做唯一編號(hào)。student_course 表的 score 設(shè)置為 NULL表示老師還沒(méi)錄入成績(jī)不要用 0 或 -1 去代替。UNIQUE KEY 加在 student_id 和 course_id 上數(shù)據(jù)庫(kù)層面直接攔住重復(fù)選課。參數(shù)說(shuō)明DECIMAL(3,1) 表示最多三位數(shù)字、一位小數(shù)能存 0.0 到 99.9課程學(xué)分和成績(jī)都?jí)蛴?。外鍵約束在課程設(shè)計(jì)里建議保留雖然很多人說(shuō)生產(chǎn)環(huán)境不用外鍵但答辯時(shí)“數(shù)據(jù)完整性”是一個(gè)重要加分點(diǎn)。所有表顯式指定 utf8mb4是為了和后面 JDBC 連接串里的 characterEncodingUTF-8 對(duì)齊避免中文亂碼。建完表后再補(bǔ)幾條測(cè)試數(shù)據(jù)后續(xù)界面調(diào)試會(huì)舒服很多INSERT INTO student (sno, sname, class_name) VALUES (2023001, 張三, 計(jì)科2301), (2023002, 李四, 計(jì)科2301); INSERT INTO course (cno, cname, credit, capacity) VALUES (CS101, Java程序設(shè)計(jì), 3.0, 30), (CS102, 數(shù)據(jù)庫(kù)原理, 2.5, 30); INSERT INTO student_course (student_id, course_id) VALUES (1, 1), (1, 2);這段 SQL 的用意是給登錄和查詢頁(yè)面留好基礎(chǔ)數(shù)據(jù)。張三有兩門(mén)選課且都沒(méi)成績(jī)正好用來(lái)測(cè)試“未錄入”的展示邏輯。2.3 項(xiàng)目骨架普通 Java 工程加 lib 目錄不折騰 Maven 也夠用很多 java 課程設(shè)計(jì)案例源碼喜歡上 Maven但對(duì)課程設(shè)計(jì)來(lái)說(shuō)Maven 第一次下載依賴就可能勸退一半人。我更推薦普通 Java 工程把 MySQL 驅(qū)動(dòng) JAR 放進(jìn) lib 目錄五步就能跑起來(lái)course-system/ src/ com/example/ Main.java util/DBUtil.java dao/CourseDao.java dao/StudentDao.java model/Course.java model/ScoreVO.java view/LoginFrame.java view/MainFrame.java view/CoursePanel.java view/SelectedCoursePanel.java view/ScorePanel.java lib/ mysql-connector-java-8.0.x.jar入口類(lèi)不需要任何花哨寫(xiě)法public class Main { public static void main(String[] args) { new LoginFrame().setVisible(true); } }邏輯說(shuō)明main 方法只做一件事創(chuàng)建 LoginFrame 并顯示。至于 LoginFrame 構(gòu)造方法里要初始化組件、注冊(cè)監(jiān)聽(tīng)器那是 view 包自己的職責(zé)。這種寫(xiě)法雖然簡(jiǎn)單但答辯時(shí)可以順手提一句“單層入口組件自包含”。參數(shù)說(shuō)明這里不能忽略 IDEA 的類(lèi)庫(kù)添加操作。寫(xiě)完代碼后右鍵 lib 目錄并選擇“Add as Library”再運(yùn)行。如果漏了這一步控制臺(tái)會(huì)報(bào)ClassNotFoundException: com.mysql.cj.jdbc.Driver。Eclipse 用戶則要 Build Path - Configure Build Path - Add JARs。在正式寫(xiě)功能前建議先用一個(gè)測(cè)試類(lèi)確認(rèn)驅(qū)動(dòng)能加載排除環(huán)境問(wèn)題再往下寫(xiě)public class TestDriver { public static void main(String[] args) throws Exception { Class.forName(com.mysql.cj.jdbc.Driver); System.out.println(驅(qū)動(dòng)加載成功); } }邏輯說(shuō)明MySQL 8.0 的驅(qū)動(dòng)類(lèi)名是com.mysql.cj.jdbc.Driver舊版本是com.mysql.jdbc.Driver。寫(xiě)死這個(gè)類(lèi)名跑通就說(shuō)明 JAR 和環(huán)境變量都沒(méi)問(wèn)題。這里先走一遍能幫后面省掉大量無(wú)關(guān)報(bào)錯(cuò)。3. JDBC 連接層寫(xiě)好了后面所有功能都是照著填 SQL3.1 DBUtil 工具類(lèi)連接串、驅(qū)動(dòng)加載和資源關(guān)閉Swing 每個(gè)窗口都可能要查庫(kù)所以連接邏輯必須收斂到一個(gè)工具類(lèi)里。一個(gè)合格的 DBUtil 至少要做到三件事加載驅(qū)動(dòng)、獲取連接、釋放資源。我的習(xí)慣是用靜態(tài)塊加載驅(qū)動(dòng)用可變參數(shù)關(guān)閉資源。package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/course_system ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驅(qū)動(dòng)未找到請(qǐng)檢查lib目錄); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r ! null) { try { r.close(); } catch (Exception ignored) {} } } } }邏輯說(shuō)明靜態(tài)塊在類(lèi)加載時(shí)執(zhí)行一次Class.forName的作用是觸發(fā)驅(qū)動(dòng)注冊(cè)到 DriverManager。getConnection 每次調(diào)用都返回一個(gè)物理連接課程設(shè)計(jì)階段沒(méi)必要做連接池。close 方法接收可變個(gè)數(shù)的 AutoCloseableConnection、Statement、ResultSet 都實(shí)現(xiàn)了這個(gè)接口finally 里一次調(diào)用就能全部關(guān)掉。參數(shù)說(shuō)明URL 里有三個(gè)高頻坑別刪。useSSLfalse 關(guān)掉 SSL 握手本地開(kāi)發(fā)能少一個(gè)證書(shū)報(bào)錯(cuò)serverTimezoneAsia/Shanghai 解決 MySQL 8.0 的時(shí)區(qū)校驗(yàn)characterEncodingUTF-8 和建庫(kù)的 utf8mb4 配套防止中文亂碼。USER 和 PASSWORD 要改成你自己安裝 MySQL 時(shí)的賬號(hào)密碼。接著寫(xiě)一個(gè)查詢方法看看 DBUtil 怎么用public ListCourse findAllCourses() { ListCourse list new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(SELECT id, cno, cname, credit, capacity FROM course); rs ps.executeQuery(); while (rs.next()) { Course c new Course(); c.setId(rs.getInt(id)); c.setCno(rs.getString(cno)); c.setCname(rs.getString(cname)); c.setCredit(rs.getDouble(credit)); c.setCapacity(rs.getInt(capacity)); list.add(c); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; }邏輯說(shuō)明先拿連接再預(yù)處理 SQL然后執(zhí)行查詢。rs.next()每調(diào)用一次就向下移動(dòng)一行返回 false 表示沒(méi)有更多數(shù)據(jù)。最后 finally 里關(guān)閉三個(gè)資源順序從 ResultSet 到 Statement 再到 Connection正好是反向創(chuàng)建順序。參數(shù)說(shuō)明rs.getInt(id)是按列名取數(shù)據(jù)比rs.getInt(1)更不容易出錯(cuò)SQL 里一旦改字段順序按列名取就依然安全。3.2 PreparedStatement 登錄校驗(yàn)為什么不用 Statement 拼接登錄功能是評(píng)委最?lèi)?ài)問(wèn)的地方。網(wǎng)上很多舊教程還在用字符串拼接 SQL看著簡(jiǎn)單其實(shí)一個(gè)大坑。只要學(xué)生在學(xué)號(hào)框里輸入 OR 11拼接出的 SQL 就變成了恒真條件密碼直接繞過(guò)。最典型的反例長(zhǎng)這樣// 反例別這么寫(xiě) String sql SELECT * FROM student WHERE sno sno AND password password ;所以我從第一版開(kāi)始就只用 PreparedStatementpublic boolean login(String sno, String password) { String sql SELECT id FROM student WHERE sno ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, sno); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { return rs.next(); } } catch (SQLException e) { e.printStackTrace(); return false; } }邏輯說(shuō)明占位符?只代表參數(shù)位置驅(qū)動(dòng)會(huì)把 setString 傳入的值轉(zhuǎn)義后再放進(jìn) SQL 中因此即使用戶輸入引號(hào)或關(guān)鍵字也不會(huì)改變 SQL 結(jié)構(gòu)。只要rs.next()能返回 true就說(shuō)明找到了匹配的學(xué)號(hào)和密碼。參數(shù)說(shuō)明這里沒(méi)有做密碼加密因?yàn)檎n程設(shè)計(jì)一般不需要但答辯時(shí)最好加一句“生產(chǎn)環(huán)境應(yīng)該存加鹽后的哈希值”。sno 和 password 都通過(guò) setString 傳入數(shù)據(jù)庫(kù)字段長(zhǎng)度要留夠sno 用 20password 用 64足夠裝下常見(jiàn)的哈希串。3.3 DAO 接口與實(shí)現(xiàn)類(lèi)把 SQL 從界面里趕出去Swing 界面里不能直接寫(xiě) JDBC 代碼否則按鈕監(jiān)聽(tīng)器會(huì)變得又長(zhǎng)又亂。常見(jiàn)做法是拆出一個(gè)數(shù)據(jù)訪問(wèn)層接口定義行為實(shí)現(xiàn)類(lèi)寫(xiě) SQL。接口是“合同”實(shí)現(xiàn)類(lèi)才是“施工”。public interface CourseDao { ListCourse findAllCourses(); boolean selectCourse(int studentId, int courseId); boolean dropCourse(int studentId, int courseId); ListScoreVO findScoresBySno(String sno); }邏輯說(shuō)明CourseDao 把課程相關(guān)操作全部抽象出來(lái)。界面層只依賴接口不知道底層是 MySQL 還是別的數(shù)據(jù)庫(kù)。這樣做的好處是以后想換 ORM 框架Swing 界面一行都不用動(dòng)。實(shí)現(xiàn)類(lèi)里選課方法邏輯是插入一條選課記錄public class CourseDaoImpl implements CourseDao { Override public boolean selectCourse(int studentId, int courseId) { String sql INSERT INTO student_course (student_id, course_id) VALUES (?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, studentId); ps.setInt(2, courseId); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }邏輯說(shuō)明executeUpdate 返回受影響行數(shù)。插入成功則返回 1返回 true失敗則返回 false。如果學(xué)生已經(jīng)選過(guò)這門(mén)課數(shù)據(jù)庫(kù)的 UNIQUE KEY 會(huì)直接拋 SQLException界面層可以把這個(gè)異常翻譯成“你已經(jīng)選過(guò)這門(mén)課”的提示框。參數(shù)說(shuō)明這里還要從登錄 Session 拿到 studentId但登錄時(shí)只有學(xué)號(hào)所以常見(jiàn)辦法是在登錄成功后執(zhí)行一條SELECT id FROM student WHERE sno?把真實(shí)主鍵存起來(lái)。權(quán)限暫時(shí)不做管理員登錄也走同一張 student 表只是角色字段沒(méi)加如果評(píng)委問(wèn)起來(lái)可以說(shuō)“增加一個(gè) role 字段即可”。3.4 數(shù)據(jù)模型類(lèi)Course、Student、ScoreVO別用 Map 到處傳查詢結(jié)果變成對(duì)象后界面用起來(lái)才順手。ListMapString, Object 雖然也能用但取值要轉(zhuǎn)來(lái)轉(zhuǎn)去代碼可讀性差。課程設(shè)計(jì)用三個(gè)類(lèi)就夠Course 對(duì)應(yīng)課程表Student 對(duì)應(yīng)學(xué)生表ScoreVO 對(duì)應(yīng)成績(jī)查詢視圖。public class Course { private Integer id; private String cno; private String cname; private Double credit; private Integer capacity; // 構(gòu)造方法、getter/setter 省略 }邏輯說(shuō)明Course 類(lèi)的字段類(lèi)型和數(shù)據(jù)庫(kù)要保持一致但要小心數(shù)據(jù)庫(kù) DECIMAL 映射為 Double 或 BigDecimal 的選擇。學(xué)分 DECIMAL(3,1) 用 Double 完全夠但如果涉及金額計(jì)算更推薦 BigDecimal。課程設(shè)計(jì)里選 Double 代碼更簡(jiǎn)單能在答辯時(shí)解釋清楚“簡(jiǎn)單場(chǎng)景不追求高精度”就夠了。4. Swing 界面從登錄到主窗體布局、事件和表格刷新4.1 登錄窗體的最小實(shí)現(xiàn)JFrame JTextField JPasswordFieldSwing 不需要任何第三方庫(kù)JDK 自帶就是最大的優(yōu)勢(shì)。登錄窗體用最簡(jiǎn)單的 GridLayout三行兩列足夠應(yīng)付演示。public class LoginFrame extends JFrame { private JTextField tfSno new JTextField(); private JPasswordField pfPassword new JPasswordField(); public LoginFrame() { setTitle(學(xué)生選課及成績(jī)管理系統(tǒng) - 登錄); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setLayout(new GridLayout(3, 2)); add(new JLabel(學(xué)號(hào))); add(tfSno); add(new JLabel(密碼)); add(pfPassword); JButton btnLogin new JButton(登錄); add(btnLogin); btnLogin.addActionListener(e - doLogin()); } private void doLogin() { String sno tfSno.getText().trim(); String password new String(pfPassword.getPassword()); if (new StudentDaoImpl().login(sno, password)) { new MainFrame(sno).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, 學(xué)號(hào)或密碼錯(cuò)誤); } } }邏輯說(shuō)明GridLayout 三行兩列依次擺放學(xué)號(hào)、輸入框、密碼、密碼框、空組件、登錄按鈕布局雖然樸素但不會(huì)錯(cuò)位。密碼框用 JPasswordField 而不是 JTextField用戶輸入的內(nèi)容在界面上顯示為圓點(diǎn)。doLogin 里先 trim 學(xué)號(hào)去掉前后空格再調(diào)用登錄方法。參數(shù)說(shuō)明setLocationRelativeTo(null)讓窗體居中顯示這個(gè)細(xì)節(jié)放在 360 乘 220 的窗口上剛剛好。dispose()會(huì)在登錄成功后釋放當(dāng)前窗體不釋放的話主窗體出現(xiàn)后登錄窗口還留在任務(wù)欄里進(jìn)程行為會(huì)很奇怪。注意JPasswordField 的 getPassword 返回的是 char[]用 new String 轉(zhuǎn)成字符串后用完最好及時(shí)置空。課程設(shè)計(jì)不用太較真但代碼里不要順手 System.out.println 打印密碼。4.2 主窗體用 JTabbedPane 拆頁(yè)面課程列表、我的選課、成績(jī)查詢課程設(shè)計(jì)階段的系統(tǒng)功能不會(huì)特別多主窗體用一個(gè) JTabbedPane 就足夠了。三個(gè)標(biāo)簽頁(yè)分別對(duì)應(yīng)三個(gè) JPanel 子類(lèi)每個(gè)面板只負(fù)責(zé)自己的表格和按鈕。public class MainFrame extends JFrame { public MainFrame(String sno) { setTitle(歡迎 sno); setSize(800, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabs new JTabbedPane(); tabs.addTab(課程列表, new CoursePanel()); tabs.addTab(我的選課, new SelectedCoursePanel(sno)); tabs.addTab(成績(jī)查詢, new ScorePanel(sno)); add(tabs, BorderLayout.CENTER); } }邏輯說(shuō)明登錄時(shí)把學(xué)號(hào) sno 傳進(jìn)來(lái)主窗體再把它分發(fā)給兩個(gè)需要學(xué)生信息的頁(yè)面。CoursePanel 不知道當(dāng)前用戶是誰(shuí)因?yàn)樗回?fù)責(zé)展示所有課程和“選課”按鈕選課時(shí)再根據(jù)學(xué)號(hào)回查學(xué)生 ID。這樣劃分每個(gè)類(lèi)的職責(zé)比較清晰。參數(shù)說(shuō)明setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)是主窗體特有的登錄窗體如果也用 EXIT_ON_CLOSE那么 dispose 時(shí)整個(gè)進(jìn)程都退出。登錄成功后的主窗體采用 EXIT 是合理的關(guān)閉主界面等于退出程序。4.3 成績(jī)單表格DefaultTableModel 怎么做到刷新不重復(fù)表格是 Swing 里最容易寫(xiě)錯(cuò)的部分尤其是刷新邏輯。常見(jiàn)錯(cuò)誤是點(diǎn)一次查詢表格里的成績(jī)多一行越點(diǎn)越多。原因很直接每次都 addRow從不清理舊數(shù)據(jù)。正確的刷新方式是先清空再填充public void refreshTable(ListScoreVO scores) { DefaultTableModel model (DefaultTableModel) table.getModel(); model.setRowCount(0); for (ScoreVO vo : scores) { model.addRow(new Object[]{ vo.getCno(), vo.getCname(), vo.getScore() null ? 未錄入 : vo.getScore() }); } }邏輯說(shuō)明setRowCount(0) 把 TableModel 的所有現(xiàn)有行清空相當(dāng)于一張白紙。然后再 addRow 循環(huán)添加新數(shù)據(jù)這樣表格永遠(yuǎn)只顯示最新查詢結(jié)果。這里將 score 為 null 的情況顯示為“未錄入”比直接顯示 null 要專業(yè)得多。參數(shù)說(shuō)明如果使用的是自己繼承 AbstractTableModel 的自定義模型改完數(shù)據(jù)后還要調(diào)用 fireTableDataChanged()否則界面不會(huì)重新繪制。JTable 默認(rèn)列寬會(huì)自動(dòng)均分課程列表課程名較長(zhǎng)時(shí)容易被擠壓建議加一行table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); JScrollPane sp new JScrollPane(table);邏輯說(shuō)明把表放進(jìn) JScrollPane并把自動(dòng)調(diào)整列寬關(guān)掉這樣列寬由表格自身決定內(nèi)容太長(zhǎng)時(shí)會(huì)出現(xiàn)橫向滾動(dòng)條而不是把課程名壓縮成省略號(hào)。4.4 選課按鈕的事件處理先查后插再給用戶一個(gè)反饋選課按鈕是事件處理里最有代表性的。點(diǎn)擊后不能只做一次 insert否則用戶連點(diǎn)兩下就出重復(fù)數(shù)據(jù)。除了數(shù)據(jù)庫(kù)的唯一約束之外界面層也要給出友好反饋btnSelect.addActionListener(e - { btnSelect.setEnabled(false); int selectedRow courseTable.getSelectedRow(); if (selectedRow 0) { JOptionPane.showMessageDialog(this, 請(qǐng)先選擇一門(mén)課程); btnSelect.setEnabled(true); return; } int courseId (int) courseTable.getValueAt(selectedRow, 0); boolean ok courseDao.selectCourse(studentId, courseId); if (ok) { JOptionPane.showMessageDialog(this, 選課成功); refreshTable(); } else { JOptionPane.showMessageDialog(this, 選課失敗可能重復(fù)選課或課程不存在); } btnSelect.setEnabled(true); });邏輯說(shuō)明按鈕置灰是為了防止快速連點(diǎn)導(dǎo)致重復(fù)提交數(shù)據(jù)庫(kù)唯一約束仍然是最底線的保護(hù)。getSelectedRow 返回 -1 表示沒(méi)選中行此時(shí)直接提示不執(zhí)行 SQL。成功后立即刷新課程列表的剩余容量和“我的選課”頁(yè)簽界面數(shù)據(jù)保持一致。參數(shù)說(shuō)明getValueAt 返回的是 Object這里選課表的第 0 列是課程 ID所以強(qiáng)轉(zhuǎn)成 int。如果你的表格列順序調(diào)整過(guò)一定要同步修改這里這是表格代碼里最常見(jiàn)的隱性 Bug。5. 避坑指南Swing MySQL 項(xiàng)目最容易翻車(chē)的 5 個(gè)細(xì)節(jié)5.1 中文亂碼三個(gè)地方少一個(gè)就白搭現(xiàn)象界面上課程名顯示成“???”或者 MySQL Workbench 里中文正常Java 程序里讀出亂碼。原因建庫(kù)字符集、JDBC 連接串字符集、Java 源文件編碼三者不一致。最常見(jiàn)的是建庫(kù)用了默認(rèn) latin1而連接串里指定了 UTF-8兩邊對(duì)不上。解決先確認(rèn)建庫(kù)和建表都用了DEFAULT CHARSETutf8mb4然后在 JDBC URL 里加characterEncodingUTF-8。如果已經(jīng)建好的庫(kù)是 latin1用下面兩條 SQL 補(bǔ)救ALTER DATABASE course_system CHARACTER SET utf8mb4; ALTER TABLE course CONVERT TO CHARACTER SET utf8mb4;如果是 IDEA 項(xiàng)目編譯亂碼則要檢查 Settings - File Encodings把所有位置都設(shè)為 UTF-8。這三個(gè)地方對(duì)不上任何一個(gè)亂碼就跑不了。5.2 MySQL 8.0 連不上Public Key Retrieval is not allowed現(xiàn)象點(diǎn)擊登錄按鈕后控制臺(tái)拋出java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed。原因MySQL 8.0 默認(rèn)用 caching_sha2_password 認(rèn)證客戶端第一次連接時(shí)要從服務(wù)器獲取公鑰來(lái)加密密碼但 Connector/J 出于安全考慮把 allowPublicKeyRetrieval 默認(rèn)設(shè)為 false所以連接被拒絕。解決在 JDBC URL 中顯式加上allowPublicKeyRetrievaltrue和useSSLfalse。本地開(kāi)發(fā)沒(méi)問(wèn)題但不要讓這套配置出現(xiàn)在生產(chǎn)環(huán)境里。String url jdbc:mysql://localhost:3306/course_system?useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/ShanghaicharacterEncodingUTF-8;邏輯說(shuō)明這兩個(gè)參數(shù)通常成對(duì)出現(xiàn)useSSLfalse 避免證書(shū)校驗(yàn)allowPublicKeyRetrievaltrue 允許本地請(qǐng)求公鑰。報(bào)了別的錯(cuò)誤的話優(yōu)先看完整異常堆棧而不是只搜前半段。5.3 JTable 數(shù)據(jù)不刷新或越插越多現(xiàn)象第一次點(diǎn)“查詢成績(jī)”顯示三行第二次點(diǎn)變成六行第三次變成九行數(shù)據(jù)越查越多。原因點(diǎn)擊事件里只調(diào)用 addRow沒(méi)有先清理舊數(shù)據(jù)TableModel 是在老數(shù)據(jù)基礎(chǔ)上不斷追加。解決在每次查詢填充數(shù)據(jù)前調(diào)用model.setRowCount(0)。如果是擴(kuò)展 AbstractTableModel 的自定義模型則在數(shù)據(jù)更新后調(diào)用fireTableDataChanged()通知 JTable 重新繪制。還有一個(gè)小問(wèn)題不要把查詢邏輯寫(xiě)在 JTable 的鼠標(biāo)事件里而是在一個(gè)獨(dú)立的方法中完成查詢加刷新按鈕和別的事件都能復(fù)用它。5.4 選課重復(fù)提交唯一約束兜底按鈕置灰防手滑現(xiàn)象快速雙擊“選課”按鈕同一個(gè)學(xué)生同一門(mén)課程出現(xiàn)兩條記錄。原因第一次點(diǎn)擊的插入還沒(méi)執(zhí)行完第二次點(diǎn)擊已經(jīng)發(fā)出了同樣的 INSERT。界面層沒(méi)有防抖數(shù)據(jù)庫(kù)也沒(méi)有約束時(shí)重復(fù)數(shù)據(jù)就進(jìn)了表。解決數(shù)據(jù)庫(kù)層面已經(jīng)用 UNIQUE KEY 攔住重復(fù)第一次插入成功第二次會(huì)拋 SQLException。程序里可以捕獲這個(gè)異常并提示“你已經(jīng)選過(guò)這門(mén)課程”。更穩(wěn)妥的做法是在點(diǎn)擊后立刻 disable 按鈕等數(shù)據(jù)庫(kù)返回結(jié)果后再恢復(fù)。這個(gè)場(chǎng)景在 mysql 面試題里也很常問(wèn)你答“唯一約束優(yōu)先”比答“先查再插”更高級(jí)。5.5 界面卡死耗時(shí)查詢不放在事件線程里現(xiàn)象點(diǎn)擊“查詢成績(jī)”后整個(gè)窗口像死機(jī)一樣拖不動(dòng)也不能最小化。原因Swing 的事件處理代碼運(yùn)行在事件分發(fā)線程 EDT 上。數(shù)據(jù)庫(kù)查詢、網(wǎng)絡(luò)請(qǐng)求這類(lèi)阻塞操作如果直接寫(xiě)在按鈕監(jiān)聽(tīng)器里EDT 會(huì)被阻塞所有界面繪制都停住。解決最簡(jiǎn)單的處理是查詢前禁用按鈕并顯示“查詢中”查詢完再恢復(fù)。想加分的話用 SwingWorker 把查詢放到后臺(tái)線程SwingWorkerVoid, Void worker new SwingWorkerVoid, Void() { Override protected Void doInBackground() { result courseDao.findScoresBySno(sno); return null; } Override protected void done() { refreshTable(result); } }; worker.execute();邏輯說(shuō)明doInBackground 運(yùn)行在后臺(tái)線程不會(huì)卡住界面done 方法在主線程執(zhí)行可以安全更新表格和按鈕狀態(tài)。這就是 Swing 應(yīng)用處理耗時(shí)任務(wù)的標(biāo)準(zhǔn)姿勢(shì)也是課程設(shè)計(jì)答辯中一個(gè)非常值的提的改進(jìn)點(diǎn)。6. 答辯前做這 3 件事功能不變分?jǐn)?shù)明顯不一樣如果你做完基本功能還有一兩天時(shí)間把以下三件事補(bǔ)上系統(tǒng)會(huì)從“能跑”變成“能講”。第一件事是把選課方法改成事務(wù)版。數(shù)據(jù)庫(kù)事務(wù)不用整太復(fù)雜只要把原來(lái)的“一條 INSERT”改成“檢查容量、插入選課記錄、更新已選人數(shù)”的完整過(guò)程。手動(dòng)調(diào)用conn.setAutoCommit(false)中間任何一步拋異常就conn.rollback()全部成功再conn.commit()。這會(huì)讓評(píng)委覺(jué)得你不是只會(huì)拼 SQL而是考慮過(guò)數(shù)據(jù)一致性。第二件事是在成績(jī)頁(yè)面加一個(gè)統(tǒng)計(jì)欄。SQL 用一條聚合查詢就能實(shí)現(xiàn)SELECT COUNT(*) AS total, AVG(score) AS avg_score, MAX(score) AS max_score FROM student_course WHERE student_id ?;把結(jié)果顯示在成績(jī)表格下方比如“共 3 門(mén)課平均分 86.3最高分 95”。這一小段能引出的問(wèn)題包括聚合函數(shù)、NULL 值處理、分組查詢都是 java 八股文和數(shù)據(jù)庫(kù)面試的高頻點(diǎn)你答起來(lái)也有底氣。第三件事是把成績(jī)導(dǎo)出成 CSV 文件。這個(gè)功能不依賴任何第三方庫(kù)純 JDK 就能寫(xiě)StringBuilder sb new StringBuilder(課程編號(hào),課程名稱,成績(jī)\n); for (ScoreVO vo : list) { sb.append(vo.getCno()).append(,) .append(vo.getCname()).append(,) .append(vo.getScore() null ? : vo.getScore()).append(\n); } Files.write(Paths.get(scores.csv), sb.toString().getBytes(StandardCharsets.UTF_8));邏輯說(shuō)明用逗號(hào)分隔每列用換行分隔每行。成績(jī)?yōu)榭諘r(shí)寫(xiě)空字符串而不是寫(xiě) null。Files.write 會(huì)覆蓋已有文件目錄里有老文件時(shí)會(huì)直接替換。如果成績(jī)里有特殊字符或姓名包含逗號(hào)記得再加一層引號(hào)包裹但課程設(shè)計(jì)場(chǎng)景一般用不到。我當(dāng)年做完這套系統(tǒng)后覺(jué)得能跑通就算完事結(jié)果評(píng)委問(wèn)“選課并發(fā)時(shí)怎么保證容量”我答不上來(lái)。后來(lái)把事務(wù)和統(tǒng)計(jì)加上同樣的題目講的內(nèi)容一下子多了起來(lái)。你按照這個(gè)順序往下做最后輸出的一定是一個(gè)能演示、能講清、能抗追問(wèn)的完整工程。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取