Java Swing+JDBC教务系统开发:从分层架构到Swing界面实战
2026/9/4 14:45:47 网站建设 项目流程

1. 项目概述与核心价值

最近在整理过往的项目经验,发现一个挺有意思的现象:很多计算机专业的学生或者刚入行的朋友,在课程设计或者接一些小型外包时,常常会被“带界面的教务管理系统”这类题目难住。难点不在于业务逻辑本身,而在于如何将Java、数据库和图形界面这三者顺畅地串联起来,形成一个稳定、可用的桌面应用。我自己带过不少实习生,也看过很多课程设计的代码,发现大家容易在几个地方卡壳:数据库连接池的管理、Swing界面的事件响应与数据绑定、以及业务逻辑与界面展示的分离。这个项目看似基础,实则是一个检验Java SE综合能力的绝佳试金石,它涵盖了面向对象设计、JDBC操作、多线程(Swing EDT)、模块化分层等核心知识点。

一个完整的、带数据库的Java桌面教务管理系统,绝不仅仅是“能跑通”就行。它的核心价值在于,通过一个具体的业务场景(学生、课程、成绩、教师管理),将Java桌面开发中那些琐碎但至关重要的技术点串联起来,形成一套可复用的工程实践。比如,如何设计一个健壮的数据库访问层(DAO)来应对各种CRUD操作?如何在Swing的表格(JTable)中优雅地展示和编辑从数据库查询出来的分页数据?又该如何处理用户登录、权限校验这类通用功能?把这些搞明白了,以后无论是做进销存、客户关系管理还是其他任何桌面应用,你都能快速上手。接下来,我就结合自己多次重构这类系统的经验,从头到尾拆解一遍,重点会放在那些官方文档不会细讲,但实际开发中一定会遇到的“坑”和最佳实践上。

2. 技术栈选型与架构设计思路

2.1 为什么是Swing + JDBC + MySQL?

看到这个组合,可能有人会问:现在不都流行Spring Boot + Vue/React做前后端分离吗?为什么还要用“老掉牙”的Swing和纯JDBC?这里有两个核心考量。第一,教学与原型验证目的。对于学习Java SE和数据库编程而言,Swing和JDBC是最直接、最纯粹的技术组合,能让你清晰地看到从界面点击到SQL执行再到结果渲染的完整链路,没有任何框架的“黑魔法”干扰。理解了这个底层过程,未来学习MyBatis、Hibernate乃至Spring Data JPA才会知其所以然。第二,轻量级桌面应用的特定场景。对于一些内部工具、离线操作软件或者对安装部署有极简要求的场合,一个打包成可执行JAR的Swing应用,用户双击就能运行,无需安装数据库客户端(可使用嵌入式数据库如H2)或配置Web服务器,有其不可替代的便利性。

当然,纯JDBC操作比较繁琐,所以我们通常会引入一些轻量级工具来提升效率。数据库方面,MySQL社区版免费、普及率高,是首选。为了简化JDBC编码,我会使用Apache Commons DbUtils这个小巧的库,它封装了结果集处理,能大大减少模板代码。连接池则选择HikariCP,它是目前性能最好的JDBC连接池之一,对于桌面应用来说也足够轻量。界面构建,如果不想手写大量布局代码,可以用WindowBuilder这样的Eclipse/IDEA插件进行可视化拖拽设计,但我会建议关键界面还是手写代码,以便更好地控制布局管理和事件监听。

2.2 分层架构设计:告别“意大利面条”式代码

最忌讳的写法就是把所有代码——从界面按钮监听器到SQL拼接再到业务判断——全部堆在一个类里。这样的代码几乎无法维护和测试。我们必须采用分层架构,通常分为四层:

  1. 实体层(Entity): 对应数据库表,是简单的Java Bean。例如Student类,包含idnamestudentNumber等属性及其getter/setter。
  2. 数据访问层(DAO): 职责单一,只负责与数据库交互,执行增删改查。每个实体类对应一个DAO接口及其实现类,如StudentDaoStudentDaoImpl。这里会集中处理SQL语句和PreparedStatement,使用DbUtils简化操作。
  3. 业务逻辑层(Service): 封装具体的业务规则。例如“学生选课”这个业务,在Service层里可能会依次调用CourseDao检查课程容量、StudentDao检查学生状态、SelectionDao插入选课记录,并在此过程中进行事务控制。它是连接界面层和DAO层的桥梁。
  4. 表示层(View): 即Swing界面。它的职责是接收用户输入、展示数据,并将用户请求委托给Service层处理。界面层应尽可能“笨”,不包含业务逻辑。

