☰
Servlet+JSP+MySQL学生成绩管理系统开发实战
2026/10/12 5:16:05 网站建设 项目流程

简介:基于Servlet、JSP与MySQL实现的JavaWeb学生成绩管理系统,属于原项目的升级版本,主要新增了Echarts统计图模块。适合JavaWeb初学者、正在做课程设计或毕业设计的学生,也适合小型教务场景下的成绩录入、查询与可视化展示。压缩包共344个文件,约3.59MB,核心文件类型包括Java源码与编译后的class文件、JSP动态页面、JavaScript脚本、CSS样式表,以及项目所需的JAR依赖包、配置文件和SQL数据库脚本;前端已集成Echarts相关资源,后端结构清晰,便于直接导入开发工具运行。目前已有6723人学习/下载。通过这套源码可以完整看到Servlet控制层、业务逻辑层与数据访问层的分工,了解成绩增删改查与统计图表如何联动,同时可借助SQL脚本快速初始化数据库,作为课程设计、毕业设计或项目二次开发的起点。

1. 学生成绩管理系统为什么值得用 Servlet + JSP + MySQL 重写一遍

上半年临时接手一个成绩管理需求,对方提了一句“页面上能不能加统计图”。第一反应是上 Spring Boot + Vue,结果机房环境连 Maven 仓库都拉不动,最后老老实实用 Servlet + JSP + MySQL 这套 javaweb 学生成绩管理系统把活干了。这套组合不是复杂企业应用的对手,但在成绩管理这个场景里恰好够用:管理员登录、学生档案维护、课程信息维护、成绩录入与修改,以及最新补上的 ECharts 统计图,五个环节全部打通。班级平均分对比、分数段分布、单科最高最低分都能直接看图说话。适合正在做 JavaWeb 课设、或者想补原生 Servlet/JSP 底子的开发者,拿到源码按文档复现,半天内能在本地 Tomcat 上把系统跑起来。

2. 三层架构与数据库设计:先把学生、课程、成绩三张表的边界想清楚

2.1 为什么不直接上 Spring Boot:这套组合的选型逻辑

很多人看到“Servlet + JSP”第一反应是劝退,觉得都什么年代了还在用这套。我得老实说一句:课设、内部小工具、毕业设计答辩场景,原生 Servlet 反而是最优解。原因是它的调用链短到你不需要猜——一个请求进来,被哪个 Servlet 接住,调了哪个 DAO 方法,执行了哪条 SQL,全部肉眼可见。Spring Boot 虽然省事,但它帮你把事务、依赖注入、自动配置都包起来了,一旦出问题就是一层一层剥洋葱,对新手来说反而是黑匣子。

再有就是考核场景的实际细节。很多课设评分标准里明确写“必须体现 Servlet、JSP、JDBC、Session”这些技术点,你拿 Spring Boot 交上去,老师反而很难判定底层功底。用原生 Servlet 虽然代码量多一点,但每个技术点都摆在那里,答辩时能讲的东西也多。JSP 自带page、request、session这些内置对象,写列表页比用模板引擎还直观,不需要额外引入依赖。这套组合的维护成本基本等于零,一个 Tomcat 加一个 MySQL 就能跑,不用配 Nginx,不用写 Dockerfile,对没有运维经验的学生群体最友好。

2.2 数据库设计:三张基础表加一个成绩关联表

成绩管理系统的核心是数据模型,表设计定了,后面所有代码都是围着表转。我见过不少翻车案例,问题几乎都出在成绩表上:要么没做唯一约束导致同一学生同一课程录了两条成绩,要么分数字段用了 INT 导致 89.5 这种分数被截断。这里给出我通常用的建表方案,四张表,关系非常清晰:

-- 管理员表 CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(30) NOT NULL, class_name VARCHAR(30) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(30) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩表:三张表中的核心关联表 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) NOT NULL, exam_date DATE, CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id), CONSTRAINT uk_student_course UNIQUE (student_id, course_id), CONSTRAINT chk_score CHECK (score >= 0 AND score <= 100) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个关键设计点要说明白。student_no用业务学号做唯一约束,而不是只靠自增主键,这样从 Excel 导入数据时能通过学号直接判断重复。成绩表的uk_student_course联合唯一约束是防重录的关键,同一学生同一课程只能有一条成绩记录,第二次插入直接报错,比在 Java 代码里查一遍再插入可靠得多。成绩字段用DECIMAL(5,2)而不是 INT,保留两位小数,期末成绩 89.5 这种场景才不会丢精度。

