简介:这是一套基于Java GUI技术实现的学生信息管理桌面应用,面向Java初学者与课程设计学习者,解决教育场景下学生数据录入、查询、编辑、统计等核心管理需求。资源包共38个文件,含22个编译后的class文件、5个核心java源码(涵盖GUI界面、数据库交互与业务逻辑)、4个XML配置文件(用于界面布局或数据映射)、2份Markdown说明文档(中英文README)及辅助资源,整体大小10.95MB,结构清晰体现MVC分层设计思想。已有2869人学习下载,适合通过完整可运行项目掌握Swing组件使用、JDBC数据库连接、事件驱动编程及基础权限控制实践。读者可直接导入IDE运行,深入理解登录模块、JTable动态数据绑定、学生信息增删改查全流程及CSV导出功能的代码实现细节。 如果让我给刚学完Java基础的人推荐一个练手项目,我大概率会先问一句:你有没有在Swing里亲手画出一个能增删改查的界面?如果没有,那这个"JAVA-GUI学生管理系统"就非常值得你认真做一遍。原因很简单:它没有高深算法,没有复杂的并发模型,却完整覆盖了从Java基础语法到面向对象设计、再到GUI事件驱动和数据持久化的整条链路。更重要的是,这个项目在面试场合几乎人手一个,但真正能把分层结构讲清楚、把数据访问层写干净、把界面响应逻辑说明白的人并不多。你只要在其中做得比大多数人细一点,就能拉开差距。
很多初学者会把学生管理系统当成一个"课后作业"来糊弄:一个类里塞满一切,界面直接用绝对定位拖控件,点击按钮后在事件监听里现查数据库再往表格里填。跑起来没问题,但一旦要加功能,代码就成了一团乱麻。这篇文章不会只给你一份能抄的代码,我会从项目管理者的视角,把选型、分层、布局、踩坑和面试包装讲透,适合正在做课程设计、准备实习面试,或者想系统整理Java知识的人参考。
1. 为什么学生管理系统是Java初学者最该复刻的项目
这个项目看起来被做烂了,实际上它背后的技术密度被严重低估。一个完整的学生管理系统,表面上是"学生信息的增加、删除、修改、查询",但拆开来看,它逼着你处理几个绕不开的工程问题:对象怎么建模、界面怎么和逻辑解耦、数据从哪里来、错误怎么兜底。这些问题在真实企业项目里天天遇到,只是新人通常没有机会接触,于是学生管理系统成了低成本模拟这些问题的理想沙盒。
1.1 从语法到工程,它补上了"中间地带"
你在教程里学的Java大多是零散的:封装、继承、多态、集合、异常、IO、线程,每个概念都有独立案例。学生管理系统则是第一次强制你把它们组合在一起。比如Student类要用封装保护字段,用构造器简化对象创建;StudentDao接口要体现面向接口编程;用ArrayList保存内存数据时,新增和删除的背后是集合的add和remove;用MySQL存储时又变成JDBC的PreparedStatement——同一个业务,两种存储实现,这本身就是对"面向对象"的实战理解。
更关键的是事件驱动。在控制台程序里,程序主动打印菜单然后等用户输入;到了GUI里,逻辑完全反过来了——用户不知道什么时候点按钮、改表格、输内容,程序必须随时准备好响应。这个心智模型的转变,是很多新手第一次意识到"代码不是从上往下执行的"。
1.2 规模不大,但完整覆盖"写项目"的必要环节
一个像样的学生管理系统包含五块必要内容:登录与权限、学生信息增删改查、搜索筛选、数据持久化、列表展示。五块内容单拆都不难,合在一起就会出现真实的取舍问题。例如登录时密码要不要加密?搜索时用的是全匹配还是模糊匹配?删除之前要不要二次确认?这些细节没有标准答案,但你得自己做决定,并给出理由,这就是工程能力。
我不会把这个项目写得非常大,刻意引入Spring Boot或者MyBatis去"升级"它。作为GUI练手项目,Swing加JDBC足够撑起核心价值,等这一套跑通,你再学Web框架时会有一种熟悉感:Controller处理请求,Service处理业务,Mapper处理数据,跟这里的ActionListener、StudentService、StudentDao如出一辙。实际上很多同学学完SSM觉得抽象,回头补了GUI项目才真正开窍,就是因为GUI项目把"界面、业务、数据"的边界画得非常清楚。
提示:如果时间紧张,我建议优先保证"增删改查完整、代码分层清晰、关键注释到位",界面丑一点没关系,逻辑烂才是硬伤。
2. 技术选型与项目骨架搭建:Swing、Maven与包结构
既然标题写了JAVA和GUI,第一个要定下来的就是GUI框架。目前纯Java桌面端可选的方案主要是Swing和JavaFX,两者都能完成学生管理系统,但侧重点不同。我见过很多教程默认用Swing,也有人用JavaFX拿CSS写界面,其实选哪个不丢人,丢人的是选完以后说不清为什么。
2.1 Swing还是JavaFX,我为什么推荐Swing
Swing的优点是JDK自带,不需要额外下载运行时,文档和中文资料极多,几乎任何环境变量配置好JDK就能跑。它虽然长得旧,但胜在稳定,组件体系非常完整,JTable、JTree、JTabbedPane这些高级组件足以支撑一个管理系统。JavaFX则更现代,支持FXML和CSS美化,视觉效果更干净,但需要额外引入依赖,打包和部署也比Swing麻烦一点。
对学生管理系统这种以表格和表单为主的应用,Swing是一个更务实的选择。我的建议是:第一遍做,用Swing,把精力放在逻辑本身;如果后面想折腾视觉,可以换JavaFX重写界面,业务层代码几乎不用动——这其实也是"分层"带来的好处。
技术栈我按下面这套组合来定,几乎零成本:
| 模块 | 选择 | 理由 |
|---|---|---|
| JDK | 8或11 | 兼容性最好,8是Swing时代的经典版本 |
| GUI | Swing | JDK内置,资料充足,学习成本低 |
| 构建工具 | Maven | 方便管理依赖,打可执行Jar更省事 |
| 数据库 | MySQL 5.7/8.0 | 课程设计和面试最常见的选择 |
| 数据访问 | JDBC + MySQL Connector/J | 不使用框架,便于理解底层原理 |
| 连接池 | HikariCP | 小而快,用一点生产味十足的组件 |
2.2 Maven项目结构与包命名
如果你现在还在用Eclipse里那种"鼠标右键新建Class,一路堆到default package"的方式,我建议这次直接改成Maven工程。命令很简单,也可以在IDEA里新建Maven项目,groupId写com.example,artifactId写student-manager。建完以后,包结构一定要按照分层来,我常用的分包方式是:
com.example.student ├── MainApp.java // 程序入口 ├── view/ // GUI相关:主窗口、登录窗口、自定义面板 ├── controller/ // 事件监听与页面跳转 ├── service/ // 业务逻辑层 ├── dao/ // 数据访问层 │ ├── StudentDao.java // 接口 │ └── impl/ // DAO实现 ├── entity/ // 实体类:Student、User ├── util/ // 工具类:DBUtil、Md5Util、RegexUtil └── config/ // 配置类:读取数据库配置这样一个结构最大的好處是,view层只负责接收用户输入和展示结果,controller只做转发和校验,service层处理业务规则(比如年龄合法性、学号是否重复),dao层负责增删改查SQL,entity层就是数据载体。谁改谁,谁依赖谁,一眼就能看出来。
2.3 数据库表的简单设计
学生管理系统的数据库不用整得太复杂,但两张表不能少:学生表和用户表。学生表存学号、姓名、性别、年龄、所在学院、班级、联系方式、入学日期、备注;用户表存用户名和密码,用于登录。这里有几个字段设计上的小提醒:学号不要设成自增主键,因为真实场景里学号是业务主键,由学校统一编码;手机号这种字段建议用VARCHAR而不是BIGINT,避免前端展示时丢掉前导零;所有时间字段统一使用DATE或DATETIME类型,不要图省事存字符串。
为了演示方便,我会先给出一版基于内存集合的DAO实现,后面再换成MySQL实现。很多教程一上来直接教连数据库,结果学生被环境配置劝退,最后代码也没写完。虽然本文主要内容是完整架构,但如果你想直接看数据库版本,我建议按顺序来:先跑通集合版,再替换JDBC版,这样既能理解DAO的好处,也会让你对"换实现类"这件事有直观感受。
3. 实体类与数据访问层:先从interface开始写
数据访问层是整个系统的地基。很多人上来就直接在某一个类里写JDBC,然后把Connection传给界面层到处用,这是非常糟糕的。因为一旦数据库连接方式变了,或者想从MySQL换成文件存储,你就要改所有界面代码。更合理的做法是:先写Student实体,再写StudentDao接口,然后分别提供内存版和JDBC版实现。业务层依赖接口,界面层调用业务层,这样每一层都能独立替换。
3.1 Student实体类:别把属性全写成public
实体类的设计考量点主要在两个地方:属性和构造器。属性建议全部私有,通过getter和setter访问,这是JavaBean规范,也是Swing里JTable的TableModel能拿到值的前提。构造器我通常会写两个:一个无参构造器,供框架反射和后续扩展用;一个带核心字段的全参构造器,方便代码里new对象。
public class Student { private String studentNo; // 学号 private String name; // 姓名 private String gender; // 性别 private int age; // 年龄 private String college; // 学院 private String clazz; // 班级 private String phone; // 手机号 private LocalDate enrollDate; // 入学日期 private String remark; // 备注 public Student() { } public Student(String studentNo, String name, String gender, int age, String college, String clazz, String phone, LocalDate enrollDate, String remark) { this.studentNo = studentNo; this.name = name; this.gender = gender; this.age = age; this.college = college; this.clazz = clazz; this.phone = phone; this.enrollDate = enrollDate; this.remark = remark; } // getter / setter 省略 }这里有个容易被忽略的点:如果后续要操作很多学生列表,需要重写equals和hashCode吗?我的建议是,仅当你要用List.contains或Set去重时才有必要重写,以学号作为唯一标识。不重写也不会影响增删改查,因为我们在内存版里是按学号匹配实现的。
3.2 DAO接口:把"数据从哪里来"隐藏起来
StudentDao接口一般定义这些方法:findAll、findByKeyword、findById、insert、update、deleteById、countTotal。findByKeyword支持模糊搜索,参数可以是学号或姓名关键字。下面是一个典型的接口定义:
public interface StudentDao { List<Student> findAll(); List<Student> findByKeyword(String keyword); Student findById(String studentNo); boolean insert(Student student); boolean update(Student student); boolean deleteById(String studentNo); }你要注意"是否返回boolean"这个设计决定。真实的DAO层经常返回int表示受影响行数,比如insert返回0表示没有插入成功,返回1表示成功。这样写的好处是让上层能区分"SQL执行了但影响0行"和"SQL异常"两种不同情况,更精细。不过对学生管理系统这种简单业务,boolean也够用,简单直接。我代码里用的是boolean,但如果你想显得更专业,可以改为int。
3.3 内存版DAO实现:没有数据库也能跑
内存版用HashMap来模拟数据库,key是学号,value是Student对象。好处是编写简单、运行无外部依赖,还能让初学者专注于DAO接口的语义。对课程设计答辩来说,先展示这个版本,再展示MySQL版本,本身就是个"从易到难"的演讲逻辑。
public class StudentDaoMemoryImpl implements StudentDao { private final Map<String, Student> studentMap = new LinkedHashMap<>(); @Override public List<Student> findAll() { return new ArrayList<>(studentMap.values()); } @Override public Student findById(String studentNo) { return studentMap.get(studentNo); } @Override public boolean insert(Student student) { if (student == null || studentMap.containsKey(student.getStudentNo())) { return false; } studentMap.put(student.getStudentNo(), student); return true; } @Override public boolean update(Student student) { if (student == null || !studentMap.containsKey(student.getStudentNo())) { return false; } studentMap.put(student.getStudentNo(), student); return true; } @Override public boolean deleteById(String studentNo) { return studentMap.remove(studentNo) != null; } }内存版一定要加"是否重复"的判断,不然插入多条相同学号数据,后续更新和删除都会出乱子。LinkedHashMap可以让遍历顺序保持插入顺序,这与表格展示顺序一致,体验更好。
3.4 数据库连接工具与JDBC实现
切换到MySQL版时,第一件事是准备一个DBUtil工具类,它是获取连接和释放资源的统一入口。这里必须使用PreparedStatement,而不是拼字符串的Statement。用PreparedStatement一方面可以防止SQL注入,另一方面在参数较多时代码也更好读。我见过有人写:
String sql = "SELECT * FROM student WHERE student_no = '" + keyword + "'";这种写法在学生管理系统里不会马上出问题,但一旦学号里出现单引号,SQL就会报错或者被恶意构造,属于典型的教学反面案例。你既然做的是完整项目,从一开始就不要养成这个习惯。
HikariCP连接池的配置也不复杂,在pom.xml引入依赖后,用Properties配置driver、jdbcUrl、用户名、密码和最大连接数即可。DBUtil返回DataSource,getConnection从连接池拿,用完再还回去:
public class DbUtil { private static final HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false"); config.setUsername("root"); config.setPassword("your_password"); config.setMaximumPoolSize(10); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }JDBC版DAO的写法要注意几个点:查询结果集映射时,数据库字段名用下划线,实体属性用驼峰,手动映射时别写错;插入更新操作要开启事务吗?单条SQL不需要,但如果新增学生后还要写日志,就要用事务包裹;finally里释放ResultSet、Statement、Connection的顺序要反过来,先释放结果集,再释放语句,最后释放连接。用try-with-resources可以让代码更简洁,而且能自动关闭。
4. 主界面与增删改查:Swing布局和JTable的正确打开方式
GUI部分是最容易写烂的地方,因为Swing的布局管理器和事件监听天然让人想用"拖控件"来解决。但管理系统这种以数据展示为核心的界面,绝不能把界面元素用绝对坐标摆得零零散散。正确思路是:用BorderLayout做整体划分,中间放JTable滚动面板,顶部放搜索栏和工具栏,需要编辑时弹出一个模态对话框。这样结构清晰,也方便后续扩展。
4.1 主窗体布局:BorderLayout是管理系统的默认答案
主窗体JFrame的内容面板用BorderLayout,符合"上搜索、中列表、下状态栏"的经典结构。北部放一个JPanel,里面是搜索文本框和查询、重置、新增、修改、删除按钮;中部放一个JScrollPane,JScrollPane里面放JTable;南部放一个状态栏JLabel,实时显示总人数和当前过滤条数。这样分下来,代码结构从外观上就很好懂:
setLayout(new BorderLayout()); add(createTopPanel(), BorderLayout.NORTH); add(new JScrollPane(table), BorderLayout.CENTER); add(statusLabel, BorderLayout.SOUTH);新手常犯的错误是在一个JPanel里反复嵌套多个JPanel,最后代码层级极深。其实只要记住:一个容器只负责一种排列方式,复杂布局可以用多个简单容器嵌套,但每个容器的职责要单一。顶部搜索栏用FlowLayout就可以,简单好用。
4.2 JTable与TableModel:为什么不能直接往JTable塞数据
JTable的美妙之处在于它本身不保存数据,它依赖TableModel来提供数据。这个设计初看多绕了一层,但用久了你会感谢它:当你修改数据时,只需要让TableModel通知表格"数据变了",表格就会自动刷新,不需要手动重绘。整个过程类似"模型-视图"思想,和MVC一脉相承。
最省事的方式是继承AbstractTableModel实现自定义TableModel。你需要重写三个方法:getRowCount、getColumnCount、getValueAt;如果允许直接编辑表格,还要重写isCellEditable和setValueAt。我们通常不允许用户在表格里直接编辑,因为直接编辑无法做数据校验,容易把脏数据写进数据库。所以更安全的方式是:表格只负责展示,新增、修改都通过对话框完成,点确定后再刷新TableModel。
public class StudentTableModel extends AbstractTableModel { private final String[] columns = {"学号", "姓名", "性别", "年龄", "学院", "班级", "手机号", "入学日期", "备注"}; private List<Student> students; public StudentTableModel(List<Student> students) { this.students = students; } public void setStudents(List<Student> students) { this.students = students; fireTableDataChanged(); } public Student getStudentAt(int rowIndex) { return students.get(rowIndex); } @Override public int getRowCount() { return students == null ? 0 : students.size(); } @Override public int getColumnCount() { return columns.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Student s = students.get(rowIndex); switch (columnIndex) { case 0: return s.getStudentNo(); case 1: return s.getName(); case 2: return s.getGender(); case 3: return s.getAge(); case 4: return s.getCollege(); case 5: return s.getClazz(); case 6: return s.getPhone(); case 7: return s.getEnrollDate() == null ? "" : s.getEnrollDate().toString(); case 8: return s.getRemark(); default: return ""; } } }这里有个非常实用的经验:getValueAt里不要做复杂的计算,更不能在这里发起数据库查询。表格滚动时,这个方法会被频繁调用,任何耗时操作都可能导致界面卡顿。如果字段需要格式化,在构造数据时或者getValueAt里用简单的字符串处理就好。
删除操作要拿到选中的学生,应该用table.getSelectedRow()获取视图行号,再通过table.convertRowIndexToModel(row)转换成模型行号。如果你给JTable加了排序器或者过滤了数据,视图行号和模型行号可能不一致,不转换就会删错人。这是个很隐蔽的坑,很多老手在不注意时也会栽一次。
4.3 新增/修改对话框:校验放在哪里?
新增和修改用JDialog实现,模态对话框。简单业务不需要单独一个类,写一个静态方法showStudentDialog(Component parent, Student oldStudent)返回Student对象即可。这里重点是表单校验的时机和位置。
我的经验是:基础格式校验(必填、年龄范围、手机号格式)放在对话框的确定按钮里,业务规则校验(学号是否存在、能不能重复)放在Service层。为什么这样分?因为格式校验是界面级别的体验问题,弹窗提示用户改输入;而学号冲突属于业务规则,界面层不应该知道数据库里有什么。在dialog里确认按钮被点击时,先收集所有字段,做基本合法性校验,再调用service.insert或service.update,如果service层抛了"学号已存在"的异常,再用JOptionPane提示。
下面这段是手机号校验的一个简单工具方法:
public static boolean isValidPhone(String phone) { return phone != null && phone.matches("^1\\d{10}$"); }由于中国大陆手机号现在是11位且以1开头,这个正则已经够用。如果你做毕业设计,可以考虑更宽松检查:只要非空且不超过11位即可。
4.4 事件监听:别把所有逻辑塞进ActionListener
按钮监听很常见,但见得多不代表写得对。很多人会这样写:
addButton.addActionListener(e -> { // 打开对话框 // 校验 // 调DAO // 刷新表格 // 改状态栏 });一个ActionListener里塞五件事,确实也能跑,但维护起来很痛苦。我的建议是:监听器只负责"拿到用户操作,然后调用对应的Controller方法"。比如addButton的监听就是controller.addStudent();controller内部再调用service、刷新表格。这样监听器代码非常薄,后续调整业务逻辑时不用在界面层到处翻。
刷新表格时,我会先重新从service获取最新数据,再调用tableModel.setStudents(newList)。注意一定要触发fireTableDataChanged(),否则表格不会重绘。很多新手卡在这一步,以为数据没保存成功,其实是tableModel没通知视图更新。
5. 登录、搜索与异常处理:让系统有"生产环境"的味道
增删改查跑通以后,系统还缺一层"门面"。登录窗口是很多管理系统的标配,也是面试中比较好讲的功能点。它背后的价值不在登录本身,而在两个点:怎么安全地保存密码,以及怎么进行简单的会话控制。
5.1 密码安全:绝不存明文
学生管理系统里用户的登录密码,如果在数据库里直接存明文,答辩时被问到会非常尴尬。正确的做法是存摘要值,至少用MD5加盐。真实项目建议用BCrypt或PBKDF2,但由于Swing项目尽量保持简单,MD5加固定盐也能达到演示效果。示例:
public static String md5WithSalt(String rawPassword, String salt) { String toDigest = salt + rawPassword; // 使用MessageDigest.getInstance("MD5")计算摘要,转成十六进制 }这里要说明一个概念:MD5本身不安全,但加盐后至少能防止简单的彩虹表对比。你在答辩时只要说清楚"我用了加盐的MD5摘要存储",并且能够进一步指出"生产环境会用BCrypt这样的自适应哈希算法",这就能体现出你对密码存储的深度认知。
登录流程是:用户输入用户名和密码,Service层先查用户是否存在,再把输入的密码加同样的盐做MD5摘要,与数据库里存的值比对。如果一致才允许进入主界面,否则弹窗提示。你可以指定一个默认管理员账号,比如admin/admin123,方便演示。
5.2 分页与模糊搜索:不在大列表里卡死
如果学生表里有几千条数据,一次性加载到JTable虽然也能跑,但不是说一个管理系统就应该这么干。每页加载50条,点击下一页查询,是更贴近生产环境的做法。但分页会增加不少代码量,所以我给一个折中方案:首版可以全部加载,但要在DAO里预留findByPage方法;如果你想在简历里写"支持分页查询",那就要真的写完。
模糊搜索的核心SQL是:
SELECT * FROM student WHERE student_no LIKE ? OR name LIKE ? ORDER BY enroll_date DESC LIMIT ?, ?需要注意,LIKE匹配时参数里要拼上"%",PreparedStatement里不能直接把通配符和?写在一起,要这样:
String keyword = "%" + inputKeyword + "%"; ps.setString(1, keyword);这个细节经常被忽略,明明SQL没问题,结果查不到数据。
搜索按钮触发后,要通过service层调用dao.findByKeyword,再刷新TableModel。除此之外,我建议在JTextField上加一个键盘监听,让用户按回车也能触发查询,体验会好很多。
5.3 Swing线程模型:所有界面刷新都在EDT上做
Swing不是线程安全的,所有UI操作都必须在Event Dispatch Thread(EDT)上执行。这意味着,如果你的数据库查询很慢,不能直接在一个新线程里操作完再去更新JTable,而应该使用SwingWorker或者SwingUtilities.invokeLater把刷新动作切回EDT。
学生管理系统这种量级,不涉及耗时操作,但很多同学学了多线程以后,会在加载数据时加一个加载动画,或者模拟一个"启动时读取配置"的过程,这时候就要特别注意线程模型。否则偶尔会出现莫名其妙的界面闪烁、连接报错,以及极难排查的偶发NPE。
一个标准的SwingWorker用法是:
new SwingWorker<List<Student>, Void>() { @Override protected List<Student> doInBackground() { return studentService.findAll(); } @Override protected void done() { try { tableModel.setStudents(get()); } catch (Exception e) { e.printStackTrace(); } } }.execute();这里还有一个容易忽略的点:JFrame的setVisible(true)要放在EDT里调用,标准入口写法是:
SwingUtilities.invokeLater(() -> new LoginFrame().setVisible(true));为什么这个学生管理系统要强调这些?因为很多帖子写的GUI代码几乎全是线程不安全的,平时数据量小不出事,但一到压测或者查询变慢就会暴雷。你从一开始就按规范写,后面会省很多事。
5.4 全局异常兜底:避免红字堆栈直接甩给用户
真实项目中,不可能让用户看到一堆红色堆栈。Swing应用里在事件监听器内发生的异常,如果没被捕获,会直接打印到控制台,用户看到的就是对话框突然没反应。为了改善,我通常会在程序启动时设置一个全局异常处理器:
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { e.printStackTrace(); JOptionPane.showMessageDialog(null, "操作失败:" + e.getMessage(), "系统提示", JOptionPane.ERROR_MESSAGE); });然后在每个事件监听器的外层catch具体异常,并把能预判的错误转成友好提示。例如数据库连接失败,就给"数据库连接异常,请检查配置和网络";学号重复,就给"该学号已存在,请重新输入"。这个"把底层异常翻译成用户语言"的习惯,是最容易在面试里当作亮点讲的。
6. 踩坑复盘:编码、刷新、连接泄漏这些细节能气死人
这部分我想把自己真实踩过的坑和你分享。很多坑看起来不起眼,但调试起来特别浪费时间。我按项目推进的顺序排序,你如果在写的过程里遇到类似问题,可以直接对照排查。
6.1 控制台中文乱码与页面中文乱码不是一回事
先说控制台乱码。IDEA里运行后输出中文变成乱码,一般是因为项目编译编码与IDEA控制台编码不一致。解决办法很简单,在pom.xml里设置UTF-8编译,在IDEA的VM options里加-Dfile.encoding=UTF-8,再检查设置里的File Encoding全部改成UTF-8。
但数据库里的中文乱码是另一个层面:表结构默认是latin1或数据库连接串没带characterEncoding=utf8。连接串里必须带着useUnicode=true&characterEncoding=utf8,建库时用utf8mb4,这样存进去的中文才能正常显示。这两个问题经常被混在一起,排查时先分清是Java层乱码还是数据库层乱码。
6.2 JTable刷新不生效:root cause基本都是忘记fire
很多人写完新增功能,点确认后发现表格没变化。第一步应该检查的是:数据是否真的写进去了?如果数据库里已经有新数据,那就是TableModel没有通知刷新。如果你用的是自定义TableModel,setStudents方法里必须有fireTableDataChanged();如果你用的是DefaultTableModel,则必须用setDataVector或者逐行remove、add,而不是直接拿一个int[][]塞进去。
有一个细节:当你从表格里拿选中行做修改时,修改完一定要用convertRowIndexToModel处理。尤其在查询结果筛选后,视图行号不等于模型行号,直接按视图行号去查DAO会拿到错误数据,甚至ArrayIndexOutOfBounds。
6.3 修改数据没有生效:别new一个空对象去覆盖
修改学生信息时,新手常犯的错误是先通过学号查出原对象,然后又new一个Student,把所有字段都set了一遍,最后调用update。这样很容易漏字段,导致修改完以后某些字段变成null。更稳的做法是:在修改对话框打开时,把原Student对象传入,预填到各个文本框;用户点确定后,复制原对象的所有字段,再把新值覆盖上去。或者更简单,直接new一个新对象,把表单里有的字段全部显式赋值。总之,不要依赖"没传就是空"这种隐式行为。
6.4 数据库连接泄漏:表现形式特别像"偶发送修"
连接泄漏是最难排查的问题之一。表现是:点击几次操作后突然卡死,或者提示Too many connections,重启后又恢复正常。原因通常是获取了Connection后没有在finally里关闭,尤其查询正常、插入偶尔报错时,很多人会在异常路径里忘记关闭。
用try-with-resources可以有效避免:
try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 处理结果 }连接池场景下,close()并不是物理关闭,而是把连接归还给池子。所以不要因为"池子会复用连接"就放弃关闭,恰恰相反,正是池子复用才更难发现泄漏。批量操作之前,可以在DbUtil里写一个方法打印当前活跃连接数,肉眼观察有没有增长,这对排查非常有用。
6.5 打包成可运行Jar后图标丢失、数据库连不上
开发时一切正常,打包后双击跑不起来,也是常见问题。如果项目打包成普通jar,JDBC驱动的META-INF服务文件可能没有合并,导致ClassNotFoundException。解决办法是在pom.xml里配置maven-shade-plugin,它会把所有依赖打成一个fat jar,并且处理SPI文件合并。另一个问题是打包后找不到数据库驱动,因为没把mysql-connector打进去。用shade插件就能一并解决。
如果你希望别人拿到jar就能双击运行,可以在JFrame里通过setIconImage导入资源目录下图片,但注意打包后不能用File读取,要用getResourceAsStream。这样图标和资源都会被正确打进jar里,而不是依赖外部路径。
7. 项目做完了,怎么讲才不像"背代码"
最后聊一个很现实的问题:项目做完,面试或者答辩时怎么讲。大部分学生都会拿出自己写的代码从头念到尾。听的人很累,讲的人也紧张。更好的方式是先讲痛点,再讲方案,最后讲亮点。按照这个逻辑,你可以这样组织你的讲述:
7.1 先讲这次开发遇到的最大困难
不要说"没有任何困难",那会显得没深度。你可以说:首次做GUI时,界面和业务逻辑都堆在事件监听器里面,后续加一个搜索功能都要动主界面代码,很痛。于是你参照简单工厂加分层模式,把界面、业务、数据访问拆成三层,并且用DAO接口屏蔽存储实现。这个经历虽然简单,但比模板式的"我用了MVC设计模式"更有说服力。
7.2 突出两三个有技术含量的细节
比如:自定义TableModel来解耦视图和数据;PreparedStatement防SQL注入;密码加盐摘要存储;HikariCP连接池管理连接生命周期;SwingWorker确保UI线程安全。这些不是炫技,而是每一个都能回答"为什么"的实打实细节。
如果面试官问"这个系统还能怎么优化",提前准备两个方向就行:一是引入分页查询和索引,解决大数据量场景;二是把Swing界面升级成JavaFX,或者将Service和DAO层迁移到Spring Boot,实现跨界面复用。这两句话不仅说明你有架构思考,也暗示你有继续学习的意愿。
7.3 列一张自检清单,看看你的项目到底完整不完整
我建议在收尾之前,按这样一张表自查一遍,哪一项没做就补哪一项:
| 检查项 | 达标标准 |
|---|---|
| 登录功能 | 密码非明文,错误次数有提示 |
| 增删改查 | 四种操作都完整,且操作后有明确反馈 |
| 搜索 | 支持学号/姓名模糊搜索,回车触发 |
| 表单校验 | 必填、长度、号码格式有校验 |
| 异常处理 | 数据库连接失败有友好提示,无堆栈输出 |
| 代码分层 | entity、dao、service、view、controller分离 |
| 注释 | 关键业务方法有注释,不是解释每一行语法 |
| 打包 | 能打成可运行jar,双击可启动 |
每一行都是一段能展开讲的内容。如果你的项目现在只做到"能跑",我强烈建议按这个清单重新过一遍。你会发现,修改的过程中你的理解深度会明显不一样。
8. 一个值得长期保留的起点
在整个项目设计过程中,我最深的体会是:学生管理系统最大的价值不在于它有多新颖,而在于它把Java学习中那些零散的知识点,用一条完整的工程线索串了起来。你用Swing做界面,就逼着自己理解事件驱动;你写DAO接口,就理解了面向接口编程;你连接MySQL,就理解了持久化和连接管理;你处理中文乱码和连接泄漏,就开始真正接触"生产环境里经常发生但教程里从来不教"的问题。而这些,恰恰是面试官最想听到的。
如果你做完这个项目,还有一种意犹未尽的感觉,那说明你的思维方式已经不只是学生作业水平了。接下来可以把界面升级成响应式的JavaFX,或者把DAO层换成一个简单的ORM框架,又或者给系统加入班级管理、成绩管理、课程表管理等模块。届时你会发现,底层的分层结构和核心业务逻辑都不用大改,这本身就是这次练手最大的收获。
本文还有配套的精品资源,点击获取