ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Servlet+JSP+MySQL开发JavaWeb学生成绩管理系统与ECharts统计图实战

Servlet+JSP+MySQL开发JavaWeb学生成绩管理系统与ECharts统计图实战 简介基于ServletJSPMySQL开发的JavaWeb学生成绩管理系统面向JavaWeb初学者、课程设计与毕业设计人群用于实现学生信息管理、成绩录入查询与可视化分析。它在原项目基础上新增ECharts统计图使成绩分布、班级对比、科目趋势等数据展示更加直观功能覆盖学生、课程、成绩等核心模块。资源包共344个文件包含86个Java源码、37个JSP页面、62个JavaScript脚本、28个CSS样式以及编译后的class文件、jar依赖和SQL数据库脚本等压缩包整体仅3.59MB目录结构清晰便于导入IDE运行与二次开发。目前已有6723人学习下载。通过这份资源读者可以获得完整的项目源码、数据库脚本以及ECharts图表集成思路既能直接部署演示也可作为理解ServletJSPMVC分层开发的实际案例。源码中Dao、Service、Servlet等层次结构完整对提升JavaWeb综合动手能力很有帮助。1. 学生成绩管理系统为什么说ServletJSPMySQL仍然是一个能落地的选择教务老师把成绩从Excel里复制到网页页面上只显示一张大表格看完还是不知道哪个班平均分高、哪门课不及格扎堆。标题里这个“基于Servletjspmysql开发javaWeb学生成绩管理系统”解决的就是这件事用JavaWeb最常见的三层结构做一套成绩管理后台再新增ECharts统计图把分数从“能看”变成“看得懂”。这套技术栈虽然老但数据链路很直白浏览器请求打到JSP和ServletServlet处理业务并操作MySQL返回结果交给页面渲染统计部分用ECharts画柱状图、饼图和折线图。适合正在做JavaWeb课程设计或毕业设计的同学也适合第一次接触ServletJSP的从业者。接下来的内容按开发顺序推进先讲数据库与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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有一个容易忽略的选择成绩字段用DECIMAL(5,2)而不是FLOAT或DOUBLE。百分制成绩最多三位整数加两位小数DECIMAL(5,2)刚好覆盖不会出现0.10.20.30000000000000004这类浮点误差。设置默认值0.00可以避免成绩录入时漏填导致NULL参与统计。学生表里直接冗余一个class_name字段而不是单独建班级表是因为这个项目的核心统计维度就是班级名称。如果按班级编号做第三范式每次查询都要多两次JOIN收益不大。成绩表里保留外键并且加ON DELETE CASCADE删除学生时成绩自动清理符合毕设和内部工具的实际需求。默认字符集用utf8mb4而不是utf8因为utf8mb4才能完整存下中文和特殊符号排序规则保持utf8mb4_general_ci即可。2.2 JDBC连接MySQLURL参数里必须写对的三个值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 ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; 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里有三个参数是踩坑重灾区。第一个是useSSLfalseMySQL 8默认开启SSL协商而本地开发环境通常没有配置证书不加这个参数可能直接报Communications link failure或SSL连接错误。第二个是serverTimezoneAsia/Shanghai否则连接时会因为时区不一致报The server time zone value йʱ is unrecognized之类的错。第三个是characterEncodingutf8保证中文从数据库到Java字符串不变成问号。驱动类选择也有讲究MySQL 5.x用com.mysql.jdbc.DriverMySQL 8.x必须用com.mysql.cj.jdbc.Driver。如果JDK是8以上Class.forName这一步甚至可以省略但保留它能让老版本Tomcat也稳定运行。2.3 用Druid连接池替换DriverManager参数配置与关闭连接直接用DriverManager.getConnection的问题在于每次请求都创建物理连接用完再断开。成绩管理系统的并发量不高但JSP页面频繁刷新时连接建立的开销会被放大。常见做法是引入Druid连接池项目里只需一个配置文件和一个工具类。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/score_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot password123456 initialSize5 maxActive20 maxWait3000 validationQuerySELECT 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是拿连接的超时时间。validationQuerySELECT 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。浏览器表单如果设置methodpost请求会进入doPost直接在地址栏输入URL访问则进入doGet。登录功能是这套成绩管理系统的入口。下面是一个完整的LoginServletimport 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 contentTypetext/html;charsetUTF-8 languagejava % % 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 ListScoreVO 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); // 执行查询并封装ListScoreVO } }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?pageNum2。如果后面加了关键字搜索页码链接还需要带上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 idstatsChart stylewidth:100%;height:400px;/div${pageContext.request.contextPath}会动态解析为项目上下文路径比如/score_manage。图表容器必须显式设置高度height:400px是必须的否则ECharts初始化时拿到的容器高度是0图表渲染出来是空白。这个问题在后续避坑章节会专门展开。4.2 后端接口输出JSON用Servlet返回按班级平均分统计结果ECharts的数据来源是后端接口。常见做法是在Servlet里查SQL把结果拼成JSON字符串返回前端再用Ajax拉取。下面是一个统计各班级平均分的StatsServletimport 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;charsetUTF-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;charsetUTF-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错误cant connect through socket /tmp/mysql.sock运行项目时控制台报ERROR 2002 (HY000): Cant 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加useSSLNavicat连不上MySQL 8JDBC连接报The server requested passwordless authentication或SSL connection errorNavicat连接MySQL 8时报Authentication plugin caching_sha2_password cannot be loaded。前者是MySQL 8默认启用SSL但本地没配置证书后者是MySQL 8默认认证插件改成了caching_sha2_password旧版Navicat和Java驱动不认。解决连接串问题在URL末尾加useSSLfalseallowPublicKeyRetrievaltrue。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 contentTypetext/html;charsetUTF-8 languagejava %Servlet输出文本时写response.setContentType(text/html;charsetUTF-8)。第三段是Tomcat连接器编码如果POST提交正常但GET请求乱码在conf/server.xml的Connector里加URIEncodingUTF-8。数据库连接URL里的characterEncodingutf8也要保留否则Java到MySQL这段仍会乱码。5.4 ECharts图表不显示初始化时机、容器高度为0、JSON字段对不上页面打开后图表区域一片空白控制台没有报错另一种情况是报Cant get DOM width or height。绝大多数原因是容器div没有高度或者脚本在div渲染之前执行了echarts.init。ECharts初始化时必须拿到容器的宽度和高度div没有显式高度时拿到的是0。解决方法是给容器固定高度比如styleheight:400px;并把初始化脚本放到window.onload或页面底部。还有一种隐蔽情况是后端返回的categories或values字段名与前端data.categories不一致后端写成了class_names前端读categories自然拿不到。用浏览器开发者工具看Network里接口返回的原始JSON逐字段比对最直接。5.5 传统JSP项目部署到Tomcat后404war包结构和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的整条链路。希望帮到你。本文还有配套的精品资源点击获取
返回列表