简介:基于Servlet+JSP+MySQL开发的JavaWeb学生成绩管理系统,面向JavaWeb初学者、课程设计与毕业设计人群,用于实现学生信息管理、成绩录入查询与可视化分析。它在原项目基础上新增ECharts统计图,使成绩分布、班级对比、科目趋势等数据展示更加直观,功能覆盖学生、课程、成绩等核心模块。
资源包共344个文件,包含86个Java源码、37个JSP页面、62个JavaScript脚本、28个CSS样式,以及编译后的class文件、jar依赖和SQL数据库脚本等,压缩包整体仅3.59MB,目录结构清晰,便于导入IDE运行与二次开发。目前已有6723人学习下载。
通过这份资源,读者可以获得完整的项目源码、数据库脚本以及ECharts图表集成思路,既能直接部署演示,也可作为理解Servlet+JSP+MVC分层开发的实际案例。源码中Dao、Service、Servlet等层次结构完整,对提升JavaWeb综合动手能力很有帮助。
1. 学生成绩管理系统:为什么说Servlet+JSP+MySQL仍然是一个能落地的选择
教务老师把成绩从Excel里复制到网页,页面上只显示一张大表格,看完还是不知道哪个班平均分高、哪门课不及格扎堆。标题里这个“基于Servlet+jsp+mysql开发javaWeb学生成绩管理系统”解决的就是这件事:用JavaWeb最常见的三层结构做一套成绩管理后台,再新增ECharts统计图,把分数从“能看”变成“看得懂”。
这套技术栈虽然老,但数据链路很直白:浏览器请求打到JSP和Servlet,Servlet处理业务并操作MySQL,返回结果交给页面渲染,统计部分用ECharts画柱状图、饼图和折线图。适合正在做JavaWeb课程设计或毕业设计的同学,也适合第一次接触Servlet+JSP的从业者。接下来的内容按开发顺序推进:先讲数据库与JDBC,再讲Servlet与JSP协作,然后把ECharts接进去,最后给出部署和排错清单。开发基线按JDK 8、Tomcat 8.5、MySQL 8.0来写,老项目也能直接照搬。
2. 数据库与JDBC:给成绩管理建一张不翻车的score表
2.1 建表SQL与字段类型:用decimal保存分数,默认值写成0.0
学生成绩管理系统的数据量不大,但统计场景很固定:按班级看平均分、按课程看分数段、按考试时间看趋势。所以表结构不要只建一张“成绩表”,而是拆成管理员表、学生表、课程表、成绩表四张。分开存才能用SQL的GROUP BY直接做统计,不用在Java代码里手动聚合数据。
-- 管理员表 CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(100) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 学生表 CREATE TABLE `student` ( `id` INT NOT NULL AUTO_INCREMENT, `stu_no` VARCHAR(20) NOT NULL, `name` VARCHAR(50) NOT NULL, `class_name` VARCHAR(50) NOT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_stu_no` (`stu_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表 CREATE TABLE `course` ( `id` INT NOT NULL AUTO_INCREMENT, `course_name` VARCHAR(50) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 成绩表 CREATE TABLE `score` ( `id` INT NOT NULL AUTO_INCREMENT, `student_id` INT NOT NULL, `course_id` INT NOT NULL, `score` DECIMAL(5,2) NOT NULL DEFAULT 0.00, `exam_time` DATE NOT NULL, PRIMARY KEY (`id`), KEY `idx_student_id` (`student_id`), KEY `idx_course_id` (`course_id`), CONSTRAINT `fk_score_student` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`) ON DELETE CASCADE, CONSTRAINT `fk_score_course` FOREIGN KEY (`course_id`) REFERENCES `course` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有一个容易忽略的选择:成绩字段用DECIMAL(5,2)而不是FLOAT或DOUBLE。百分制成绩最多三位整数加两位小数,DECIMAL(5,2)刚好覆盖,不会出现0.1+0.2=0.30000000000000004这类浮点误差。设置默认值0.00,可以避免成绩录入时漏填导致NULL参与统计。
学生表里直接冗余一个class_name字段,而不是单独建班级表,是因为这个项目的核心统计维度就是班级名称。如果按班级编号做第三范式,每次查询都要多两次JOIN,收益不大。成绩表里保留外键,并且加ON DELETE CASCADE,删除学生时成绩自动清理,符合毕设和内部工具的实际需求。默认字符集用utf8mb4而不是utf8,因为utf8mb4才能完整存下中文和特殊符号,排序规则保持utf8mb4_general_ci即可。
2.2 JDBC连接MySQL:URL参数里必须写对的三个值
JavaWeb项目访问MySQL,最常见做法是用JDBC。无论后面是否引入连接池,底层都离不开一个正确的连接URL。下面是一段最基础的驱动加载与获取连接代码:
import java.sql.Connection; import java.sql.DriverManager; public class JdbcUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/score_db" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws Exception { return DriverManager.getConnection(URL, USER, PASSWORD); } }这个URL里有三个参数是踩坑重灾区。第一个是useSSL=false,MySQL 8默认开启SSL协商,而本地开发环境通常没有配置证书,不加这个参数可能直接报Communications link failure或SSL连接错误。第二个是serverTimezone=Asia/Shanghai,否则连接时会因为时区不一致报The server time zone value '�й���ʱ��' is unrecognized之类的错。第三个是characterEncoding=utf8,保证中文从数据库到Java字符串不变成问号。
驱动类选择也有讲究:MySQL 5.x用com.mysql.jdbc.Driver,MySQL 8.x必须用com.mysql.cj.jdbc.Driver。如果JDK是8以上,Class.forName这一步甚至可以省略,但保留它能让老版本Tomcat也稳定运行。
2.3 用Druid连接池替换DriverManager:参数配置与关闭连接
直接用DriverManager.getConnection的问题在于:每次请求都创建物理连接,用完再断开。成绩管理系统的并发量不高,但JSP页面频繁刷新时,连接建立的开销会被放大。常见做法是引入Druid连接池,项目里只需一个配置文件和一个工具类。
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/score_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 username=root password=123456 initialSize=5 maxActive=20 maxWait=3000 validationQuery=SELECT 1import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.util.Properties; public class DruidUtil { private static DataSource dataSource; static { try (InputStream is = DruidUtil.class.getClassLoader().getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(is); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws Exception { return dataSource.getConnection(); } }initialSize表示启动时创建5个连接,maxActive限制最大20个,maxWait是拿连接的超时时间。validationQuery=SELECT 1的作用是检查连接是否有效,避免把已断开的连接交给Servlet使用。注意druid.properties一定要放在src或resources目录的根路径下,getResourceAsStream才能读得到。
连接池接入后,原来的conn.close()并不会真的关闭物理连接,而是把连接归还给池子。如果业务代码里忘记关闭ResultSet、Statement或Connection,连接会被占满,后续请求全部卡在等待获取连接上。这是很多JavaWeb项目跑一段时间后变慢的直接原因,务必在finally块中逐个释放。
3. Servlet与JSP协作:从登录判断到成绩列表分页
3.1 Servlet生命周期与请求路由:doGet和doPost分别做什么
Servlet的生命周期可以归纳为四步:加载实例化、init初始化、service处理请求、destroy销毁。init在整个生命周期只执行一次,适合放资源初始化逻辑;service每次请求都会调用,并根据HTTP方法把请求分发到doGet或doPost。浏览器表单如果设置method="post",请求会进入doPost;直接在地址栏输入URL访问,则进入doGet。
登录功能是这套成绩管理系统的入口。下面是一个完整的LoginServlet:
import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); // 先统一请求编码 String username = request.getParameter("username"); String password = request.getParameter("password"); if ("admin".equals(username) && "123456".equals(password)) { HttpSession session = request.getSession(); session.setAttribute("loginUser", username); // 登录凭证放入session response.sendRedirect(request.getContextPath() + "/list"); // 重定向到列表页 } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }这段代码有两个关键点。request.setCharacterEncoding("UTF-8")必须放在读取参数之前,否则表单提交的中文用户名会乱码。登录成功后使用sendRedirect而不是forward,是为了避免刷新页面时表单重复提交。如果校验失败,用forward转发回登录页,并携带errorMsg提示信息。
生产环境不要用明文密码,至少要加MD5或BCrypt哈希后再入库。毕设项目里明文密码虽然运行没问题,但答辩时容易被问到,提前加一层哈希处理会稳妥很多。
3.2 用Session维持登录态:JSP页面里这样读用户信息
HttpSession是Servlet容器提供的会话对象,默认30分钟无操作后过期。登录成功后执行session.setAttribute("loginUser", username),后续请求只要不换浏览器、不清Cookie,都能通过request.getSession().getAttribute("loginUser")取回登录状态。
JSP页面内置了session对象,所以可以直接用EL表达式读取。在页面顶部加一个登录判断:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <% if (session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } %>更规范的做法是用Filter统一拦截未登录请求,避免每个JSP页面都写重复判断。实际项目里我一般会写一个LoginFilter,拦截除login.jsp、/login和静态资源以外的路径。注意放行静态资源时用request.getRequestURI().contains("/js/")这类判断,否则echarts.min.js也会被拦下来,页面图表加载不出来。
3.3 成绩分页查询:LIMIT偏移量与页面翻页参数
成绩列表是系统的核心页面,数据量稍微大一点,一页展示全部成绩就不现实了。分页查询的逻辑分为两步:第一步按条件查当前页数据,第二步查总记录数算总页数。SQL用LIMIT ?, ?,第一个参数是偏移量,第二个参数是每页条数。
下面是ScoreDao里的分页查询方法:
public List<ScoreVO> findPage(int pageNum, int pageSize) throws Exception { String sql = "SELECT s.name AS stu_name, s.class_name, c.course_name, sc.score, sc.exam_time " + "FROM score sc " + "JOIN student s ON sc.student_id = s.id " + "JOIN course c ON sc.course_id = c.id " + "ORDER BY sc.exam_time DESC, sc.score DESC " + "LIMIT ?, ?"; try (Connection conn = DruidUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (pageNum - 1) * pageSize); ps.setInt(2, pageSize); // 执行查询并封装List<ScoreVO> } }ORDER BY sc.exam_time DESC, sc.score DESC表示先按考试时间倒序,同一场考试再按分数从高到低。这里特别提醒:MySQL排序时要注意字段类型,exam_time是DATE类型才能按时间顺序排,如果存成字符串,排序结果会按字典序处理,出现“2024-9”排在“2024-10”后面的问题。
对应的ListScoreServlet负责接收页面参数并计算分页信息:
@WebServlet("/list") public class ListScoreServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); int pageNum = 1; int pageSize = 10; String pageNumStr = request.getParameter("pageNum"); if (pageNumStr != null && !pageNumStr.isEmpty()) { pageNum = Integer.parseInt(pageNumStr); if (pageNum < 1) { pageNum = 1; // 页码最小为1,防止负数偏移量 } } // 调用dao查询当前页数据和总条数 // 把pageNum、totalPage、list放入request request.getRequestDispatcher("/score_list.jsp").forward(request, response); } }页面翻页时,JSP里生成页码链接要带上查询条件,比如/list?pageNum=2。如果后面加了关键字搜索,页码链接还需要带上keyword参数,否则翻到第二页搜索条件就丢了。这是分页功能最容易被忽略的业务细节。
4. ECharts统计图怎么接到JSP:从后端JSON到前端图表
4.1 把echarts.js放进WebContent并在JSP里正确引用
ECharts是一个开源可视化库,通过一个JS文件就能实现柱状图、饼图、折线图等常用图表。使用步骤很简单:下载echarts.min.js放到项目的WebContent/js目录,在JSP页面里用绝对路径引入。注意不要写相对路径js/echarts.min.js,否则在/list这样的二级路径下会找不到资源。
<script src="${pageContext.request.contextPath}/js/echarts.min.js"></script> <div id="statsChart" style="width:100%;height:400px;"></div>${pageContext.request.contextPath}会动态解析为项目上下文路径,比如/score_manage。图表容器必须显式设置高度,height:400px是必须的,否则ECharts初始化时拿到的容器高度是0,图表渲染出来是空白。这个问题在后续避坑章节会专门展开。
4.2 后端接口输出JSON:用Servlet返回按班级平均分统计结果
ECharts的数据来源是后端接口。常见做法是在Servlet里查SQL,把结果拼成JSON字符串返回,前端再用Ajax拉取。下面是一个统计各班级平均分的StatsServlet:
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONArray; @WebServlet("/stats/classAvg") public class ClassAvgStatsServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("application/json;charset=UTF-8"); String sql = "SELECT st.class_name, AVG(sc.score) AS avg_score, COUNT(*) AS total_count " + "FROM score sc JOIN student st ON sc.student_id = st.id " + "GROUP BY st.class_name ORDER BY avg_score DESC"; JSONArray categories = new JSONArray(); JSONArray values = new JSONArray(); // 执行查询,遍历结果集后往categories和values里添加数据 JSONObject result = new JSONObject(); result.put("categories", categories); result.put("values", values); response.getWriter().write(result.toJSONString()); } }这里返回的是一个固定结构:categories是班级名称数组,values是平均分数组。把数据拼成前端可直接消费的结构,比返回原始对象列表更省事,前端不需要再做二次转换。response.setContentType("application/json;charset=UTF-8")务必设置,否则中文会乱码,而且前端拿到的Content-Type是text/html,解析JSON字符串时会提示非预期格式。
4.3 用Ajax拉取JSON并渲染柱状图、饼图和折线图
前端渲染脚本放在JSP页面底部,页面加载完成后再去请求后端接口。下面是一段完整的柱状图渲染代码:
<script> var chart = echarts.init(document.getElementById('statsChart')); fetch('${pageContext.request.contextPath}/stats/classAvg') .then(function (resp) { return resp.json(); }) .then(function (data) { chart.setOption({ title: { text: '各班级平均分对比' }, tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: data.categories }, yAxis: { type: 'value', min: 0, max: 100 }, series: [{ name: '平均分', type: 'bar', data: data.values, itemStyle: { color: '#5470c6' } }] }); }); </script>饼图统计的是各班级成绩录入人数占比,只需要把type从bar改成pie,再给series.data传{name: 班级名, value: 人数}这种结构。折线图则适合展示某门课程历次考试的平均分变化,xAxis.data换成考试日期数组,按exam_time升序排列。折线图的X轴刻度要注意:如果考试次数超过10次,axisLabel默认会显示得很挤,可以设置axisLabel: { interval: 'auto', rotate: 30 },让标签自动隐藏或旋转,避免文字重叠。
5. 避坑清单:学生成绩管理系统最常见的五个故障
5.1 MySQL 2002错误:can't connect through socket '/tmp/mysql.sock'
运行项目时控制台报ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock',页面直接连不上数据库。
原因是MySQL服务没有启动,或者客户端连接时走的socket路径与服务器实际socket路径不一致。本地开发时,localhost会被MySQL客户端默认解析为socket连接,而不是TCP连接。解决方法是先确认服务状态:在Linux上执行systemctl status mysqld或service mysql status,服务没起来就启动它。如果用Navicat连接正常,但Java项目连不上,多半是JDBC URL里用了localhost,改为127.0.0.1:3306强制走TCP即可绕过socket路径问题。
5.2 mysql ssl连接错误与认证插件问题:URL加useSSL,Navicat连不上MySQL 8
JDBC连接报The server requested passwordless authentication或SSL connection error,Navicat连接MySQL 8时报Authentication plugin 'caching_sha2_password' cannot be loaded。
前者是MySQL 8默认启用SSL但本地没配置证书,后者是MySQL 8默认认证插件改成了caching_sha2_password,旧版Navicat和Java驱动不认。解决连接串问题,在URL末尾加useSSL=false&allowPublicKeyRetrieval=true。allowPublicKeyRetrieval是配合caching_sha2_password用的,允许客户端从服务器获取公钥。如果是Navicat,升级到新版即可;如果必须用老客户端,可以在MySQL里创建用户时指定旧认证插件:
CREATE USER 'score_app'@'%' IDENTIFIED WITH mysql_native_password BY '123456'; GRANT ALL ON score_db.* TO 'score_app'@'%';5.3 JSP中文乱码:请求和响应两边都要设成UTF-8
登录页输入中文用户名,提交后Servlet里读出来是乱码;或者JSP页面显示成绩单时中文变成问号。
乱码要分三段排查。第一段是请求编码:request.setCharacterEncoding("UTF-8")必须在读取参数前调用。第二段是响应编码:JSP顶部写<%@ page contentType="text/html;charset=UTF-8" language="java" %>,Servlet输出文本时写response.setContentType("text/html;charset=UTF-8")。第三段是Tomcat连接器编码:如果POST提交正常但GET请求乱码,在conf/server.xml的Connector里加URIEncoding="UTF-8"。数据库连接URL里的characterEncoding=utf8也要保留,否则Java到MySQL这段仍会乱码。
5.4 ECharts图表不显示:初始化时机、容器高度为0、JSON字段对不上
页面打开后图表区域一片空白,控制台没有报错;另一种情况是报Can't get DOM width or height。
绝大多数原因是容器div没有高度,或者脚本在div渲染之前执行了echarts.init。ECharts初始化时必须拿到容器的宽度和高度,div没有显式高度时拿到的是0。解决方法是给容器固定高度,比如style="height:400px;",并把初始化脚本放到window.onload或页面底部。还有一种隐蔽情况是后端返回的categories或values字段名与前端data.categories不一致,后端写成了class_names,前端读categories自然拿不到。用浏览器开发者工具看Network里接口返回的原始JSON,逐字段比对最直接。
5.5 传统JSP项目部署到Tomcat后404:war包结构和JSP不解析
在IDEA里运行正常,打成war包放到Tomcat后,访问/list返回404,或者浏览器直接显示出JSP源代码。
先确认war包结构:WEB-INF/classes放编译后的class文件,WEB-INF/lib放所有依赖jar包,JSP文件放在war包根目录。如果JSP文件被放到了WEB-INF下面,浏览器直接访问会被拒绝,需要通过Servlet转发才能访问。JSP源码直接显示则说明请求没有被Tomcat的JSP Servlet处理,常见原因是把war包部署到了nginx静态目录。nginx本身不支持JSP解析,它只能托管静态文件,动态请求要转发到Tomcat处理。部署时用IDEA的Artifacts功能打war包,确认WEB-INF/lib里有mysql-connector-java、druid、fastjson这些jar包,否则运行时抛ClassNotFoundException。
6. 进阶:给统计图加渐变色、工具箱,并用接口验证数据链路
6.1 柱状图渐变色与工具箱配置
默认的纯色柱状图能看,但期末汇报或答辩演示时有点单薄。ECharts的柱状图可以用LinearGradient实现从上到下的渐变效果,配置写在series.itemStyle.color里:
series: [{ name: '平均分', type: 'bar', data: data.values, itemStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#83bff6' }, { offset: 1, color: '#5470c6' } ]) } }]渐变方向由四个参数控制:(0, 0)到(0, 1)表示从上到下,(0, 0)到(1, 0)表示从左到右。再给图表加一个toolbox,用户可以直接点击保存图片或切换数据视图:
toolbox: { feature: { saveAsImage: { title: '保存图片' }, dataView: { title: '数据视图' } } }6.2 用setInterval自动刷新成绩统计图
成绩录入后列表页会更新,但统计图不会自动变化。页面里加一个定时器,每30秒重新拉取一次统计数据:
setInterval(function () { fetch('${pageContext.request.contextPath}/stats/classAvg') .then(function (resp) { return resp.json(); }) .then(function (data) { chart.setOption({ xAxis: { data: data.categories }, series: [{ data: data.values }] }); }); }, 30000);刷新间隔不要小于5秒,否则多个用户同时操作时后端查询压力会成倍增加。如果只想手动刷新,也可以把这个逻辑绑定到“刷新图表”按钮上。
验证整个数据链路有个笨但有效的办法:启动Tomcat后,在浏览器直接访问http://localhost:8080/score_manage/stats/classAvg,如果浏览器能显示JSON数据,说明后端SQL和Servlet没问题,问题大概率在前端渲染;如果显示404,检查Servlet注解和war包部署路径;如果显示报错信息,回Tomcat日志看异常堆栈。
我早期做这类JavaWeb项目时,最喜欢把统计SQL直接写在Servlet里,后来发现所有查询逻辑混在一起,一个报表接口改动就要动Servlet代码。现在我会把统计方法收到ScoreDao或单独的StatsDao里,Servlet只负责接收参数和转发JSON,排查问题时思路清楚很多。这套学生成绩管理系统做完,最值钱的部分不是页面多好看,而是你能完整说清楚一次请求从JSP到Servlet再到MySQL最后回到ECharts的整条链路。希望帮到你。
本文还有配套的精品资源,点击获取