此外,还需要一些辅助层

  • 数据库连接与配置层: 使用单例模式或静态工厂方法提供DataSource(HikariCP),确保整个应用共用同一个连接池。
  • 工具层: 包含一些通用方法,如日期格式转换、对话框弹出、表格模型封装等。

注意: 很多新手会忽略事务处理。在Service层的一个方法中,如果涉及多次数据库更新(如扣库存和生成订单),必须使用事务来保证原子性。纯JDBC环境下,我们需要手动获取连接、关闭自动提交、在try-catch-finally中处理提交或回滚。这是Service层的核心职责之一。

3. 数据库设计与核心表结构解析

数据库设计是系统的基石,设计不好,后期编码会处处掣肘。教务系统的核心实体通常包括:用户(管理员、教师、学生)、课程、班级、成绩、选课记录等。这里给出一个高度简化的核心表结构,并解释设计用意。

3.1 核心表结构设计

-- 用户表(统一存储管理员、教师、学生,通过user_type区分) CREATE TABLE `sys_user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) UNIQUE NOT NULL COMMENT '登录名', `password` VARCHAR(255) NOT NULL COMMENT '加密后的密码', `real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名', `user_type` TINYINT NOT NULL COMMENT '用户类型:0-管理员,1-教师,2-学生', `student_number` VARCHAR(20) NULL COMMENT '学号(仅学生有)', `teacher_number` VARCHAR(20) NULL COMMENT '工号(仅教师有)', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT='系统用户表'; -- 课程表 CREATE TABLE `course` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `course_code` VARCHAR(20) UNIQUE NOT NULL COMMENT '课程代码', `course_name` VARCHAR(100) NOT NULL COMMENT '课程名称', `credit` DECIMAL(3,1) NOT NULL COMMENT '学分', `total_hours` INT NOT NULL COMMENT '总学时', `teacher_id` INT NOT NULL COMMENT '授课教师ID', `max_students` INT DEFAULT 60 COMMENT '最大选课人数', `current_students` INT DEFAULT 0 COMMENT '当前选课人数', FOREIGN KEY (`teacher_id`) REFERENCES `sys_user`(`id`) ) COMMENT='课程信息表'; -- 选课记录表(学生与课程的多对多关系) CREATE TABLE `course_selection` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `student_id` INT NOT NULL, `course_id` INT NOT NULL, `selection_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', `status` TINYINT DEFAULT 1 COMMENT '状态:1-已选,2-已退选', UNIQUE KEY `uk_student_course` (`student_id`, `course_id`), -- 防止重复选课 FOREIGN KEY (`student_id`) REFERENCES `sys_user`(`id`), FOREIGN KEY (`course_id`) REFERENCES `course`(`id`) ) COMMENT='选课记录表'; -- 成绩表 CREATE TABLE `score` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `selection_id` INT NOT NULL UNIQUE COMMENT '关联的选课记录ID', `usual_score` DECIMAL(5,2) COMMENT '平时成绩', `exam_score` DECIMAL(5,2) COMMENT '考试成绩', `total_score` DECIMAL(5,2) GENERATED ALWAYS AS (COALESCE(usual_score,0)*0.3 + COALESCE(exam_score,0)*0.7) STORED COMMENT '总评成绩(生成列)', `teacher_comment` TEXT COMMENT '教师评语', FOREIGN KEY (`selection_id`) REFERENCES `course_selection`(`id`) ) COMMENT='成绩表';

3.2 设计要点与避坑指南

  1. 密码存储: 绝对不要明文存储密码!password字段使用VARCHAR(255),在Java端使用BCrypt或PBKDF2等强哈希算法进行加密后存储。校验时比较哈希值。
  2. 用户表设计: 采用单表继承(user_type区分)还是多表(student,teacher表)?这里采用单表,好处是查询简单,权限控制统一。缺点是字段会有冗余(学生的teacher_number为NULL)。对于中小型系统,单表更简单高效。
  3. 外键与索引: 像course_selection表中的student_idcourse_id,必须建立外键约束以保证数据完整性。同时,为它们创建复合唯一索引uk_student_course,这是业务逻辑(一个学生不能重复选同一门课)在数据库层的保障。
  4. 生成列的使用score表中的total_score是一个“生成列”,其值由usual_scoreexam_score自动计算得出。这保证了数据的一致性,避免了在应用层重复计算可能带来的错误。这是MySQL 5.7+和MariaDB提供的实用特性。
  5. 数据字典: 像user_typestatus这类字段,建议在项目内维护一个常量类,而不是在代码中硬编码数字。例如:public interface UserType { int ADMIN = 0; int TEACHER = 1; int STUDENT = 2; }

