简介这是一套面向JavaWeb初学者与课程设计实践者的学生成绩管理系统完整源码基于Java语言与MySQL数据库开发适用于高校《Java Web程序设计》《数据库应用》等课程实训或毕业设计参考。系统采用MVC架构涵盖用户权限管理、学生/课程/成绩全生命周期操作及多维度统计分析功能可直接部署于Tomcat运行。压缩包共244个文件包含21个JSP页面实现动态交互视图、36个JAR依赖库含Servlet、JDBC等核心组件、15个Java源文件如AdminController、StudentController等控制器类以及1个SQL建表脚本辅以XML配置、CSS样式与JS交互逻辑结构清晰、模块职责分明。资源大小为17.15MB已有1440人学习下载。读者可获得可运行的完整工程、典型三层架构代码范例、常见Web安全防护实践如防SQL注入及真实教学场景下的业务逻辑实现细节是理解JavaWeb开发全流程的优质实操素材。1. 这不是又一个 CRUD 演示项目JavaWeb 学生成绩管理系统是真实教学场景下能跑通、能改、能交作业的最小可行闭环你手头那份“JavaWeb 学生成绩管理系统”压缩包大概率不是教科书附录里的伪代码也不是某培训机构打包出售的带水印演示站。它是一套在 Tomcat 8.5、JDK 1.8、MySQL 5.7 环境下真能连上本地数据库、增删查改学生/课程/成绩三张表、登录后按角色管理员/教师/学生看到不同菜单、导出 Excel 成绩单、且所有 JSP 页面不报 500 错误的完整工程。它解决的不是“怎么写 Servlet”而是“怎么让 A 同学的课程设计答辩不卡在部署环节”——比如登录验证跳转错页面、MySQL 驱动版本不匹配导致 Connection 为空、中文乱码让“张三”的成绩显示成“??”这种血泪问题。适合刚学完 JDBC 和 JSP 基础、正被课程设计 deadline 追着跑的本科生也适合需要快速搭个教学 Demo 给学生看数据流向的某高校实验课导师。它不追求 Spring Boot 自动装配的优雅但每行 DAO 层代码都暴露着 JDBC 的裸机逻辑方便你理解事务怎么手动 commit、ResultSet 怎么防空指针、为什么 PreparedStatement 能防 SQL 注入。这不是玩具是能当真系统用的脚手架。2. 从解压到首页可访问环境准备与项目结构破译2.1 环境清单三个版本号决定成败这套系统对运行时环境有明确依赖不是“装了 JDK 就能跑”。我实测过 JDK 11 下Class.forName(com.mysql.jdbc.Driver)会直接抛ClassNotFoundException因为 MySQL 5.7 官方驱动已弃用该类名而 Tomcat 10 又因 Jakarta EE 命名空间变更导致所有javax.servlet.*包全报错。所以必须锁定JDK1.8.0_202 或更高更新版但严格 ≤ 1.8.0_301提示java -version输出必须含1.8.0_若显示11.0.15或17.0.2请立即切换 JDK。Windows 用户用set JAVA_HOMEC:\Program Files\Java\jdk1.8.0_202Linux/macOS 用export JAVA_HOME/usr/lib/jvm/java-8-openjdk-amd64。Tomcat8.5.94推荐或 8.5.x 最新版注意不要用 Tomcat 9。其lib目录下servlet-api.jar已升级为jakarta.servlet-api.jar而本项目所有import javax.servlet.http.HttpServlet;会编译失败。MySQL5.7.33兼容性最佳或 5.7.x严禁 8.0关键点MySQL 8.0 默认启用caching_sha2_password认证插件而本项目jdbc:mysql://localhost:3306/score?useSSLfalse连接串不支持该协议会卡在Access denied for user。降级到 5.7 是最快解法。2.2 项目目录解剖看清哪是骨头哪是肉解压后你会看到典型 JavaWeb 结构但新手常误删关键文件。以下是真实路径与作用对照以score_system为根目录路径类型关键内容为什么不能删/src/com/score/dao/StudentDAO.javaJava 源码public ListStudent findAll()方法体含原始 JDBC 查询逻辑删除则学生列表页空白且无编译错误提示因接口还在/WebContent/WEB-INF/web.xml配置文件servlet-mapping中url-pattern定义/login对应LoginServlet缺失则所有请求 404Tomcat 启动日志会报No servlet found for /login/WebContent/js/student.js前端脚本function validateForm()校验学号长度和成绩范围0-100删除后表单可提交非法数据如成绩填-5或150后端虽有校验但用户体验崩坏/WebContent/WEB-INF/classes/db.properties配置文件drivercom.mysql.jdbc.Driver、urljdbc:mysql://localhost:3306/score?useSSLfalse此文件决定数据库连哪台机器、哪个库、用什么驱动类改错一个字符就连接失败提示/WebContent/WEB-INF/lib/下必须有mysql-connector-java-5.1.47.jar非 8.0 版。若发现mysql-connector-java-8.0.28.jar请立刻替换——这是新手部署翻车第一高发区。2.3 数据库初始化四步建库建表拒绝空库启动系统不会自动建库必须手动执行 SQL。别信“运行项目自动初始化”的 README那通常是 Spring Boot 项目的习惯。打开 MySQL 命令行或 Navicat依次执行-- 1. 创建数据库编码必须为 utf8mb4否则中文姓名存成问号 CREATE DATABASE score DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 切换到 score 库 USE score; -- 3. 建学生表注意id 是 INT NOT NULL AUTO_INCREMENT PRIMARY KEY CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, sno VARCHAR(12) NOT NULL UNIQUE, name VARCHAR(20) NOT NULL, gender ENUM(男,女) DEFAULT 男, class_name VARCHAR(30) ); -- 4. 建成绩表外键关联 student.id 和 course.id CREATE TABLE score_record ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) CHECK (score 0 AND score 100), FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE );参数说明DECIMAL(5,2)表示最多 5 位数字小数点后 2 位如89.50比FLOAT更精确ON DELETE CASCADE确保删除学生时其所有成绩记录自动清除避免外键冲突。3. 登录与权限分流Servlet 如何识别你是管理员还是学生3.1 登录流程图谱从表单提交到 session 写入整个权限控制不靠框架靠三处硬编码逻辑。理解它才能改角色、加新权限前端提交login.jsp表单actionLoginServletmethodpost传username和passwordServlet 处理LoginServlet.java的doPost()方法中String username request.getParameter(username); String password request.getParameter(password); // 关键查数据库获取用户信息含 role 字段 User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { // 写入 sessionkey 固定为 currentUser HttpSession session request.getSession(); session.setAttribute(currentUser, user); // user 对象含 roleadmin 或 student // 根据 role 重定向 if (admin.equals(user.getRole())) { response.sendRedirect(admin/index.jsp); } else if (teacher.equals(user.getRole())) { response.sendRedirect(teacher/course_list.jsp); } else { response.sendRedirect(student/score_view.jsp); } } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); }页面拦截每个受保护页面如admin/index.jsp开头必有% User user (User) session.getAttribute(currentUser); if (user null || !admin.equals(user.getRole())) { response.sendRedirect(../login.jsp); // 强制跳回登录页 return; } %逻辑说明session.setAttribute(currentUser, user)是权限流转核心。user对象必须含role字段且数据库user表中该字段值必须为admin/teacher/student大小写敏感。若你新增assistant角色必须同步修改所有if-else if判断和对应跳转路径。3.2 角色数据来源数据库 user 表的构造铁律权限分流的前提是数据库里真有带role字段的用户。user表结构必须包含CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存的是明文或简单 MD5非 BCrypt role ENUM(admin,teacher,student) NOT NULL DEFAULT student );插入测试数据时密码必须是明文存储此项目未做加密INSERT INTO user (username, password, role) VALUES (admin, 123456, admin), (teacher01, 123456, teacher), (stu001, 123456, student);参数说明ENUM类型强制约束role只能取预设值避免拼写错误如Admin或administrator导致权限失效。DEFAULT student保证新注册用户默认为学生角色。3.3 权限漏洞自查三处易被绕过的逻辑断点新手常以为“加了 session 判断就安全”实际存在三个经典绕过点断点 1登录成功后未清除登录表单参数现象用户登录后地址栏仍为http://localhost:8080/score_system/LoginServletF5 刷新会重复提交表单可能触发两次session.setAttribute。原因response.sendRedirect()是客户端重定向但若误写成request.getRequestDispatcher().forward()则 URL 不变刷新即重发。解决确认LoginServlet.java中所有跳转均为response.sendRedirect(...)绝不可用forward。断点 2JSP 页面未校验 session 有效性现象直接在浏览器输入http://localhost:8080/score_system/admin/index.jsp未登录也能看到页面空白或报错但 HTML 已加载。原因admin/index.jsp开头的 session 判断被注释或缺失。解决逐个检查/admin/、/teacher/、/student/下所有.jsp文件确保首行含上述if (user null || ...)逻辑。断点 3DAO 层未过滤用户 ID 范围现象学生stu001在 URL 中把student_id1改成student_id2竟能查看stu002的成绩。原因ScoreDAO.findByStudentId(int studentId)方法只查数据库未校验该studentId是否属于当前登录用户。解决在ScoreDAO.java中修改方法签名加入String currentUsername参数并在 SQL 中增加AND s.sno ?关联student表的学号。4. 成绩录入与导出业务逻辑落地的关键两步4.1 教师端成绩录入表单验证与数据库事务教师在teacher/score_input.jsp录入成绩时有两层校验前端 JS 校验/js/teacher.jsfunction validateScore() { var score document.getElementById(score).value; // 必须是数字且在 0-100 之间 if (isNaN(score) || score 0 || score 100) { alert(成绩必须是 0-100 之间的数字); return false; } // 小数点后最多两位 if (!/^\d(\.\d{1,2})?$/.test(score)) { alert(成绩最多保留两位小数); return false; } return true; }后端 Servlet 校验ScoreInputServlet.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String studentSno request.getParameter(studentSno); String courseName request.getParameter(courseName); String scoreStr request.getParameter(score); // 1. 二次校验防绕过 JS double score Double.parseDouble(scoreStr); if (score 0 || score 100) { request.setAttribute(error, 成绩必须在 0-100 之间); request.getRequestDispatcher(score_input.jsp).forward(request, response); return; } // 2. 开启事务先查学生ID和课程ID再插入成绩 Connection conn null; PreparedStatement pstmt null; try { conn DBUtil.getConnection(); // 从 db.properties 读配置 conn.setAutoCommit(false); // 关闭自动提交 // 查 student.id pstmt conn.prepareStatement(SELECT id FROM student WHERE sno ?); pstmt.setString(1, studentSno); ResultSet rs pstmt.executeQuery(); int studentId rs.next() ? rs.getInt(id) : -1; // 查 course.id pstmt conn.prepareStatement(SELECT id FROM course WHERE name ?); pstmt.setString(1, courseName); rs pstmt.executeQuery(); int courseId rs.next() ? rs.getInt(id) : -1; if (studentId -1 || courseId -1) { throw new RuntimeException(学生或课程不存在); } // 插入成绩 pstmt conn.prepareStatement( INSERT INTO score_record (student_id, course_id, score) VALUES (?, ?, ?)); pstmt.setInt(1, studentId); pstmt.setInt(2, courseId); pstmt.setDouble(3, score); pstmt.executeUpdate(); conn.commit(); // 手动提交 request.setAttribute(msg, 成绩录入成功); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute(error, 录入失败 e.getMessage()); } finally { DBUtil.close(conn, pstmt, null); } request.getRequestDispatcher(score_input.jsp).forward(request, response); }参数说明conn.setAutoCommit(false)是事务起点conn.commit()成功才写库conn.rollback()在异常时回滚避免脏数据。DBUtil.close()是工具类确保Connection、PreparedStatement、ResultSet全部关闭防止连接泄漏。4.2 学生成绩导出Apache POI 生成 Excel 的零配置方案导出功能在StudentScoreExportServlet.java中实现不依赖 Maven直接使用poi-3.17.jar已放在/WEB-INF/lib/protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String studentSno request.getParameter(sno); // 1. 从数据库查该学生所有成绩JOIN student, course, score_record ListScoreExportVO list scoreDAO.findScoresByStudentSno(studentSno); // 2. 创建 Excel 工作簿 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. 写数据行 for (int i 0; i list.size(); i) { ScoreExportVO vo list.get(i); HSSFRow row sheet.createRow(i 1); row.createCell(0).setCellValue(vo.getCourseName()); row.createCell(1).setCellValue(vo.getScore()); row.createCell(2).setCellValue(new SimpleDateFormat(yyyy-MM-dd HH:mm).format(vo.getCreateTime())); } // 5. 设置响应头触发下载 response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment;filename studentSno _成绩单.xls); // 6. 写出流 OutputStream out response.getOutputStream(); workbook.write(out); out.close(); }逻辑说明HSSFWorkbook对应.xls格式Excel 97-2003兼容性最好若需.xlsx需换XSSFWorkbook和poi-ooxml包但本项目未提供强行替换会报NoClassDefFoundError。Content-Disposition中的filename必须为英文或 URL 编码中文否则 Chrome 下下载文件名乱码建议用URLEncoder.encode(studentSno, UTF-8)。5. 避坑指南五个让开发者凌晨三点还在抓头发的真实问题5.1 现象登录后跳转到空白页浏览器地址栏显示http://localhost:8080/score_system/null原因LoginServlet.java中response.sendRedirect()的参数为null通常是因为user.getRole()返回null而数据库user.role字段值为空或拼写错误如Admin。解决检查user表中该用户的role值是否为小写admin/teacher/student在LoginServlet的if-else前加日志System.out.println(Role: user.getRole());。5.2 现象成绩录入页提交后报HTTP Status 500 – Internal Server Error日志显示java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因MySQL 8.0 驱动类名已改为com.mysql.cj.jdbc.Driver但项目db.properties中仍为com.mysql.jdbc.Driver且lib下放的是mysql-connector-java-8.0.x.jar。解决① 下载mysql-connector-java-5.1.47.jar② 替换/WEB-INF/lib/下旧 jar③ 确认db.properties中drivercom.mysql.jdbc.Driver未改动。5.3 现象中文姓名在 JSP 页面显示为??但数据库里是好的原因Tomcat 的server.xml中Connector缺少URIEncodingUTF-8导致 GET 请求参数如 URL 中的sno张三被 ISO-8859-1 解码。解决打开conf/server.xml找到Connector port8080 ... /在末尾添加URIEncodingUTF-8重启 Tomcat。5.4 现象点击“导出 Excel”无反应浏览器控制台报Failed to load resource: the server responded with a status of 404 ()原因web.xml中servlet-mapping的url-pattern与StudentScoreExportServlet.java的WebServlet(/export)注解冲突或web.xml未声明该 Servlet。解决① 若用注解确保web.xml的web-app版本 ≥ 3.0即web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.0② 若用web.xml检查servlet-mapping的url-pattern是否为/export且与 JSP 中hrefexport?snoxxx一致。5.5 现象管理员修改学生信息后页面显示“操作成功”但数据库没变化原因StudentDAO.update()方法中 SQL 的WHERE条件用的是id ?但前端传参时id字段名写成studentId导致PreparedStatement的setInt(1, id)实际传入nullSQL 变成UPDATE student SET ... WHERE id NULL不匹配任何行。解决检查update.jsp表单中input typehidden nameid value${student.id}的name属性是否为id在StudentDAO.update()中加日志System.out.println(Updating student id: id);。6. 从“能跑”到“能交”三个让课程设计脱颖而出的实战技巧6.1 技巧一给成绩列表页加搜索与分页——两小时提升专业感原系统admin/student_list.jsp是全量查表数据一多就卡死。加简单分页只需改三处DAO 层加带 limit 的查询StudentDAO.javapublic ListStudent findStudentsByPage(int start, int pageSize) { ListStudent list new ArrayList(); String sql SELECT * FROM student ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, start); pstmt.setInt(2, pageSize); ResultSet rs pstmt.executeQuery(); while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setSno(rs.getString(sno)); s.setName(rs.getString(name)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; }Servlet 传页码参数StudentListServlet.javaint page 1; String pageParam request.getParameter(page); if (pageParam ! null !pageParam.trim().isEmpty()) { page Integer.parseInt(pageParam); } int pageSize 10; int start (page - 1) * pageSize; ListStudent students studentDAO.findStudentsByPage(start, pageSize); request.setAttribute(students, students); // 计算总页数需另写 count 方法 int total studentDAO.countAll(); int totalPages (int) Math.ceil((double) total / pageSize); request.setAttribute(totalPages, totalPages); request.setAttribute(currentPage, page);JSP 页加分页导航student_list.jsp底部div classpagination c:if test${currentPage 1} a hrefStudentListServlet?page${currentPage - 1}上一页/a /c:if span第 ${currentPage} 页共 ${totalPages} 页/span c:if test${currentPage totalPages} a hrefStudentListServlet?page${currentPage 1}下一页/a /c:if /div效果列表页右下角出现“上一页/下一页”URL 带?page2老师点开就知道你懂数据量大的处理逻辑。这比加十个无用 CSS 动画更显功力。6.2 技巧二用 Filter 统一处理中文乱码——告别每个 Servlet 写request.setCharacterEncoding(UTF-8)新建EncodingFilter.javapublic class EncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; // POST 请求编码 req.setCharacterEncoding(UTF-8); // 响应编码 resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }在web.xml中注册filter filter-nameencodingFilter/filter-name filter-classcom.score.filter.EncodingFilter/filter-class /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping价值从此所有 Servlet 不用再写request.setCharacterEncoding(UTF-8)且 GET 请求乱码通过URIEncoding和 POST 乱码通过 Filter全部覆盖。答辩时老师问“怎么解决中文乱码”你答“用 Filter 统一拦截”比说“我在每个 Servlet 里都写了”高明十倍。6.3 技巧三用 Log4j 记录关键操作——让系统有“记忆”答辩时可演示在WEB-INF/classes/下新建log4j.propertieslog4j.rootLoggerINFO, stdout, file log4j.appender.stdoutorg.apache.log4j.ConsoleAppender log4j.appender.stdout.layoutorg.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n log4j.appender.fileorg.apache.log4j.RollingFileAppender log4j.appender.file.Filelogs/app.log log4j.appender.file.MaxFileSize10MB log4j.appender.file.MaxBackupIndex5 log4j.appender.file.layoutorg.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern%d{yyyy-MM-dd HH:mm:ss} [%p] %c{1} - %m%n在ScoreInputServlet.java中记录private static final Logger logger Logger.getLogger(ScoreInputServlet.class); // ... logger.info(教师 teacherUsername 为学生 studentSno 录入课程 courseName 成绩 score);效果logs/app.log里自动生成操作日志答辩时打开文件指着2024-06-15 14:22:33 [INFO] ScoreInputServlet - 教师 teacher01 为学生 2022001 录入课程 数学 成绩95.0这一行老师立刻明白你做了生产级日志管理。这比“系统很稳定”这种空话有力得多。从那以后我每次交付 JavaWeb 课程设计都会强制走一遍这三步加基础分页、配统一 Filter、埋至少两条 Log4j 日志。不是为了炫技是让系统在老师点开的 30 秒内就传递出“这人懂真实开发”的信号。希望帮到你。本文还有配套的精品资源点击获取