☰
JSP+Servlet学生成绩管理系统实战:从环境搭建到成绩统计导出
2026/10/7 23:39:06 网站建设 项目流程

简介:这是一套面向计算机相关专业学生与Java Web初学者的学生成绩管理系统源码,采用JSP+Servlet技术栈搭配MySQL数据库实现,适合用作毕业设计、课程设计或Web开发练手项目。压缩包共4个文件,约6.33MB,包含可导入的SQL数据库脚本、项目源码压缩包、依赖库文件以及一份使用说明文档,覆盖从建库到部署运行的完整环节。目前已有109人学习下载,说明该方案在同类选题中具备一定参考价值。读者可获得一套结构清晰、可直接编译运行的工程代码,借助说明文档快速完成环境配置与功能验证,同时理解JSP与Servlet的请求处理流程、数据库连接及成绩增删改查等核心逻辑,为后续二次开发或答辩准备提供扎实基础。

1. 从一份 JSP+Servlet 成绩单说起:为什么老技术栈还在接新需求

打开招聘网站搜“学生成绩管理系统”,跳出来的岗位描述里十有八九写着 Spring Boot + Vue。但如果你去翻高校教务处的采购公告、去问二三线城市做教育信息化的外包团队,会发现大量已交付、正在维护、甚至还在新签的系统,底层依然是 JSP + Servlet。这不是技术倒退,而是存量系统的惯性——一套跑在 Tomcat 上的成绩管理模块,部署简单、依赖极少、学校信息中心的运维人员能看懂,换技术栈的迁移成本远高于继续迭代。

这个标题指向的,就是这样一个典型场景:用 JSP 做视图层、Servlet 做控制层、JDBC 或简单 DAO 做数据访问,搭出一套能录入、查询、统计、导出学生成绩的 Web 系统。它适合三类人:课程设计需要交作业的学生、接手了老系统需要二次开发的初级工程师、以及想理解“请求怎么从浏览器走到数据库再走回来”这条完整链路的自学者。热搜里出现的“学生成绩管理系统用例图”“jsp个人信息展示页面”这些词,恰恰说明很多人卡在“知道要做什么功能,但不知道怎么把页面和数据接起来”这一步。接下来的内容,就是把这套系统的骨架拆开,让你能自己搭一遍、改一遍、跑起来。

2. 环境搭建与项目骨架:从零把 Tomcat 跑起来

2.1 JDK、Tomcat、IDE 的版本选择与目录结构

这套技术栈对版本不挑,但组合不对会出玄学问题。我一般推荐 JDK 8 或 JDK 11,Tomcat 用 8.5 或 9.0,IDE 用 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/ │ └── *.jsp

pom.xml里只需要三个核心依赖:javax.servlet-api(scope 设为 provided,因为 Tomcat 自带)、jstl(JSP 页面里做循环和判断用)、mysql-connector-java(连数据库)。版本号不要追新,servlet-api 用 4.0.1,jstl 用 1.2,mysql 驱动用 8.0.x 即可。

<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

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?useSSL=false&serverTimezone=Asia/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); } }

参数说明:serverTimezone=Asia/Shanghai必须加,MySQL 8 不配时区会报The server time zone value is unrecognized。useSSL=false在本地开发时关掉,避免证书警告拖慢启动。生产环境这两个参数要按实际情况调整。

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;charset=UTF-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 contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>成绩列表</title></head> <body> <table border="1"> <tr> <th>学号</th><th>姓名</th><th>课程</th><th>成绩</th><th>考试时间</th> </tr> <c:forEach items="${scoreList}" var="s"> <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。如果字段名是studentName,getter 必须是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;charset=UTF-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 里返回 NULL,Java 取值时要做空判断。

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 { List<ScoreStat> stats = new ScoreDao().getStatistics(); resp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); resp.setHeader("Content-Disposition", "attachment; filename=score_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对应.xlsx,HSSFWorkbook对应.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. 避坑与排查:JSP+Servlet 项目里最容易翻车的五件事

5.1 中文乱码:现象是页面显示问号,原因是编码链路没统一

现象:表单提交后数据库里存的是????,或者 JSP 页面显示乱码。原因通常有三处:JSP 文件本身没存成 UTF-8、request.setCharacterEncoding没调或调晚了、数据库连接 URL 没配characterEncoding=utf8。解决:JSP 顶部<%@ page contentType="text/html;charset=UTF-8" %>,Filter 里第一行就设编码,JDBC URL 加useUnicode=true&characterEncoding=utf8。三处都对齐,乱码基本绝迹。

5.2 404:现象是访问 Servlet 报资源不存在,原因是路径拼错或注解没生效

现象:浏览器访问/score/list返回 404。原因可能是@WebServlet("/score/list")写成了/scoreList,或者web.xml里metadata-complete="true"导致注解被忽略。解决:先看 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) { List<Course> 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 name="teacher">,把判断收拢到 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 prefix="auth" 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-code>500</error-code> <location>/error.jsp</location> </error-page> <error-page> <error-code>404</error-code> <location>/404.jsp</location> </error-page>

逻辑说明:error.jsp里加<%@ page isErrorPage="true" %>,就能用${pageContext.exception.message}取异常信息,但生产环境不要展示给用户,只记日志。404.jsp给一个友好提示和返回首页的链接。

参数说明:error-page对Exception和Throwable也生效,但isErrorPage="true"的页面本身不能再抛异常,否则会循环。我一般会在error.jsp里只写静态提示,异常详情通过log4j或java.util.logging写到文件,方便事后排查。

这套 JSP + Servlet 的成绩管理系统,代码量不大,但把请求流转、数据持久化、页面渲染、权限控制、异常兜底这几件事串了一遍。我自己的习惯是每接一个老系统,先跑通登录和列表页,再顺着一条数据从表单追到数据库再追回页面,链路通了,剩下的功能都是复制粘贴加改改。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询