4. 核心模块实现与Swing界面开发实战

4.1 数据库连接池与通用DAO封装

首先,我们解决数据库访问的基础问题。使用HikariCP配置连接池。

// DatabaseConfig.java public class DatabaseConfig { private static volatile DataSource dataSource; private DatabaseConfig() {} public static DataSource getDataSource() { if (dataSource == null) { synchronized (DatabaseConfig.class) { if (dataSource == null) { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/edu_sys?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"); config.setUsername("your_username"); config.setPassword("your_password"); config.setMaximumPoolSize(10); // 桌面应用,10个连接足够 config.setMinimumIdle(2); config.setConnectionTimeout(30000); // 30秒 config.setIdleTimeout(600000); // 10分钟 config.setMaxLifetime(1800000); // 30分钟 // 其他优化配置... dataSource = new HikariDataSource(config); } } } return dataSource; } }

接着,封装一个基于DbUtils的通用JdbcTemplate,简化DAO层的编写。

// JdbcTemplate.java (简化版) public class JdbcTemplate { private final QueryRunner queryRunner = new QueryRunner(DatabaseConfig.getDataSource()); // 查询单条记录 public <T> T queryForObject(String sql, ResultSetHandler<T> rsh, Object... params) throws SQLException { return queryRunner.query(sql, rsh, params); } // 查询列表 public <T> List<T> queryForList(String sql, ResultSetHandler<List<T>> rsh, Object... params) throws SQLException { return queryRunner.query(sql, rsh, params); } // 执行更新(增、删、改) public int update(String sql, Object... params) throws SQLException { return queryRunner.update(sql, params); } // 在事务中执行操作 public <T> T executeTransaction(TransactionCallback<T> action) throws SQLException { Connection conn = null; try { conn = DatabaseConfig.getDataSource().getConnection(); conn.setAutoCommit(false); T result = action.doInTransaction(conn); conn.commit(); return result; } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { DbUtils.closeQuietly(conn); } } }

有了这个模板,DAO层的实现就非常清晰了。以StudentDaoImpl为例:

// StudentDaoImpl.java public class StudentDaoImpl implements StudentDao { private JdbcTemplate jdbcTemplate = new JdbcTemplate(); private BeanProcessor beanProcessor = new BeanProcessor(); @Override public Student findById(int id) throws SQLException { String sql = "SELECT * FROM sys_user WHERE id = ? AND user_type = ?"; // 使用自定义的ResultSetHandler将结果集映射为Student对象 return jdbcTemplate.queryForObject(sql, new ResultSetHandler<Student>() { @Override public Student handle(ResultSet rs) throws SQLException { if (rs.next()) { return beanProcessor.toBean(rs, Student.class); } return null; } }, id, UserType.STUDENT); } @Override public List<Student> findAll(int page, int size) throws SQLException { String sql = "SELECT * FROM sys_user WHERE user_type = ? LIMIT ?, ?"; int offset = (page - 1) * size; return jdbcTemplate.queryForList(sql, new BeanListHandler<>(Student.class), UserType.STUDENT, offset, size); } // ... 其他方法 }

4.2 Swing界面开发:以学生信息管理为例

Swing开发的核心是理解**事件分发线程(EDT)模型-视图-控制器(MVC)**模式。所有UI的创建和更新都必须在EDT上执行,而耗时的操作(如数据库查询)必须放在后台线程,否则界面会卡死。

我们构建一个学生信息管理界面,包含查询表格和表单。

