简介:面向Java初学者、高校软件课程设计学生及需要小型信息管理系统原型的开发者,这份学生信息管理系统项目围绕学生信息的增删改查展开,并涉及登录、成绩维护、查询统计与管理员操作等常见业务,适合用作课程设计参考、答辩演示或二次开发基础。压缩包共54个文件,以22个java源码和29个class编译文件为主,辅以Eclipse工程配置,整体约51KB;源码对应功能实现,class便于直接运行验证,工程文件可帮助快速导入调试。已有560人学习下载,说明其在同类课程作业中具有一定参考价值。读者可重点观察Swing界面布局、按钮事件监听、集合存储、JDBC工具封装、异常处理及基础设计模式的组织方式,理解从界面交互到数据库访问的完整链路,并据此调整字段、界面和业务逻辑。
1. 从一份课程设计源码说起:它到底解决了谁的什么问题
期末周最典型的一幕,是某同学从群里拖下来一个压缩包,文件名写着 java 学生信息管理系统课程设计,解压、双击、报错、再双击、再报错,最后卡在ClassNotFoundException上干瞪眼。这份源码的本质是一套带图形界面的增删改查系统:登录鉴权、学生档案录入、按学号姓名检索、分页浏览、数据落库,业务闭环完整、表结构干净。它适合两类人:一是赶答辩、需要一份能当场演示的课设骨架;二是刚学完 JDBC、想看看分层到底怎么落地的新手。别指望它能扛生产流量,但把"界面—业务—数据"这条链路讲明白,它比看十篇教程都直接。
2. 技术栈与目录结构:一个 Swing + JDBC 课设项目的完整骨架
2.1 为什么课程设计偏爱 Swing + JDBC 这套组合
拿到源码第一件事不是运行,而是看它用了什么。这个项目走的是 Java SE 路线:Swing 做桌面界面,JDBC 直连 MySQL,中间夹一层手写的 DAO。同一门课还有另一种常见写法是 Servlet + JSP + Tomcat 的 Web 版,两者业务逻辑几乎一样,区别只在展示层。我一般先确认这一点,因为它决定了你要装什么、配什么、怎么调。
选 Swing 的理由很实际。它不需要额外容器,javac编译完就能java -jar跑;不需要配 web.xml,不需要理解请求转发和会话;界面拖拽用 IDE 的可视化设计器十分钟能出一个能看的窗体。对课设这种"两周做完、三分钟演示"的场景,容错率最高的就是它。代价也明显:布局写死在代码里,换个分辨率就错位;界面和业务容易糊在一个类里,改一处牵全身。
JDBC 这边同样是"够用就好"的思路。常见做法是不上连接池、不引 ORM,直接DriverManager.getConnection()拿连接,用完close()。这在课设规模下不会出问题,因为并发用户就是你自己。但如果打算继续往下做,连接池和 DAO 抽象这两件事迟早要补,第 6 章会具体说。
真正值得看的是分层。一个合格的课设骨架应该长这样:实体类只装字段,DAO 只写 SQL,Service 做校验和事务,UI 只负责取值和刷新。源码里这四层是不是分清楚了,直接决定你后面能不能改得动。我见过太多把SELECT语句直接写进按钮点击事件里的版本,加个字段要改五个文件,血泪经验。
2.2 目录结构逐个文件拆开看
解压之后先别急着导入 IDE,用文件管理器把src展开看一遍,五分钟能省两小时。典型结构如下:
| 路径 | 职责 | 改动频率 |
|---|---|---|
src/main/java/com/demo/sms/entity | 实体类,字段与表列一一对应 | 低 |
src/main/java/com/demo/sms/dao | 接口 + 实现,所有 SQL 集中在此 | 中 |
src/main/java/com/demo/sms/service | 业务校验、事务边界 | 中 |
src/main/java/com/demo/sms/ui | JFrame / JPanel / 事件监听 | 高 |
src/main/java/com/demo/sms/util | 连接工具、日期工具、加密工具 | 低 |
lib/ | mysql-connector-java-8.x.jar等第三方包 | 一次性 |
sql/init.sql | 建库建表 + 初始管理员账号 | 一次性 |
README.md | 运行说明、默认账号密码 | 一次性 |
看到lib目录下有 jar 就直接说明这是个非 Maven 工程,依赖全靠手工挂。这种情况 IDEA 里要右键 jar 选Add as Library,Eclipse 里要进Build Path → Add External JARs,命令行编译还得写-cp。别嫌麻烦,这一步没做对,后面所有报错都源自这里。
entity包里的类基本是纯 POJO:私有字段、getter/setter、可能有个toString()。检查一下字段类型——birthday应该是java.util.Date或LocalDate,如果写成String,说明作者图省事,后面按日期排序或算年龄时会踩坑。dao包里应该只有接口和实现类,如果你在里面看到JOptionPane.showMessageDialog,那分层就是假的,业务和界面已经粘在一起了。
2.3 数据库表设计与连接配置
表结构是整个项目的地基,先看清楚再动代码。核心两张表:学生表和用户表。
-- sql/init.sql 核心部分 CREATE DATABASE IF NOT EXISTS sms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE sms; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号,业务主键', name VARCHAR(50) NOT NULL, gender CHAR(1) DEFAULT '男' COMMENT '男/女', birthday DATE NULL, class_name VARCHAR(50) NULL, major VARCHAR(50) NULL, phone VARCHAR(20) NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 'SHA-256 十六进制', role VARCHAR(20) DEFAULT 'admin' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 初始账号 admin / 123456(密码需按 SHA-256 存,见 3.1) INSERT INTO sys_user(username, password, role) VALUES ('admin', '8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92', 'admin');三个细节决定你后面会不会翻车。第一,id用自增代理主键、sno加唯一约束,这是标准做法;如果源码把sno直接当主键,改学号就等于删了重插,外键一多就演不下去。第二,DEFAULT CHARSET=utf8mb4不能省,我见过建库时用了latin1,姓名里带个生僻字就变成问号,答辩时特别尴尬。第三,密码列给了 64 位长度,正好放 SHA-256 的十六进制串,说明作者至少做了基本加密而不是明文存库。
连接配置通常藏在DBUtil里,写死 IP、端口、库名、账号密码:
package com.demo.sms.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public final class DBUtil { // serverTimezone 不能省,MySQL 8 驱动不写会直接抛 SQLException private static final String URL = "jdbc:mysql://127.0.0.1:3306/sms" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PWD = "123456"; static { try { // JDBC 4.0 之后理论上可省略,但老驱动和老 IDE 组合下显式加载最稳 Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL 驱动不在 classpath:" + e.getMessage()); } } private DBUtil() { } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } }参数逐个说清。serverTimezone=Asia/Shanghai是 MySQL 8 驱动的硬要求,不写会报The server time zone value ... is unrecognized,这是新手第一大坑。useSSL=false是本地连接免掉证书握手,不然控制台一片红色警告。allowPublicKeyRetrieval=true配合 MySQL 8 默认的caching_sha2_password认证插件,缺了它连不上。characterEncoding=utf8决定中文能不能正确进库。
这里的USER和PWD建议你在自己机器上跑通之后再决定要不要外置到db.properties。课设演示无所谓,但如果这份代码要放出去或者继续迭代,硬编码的密码迟早是个麻烦,配置文件 +.gitignore是最低成本的补救。
3. 核心链路拆解:登录、增删改查到底怎么串起来的
3.1 登录模块:从输入框到 SQL 参数
登录是整条链路里最短但最容易出安全问题的一段。界面层拿到两个文本框的值,交给 Service,Service 调 DAO 查库,比对成功就dispose()掉登录窗、new MainFrame().setVisible(true)。逻辑不复杂,问题在于很多课设版本用字符串拼接 SQL:
// 反面教材:能跑,但输入框里敲 ' or '1'='1 就能进 String sql = "SELECT * FROM sys_user WHERE username='" + user + "' AND password='" + pwd + "'";正确写法是PreparedStatement占位符,参数通过setString塞进去,数据库驱动负责转义。同时密码要比对哈希而不是明文:
public User login(String username, String rawPwd) { String sql = "SELECT id, username, password, role FROM sys_user WHERE username = ?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { return null; // 用户不存在,统一返回 null,不告诉调用方差在哪 } String stored = rs.getString("password"); // 库里存 SHA-256 十六进制,输入框给的是明文,先运算再忽略大小写比对 if (!stored.equalsIgnoreCase(SHA256.hex(rawPwd))) { return null; } User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setRole(rs.getString("role")); return u; } } catch (SQLException e) { throw new DaoException("登录查询失败, username=" + username, e); } }逻辑上有三处值得说。try-with-resources保证Connection、PreparedStatement、ResultSet三层资源按逆序关闭,不用手写一大坨finally,这是 JDBC 代码整洁度的分水岭。用户不存在和密码错误都返回null,是为了不给爆破者提供"这个账号存在"的额外信息,课上不考,但习惯要养。DaoException是自定义运行时异常,把SQLException包一层往上抛,好处是界面层只处理一种异常类型,不用在每个按钮里catch (SQLException)。
SHA256.hex()的实现十几行,用MessageDigest.getInstance("SHA-256"),把byte[]转成十六进制小写串。注意MessageDigest实例不是线程安全的,每次都getInstance或者用ThreadLocal,课设单线程无所谓,但别把它写成静态成员到处复用。
界面层这边,事件监听里该做的事只有三件:取值、调 Service、根据结果决定是否跳转。校验(用户名非空、长度限制)放在 Service,弹窗提示放在界面,两者不要互串。我见过把SELECT写在按钮监听里的版本,加个"记住密码"功能得改三处,后悔药都来不及吃。
3.2 增删改:DAO 层怎么写才不返工
增删改三个操作的代码骨架高度相似,所谓"不返工"就是别每个方法都从头抄一遍连接管理。标准套路是每个方法一个PreparedStatement,参数按占位符顺序set,返回值是受影响行数或者布尔。
public boolean update(Student s) { String sql = "UPDATE student SET sno=?, name=?, gender=?, birthday=?, " + "class_name=?, major=?, phone=? WHERE id=?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getSno()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); // setDate 需要 java.sql.Date,实体里是 java.util.Date 时要显式转换 ps.setDate(4, s.getBirthday() == null ? null : new java.sql.Date(s.getBirthday().getTime())); ps.setString(5, s.getClassName()); ps.setString(6, s.getMajor()); ps.setString(7, s.getPhone()); ps.setInt(8, s.getId()); // 注意 WHERE 用的是代理主键 id,不是 sno return ps.executeUpdate() == 1; } catch (SQLException e) { throw new DaoException("更新学生失败, id=" + s.getId(), e); } }两个参数层面的坑。java.util.Date和java.sql.Date是父子关系但不是同一个东西,直接ps.setDate(4, s.getBirthday())编译不过,必须显式转换,转换后时分秒会丢失——对生日字段无所谓,对"创建时间"字段就致命,那种场景要用java.sql.Timestamp。WHERE id=?用代理主键而不是学号,是因为学号本身可被用户修改,用可变的业务字段定位更新目标,一旦学号被改过就更新错行。
新增方法把UPDATE换成INSERT INTO student(sno, name, ...) VALUES(?,?,...),参数少一个id,返回executeUpdate() == 1。删除方法更简单,DELETE FROM student WHERE id=?。真正需要额外处理的是新增时的唯一约束冲突:学号重复会抛SQLIntegrityConstraintViolationException,getErrorCode()返回 1062。别让它冒到界面上变成一坨英文堆栈。
} catch (SQLIntegrityConstraintViolationException e) { if (e.getErrorCode() == 1062) { throw new BizException("学号 " + s.getSno() + " 已存在,请换一个"); } throw new DaoException("新增学生失败", e); }BizException是给用户看的,消息必须是中文人话;DaoException是给开发看的,里面包原始SQLException。界面层catch (BizException e)就JOptionPane.showMessageDialog(this, e.getMessage()),catch (DaoException e)就弹"系统繁忙"再写日志。这个分法看着小题大做,但你改到第四五个功能时会感谢自己。
还有一点:批量导入(比如从 Excel 一次进 200 条)不要循环调单条insert,那是 200 次网络往返。用addBatch()+executeBatch(),并且把conn.setAutoCommit(false),全部成功再commit(),中途出错rollback()。这是课设里少数几个能明显讲出"工程感"的地方,答辩时提一句很加分。
3.3 表格模型与模糊查询:把 ResultSet 搬进 JTable
界面上那块列表用的是JTable,它的数据来源不是List而是TableModel。很多课设版本图省事用new JTable(Object[][], String[])直接塞二维数组,代价是数据一变就得重新setModel,滚动位置和选中行全丢。正确做法是继承AbstractTableModel:
public class StudentTableModel extends AbstractTableModel { private final String[] cols = {"学号", "姓名", "性别", "出生日期", "班级", "专业", "电话"}; private List<Student> data = new ArrayList<>(); public void setData(List<Student> list) { this.data = list; // 少了这一行,数据换了但界面还是老样子,是查询按钮"没反应"的头号原因 fireTableDataChanged(); } public Student getRow(int rowIndex) { return data.get(rowIndex); // 界面选中行 → 实体,供编辑/删除使用 } @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return cols.length; } @Override public String getColumnName(int c) { return cols[c]; } @Override public Object getValueAt(int r, int c) { Student s = data.get(r); switch (c) { case 0: return s.getSno(); case 1: return s.getName(); case 2: return s.getGender(); case 3: return s.getBirthday() == null ? "" : s.getBirthday().toString(); case 4: return s.getClassName(); case 5: return s.getMajor(); case 6: return s.getPhone(); default: return ""; } } }fireTableDataChanged()这一行是整个表格刷新的开关。查询按钮点下去数据变了但界面纹丝不动,九成是忘了调它,或者调了fireTableRowsInserted却传错了区间。getValueAt里对birthday做空判断是必须的,数据库允许 NULL,toString()一个 null 会直接NullPointerException,而且是在渲染线程里抛,堆栈看着特别莫名其妙。
模糊查询和分页通常合在一个方法里:
public List<Student> findByPage(String keyword, int page, int size) { // 注意 LIMIT 两个参数是 offset 和 count,不是页码和每页条数 String sql = "SELECT * FROM student " + "WHERE name LIKE ? OR sno LIKE ? " + "ORDER BY id DESC LIMIT ?, ?"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + (keyword == null ? "" : keyword.trim()) + "%"; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, (page - 1) * size); // offset ps.setInt(4, size); // count try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } catch (SQLException e) { throw new DaoException("分页查询失败, keyword=" + keyword, e); } return list; } private Student mapRow(ResultSet rs) throws SQLException { Student s = new Student(); s.setId(rs.getInt("id")); s.setSno(rs.getString("sno")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setBirthday(rs.getDate("birthday")); // 返回 java.sql.Date,是 util.Date 的子类 s.setClassName(rs.getString("class_name")); s.setMajor(rs.getString("major")); s.setPhone(rs.getString("phone")); return s; }LIMIT ?, ?的参数顺序是 offset 在前、count 在后,写成LIMIT page, size就是典型翻车点:第 1 页看着正常(offset 0),第 2 页直接跳到第 11 条。mapRow抽成私有方法是为了让"查询列表"和"按 id 查单条"共用同一套映射,字段加一个只改一处。%keyword%的前缀通配符会让索引失效,数据量上万后明显变慢,课设数据量下感觉不到,真要优化得改成前缀匹配或者上全文索引。
4. 跑起来:环境准备、建库、导入与第一次运行
4.1 JDK、MySQL、驱动包三件套
跑通这份代码需要三样东西,版本对不上就是无穷无尽的报错。JDK 用 8 或 11 都行,源码里如果有var或者switch表达式就必须 11 以上,看pom.xml或源码语法判断。MySQL 建议 8.0,和驱动版本配套。
驱动包的版本对应关系是新手最容易忽略的:
| MySQL 服务端 | 推荐驱动 | 驱动类名 | 注意事项 |
|---|---|---|---|
| 5.7 | 5.1.x | com.mysql.jdbc.Driver | 老版本,serverTimezone可不写 |
| 8.0 | 8.0.x | com.mysql.cj.jdbc.Driver | 必须写serverTimezone |
驱动类和版本错配的典型表现是ClassNotFoundException或No suitable driver found。检查方法很直接:解压那个 jar,看META-INF/MANIFEST.MF里的Version,或者直接把包名点开看有没有cj这一层。
导入 IDE 的步骤分两条路。用 IDEA 的话,File → Open选源码根目录,等索引建完,右键lib下的 jar 选Add as Library,然后在Project Structure → Modules → Dependencies里确认作用域是 Compile。用 Eclipse 的话,File → Import → Existing Projects into Workspace,右键项目Build Path → Configure Build Path → Libraries → Add JARs。命令行党直接:
# 编译,注意 Windows 下分隔符是 ; Linux/macOS 是 : javac -encoding UTF-8 -cp "lib/*" -d out $(find src -name "*.java") # 运行,主类名按源码实际包路径改 java -cp "out:lib/*" com.demo.sms.ui.LoginFrame-encoding UTF-8必须加,不加的话源码里的中文注释和字符串常量会在编译期就变乱码,运行时弹窗全是问号。
4.2 建库建表与初始数据
打开命令行或任意客户端,把sql/init.sql整个执行一遍。执行完确认三件事:库sms存在、两张表结构正确、sys_user里有一行 admin。
mysql -u root -p < sql/init.sql-- 验证脚本 SHOW DATABASES LIKE 'sms'; USE sms; DESC student; SELECT id, username, role FROM sys_user;如果init.sql里的建库语句写了IF NOT EXISTS而表没有,说明库里已经有同名表但结构不对,先DROP TABLE再重建。这一步偷懒的后果是运行时报Unknown column 'class_name',然后你花半小时翻代码找问题,其实是表结构停留在旧版本。我一般会在init.sql开头加上DROP TABLE IF EXISTS,反复重建不心疼。
字符集也要落实到位。库、表、连接串三处的字符集必须一致,任何一处是latin1或gbk,中文就可能在某一段链路上坏掉。执行SHOW VARIABLES LIKE 'character_set%'看一眼服务端默认值,如果character_set_server是latin1,那建库时不显式指定utf8mb4就会继承成拉丁字符集。
4.3 从 IDE 到命令行:两种启动方式与失败定位
IDE 里直接右键主类Run,控制台没异常、登录窗弹出来,就算成功。如果窗口一闪而过,说明main里抛了异常但没打印,检查是不是在static块里加载驱动失败被吞了。
命令行方式的命令在 4.1 已经给过,补一个打包成可执行 jar 的写法,方便拷给别人:
# 清单文件指定主类,注意 Main-Class 后面留一个空行 cat > manifest.txt <<'EOF' Main-Class: com.demo.sms.ui.LoginFrame Class-Path: lib/mysql-connector-java-8.0.33.jar EOF jar cfm sms.jar manifest.txt -C out . java -jar sms.jarClass-Path里的路径是相对 jar 文件位置的,jar 和lib目录必须放在一起,单独把 jar 拷到别的机器上会找不到驱动。这是发给同学演示时最常见的"我这儿能跑你那儿不能跑"。
启动失败的定位顺序我固定成三步:先看控制台第一行异常是什么类(ClassNotFoundException找驱动,SQLException找连接和库,NullPointerException找空字段),再看异常里的中文消息(好的自定义异常会告诉你哪个参数错了),最后才去打断点。反过来做,往往在一个根本不相关的方法里耗掉一下午。
5. 排查清单:课设跑不起来时先看这几处
5.1 连接与环境类踩坑:三条高频记录
现象:启动即报The server time zone value '?й???????' is unrecognized。原因:MySQL 8 服务端时区名是中文乱码形式,驱动无法解析,本质是连接串缺serverTimezone。 解决:在 URL 后追加&serverTimezone=Asia/Shanghai,重启应用。如果仍然报错,改用serverTimezone=UTC先跑通,再由业务层处理时区换算。
现象:登录时提示账号密码错误,但库里的账号密码确认无误。原因:密码列存的是 SHA-256 十六进制串,界面输入的明文没有经过哈希就做了比对;或者用了 SHA-256 但库里存的还是旧版明文。 解决:先在命令行算一次哈希echo -n "123456" | sha256sum,和库里那行对照。不一致就UPDATE sys_user SET password='...' WHERE username='admin'补上,不要改代码去迁就脏数据。
现象:No suitable driver found for jdbc:mysql://...,或者ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因:驱动 jar 没进 classpath;或者 jar 是 5.1 版本但代码里写的是带cj的 8.x 驱动类名。 解决:先确认lib下的 jar 已被 IDE 识别为依赖,命令行走-cp "lib/*";再核对驱动类名与 jar 版本是否匹配,5.1 用com.mysql.jdbc.Driver,8.x 用com.mysql.cj.jdbc.Driver。
5.2 数据与界面类踩坑:两条容易耗时间的记录
现象:新增一条学生记录,中文姓名存进库里变成???或乱码方块。原因:链路中有一处字符集不是 UTF-8。可能是建库时没指定、连接串缺characterEncoding,也可能是javac编译时没加-encoding UTF-8导致字符串常量本身已经坏了。 解决:按"库 → 表 → 连接串 → 编译参数"四层依次核对。SHOW CREATE TABLE student看表字符集,SHOW VARIABLES LIKE 'character_set%'看服务端,连接串补useUnicode=true&characterEncoding=utf8,编译加-encoding UTF-8。改完必须重新建表并重编译,只改一侧不生效。
现象:点"查询"按钮没任何反应,表格数据不更新,但控制台没有报错。原因:DAO 查到了新数据,但TableModel的setData里漏了fireTableDataChanged();或者界面用的是new JTable(Object[][], ...)的静态二维数组,数据换了没重新setModel。 解决:确认setData内部有fireTableDataChanged();如果确实是二维数组写法,改成AbstractTableModel派生类,这活儿躲不掉。顺带检查查询按钮的监听器里有没有在try块外提前return。
6. 二次开发:从"能交作业"到"能拿得出手"的改造路径
答辩完这份代码通常还有两种命运:扔进硬盘吃灰,或者接着往上加东西。想加东西,先看清投入产出比。我给常见的五类改造按工作量排了个序:
| 改造点 | 关键改动 | 大致工作量 | 收益 |
|---|---|---|---|
| 导出 CSV | 新增util/CsvExporter,界面加一个按钮 | 半天 | 演示效果立竿见影 |
| 分页控件 | findByPage已就绪,补上一页/下一页与总条数 | 半天 | 数据量上去后必需 |
| 照片字段 | 表加photo存路径,实体加字段,界面加图片预览 | 一天 | 界面质感提升 |
| 连接池 | 引入 Druid/HikariCP 替换DriverManager | 一天 | 从玩具变工程 |
| 角色权限 | sys_user.role已在,按角色隐藏删除按钮 | 一天半 | 逻辑复杂度上一个台阶 |
优先级我的建议是导出 → 分页 → 连接池。导出最便宜,几条命令就能完成;分页 DAO 层已经写好,只差界面;连接池虽然最"工程化",但改完之后你才会真正理解close()为什么不该真的关连接。
导出 CSV 是我最常做的第一个改造,因为它把"系统里的数据"变成"能交出去的文件",价值最直观。实现要注意 BOM:
public static void exportCsv(List<Student> list, File file) throws IOException { // 头部写 BOM,否则 Excel 双击打开时中文列名全是乱码,这个坑几乎人人踩一次 try (BufferedWriter w = new BufferedWriter( new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) { w.write('\uFEFF'); w.write("学号,姓名,性别,出生日期,班级,专业,电话"); w.newLine(); for (Student s : list) { w.write(String.join(",", nvl(s.getSno()), nvl(s.getName()), nvl(s.getGender()), s.getBirthday() == null ? "" : s.getBirthday().toString(), nvl(s.getClassName()), nvl(s.getMajor()), nvl(s.getPhone()))); w.newLine(); } } } private static String nvl(String v) { return v == null ? "" : v.replace(",", ","); // 字段内逗号会撑破列,替换成全角 }逻辑上有两个点值得停一下。\uFEFF是 UTF-8 BOM,写在一开始,Excel 才会按 UTF-8 解析而不是按系统默认编码,不加这个前缀十有八九是乱码。字段里出现半角逗号会把一列拆成两列,替换成全角是最省事的解法,正规做法是加引号包裹并转义内部引号,课设场景下替换够用。导出用的是"当前页数据"还是"全部结果",取决于你传进去的list,如果用户期望导出全量,就得再调一次不带LIMIT的findAll,这一点提前和演示对象对齐好,别现场被问懵。
验证改造是否真的成功,我的习惯是每次都走三遍:正常路径跑一遍、边界数据(空姓名、超长学号、生日为空)跑一遍、异常路径(库断了、学号重复)跑一遍。这三遍走完才敢说改完了。从那以后我每次动 DAO 层都强制自己先跑一遍空表和一万条数据两个极端,因为课设代码最常见的毛病就是"能吃一条、吃不了量"。
希望帮到你。
本文还有配套的精品资源,点击获取