ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

教务管理系统JavaWeb项目实战:从表结构到部署避坑指南

教务管理系统JavaWeb项目实战:从表结构到部署避坑指南 简介这是一个基于J2EE规范开发的教务管理系统JavaWeb项目面向正在学习Servlet、JSP、MVC设计模式与DAO模式的初中级JavaWeb开发者可用于课程设计、毕业设计或日常练习。系统覆盖登录认证、找回密码、修改密码、注销以及学生端的成绩查询、选修与考级报名、学籍信息维护教师端个人信息查询教务员的成绩管理与报名名单查看管理员端用户管理等核心业务模块结构上按学生、教师、教务员、系统管理员四类角色划分便于理解权限设计与业务分层。资源共223个文件以Java源码、Servlet类文件、JSP页面为主另有jar依赖包、SQL建库脚本、XML配置及项目配置文件压缩包仅2.6MB解压后配合Tomcat 7.0.84与JDK 1.8即可运行。目前已有11656人学习适合用来对照参考分层架构、请求转发与数据库交互的实现思路README中也提供了启动说明后续还可自行扩展选课管理、考级模块等功能。1. 教务管理系统 javaweb 项目课设代码里藏着的真实加分项不少同学把「教务管理系统 javaweb 项目」当成又一个毕业设计模板生成的代码千篇一律JSP 里塞查询、Servlet 里拼 SQL、数据库密码明文写在 jdbc.properties。可真正拿去答辩时老师一眼就能看出这项目是照搬还是自己吃透了。这门课设的价值不是「跑通增删改查」而是让你把 JavaWeb 最核心的那几条链路——请求路由、参数封装、事务边界、状态保持、SQL 注入防线——亲手过一遍。适合正在做课程设计、准备实习面试项目的计算机类专业学生也适合想重新捡起 Servlet/JSP 技术栈的从业者。本文从一个可运行的教务管理系统落地角度出发带你从表结构设计一路走到部署排错把隐藏的加分项逐个扣出来。2. 先把地基打对教务系统的技术选型与表结构设计2.1 JavaWeb 技术栈怎么选才不会被答辩老师问倒「教务管理系统 javaweb 项目」最稳妥的技术组合是 Servlet 3.0 JSP JSTL MySQL 5.7/8.0 Tomcat 9。这套组合的好处是每一层都有明确的规范可查答辩时老师问你「请求怎么从页面走到数据库的」你能一条线讲清楚浏览器发出 HTTP 请求Tomcat 根据 web.xml 或注解找到对应的 ServletServlet 调用 Service 层Service 调 DAO 层访问 MySQL返回结果通过请求转发交给 JSP 渲染。链路短、角色清晰比一上来就上 Spring Boot 更能在课设里展示你对 Web 底层的理解。这里有一个常见的选型误区很多人为了「看起来高级」硬上 Spring Boot MyBatis Plus结果连自动配置原理都没弄懂答辩被问「为什么 MyBatis 的 mapper 接口没有实现类就能调用」直接卡壳。而纯 JavaWeb 项目的每一个组件都要求你写清楚项目的知识含量反而更饱满。我一般建议Servlet 用注解方式WebServlet而不是 web.xml 配置省掉繁琐的 XML同时不影响理解「请求映射」这个核心概念JSP 只做视图渲染业务逻辑全部放在 Service 层避免出现 JSP 里写 JDBC 这种答辩时让人尴尬的代码。2.2 五张核心表的设计从学生到选课成绩的闭环教务管理系统的业务核心是「学生—课程—选课成绩—教师—公告」五张表就能闭环。学生表student存放学号、姓名、性别、入学年份、所在班级教师表teacher存放工号、姓名、职称、所属院系课程表course存放课程编号、课程名称、学分、授课教师ID、选课人数上限选课表sc作为学生和课程的关联表额外携带成绩字段和选课时间公告表notice存放标题、内容、发布时间、发布人。这个模型是绝大多数课设项目的骨架增删改查和业务规则都围绕这几张表展开。建表时有两个细节值得抠。第一选课表的主键用自增ID还是联合主键我建议用自增ID做主键为学生和课程ID各建一个普通索引。虽然联合主键能保证不重复选课但在 MySQL 的 InnoDB 引擎下联合主键的二级索引存储开销和死锁概率都比自增主键高对课设演示来说得不偿失。第二成绩字段类型别用 FLOAT用 DECIMAL(5,2)。搞教务系统的都知道FLOAT 存在精度丢失学生成绩查出来 89.999999 这种数据在答辩现场展示一次就社死了。CREATE DATABASE IF NOT EXISTS edu_admin DEFAULT CHARSET utf8mb4; USE edu_admin; CREATE TABLE student ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1男 2女, enroll_year SMALLINT NOT NULL COMMENT 入学年份, class_name VARCHAR(50) DEFAULT NULL COMMENT 班级, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在读 2休学 3毕业, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_class (class_name) ) ENGINEInnoDB COMMENT 学生表; CREATE TABLE teacher ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, teacher_no VARCHAR(20) NOT NULL UNIQUE COMMENT 工号, name VARCHAR(50) NOT NULL, title VARCHAR(30) DEFAULT 讲师 COMMENT 职称, dept VARCHAR(50) NOT NULL COMMENT 院系, phone VARCHAR(20) DEFAULT NULL ) ENGINEInnoDB COMMENT 教师表; CREATE TABLE course ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) NOT NULL DEFAULT 2.0 COMMENT 学分, teacher_id INT UNSIGNED NOT NULL COMMENT 授课教师, max_students INT UNSIGNED NOT NULL DEFAULT 50 COMMENT 选课人数上限, selected_count INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 已选人数, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINEInnoDB COMMENT 课程表; CREATE TABLE sc ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_id INT UNSIGNED NOT NULL, course_id INT UNSIGNED NOT NULL, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩未录入为NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB COMMENT 选课成绩表; CREATE TABLE notice ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(100) NOT NULL, content TEXT NOT NULL, publisher VARCHAR(50) NOT NULL, publish_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT 公告表;这里是按照「先有业务闭环、后有表结构」的顺序设计的student 与 teacher 互不引用course 引用 teacher 的外键sc 引用 student 与 course 的外键。外键约束保留了但连接池里跑的都是渲染层生成的 SQL不会出现跨表 JOIN 写错导致的外键错误。UNIQUE KEY uk_student_course在数据库层防了重复选课这比在 Java 代码里先 SELECT 再 INSERT 的「防重」思路可靠得多——多线程并发选课时代码里的检查根本不安全数据库唯一索引才是唯一的保证。2.3 建一个 anyone 能跑通的连接池配置MySQL 8 与 Tomcat 9 的兼容细节很多人在这里第一次「翻车」本机 MySQL 5.7 好好的换到 MySQL 8 就连接报错Public Key Retrieval is not allowed。原因不复杂MySQL 8 默认使用 caching_sha2_password 认证插件JDBC 驱动首次连接需要从服务器获取公钥进行 RSA 加密传输密码而连接串没告诉驱动「允许获取公钥」。解决方式是在 JDBC URL 上拼两个参数allowPublicKeyRetrievaltrueuseSSLfalse。用 Tomcat 9 mysql-connector-java 8.0.x 时我一般直接在项目里用连接池而不是每次DriverManager.getConnection()。连接池的好处不用多说对课设项目来说更重要的是它能帮你暴露一个典型错误连接没关。如果你发现跑了几次操作后页面越来越慢、到最后 Tomcat 直接报Too many connections那就是 DAO 里的connection.close()写漏了。用连接池时 close 是归还连接不是真的断开但这行代码漏掉池里的连接会被耗尽。jdbc.urljdbc:mysql://localhost:3306/edu_admin?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码 jdbc.driverClassNamecom.mysql.cj.jdbc.Driver jdbc.initialSize5 jdbc.maxTotal20 jdbc.maxIdle10 jdbc.maxWaitMillis3000serverTimezoneAsia/Shanghai不能随便删。MySQL 8 驱动要求显式指定时区否则报 CST 时区识别歧义错误。characterEncodingutf8管的是从数据库读出和写入的字符集映射JSP 页面也要保持一致否则中文乱码会从数据库一路乱到浏览器。maxWaitMillis3000表示拿不到连接时最多等 3 秒超过就抛异常——这个参数在演示现场很关键学生同时在线上百人时它能防止页面无限挂起。3. 从登录到选课业务链路的逐层落地3.1 登录会话与安全设计为什么不能用 MD5 直接存密码教务管理系统的登录入口有两类用户管理员和学生。管理员账号存 admin 表学生就是 student 表。登录校验逻辑抽成一个 LoginServlet成功后把用户ID、姓名、角色写进 SessionJSP 页面在头部判断 Session 里有没有 user 对象没有就跳回登录页。这个流程本身不难难的是安全细节。密码存储绝对不要用裸 MD5。虽然答辩老师可能不查这一步但你的项目里留一个「密码直接明文放在数据库」的坏味道内行扫一眼就看到了。常见做法是加盐用户注册或首次初始化时生成一个随机盐值存进数据库密码字段存的是MD5(盐值 原始密码)的结果。校验时取出该用户的盐重新拼接计算比对摘要。在课设项目里用 SHA-256 也行但记住一点哈希算法不等于加密算法哈希是不可逆的你比对的是摘要而非解密原文。public class PasswordUtil { private static final String SALT_CHARS abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789; public static String generateSalt() { SecureRandom random new SecureRandom(); StringBuilder sb new StringBuilder(16); for (int i 0; i 16; i) { sb.append(SALT_CHARS.charAt(random.nextInt(SALT_CHARS.length()))); } return sb.toString(); } public static String hashWithSalt(String rawPassword, String salt) { String input salt rawPassword; return DigestUtils.md5DigestAsHex(input.getBytes(StandardCharsets.UTF_8)); } public static boolean verify(String rawPassword, String salt, String storedHash) { return storedHash.equals(hashWithSalt(rawPassword, salt)); } }SecureRandom用的是操作系统提供的熵源比Math.random()这种伪随机安全得多。DigestUtils.md5DigestAsHex是 Spring 里的工具如果你坚持不带 Spring 依赖就用MessageDigest.getInstance(MD5)手动实现,代码量差不了一行。盐值长度固定为 16保障每个用户的摘要都不一样——两个用户密码相同数据库里存的哈希也不相同这样不会被反向彩虹表击中。登录成功后的 Session 也有讲究登录之前用request.getSession()拿旧会话登录成功后必须session.invalidate()再重新request.getSession(true)这招叫「会话固定攻击防护」。攻击者在公共电脑上先访问你的登录页得到一个会话ID然后诱导用户用这个会话ID登录登录成功后攻击者就能拿着相同ID共享会话——重新生成ID直接把这个坑填了。3.2 学生管理模块的 CRUD 骨架分页、批量启停与校验逻辑学生管理是教务系统的门面模块面试官或答辩老师多半先点这个菜单。除了最基本的新增、编辑、删除你应该给它加分项分页查询、姓名/学号模糊搜索、批量启用停用。这四件事做完学生管理模块就不再是「课设凑数代码」了。分页的 SQL 写法是LIMIT offset, rowsoffset 等于(page-1) * rows。注意LIMIT后面不能直接拼用户传来的页码——不是性能问题是 SQL 注入风险。页码参数一定要先Integer.parseInt解析解析失败就默认第一页。DAO 层再写一个SELECT COUNT(*)查总记录数Service 层把当前页数据、总页数、总记录数打包进一个 PageBean。翻页的 JSP 端还要隐藏当前页数、搜索关键词两个参数否则翻到第 3 页后搜索条件就丢了。public ListStudent pageQuery(String keyword, int page, int rows) { StringBuilder sql new StringBuilder( SELECT id, student_no, name, gender, enroll_year, class_name, status FROM student WHERE 11 ); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (student_no LIKE ? OR name LIKE ?) ); params.add(% keyword.trim() %); params.add(% keyword.trim() %); } sql.append(ORDER BY student_no LIMIT ?, ?); params.add((page - 1) * rows); params.add(rows); return jdbcTemplate.query(sql.toString(), params.toArray(), studentRowMapper); }这段代码里注意「WHERE 11」的写法——它看起来像偷懒实际目的是后面 AND 条件可以用同一个sql.append逻辑拼上去省掉「第一个条件要不要加 WHERE」的分支判断。危险在于如果 keyword 直接拼进 SQL 字符串这里就是注入点。参数全部用?占位符传给 PreparedStatement%是拼接在参数值里的MySQL 的 LIKE 模糊匹配才会生效。校验逻辑要放在 Servlet 层还是 Service 层我建议在 Service 层做而且要「先查重、再插入」。新增学生时先用student_no查一条SELECT 1 FROM student WHERE student_no? LIMIT 1存在就抛业务异常编辑时则要排除自己也就是SELECT 1 FROM student WHERE student_no? AND id?。因为业务异常和数据库异常要区分开Service 层抛的是自定义ServiceExceptionServlet 里 catch 住后通过request.setAttribute(error, e.getMessage())带回到表单页回显。批量启停更简单接收一组 id 数组执行UPDATE student SET status? WHERE id IN (?,?,?)IN 的占位符数量要根据 id 数量动态生成——这里又是拼 SQL 的常见失误点写成WHERE id IN ?在 MySQL 里会直接报语法错误。3.3 选课业务的两个隐藏规则容量校验与成绩的 NULL 语义选课是整个系统里业务规则最复杂的一个操作也是最容易被课设项目做成「INSERT 一条记录就完事」的模块。真实教务系统里选课要检查三门事学生存在且状态在读、课程存在且没有选过、课程已选人数没超过上限。前两条可以靠数据库唯一索引和检查代码解决第三条必须在同一事务里处理否则就有并发超卖。常见做法是在 SC 表插入前先用SELECT selected_count FROM course WHERE id? FOR UPDATE锁住课程行在 Java 代码里判断selectedCount maxStudents成立则执行插入并更新已选人数。用悲观锁的原因很简单你现在没有 Redis也没有分布式锁在数据库支持事务的前提下SELECT FOR UPDATE是最可靠的防超卖手段。代码执行完别忘了在 finally 块里 return否则连接不归还连接池会被耗尽。Transactional public void selectCourse(Long studentId, Long courseId) { Course course courseDao.getForUpdate(courseId); if (course.getSelectedCount() course.getMaxStudents()) { throw new ServiceException(课程 course.getCourseName() 选课人数已满); } int inserted scDao.insert(studentId, courseId); if (inserted ! 1) { throw new ServiceException(该课程你已经选过了不能重复选课); } courseDao.increaseSelectedCount(courseId); }逻辑说明getForUpdate里执行的是SELECT * FROM course WHERE id? FOR UPDATE这一步拿到行级排他锁其他事务在此刻对该课程行的更新和加锁操作都会被阻塞直到当前事务提交。insert返回 0 是因为唯一索引uk_student_course触发了 DuplicateKeyException——注意插入重复记录时 MySQL 不是返回 0而是抛异常所以严格写法里要 catch 住DuplicateKeyException再转成业务异常否则默认的异常处理器会把 500 页面甩给用户。成绩字段用 NULL 表示「未录入」而不是 0——这是很多人第一次写教务系统时候的「血泪经验」。成绩录入页面要把 NULL 和 0 区分开录入 0 分是真考了零分NULL 是还没录。JSP 渲染成绩单元格时c:if test${sc.score ! null}控制显示「未录入」还是具体分数。统计平均分和及格率时SQL 里AVG(score)自动忽略 NULL不会把没考试的选课记录当成 0 分拖低平均分。3.4 公告模块里的时间陷阱JSP 日期格式化与时区不一致公告列表这个模块容易被忽略但踩坑概率很高。公告表里publish_time用的是DATETIME DEFAULT CURRENT_TIMESTAMPJava 端ResultSet.getTimestamp()取出来要传给 JSP 显示。问题出在 JSP 里直接用toString()输出时间戳或者用fmt:formatDate没指定timeZone导致显示的日期和数据库里差 8 个小时——因为 MySQL 驱动默认把 DATETIME 按 JVM 本地时区取而 JVM 时区可能被容器设置成 UTC。正确做法是列表 SQL 里就不要取原始时间戳而是用 MySQL 的DATE_FORMAT(publish_time, %Y-%m-%d %H:%i:%s)格式化成字符串直接以 String 传到 JSP 显示。这样时区问题直接从源头消失代码也省掉一层 SimpleDateFormat 转换。如果是展示「今天发布的公告」这类日期条件SQL 里用DATE(publish_time) CURDATE()而不是在 Java 里手动比较字符串——后者碰上夏令时切换、月份进位、跨年会给你秀一波智商下限。公告追加接口里发布人从 Session 取而不是让前端传过来防止有人伪造发布人字段。SELECT id, title, DATE_FORMAT(publish_time, %Y-%m-%d %H:%i) AS publish_time_str, publisher FROM notice ORDER BY publish_time DESC LIMIT 10;LIMIT 10是首页公告的典型写法只取最近十条让页面保持清爽。DATE_FORMAT的具体格式里如果你写成%H24小时制而你的业务想显示上午/下午就换成%h %p格式。这里用 AS 别名在 Java 里就能直接rs.getString(publish_time_str)不用再管原字段的 Timestamp 类型——这个细节就是课设代码显得「老练」和「学生气」的分界线。4. 导入导出与批量操作让课设项目亮起来的实战功能4.1 用 Apache POI 实现学生名单的 Excel 导入纯增删改查的教务系统实在太常见导入导出能让你从同质化的课设里「冒头」。学生名单导入是个很有代表性的场景教务处手里有 Excel 表格按格式填好了学号、姓名、性别、班级你要把这个文件上传到 Web 系统里解析每一行批量写入数据库。技术选型用 Apache POI 的 XSSFWorkbook 处理 .xlsxHSSFWorkbook 处理 .xls。两种格式的 API 略有差异为了兼容性一般先根据文件名后缀判断用哪个实现类。POI 解析流程固定四步WorkbookFactory.create(inputStream)统一创建、拿到第一个 Sheet、遍历每一行拿 Cell、按单元格类型取值。try (Workbook workbook WorkbookFactory.create(inputStream)) { Sheet sheet workbook.getSheetAt(0); // 从第1行开始第0行通常是表头 for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getCell(i); if (row null) continue; String studentNo getCellStringValue(row.getCell(0)); String name getCellStringValue(row.getCell(1)); if (studentNo null || studentNo.trim().isEmpty()) { errors.add(第 (i 1) 行: 学号为空); continue; } // 批量插入 } } catch (Exception e) { // 文件格式损坏/版本不兼容/数据列数缺失 }4.2 分批插入与事务边界导入一万条数据不再撑爆连接池导入模块的性能问题一千行和一万行完全是两个世界。最简单朴素的方案是一条条 INSERT一万条跑下来数据库连接被反复获取释放即便有连接池也要额外耗掉大量时间。常见做法是改造成「分批提交」每 500 条执行一次batchUpdate每批次提交一个事务。PreparedStatement 的addBatch()/executeBatch()可以把多次网络往返压缩成一次MySQL 驱动在 URL 后加rewriteBatchedStatementstrue还能进一步把多条 INSERT 重写成一条多 VALUES 的语句。事务边界的控制要格外小心课设项目里最容易出现的问题是写完循环提交后才发现中间某条数据被外键约束挡住导致批量导入成功一半。解决方式是把「收集所有行数据 → 统一校验 → 分批插入」拆成三个大阶段数据全部合法才真正动数据库。校验时学生存在性用一条SELECT student_no FROM student WHERE student_no IN (?,?,...)把已有学号一次性查出放进 Set然后逐行判断。导入结束返回一个统计结果对象显示成功多少条、失败多少条、每行失败原因——这个反馈机制才是导入功能让人愿意用的关键。public ImportResult importStudents(ListStudentExcelRow rows) { ImportResult result new ImportResult(); ListString invalidRows new ArrayList(); // 1. 预校验 SetString existingNos studentDao.findExistingNos( rows.stream().map(r - r.getStudentNo()).collect(Collectors.toList())); for (int i 0; i rows.size(); i) { StudentExcelRow row rows.get(i); if (existingNos.contains(row.getStudentNo())) { invalidRows.add(第 (i 1) 行学号重复); continue; } if (row.getName() null || row.getName().trim().isEmpty()) { invalidRows.add(第 (i 1) 行姓名为空); continue; } // 其他字段校验 } // 2. 分批插入 if (invalidRows.isEmpty()) { studentDao.batchInsert(rows); result.setSuccessCount(rows.size()); } else { result.setFailedMessages(invalidRows); } return result; }4.3 文件上传的两个坑Tomcat 临时目录 vs 自定义存储路径文件上传除了 POI 解析还有一层容易被忽视的坑上传文件用request.getPart(file)获取时Tomcat 会把文件先写入临时目录这个目录默认为$CATALINA_BASE/work/Catalina/localhost/项目名。如果你的项目目录是系统盘临时空间很小的位置大文件上传时会被 Tomcat 的maxSwallowSize限制拦下来抛FileUploadBase$SizeLimitExceededException。我一般会在MultipartConfig注解里显式指定location属性到一个自定义目录比如项目根下的upload_temp。但要注意这个目录是「磁盘临时写入目录」不是「最终存储目录」。最终存储路径另外存在配置里上传完成后立刻用File.renameTo()移动到真正的位置。处理文件时我们通常追求逻辑性MultipartConfig(maxFileSize 10 * 1024 * 1024, maxRequestSize 50 * 1024 * 1024) WebServlet(/upload/student) public class UploadStudentServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) { Part filePart request.getPart(file); String fileName Paths.get(filePart.getSubmittedFileName()).getFileName().toString(); // 判断扩展名 if (!fileName.endsWith(.xlsx) !fileName.endsWith(.xls)) { request.setAttribute(error, 仅支持 Excel 文件); request.getRequestDispatcher(/admin/import.jsp).forward(request, response); return; } try (InputStream is filePart.getInputStream()) { ImportResult result studentService.importFromExcel(is); request.setAttribute(result, result); } catch (Exception e) { request.setAttribute(error, e.getMessage()); } request.getRequestDispatcher(/admin/import_result.jsp).forward(request, response); } }4.4 成绩导出避免导出 2 万行时浏览器内存溢出导出模块相对简单但也要注意一个边界场景成绩表导出全部数据时如果课程多、学生多一次性查出两万行再写 Excel内存和响应时间都会失控。对课设系统来说成绩导出按课程筛选是符合真实使用习惯的。导出功能要接受一个courseId参数按课程查选课表 JOIN 学生表只有该课程选课记录和成绩会落进导出文件。这样既保证数据量可控也让导出的文件结构更清晰。POI 写 Excel 时单元格样式尽量少设置。每一行都手动创建 CellStyle 会导致文件变大 10 倍以上而且是毫无必要的——只有表头用粗体样式数据行纯文本即可。还有一点导出文件的文件名不要写死拼接当前日期时间export_score_20250101_1530.xlsx这样用户在接收多个文件时能分清版本。try (Workbook workbook new XSSFWorkbook()) { Sheet sheet workbook.createSheet(成绩单); Row header sheet.createRow(0); String[] cols {学号, 姓名, 课程名称, 学分, 成绩}; for (int i 0; i cols.length; i) header.createCell(i).setCellValue(cols[i]); int rowIdx 1; for (ScoreVO vo : scoreList) { Row row sheet.createRow(rowIdx); row.createCell(0).setCellValue(vo.getStudentNo()); row.createCell(1).setCellValue(vo.getStudentName()); row.createCell(2).setCellValue(vo.getCourseName()); row.createCell(3).setCellValue(vo.getCredit()); row.createCell(4).setCellValue(vo.getScore() null ? 未录入 : vo.getScore().toString()); } // 设置响应头 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filename URLEncoder.encode(fileName, UTF-8)); workbook.write(response.getOutputStream()); }5. 运行、部署与避坑指南三小时从源码到本地跑通5.1 IDEA 运行 JavaWeb 项目的配置清单Artifact 与 Tomcat 的关联很多人第一次在 IDEA 里跑 JavaWeb 项目卡在「明明代码没问题但浏览器 404」这一步。背后的原因是 Artifact 没配对。IDEA 里新建 Web 项目后Artifact 默认是exploded类型输出到out\artifacts\项目名_war_exploded。配置 Tomcat 时要在 Deployment 标签页把 Artifact 加进去还要设置 Application context也就是访问路径的根上下文。常见的配置组合是Application context填/edu-admin访问首页就是http://localhost:8080/edu-admin/index.jsp。在启动之前还要检查一下 Project Structure 里的 Libraries 有没有把mysql-connector-java.jar、jstl.jar、standard.jar加进去。如果你用的是 Maven 项目pom.xml里依赖对了还不够要确认这些依赖有没有打进 Artifact——在 Artifact 的 Output Layout 标签页里能看到一个Available Elements列表依赖会在这里显示右键把它放进WEB-INF/lib目录下。这一步是 IDEA 跑 JavaWeb 最经典的「翻车」现场代码编译通过但运行时ClassNotFoundException: com.mysql.cj.jdbc.Driver因为 jar 没打进去。5.2 MySQL 5.7 与 8.0 的驱动差异一个错误引发的连带排查连接数据库时报java.sql.SQLException: Unknown database edu_admin时第一反应不是去检查CREATE DATABASE有没有执行而是先看连接串里的数据库名和用户名密码对不对——但还有一层可能被忽略你本机装的是 MySQL 8驱动用的还是 5.x 版本。MySQL 5.x 驱动即使硬连 8.0 也能连上但会报一个警告Establishing SSL connection without servers identity verification is not recommended。正确姿势是驱动版本和数据库版本对应MySQL 5.7 用 5.1.49 或 8.0.x 都能连但 MySQL 8.0 就只能用 8.0.x 驱动。如果你在答辩现场才临时发现数据库版本不匹配最快的处理办法是pom.xml里改mysql-connector-java版本为8.0.33然后 Reimport 一次重新启动。这种事情定要在演示前一天自己先验一遍别把宝押在「应该没问题」上。5.3 登录 Session 频繁失效为什么每次刷新页面都要重新登录纯 JSP 项目里有一个特有现象Tomcat 重启后所有用户的 Session 全部失效——这很好理解Session 默认存在 Tomcat 内存里重启即清空。但还有一种情况会让用户无感IDEA 里改完 JSP 或类文件Tomcat 触发了自动重载reload此时所有会话也丢了。如果你需要在开发期保留登录状态把 Tomcat 的 autoReload 关掉或者改用update resources而不是 restart server。另外要检查 Cookie 的有效期。默认情况下 Session Cookie 是浏览器会话级别的关掉浏览器再打开就要重新登录。如果希望学生端记住登录状态可以给 Cookie 设置setMaxAge(30*24*60*60)并手动把 Session ID 写进这个 Cookie。但要注意这样做以后学生换台电脑直接拿这个 Cookie 就能以原身份登录等同于「万能钥匙」。我建议课设项目不要做这个功能除非你要展示的正是「记住我 Token 失效时间」这套进阶机制。5.4 常见部署问题排查表按现象对照原因现象常见原因解决方式访问页面报 404Artifact 没部署或 context path 配错IDEA Deployment 标签页检查 artifact 是否添加、路径是否含项目名Tomcat 能启动但页面报 500Servlet 类找不到或 jar 缺失检查 Artifact Output Layout 里 WEB-INF/lib 是否有所有依赖数据库连接被拒用户名密码错 / 端口非 3306 / 驱动版本过旧先在 Navicat 里测连接再用 netstat -ano中文乱码JSP 没设 pageEncoding / 数据库字符集不对JSP 页面顶部加 contentType 和 pageEncodingJDBC URL 加 useUnicode 和 characterEncoding表单提交后中文变问号请求没有指定 UTF-8 编码Servlet 里request.setCharacterEncoding(UTF-8)必须放在读取任何参数之前上传大文件报 SizeLimitExceededMultipartConfig的 maxFileSize 太小调大 maxFileSize 与 maxRequestSize并确认临时目录有足够空间6. 进阶方向与验证方法从课设项目走向工程意识如果你还有余力把「高并发选课」这个场景往深做一层项目含金量会直接上一个台阶。目前的SELECT FOR UPDATE方案在单机 Tomcat、单库 MySQL 下没问题但真实教务系统的并发量远超这个模型。常见做法是引入 Redis 的课程库存预减选课开始前把maxStudents预加载进 Redis用DECR原子递减减到 0 直接拒绝选课成功后写一个消息队列异步落库数据库只做最终一致性校验。这个改造涉及 Redis、消息队列、异步任务三块做对任何一个都能在项目描述里多写一行「具备高并发下的库存防超卖设计」。另一个值得补的方向是操作审计。当前系统里谁在什么时候把成绩从 85 改成 95数据库不会留下痕迹。加一张operate_log表AOP 或过滤器里拦截每一次写操作记录操作人、操作时间、操作类型、变更前后快照。课设项目做到这一步已经不是「作业」的水平了而是一套可追溯日志体系的最小实现。把你最终做出来的项目放进自己电脑的本地仓库用 Git 从第一天就管理起来。每次功能完成打一个 tag答辩的时候把提交记录展示出来——这比任何文字描述都能证明项目是你亲手从零写的。当你把项目推到 JavaWeb 技术栈的体系里重新审视时会发现代码组织方式、异常处理层次、SQL 写法、部署思维每一层都还有优化空间多改一版你离一个合格的 Java 工程师就更近一步。我就是这么干的第一次做教务系统课设时连连接池和预处理语句都没听说过后来把这些踩过的坑一个一个补上再去面实习岗位时面试官问「JavaWeb 项目你最大收获是什么」我能掰着手指跟他讲二十分钟。希望帮到你。本文还有配套的精品资源点击获取
返回列表