需要留意一个兼容性问题:CHECK约束在 MySQL 5.7 及之前版本是解析通过但不生效的,只有 8.0.16 之后才真正强制执行。如果你用的是 5.7,分数范围必须在后端代码里校验,不能只依赖数据库约束。字符集统一用utf8mb4而不是utf8,前者是后者的超集,遇到生僻字或者 Emoji 符号不会乱码。外键约束建议保留,虽然有些人为了性能会去掉,但成绩系统数据量小,外键能保证你不小心删了学生档案后成绩表里残留孤儿数据。

2.3 DAO 层与连接管理:一个轻量 JDBC 工具类就够了

数据库连接的管理方式直接影响代码的整洁度。我见过有人在每个 Servlet 里DriverManager.getConnection(),写 12 个接口就复制 12 遍,后来要改数据库密码恨不得全文替换。正确的做法是抽一个JdbcUtil工具类,把驱动加载和连接获取收拢到一个地方:

public class JdbcUtil { private static final String URL = "jdbc:mysql://localhost:3306/grade_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败", e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception ignored) {} } } } }

连接串里的三个参数是有讲究的。useSSL=false用来关掉本地开发时 MySQL 8 默认的 SSL 握手,不关的话控制台会刷一堆警告,连接速度也会慢。serverTimezone=Asia/Shanghai解决 MySQL 8 与 JDBC 驱动之间的时区歧义报错,少了这个直接抛异常。characterEncoding=utf8保证数据库读写中文不乱码。驱动类名要注意版本差异,MySQL 8.x 驱动要求写全类名com.mysql.cj.jdbc.Driver,而 5.x 驱动是com.mysql.jdbc.Driver,写混了直接ClassNotFoundException。

在此基础上封装一个通用查询方法,可以省掉每个 DAO 里重复的模板代码:

public static <T> List<T> query(String sql, RowMapper<T> mapper, Object... args) { List<T> list = new ArrayList<>(); try (Connection conn = getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < args.length; i++) { ps.setObject(i + 1, args[i]); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapper.map(rs)); } } } catch (SQLException e) { throw new RuntimeException("查询执行失败: " + e.getMessage(), e); } return list; }

RowMapper接口定义一行结果集怎么映射成 Java 对象,调用时用 Lambda 表达式就能写:rs -> new ScoreVO(rs.getInt("student_id"), ...)。PreparedStatement 比 Statement 好在两点:参数用占位符绑定,不走字符串拼接,天然防 SQL 注入;同时预编译的 SQL 在重复执行同一条语句时性能更好。注意setObject的参数索引从 1 开始,写 0 会抛SQLException,这是新手最常踩的坑。

3. Servlet + JSP 核心功能:登录、成绩列表与 CRUD 的完整链路

3.1 登录与 Session 鉴权:一个 Servlet 撑起权限边界

