
简介这是一套面向计算机相关专业学生与Java Web初学者的学生成绩管理系统源码采用JSPServlet技术栈搭配MySQL数据库实现适合用作毕业设计、课程设计或Web开发练手项目。压缩包共4个文件约6.33MB包含可导入的SQL数据库脚本、项目源码压缩包、依赖库文件以及一份使用说明文档覆盖从建库到部署运行的完整环节。目前已有109人学习下载说明该方案在同类选题中具备一定参考价值。读者可获得一套结构清晰、可直接编译运行的工程代码借助说明文档快速完成环境配置与功能验证同时理解JSP与Servlet的请求处理流程、数据库连接及成绩增删改查等核心逻辑为后续二次开发或答辩准备提供扎实基础。1. 从一份 JSPServlet 成绩单说起为什么老技术栈还在接新需求打开招聘网站搜“学生成绩管理系统”跳出来的岗位描述里十有八九写着 Spring Boot Vue。但如果你去翻高校教务处的采购公告、去问二三线城市做教育信息化的外包团队会发现大量已交付、正在维护、甚至还在新签的系统底层依然是 JSP Servlet。这不是技术倒退而是存量系统的惯性——一套跑在 Tomcat 上的成绩管理模块部署简单、依赖极少、学校信息中心的运维人员能看懂换技术栈的迁移成本远高于继续迭代。这个标题指向的就是这样一个典型场景用 JSP 做视图层、Servlet 做控制层、JDBC 或简单 DAO 做数据访问搭出一套能录入、查询、统计、导出学生成绩的 Web 系统。它适合三类人课程设计需要交作业的学生、接手了老系统需要二次开发的初级工程师、以及想理解“请求怎么从浏览器走到数据库再走回来”这条完整链路的自学者。热搜里出现的“学生成绩管理系统用例图”“jsp个人信息展示页面”这些词恰恰说明很多人卡在“知道要做什么功能但不知道怎么把页面和数据接起来”这一步。接下来的内容就是把这套系统的骨架拆开让你能自己搭一遍、改一遍、跑起来。2. 环境搭建与项目骨架从零把 Tomcat 跑起来2.1 JDK、Tomcat、IDE 的版本选择与目录结构这套技术栈对版本不挑但组合不对会出玄学问题。我一般推荐 JDK 8 或 JDK 11Tomcat 用 8.5 或 9.0IDE 用 IntelliJ IDEA 社区版就够。JDK 17 配 Tomcat 9 也能跑但 JSP 编译时偶尔报模块访问异常新手排查起来很痛苦不如直接用 JDK 8 省心。项目目录结构按 Maven 标准来不要用 Eclipse 那种 WebContent 的老结构后期加依赖会乱student-score/ ├── pom.xml ├── src/ │ └── main/ │ ├── java/ │ │ └── com/example/score/ │ │ ├── dao/ │ │ ├── model/ │ │ ├── servlet/ │ │ └── util/ │ ├── resources/ │ │ └── db.properties │ └── webapp/ │ ├── WEB-INF/ │ │ └── web.xml │ ├── css/ │ ├── js/ │ └── *.jsppom.xml里只需要三个核心依赖javax.servlet-apiscope 设为 provided因为 Tomcat 自带、jstlJSP 页面里做循环和判断用、mysql-connector-java连数据库。版本号不要追新servlet-api 用 4.0.1jstl 用 1.2mysql 驱动用 8.0.x 即可。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies逻辑说明provided这个 scope 很关键它告诉 Maven 编译时用这个 jar打包时不要放进去否则和 Tomcat 自带的 servlet-api 冲突启动时报ClassCastException。参数上如果你的 Tomcat 是 10.x包名已经从javax.servlet变成jakarta.servlet所有 import 都要改新手建议直接用 Tomcat 9。2.2 数据库表设计与连接池配置成绩管理系统的表不用多四张就够学生表、课程表、成绩表、用户表。成绩表是核心用学生 ID 和课程 ID 做联合唯一索引防止同一学生同一课程录入多条成绩。CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(20) NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), exam_time DATE, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;DECIMAL(5,2)而不是FLOAT是因为成绩要精确到小数点后一位或两位浮点数会出现 89.99999 这种鬼值导出 Excel 时被教务老师骂。utf8mb4是为了存中文姓名不出乱码。数据库连接不要在每个 Servlet 里DriverManager.getConnection那样每次请求都新建连接并发一高就崩。常见做法是写一个DBUtil类用静态代码块加载驱动或者更规范一点配一个 Druid 连接池。课程设计级别用静态方法就够public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/score_db?useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }参数说明serverTimezoneAsia/Shanghai必须加MySQL 8 不配时区会报The server time zone value is unrecognized。useSSLfalse在本地开发时关掉避免证书警告拖慢启动。生产环境这两个参数要按实际情况调整。2.3 用 Maven 命令启动并验证第一个 JSP 页面骨架搭好后在webapp下建一个index.jsp写一行% new java.util.Date() %然后用 Maven 的 Tomcat 插件启动mvn clean package mvn org.apache.tomcat.maven:tomcat7-maven-plugin:2.2:run浏览器访问http://localhost:8080/student-score/index.jsp看到时间就说明环境通了。这一步看着简单但能过滤掉 80% 的环境问题——如果 404检查webapp目录是否被识别为 Web 根目录如果 500看 Tomcat 日志里 JSP 编译报错的具体行号。我见过有人把 JSP 放在WEB-INF下面直接访问那当然 404因为WEB-INF是保护目录只能通过 Servlet 转发进去。3. Servlet 接收请求与 JSP 渲染把成绩数据从表单送到页面3.1 一个成绩录入 Servlet 的完整写法成绩录入是这套系统里最典型的“表单提交 → 数据库写入 → 返回结果”链路。Servlet 里要做四件事设置编码、取参数、调 DAO、跳转页面。WebServlet(/score/add) public class AddScoreServlet extends HttpServlet { private ScoreDao scoreDao new ScoreDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String studentId req.getParameter(studentId); String courseIdStr req.getParameter(courseId); String scoreStr req.getParameter(score); // 参数校验空值和数字格式 if (studentId null || studentId.trim().isEmpty() || courseIdStr null || scoreStr null) { req.setAttribute(msg, 学号、课程、成绩均不能为空); req.getRequestDispatcher(/score/add.jsp).forward(req, resp); return; } try { int courseId Integer.parseInt(courseIdStr); double score Double.parseDouble(scoreStr); if (score 0 || score 100) { req.setAttribute(msg, 成绩必须在0到100之间); req.getRequestDispatcher(/score/add.jsp).forward(req, resp); return; } Score s new Score(studentId, courseId, score); boolean ok scoreDao.insert(s); if (ok) { resp.sendRedirect(req.getContextPath() /score/list); } else { req.setAttribute(msg, 录入失败可能该学生该课程已有成绩); req.getRequestDispatcher(/score/add.jsp).forward(req, resp); } } catch (NumberFormatException e) { req.setAttribute(msg, 成绩格式不正确); req.getRequestDispatcher(/score/add.jsp).forward(req, resp); } } }逻辑说明setCharacterEncoding必须在取参数之前调用否则中文参数会乱码。校验分两层先判空再判数值范围任何一层不过就forward回原页面并带错误提示。成功后用sendRedirect而不是forward是为了防止用户刷新页面重复提交——这是血泪经验当年没做重定向教务老师双击提交按钮同一个成绩录了两遍。参数说明req.getContextPath()返回项目部署路径拼在重定向 URL 前面避免项目名变化后跳转 404。WebServlet注解代替了web.xml配置Tomcat 7 以上都支持少写一个文件。3.2 JSP 页面用 JSTL 循环渲染成绩列表Servlet 查完数据后request.setAttribute(scoreList, list)转发到 JSP。JSP 里不要写 Java 脚本片段用 JSTL 和 EL 表达式页面干净且不容易出空指针。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle成绩列表/title/head body table border1 tr th学号/thth姓名/thth课程/thth成绩/thth考试时间/th /tr c:forEach items${scoreList} vars tr td${s.studentId}/td td${s.studentName}/td td${s.courseName}/td td${s.score}/td td${s.examTime}/td /tr /c:forEach /table c:if test${empty scoreList} p暂无成绩记录/p /c:if /body /html逻辑说明c:forEach的items属性直接写 EL 表达式${scoreList}不需要% %。c:if test${empty scoreList}处理空列表比在 Java 里判断再传标志位优雅。注意 JSP 顶部taglib指令不能少否则c:forEach会被当成普通 HTML 标签原样输出。参数说明${s.score}调用的是Score对象的getScore()方法所以实体类里必须有对应的 getter。如果字段名是studentNamegetter 必须是getStudentName()EL 对大小写敏感。3.3 用 Filter 统一处理中文乱码和登录拦截每个 Servlet 都写一遍setCharacterEncoding太蠢用 Filter 统一处理。同时登录拦截也在这里做没登录的请求直接踢回登录页。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String uri request.getRequestURI(); // 放行登录页、登录接口、静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(/login) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }逻辑说明WebFilter(/*)拦截所有请求放行名单里必须包含登录页和静态资源否则登录页自己也被拦截形成死循环。chain.doFilter是放行到下一个过滤器或目标 Servlet不调用它请求就断在这里了。参数说明uri.contains(/css/)这种判断方式比较粗糙如果项目路径里恰好有css字样会误放行更严谨的做法是用request.getServletPath()判断后缀。课程设计级别用contains够用生产环境建议换成精确匹配。4. 成绩统计与导出从 SQL 聚合到 Excel 下载4.1 用 SQL 做班级平均分和及格率统计成绩管理系统光有录入和列表不够教务最常要的是统计。平均分、最高分、最低分、及格率这些用 SQL 聚合函数一次查出来不要在 Java 里循环算。SELECT c.course_name, COUNT(*) AS total, ROUND(AVG(s.score), 2) AS avg_score, MAX(s.score) AS max_score, MIN(s.score) AS min_score, ROUND(SUM(CASE WHEN s.score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS pass_rate FROM score s JOIN course c ON s.course_id c.id GROUP BY c.course_name;逻辑说明CASE WHEN是统计及格人数的关键SUM配合CASE相当于条件计数。ROUND保留两位小数避免前端显示一长串。GROUP BY按课程分组如果还要按班级分再加一个JOIN student和GROUP BY c.course_name, st.class_name。参数说明pass_rate算出来是百分比数值前端加%即可。如果某课程没人考COUNT(*)为 0 会导致除零错误MySQL 里返回 NULLJava 取值时要做空判断。4.2 用 POI 导出 Excel 并处理浏览器下载响应统计结果要能导出成 Excel教务老师才认。用 Apache POI 生成.xlsx文件通过response的OutputStream写回浏览器。WebServlet(/score/export) public class ExportServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListScoreStat stats new ScoreDao().getStatistics(); resp.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); resp.setHeader(Content-Disposition, attachment; filenamescore_stat.xlsx); try (XSSFWorkbook wb new XSSFWorkbook(); OutputStream out resp.getOutputStream()) { XSSFSheet sheet wb.createSheet(成绩统计); XSSFRow header sheet.createRow(0); String[] cols {课程, 人数, 平均分, 最高分, 最低分, 及格率}; for (int i 0; i cols.length; i) { header.createCell(i).setCellValue(cols[i]); } int rowIdx 1; for (ScoreStat st : stats) { XSSFRow row sheet.createRow(rowIdx); row.createCell(0).setCellValue(st.getCourseName()); row.createCell(1).setCellValue(st.getTotal()); row.createCell(2).setCellValue(st.getAvgScore()); row.createCell(3).setCellValue(st.getMaxScore()); row.createCell(4).setCellValue(st.getMinScore()); row.createCell(5).setCellValue(st.getPassRate() %); } wb.write(out); } } }逻辑说明Content-Disposition设为attachment才会触发浏览器下载而不是直接打开。try-with-resources确保Workbook和OutputStream自动关闭否则文件写不完整。XSSFWorkbook对应.xlsxHSSFWorkbook对应.xls别用混了。参数说明filename如果包含中文需要URLEncoder.encode(成绩统计.xlsx, UTF-8)处理否则某些浏览器下载下来文件名是乱码。POI 的setCellValue对double和String有重载传错类型编译期就报错。4.3 分页查询的 SQL 写法与 PageHelper 的取舍成绩列表数据一多就要分页。最朴素的做法是LIMIT offset, size配合一条COUNT(*)查总数。-- 查总数 SELECT COUNT(*) FROM score s JOIN student st ON s.student_id st.id; -- 查当前页 SELECT s.*, st.name, c.course_name FROM score s JOIN student st ON s.student_id st.id JOIN course c ON s.course_id c.id ORDER BY s.id DESC LIMIT #{offset}, #{size};逻辑说明offset (pageNum - 1) * pageSize在 Java 里算好传进来。ORDER BY必须加否则分页结果不稳定同一页刷新两次可能看到不同数据。参数说明LIMIT在数据量大时性能差因为要扫描 offset 行。课程设计数据量小无所谓生产环境建议用游标分页WHERE id lastId LIMIT size。PageHelper 插件能自动拼分页 SQL但引入额外依赖老项目里手写更可控。5. 避坑与排查JSPServlet 项目里最容易翻车的五件事5.1 中文乱码现象是页面显示问号原因是编码链路没统一现象表单提交后数据库里存的是????或者 JSP 页面显示乱码。原因通常有三处JSP 文件本身没存成 UTF-8、request.setCharacterEncoding没调或调晚了、数据库连接 URL 没配characterEncodingutf8。解决JSP 顶部% page contentTypetext/html;charsetUTF-8 %Filter 里第一行就设编码JDBC URL 加useUnicodetruecharacterEncodingutf8。三处都对齐乱码基本绝迹。5.2 404现象是访问 Servlet 报资源不存在原因是路径拼错或注解没生效现象浏览器访问/score/list返回 404。原因可能是WebServlet(/score/list)写成了/scoreList或者web.xml里metadata-completetrue导致注解被忽略。解决先看 Tomcat 启动日志里有没有注册这个 Servlet 的映射没有的话检查注解拼写和web.xml的metadata-complete属性。另外web.xml版本低于 3.0 不支持注解要换成 3.1 或 4.0 的 schema。5.3 数据库连接泄漏现象是跑一会儿就报 Too many connections现象系统用一段时间后所有请求都卡住日志报Too many connections。原因是在 DAO 里开了Connection但没在finally里关闭或者ResultSet、PreparedStatement没关。解决所有 JDBC 资源用 try-with-resources 包起来或者用连接池并确保close()是归还而不是真正关闭。检查方法在 MySQL 里执行SHOW PROCESSLIST看有多少 Sleep 状态的连接堆积。5.4 JSP 页面刷新重复提交现象是刷新后数据多了一条现象成绩录入成功后按 F5又插入一条相同记录。原因是表单提交后直接forward到结果页浏览器地址栏还是 POST 的 URL。解决提交成功后用response.sendRedirect重定向到列表页浏览器地址栏变成 GET 请求刷新不会重复提交。这是 PRG 模式Post-Redirect-Get老技术栈里同样适用。5.5 导出 Excel 文件名乱码现象是下载下来文件名是乱码或变成 download现象点击导出按钮下载的文件名不是“成绩统计.xlsx”而是一串乱码。原因是Content-Disposition里的 filename 没有做 URL 编码或者不同浏览器对编码方式支持不一致。解决用URLEncoder.encode(fileName, UTF-8).replaceAll(\\, %20)处理并设置filename*UTF-8格式。如果还不行退而求其次用纯英文文件名在页面提示里说明内容。6. 进阶技巧用监听器预热数据与 JSP 自定义标签封装权限判断6.1 ServletContextListener 在项目启动时加载课程列表课程列表这种全站都要用的数据没必要每次请求都查库。写一个ServletContextListener在项目启动时查一次放到ServletContext里所有 JSP 和 Servlet 都能取。WebListener public class InitListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { ListCourse courses new CourseDao().findAll(); sce.getServletContext().setAttribute(courseList, courses); } }逻辑说明contextInitialized在 Tomcat 启动完成后调用此时数据库连接可用。把courseList放进ServletContext而不是Session因为它是全局共享的放 Session 每个用户存一份浪费内存。JSP 里用${applicationScope.courseList}取。参数说明如果课程数据会动态增删这个缓存会过期需要在增删课程的 Servlet 里同步更新ServletContext里的属性。课程设计级别数据不变启动加载一次就够。6.2 自定义 JSP 标签封装“只有教师才能看成绩”的判断JSP 里到处写c:if test${sessionScope.currentUser.role teacher}很啰嗦而且角色判断逻辑散落各处改起来容易漏。写一个自定义标签auth:role nameteacher把判断收拢到 Java 类里。public class RoleTag extends SimpleTagSupport { private String name; public void setName(String name) { this.name name; } Override public void doTag() throws JspException, IOException { PageContext pc (PageContext) getJspContext(); User user (User) pc.getSession().getAttribute(currentUser); if (user ! null name.equals(user.getRole())) { getJspBody().invoke(null); } } }逻辑说明SimpleTagSupport的doTag里判断角色匹配才invoke标签体内容不匹配直接跳过。需要在.tld文件里注册这个标签JSP 顶部用% taglib prefixauth uri/WEB-INF/auth.tld %引入。参数说明name属性对应角色字符串比如teacher、admin、student。如果角色层级复杂可以在doTag里扩展成“教师及以上”的判断逻辑但别把业务规则写死在标签里标签只负责“显示或不显示”具体权限规则放在 Service 层。6.3 用 JSP 的 errorPage 做统一异常兜底最后一个技巧跟“后悔药”有关线上系统最怕用户看到 500 堆栈。在web.xml里配全局错误页或者在 JSP 顶部加% page errorPage/error.jsp %把异常统一收口。error-page error-code500/error-code location/error.jsp/location /error-page error-page error-code404/error-code location/404.jsp/location /error-page逻辑说明error.jsp里加% page isErrorPagetrue %就能用${pageContext.exception.message}取异常信息但生产环境不要展示给用户只记日志。404.jsp给一个友好提示和返回首页的链接。参数说明error-page对Exception和Throwable也生效但isErrorPagetrue的页面本身不能再抛异常否则会循环。我一般会在error.jsp里只写静态提示异常详情通过log4j或java.util.logging写到文件方便事后排查。这套 JSP Servlet 的成绩管理系统代码量不大但把请求流转、数据持久化、页面渲染、权限控制、异常兜底这几件事串了一遍。我自己的习惯是每接一个老系统先跑通登录和列表页再顺着一条数据从表单追到数据库再追回页面链路通了剩下的功能都是复制粘贴加改改。希望帮到你。本文还有配套的精品资源点击获取