ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

JavaWeb学生成绩管理系统实现:从MySQL建库到Tomcat部署的完整避坑指南

JavaWeb学生成绩管理系统实现:从MySQL建库到Tomcat部署的完整避坑指南 简介基于JavaWeb技术实现的高分学生成绩管理系统源码与数据库面向计算机相关专业期末大作业、课程设计学习者是一款经过导师指导、最终评定98分的完整实训项目。系统采用模块化设计涵盖学生信息管理、成绩录入与查询、统计分析等核心功能前后端交互由Servlet与JSP完成数据层搭配MySQL数据库代码注释详尽、分层清晰便于理解Java Web开发流程与MVC设计模式的具体落地。压缩包共405个文件、约3.45MB以84个Java源文件、84个class编译文件、36个JSP页面、61个JS脚本、28个CSS样式表为主体另有SQL数据库脚本、项目配置及依赖JAR包等其中JS与CSS分别负责前端交互和页面样式SQL脚本可供快速初始化数据。整体结构规整导入IDE即可查看运行。该资源已有64人学习下载适合期末赶工、课程设计答辩或项目二次开发时快速借鉴能有效节省从零搭建系统的时间也可从源码注释中学习分层架构、Servlet与JSP协作方式等常见业务写法。1. 基于JavaWeb的高分学生成绩管理系统源码与数据库实现卡点从来不在业务代码基于JavaWeb的高分学生成绩管理系统源码与数据库实现这类项目在课程设计和毕设里几乎是标配MySQL存数据Servlet接请求JSP还页面能登录、能增删改查、能出成绩统计就算交差。可它的翻车率一直不低问题往往不在业务代码而在建库脚本导不进去、JDBC连不上MySQL 8、一登录就白屏这几道坎上。这篇笔记按我自己的落地顺序写先把数据模型和高分判定口径立住再把连接池、登录、成绩统计串起来接着把IDEA配置Tomcat和页面组织讲透最后用一条避坑清单收尾。适合正在做JavaWeb课程设计、想快速复现并二次改造成绩系统的读者。2. 先把数据模型和高分判定口径立住四张表与初始化SQL2.1 为什么基线选 Servlet JSP JDBC MySQL而不是一上来就 Spring Boot成绩管理系统这类题目业务逻辑高度规整用户登录、学生管理、课程管理、成绩录入与统计。用原生JavaWeb的组合Servlet JSP JDBC MySQL做基线不是因为它新而是因为它够透。答辩时你能从浏览器请求一路讲到SQL执行三层结构各干各的课程设计评分最看重这种“能讲清楚”的分层。如果一上来就引Spring Boot MyBatis环境配置占的精力会超过业务本身而且Tomcat部署、Filter配置、Web应用目录结构这些基础问题都没踩过后面看任何框架都有黑匣子。反过来把原生链路跑通一次之后再换MyBatis只需要替换DAO层Service和Servlet基本不动改造路径很清晰。对于已经工作的从业者这套基线还有另一个价值成绩管理本质是标准CRUD加统计报表用原生链路能精确控制SQL和事务行为排查生产环境的数据问题时少一层框架封装就少一层猜测。所以我的建议是课程设计、毕设、内部小工具优先走这条路线等项目业务复杂度上来再谈框架迁移。2.2 “高分”不是形容词先定判定规则再写表“高分学生成绩管理系统”里“高分”必须是一个可计算的规则否则统计接口和JSP页面根本没法写。我一般这样约定口径单科成绩大于等于90记为优秀平均分大于等于85记为高分优秀率优秀科目数/总科目数。两个口径分开因为一个学生可能单科满分但平均分没过85也可能是各科都86、87的稳定高分。这个口径要写进表结构和统计SQL而不是在JSP里用一堆if判断。如果需求允许管理员调整阈值就把90和85放到配置表或Properties文件不要在SQL里到处散落。能跑、能把规则说清楚这是“高分管理系统”和普通成绩管理拉开差距的地方。2.3 四张核心表的结构字段、约束与索引怎么设计我一般用四张表t_user、t_student、t_course、t_score。用户表命名成t_user而不是user是为了避免和MySQL自带的mysql.user表混淆。表字段类型关键约束t_userid, username, password, real_name, roleBIGINT / VARCHAR(50) / VARCHAR(64) / VARCHAR(50) / VARCHAR(20)username UNIQUErole取值ADMIN/TEACHER/STUDENTt_studentid, student_no, name, class_name, gender, phoneBIGINT / VARCHAR(20) / VARCHAR(50) / VARCHAR(50) / TINYINT / VARCHAR(20)student_no UNIQUEt_courseid, course_no, course_name, creditBIGINT / VARCHAR(20) / VARCHAR(50) / DECIMAL(3,1)course_no UNIQUEt_scoreid, student_id, course_id, score, exam_timeBIGINT / BIGINT / BIGINT / DECIMAL(5,2) / DATETIME唯一索引(student_id, course_id)t_user里的role只用VARCHAR(20)不用ENUM避免MySQL版本和字符集对ENUM的隐式转换问题。登录用户的usernameADMIN和TEACHER随便起STUDENT角色的username直接存student_no登录后通过username反查学生信息少一张关联表。t_score里那个唯一索引uk_stu_course是全表最重要的约束同一个学生同一门课只允许一行成绩。重复录入由数据库挡掉而不是靠Service里先查再判断。这个设计直接避免统计时平均分被重复记录拉低也是后面并发录入验证的基础。外键建议保留。课设数据量小外键带来的性能损失可以忽略换来的是手工删数据时多一道安全网。如果源码里外键被删掉了演示前手动补上逻辑上更站得住。2.4 初始化SQL建库、建表、种子数据一次跑通拿到的源码包通常带一个init.sql但我习惯自己重写一份精简版的原因有两个一是原脚本可能带库名和用户名写死二是种子数据要为“高分统计”的演示服务。下面这份SQL可以直接在命令行或Navicat里执行CREATE DATABASE IF NOT EXISTS score_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE score_db; CREATE TABLE t_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 课设演示用明文生产环境必须加密, real_name VARCHAR(50), role VARCHAR(20) NOT NULL, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_student ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, class_name VARCHAR(50), gender TINYINT DEFAULT 0, phone VARCHAR(20), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_course ( id BIGINT AUTO_INCREMENT PRIMARY KEY, course_no VARCHAR(20) NOT NULL, course_name VARCHAR(50) NOT NULL, credit DECIMAL(3,1), UNIQUE KEY uk_course_no (course_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_score ( id BIGINT AUTO_INCREMENT PRIMARY KEY, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(5,2) NOT NULL, exam_time DATETIME, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES t_course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 种子数据 INSERT INTO t_user (username, password, real_name, role) VALUES (admin, 123456, 系统管理员, ADMIN), (teacher01, 123456, 王老师, TEACHER), (S001, 123456, 张三, STUDENT), (S002, 123456, 李四, STUDENT), (S003, 123456, 王五, STUDENT); INSERT INTO t_student (student_no, name, class_name, gender, phone) VALUES (S001, 张三, 计科2101, 1, 13800000001), (S002, 李四, 计科2101, 1, 13800000002), (S003, 王五, 计科2102, 0, 13800000003); INSERT INTO t_course (course_no, course_name, credit) VALUES (C001, Java程序设计, 4.0), (C002, 数据库原理, 3.5), (C003, 数据结构, 4.0); INSERT INTO t_score (student_id, course_id, score, exam_time) VALUES (1, 1, 92.00, 2024-07-01 10:00:00), (1, 2, 88.00, 2024-07-02 10:00:00), (1, 3, 95.00, 2024-07-03 10:00:00), (2, 1, 100.00, 2024-07-01 10:00:00), (2, 2, 65.00, 2024-07-02 10:00:00), (2, 3, 78.00, 2024-07-03 10:00:00), (3, 2, 91.00, 2024-07-02 10:00:00), (3, 3, 72.00, 2024-07-03 10:00:00);这段脚本里有两个设计意图。第一张三三门课平均分91.67两科90以上同时满足“高分”和“优秀”李四有一科满分但平均分只有81只满足“单科优秀”不满足“高分”。这样统计模块跑出来三组数据是能讲出差异的不会出现全是90分的“假演示”。第二student表和course表的主键从1开始但student_no和course_no用业务编号这个习惯是从真实系统带过来的避免主键变了业务编号跟着乱。注意执行方式有两种命令行mysql -u root -p init.sql或者在Navicat里直接复制运行。如果之前建过同名库先DROP DATABASE score_db再执行否则外键和唯一索引可能因为老数据建不上去。3. 后端实现顺序连接池、登录鉴权、成绩CRUD与统计SQL3.1 用Druid连接池替换裸JDBC一处配置省掉一类环境问题很多课设源码里是Class.forName加DriverManager.getConnection硬编码在DAO里优点是代码少缺点是一换MySQL版本就报驱动类找不到而且每个DAO方法都要写获取连接、关闭连接一漏就连接泄漏。常见做法是抽一个JdbcUtils工具类配上Druid连接池。druid.properties放在src/main/resources下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/score_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot passwordroot initialSize5 maxActive20 maxWait3000JdbcUtils的核心public class JdbcUtils { private static DataSource dataSource; static { try (InputStream in JdbcUtils.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement st, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (st ! null) { try { st.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }几个参数说明maxWait设成3000毫秒连接排队超过3秒直接抛异常比页面无限假死好排查initialSize设成5避免第一次请求时冷启动慢driverClassName必须和mysql-connector-java的版本匹配——8.x用com.mysql.cj.jdbc.Driver5.x用com.mysql.jdbc.Driver写错就是控制台报No suitable driver。这种问题在课设里几乎天天见不是玄学就是版本对不上。3.2 登录与权限Session加Filter统一拦截而不是每个页面都判断登录流程看起来简单但很多源码只在JSP里用%session.getAttribute()%判断这种方案拦住不直接访问Servlet的URL。正确做法是LoginServlet处理登录Filter统一拦截受保护路径。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); User user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/WEB-INF/views/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(loginUser, user); String ctx req.getContextPath(); if (ADMIN.equals(user.getRole())) { resp.sendRedirect(ctx /admin/index); } else if (TEACHER.equals(user.getRole())) { resp.sendRedirect(ctx /teacher/score/input); } else { resp.sendRedirect(ctx /student/score/list); } } }WebFilter(/admin/*) public class AuthAdminFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpSession session req.getSession(false); Object user (session null) ? null : session.getAttribute(loginUser); if (user null) { ((HttpServletResponse) response).sendRedirect( req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这里有三点要注意。第一req.setCharacterEncoding(UTF-8)必须在读取任何参数之前调用这是POST中文乱码的头号原因。第二sendRedirect必须带req.getContextPath()这关系到后面说的404问题。第三Filter只拦截受保护路径/login和login.jsp放行不要反过来做“排除法”排除列表漏写一个路径就裸奔了。ADMIN、TEACHER、STUDENT三种角色建议各写一个Filter或者一个Filter里按路径前缀判断角色别在一个Filter里堆一堆if。3.3 成绩增删改查与高分统计DAO里最值得抄的4条SQL成绩管理说到底就是数据库增删改查。但“高分统计”的几条SQL值得单独拿出来说它们决定了成绩管理系统能不能成为“高分”系统。// 录入或更新成绩利用唯一键做“存在则更新” public int saveOrUpdateScore(Long studentId, Long courseId, BigDecimal score) { String sql INSERT INTO t_score(student_id, course_id, score, exam_time) VALUES(?,?,?,NOW()) ON DUPLICATE KEY UPDATE scoreVALUES(score), exam_timeNOW(); // 执行update } // 高分学生列表平均分85 public ListMapString, Object listHighScoreStudents() { String sql SELECT s.student_no, s.name, s.class_name, ROUND(AVG(sc.score),2) AS avg_score, SUM(CASE WHEN sc.score 90 THEN 1 ELSE 0 END) AS excellent_cnt FROM t_student s LEFT JOIN t_score sc ON sc.student_id s.id GROUP BY s.id, s.student_no, s.name, s.class_name HAVING AVG(sc.score) 85 ORDER BY avg_score DESC; } // 每门课优秀率score90 在 MySQL 里比较结果为1/0可以直接SUM public ListMapString, Object listCourseExcellentRate() { String sql SELECT c.course_name, COUNT(*) AS total, SUM(sc.score 90) AS excellent, ROUND(SUM(sc.score 90) * 100.0 / COUNT(*), 1) AS rate FROM t_course c JOIN t_score sc ON sc.course_id c.id GROUP BY c.id, c.course_name; }第一条SQL的ON DUPLICATE KEY UPDATE依赖t_score表里的唯一索引uk_stu_course。重复录入时不再报错而是覆盖旧成绩并刷新考试时间。如果你希望重复录入直接拒绝去掉后半句、让唯一索引抛异常就行。第二条SQL里HAVING AVG(sc.score) 85是“高分”判定的唯一出口不要在Java里循环算平均再过滤统计逻辑分散在多处改阈值时漏改一个地方就是事故。第三条SQL用SUM(sc.score 90)统计优秀人数MySQL里比较表达式返回1或0可以直接求和答辩时这是一个能讲半分钟的实现细节。Service层在调用saveOrUpdateScore之前必须校验score在0到100区间超出直接抛ServiceException。数据库的CHECK约束要MySQL 8.0.16以上才真正生效老版本会直接忽略所以第一道闸放在Service层最稳。4. 从源码到可访问JSP页面、IDEA配置Tomcat与依赖清单4.1 JSP页面组织页面放进WEB-INF数据交给JSTL页面放在webapp/WEB-INF/views目录下外部无法直接访问必须通过Servlet转发。这是最简单有效的防绕过手段比在JSP里判断session干净得多。登录后的学生成绩列表页核心渲染片段长这样% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body table trth课程/thth成绩/thth是否优秀(90)/th/tr c:forEach items${scoreList} varsc tr td${sc.courseName}/td td${sc.score}/td td c:choose c:when test${sc.score 90}优秀/c:when c:otherwise未达优秀/c:otherwise /c:choose /td /tr /c:forEach /table /body /html这里${scoreList}是Servlet放在request里的List每个元素是Map或VO。用JSTL而不是scriptlet是为了不让Java代码散进页面。另外要注意页面上“是否优秀”的判断只负责展示统计口径仍以Service层SQL为准不要在页面里改阈值否则页面显示和后台统计对不上。4.2 IDEA跑通的最小配置Maven依赖、Artifact与Tomcat Deployment这是网上“idea运行javaweb项目配置”类教程里最容易翻车的环节。很多人照着教程把Tomcat配好启动也成功访问却404原因通常是Artifact没有挂载到Deployment里。先把pom.xml的依赖放出来dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependenciesservlet-api和jsp-api的scope必须设成providedTomcat的lib目录里已经有这些类不设会把Tomcat同名类打进war部署时可能冲突。IDEA里配置Tomcat的四步第一步File - Project Structure - Facets确认有WebArtifacts里有war exploded没有就手动Add。第二步Run - Edit Configurations左上角加号选Tomcat Server - LocalApplication server选本机Tomcat。第三步Deployment标签页点加号选Artifact - war explodedApplication context填/score。第四步启动后访问http://localhost:8080/score/login.jsp。很多JavaWeb笔记把这一步写得很松核心是第三步没有Deployment挂载Tomcat启动再成功也不会有应用可访问Application context拼错路径不对一样404。这四步按顺序核对一遍基本不会再倒在环境上。4.3 拿到源码包后先确认的五个文件源码包形态不定有的是Maven工程有的是普通Web工程但有几样东西是通用的。拿到手先检查这些文件/目录作用缺失时的表现init.sql建库、建表、种子数据启动后查不到表或登录失败src/main/java下的model/dao/service/servlet包三层代码编译都过不去webapp/WEB-INF/web.xmlServlet映射、欢迎页404或找不到Servletresources/druid.properties数据库连接池配置启动即连不上数据库pom.xmlMaven依赖管理import全部标红如果源码用的是lib目录而非Maven注意lib里是否包含mysql-connector-java和druid的jar缺了控制台会报ClassNotFoundException这是最常见的“拿到的javaweb项目完整案例mysql版跑不起来”的原因。不依赖IDE的部署方式也要会IDEA里Build - Build Artifacts - Build生成war包把war复制到Tomcat安装目录的webapps下启动Tomcat自动解压访问http://localhost:8080/war包名/。war包名就是URL的第一段路径想改访问路径直接改文件名。这种方式演示给老师看最稳因为不依赖IDEA还开着。5. JavaWeb成绩系统常见问题排查4个高频翻车现场5.1 中文乱码数据库、连接串、HTTP三处要一起对齐【现象】页面显示乱码插入数据库的姓名或课程名变成问号。 【原因】三个点没对齐建库时用了latin1或没指定字符集JDBC连接URL没传characterEncodingJSP或Servlet响应没指定UTF-8。最典型的场景是GET请求正常、POST乱码因为Tomcat 8以后GET的URI默认UTF-8但POST必须手动setCharacterEncoding。 【解决】建库脚本用utf8mb4JDBC URL加上useUnicodetruecharacterEncodingutf8再写一个统一的编码过滤器WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }过滤器拦截所有路径后LoginServlet里那行setCharacterEncoding可以留着双保险无所谓。JSP顶部也要写contentTypetext/html;charsetUTF-8。这三处对齐乱码基本绝迹。5.2 登录成功却404URL里少了一个contextPath【现象】账号密码正确跳转后地址栏变成http://localhost:8080/admin/index页面404。 【原因】代码里写的sendRedirect(/admin/index)漏了request.getContextPath()。部署时的Application context是/score访问根路径应该是http://localhost:8080/score/admin/index少了contextPath自然找不到资源。 【解决】所有跳转和表单action都用req.getContextPath()拼接JSP里用${pageContext.request.contextPath}。例如form action${pageContext.request.contextPath}/login methodpost同时检查web.xml里servlet-mapping的url-pattern确认和表单action完全一致。这个坑在多人协作的课设里尤其常见一个人改部署路径另一个人没同步改代码里的重定向。5.3 数据库连不上No suitable driver和Communications link failure要分开看【现象】启动后控制台报ClassNotFoundException: com.mysql.cj.jdbc.Driver或者报Communications link failure、Cannot create PoolableConnectionFactory。 【原因】前者是驱动没进classpath或版本不对后者是驱动有但连不上常见诱因MySQL服务没启动、端口3306被占用、密码错误、少了serverTimezoneAsia/Shanghai导致时区解析失败。 【解决】先把“连不连得上”和“代码对不对”分开。用IDEA右侧Database面板填同一份URL、用户名和密码能连通说明连接串没问题再去查代码连不通就逐个排查服务状态和端口。命令行检查3306netstat -ano | findstr :3306 # Windows lsof -i:3306 # Linux / macOS注意MySQL 8的密码认证插件是caching_sha2_password老版本的mysql-connector-java驱动可能握手失败。看到Unable to load authentication plugin时优先升级驱动而不是改MySQL的认证插件。5.4 Tomcat启动失败端口占用与日志被吞【现象】IDEA启动Tomcat报Address localhost:8080 is already in use或者启动后页面一直转圈。 【原因】上一次Tomcat实例没停干净或其他程序占用了8080端口。 【解决】三步先在IDEA里Stop所有Tomcat实例再在Run Configuration里把HTTP port改成8081避开冲突最后才是命令行杀掉占用进程。还需要养成的习惯是Tomcat真报错时别只看IDEA控制台去Tomcat安装目录的logs/catalina.out看完整堆栈IDEA偶尔会把关键报错行吞掉。这两个文件一对比定位速度完全不一样。6. 别让统计结果变成黑匣子三个办法验证“高分”没算错成绩管理系统交付前最怕的就是演示当天发现优秀率对不上。我的做法是用三层验证把统计结果钉死。第一层SQL对拍。在Navicat或命令行里直接跑一遍第3.3节里的统计SQL和页面上显示的数字比对。拿张三来举例平均分91.67、优秀科目2门页面上必须一致。不一致就排查是不是统计SQL带了额外WHERE条件比如按班级过滤或按学期过滤这种“页面带条件、SQL裸跑”的差异是优秀率对不上的头号原因。第二层并发重复录入验证。开两个浏览器窗口同时给同一学生同一课程提交成绩。如果没有唯一索引t_score表会出现两行有了uk_stu_course第二次提交会走ON DUPLICATE KEY UPDATE覆盖。验证方法很简单提交后查一遍表确认只有一行且examt_time刷新了。这一步验证的不只是代码而是整条链路对重复数据的防御能力。第三层边界成绩验证。Service层里把录入锁在0到100同时建议在MySQL 8.0.16以上版本加一道CHECK约束ALTER TABLE t_score ADD CONSTRAINT chk_score_range CHECK (score 0 AND score 100)。老版本MySQL会忽略CHECK所以不能只靠数据库应用层才是主防线。我自己带课设时翻车最多的一天就是演示时发现优秀率对不上追根到底是统计SQL里GROUP BY少了一个课程ID页面和SQL各算各的。从那以后我的习惯永远是先跑SQL再点页面。希望帮到你。本文还有配套的精品资源点击获取
返回列表