简介:Java课程设计常让初学者卡在界面与数据的衔接上。从基础概念看,桌面应用开发需要一套清晰的UI框架与持久化存储方案:Swing负责构建用户交互界面,MySQL承担数据存储,二者通过JDBC建立连接。理解PreparedStatement防注入、事务边界和连接参数配置,是任何Java数据库应用的核心技术价值。无论是教务管理、学生选课还是成绩录入,这类典型的CRUD系统都是学习Java桌面开发的常见应用场景。本文以学校教务管理系统为载体,围绕MySQL建表、JDBC驱动配置、Swing窗体布局、JTable数据展示以及打包部署展开,系统梳理从零到可运行的全过程,并针对中文乱码、时区错误、界面卡死等高频工程问题给出排查思路,帮助读者快速交付一个能上台演示的完整课设项目。
1. 教务管理系统这套组合拳:Swing做界面、MySQL存数据,课程设计最稳的选择
每年毕业季和期末季,总有一批人被Java课程设计折磨到凌晨两点。网上能下载的源码要么是SSM框架的Web项目看不懂,要么是纯控制台程序拿不出手。而Java+Swing+MySQL这套组合,恰好卡在最讨巧的位置:Swing负责把界面画出来,MySQL负责把数据存进去,两者用JDBC一连接,就是一个看得见摸得着的桌面应用。学校教务管理系统这类题目,本质上是学生管理、教师管理、课程管理、选课和成绩录入这五个模块的CRUD组合,用Swing做前端表格展示,用MySQL做持久化存储,逻辑链路短,出成果快,答辩时也能把每个类的作用讲清楚。这篇文章从建表到跑通,把整条路上的关键代码和坑都摊开讲。
2. 先把数据层立住:教务系统的MySQL表设计与JDBC连接细节
2.1 六张核心表的字段设计:从ER图到建表SQL
教务管理系统不管界面长什么样,数据层总是那几件事:用户要登录,所以要有一张用户表;学生和教师是两个不同的角色,信息字段不一样,所以要分开建;课程要开课和选课,需要课程表;选课记录和成绩是强关联的,可以合并成一张选课成绩表,也可以拆开。我一般拆成六张表,字段设计如下。
学生表(student)、教师表(teacher)、课程表(course)这三张是基础信息表,字段设计都不复杂。选课表(select_course)是关联表,它同时关联student_id和course_id,还要存一个score字段用于成绩录入。用户表(sys_user)单独存在,不跟学生教师表合并,原因是学生的学号和教师的工号可能重复,但登录用户名必须全局唯一。
建表SQL直接放在MySQL命令行或Navicat里执行。前面提到的mysql安装教程、mysql安装配置教程这些热搜词的搜索结果里,安装的坑基本集中在服务启动失败和root密码遗忘,这些跟建表无关,但需要注意的是字符集一定要用utf8mb4,否则后面存中文姓名时会出现问号。
CREATE DATABASE IF NOT EXISTS edu_admin DEFAULT CHARSET utf8mb4; USE edu_admin; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(10) NOT NULL COMMENT 'ADMIN/TEACHER/STUDENT' ); CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, gender VARCHAR(4), class_name VARCHAR(30) ); CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, title VARCHAR(20) ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(50) NOT NULL, credit INT, teacher_id INT, FOREIGN KEY (teacher_id) REFERENCES teacher(id) ); CREATE TABLE select_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) DEFAULT NULL, UNIQUE KEY uk_stu_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) );这段SQL的AUTO_INCREMENT是MySQL的自增主键写法,跟Oracle的序列不一样,Swing程序里插入记录后要马上拿到自增ID的话,可以用JDBC的getGeneratedKeys方法,后面写DAO层时会讲到。UNIQUE KEY uk_stu_course这里是个两层含义的约束,它既防止同一个学生重复选同一门课,又是后续做选课逻辑时捕获重复异常的判定依据。角色字段role用字符串类型,取值就ADMIN、TEACHER、STUDENT三个,不要用数字代替,程序里读出来还要翻译一遍,纯属给自己找麻烦。
2.2 JDBC连接MySQL:驱动加载、URL参数与连接池取舍
数据表建好之后,Java这边要解决的是怎么连上MySQL。JDBC的套路是固定的四步:加载驱动、获取连接、执行SQL、关闭资源。这里最关键的是MySQL 8以上的驱动类名和URL参数跟MySQL 5有区别。MySQL 5用com.mysql.jdbc.Driver,MySQL 8必须换成com.mysql.cj.jdbc.Driver,URL里还要额外加serverTimezone和useSSL两个参数,不然会报时区错误。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/edu_admin" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动加载失败,请检查mysql-connector-java的jar包是否导入", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }数据库连接池是否要用,是很多新手纠结的地方。课程设计这个体量,同时在线操作的人数不超过几十个,每次操作新建一个Connection完全够用。C3P0、Druid这些连接池是给Web应用的高并发场景用的,Swing单机程序引入它们反而是负优化——配置麻烦、依赖变多、程序启动变慢。所以DBUtil里直接写DriverManager获取连接,简单直接。
URL参数里的characterEncoding=utf8值得单独说,它的作用是告诉MySQL驱动这边传输的字节流是UTF-8编码。这个参数和建表时的DEFAULT CHARSET utf8mb4是两个层面的东西,表的字符集决定数据怎么存,URL的编码参数决定连接怎么传,两个都要配,缺一个都会在写入中文时出问题。serverTimezone=Asia/Shanghai是针对MySQL 8的强制要求,MySQL 5.6及以下版本不填也能跑,但填了也不会出错,建议统一加上。
2.3 数据库操作封装:用PreparedStatement代替Statement
连接写好后,接下来的数据库操作封装直接决定后面DAO层的代码量。我见过不少课设源码在DAO里大量拼接字符串SQL,然后用Statement执行。这种写法在单机课设里能跑,但有两个隐患:一是SQL注入不是Web项目专有的,Swing程序里用户在JTextField里输入的内容同样能拼进SQL;二是字符串拼接遇到单引号就得手工转义,中文姓名里有'的情况很常见。PreparedStatement能同时解决这两个问题,代价只是多写两行代码。
public boolean addStudent(Student s) { String sql = "INSERT INTO student(student_no, name, gender, class_name) VALUES(?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setString(4, s.getClassName()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }这里用了try-with-resources语法,Java 7以后的推荐写法,Connection和PreparedStatement都会自动关闭,不用在finally里手工判断null。setString那几行就是把用户输入的内容作为参数传给SQL,有特殊字符时由驱动负责转义,不需要自己处理。executeUpdate的返回值是受影响的行数,插入成功返回1,更新或删除返回对应行数。这个方法返回boolean是为了让Swing界面的按钮监听器拿返回值做提示,比如「添加成功」或「添加失败」。
3. Swing界面从登录页到主窗体:布局、表格与事件监听
3.1 登录窗体:JFrame、JTextField与用户校验的完整链路
登录表单是系统的入口,也是第一眼印象。Swing的登录窗体有个很典型的布局套路:中间放一个JPanel,用GridBagLayout或空布局绝对定位,放两个JLabel、两个JTextField(密码框用JPasswordField)、一个JButton。绝对布局在这里其实比GridBagLayout更好用,因为登录框的位置固定就那么几个控件,手写坐标反而直观。
public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { setTitle("教务管理系统登录"); setSize(400, 280); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel = new JPanel(null); JLabel userLabel = new JLabel("用户名:"); userLabel.setBounds(60, 60, 60, 30); usernameField = new JTextField(); usernameField.setBounds(130, 60, 180, 30); JLabel pwdLabel = new JLabel("密码:"); pwdLabel.setBounds(60, 110, 60, 30); passwordField = new JPasswordField(); passwordField.setBounds(130, 110, 180, 30); JButton loginBtn = new JButton("登录"); loginBtn.setBounds(150, 170, 100, 35); loginBtn.addActionListener(e -> doLogin()); panel.add(userLabel); panel.add(usernameField); panel.add(pwdLabel); panel.add(passwordField); panel.add(loginBtn); add(panel); } private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空"); return; } // 调用UserDao的login方法校验,成功后跳转主窗体 } }JPasswordField的getPassword返回的是char数组而不是String,这是Swing故意做的设计,目的是避免密码字符串驻留在内存中。实际使用时new String(passwordField.getPassword())把它转成String,用的地方顺手,缺点是这个字符串会留在堆里,对课设项目来说不算什么大问题。登录成功后要做的动作是dispose掉当前窗体,然后new一个MainFrame并setVisible(true)。注意不要用setVisible(false)替代dispose,登录窗体被dispose后资源会释放,用户关掉主窗体时整个进程能正常退出。
3.2 主窗体布局:JTabbedPane挂载功能面板
登录后的主窗体用JTabbedPane是最主流的选择,顶部是选项卡,每个选项卡对应一个功能模块。教务管理系统的功能模块通常是学生管理、教师管理、课程管理、选课管理、成绩管理,再加一个个人信息。这样设计的理由是JTabbedPane天然隔离了各个模块的界面复杂度,每个模块一个JPanel,面板内部自己管自己的布局,跟其他模块互不干扰。
public class MainFrame extends JFrame { private JTabbedPane tabbedPane; private String currentUser; private String currentRole; public MainFrame(String username, String role) { this.currentUser = username; this.currentRole = role; setTitle("学校教务管理系统 - 当前用户: " + username); setSize(900, 600); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); tabbedPane = new JTabbedPane(); tabbedPane.addTab("学生管理", new StudentPanel()); tabbedPane.addTab("课程管理", new CoursePanel()); tabbedPane.addTab("选课管理", new SelectCoursePanel()); add(tabbedPane, BorderLayout.CENTER); } }根据角色隐藏选项卡是权限控制里最简单的一种做法:学生登录后不应该看到教师管理和成绩录入,教师登录后不应该看到学生管理。实现方式是在构造MainFrame时判断currentRole,对需要隐藏的模块不执行addTab。角色权限过滤的问题在于它只控制了界面入口,并没有拦截底层DAO方法——如果学生通过某种方式调用TeacherDao方法,数据还是能被改。课设答辩一般不会追问到这一层,但知道这个薄弱点在哪里总比不知道要好。
3.3 JTable显示数据:DefaultTableModel与数据刷新机制
JTable是Swing里展示数据库记录的核心控件。它本身只负责显示,数据放在TableModel里。初学者常见的误区是直接往JTable里塞数据数组,然后发现表格列头和排序功能都没法正常用。正确的做法是创建一个DefaultTableModel,用setColumnIdentifiers设置列名,再用setDataVector或逐行addRow填充数据。
public class StudentPanel extends JPanel { private JTable table; private DefaultTableModel model; public StudentPanel() { setLayout(new BorderLayout()); model = new DefaultTableModel(); model.setColumnIdentifiers(new Object[]{"ID", "学号", "姓名", "性别", "班级"}); table = new JTable(model); add(new JScrollPane(table), BorderLayout.CENTER); refreshData(); } public void refreshData() { model.setRowCount(0); // 清空旧数据 List<Student> list = new StudentDao().findAll(); for (Student s : list) { model.addRow(new Object[]{ s.getId(), s.getStudentNo(), s.getName(), s.getGender(), s.getClassName() }); } } }refreshData放在每次增删改操作后调用一次,界面就能立即显示最新数据。setRowCount(0)是清空表格常用手段,它的作用是把所有行删掉但保留列结构,比反复new DefaultTableModel更轻量。操作按钮通常放在表格下方的按钮面板里,点击「添加」时弹出一个JDialog收集输入,点击「删除」时通过table.getSelectedRow()获取选中行,再通过model.getValueAt(row, 1)拿到学号传给删除方法。这里有个隐藏坑:getSelectedRow返回的是视图行索引,当表格排序功能被启用后视图行和数据行可能不一致。课上不启用排序,就没这个问题,但一旦调用了table.setAutoCreateRowSorter(true),取值逻辑就要用convertRowIndexToModel转换。
4. 主流程串起来:学生管理、选课与成绩录入的完整功能链
4.1 学生管理的增删改查:DAO层与UI层怎么配合
教务系统最核心的模块是学生管理。这个模块的完整链路是:UI层在事件监听器里调用DAO层方法,DAO层返回数据给UI层,UI层用JOptionPane提示结果并刷新表格。核心操作就四件事:查询全部、按学号查询、新增、删除。修改可以不单独做,用「删除后重新添加」的替代方案,很多课设就是这么干的,虽然不够优雅,但工作量能省一半。
public class StudentDao { public List<Student> findAll() { List<Student> list = new ArrayList<>(); String sql = "SELECT id, student_no, name, gender, class_name FROM student"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student s = new Student(); s.setId(rs.getInt("id")); s.setStudentNo(rs.getString("student_no")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setClassName(rs.getString("class_name")); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; } public boolean deleteByStudentNo(String studentNo) { String sql = "DELETE FROM student WHERE student_no = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, studentNo); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }删除学生时有个关联数据问题:如果这个学生已经有选课记录,直接在student表删掉会报外键约束错误。两个解决办法,一是删除前先查select_course表有没有关联记录,有就提示「请先删除选课记录」;二是建表时给外键加上ON DELETE CASCADE,让MySQL自动级联删除选课记录。课设里推荐第一种,因为第二种会让数据在用户无感知的情况下丢失,答辩时被问到会很难解释清楚。
4.2 选课模块:一个事务搞定选课与选课记录写入
选课的流程是:界面左侧一个学生列表,右侧一个可选课程列表,点「选课」按钮后把当前选中的学生和课程组合写入select_course表。这个动作本身很简单,关键是对重复选课的处理。前面建表时已经定义了UNIQUE KEY uk_stu_course,所以重复插入会抛SQLIntegrityConstraintViolationException。
public boolean selectCourse(int studentId, int courseId) { String sql = "INSERT INTO select_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 (SQLIntegrityConstraintViolationException e) { // 重复选课:唯一索引拦截 return false; } catch (SQLException e) { e.printStackTrace(); return false; } }业务层捕捉SQLIntegrityConstraintViolationException并返回false,UI层拿到false后提示「该学生已选此课程」。这个方案比先SELECT再INSERT的高明之处在于:两条SQL之间有时间窗口,并发时两个请求同时通过SELECT判断,然后同时执行INSERT,还是会产生重复数据。数据库唯一约束加上异常捕获,是从根上规避问题。课程设计虽然是单机程序,但养成这种意识对后续做Web项目帮助很大。事务在这里没有强需求,因为一次选课只操作一张表,不需要引入事务。
4.3 成绩录入:跨表联查与JTable回填
成绩录入模块是最能体现SQL能力的部分。它需要关联select_course表、student表和course表,查出某门课程的所有选课学生,再允许教师录入或修改分数。查询SQL用JOIN实现,比多次查库然后内存里拼数据要高效得多,代码也更短。界面用一个下拉框选择课程,选中后表格刷新,显示该课程下的学生列表和当前分数。
public List<Map<String, Object>> findScoreByCourse(int courseId) { List<Map<String, Object>> list = new ArrayList<>(); String sql = "SELECT sc.id, st.student_no, st.name, sc.score " + "FROM select_course sc " + "JOIN student st ON sc.student_id = st.id " + "WHERE sc.course_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, courseId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Map<String, Object> row = new HashMap<>(); row.put("id", rs.getInt("id")); row.put("studentNo", rs.getString("student_no")); row.put("name", rs.getString("name")); row.put("score", rs.getObject("score")); list.add(row); } } } catch (SQLException e) { e.printStackTrace(); } return list; }表格里显示score这一列后,双击单元格即可修改分数,Swing的JTable默认不开启单元格编辑,需要table.setRowSelectionAllowed(true)配合DefaultCellEditor才能实现双击编辑。设置方式可以单独配置某一列可编辑:重写DefaultTableModel的isCellEditable方法,只对score列返回true,其他列都返回false。这样能防止学生信息被误改,同时允许教师直接双击录入分数。分数列设置成可编辑后,还需要在单元格失去焦点时触发保存,写法是给JTable添加CellEditorListener,监听editingStopped事件,在事件里读取编辑后的值并调用DAO更新数据库。
5. 教务系统开发避坑:乱码、时区、线程阻塞与SQL注入的五条血泪记录
5.1 中文乱码:从数据库到界面三层逐个排查
现象:Swing界面显示中文姓名变成「???」或「鏉庣幇」。原因通常有三层:第一层是建表时字符集不是utf8mb4,存进去就是乱码;第二层是JDBC的URL里缺少characterEncoding=utf8,驱动和MySQL之间传输时转换出错;第三层是Swing程序本身的编译编码不对,用javac编译时默认使用平台编码,Windows中文系统会用GBK去读UTF-8的源文件,字符串常量直接变乱码。解决:建表用第2章的SQL指定utf8mb4;JDBC的URL加characterEncoding=utf8;编译时用javac -encoding UTF-8,或者在IntelliJ IDEA里把Project Encoding和File Encoding都设为UTF-8。绝大多数乱码问题逃不出这三个位置。
5.2 MySQL 8连接报错:serverTimezone不配就翻车
现象:连接数据库时报java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized,或者Commucations link failure。原因:MySQL 8的驱动对时区敏感,默认要跟服务器协商时区,而JDBC URL里没指定时区时驱动会使用JVM默认时区,双方对不上就报错。解决:JDBC URL里加serverTimezone=Asia/Shanghai。这个参数的效果是指定驱动使用中国时区,如果服务器部署在国外机房而用户在国内,建议用UTC或系统环境变量动态获取,避免写死。课设本地跑写Asia/Shanghai就够了。
5.3 Swing界面卡死:耗时操作阻塞了EDT线程
现象:点击「查询所有学生」按钮后整个界面变成白板,鼠标变成转圈状态,几秒或十几秒后才恢复。原因:Swing的UI刷新和事件处理都在同一个线程EDT(Event Dispatch Thread)上执行,查询数据库是耗时操作,在事件监听器里同步执行时整个UI线程被阻塞。解决:查询操作拿到子线程里执行,执行完再用SwingUtilities.invokeLater回到EDT刷新表格。
new Thread(() -> { List<Student> list = new StudentDao().findAll(); SwingUtilities.invokeLater(() -> { model.setRowCount(0); for (Student s : list) { model.addRow(...); } }); }).start();数据量在几百行以下时同步查询的卡顿感知不强,但一旦select_course表数据量大或者和远程MySQL相连,卡顿立刻出现。线程方案的副作用是快速连续点击按钮会创建多个线程同时查询,课设可以忽略这个并发问题,但知道有这个问题存在。
5.4 SQL注入防御:PreparedStatement不是可选项
现象:在登录界面输入用户名admin' OR '1'='1和任意密码,直接登录成功。原因:登录SQL如果写成SELECT * FROM sys_user WHERE username = '" + username + "' AND password = '" + password + "'",输入内容被拼接进SQL,引号提前闭合,OR条件让整个WHERE永真。解决:所有需要拼接用户输入的地方一律用PreparedStatement参数占位。第2章和第4章的DAO代码已经全部使用占位符风格。另外一个容易忽略的注入点是排序字段,ORDER BY子句不能使用PreparedStatement占位符,如果排序字段来自用户输入,必须用白名单校验。
5.5 驱动版本与MySQL版本不匹配:ClassNotFoundException的根源
现象:运行程序时抛ClassNotFoundException: com.mysql.jdbc.Driver。原因:程序里写了com.mysql.jdbc.Driver,但实际导入的jar包是mysql-connector-java 8.x版本,8.x版本已经把这个类移除了,驱动类换成了com.mysql.cj.jdbc.Driver。解决:要么把代码里的DRIVER常量改成com.mysql.cj.jdbc.Driver,要么把依赖降级到mysql-connector-java 5.1.x版本并保持代码不变。建议直接改代码,因为MySQL 8是当前主流,新装的mysql安装教程基本都是8.x,把代码适配到新驱动比找老jar包更省事。
6. 让系统更像个产品:登录状态保持、报表导出与一键打包的进阶验证
6.1 登录状态保持:用ThreadLocal模拟Session
Swing没有Web容器那样的Session对象,跨窗口传递用户信息全靠构造参数或静态变量。构造参数方式在代码上要层层透传,从LoginFrame传到MainFrame再传到每个Panel,类一多就乱。静态变量的方式简单但不够隔离。一个折中方案是用ThreadLocal存当前登录用户,代码里任何地方都能取到,又不会污染全局命名空间。
public class UserContext { private static final ThreadLocal<String> CURRENT_USER = new ThreadLocal<>(); private static final ThreadLocal<String> CURRENT_ROLE = new ThreadLocal<>(); public static void login(String username, String role) { CURRENT_USER.set(username); CURRENT_ROLE.set(role); } public static String getUsername() { return CURRENT_USER.get(); } public static String getRole() { return CURRENT_ROLE.get(); } public static void logout() { CURRENT_USER.remove(); CURRENT_ROLE.remove(); } }登录成功时调用UserContext.login,退出登录时调用logout。这个设计在Swing里是"用不上但值得练手"的形态,它背后的ThreadLocal原理在java面试题里出现频率极高,面试官问「ThreadLocal怎么保证数据一致性」「内存泄漏怎么避免」时,亲手用过和只在八股文里背过是完全不一样的回答状态。这样就可以说"data source"。jnuitangele出一个tab panel。leTab。leTab。
6.2 成绩单导出:从JTable生成Excel兼容的CSV文件
教务系统的成绩模块做出来后,通常会有「导出成绩单」的需求。直接引入Apache POI这个重量级依赖对于课程设计有点过剩,为了导出一个表格引入一个几MB的jar包性价比不高。简单做法是生成CSV文件,Excel能直接打开,而且它是纯文本格式,编码控制好了不会有乱码问题。关键点是Excel打开CSV默认按ANSI编码解释文本,中文内容必须先写入UTF-8 BOM头,否则打开后中文全乱。
public void exportCsv(JTable table, File file) throws IOException { try (FileOutputStream fos = new FileOutputStream(file)) { fos.write(new byte[]{(byte) 0xEF, (byte) 0xBB, (byte) 0xBF}); // UTF-8 BOM ByteArrayOutputStream baos = new ByteArrayOutputStream(); baos.write((table.getColumnName(0) + ",").getBytes(StandardCharsets.UTF_8)); for (int i = 1; i < table.getColumnCount(); i++) { baos.write(("," + table.getColumnName(i)).getBytes(StandardCharsets.UTF_8)); } baos.write("\n".getBytes(StandardCharsets.UTF_8)); for (int r = 0; r < table.getRowCount(); r++) { for (int c = 0; c < table.getColumnCount(); c++) { Object val = table.getValueAt(r, c); baos.write((val == null ? "" : val.toString()).getBytes(StandardCharsets.UTF_8)); if (c < table.getColumnCount() - 1) { baos.write(",".getBytes(StandardCharsets.UTF_8)); } } baos.write("\n".getBytes(StandardCharsets.UTF_8)); } fos.write(baos.toByteArray()); } }这段代码写的CSV没有对字段做双引号包裹,正常成绩数据全是数字和姓名不会出问题,但如果某个字段内容里含逗号,打开时会导致错列。稳妥做法是每个字段拼入CSV前先判断是否含逗号或引号,含的话用双引号包起来并把内部引号加倍。原始实现没做,是因为课设成绩数据一般不涉及标点符号,补上包裹逻辑也就是多一个if判断,想做得严谨的可以自己加。
6.3 一键打包:IntelliJ IDEA的Artifacts配置
程序写完后要交作业或答辩演示,不能每次都在IDE里点运行。打包成可执行jar是常规操作。IDEA里的配置路径是File -> Project Structure -> Artifacts -> 点加号 -> JAR -> From modules with dependencies,Main Class选择登录窗体所在的类。配置完成后Build -> Build Artifacts,会在out目录生成jar文件。双击运行jar若没反应,大概率是双击时用的javaw.exe把标准输出吞掉了,命令行用java -jar xxx.jar跑会看到具体报错。
打包时容易漏掉MySQL驱动jar包,IDEA的From modules with dependencies会把依赖一起打进去,但要是手写MANIFEST.MF就很容易忘。我踩过的一个坑是打包后运行报「No suitable driver found」,排查半天发现驱动jar没被包含进产物。验证jar包内容用jar tf命令看一眼有没有com/mysql/cj/jdbc/Driver.class的路径,没有就重新检查Artifacts配置的依赖范围。
带界面的jar包还有一个体验优化点:把启动命令写成一个start.bat批处理,内容两行——第一行设置编码chcp 65001,第二行java -jar edu-admin.jar。这样用户双击bat文件启动时中文不会乱码,不用手动打开命令行。交付物一般是jar加bat加一个说明文档,说明文档里写明JDK版本要求(Swing程序建议JDK 8以上但不要用JDK 16+,因为JDK 9模块化后Swing某些行为有调整)和MySQL初始化步骤。
从Swing学到真正能迁移到Web项目的能力,是DAO层的PreparedStatement写法、事务边界意识和JDBC异常处理这套东西。Swing本身会被淘汰,但这些底层的数据库交互模式是Java Web开发的起点。做这个系统的过程中如果遇到「界面加载慢」「数据不对」「连接失败」这类问题,排错路径基本固定:先看MySQL服务有没有启动,再看驱动类名和URL参数,接着看SQL在Navicat里能不能跑通,最后才是Swing事件逻辑的问题。这套排查顺序是我做过几个课设项目后才总结出来的血泪经验,希望能帮你在做这个方向时少走两步弯路。
本文还有配套的精品资源,点击获取