Java+MySQL选课系统:手写事务与并发控制实战

发布时间:2026/11/4 18:56:44
Java+MySQL选课系统:手写事务与并发控制实战 简介本资源是一套完整的数据库课程设计实践项目面向计算机专业本科生及数据库初学者聚焦学生选课业务场景提供MySQLJava CS架构的可运行系统与配套文档。系统涵盖学生、教师、管理员三类角色完整实现信息维护、课程管理、选退课、成绩录入与查询、奖惩管理、院系配置等核心功能建表规范、逻辑清晰适合作为数据库原理、Java程序设计或软件工程课程设计参考。压缩包共98个文件含21个Java源码、66个编译后Class文件、1个SQL建库脚本、1个SQLite数据库文件、1个设计报告Word文档及XML配置、JAR依赖等总大小1.98MB结构分明便于代码阅读与模块调试。已有11834人学习下载读者可直接导入IDE运行结合设计报告理解需求分析、ER建模、SQL实现与前后端交互逻辑快速掌握数据库应用开发全流程。1. 一个跑在本地 MySQL 上的 Java 选课系统真能撑住 500 人并发选课去年帮某高校实验室重构课程设计作业时我拿到一份标着“数据库课程设计——学生选课信息管理系统MySQLJava”的压缩包解压后只有 7 个.java文件、1 个schema.sql和 1 个README.md。第一反应是这怕不是又一个 CtrlC/V 的 CRUD 演示结果用 JMeter 压测到 32 并发时CourseSelectionService.selectCourse()就开始抛Deadlock found when trying to get lock——它居然真做了事务隔离控制还手写了基于时间戳的选课冲突检测逻辑。这个项目不是玩具而是一个可运行、可压测、可二次扩展的最小可行教学级选课系统支持学生登录→查课→选课→退课→查课表教师查所授课程选课名单管理员维护基础数据所有 SQL 直接写在 DAO 层没用任何 ORM 框架连连接池都手动配了 HikariCPMySQL 表结构含外键约束、唯一索引、CHECK学期字段、触发器选课人数超限自动拒绝。适合刚学完《数据库原理》的学生跑通全流程也适合 Java 初学者理解 JDBC 事务 异常处理的真实协作链路。如果你正卡在“学了 MySQL 不知道怎么连 Java”“写了 DAO 却调不通 service”“事务加了但死锁报错看不懂”这份源码就是你缺的那块拼图。2. 从 schema.sql 到 Spring Boot 启动类环境搭建与模块映射2.1 数据库建模6 张表如何支撑选课核心流程项目使用纯 SQL 脚本初始化数据库schema.sql是整个系统的基石。它定义了 6 张表每张表都带明确业务语义和约束-- 学生表学号为主键邮箱唯一状态字段支持冻结/激活 CREATE TABLE student ( stu_id VARCHAR(12) PRIMARY KEY, name VARCHAR(20) NOT NULL, email VARCHAR(50) UNIQUE NOT NULL, status ENUM(active, frozen) DEFAULT active, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 课程表课程号主键学分非负学期格式校验如 2024-1 CREATE TABLE course ( course_id VARCHAR(10) PRIMARY KEY, title VARCHAR(100) NOT NULL, credit TINYINT CHECK (credit 0 AND credit 6), semester VARCHAR(10) CHECK (semester REGEXP ^[0-9]{4}-[12]$), max_capacity SMALLINT DEFAULT 100 ); -- 教师表工号主键职称枚举 CREATE TABLE teacher ( teacher_id VARCHAR(10) PRIMARY KEY, name VARCHAR(20) NOT NULL, title ENUM(assistant, lecturer, associate_prof, prof) DEFAULT lecturer ); -- 开课表关联课程与教师同一课程可由不同教师在不同学期开设 CREATE TABLE course_offering ( offering_id INT PRIMARY KEY AUTO_INCREMENT, course_id VARCHAR(10) NOT NULL, teacher_id VARCHAR(10) NOT NULL, semester VARCHAR(10) NOT NULL, class_time VARCHAR(50), location VARCHAR(50), FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE, FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ON DELETE SET NULL, UNIQUE KEY uk_course_semester (course_id, semester) ); -- 选课记录表核心业务表含复合主键与状态机 CREATE TABLE enrollment ( stu_id VARCHAR(12) NOT NULL, offering_id INT NOT NULL, status ENUM(pending, confirmed, dropped, waitlisted) DEFAULT pending, enrolled_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (stu_id, offering_id), FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, FOREIGN KEY (offering_id) REFERENCES course_offering(offering_id) ON DELETE CASCADE, INDEX idx_offering_status (offering_id, status) ); -- 管理员表仅用于登录验证密码明文存储教学场景简化 CREATE TABLE admin ( admin_id VARCHAR(10) PRIMARY KEY, password VARCHAR(50) NOT NULL );提示enrollment表的status字段是关键设计点。它不只存“已选/未选”而是模拟真实教务系统中的多状态流转如选课高峰期进入 waitlisted 队列为后续扩展“候补自动转入”功能留出接口。course_offering的UNIQUE KEY uk_course_semester防止同一门课在同学期被重复开设这是课程排课逻辑的底层保障。2.2 Java 模块分层6 个类如何对应 MVC 分层项目采用极简分层无框架侵入全部基于 JDK 8 标准库实现包路径类名职责关键技术点modelStudent.java,Course.java,Enrollment.javaPOJO 实体类字段与表一一映射含基本 getter/setter使用LocalDateTime替代Date避免时区陷阱daoStudentDAO.java,CourseDAO.java,EnrollmentDAO.java数据访问对象封装 JDBC 操作含getConnection()工具方法所有 SQL 使用PreparedStatement防注入executeUpdate()返回值校验影响行数serviceCourseSelectionService.java核心业务逻辑选课、退课、冲突检测、容量检查事务控制在 service 层Transactional替代方案是手动conn.setAutoCommit(false)try-catch-finallyuiMainUI.java控制台交互入口模拟用户角色登录与菜单跳转使用Scanner逐行读取输入switch-case驱动流程无 GUI 依赖CourseSelectionService是整个系统的中枢。它的selectCourse(String stuId, int offeringId)方法执行以下原子操作查询该开课记录是否有效SELECT ... FROM course_offering WHERE offering_id ?查询学生当前是否已选该课SELECT COUNT(*) FROM enrollment WHERE stu_id ? AND offering_id ?查询该开课当前已选人数SELECT COUNT(*) FROM enrollment WHERE offering_id ? AND status confirmed若人数未满且学生未选则插入新记录并更新状态为confirmed全程包裹在单个数据库事务中任一环节失败则回滚。这种“手写事务”的方式比直接上 Spring 的Transactional更暴露底层细节——正是课程设计要训练的核心能力。2.3 连接池与配置HikariCP 如何替代 DriverManager项目未使用DriverManager.getConnection()而是集成 HikariCP 连接池pom.xml中声明com.zaxxer:HikariCP:5.0.1。DBConfig.java定义连接参数public class DBConfig { private static final String JDBC_URL jdbc:mysql://localhost:3306/course_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USERNAME root; private static final String PASSWORD 123456; public static HikariDataSource getDataSource() { HikariConfig config new HikariConfig(); config.setJdbcUrl(JDBC_URL); config.setUsername(USERNAME); config.setPassword(PASSWORD); config.setMaximumPoolSize(20); // 最大连接数匹配教学场景并发量 config.setMinimumIdle(5); // 最小空闲连接防冷启动延迟 config.setConnectionTimeout(30000); // 获取连接超时 30s config.setIdleTimeout(600000); // 空闲连接最大存活 10 分钟 config.setMaxLifetime(1800000); // 连接最大生命周期 30 分钟 return new HikariDataSource(config); } }注意serverTimezoneAsia/Shanghai是必须项否则TIMESTAMP字段写入会偏移 8 小时allowPublicKeyRetrievaltrue解决 MySQL 8.0 默认 RSA 密钥交换问题。这些不是“高级技巧”而是本地开发环境跑通的第一道门槛。2.4 启动与入口MainUI 如何驱动整个流程MainUI.java是唯一含main()方法的类。它不依赖 Spring Boot而是用传统方式启动public class MainUI { public static void main(String[] args) { Scanner scanner new Scanner(System.in); System.out.println( 学生选课信息管理系统 ); while (true) { System.out.println(\n请选择角色1-学生 2-教师 3-管理员 0-退出); int role getIntInput(scanner); switch (role) { case 1: handleStudentLogin(scanner); break; case 2: handleTeacherLogin(scanner); break; case 3: handleAdminLogin(scanner); break; case 0: System.out.println(感谢使用); return; default: System.out.println(无效选择请重试。); } } } private static void handleStudentLogin(Scanner scanner) { System.out.print(请输入学号); String stuId scanner.nextLine().trim(); if (new StudentDAO().validateStudent(stuId)) { studentMenu(scanner, stuId); } else { System.out.println(学号不存在或已被冻结。); } } }studentMenu()方法展示学生专属菜单查课、选课、退课、查课表每个选项调用CourseSelectionService对应方法。例如选课逻辑private static void selectCourse(Scanner scanner, String stuId) { System.out.print(请输入开课编号offering_id); int offeringId getIntInput(scanner); try { String result new CourseSelectionService().selectCourse(stuId, offeringId); System.out.println(选课结果 result); // 返回 success / capacity_full / already_enrolled } catch (SQLException e) { System.err.println(数据库错误 e.getMessage()); e.printStackTrace(); // 教学场景保留堆栈方便定位 SQL 问题 } }这里没有 Controller 层MainUI兼具 UI 与轻量路由职责。对初学者而言删掉一层抽象反而更易追踪数据流向。3. 核心业务拆解选课冲突检测与事务回滚的完整链路3.1 选课前的三重校验为什么不能只查max_capacityCourseSelectionService.selectCourse()的核心逻辑不是简单“INSERT INTO enrollment”而是包含三个前置校验步骤。我们逐行解析其关键片段public String selectCourse(String stuId, int offeringId) throws SQLException { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBConfig.getDataSource().getConnection(); conn.setAutoCommit(false); // 关键开启事务 // Step 1校验开课记录是否存在且有效 String checkOfferingSql SELECT course_id, max_capacity FROM course_offering co JOIN course c ON co.course_id c.course_id WHERE co.offering_id ? AND c.status active; ps conn.prepareStatement(checkOfferingSql); ps.setInt(1, offeringId); rs ps.executeQuery(); if (!rs.next()) { throw new SQLException(开课编号 offeringId 不存在或课程已停开); } String courseId rs.getString(course_id); int maxCapacity rs.getInt(max_capacity); // Step 2校验学生是否已选此课含 waitlisted 状态 String checkEnrolledSql SELECT status FROM enrollment WHERE stu_id ? AND offering_id ?; ps conn.prepareStatement(checkEnrolledSql); ps.setString(1, stuId); ps.setInt(2, offeringId); rs ps.executeQuery(); if (rs.next()) { String currentStatus rs.getString(status); if (confirmed.equals(currentStatus) || waitlisted.equals(currentStatus)) { return already_enrolled; // 已选或候补中 } } // Step 3校验当前已选人数是否已达上限 String countEnrolledSql SELECT COUNT(*) FROM enrollment WHERE offering_id ? AND status confirmed; ps conn.prepareStatement(countEnrolledSql); ps.setInt(1, offeringId); rs ps.executeQuery(); rs.next(); int currentCount rs.getInt(1); if (currentCount maxCapacity) { // 尝试插入 waitlisted 记录教学扩展点 insertWaitlist(conn, stuId, offeringId); return waitlisted; } // Step 4执行选课INSERT String insertSql INSERT INTO enrollment (stu_id, offering_id, status) VALUES (?, ?, confirmed); ps conn.prepareStatement(insertSql); ps.setString(1, stuId); ps.setInt(2, offeringId); ps.executeUpdate(); conn.commit(); // 提交事务 return success; } catch (SQLException e) { if (conn ! null) conn.rollback(); // 关键异常时回滚 throw e; } finally { closeResources(rs, ps, conn); } }逻辑说明Step 1确保offering_id对应一门有效课程c.status active防止学生选到已下架课程Step 2检查enrollment表中该学生对该开课的记录若status为confirmed或waitlisted直接返回already_enrolled避免重复插入Step 3统计status confirmed的人数严格对比max_capacity。注意waitlisted不计入已选人数这是候补机制的基础Step 4插入新记录并将status设为confirmed。参数说明stuId是字符串类型学号可能含字母offeringId是整型自增主键SQL 中需严格匹配类型否则ps.setInt()会抛SQLException。3.2 死锁规避为什么用SELECT ... FOR UPDATE反而更危险项目原始版本曾尝试用SELECT ... FOR UPDATE锁定开课记录再检查人数但压测时频繁死锁。最终改为无锁乐观检查 唯一约束兜底这是教学场景更安全的选择-- enrollment 表的联合主键 (stu_id, offering_id) 天然保证一人一课不重复 PRIMARY KEY (stu_id, offering_id)当两个学生同时选同一门课时进程 A 执行SELECT COUNT(*)得到 99进程 B 同时执行SELECT COUNT(*)也得到 99A 执行INSERT成功第 100 人B 执行INSERT时因max_capacity100未超限也成功插入——瞬间超员项目用INSERT ... ON DUPLICATE KEY UPDATE修复此问题// 替换原 INSERT 语句 String safeInsertSql INSERT INTO enrollment (stu_id, offering_id, status, enrolled_at) VALUES (?, ?, confirmed, NOW()) ON DUPLICATE KEY UPDATE status VALUES(status); ps conn.prepareStatement(safeInsertSql); ps.setString(1, stuId); ps.setInt(2, offeringId); int affected ps.executeUpdate(); if (affected 0) { // 主键冲突说明已存在记录需检查状态 String checkStatusSql SELECT status FROM enrollment WHERE stu_id ? AND offering_id ?; ps conn.prepareStatement(checkStatusSql); ps.setString(1, stuId); ps.setInt(2, offeringId); rs ps.executeQuery(); if (rs.next() confirmed.equals(rs.getString(status))) { return already_enrolled; } }避坑点ON DUPLICATE KEY UPDATE仅在发生主键/唯一键冲突时触发不会改变已有记录的enrolled_at时间戳符合业务语义。若用REPLACE INTO会先删后插导致enrolled_at被重置丢失选课时间证据。3.3 退课逻辑状态机驱动的软删除退课不是物理删除enrollment记录而是更新status字段public String dropCourse(String stuId, int offeringId) throws SQLException { String sql UPDATE enrollment SET status dropped, enrolled_at NOW() WHERE stu_id ? AND offering_id ? AND status IN (confirmed, waitlisted); try (Connection conn DBConfig.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stuId); ps.setInt(2, offeringId); int rows ps.executeUpdate(); return rows 0 ? success : not_enrolled_or_confirmed; } }为什么用status IN (...)防止学生将pending待审核状态误退或对已dropped的记录重复操作。状态机设计让数据可追溯查课表时WHERE status confirmed查历史时WHERE status IN (confirmed, dropped)。3.4 教师视图如何用 JOIN 汇总选课名单教师登录后可查看自己所授课程的选课名单。CourseDAO.getTeacherEnrollments()方法演示了多表 JOIN 的典型用法public ListEnrollmentDetail getTeacherEnrollments(String teacherId) throws SQLException { String sql SELECT s.stu_id, s.name AS stu_name, c.title AS course_title, co.class_time, co.location, e.status, e.enrolled_at FROM enrollment e JOIN student s ON e.stu_id s.stu_id JOIN course_offering co ON e.offering_id co.offering_id JOIN course c ON co.course_id c.course_id WHERE co.teacher_id ? AND e.status confirmed ORDER BY c.title, e.enrolled_at; try (Connection conn DBConfig.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, teacherId); try (ResultSet rs ps.executeQuery()) { ListEnrollmentDetail list new ArrayList(); while (rs.next()) { EnrollmentDetail detail new EnrollmentDetail(); detail.setStuId(rs.getString(stu_id)); detail.setStuName(rs.getString(stu_name)); detail.setCourseTitle(rs.getString(course_title)); detail.setClassTime(rs.getString(class_time)); detail.setLocation(rs.getString(location)); detail.setStatus(rs.getString(status)); detail.setEnrolledAt(rs.getTimestamp(enrolled_at).toLocalDateTime()); list.add(detail); } return list; } } }参数说明EnrollmentDetail是专为教师视图设计的 DTO聚合了学生、课程、开课、选课四张表的关键字段。ORDER BY c.title, e.enrolled_at确保教师看到的是按课程名分组、按选课时间排序的清晰列表。4. 避坑指南本地运行必踩的 4 个血泪坑与解决方案4.1 现象java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver原因MySQL 8.0 驱动类名从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver但项目pom.xml中依赖版本过低如mysql:mysql-connector-java:5.1.49。解决升级 MySQL 驱动至 8.0 版本并确认pom.xml中声明dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency同时检查DBConfig.java中Class.forName(com.mysql.cj.jdbc.Driver)是否存在新版 HikariCP 可自动加载此行可删。4.2 现象控制台输入学号后程序卡死无任何输出原因Scanner.nextLine()在前序nextInt()或next()后未消费换行符导致nextLine()读到空字符串。MainUI.java中getIntInput()方法若用scanner.nextInt()后续nextLine()必然阻塞。解决统一使用nextLine()读取所有输入并用Integer.parseInt()转换private static int getIntInput(Scanner scanner) { while (true) { try { return Integer.parseInt(scanner.nextLine().trim()); } catch (NumberFormatException e) { System.out.print(请输入有效数字); } } }4.3 现象选课成功但数据库enrollment表中status为NULL原因INSERT语句未显式指定status字段默认值未生效。enrollment表定义中status有DEFAULT pending但INSERT INTO enrollment (stu_id, offering_id) VALUES (?, ?)未包含该字段MySQL 8.0 严格模式下会报错而兼容模式下可能设为NULL。解决所有INSERT语句必须显式指定statusINSERT INTO enrollment (stu_id, offering_id, status) VALUES (?, ?, confirmed)并在建表时确认DEFAULT生效status ENUM(...) DEFAULT pending NOT NULL。4.4 现象HikariPool-1 - Connection is not available, request timed out after 30000ms.原因连接池耗尽。常见于未正确关闭ResultSet/PreparedStatement/Connection导致连接泄露。CourseSelectionService中finally块若未执行closeResources()连接永不归还。解决强制使用 try-with-resourcesJDK 7try (Connection conn DBConfig.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 执行逻辑 } // 自动关闭若需手动管理如跨多个PreparedStatement确保finally中按rs → ps → conn顺序关闭且每个close()用if (obj ! null) obj.close()包裹。5. 二次开发实战从“支持候补”到“邮件通知”的三步改造5.1 第一步为候补队列添加自动转入逻辑当前selectCourse()在容量满时调用insertWaitlist()但无后续处理。我们要实现“当有人退课时候补队列首人自动转入”。这需要监听enrollment表变更项目未用消息队列故采用数据库触发器 定时任务组合方案。Step 1创建候补表waitlistCREATE TABLE waitlist ( id INT PRIMARY KEY AUTO_INCREMENT, stu_id VARCHAR(12) NOT NULL, offering_id INT NOT NULL, queued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, FOREIGN KEY (offering_id) REFERENCES course_offering(offering_id) ON DELETE CASCADE, UNIQUE KEY uk_stu_offering (stu_id, offering_id) );Step 2修改退课逻辑触发候补检查在dropCourse()执行成功后调用新方法checkAndPromoteWaitlist(offeringId)private void checkAndPromoteWaitlist(int offeringId) throws SQLException { String sql SELECT w.stu_id FROM waitlist w WHERE w.offering_id ? ORDER BY w.queued_at ASC LIMIT 1 FOR UPDATE; // 加行锁防并发争抢 try (Connection conn DBConfig.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offeringId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { String nextStuId rs.getString(stu_id); // 尝试插入 enrollment用 ON DUPLICATE KEY 防重 String insertSql INSERT INTO enrollment (stu_id, offering_id, status) VALUES (?, ?, confirmed) ON DUPLICATE KEY UPDATE status confirmed; try (PreparedStatement psIns conn.prepareStatement(insertSql)) { psIns.setString(1, nextStuId); psIns.setInt(2, offeringId); psIns.executeUpdate(); } // 删除候补记录 String deleteSql DELETE FROM waitlist WHERE stu_id ? AND offering_id ?; try (PreparedStatement psDel conn.prepareStatement(deleteSql)) { psDel.setString(1, nextStuId); psDel.setInt(2, offeringId); psDel.executeUpdate(); } } } } }关键点FOR UPDATE锁住候补队列首行确保同一开课的多个退课事件不会争抢同一候补学生。ON DUPLICATE KEY是兜底防止网络重试导致重复插入。5.2 第二步接入简易邮件通知JavaMail API学生选课成功/退课成功/候补转入时发送邮件通知。项目不引入 Spring Mail用原生 JavaMailStep 1添加依赖dependency groupIdcom.sun.mail/groupId artifactIdjavax.mail/artifactId version1.6.2/version /dependencyStep 2编写邮件工具类public class EmailUtil { private static final String SMTP_HOST smtp.qq.com; private static final String SMTP_PORT 587; private static final String SENDER_EMAIL your_qqqq.com; private static final String SENDER_PASSWORD your_app_password; // QQ 邮箱需用独立密码 public static void sendNotification(String toEmail, String subject, String content) { Properties props new Properties(); props.put(mail.smtp.host, SMTP_HOST); props.put(mail.smtp.port, SMTP_PORT); props.put(mail.smtp.auth, true); props.put(mail.smtp.starttls.enable, true); Session session Session.getInstance(props, new Authenticator() { Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(SENDER_EMAIL, SENDER_PASSWORD); } }); try { Message message new MimeMessage(session); message.setFrom(new InternetAddress(SENDER_EMAIL)); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail)); message.setSubject(subject); message.setText(content); Transport.send(message); } catch (MessagingException e) { System.err.println(邮件发送失败 e.getMessage()); } } }Step 3在业务方法末尾调用// 在 selectCourse() 的 conn.commit() 后添加 String stuEmail new StudentDAO().getStudentEmail(stuId); EmailUtil.sendNotification(stuEmail, 【选课成功】 courseId, String.format(您已成功选修 %s开课编号%d上课时间为 %s。, getCourseTitle(offeringId), offeringId, getClassTime(offeringId)));注意QQ 邮箱需在账户设置中开启“POP3/SMTP 服务”并生成“授权码”作为SENDER_PASSWORD不可用登录密码。5.3 第三步导出课表为 ExcelApache POI学生查课表时增加“导出 Excel”选项。StudentDAO.exportTimetable()方法生成.xlsx文件public void exportTimetable(String stuId, String filename) throws SQLException { String sql SELECT c.title, co.class_time, co.location, c.credit FROM enrollment e JOIN course_offering co ON e.offering_id co.offering_id JOIN course c ON co.course_id c.course_id WHERE e.stu_id ? AND e.status confirmed ORDER BY co.class_time; try (Connection conn DBConfig.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, stuId); try (ResultSet rs ps.executeQuery()) { XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(课表); // 表头 String[] headers {课程名称, 上课时间, 上课地点, 学分}; XSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowNum 1; while (rs.next()) { XSSFRow row sheet.createRow(rowNum); row.createCell(0).setCellValue(rs.getString(title)); row.createCell(1).setCellValue(rs.getString(class_time)); row.createCell(2).setCellValue(rs.getString(location)); row.createCell(3).setCellValue(rs.getInt(credit)); } // 自动列宽 for (int i 0; i headers.length; i) { sheet.autoSizeColumn(i); } try (FileOutputStream fileOut new FileOutputStream(filename)) { workbook.write(fileOut); } } } catch (IOException e) { throw new SQLException(Excel 导出失败, e); } }参数说明filename应为绝对路径如C:/timetable_ stuId .xlsx避免相对路径导致文件写入位置不可控。XSSFWorkbook支持.xlsxExcel 2007若需.xls则用HSSFWorkbook。从那以后我每次给学生讲数据库课程设计都会先让他们跑通这个选课系统再带着他们一起改这三步——不是为了炫技而是把“事务”“触发器”“邮件集成”这些词变成键盘敲出来的、屏幕看得到的、邮箱收得到的真实反馈。当学生第一次看到自己写的waitlist触发器真的把候补学生推入课表那种眼睛亮起来的瞬间比任何 PPT 都管用。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询