// StudentManagementPanel.java - 主面板 public class StudentManagementPanel extends JPanel { private JTable studentTable; private StudentTableModel tableModel; // 自定义的TableModel private JTextField searchField; private JButton searchButton, addButton, editButton, deleteButton; private StudentService studentService = new StudentServiceImpl(); public StudentManagementPanel() { initUI(); loadStudentData(1, 20); // 加载第一页数据 } private void initUI() { setLayout(new BorderLayout(10, 10)); // 1. 顶部工具栏 JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); searchField = new JTextField(20); searchButton = new JButton("查询"); addButton = new JButton("新增"); editButton = new JButton("编辑"); deleteButton = new JButton("删除"); topPanel.add(new JLabel("姓名:")); topPanel.add(searchField); topPanel.add(searchButton); topPanel.add(Box.createHorizontalStrut(20)); // 添加间距 topPanel.add(addButton); topPanel.add(editButton); topPanel.add(deleteButton); // 2. 中央表格 tableModel = new StudentTableModel(); studentTable = new JTable(tableModel); studentTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); studentTable.getColumnModel().getColumn(0).setPreferredWidth(60); // ID列宽 studentTable.getColumnModel().getColumn(1).setPreferredWidth(100); // 学号 studentTable.getColumnModel().getColumn(2).setPreferredWidth(80); // 姓名 JScrollPane scrollPane = new JScrollPane(studentTable); // 3. 底部分页栏(略) // JPanel bottomPanel = ... add(topPanel, BorderLayout.NORTH); add(scrollPane, BorderLayout.CENTER); // add(bottomPanel, BorderLayout.SOUTH); // 4. 绑定事件监听器 setupEventListeners(); } private void setupEventListeners() { searchButton.addActionListener(e -> onSearch()); addButton.addActionListener(e -> onAddStudent()); editButton.addActionListener(e -> onEditStudent()); deleteButton.addActionListener(e -> onDeleteStudent()); } private void loadStudentData(int page, int size) { // 使用SwingWorker在后台加载数据,避免阻塞EDT SwingWorker<List<Student>, Void> worker = new SwingWorker<>() { @Override protected List<Student> doInBackground() throws Exception { // 这里是耗时操作 return studentService.getStudentsByPage(page, size); } @Override protected void done() { try { List<Student> students = get(); tableModel.setStudents(students); // 更新表格模型 tableModel.fireTableDataChanged(); // 通知表格刷新 } catch (InterruptedException | ExecutionException e) { JOptionPane.showMessageDialog(StudentManagementPanel.this, "加载数据失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } }; worker.execute(); } private void onSearch() { String keyword = searchField.getText().trim(); // 同样使用SwingWorker执行查询... } private void onAddStudent() { // 弹出对话框,使用StudentDialog(自定义的对话框) StudentDialog dialog = new StudentDialog((Frame) SwingUtilities.getWindowAncestor(this), "新增学生"); dialog.setVisible(true); if (dialog.isConfirmed()) { Student newStudent = dialog.getStudent(); // 调用Service层保存 try { studentService.addStudent(newStudent); loadStudentData(1, 20); // 重新加载数据 JOptionPane.showMessageDialog(this, "添加成功!"); } catch (ServiceException ex) { JOptionPane.showMessageDialog(this, "添加失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } } // ... 其他方法 }

自定义TableModel是关键,它连接了数据和JTable视图。

// StudentTableModel.java public class StudentTableModel extends AbstractTableModel { private List<Student> students = new ArrayList<>(); private String[] columnNames = {"ID", "学号", "姓名", "性别", "入学时间"}; @Override public int getRowCount() { return students.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public String getColumnName(int column) { return columnNames[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Student student = students.get(rowIndex); switch (columnIndex) { case 0: return student.getId(); case 1: return student.getStudentNumber(); case 2: return student.getRealName(); case 3: return student.getGender() == 1 ? "男" : "女"; case 4: return new SimpleDateFormat("yyyy-MM-dd").format(student.getEnrollmentDate()); default: return null; } } public void setStudents(List<Student> students) { this.students = students; } public Student getStudentAt(int row) { if (row >= 0 && row < students.size()) { return students.get(row); } return null; } }

4.3 业务逻辑层(Service)与事务控制

Service层是业务规则的家。以“学生选课”为例,它需要处理多个DAO操作,并保证事务。

// CourseSelectionServiceImpl.java public class CourseSelectionServiceImpl implements CourseSelectionService { private CourseSelectionDao selectionDao = new CourseSelectionDaoImpl(); private CourseDao courseDao = new CourseDaoImpl(); private JdbcTemplate jdbcTemplate = new JdbcTemplate(); @Override public void selectCourse(int studentId, int courseId) throws ServiceException { // 使用我们封装的JdbcTemplate执行事务 try { jdbcTemplate.executeTransaction(conn -> { // 1. 检查课程是否存在且未满 Course course = courseDao.findById(courseId, conn); // 需要传递连接 if (course == null) { throw new SQLException("课程不存在"); } if (course.getCurrentStudents() >= course.getMaxStudents()) { throw new SQLException("课程人数已满"); } // 2. 检查学生是否已选该课程 if (selectionDao.exists(studentId, courseId, conn)) { throw new SQLException("不能重复选课"); } // 3. 插入选课记录 CourseSelection selection = new CourseSelection(); selection.setStudentId(studentId); selection.setCourseId(courseId); selectionDao.insert(selection, conn); // 4. 更新课程当前人数 courseDao.incrementCurrentStudents(courseId, conn); return null; // 回调函数需要返回值,这里返回null即可 }); } catch (SQLException e) { // 将SQLException转换为业务异常 throw new ServiceException("选课失败: " + e.getMessage(), e); } } }

