简介这是一套基于ServletJSPMySQL实现的JavaWeb学生成绩管理系统升级版面向Java初学者与Web开发入门学习者聚焦教学管理场景下的成绩录入、查询、统计与可视化需求。资源包共344个文件包含86个Java源码如ScoreDaoImpl、StudentDaoImpl等核心DAO与Service类、86个编译后class文件、62个JavaScript脚本含ECharts图表初始化与数据渲染逻辑、37个JSP页面涵盖登录、课程管理、成绩录入等完整功能视图以及28个CSS样式文件整体压缩包仅3.59MB轻量易部署。已有6724人学习下载体现了其在实践教学中的广泛认可。读者可直接运行项目获得含前后端交互、数据库操作、权限控制及ECharts动态统计图如各科成绩分布、班级平均分趋势在内的完整可运行系统同时通过清晰的分层代码结构DAO/Service/Servlet/JSP深入理解JavaWeb经典MVC开发模式。1. 这不是又一个“学生信息管理系统”ServletJSPMySQLecharts 四件套落地真能跑通成绩分析闭环你肯定见过太多标着“JavaWeb学生管理系统”的课程设计压缩包——点开一看登录页能进添加学生能存但一到“按班级查平均分”就报空指针echarts 图表永远显示“Loading…”数据库建了五张表却只用了一张JSP 里混着 Java 脚本和 HTML连个基本的 SQL 注入防护都没有。这不是教学 Demo 的问题是资源没把「真实业务流」跑通从成绩录入 → 数据持久化 → 多维查询 → 可视化呈现四个环节必须环环相扣、参数对得上、路径不越界、前后端数据类型不打架。这个资源不一样它用标准 Servlet 生命周期管理请求JSP 仅作视图渲染无 scriptletMySQL 表结构含 student、course、score、teacher 四张主表并设外键约束echarts 图表全部通过 Ajax 异步加载 JSON 数据且所有图表都带「按学期筛选」「按课程筛选」下拉联动逻辑。适合正在做课程设计、需要交完整可运行源码的 Java 初学者也适合想快速复现一个轻量级教务后台的实训导师——它不炫技但每一步都能在 Tomcat 9 JDK 8 环境下实测启动、增删改查、图表刷新。2. 项目结构与技术栈选型为什么是 Servlet 而不是 Spring Boot为什么 JSP 没被抛弃2.1 目录结构即开发逻辑src 下三层WebContent 下三类这个项目的目录组织不是随意堆砌而是严格对应 JavaWeb 标准部署规范。src/下只有三个包com.example.servlet所有控制器类如ScoreAddServlet.java、ChartQueryServlet.java每个类只处理单一 HTTP 请求不掺杂业务逻辑com.example.dao数据访问层含ScoreDAO.java、CourseDAO.java封装 JDBC 操作使用PreparedStatement防注入所有 SQL 语句外部化为常量字符串com.example.entity纯 POJO字段名与 MySQL 表列名完全一致如student_id对应Student类中private int studentId避免 MyBatis 映射歧义。WebContent/下则划清职责边界/jsp/所有.jsp文件仅含% %输出表达式和c:if等 JSTL 标签零行 Java 代码% %全禁/js/chart.js封装 echarts 初始化逻辑含initBarChart()、initPieChart()两个函数接收后端返回的 JSON 并自动渲染/css/style.css仅控制布局与响应式断点不干预图表容器尺寸——echarts 容器宽高由div#chart-container的stylewidth:100%;height:400px;硬编码这是避坑关键。提示不要试图把web.xml删除改用注解配置。本项目依赖web.xml中的servlet-mapping显式绑定 URL 路径如/score/add→ScoreAddServletTomcat 7 默认不扫描WebServlet注解强行改会直接 404。2.2 Servlet 是骨架JSP 是皮肤MySQL 是血肉echarts 是神经末梢很多人问“都 2024 年了还学 ServletJSP”——答案是当你要讲清楚 HTTP 请求如何从浏览器抵达数据库再返回图表就必须拆开看。Spring Boot 自动装配掩盖了 request.getParameter() 如何取值、response.getWriter() 如何写 JSON、session 如何跨跳转保持、filter 怎么拦截未登录请求这些底层事实。而本项目每一处都暴露给你登录校验走LoginFilter.java检查session.getAttribute(user) ! null否则重定向到/login.jsp成绩录入用ScoreAddServletrequest.getParameter(studentId)取值后强转Integer.parseInt()失败则跳回表单页并带错误提示echarts 数据来源是ChartQueryServlet它不返回 HTML而是response.setContentType(application/json); response.getWriter().write(jsonString)前端$.ajax()拿到后直接喂给echarts.init(dom).setOption(option)。MySQL 方面建表脚本db_init.sql包含明确约束CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) CHECK(score BETWEEN 0 AND 100), semester VARCHAR(20) NOT NULL, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) );注意CHECK(score BETWEEN 0 AND 100)和双外键这比“随便建个 score 表插数据”更能训练数据完整性意识。2.3 echarts 不是贴图是动态数据管道JSON 结构决定图表形态本项目 echarts 图表不写死数据所有 option 配置项中的series.data均为空数组[]靠 Ajax 动态填充。关键在于后端返回的 JSON 结构必须与前端 JS 解析逻辑严格匹配。例如柱状图要求{ categories: [数学, 英语, 物理], values: [85.5, 78.0, 92.3] }而饼图要求{ data: [ {name: 优秀, value: 12}, {name: 良好, value: 25}, {name: 及格, value: 8} ] }ChartQueryServlet根据请求参数typebar或typepie分支处理调用不同 DAO 方法组装对应 JSON。这种“请求驱动数据结构”的设计让初学者一眼看懂不是 echarts 决定数据是业务需求决定 JSON 格式JSON 格式决定图表类型。3. 核心功能实现从成绩录入到多维统计四步闭环全代码实录3.1 成绩录入Servlet 接参 → DAO 插库 → 页面跳转带提示成绩录入页面add_score.jsp是一个标准 HTML 表单methodpostaction/score/addform action/score/add methodpost select namestudentId c:forEach items${studentList} vars option value${s.id}${s.name} (${s.studentNo})/option /c:forEach /select select namecourseId c:forEach items${courseList} varc option value${c.id}${c.courseName}/option /c:forEach /select input typenumber namescore step0.1 min0 max100 required input typetext namesemester placeholder例如2023-2024-1 required button typesubmit提交成绩/button /form对应ScoreAddServlet.java的核心逻辑protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { try { // 1. 严格取参并校验非空 String studentIdStr request.getParameter(studentId); String courseIdStr request.getParameter(courseId); String scoreStr request.getParameter(score); String semester request.getParameter(semester); if (studentIdStr null || courseIdStr null || scoreStr null || semester null) { request.setAttribute(msg, 请填写完整信息); request.getRequestDispatcher(/jsp/add_score.jsp).forward(request, response); return; } // 2. 类型转换捕获 NumberFormatException int studentId Integer.parseInt(studentIdStr); int courseId Integer.parseInt(courseIdStr); double score Double.parseDouble(scoreStr); // 3. 调用 DAO 插入 Score scoreObj new Score(); scoreObj.setStudentId(studentId); scoreObj.setCourseId(courseId); scoreObj.setScore(score); scoreObj.setSemester(semester); ScoreDAO dao new ScoreDAO(); boolean success dao.insert(scoreObj); // 4. 根据结果跳转带成功/失败提示 if (success) { request.setAttribute(msg, 成绩录入成功); request.getRequestDispatcher(/jsp/score_list.jsp).forward(request, response); } else { request.setAttribute(msg, 录入失败请重试); request.getRequestDispatcher(/jsp/add_score.jsp).forward(request, response); } } catch (NumberFormatException e) { request.setAttribute(msg, 分数或ID格式错误请输入数字); request.getRequestDispatcher(/jsp/add_score.jsp).forward(request, response); } }参数说明request.getParameter()是 Servlet 获取表单数据的唯一正道不能用request.getQueryString()解析 POSTInteger.parseInt()和Double.parseDouble()必须包裹在 try-catch 中否则 400 错误会直接暴露给用户request.getRequestDispatcher().forward()是服务端跳转URL 不变可携带request.setAttribute()数据response.sendRedirect()是客户端重定向URL 改变无法传 request 属性。3.2 成绩列表JSP 使用 JSTL 循环展示DAO 分页查询防 OOMscore_list.jsp不再用% while(rs.next()) { %脚本而是标准 JSTL% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:if test${empty scoreList} p暂无成绩记录/p /c:if c:forEach items${scoreList} vars tr td${s.studentName}/td td${s.courseName}/td td${s.score}/td td${s.semester}/td td a href/score/delete?id${s.id}删除/a | a href/score/edit?id${s.id}编辑/a /td /tr /c:forEach对应ScoreDAO.java的分页查询方法public ListScore listByPage(int pageNum, int pageSize) { ListScore list new ArrayList(); String sql SELECT s.id, s.score, s.semester, st.name AS studentName, st.studentNo, c.courseName 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 ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); // 起始位置 ps.setInt(2, pageSize); // 每页条数 ResultSet rs ps.executeQuery(); while (rs.next()) { Score score new Score(); score.setId(rs.getInt(id)); score.setScore(rs.getDouble(score)); score.setSemester(rs.getString(semester)); score.setStudentName(rs.getString(studentName)); score.setStudentNo(rs.getString(studentNo)); score.setCourseName(rs.getString(courseName)); list.add(score); } } catch (SQLException e) { e.printStackTrace(); } return list; }关键点JOIN 查询一次性获取学生姓名、课程名避免 N1 查询即查出 score 后再循环查 student 表LIMIT ?, ?参数化分页防止 SQL 注入(pageNum-1)*pageSize是 MySQL 偏移量计算公式DBUtil.getConnection()是封装好的工具类内部使用DriverManager.getConnection()连接字符串含useSSLfalseserverTimezoneGMT%2B8适配 MySQL 8 时区问题。3.3 统计图表Servlet 返回 JSONJS 初始化 echarts联动筛选实时刷新chart.jsp页面结构极简div classfilter-bar select idsemesterSelect option value全部学期/option c:forEach items${semesterList} varsem option value${sem}${sem}/option /c:forEach /select select idcourseSelect option value全部课程/option c:forEach items${courseList} varc option value${c.id}${c.courseName}/option /c:forEach /select button onclickloadCharts()刷新图表/button /div div idbarChart stylewidth:100%;height:400px;/div div idpieChart stylewidth:100%;height:400px;/div script src/js/echarts.min.js/script script src/js/chart.js/script script // 页面加载时初始化 window.onload function() { loadCharts(); }; /scriptchart.js的核心是loadCharts()函数function loadCharts() { const semester document.getElementById(semesterSelect).value; const courseId document.getElementById(courseSelect).value; // 1. 加载柱状图各班平均分 $.ajax({ url: /chart/query?typebar, data: { semester: semester, courseId: courseId }, dataType: json, success: function(data) { const barDom document.getElementById(barChart); const barChart echarts.init(barDom); const option { title: { text: 班级平均分统计 }, tooltip: {}, xAxis: { data: data.categories }, yAxis: {}, series: [{ name: 平均分, type: bar, data: data.values }] }; barChart.setOption(option); } }); // 2. 加载饼图成绩等级分布 $.ajax({ url: /chart/query?typepie, data: { semester: semester, courseId: courseId }, dataType: json, success: function(data) { const pieDom document.getElementById(pieChart); const pieChart echarts.init(pieDom); const option { title: { text: 成绩等级分布 }, tooltip: { trigger: item }, series: [{ name: 等级, type: pie, radius: 50%, data: data.data }] }; pieChart.setOption(option); } }); }后端ChartQueryServlet.java的doGet方法protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String type request.getParameter(type); String semester request.getParameter(semester); String courseIdStr request.getParameter(courseId); JSONObject result new JSONObject(); if (bar.equals(type)) { // 柱状图按班级统计平均分 ListObject[] dataList chartDAO.getAvgScoreByClass(semester, courseIdStr); JSONArray categories new JSONArray(); JSONArray values new JSONArray(); for (Object[] row : dataList) { categories.add(row[0].toString()); // class_name values.add(Double.parseDouble(row[1].toString())); // avg_score } result.put(categories, categories); result.put(values, values); } else if (pie.equals(type)) { // 饼图按等级统计人数 ListObject[] dataList chartDAO.getCountByGrade(semester, courseIdStr); JSONArray dataArr new JSONArray(); for (Object[] row : dataList) { JSONObject item new JSONObject(); item.put(name, row[0].toString()); // grade_level item.put(value, Integer.parseInt(row[1].toString())); // count dataArr.add(item); } result.put(data, dataArr); } response.setContentType(application/json;charsetUTF-8); response.getWriter().write(result.toJSONString()); }参数说明dataType: json告诉 jQuery 自动解析 JSON不用JSON.parse()response.setContentType(application/json;charsetUTF-8)必须设置否则中文乱码result.toJSONString()使用 alibaba fastjson比Gson.toJson()更轻量项目 lib 下已包含fastjson-1.2.83.jar。4. 避坑指南五个真实翻车现场与血泪修复方案4.1 现象echarts 图表区域一片空白控制台无报错Network 查看返回 JSON 正常原因div#barChart容器没有设置宽高echarts 初始化时getBoundingClientRect()返回{width:0,height:0}导致渲染失败。常见于开发者删掉了stylewidth:100%;height:400px;试图用 CSS 类控制但类中height:400px被父容器overflow:hidden截断。解决强制在 HTML 中写内联样式div idbarChart stylewidth:100%;height:400px;/div并在chart.js初始化前加检测const barDom document.getElementById(barChart); if (barDom.offsetWidth 0 || barDom.offsetHeight 0) { console.error(Chart container has zero size!); barDom.style.height 400px; }4.2 现象添加成绩时提示“java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver”原因MySQL 8 驱动类名从com.mysql.jdbc.Driver变更为com.mysql.cj.jdbc.Driver而DBUtil.java中仍写旧类名或mysql-connector-java-8.0.33.jar未放入WebContent/WEB-INF/lib/目录Tomcat 运行时只加载此目录下的 jar。解决打开DBUtil.java将Class.forName(com.mysql.jdbc.Driver)改为Class.forName(com.mysql.cj.jdbc.Driver)确认 jar 包在WEB-INF/lib/下且文件名不含-sources或-javadoc后缀。4.3 现象JSP 页面中文显示为???数据库存入也是乱码但 Navicat 查看表结构显示utf8mb4原因MySQL 连接字符串缺少characterEncodingutf8参数或 Tomcatserver.xml中Connector未设URIEncodingUTF-8。解决修改DBUtil.getConnection()中的 URLString url jdbc:mysql://localhost:3306/school_db?useSSLfalseserverTimezoneGMT%2B8characterEncodingutf8;同时检查conf/server.xml确保Connector port8080 ... URIEncodingUTF-8 /存在。4.4 现象点击“删除”链接后成绩确实没了但页面跳转到http://localhost:8080/score/delete?id123显示 404原因web.xml中servlet-mapping的url-pattern写成了/score/delete/*带星号而链接是/score/delete?id123查询参数Servlet 无法匹配。正确应为/score/delete。解决检查web.xmlservlet-mapping servlet-nameDeleteScoreServlet/servlet-name url-pattern/score/delete/url-pattern !-- 不能有 * -- /servlet-mapping4.5 现象echarts 饼图点击某扇形后页面跳转到undefined原因chart.js中未给饼图 series 添加click事件监听或监听函数中params.name取值后未拼接正确 URL。本项目默认不启用点击跳转若自行添加需确保pieChart.on(click, function(params) { const grade params.name; // 如 优秀 window.location.href /score/list?grade encodeURIComponent(grade); });且ScoreListServlet中要能解析request.getParameter(grade)并查库。5. 进阶技巧让统计图表支持导出 PNG且兼容 IE11 的降级方案5.1 一键导出 PNGecharts 内置方法 后端兜底echarts 本身提供chart.getDataURL()方法生成 base64 图片但直接window.open(dataUrl)在 Chrome 有效在 IE11 会失败。更稳妥的做法是前端生成 base64 后 POST 给 Servlet由后端转成 PNG 文件流返回chart.js中添加导出按钮事件function exportBarChart() { const barChart echarts.getInstanceByDom(document.getElementById(barChart)); if (!barChart) return; const dataUrl barChart.getDataURL({ type: png, pixelRatio: 2, // 高清 backgroundColor: #fff }); // 构造表单提交 base64 const form document.createElement(form); form.method post; form.action /chart/export; form.style.display none; const input document.createElement(input); input.type hidden; input.name imageData; input.value dataUrl; form.appendChild(input); document.body.appendChild(form); form.submit(); }后端ExportServlet.java接收并写文件protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String imageData request.getParameter(imageData); if (imageData null || !imageData.startsWith(data:image/png;base64,)) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, Invalid image data); return; } // 解码 base64 String base64Image imageData.substring(data:image/png;base64,.length()); byte[] imageBytes Base64.getDecoder().decode(base64Image); // 设置响应头触发下载 response.setContentType(image/png); response.setHeader(Content-Disposition, attachment; filenamescore_chart.png); response.getOutputStream().write(imageBytes); }注意Base64.getDecoder()是 JDK 8 内置类无需额外依赖Content-Disposition中attachment强制下载inline则在浏览器中打开。5.2 IE11 兼容性补丁polyfill CSS 降级IE11 不支持Promise、Array.from()、Object.assign()而 echarts 4.0 依赖这些。必须引入 polyfill在chart.jsphead中加入!--[if IE] script srchttps://cdn.jsdelivr.net/npm/promise-polyfill8/dist/polyfill.min.js/script script srchttps://cdn.jsdelivr.net/npm/array-from-polyfill1.0.0/index.min.js/script ![endif]--同时echarts 容器 CSS 需避免 Flex 布局IE11 Flex bug 多改用传统浮动.chart-container { zoom: 1; } .chart-container:before, .chart-container:after { content: ; display: table; } .chart-container:after { clear: both; } #barChart, #pieChart { float: left; width: 100%; margin-bottom: 20px; }5.3 成绩预警功能在图表中高亮低于 60 分的班级业务常需“一眼看出问题班级”。可在柱状图中为低于 60 的柱子染红色修改ChartQueryServlet返回的 JSON增加colors字段// 在返回柱状图数据时 JSONArray colors new JSONArray(); for (Object[] row : dataList) { double avg Double.parseDouble(row[1].toString()); colors.add(avg 60 ? #e74c3c : #3498db); // 红色预警蓝色正常 } result.put(colors, colors);前端chart.js中series: [{ name: 平均分, type: bar, data: data.values, itemStyle: { color: function(params) { return data.colors[params.dataIndex]; } } }]这样当某班平均分跌破 60柱子立刻变红无需额外开发告警模块。从那以后我每次交付 JavaWeb 项目都会在web.xml里手敲一遍servlet-mapping在DBUtil里核对三遍驱动类名把echarts.init(dom)前的宽高检查写成函数模板。不是信不过 IDE是信不过自己哪次手抖漏掉的分号。希望帮到你。本文还有配套的精品资源点击获取