
简介一份面向Java初学者与课程设计的简易Web学生成绩管理系统源码包覆盖学生信息、课程与成绩的录入、查询、统计、筛选等典型教务场景适合用于理解Servlet/JSP、JDBC及MVC分层开发流程。压缩包共36个文件其中10个java源码、9个jsp页面与10张jpg运行截图构成主体另有png界面图、XML配置、MF清单、README说明及gitignore等文件整体仅2.94MB便于快速下载与本地部署。作者通过实际运行截图展示了系统首页、数据表设计、查询、成绩录入修改、筛选与统计等核心界面README可辅助搭建环境与启动项目。已有55人学习下载对于正在做Java Web课设或想从零梳理B/S结构数据流的学习者这份‘渣渣’级但五脏俱全的小项目具有不错的参考价值。1. 这个Java Web学生成绩管理系统zip到底是什么水平的代码你拿到手的这个压缩包文件名里就写着“渣渣”这其实是作者自嘲不代表它没有参考价值。这类用Java写的基于Web的学生成绩管理系统是课程设计和毕业设计里出现频率最高的一类题目学生登录能查成绩教师登录能录成绩管理员管课程和学生信息三个角色共用一套登录页背后对应几张关系表。它解决的问题很具体——让一个刚学完Java Web的人用最小的代码量跑通“页面 → Servlet → DAO → 数据库 → 再回到页面”的完整闭环。它的典型技术栈逃不开三样JSP或者Servlet做交互JDBC连MySQLTomcat当容器。没有Spring没有MyBatis偶尔用个JSTL标签就算是讲究了。这正好是它的价值代码短、依赖少、链路透明适合那些不想把框架当黑匣子吞下去的读者。这篇文章就按这个方向把技术栈、建库脚本、核心登录与成绩增删改、本地部署再加上我踩过的几个高频坑完整拆给你看。2. JSP Servlet JDBC三条主线先看清这套成绩管理系统的家底2.1 为什么这类基于Web的成绩管理系统十个有九个选无框架组合先回答一个新人最爱问的问题都什么年代了为什么不用Spring Boot原因很简单教学进度刚好停在Servlet和JSP这一站作业也要求手写原生Servlet另一个原因更实在无框架项目的请求链路短从浏览器地址栏到数据库中间每一跳都能在代码里找到对应文件这对学习期的可读性比开发效率重要得多。我拆过不少同类项目的zip包发现它们的包结构高度相似基本是三层分包entity放实体类dao放SQL访问servlet放控制逻辑JSP放在web目录下直接面对浏览器。偶尔会有工具类Util包里面一个DBHelper负责拿Connection。你会看到大量重复的getConnection、PreparedStatement、ResultSet代码这是这类项目最常见的观感。但正是这层重复让新手能照着看懂。Spring Boot把这一切封装进注解和框架约定反而让人搞不清“一个请求到底经过了谁”。这套老组合的每一行都在明面上出了问题顺着代码走不需要猜测框架内部干了什么。所以如果你是第一次接触这类成绩管理系统别急着嫌弃技术栈老先接受它“教学友好”这一面。角色对应技术在这个系统里的职责展示层JSP登录表单、成绩列表、学生信息页的HTML渲染控制层Servlet接收请求参数、调用DAO、控制页面跳转数据层JDBC MySQL执行SQL、返回结果集、完成成绩的增删改查2.2 解压后先别运行用三分钟定位zip包里的分层结构解压后的第一件事不是找README而是先看目录。这类项目的源码组织有两种常见风格老式Eclipse工程目录直接叫src和WebContent新一点的Maven风格是src/main/java和src/main/webapp。无论哪种核心结构逃不出下面这个框架。src/main/java ├── com/xxx/entity/ │ ├── User.java # 用户实体id、用户名、密码、角色 │ ├── Student.java # 学生实体学号、姓名、班级 │ ├── Course.java # 课程实体课程号、课程名、学分 │ └── Score.java # 成绩实体学生ID、课程ID、分数 ├── com/xxx/dao/ │ ├── UserDAO.java # 登录查询、按用户名单查 │ ├── StudentDAO.java # 学生信息管理增删改查 │ ├── CourseDAO.java # 课程管理 │ └── ScoreDAO.java # 成绩查询、插入、修改、删除 └── com/xxx/servlet/ ├── LoginServlet.java # 处理登录请求 ├── ScoreQueryServlet.java ├── ScoreAddServlet.java └── ... webapp/ ├── WEB-INF/ │ └── web.xml # Servlet映射、欢迎页配置 ├── login.jsp # 登录页 ├── index.jsp # 首页/重定向 ├── student/ # 学生模块页面 ├── teacher/ # 教师模块页面 └── admin/ # 管理员模块页面这个结构里最要紧的是DAO和Servlet这两个目录。如果你拿到源码后想改功能先按角色找改登录逻辑去UserDAO改成绩列表去ScoreDAO改页面跳转去对应名字的Servlet类。不要一上来在JSP里乱翻那个只会让你越看越乱。这里有个细节值得留意简单项目往往没有独立的Service业务层Servlet直接调DAO。这在几十行代码的规模下没问题但如果你后面往里面加了复杂的成绩统计逻辑再去改Servlet就会很痛苦。我的建议是先不动结构等实现完功能再考虑是否抽出Service层。2.3 从浏览器地址栏到数据库一条成绩查询请求的完整路线如果你挡住了HTML只看流程一次成绩查询是这样的浏览器访问某个URLTomcat根据web.xml或Servlet注解里的URL映射找到对应类容器调用doGet或doPost方法方法里读取request中的参数调用DAO执行SQL把结果放进request或session最后forward到某个JSP页面渲染成表格。这套链路和Spring MVC的核心思想没有区别只是少了注解和DispatcherServlet的自动化。!-- web.xml 中典型的Servlet映射写法 -- servlet servlet-namescoreQuery/servlet-name servlet-classcom.xxx.servlet.ScoreQueryServlet/servlet-class /servlet servlet-mapping servlet-namescoreQuery/servlet-name url-pattern/score/query/url-pattern /servlet-mapping关键参数是url-pattern里的/score/query它决定表单action该写成什么。我在实际项目中见过最多的404错误就是JSP页面的表单提交地址和web.xml里的url-pattern不一致。另一种情况是这个系统用了WebServlet(/score/query)注解方式web.xml里反而没有配置导致有人去web.xml里找不到映射而疑惑。识别方法是先打开一个Servlet源码看类上方有没有WebServlet注解有就说明走注解映射项目启动时容器自动扫描没有就去web.xml里找servlet-class对应配置。两个方案没有好坏之分但你必须先确认是哪种否则后面对照地址排查时方向从一开始就是错的。3. 从建库SQL到四张表把成绩管理系统的数据底子打牢3.1 四张核心表的字段规划为什么用户表要放学生和教师两类身份基于Web的成绩管理系统再怎么简单至少需要承载四类数据人、学生、课程、成绩。这里的“人”包括管理员、教师、学生三类账号。常见做法不是分别建管理员表、教师表、学生表三张账号库而是搞一张用户表用role字段区分身份。这个设计的理由是登录时只需查一张表不用先判断身份再去不同表里二次查询。CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(20) UNIQUE NOT NULL COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT 登录密码当前多数项目存明文, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色1学生 2教师 3管理员 ); CREATE TABLE t_student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, user_id INT UNIQUE COMMENT 关联t_user的用户ID, name VARCHAR(50) NOT NULL COMMENT 学生姓名, class_name VARCHAR(50) COMMENT 班级, gender CHAR(2) COMMENT 性别 ); CREATE TABLE t_course ( course_id INT AUTO_INCREMENT PRIMARY KEY COMMENT 课程ID, course_name VARCHAR(100) NOT NULL COMMENT 课程名, credit TINYINT COMMENT 学分 ); CREATE TABLE t_score ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL COMMENT 学号外键关联t_student, course_id INT NOT NULL COMMENT 课程ID外键关联t_course, score DECIMAL(5,1) COMMENT 分数最高999.9实际只用到100.0, exam_date DATE COMMENT 考试日期, UNIQUE KEY uk_student_course (student_id, course_id) );字段规划时有三个点值得你逐个确认。第一个是主键选型学生表用学号字符串当主键还是用自增ID简单项目用学号直接当主键会让查询省一次联表但学号一旦变更关联表都要跟着改。第二个是成绩表的联合唯一键uk_student_course它能防止同一位学生对同一门课程重复录入两条成绩——这是成绩管理系统最常出现的脏数据。第三个是分数类型用DECIMAL(5,1)能保证小数位存储精确避免用FLOAT和DOUBLE产生误差。如果你拿到的zip包自带的SQL里只有三张表少了课程表那么成绩表里大概率只存了课程名字段而不是course_id。这种设计能少写一个下拉框的联动查询但在后续做“按课程统计平均分”时非常痛苦。建议看到这种情况就自己补上课程表代价很小收益明显。3.2 建库SQL和初始化数据连上MySQL第一件事做什么新建数据库后要做的第一件事不是建表而是先把SQL脚本整体执行一遍确认没有报错。之后插入基础账号保证系统一跑起来就能登录而不是登录时连账号都没有。CREATE DATABASE IF NOT EXISTS stu_score_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE stu_score_db; -- 先建四张表代码见3.1节此处省略重复DDL -- 初始化账号admin/admin, teacher/123456, student/10001 INSERT INTO t_user (username, password, role) VALUES (admin, admin123, 3), (teacher01, 123456, 2), (stu10001, 123456, 1); INSERT INTO t_student (student_id, user_id, name, class_name, gender) VALUES (10001, 3, 张三, 计科2301, 男), (10002, NULL, 李四, 计科2301, 女); INSERT INTO t_course (course_id, course_name, credit) VALUES (1, Java程序设计, 4), (2, 数据库原理, 3), (3, Web应用开发, 3); INSERT INTO t_score (student_id, course_id, score, exam_date) VALUES (10001, 1, 88.5, 2025-01-15), (10001, 2, 76.0, 2025-01-18), (10002, 1, 92.0, 2025-01-15);执行完SQL后检查一个关键点密码字段在简单项目里通常存的是明文。这当然不安全但在课程设计里很常见先别急着骂作者等你把系统跑通可以再改成MD5加盐最后一章我会讲这个改造思路。另外初始化数据里设置一个NULL的user_id是为了测试“学生用户还没绑定学号时系统能不能正常登录”。3.3 MySQL版本与连接串参数一处配置不对登录页永远转圈这个目录里最新的“成绩管理系统”多半跑在MySQL 8.0上但你电脑上可能还是MySQL 5.7。这两类数据库在JDBC连接串上的配置有差异最常见的坑是MySQL 8驱动要求必须显式指定时区否则报serverTimezone错误而5.7通常是另一个坑驱动包太旧直接报CLIENT_PLUGIN_AUTH。# DBHelper.properties或写在DBHelper.java里的常量 drivercom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/stu_score_db?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password你的数据库密码代码里的useSSLfalse是关掉MySQL 8.x默认的SSL连接检查characterEncodingutf8保证中文从数据库到JSP不乱码serverTimezoneAsia/Shanghai解决驱动默认读取本机时区失败的问题。需要注意的是MySQL 5.7的驱动类名是com.mysql.jdbc.DriverMySQL 8改成com.mysql.cj.jdbc.Driver。如果你用的MySQL 5.7却在properties里写成8的驱动类名会报ClassNotFoundException。这是环境相关不属于代码bug但排查时间往往最长。如果你改了properties文件里的任何参数记得要重启Tomcat再刷新浏览器。JDBC的连接参数在DBHelper类加载时读取很多静态代码块是在第一次访问时才初始化的热部署重载不彻底时会出现改了配置但还在用旧参数的情况。我的做法是在DBHelper里临时打一行System.out.println(url)启动日志里看到打印内容才算配置真正生效。4. 把登录和成绩CRUD跑起来的核心代码路径4.1 登录链路数据库里的角色字段决定了跳转到哪个页面登录是最能体现这类项目“绕一圈”的路径。用户提交表单LoginServlet拿到用户名和密码调UserDAO查数据库把查出来的User对象放进Session再根据role值跳转到不同的首页。密码比对通常在DAO层完成SQL里直接带上username和password两个条件查得到就返回User对象查不到就返回null。public User findByUsernameAndPassword(String username, String password) { String sql SELECT id, username, role FROM t_user WHERE username ? AND password ?; try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRole(rs.getInt(role)); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; }参数说明这里用PreparedStatement而不是直接拼字符串拼接SQL是因为后者存在注入风险比如用户名输入 or 11会让整个登录验证失效。SQL里的?是占位符setString负责把参数安全地传给数据库。这段代码还有个细节查询列只选了id, username, role没有把密码查出来这是好习惯——密码字段只在登录比对时使用不应被反复加载进内存。LoginServlet拿到User对象之后怎么做跳转是这类系统里最容易写得乱的环节。常见做法是这样protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); UserDAO dao new UserDAO(); User user dao.findByUsernameAndPassword(username, password); if (user null) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } HttpSession session request.getSession(); session.setAttribute(currentUser, user); if (user.getRole() 1) { response.sendRedirect(request.getContextPath() /student/index.jsp); } else if (user.getRole() 2) { response.sendRedirect(request.getContextPath() /teacher/index.jsp); } else { response.sendRedirect(request.getContextPath() /admin/index.jsp); } }这段里的关键参数有两个。第一个是request.getContextPath()它返回项目的部署上下文路径比如你把war包命名为stuScore这里就是/stuScore。开发时为了省事可以直接写死/stuScore/student/index.jsp但部署换名字以后全都会404所以用getContextPath永远是对的。第二个是登录成功后用sendRedirect而不是forward因为用户主动登录成功后浏览器地址栏应该更新为新的地址如果用户按F5刷新不会重复提交登录表单。跳转前先查一下用户是否存在这里关联了重定向技术属于用户体验。4.2 成绩查询与多条件筛选拼接SQL的坑比想象中多成绩查询是这个系统的核心功能但简版系统一般只做到“按学生查该生的所有成绩”。如果你想做得好看一点可以支持三个条件过滤学生姓名、课程名、分数区间。这个需求就触发了一个经典问题动态拼接SQL。条件数量不定不能写死一条SQL而要在代码里用StringBuilder拼同时保持PreparedStatement参数占位符的数量正确。public ListScore query(String studentName, Integer courseId, Double minScore) { StringBuilder sql new StringBuilder( SELECT s.id, s.score, s.exam_date, st.name AS student_name, c.course_name ); sql.append(FROM t_score s ); sql.append(JOIN t_student st ON s.student_id st.student_id ); sql.append(JOIN t_course c ON s.course_id c.course_id ); sql.append(WHERE 1 1 ); if (studentName ! null !studentName.isEmpty()) { sql.append(AND st.name LIKE ? ); } if (courseId ! null) { sql.append(AND s.course_id ? ); } if (minScore ! null) { sql.append(AND s.score ? ); } try (Connection conn DBHelper.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { int index 1; if (studentName ! null !studentName.isEmpty()) { ps.setString(index, % studentName %); } if (courseId ! null) { ps.setInt(index, courseId); } if (minScore ! null) { ps.setDouble(index, minScore); } // 执行查询遍历ResultSet转成ListScore省略循环代码 } catch (SQLException e) { e.printStackTrace(); } return Collections.emptyList(); }这里的拼接手法值得背下来先用WHERE 1 1占位后续每个条件都从AND开始拼这样就算一个条件都不传SQL也是合法的。注意参数设置顺序必须与拼接顺序严格一致这是动态SQL最容易翻车的地方——SQL里第1个?对应学生姓名第2个对应课程ID可代码里漏了一个setXxx运行到查询时就报参数数量不匹配。如果这版查询还要求分页你会遇到另一个坑LIMIT ? OFFSET ?里的参数在JDBC里必须用setInt明确设置不能直接拼凑数字进SQL。分页参数里最关键的pageSize一般设10但列表页如果每行高度较大建议改成5否则一页会非常长。计算总页数需要先查一条SELECT COUNT(*)再把总记录数除以pageSize后向上取整这个逻辑简版系统里经常会漏掉。4.3 成绩增删改与事务边界教师录入时分数校验要在最前面教师端的发成绩功能相对查询要复杂一些。录入前先判断分数是否在0到100之间再检查联合唯一键是否冲突最后执行插入。很多简版项目会在SQL里直接插导致错误提示不友好。处理方案是先查后插或者捕获数据库的DuplicateKeyException再转成提示语。public boolean addScore(String studentId, Integer courseId, Double score) throws ScoreDuplicateException { String checkSql SELECT COUNT(*) FROM t_score WHERE student_id ? AND course_id ?; String insertSql INSERT INTO t_score (student_id, course_id, score, exam_date) VALUES (?, ?, ?, ?); try (Connection conn DBHelper.getConnection(); PreparedStatement checkPs conn.prepareStatement(checkSql)) { checkPs.setString(1, studentId); checkPs.setInt(2, courseId); try (ResultSet rs checkPs.executeQuery()) { rs.next(); if (rs.getInt(1) 0) { throw new ScoreDuplicateException(该学生此课程已有成绩不能重复录入); } } try (PreparedStatement insertPs conn.prepareStatement(insertSql)) { insertPs.setString(1, studentId); insertPs.setInt(2, courseId); insertPs.setDouble(3, score); // 假设examDate由前端传入这里用java.sql.Date.valueOf() insertPs.setDate(4, java.sql.Date.valueOf(requestDateStr)); return insertPs.executeUpdate() 1; } } catch (SQLException e) { e.printStackTrace(); return false; } }事务边界在这里是个容易被忽略的问题检查与插入用了同一个Connection但默认autocommit为true假如检查通过之后插入失败系统不会回滚数据状态没有变化也不会留下半截数据。可如果未来你扩展成“批量录入多个学生的成绩”那就必须把这两步放进同一个事务里了关闭自动提交全部执行成功后统一commit任何一步失败就rollback。否则就有可能出现一半学生录入成功另一半失败成绩表处于不完整状态。删除和修改逻辑类似核心是记住在SQL里带WHERE id ?并且id来自隐藏域而不是用户输入的文本框中。隐藏域里传id虽然从安全上看仍然能被伪造但至少比让用户自己填id更规范。修改成绩时要把原分数回显到表单里同时记录成绩记录ID避免提交时找不到是哪一行在更新。5. 本地部署20分钟内跑通五个高频避坑点逐个排查5.1 从zip到能访问的第一关把项目放进Tomcat的正确姿势拿到项目后最稳妥的路径是在IDE里新建一个Web项目再把解压出来的src和webapp内容复制进去配好Artifacts后运行在Tomcat上。IDE的Artifacts配置不理解没关系但要记住核心逻辑Tomcat运行的是Web应用的编译输出目录不是你的源码目录。如果你的页面能访问但一点登录就报404或者500多半是编译后的classes没有跟着Tomcat走。# 手工部署到Tomcat的最简路径了解原理用 cp -r webapp/. $CATALINA_HOME/webapps/stu_score/ cp -r target/classes/. $CATALINA_HOME/webapps/stu_score/WEB-INF/classes/ cp lib/mysql-connector-java-8.0.x.jar $CATALINA_HOME/webapps/stu_score/WEB-INF/lib/用IDE开发时这条命令不用执行但你要理解背后发生的三件事JSP和静态资源要进webappsJava编译后的class要进WEB-INF/classesMySQL驱动jar要进WEB-INF/lib。很多新手在IDE里看到项目能启动却不知道Tomcat实际部署的是IDEA生成的out目录一旦这个目录里的库不完整就会在启动阶段报ClassNotFoundException或者NoClassDefFoundError。5.2 运行环境的版本组合先对照这张表再决定要不要换Tomcat这类老项目对运行环境非常挑剔版本组合决定了你接下来会遇见多少坑。我建议直接套用这张经过大量同类项目验证的组合表别擅自上最新版。JDK版本推荐Tomcat版本注意事项JDK 8Tomcat 8.5 / 9.0兼容性最稳支持javax.*JDK 11Tomcat 9.0需要的Servlet API版本要够JDK 17Tomcat 9.0不要用Tomcat 10API包路径变化JDK 17Tomcat 10.x项目要把javax改成jakarta否则启动报错这里最大的坑是Tomcat 10。Tomcat 10把Servlet API的包名从javax.servlet换成了jakarta.servlet老项目全是javax开头的import直接部署在Tomcat 10上会报java.lang.NoClassDefFoundError: javax/servlet/ServletException或者Servlet类加载不出来。如果你电脑只装了Tomcat 10又实在不想换就需要全局替换源码中所有javax.为jakarta.但这样项目就只能在Tomcat 10上跑了失去了老环境兼容性。对于想要赶紧跑通看效果的人来说我个人建议直接下载Tomcat 9更省心。5.3 高频踩坑记录现象、原因和解决办法我按被问过的频率把这类成绩管理系统在部署期最容易翻车的五个问题整理成下面的排查清单。每一条都是实际报错里重复出现的。第一条登录时报java.sql.SQLException: The server time zone value...。现象是数据库连接失败原因定位到MySQL 8.0的驱动要求指定时区而你的连接串里没写serverTimezone。解决方法是检查DBHelper里那个url补上serverTimezoneAsia/Shanghai压测时如果还不行就把MySQL的default-time-zone也手动设成08:00。第二条页面能打开但提交表单后404。现象是浏览器地址栏跳到/stu_score/score/query控制台输出Tomcat的404日志原因通常是JSP里的form action路径没带上项目的contextPath或者WebServlet注解里的url-pattern和实际请求路径不一致。解决方法是先看Servlet类有没有WebServlet注解有的话把注解路径原样搬到action里没有的话去web.xml里查映射。最常见的写法错误是action写成/score/query而项目部署名是/stu_score少了项目前缀。第三条中文乱码登录后用户名显示成乱码。现象是数据库里存进去就是乱码页面上读出来也是乱码。原因有三个位置要统一数据库表字符集、JDBC连接串的characterEncoding、JSP页面的pageEncoding。解决方法是建表统一用utf8mb4连接串加characterEncodingutf8JSP文件第一行设置pageEncodingUTF-8同时把Tomcat的URIEncoding也设为UTF-8。三处缺一处都会导致中间某个环节把编码搞坏这种玄学问题排查起来最费时间。第四条IDEA运行窗口不报错浏览器访问一直白屏或转圈。现象是页面资源加载不出来随后控制台出现Unable to compile class for JSP。原因通常是JSP页面里用了JSTL标签库但lib目录下没有放jstl.jar和standard.jar。解决方法是去Maven仓库下载这两个jar放进WEB-INF/lib如果项目是Maven工程就在pom.xml加两个依赖。注意JSTL版本要和Servlet版本匹配老一点的JSTL 1.1在Tomcat 9上依然能跑但别用JSTL 2.0配Tomcat 9以外的环境。第五条部署后重启项目修改的Java代码不生效。现象是改完DAO里的一条SQL刷新浏览器后还是旧逻辑。原因有几种Tomcat没有重新编译或者IDEA的HotSwap只替换方法签名没替换实现又或者你改的是lib里的jar但没重新构建。解决方法是先Build → Rebuild Project再重启Tomcat。如果你用的是Tomcat的bin目录部署模式记得先删掉webapps里旧的解压目录再拷贝Tomcat有时不会覆盖同名旧class文件血泪经验是删干净再放永远比覆盖可靠。6. 值得做的两项进阶改造权限收紧与成绩单导出这套系统把登录跑通之后其实只做了一半。很多课程设计答辩时会被问“你的系统安全吗”如果回答不上来整体印象分会受影响。我向你推荐两个改动量不大但很见功力的小改造它们都能在半小时内完成。第一个是加一个Filter做统一登录拦截。现在大多数简单项目的拦登录只写在每个Servlet开头几行判断Session是否为空代码重复还容易漏掉某个页面。用Filter可以一次性拦截全部需要登录的URL。public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String uri request.getRequestURI(); if (uri.endsWith(login.jsp) || uri.contains(/login)) { chain.doFilter(req, resp); return; } if (session null || session.getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }这段代码的逻辑不复杂getSession(false)是关键参数false表示拿不到Session时直接返回null而不是强制创建一个新Session。如果你写成getSession()不传参数那么每次未登录请求都会new一个Session对象过滤器判断永远走不到重定向分支拦截就失效了。过滤器的权限判断放在Session为空或没有currentUser属性时统一跳回登录页。实际部署时url-pattern建议写成/*然后在代码里放行登录页自己和静态资源路径否则登录页面也会被拦住形成死循环。第二个是给教师端加分“导出成绩单为CSV”按钮。用Servlet直接输出CSV不需要额外依赖核心是设置响应的Content-Type和写入BOM头否则Excel打开中文会乱码。response.setContentType(application/csv; charsetUTF-8); response.setHeader(Content-Disposition, attachment; filenamescore_list.csv); OutputStream os response.getOutputStream(); os.write(0xEF); // UTF-8 BOM防止Excel识别乱码 os.write(0xBB); os.write(0xBF); BufferedWriter writer new BufferedWriter(new OutputStreamWriter(os, StandardCharsets.UTF_8)); writer.write(学号,课程,分数,考试日期\n); // 从ScoreDAO查询列表逐行写数据 for (Score s : scoreList) { writer.write(s.getStudentId() , s.getCourseName() , s.getScore() , s.getExamDate() \n); } writer.flush();这段有两个参数容易改错。Content-Disposition里的filename如果带中文要记得做URL编码否则浏览器下载时文件名会变成一串百分号。第二个是CSV的逗号分隔如果课程名里本身包含逗号Excel会把这个字段拆成两列稳妥做法是用双引号包住每个字段或者把课程名里的逗号在写入前替换成空格。这两个细节答辩论起来都算亮点操作难度却不大。我现在的习惯是拿到任何这类“渣渣”系统第一件事不是吐槽代码简陋而是先把它部署跑通然后立刻做这两项改造Filter把权限统一收紧CSV导出把数据变成可交付的成果。这两个小功能加完之后这个系统就从“课程设计作业”变成了“有完整访问控制的学习项目”。如果你也在折腾这个zip先把数据库脚本执行成功再解决本地登录问题慢慢来每一步都值得。希望帮到你。本文还有配套的精品资源点击获取