成绩管理系统的第一个入口是登录。这里最容易犯的错误是把用户名校验逻辑写在 JSP 页面里,用<% if (...) %>去判断。JSP 的本质是视图,写业务逻辑不仅难看,而且后续想加个 Session 超时跳转都无从下手。正确做法是单独建一个LoginServlet,接收表单提交,查库校验,再把登录状态写入 Session:

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); AdminDao dao = new AdminDao(); Admin admin = dao.findByUsernameAndPassword(username, password); if (admin != null) { req.getSession().setAttribute("admin", admin); resp.sendRedirect(req.getContextPath() + "/score/list"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

这段逻辑里有几个细节值得展开。req.setCharacterEncoding("utf-8")必须放在获取任何参数之前,否则 POST 提交的中文用户名直接乱码,后面怎么查都查不到。Session 里存的是admin对象而不是只存一个用户名,这样后续 JSP 页面可以随时取到管理员的完整信息,比如在页头显示“当前操作员:XXX”。登录成功用sendRedirect而不是forward,区别在于重定向会让浏览器发起一次新的 GET 请求,地址栏变成/score/list,用户按 F5 刷新不会重复提交登录表单;如果用了forward,刷新页面就会弹出“确认重新提交表单”的对话框,这是很多系统被吐槽体验差的根源。登录失败用forward转发回登录页,因为需要把error这个错误提示字段带到 JSP 里展示。密码字段比较的时候,我建议在 AdminDao 里把查询结果整体封装成对象再比对,而不是在 Servlet 里操作 ResultSet,代码更干净。

3.2 成绩列表查询:DAO 到 JSP 的数据流转

成绩列表是系统使用频率最高的页面。成绩表本身只有student_id和course_id两个外键,直接查 score 表拿不到学生姓名和课程名,必须联合查询。这里我建议先写一个ScoreVO视图对象,而不是把三张表的实体都拆开传:

public class ScoreVO { private int id; private String studentNo; private String studentName; private String className; private String courseName; private double score; private java.sql.Date examDate; // getter/setter 省略 }

对应的 DAO 查询方法用 JOIN 把三张表拼起来:

public List<ScoreVO> findList(String keyword) { StringBuilder sql = new StringBuilder( "SELECT s.id, st.student_no, st.name, st.class_name, " + "c.course_name, sc.score, sc.exam_date " + "FROM score sc " + "JOIN student st ON sc.student_id = st.id " + "JOIN course c ON sc.course_id = c.id " + "WHERE 1 = 1 "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.isEmpty()) { sql.append("AND (st.student_no LIKE ? OR st.name LIKE ? OR c.course_name LIKE ?) "); String kw = "%" + keyword + "%"; params.add(kw); params.add(kw); params.add(kw); } sql.append("ORDER BY sc.exam_date DESC"); return JdbcUtil.query(sql.toString(), rs -> new ScoreVO(...), params.toArray()); }

三个 LIKE 条件共用一个关键词,注意%通配符要拼接在参数里而不是拼在 SQL 字符串里,这是防注入的底线。WHERE 1=1是动态拼接条件时的惯用手法,看起来有点怪,但它能让后面的AND无条件成立,省去判断“是否第一个条件”的麻烦。

Servlet 层的转发逻辑很简短,核心是把查询结果塞进 request:

@WebServlet("/score/list") public class ScoreListServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword = req.getParameter("keyword"); List<ScoreVO> list = new ScoreDao().findList(keyword); req.setAttribute("scoreList", list); req.setAttribute("keyword", keyword); req.getRequestDispatcher("/WEB-INF/jsp/score_list.jsp").forward(req, resp); } }

JSP 页面只负责渲染,用 EL 表达式和 JSTL 标签处理循环,不写一句 Java 代码:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table class="table table-bordered"> <thead> <tr> <th>学号</th><th>姓名</th><th>班级</th> <th>课程</th><th>成绩</th><th>考试日期</th><th>操作</th> </tr> </thead> <tbody> <c:forEach items="${scoreList}" var="s"> <tr> <td>${s.studentNo}</td> <td>${s.studentName}</td> <td>${s.className}</td> <td>${s.courseName}</td> <td>${s.score}</td> <td>${s.examDate}</td> <td> <a href="${pageContext.request.contextPath}/score/edit?id=${s.id}">编辑</a> <a href="${pageContext.request.contextPath}/score/delete?id=${s.id}" onclick="return confirm('确认删除这条成绩?');">删除</a> </td> </tr> </c:forEach> </tbody> </table>

链接里的${pageContext.request.contextPath}是必须写的前缀。如果不加,部署到名为grade_system的上下文时,所有绝对路径都会变成http://localhost:8080/score/list,Tomcat 找不到资源返回 404。加上之后会变成http://localhost:8080/grade_system/score/list,项目换个上下文名也不用改页面。

3.3 成绩新增与修改:ID 回填与更新时机

新增和编辑共用一张表单页,是减少代码量的常用做法。关键在于 Servlet 里通过表单隐藏域id判断当前是插入还是更新:

@WebServlet("/score/save") public class ScoreSaveServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); String idParam = req.getParameter("id"); int studentId = Integer.parseInt(req.getParameter("studentId")); int courseId = Integer.parseInt(req.getParameter("courseId")); double score = Double.parseDouble(req.getParameter("score")); // 后端必须重新校验分数范围,不能信前端 if (score < 0 || score > 100) { req.setAttribute("error", "分数必须在 0 到 100 之间"); req.getRequestDispatcher("/WEB-INF/jsp/score_edit.jsp").forward(req, resp); return; } ScoreDao dao = new ScoreDao(); if (idParam != null && !idParam.trim().isEmpty()) { dao.update(new Score(Integer.parseInt(idParam), studentId, courseId, score)); } else { dao.insert(new Score(studentId, courseId, score)); } resp.sendRedirect(req.getContextPath() + "/score/list"); } }

