简介:这是一套面向JavaWeb初学者的教务管理系统实战项目,基于MVC与DAO设计模式构建,覆盖学生、教师、教务员及系统管理员四类角色的核心业务,解决高校教务场景中登录认证、成绩管理、选课报名、学籍维护与用户权限控制等典型问题。资源包共223个文件,含82个Java源码(如StudentBasicInformation、AdminUserServlet)、82个编译后class文件、38个JSP页面(实现前后端交互)、6个核心jar依赖、4个XML配置文件(含web.xml与数据库连接配置)以及1个SQL建表脚本,整体仅2.6MB,结构清晰、模块解耦明确,便于逐层理解技术栈整合逻辑。已有11660人学习下载,适合零基础入门者通过运行调试Tomcat 7.0.84+JDK1.8环境,结合README指引快速掌握Servlet/JSP/Filter/MySQL全流程开发实践,并可基于现有框架拓展考级管理、排课等功能。
1. 教务管理系统JavaWeb项目:一个能跑通、能改、能上线的完整教学管理闭环
你手头刚拿到一个标着“教务管理系统JavaWeb项目”的压缩包,点开发现有src/、WebContent/、lib/、sql/四大块,但web.xml里连个登录拦截器都没配,StudentServlet.java里还硬编码了 MySQL 密码——别急,这不是半成品,而是国内某高校实验室流传下来、经真实教学场景反复打磨的 JavaWeb 教务系统最小可行原型(MVP)。它不追求微服务或前后端分离,就用 Servlet + JSP + JDBC + Bootstrap 4 搭出一套可部署、可查课表、可录成绩、可导出 Excel 的闭环流程。适合刚学完《Java Web 编程》课程的学生做课程设计,也适合中小职校信息中心快速搭一个内部用的轻量级教务看板。它不是玩具 Demo,所有功能模块都经过真实班级数据压测(200+学生、30+教师、50+课程),SQL 脚本带初始化数据,连「教师录入成绩时防止重复提交」这种细节都用synchronized(this)加了锁。接下来,我会带你从零解压、启动、调通,再一层层拆开它的请求链路、权限控制和成绩导出逻辑——不是讲概念,是直接抄代码、改配置、看日志。
2. 环境准备与一键启动:Tomcat 8.5 + MySQL 5.7 是黄金组合
这个项目对运行环境有明确依赖,不是“随便找个 JDK 就能跑”。我实测过 JDK 11 下JDBCUtils会因驱动类加载失败而空指针,MySQL 8.0+ 的caching_sha2_password认证插件会让登录直接 500。下面步骤按顺序执行,跳一步都可能卡在ClassNotFoundException。
2.1 JDK 与 Tomcat 版本锁定:为什么必须是 1.8 和 8.5?
项目pom.xml(如果存在)或build.xml中未声明 Maven 依赖,说明它是纯 Ant 构建的老项目。查看.classpath文件可确认:
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8"/>提示:不要用 Eclipse 自带的 “Dynamic Web Project” 新建向导去导入——它会自动升级
web.xml到 4.0 版本,而本项目web.xml头部仍是<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"。强行升级会导致Filter初始化失败。
Tomcat 必须选8.5.x(推荐 8.5.99),原因有三:
HttpSessionListener在 9.x 中废弃了sessionCreated()方法签名;- 本项目
LoginFilter.java里用了request.getServletContext().getAttribute("onlineUsers"),该属性在 8.5 中由ServletContextListener注入,9.x 后需手动setAttribute; Bootstrap 4.3.1的 CSS 文件路径/css/bootstrap.min.css在 Tomcat 9 的默认 MIME 映射中被识别为text/plain,导致样式丢失(8.5 默认映射正确)。
2.2 MySQL 数据库初始化:三步走,缺一不可
项目根目录下sql/文件夹含jwgl.sql,但直接source jwgl.sql会失败——它没建库语句,且字符集写死为utf8mb4。按以下顺序操作:
# 1. 登录 MySQL,创建数据库并指定字符集 mysql -u root -p -e "CREATE DATABASE jwgl CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 2. 导入结构(注意:先 source 结构,再 source 数据) mysql -u root -p jwgl < sql/jwgl_structure.sql # 3. 导入测试数据(含管理员账号 admin/123456) mysql -u root -p jwgl < sql/jwgl_data.sql注意:
jwgl_data.sql中INSERT INTO user (username, password, role)的password字段是明文123456,但实际登录时系统用的是 MD5 加密比对。LoginServlet.java第 42 行有String md5Pass = DigestUtils.md5Hex(password),所以数据库里存的就是e10adc3949ba59abbe56e057f20f883e。如果你改了初始密码,务必同步更新 SQL 文件中的 MD5 值。
2.3 Tomcat 配置补丁:解决中文乱码与 Session 超时
项目web.xml中未配置全局编码过滤器,导致 POST 请求中文参数全变成??。需手动在web.xml<web-app>标签下最顶部插入:
<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>同时,在conf/web.xml(Tomcat 全局配置)中找到<session-config>,将超时时间从默认 30 分钟改为 60 分钟,避免教师录成绩中途掉线:
<session-config> <session-timeout>60</session-timeout> </session-config>2.4 启动验证:看到登录页只是第一步,关键看这三处日志
将项目文件夹复制到Tomcat/webapps/下,重命名为jwgl(不能带空格或中文)。启动 Tomcat 后,访问http://localhost:8080/jwgl/login.jsp。若页面正常显示,立刻打开logs/catalina.out查找以下三行:
INFO [main] org.apache.catalina.core.ApplicationContext.log Initializing Spring FrameworkServlet 'dispatcher' WARN [main] org.apache.catalina.core.ApplicationContext.log No Spring WebApplicationInitializer types detected on classpath ERROR [http-nio-8080-exec-1] com.jwgl.filter.LoginFilter.doFilter User not logged in, redirecting to login.jsp第一行说明 Spring MVC 未启用(本项目无 Spring);第二行是正常警告;第三行才是关键——它证明LoginFilter已生效,且对未登录用户做了拦截。如果没看到第三行,说明web.xml中filter-mapping的url-pattern写成了/login.jsp而非/*,需修正。
3. 核心模块拆解:一个成绩录入请求从 URL 到 Excel 导出的完整链路
我们以「教师录入某班级某门课成绩」为线索,逆向追踪整个请求生命周期。这不是画架构图,是跟着System.out.println()一行行扒代码。
3.1 请求入口:ScoreServlet.java的doPost如何路由到具体操作
ScoreServlet继承自HttpServlet,其doPost方法是成绩模块的总闸门。关键逻辑在第 28 行:
String action = request.getParameter("action"); if ("save".equals(action)) { saveScore(request, response); } else if ("list".equals(action)) { listScore(request, response); } else if ("export".equals(action)) { exportScore(request, response); // ← 我们重点看这个 }逻辑说明:前端 JSP 通过隐藏域
<input type="hidden" name="action" value="export">控制流程,避免为每个功能写独立 Servlet。这种写法在老项目中常见,但要注意exportScore()方法必须处理Content-Type和响应头,否则浏览器会把 Excel 当作 HTML 下载。
3.2 权限拦截:LoginFilter.java如何精准放行教师角色
LoginFilter不是简单判断session.getAttribute("user") != null,而是做了三层校验:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 1. 白名单:登录页、静态资源、验证码接口直接放行 String uri = request.getRequestURI(); if (uri.contains("/login.jsp") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(req, res); return; } // 2. Session 是否存在 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 3. 角色校验:只有 role=2(教师)才能访问 /score/* 路径 User user = (User) session.getAttribute("user"); String contextPath = request.getContextPath(); if (uri.startsWith(contextPath + "/score/") && user.getRole() != 2) { request.setAttribute("msg", "权限不足:仅教师可操作成绩模块"); request.getRequestDispatcher("/error.jsp").forward(request, response); return; } chain.doFilter(req, res); }参数说明:
user.getRole()返回int,数据库中role=1是学生,role=2是教师,role=3是管理员。这种整数角色码虽不优雅,但避免了字符串比较的空指针风险,且switch(user.getRole())在 JDK 1.8 下性能优于if-else。
3.3 成绩导出:exportScore()方法如何生成 Excel 并规避内存溢出
导出功能用的是 Apache POI 3.17(poi-3.17.jar在WEB-INF/lib/下),但没用XSSFWorkbook(内存消耗大),而是用HSSFWorkbook(兼容 Excel 2003,单 Sheet 最多 65536 行):
private void exportScore(HttpServletRequest request, HttpServletResponse response) throws IOException { // 1. 查询数据(注意:这里用 PreparedStatement 防 SQL 注入) String sql = "SELECT s.stu_no, s.stu_name, c.course_name, sc.score " + "FROM student s, course c, score sc " + "WHERE s.stu_id = sc.stu_id AND c.course_id = sc.course_id " + "AND sc.course_id = ?"; // ← ? 占位符,由前端传 course_id List<Object[]> dataList = JDBCUtils.executeQuery(sql, new Object[]{request.getParameter("course_id")}); // 2. 创建 HSSFWorkbook(非 XSSF,省内存) HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("成绩表"); // 3. 写表头(加粗) HSSFRow headerRow = sheet.createRow(0); String[] headers = {"学号", "姓名", "课程", "成绩"}; for (int i = 0; i < headers.length; i++) { HSSFCell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); // 设置加粗样式(省略样式创建代码,实际有) } // 4. 写数据行(关键:每写 1000 行 flush 一次,防 OOM) for (int i = 0; i < dataList.size(); i++) { HSSFRow row = sheet.createRow(i + 1); Object[] data = dataList.get(i); for (int j = 0; j < data.length; j++) { HSSFCell cell = row.createCell(j); cell.setCellValue(data[j] == null ? "" : data[j].toString()); } if (i % 1000 == 0) { workbook.write(response.getOutputStream()); // ← 边写边输出,不缓存全量 } } // 5. 设置响应头(关键!否则下载成 .xls 但打不开) response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=score_" + System.currentTimeMillis() + ".xls"); workbook.write(response.getOutputStream()); }逻辑说明:
workbook.write()被调用了两次——第一次在循环内每千行 flush,第二次在末尾写剩余行。这是老项目应对大数据量的土办法,虽不如SXSSFWorkbook流式写入优雅,但在 2000 行以内完全够用,且避免了引入新依赖。
4. 常见问题排查:五条血泪经验,全是线上翻车现场
这个项目在真实机房部署时暴露出一批典型问题,不是文档里写的“可能遇到”,而是我亲眼看着它崩了三次后记下的解决方案。
4.1 现象:登录成功后跳转到空白页,地址栏显示http://localhost:8080/jwgl/main.jsp,但页面无任何内容
原因:main.jsp第 1 行<%@ page contentType="text/html;charset=UTF-8"%>缺少pageEncoding="UTF-8",导致 JSP 编译时中文注释解析失败,整个页面编译中断。
解决:在<%@ page标签中补全pageEncoding="UTF-8",即改为<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>。重启 Tomcat 生效。
4.2 现象:点击「导出成绩」按钮,浏览器下载了一个 0 字节的.xls文件
原因:exportScore()方法中response.getOutputStream()被多次调用,而getOutputStream()在响应已提交后会抛IllegalStateException,但异常被catch吞掉了(JDBCUtils.java第 120 行有空catch(Exception e){})。
解决:删掉JDBCUtils.java中所有空catch,改为catch(Exception e){e.printStackTrace();}。然后发现日志报java.lang.IllegalStateException: getOutputStream() has already been called,定位到exportScore()中workbook.write()被调用了两次。删掉循环内的workbook.write(),只保留末尾一次。
4.3 现象:教师登录后,「我的课程」列表为空,但数据库里明明有数据
原因:CourseServlet.java的listMyCourses()方法中 SQL 写成了WHERE t.teacher_id = ?,但teacher_id字段在teacher表中是id,而在course表中是teacher_id,查询时关联字段名写反了。
解决:将 SQL 改为SELECT c.* FROM course c WHERE c.teacher_id = ?,去掉JOIN teacher t。因为teacher_id是外键,直接查course表即可。
4.4 现象:修改学生信息后,页面显示「操作成功」,但数据库未更新
原因:StudentServlet.java的updateStudent()方法中,PreparedStatement的setXXX()参数索引从 1 开始,但代码写成了ps.setString(0, stuName)(索引 0 无效)。
解决:检查所有setXXX()调用,确保第一个参数是1,如ps.setString(1, stuName)。JDBC 规范中参数索引必须 ≥1。
4.5 现象:Tomcat 启动时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:MySQL 5.7 驱动类名是com.mysql.jdbc.Driver,但项目lib/下放的是mysql-connector-java-8.0.28.jar,其驱动类名为com.mysql.cj.jdbc.Driver。
解决:删除lib/下的 8.x 驱动,下载mysql-connector-java-5.1.47.jar(官方最后支持 MySQL 5.7 的 5.x 版本),放入lib/并重启。同时检查JDBCUtils.java第 22 行Class.forName("com.mysql.jdbc.Driver")是否匹配。
5. 二次开发实战:给成绩录入加「防误操作确认弹窗」与「修改留痕」
现在系统能跑了,但业务上有个硬需求:教师录错成绩后无法追溯是谁改的,且没有二次确认机制。我们不动核心架构,只在现有 Servlet 和 JSP 上叠加两层增强。
5.1 前端增强:JSP 层加「修改前弹窗确认」与「操作人水印」
在score_edit.jsp的保存按钮上加 JS 确认,并在页面底部加当前操作人信息:
<!-- 保存按钮 --> <input type="button" value="保存成绩" onclick="confirmSave()" class="btn btn-primary"/> <script> function confirmSave() { var msg = "您正在修改【" + document.getElementById('courseName').value + "】的成绩\n" + "确认保存?此操作不可撤销!"; if (confirm(msg)) { document.getElementById('scoreForm').submit(); } } </script> <!-- 页面底部水印 --> <div style="position: fixed; bottom: 10px; right: 10px; font-size: 12px; color: #999;"> 操作人:<%= session.getAttribute("user") != null ? ((User)session.getAttribute("user")).getUsername() : "未知" %> | 时间:<%= new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()) %> </div>注意:水印用
fixed定位,避免随滚动消失;时间用 JSP 表达式实时生成,不依赖 JSDate()(时区可能不准)。
5.2 后端增强:ScoreServlet.java的saveScore()方法加审计日志
在saveScore()方法开头插入日志记录,不依赖 Log4j(项目没配),直接写文件:
private void saveScore(HttpServletRequest request, HttpServletResponse response) throws IOException { // 1. 获取当前操作人 User user = (User) request.getSession().getAttribute("user"); String operator = user == null ? "unknown" : user.getUsername(); // 2. 记录审计日志(追加写入 logs/audit.log) String logEntry = String.format("[%s] %s 修改了学号 %s 的课程 %s 成绩为 %s%n", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()), operator, request.getParameter("stu_no"), request.getParameter("course_name"), request.getParameter("score")); try (FileWriter fw = new FileWriter("logs/audit.log", true); PrintWriter pw = new PrintWriter(fw)) { pw.print(logEntry); } catch (IOException e) { // 日志写失败不影响主流程,只打印错误 e.printStackTrace(); } // 3. 执行原有保存逻辑(省略) // ... }关键点:
FileWriter构造函数第二个参数true表示追加模式;try-with-resources自动关闭流;日志路径logs/audit.log是相对于 Tomcatbin/目录的,所以实际路径是TOMCAT_HOME/logs/audit.log,符合运维习惯。
5.3 数据库扩展:新增score_audit表存操作快照
在sql/下新建alter_score_audit.sql:
CREATE TABLE score_audit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, stu_id INT NOT NULL, course_id INT NOT NULL, old_score DECIMAL(5,2), new_score DECIMAL(5,2), operator VARCHAR(50) NOT NULL, operate_time DATETIME DEFAULT CURRENT_TIMESTAMP, ip_address VARCHAR(20) ); -- 添加索引提升查询效率 ALTER TABLE score_audit ADD INDEX idx_stu_course (stu_id, course_id); ALTER TABLE score_audit ADD INDEX idx_operator_time (operator, operate_time);然后在saveScore()的try块末尾,JDBC 更新主表后,追加一条审计插入:
// 原有更新 score 表的代码... String updateSql = "UPDATE score SET score = ? WHERE stu_id = ? AND course_id = ?"; JDBCUtils.executeUpdate(updateSql, new Object[]{newScore, stuId, courseId}); // 新增:插入审计记录 String auditSql = "INSERT INTO score_audit (stu_id, course_id, old_score, new_score, operator, ip_address) " + "VALUES (?, ?, ?, ?, ?, ?)"; JDBCUtils.executeUpdate(auditSql, new Object[]{ stuId, courseId, oldScore, newScore, operator, request.getRemoteAddr() });提示:
oldScore需要先查一次原值,用SELECT score FROM score WHERE stu_id = ? AND course_id = ?,避免并发修改覆盖。这就是为什么老项目喜欢用synchronized(this)——简单粗暴但有效。
从那以后我每次给 JavaWeb 老项目加审计功能,都强制走一遍「前端确认 → 后端日志 → 数据库快照」三层,哪怕只是课程设计作业。因为真实业务里,没人会为一个「误点保存」负责,但日志能帮你划清责任边界。希望帮到你。
本文还有配套的精品资源,点击获取