实操心得: Service层的异常处理很重要。底层DAO抛出的SQLException是技术细节,不应该直接抛给界面层。我们将其捕获,并转换为自定义的ServiceException(一个运行时异常),这样界面层只需要处理一种业务异常,代码更清晰。同时,在事务回调中,所有DAO方法都需要重载一个接收Connection参数的版本,以保证它们在同一个事务连接中执行。

5. 功能模块详解与高级技巧

5.1 用户登录与权限控制

一个完整的系统必须有权限控制。我们设计一个简单的基于角色的访问控制(RBAC)。

  1. 登录验证: 在UserService中,根据用户名查询用户,并使用BCrypt校验密码。登录成功后,将用户对象(包含user_type)存入一个全局的SessionContext(一个单例或使用ThreadLocal)中。
  2. 界面权限: 在主界面(如MainFrame)初始化时,根据SessionContext中的用户类型,动态设置菜单项和按钮的可用状态(setEnabled)。例如,学生看不到“课程管理”菜单。
  3. 数据权限: 在Service层的方法中,加入权限校验。例如,在TeacherServicesubmitScore方法里,首先要判断当前登录教师是否是这门课的授课教师。
// 简单的Session上下文 public class AppContext { private static final ThreadLocal<SysUser> currentUser = new ThreadLocal<>(); public static void setCurrentUser(SysUser user) { currentUser.set(user); } public static SysUser getCurrentUser() { return currentUser.get(); } public static void clear() { currentUser.remove(); } } // 在登录成功后 SysUser user = userService.login(username, password); AppContext.setCurrentUser(user);

5.2 数据导出与报表生成

教务系统经常需要导出学生名单、成绩单等。我们可以使用Apache POI库来生成Excel文件。

// ExportService.java 片段 public void exportStudentList(List<Student> students, String filePath) throws IOException { Workbook workbook = new XSSFWorkbook(); // 创建.xlsx格式工作簿 Sheet sheet = workbook.createSheet("学生名单"); // 创建标题行 Row headerRow = sheet.createRow(0); String[] headers = {"学号", "姓名", "性别", "学院", "专业"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); // 可以设置单元格样式... } // 填充数据行 int rowNum = 1; for (Student student : students) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(student.getStudentNumber()); row.createCell(1).setCellValue(student.getRealName()); row.createCell(2).setCellValue(student.getGender() == 1 ? "男" : "女"); row.createCell(3).setCellValue(student.getCollege()); row.createCell(4).setCellValue(student.getMajor()); } // 自动调整列宽(按内容) for (int i = 0; i < headers.length; i++) { sheet.autoSizeColumn(i); } // 写入文件 try (FileOutputStream outputStream = new FileOutputStream(filePath)) { workbook.write(outputStream); } finally { workbook.close(); } }

在界面上,添加一个“导出”按钮,点击后弹出文件选择器(JFileChooser),选择保存路径后调用上述服务即可。

5.3 使用嵌入式数据库简化部署

对于课程设计或演示,要求用户安装MySQL可能太麻烦。我们可以使用嵌入式数据库H2,它可以将数据库打包在应用内。

  1. 在Maven或Gradle中引入H2依赖。
  2. 修改DatabaseConfig中的JDBC URL为内存模式或文件模式:
    • 内存模式:jdbc:h2:mem:edu_sys;DB_CLOSE_DELAY=-1(程序关闭后数据消失)。
    • 文件模式:jdbc:h2:file:~/.edu_sys/db(数据持久化到用户目录)。
  3. 在应用启动时,执行初始化SQL脚本(schema.sqldata.sql)来创建表和插入基础数据。

这样,最终用户只需要有JRE环境,双击JAR包就能运行完整系统,体验非常好。

6. 项目打包、部署与常见问题排查

6.1 使用Maven Shade Plugin打包可执行JAR

一个专业的项目需要打包成包含所有依赖的“胖JAR”(uber JAR)。Maven的maven-shade-plugin可以很好地完成这个任务。