这里最容易翻车的点有两个。第一个是id参数没做空值判断,直接Integer.parseInt("")抛NumberFormatException,页面白屏。第二个是前端表单里写了正则校验就把后端校验省了,我在实际测试中用浏览器 F12 把required属性删掉就能直接提交 999 分,数据库 CHECK 约束在 MySQL 5.7 下又不生效,脏数据就这么进去了。所以后端校验这一道不能省,核心原则是:前端校验是体验,后端校验是安全。sendRedirect放在 save 操作的最后,同样是为了防止用户按 F5 后表单被重复提交,这是“刷新翻车”场景里最简单的后悔药。

编辑表单的预填逻辑在ScoreEditServlet里,按 ID 查出单条记录后转发到编辑页,JSP 里用${scoreVO.studentId}之类的方式给<input>的value赋值。这条链路里最常见的低级错误是表单的action写成了/score/save而没有加contextPath,导致提交时 404,调试半天才发现是路径少了项目前缀。

4. ECharts 统计图接入:Servlet 输出 JSON,前端异步加载

4.1 统计需求拆解:哪些指标真正有看图价值

加统计图不是把数据画出来就完事,先想清楚哪些指标对使用者有用。我在成绩系统里最终保留了三个维度:班级平均分对比用柱状图,单科成绩分数段分布用饼图,所有成绩的散点分布用折线图。其中分数段分布最直观,一眼能看出一个班是高分扎堆还是低分扎堆。

对应的统计 SQL 有讲究。平均分按班级分组,分数段用CASE WHEN做区间映射:

-- 班级平均分 SELECT st.class_name AS name, ROUND(AVG(sc.score), 1) AS value FROM student st JOIN score sc ON st.id = sc.student_id GROUP BY st.class_name ORDER BY value DESC; -- 分数段分布 SELECT CASE WHEN sc.score >= 90 THEN '90-100' WHEN sc.score >= 80 THEN '80-89' WHEN sc.score >= 70 THEN '70-79' WHEN sc.score >= 60 THEN '60-69' ELSE '60以下' END AS segment, COUNT(*) AS cnt FROM score sc GROUP BY segment ORDER BY segment;

这里有个 SQL 层面容易被忽略的点:CASE WHEN的区间判断从上往下匹配,所以从 90 分开始往下写,不需要写AND score < 100这种冗余条件。GROUP BY用别名segment是 MySQL 特有的语法糖,标准 SQL 不允许这样写,但 MySQL 8 支持,功能上没问题。

4.2 JSON 输出 Servlet:把统计逻辑从 JSP 里抽出来

图表的数据来源是后端接口,千万不要为了省事直接建一个chart_data.jsp在里面用<% out.print(json); %>输出 JSON。JSP 默认会输出完整的 HTML 文档结构,浏览器拿到后解析出来的是一堆标签而不是 JSON,fetch 解析直接报错。正确做法是单独建一个 Servlet,只输出纯 JSON 数据:

@WebServlet("/stat/classAvg") public class ClassAvgServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { List<Map<String, Object>> rows = new ScoreDao().statClassAvg(); JSONArray arr = new JSONArray(); for (Map<String, Object> row : rows) { JSONObject obj = new JSONObject(); obj.put("name", String.valueOf(row.get("name"))); obj.put("value", row.get("value")); arr.put(obj); } resp.setContentType("application/json;charset=utf-8"); resp.setCharacterEncoding("utf-8"); resp.getWriter().write(arr.toJSONString()); } }

这里resp.setContentType("application/json;charset=utf-8")的作用是告诉浏览器返回的是 JSON 而不是 HTML,setCharacterEncoding("utf-8")保证中文班级名不乱码,两个都要写。JSON 的name字段对应 ECharts X 轴的刻度标签,value对应柱子的高度,字段名要跟前端约定好,改一个地方忘了改另一个就会看到图表空白。我用的是 fastjson,也可以用 Gson 或 Jackson,功能等价。

4.3 前端 ECharts 初始化与图表刷新

前端页面里引入 ECharts 库文件后,用 fetch 拉取接口数据,再用setOption渲染图表:

<div id="classAvgChart" style="width:100%;height:360px;"></div> <script src="${pageContext.request.contextPath}/js/echarts.min.js"></script> <script> var chart = echarts.init(document.getElementById('classAvgChart')); fetch('${pageContext.request.contextPath}/stat/classAvg') .then(res => res.json()) .then(data => { chart.setOption({ title: { text: '班级平均分对比' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.map(x => x.name) }, yAxis: { type: 'value', min: 0, max: 100 }, series: [{ type: 'bar', data: data.map(x => x.value), barWidth: 30 }] }); }); </script>

y 轴的min: 0, max: 100要显式写出来,否则 ECharts 会自动根据数据范围调整刻度,比如所有班级平均分在 75 到 85 之间时,Y 轴从 70 开始显示,读者容易误以为分数差距很大。data.map(x => x.name)是从后端返回的 JSON 数组里提取字段生成新数组,如果拿到的数据是字符串"90.5"而不是数字90.5,ECharts 也会正常渲染,但排序和 tooltip 的数值计算会出问题,稳妥做法是parseFloat(x.value)做一次转换再传进去。

ECharts 图表显示不出来的两个高频原因要提前知道。第一是容器 div 的宽高为 0,比如父级元素用了display:none或者百分比高度在父容器没有设定高度时不生效,图表画布没有空间自然不显示。第二是echarts.init在动态加载的标签页里执行时容器还没渲染完成,解决办法是把初始化代码放在window.onload或者标签页切换完成后的回调里执行。给统计图加一个刷新按钮的写法也很简单,把 fetch 的代码包成function loadChart(),按钮onclick调用重新执行一遍,用户改动成绩后不用整页刷新就能拿到新图表。

5. 部署与常见问题排查:五个真实翻车场景和后悔药

5.1 中文乱码满地跑:三处编码必须同时校正

现象:页面提交中文姓名后,列表显示成???或者一堆乱码字符;按用户名搜索时查不到任何结果。

原因:编码问题同时涉及三个环节,只改一处是修不好的。浏览器提交表单时按 JSP 页面声明的编码对内容编码,Tomcat 接收后用默认的 ISO-8859-1 解码,数据库连接串里又没有指定characterEncoding,到 JDBC 驱动写入 MySQL 时二次转码,中间任何一步断裂就乱码。

解决:在 Servlet 的doPost方法第一行执行req.setCharacterEncoding("utf-8"),JSP 页面头部声明<%@ page pageEncoding="utf-8" %>,JDBC 连接串添加characterEncoding=utf8。如果还要处理 GET 请求的中文参数,需要改 Tomcat 的server.xml给 Connector 加URIEncoding="UTF-8",或者干脆把查询表单改用 POST 提交。这里要注意顺序:setCharacterEncoding必须在第一次读取参数之前调用,写在getParameter后面就失效了。

5.2 修改 JSP 后刷新还是旧页面,疑似“没生效”

现象:改了 JSP 里的表格列或者提示文案,重启 Tomcat 后页面还是原来的内容,强制刷新也没用。

原因:这不是代码问题,是浏览器缓存和 Tomcat 热部署联合作祟。JSP 文件更新后 Tomcat 会重新编译成 Servlet class,但如果浏览器端缓存了完整的 HTML 响应,刷新时直接读本地缓存,服务器的新内容根本到不了浏览器。另一个隐蔽场景是项目被重复部署,Tomcat 的webapps目录下有两个同名目录,加载的是旧的那份。

解决:先把浏览器缓存清掉,用 Ctrl+F5 强制刷新排除缓存因素。如果还不行,检查webapps目录下是不是有重复的项目副本,删掉旧目录再重新部署。从根上解决的话,开发阶段不要用生产模式部署,直接把项目挂在 Eclipse 或 IDEA 的 Tomcat 插件的虚拟目录下,改完 JSP 自动重载。

5.3 Tomcat 启动报端口被占,应用打不开

现象:启动 Tomcat 时控制台报错Port 8080 was already in use,浏览器访问localhost:8080显示空白或跳转到别的应用。