<!-- 在pom.xml中配置 --> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <!-- 合并多个依赖中的META-INF/services文件 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/> <!-- 指定主类 --> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.yourcompany.edusys.MainApp</mainClass> </transformer> </transformers> <filters> <filter> <!-- 排除签名文件,避免冲突 --> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> </plugins> </build>

运行mvn clean package后,会在target目录下生成一个*-shaded.jar文件,直接使用java -jar your-app-shaded.jar命令即可运行。

6.2 常见问题与排查技巧实录

在开发过程中,你几乎一定会遇到下面这些问题:

问题1:界面卡死,无响应

  • 现象: 点击查询按钮后,整个程序界面卡住,鼠标变成等待状态,直到数据库查询完成才恢复。
  • 原因: 在Swing的事件分发线程(EDT)中执行了耗时的数据库操作。
  • 解决: 必须使用SwingWorkerThreadExecutorService将耗时操作放到后台线程。如上面loadStudentData方法所示。记住黄金法则:在EDT中只做UI更新,所有I/O、网络、复杂计算都丢到后台

问题2:数据库连接泄露

  • 现象: 程序运行一段时间后,无法获取新的数据库连接,最终导致连接池耗尽。
  • 原因: 没有正确关闭ConnectionStatementResultSet
  • 解决
    1. 使用try-with-resources语法(Java 7+),确保资源自动关闭。
    2. 使用DbUtils的DbUtils.closeQuietly(...)方法,它内部处理了null检查和异常捕获。
    3. 在DAO层,确保每个方法在最后都释放了资源。我们的JdbcTemplate和DbUtils的QueryRunner已经帮我们做了这件事。

问题3:中文字符乱码

  • 现象: 从数据库读出的中文,或写入数据库的中文显示为问号“???”。
  • 原因: 数据库连接字符集不匹配。
  • 解决: 确保三处编码统一为UTF-8:
    1. 数据库/表创建时指定CHARACTER SET utf8mb4(MySQL)。
    2. JDBC连接URL中加入参数:?useUnicode=true&characterEncoding=UTF-8
    3. Java源文件本身保存为UTF-8格式。

问题4:表格(JTable)刷新后数据错乱或显示旧数据

  • 现象: 新增或删除数据后,调用fireTableDataChanged(),但表格显示不对。
  • 原因: 直接修改了TableModel背后的数据列表,但没有在修改后立即通知表格。
  • 解决
    1. TableModel中提供完整的数据操作方法,如addRow(Student s)removeRow(int row)
    2. 在这些方法内部,先更新数据列表,再调用对应的fire方法(fireTableRowsInsertedfireTableRowsDeletedfireTableDataChanged)。
    3. 永远在EDT中更新表格模型和调用fire方法。

问题5:程序打包后找不到数据库驱动或配置文件

  • 现象: 在IDE里运行正常,打包成JAR后运行报ClassNotFoundException: com.mysql.cj.jdbc.Driver或找不到config.properties
  • 原因: 依赖的JAR包没有被打进去,或配置文件路径不对。
  • 解决
    1. 使用上述的Shade Plugin打包,确保所有依赖都在最终的JAR里。
    2. 对于配置文件,不要使用绝对路径或相对于当前工作目录的路径。应该将配置文件(如config.properties)放在src/main/resources目录下,然后使用ClassLoader.getResourceAsStream(“config.properties”)来读取。这样无论JAR在哪里运行,都能正确找到资源。

问题6:界面在不同分辨率下布局错乱

  • 现象: 在自己电脑上布局完美,在别人高分辨率屏幕上控件挤在一起或位置偏移。
  • 原因: 使用了绝对布局(setBounds)或固定大小的组件。
  • 解决
    1. 优先使用Swing的布局管理器,如BorderLayoutGridBagLayoutMigLayout(第三方,更强大)。
    2. 使用JPanel嵌套来组合复杂布局。
    3. 为组件设置最小、最大、首选尺寸(setMinimumSize,setPreferredSize)。
    4. 考虑使用JFrame.pack()方法让窗口根据内容自动调整大小。

把这个项目从头到尾实现一遍,你会对Java桌面应用开发有一个非常扎实和全面的理解。它就像一块敲门砖,掌握了这些核心模式和技巧,无论是转向Java EE开发,还是深入学习客户端框架,都会轻松很多。最关键的是,在这个过程中培养出来的分层设计、异常处理和问题排查能力,是任何Java开发者都不可或缺的硬实力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询