原因:上一次 Tomcat 非正常关闭,java进程没有完全退出,8080 端口还被占用;或者机器上本来就有别的服务占了这个端口。

解决:Windows 下先执行netstat -ano | findstr 8080找到占用进程的 PID,然后taskkill /PID 进程号 /F强制杀掉,再启动 Tomcat。如果端口是系统服务占用且不方便杀,改端口也容易,编辑conf/server.xml,把 HTTP Connector 的port="8080"改成8081,同时把<Server port="8005"改成8006,避免关闭端口也被占。

5.4 数据库驱动报 ClassNotFoundException,页面 500

现象:Tomcat 启动正常,但访问任何查询接口都抛java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者Access denied for user。

原因:驱动 jar 包没有放进WEB-INF/lib目录。很多人把mysql-connector-java.jar放在项目根目录或者 Tomcat 的lib目录下,但 Web 应用运行时类加载器优先从自身WEB-INF/lib读取 jar,找不到就会报错。Access denied则是 MySQL 8 的认证插件问题,旧版驱动不支持新的caching_sha2_password认证方式。

解决:把驱动 jar 复制或通过构建工具配置到WEB-INF/lib下,驱动全类名写成com.mysql.cj.jdbc.Driver。如果已经是最新驱动还报认证问题,在连接串里把serverTimezone写准确,并且确认数据库用户的主机限制是localhost还是远程可达的%,连接 IP 对不上也会被拒绝。

5.5 ECharts 图表空白且控制台无报错,数据哪去了

现象:访问/stat/classAvg接口能看到 JSON 数据,页面图表区域却是空白,浏览器控制台干净得没有任何错误。

原因:ECharts 的setOption传入的结构不对。最常见的是后端把value字段返回成了字符串类型,或者字段名与前端的x.name对应不上。另外echarts.init执行时容器 div 可能还没完成布局,宽度为 0,画布画了但看不见。

解决:在then回调里console.log(data)看一次接口真实返回,确认字段名是name还是别的。数值字段用parseFloat强转一次。容器宽度问题用getBoundingClientRect().width打印检查,如果是 0,把echarts.init移到window.onload里执行,或者给 div 一个明确写死的宽度而不是依赖父容器。

6. 进阶:用一个 Filter 收掉编码和登录校验两件事

JdbcUtil收拢了数据库连接以后,我意识到编码处理和登录校验也适合用同样思路收拢。之前每个 Servlet 里都写一遍setCharacterEncoding,登录拦截逻辑散落在各个 Servlet 的doGet开头,后来项目里加了一个新页面忘了写检查,一个未登录请求直接拿到了成绩列表页面。从那以后我每次新建 Servlet 都强制走一遍标准流程:编码设置收进Filter,登录校验收进同一个Filter,所有请求先过这一关。

@WebFilter("/*") public class EncodingAndAuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest r = (HttpServletRequest) req; HttpServletResponse res = (HttpServletResponse) resp; req.setCharacterEncoding("utf-8"); resp.setCharacterEncoding("utf-8"); String uri = r.getRequestURI(); boolean isStatic = uri.contains("/js/") || uri.contains("/css/") || uri.contains("/images/"); if (isStatic || uri.endsWith("/login") || uri.endsWith("/login.jsp") || uri.equals("/")) { chain.doFilter(req, resp); return; } if (r.getSession().getAttribute("admin") == null) { res.sendRedirect(r.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

这段逻辑的关键是一个Filter里做了两件事,且必须注意放行顺序。静态资源、登录页、登录请求的路径要先判断,否则chain.doFilter没执行,CSS 和 JS 文件全被拦截,页面裸奔。@WebFilter("/*")拦截所有路径,包括登录页本身,所以白名单条件要完整。Session 里没值直接重定向到登录页,不需要在每个 Servlet 里重复写判断。部署这一步之后,新增页面天然带编码和鉴权,代码里再没见过重复的编码设置。

踩过的坑还有一点值得记住:Filter里resp可以强制成HttpServletResponse,但读取路径时要用uri字符串而不是r.getRequestURL(),后者可能带上协议和域名,判断时容易出错。希望这些坑能帮你少走几步弯路,这套源码下载下来后照着第五章的清单逐个检查,部署过程会顺畅很多。

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

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

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

立即咨询