ARTICLE DETAIL

资讯详情

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

JavaWeb学生成绩管理系统源码解析:MVC、动态代理与分页实战

JavaWeb学生成绩管理系统源码解析:MVC、动态代理与分页实战 简介面向刚学完JavaWeb基础、希望将MVC设计模式和动态代理模式落地实践的初学者这套学生成绩管理系统源码包是合适的练手项目。压缩包内共271个文件整体大小约23.41MB既有78个jar依赖库、32个java源码和对应的class编译文件也有22个jsp页面、20个xml配置还附带用于初始化数据库的sql脚本和doc文档资源类型覆盖后端逻辑、前端页面、配置与说明目录结构清晰便于检索。系统围绕登录、课程、学生、成绩等核心模块展开从StudentAction、ScoreAction等控制类中可以看到请求分发、业务处理和数据持久化的完整链路对理解MVC分层与动态代理在事务控制中的应用很有帮助。目前已有6978人浏览学习适合用作课程设计、毕业设计参考或项目实战练手既可直接运行体验也方便在此基础上二次开发。1. JavaWeb 学生成绩管理系统这份资源包里到底有什么学生成绩管理系统是 JavaWeb 入门绕不开的经典案例。这份资源包把源码、数据库脚本、文档打包齐全解压后能直接运行四个 Action 控制器、三个 DaoImp 实现、Student 和 Major 实体类、Pager 分页类MVC 分层完整。它覆盖 JavaWeb 最核心的练手诉求学生管理、课程管理、成绩录入查询、登录认证、分页列表。刚学完 Servlet、JSP、JDBC 的新手靠它把散装知识串成项目准备 java 面试的人也能拿「登录校验、分页查询、DAO 封装」当项目经历讲。环境是 Java MySQL技术栈涉及 MVC 设计模式和动态代理适合先拆开读懂每一层再动手改。2. 从 MVC 到动态代理类清单拆解与事务边界2.1 先看类清单Action、DaoImp、实体类各管什么我拆项目有个习惯先不看代码把文件清单按职责排一遍。这份资源的类名起得很规矩几乎不用看内容就能猜到每一层在干嘛。LoginAction、StudentAction、ScoreAction、CourseAction 是控制层入口对应登录认证、学生管理、成绩管理、课程管理四个模块三个 DaoImp 是数据层实现Student、Major 是实体类Pager 是分页工具类。类名所属层核心职责LoginAction控制层登录参数校验、会话写入、跳转StudentAction控制层学生列表、新增、修改、删除ScoreAction控制层成绩录入、条件查询CourseAction控制层课程维护StudentDaoImp数据层student 表 CRUDScoreDaoImp数据层score 表 CRUD 与联表查询CourseDaoImp数据层course 表 CRUDStudent / Major实体层表字段的 Java 映射Pager工具层分页参数封装与起始下标计算这个结构就是典型 MVCJSP 当 ViewAction 当 ControllerDaoImp 当 Model 的数据访问实现。Action 里不出现 SQLDaoImp 里不出现 HttpSession两层之间靠实体类传值。这样拆的好处是后面加一个「按班级筛选成绩」的需求改动范围基本锁死在 ScoreAction 和 ScoreDaoImp不用把整个项目翻一遍。对应的 java基础 知识点也全用上了实体类是封装和构造器的练习DaoImp 是 JDBC 和集合的练习Action 是 Servlet API 的练习。所以我说这个项目适合当 java 面试的项目经历讲因为每一层都能对应到一个你确实写过的具体类而不是背出来的概念。你拿到手之后可以先打开 LoginAction 看一眼方法体如果里面出现了 conn 和 sql 字符串说明这个项目分层没做干净正常情况应该是调一个 DAO 方法就完事这本身就是检验资源质量的第一个标尺。2.2 动态代理在项目里的落点事务控制与日志埋点摘要里写了动态代理模式这是这套资源比普通增删改查项目值钱的地方。学生成绩管理系统里最容易出问题的是成绩录入往 score 表插一条记录可能同时要更新 student 表的统计字段或者 course 表的选课人数两步操作必须在一个事务里第二步失败第一步得回滚。我在做类似项目时一般不会在每个 DaoImp 方法里手写事务开启和提交那样代码会非常散。常见做法是抽一个事务代理用 JDK 动态代理统一处理代理对象拦截 DaoImp 的方法调用方法执行前从连接池拿连接并关闭自动提交执行成功提交抛异常回滚。资源源码里既然标了动态代理你可以在 src 目录下直接搜 Proxy.newProxyInstance 和 invoke 两个关键词看它把事务边界放在了哪一层grep -rn Proxy.newProxyInstance src/ grep -rn invoke src/搜完你会发现动态代理的落点基本就两类一类是事务一类是日志。事务代理保证一个业务方法里的多条 SQL 要么全成功要么全回滚日志代理在每个 DAO 方法前后打印参数和耗时排查慢查询时特别好用。面试问「动态代理在你项目里怎么用的」别回答「JDK 动态代理基于接口」这种八股直接说「我用来给 DAO 层统一加事务代理对象包住所有 DaoImp进入时开启事务、异常回滚、成功提交」这才是项目级的答法。2.3 数据库脚本里的表关系student、major、course、score 怎么关联实体类里有 Student 和 Major再加上课程和成绩数据库脚本里大概率是四张表major专业、student学生、course课程、score成绩。四张表是典型的「一对多 关联表」结构一个专业下有多个学生一个学生选修多门课程每个学生每门课程有一条成绩记录。设计上有几个点值得注意。major 单独建表而不是在学生表里直接写专业名字符串是为了避免同一专业名在不同记录里写法不一致——「计算机科学与技术」和「计算机科学」在统计时就变成两个专业了。score 表用 student_id 加 course_id 定位一条成绩粒度是一行代表「某个学生某门课的成绩」。这一点在导入脚本时就要想清楚如果你后面要做「按专业统计平均分」这种报表JOIN 路径是 score 到 student 再到 major三层关联索引怎么建提前心里有数。资源脚本里的具体字段名我没法逐列背出来但你用 Navicat 打开表结构后按「主键、外键、唯一约束」三个维度把关系画出来整个系统的数据流就清楚了。画完你会发现在这套表结构下所有业务查询都逃不出学生维度、课程维度、专业维度三条线。2.4 Pager 分页类四个字段一个方法撑起所有列表Pager.class 是这套系统里复用率最高的类。它一般封装四个参数pageIndex 当前页码、pageSize 每页条数、totalCount 总记录数、totalPage 总页数再加一个计算 SQL 起始下标的方法。public class Pager { private int pageIndex; // 当前页码用户看到的是从 1 开始 private int pageSize; // 每页条数默认 10 private int totalCount; // 总记录数来自 SELECT COUNT(*) private int totalPage; // 总页数由 totalCount 和 pageSize 算出 public int getStartPos() { // 第一页 startPos (1 - 1) * 10 0从第 0 条开始取 return (pageIndex - 1) * pageSize; } public int getTotalPage() { // 11 条数据每页 5 条结果是 2.2向上取整得 3 页 return (totalCount pageSize - 1) / pageSize; } }这里有两个易错点。第一pageIndex 是给用户看的从 1 开始startPos 是给 SQL 的 LIMIT 用的从 0 开始两者永远差一个 pageSize写成 pageIndex * pageSize 的话第一页就会漏掉前 10 条。第二总页数必须向上取整整数除法会把最后一页吞掉。资源里所有列表查询都走 Pager改每页条数只动一个地方这就是工具类存在的意义。3. 把源码跑起来MySQL 脚本导入与 Tomcat 部署的完整链路3.1 环境准备JDK、MySQL、Tomcat 的版本搭配先说版本这决定你会不会在第一步就翻车。这套资源是传统 JavaWeb 项目Servlet JSP JDBC 技术栈不是 Spring Boot所以环境要按老规矩来JDK 8 最稳MySQL 5.7 或 8.0 都行Tomcat 8.5 或 9。JDK 11 以上跑老项目偶尔会碰到 JSP 编译问题能换回 8 就换回 8省得把时间花在和环境较劲上。java 环境配置 是第一个关卡。装完 JDK 要配 JAVA_HOME 和 PATH命令行里能正常输出版本号才算就绪java -version mysql --version两个命令都有输出说明基础环境到位。Tomcat 不用往系统里装下载解压就行后面 IDEA 里直接指定这个解压目录。这里有个选型问题值得说一句为什么不用 Spring Boot 一键启动因为这个项目的定位是练手和面试复习JDBC 原生连接、Servlet 生命周期、手动配置 Tomcat 这些恰恰是 Spring Boot 帮你隐藏掉的底层知识。把老项目跑起来的过程本身就是在复习这些点所以我建议老老实实按传统方式部署一遍别图省事。3.2 导入数据库脚本命令行与图形化两种姿势资源包里的数据库脚本是一个 .sql 文件建库、建表、插初始数据都在里面。导入前先确认 MySQL 服务起来了然后命令行方式最直接# 方式一登录后执行 source 命令 mysql -uroot -p你的密码 mysql source D:/student_db.sql; # 方式二不进入交互模式直接重定向导入 mysql -uroot -p你的密码 D:/student_db.sql导入完用 show tables 验证能看到 student、course、score、major 这几张表具体库名以脚本里的 CREATE DATABASE 语句为准就说明执行成功。如果你平时用图形化工具Navicat 里右键目标库 →「运行 SQL 文件」选那个 .sql 文件同样效果注意导入时文件编码选 UTF-8不然初始数据里的中文会变成问号。如果你后面改了表结构想留一份备份IDEA 的 Database 面板里右键表 → SQL Scripts → Generate 可以导出当前库完整脚本。这比手动在原 .sql 文件上加字段靠谱导出的就是你当前库的真实状态不会漏约束。3.3 改连接配置db.properties 里的四个坑项目里一般有一个数据库配置文件可能叫 db.properties也可能直接写在 JDBC 工具类里。资源带文档的话文档会标清楚配置文件在哪。重点改四处jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password你的密码driver 这一行是 MySQL 8.0 的驱动类名带 .cj如果你用的是 MySQL 5.7 加 5.x 驱动要改回 com.mysql.jdbc.Driver。url 里三个参数各管一件事useSSLfalse 关掉安全连接握手不然控制台刷一堆警告serverTimezoneAsia/Shanghai 解决驱动时区报错characterEncodingutf8 管中文写入不乱码。username 和 password 改成你本机 MySQL 的实际账号。提示改完配置先别启动 Tomcat用一份最简单的 Java 代码或者 IDE 的 Database 面板测试连接通了再进下一步。把问题隔离在配置层比在 Tomcat 日志里猜快得多。3.4 部署到 TomcatIDEA 里跑通第一个页面打开 IDEA用 Open 方式选择资源解压目录。老工程通常没有 pom.xml是普通 Web 工程结构需要手动添加 Tomcat 运行配置Run → Edit Configurations → 左上角加号 → Tomcat Server → LocalApplication server 选你解压的 Tomcat 目录。然后切到 Deployment 页签点加号选 Artifact挑 war exploded 模式Application context 改成 /student具体值以资源文档为准要和项目里的跳转路径对得上。启动后浏览器访问http://localhost:8080/student/login.jsp能看到登录页说明部署成功。初始账号密码以文档为准一般不复杂登录后先点学生列表和成绩录入两个页面这两个页面覆盖了分页查询和表单提交两条最核心的链路。跑通这两个页面这套系统的八成功能就验证完了。4. 登录校验与分页查询两个核心模块的代码走读4.1 LoginAction登录请求的 MVC 完整链路登录是整个系统的入口把这条链路走一遍MVC 在项目里长什么样就清楚了。用户在 login.jsp 输入账号密码提交表单 POST 到 LoginActionLoginAction 调 StudentDaoImp 查库结果决定跳转还是留在登录页。核心逻辑是这样一段WebServlet(/login) public class LoginAction extends HttpServlet { private StudentDaoImp studentDao new StudentDaoImp(); 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); Student student studentDao.findByUsernameAndPassword(username, password); if (student ! null) { request.getSession().setAttribute(loginUser, student); response.sendRedirect(request.getContextPath() /student/list); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }逻辑分三段。第一段是字符编码POST 表单的中文参数必须在这里先设置否则后面全是乱码这是老 JavaWeb 项目里最容易被忽略的一行。第二段是取参调 DAO注意这里没有任何 SQL查库细节被封装在 findByUsernameAndPassword 里这就是分层的好处。第三段是分支跳转成功就把用户对象塞进 session用 sendRedirect 重定向防止用户刷新页面时重复提交表单失败用 forward 回登录页error 属性携带提示信息。重定向和转发的区别在这个例子里非常直观一个改变地址栏且发新请求一个在服务端内部转交请求java 面试题里常考这个对比。密码字段的处理方式值得看一眼源码常见做法是数据库里存 MD5 或加盐后的摘要登录时把明文密码加密后再比对。资源里具体怎么存的看 StudentDaoImp 里密码相关的语句就知道不建议明文存。4.2 Pager 分页limit 起始下标到底怎么算列表页是这套系统最典型的查询场景。页面传 pageIndex 和 pageSizeDaoImp 先查一次总数再按起始下标查当前页数据public ListStudent findByPage(Pager pager) { String countSql SELECT COUNT(*) FROM student; // 执行 countSql把 totalCount 填进 pager int startPos pager.getStartPos(); String pageSql SELECT * FROM student ORDER BY student_id LIMIT ?, ?; PreparedStatement ps conn.prepareStatement(pageSql); ps.setInt(1, startPos); // 偏移量跳过多少条 ps.setInt(2, pager.getPageSize()); // 取多少条 ResultSet rs ps.executeQuery(); // 封装成 ListStudent 返回 }为什么先查 count 再查数据因为页面底部要渲染「共 X 条 / 共 Y 页」这两个数字必须在查数据之前拿到。两条 SQL 是配套操作任何分页都逃不掉。LIMIT 的第一个参数是偏移量不是页码第 1 页偏移是 0第 2 页偏移是 pageSize这个转换逻辑在 Pager.getStartPos() 里收口前面已经看过了。需要特别留意的是分页查询必须带 ORDER BY。没有排序的分页MySQL 返回顺序不保证稳定翻到后面几页可能看到前面出现过的记录这就是「分页翻车」。另外 count 查询在小数据量下没压力但如果 student 表数据量上了几十万COUNT(*) 会全表扫这时可以考虑用一个近似值或者缓存这是后面优化的话题练手阶段不用较真。4.3 ScoreDaoImp条件查询与联表统计的实现成绩模块比学生模块复杂因为成绩数据要和课程表关联才能展示课程名。ScoreDaoImp 里会有一类联表查询常见写法是这样public ListScore findByStudentId(int studentId, Pager pager) { String sql SELECT s.score, c.course_name, c.credit FROM score s JOIN course c ON s.course_id c.course_id WHERE s.student_id ? ORDER BY s.id LIMIT ?, ?; PreparedStatement ps conn.prepareStatement(sql); ps.setInt(1, studentId); ps.setInt(2, pager.getStartPos()); ps.setInt(3, pager.getPageSize()); // 执行并把结果封装成 ListScore }JOIN 在这里的作用是补全信息score 表里只有 course_id页面上要显示「Java 程序设计 90 分」课程名在 course 表里不联表就得在 Java 代码里循环再查一次那种 N1 查询在列表页是性能灾难。给表起别名 s 和 c 是联表查询的常规操作SQL 能短不少。条件查询的另一个常见需求是按分数段筛选SQL 会在 WHERE 后面拼 AND s.score BETWEEN ? AND ?。这里有个实现细节值得学用 StringBuilder 动态拼 SQL根据页面传参决定要不要拼某个条件最后统一 set 参数。PreparedStatement 的占位符既能防 SQL 注入又让动态条件可控比直接拼接字符串靠谱。如果你在资源里看到这种写法说明作者的基本功是到位的。5. 学生成绩管理系统避坑五个真实翻车现场与修复方法5.1 数据库连不上时区、驱动版本、密码三连坑现象Tomcat 启动没报错但登录页一提交就 500控制台打印 Communications link failure 或 Access denied for user。原因九成是三个配置问题叠在一起。驱动类名写成 MySQL 5 时代的 com.mysql.jdbc.Driver而本地装的是 MySQL 8.0JDBC URL 缺 serverTimezone 参数驱动要求显式指定时区db.properties 里的密码和本机 MySQL root 实际密码不一致。解决三件套一次改齐。驱动类名带 .cjURL 加 ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8密码改成实际值。改完先单独验证连接再启 Tomcat。这个坑几乎每个第一次跑别人项目的人都会踩因为发布者用的是他的环境你用的是你的环境配置文件不可能天然通用。5.2 分页翻到最后几页出现重复数据或缺失现象列表页第 1 页正常翻到第 3、4 页时前面出现过的学生又冒出来了而且最后一页的记录条数和预期不符。原因分页查询 SQL 没有 ORDER BY。MySQL 执行 LIMIT 时如果没有排序返回行顺序不保证稳定可能走索引也可能全表扫描两次查询结果顺序不一致翻页自然就乱。解决所有分页查询强制加 ORDER BY用主键或唯一字段排序。这也是 java 面试题里「分页查询有哪些坑」的标准答案之一。从那以后我写分页 SQL第一行永远是 SELECT 字段 FROM 表 ORDER BY 主键条件可以后加排序不能没有。5.3 页面正常但中文存进数据库变成 ??现象添加学生时姓名填「张三」列表页显示「??」数据库表里也是问号。原因编码链路断了三个环节。Servlet 里没设 request.setCharacterEncoding(UTF-8)表单参数按平台默认编码解析JDBC URL 没带 characterEncodingutf8驱动用默认编码写库最隐蔽的是建表时表或字段的 charset 是 latin1连接层改了也存不进去中文。解决三层都设 UTF-8。Servlet 或过滤器里统一 setCharacterEncoding(UTF-8)JDBC URL 末尾加 characterEncodingutf8表结构层面执行 ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4;。前两层好改第三层得动表顺手把库和表的默认字符集一起确认了。改完重启重新插入一条数据验证。5.4 编译通过但 Tomcat 启动报 ClassNotFoundException现象IDEA 里编译没有任何报错一启动 Tomcat 就抛 ClassNotFoundException: com.mysql.jdbc.Driver或者某个 Action 类找不到。原因老 Web 工程通病。mysql-connector-java.jar 只在 IDEA 的 Libraries 里被引用Artifacts 打包时没带进 WEB-INF/lib或者 jar 依赖被标记成 Provided运行时根本不参与部署。解决打开 File → Project Structure → Artifacts看 Output Layout 里 WEB-INF/lib 下有没有这个 jar没有就右键依赖 → Put into Output Root。手动部署的话直接解压 war 包看 WEB-INF/lib 目录缺哪个 jar 就放哪个。判断标准很简单运行时报什么类找不到就把包含这个类的 jar 部署上去别在类路径上瞎猜。5.5 改了源码重启还是旧页面在跑现象改了 LoginAction 的跳转逻辑重启 Tomcat页面行为和改之前一模一样怀疑自己改错了文件。原因Tomcat 的 work 目录缓存了旧的编译产物IDEA 的增量部署只更新了部分文件再加上浏览器缓存了静态页面一叠加就显得改了个寂寞。解决IDEA 里 Build → Rebuild Project 强制全量编译把 Tomcat 的 work/Catalina 目录整个删掉再启动浏览器开无痕窗验证。这套组合拳能解决九成的「改了不生效」剩下的那一成基本都是部署的 war 包路径和源码头不对。6. 二次开发技巧把 Pager 升级成通用分页组件先说说为什么要动 Pager。现在这个类只负责分页参数不携带数据每个列表 Action 都要自己准备一个 List 或 List 放进 request再单独放一个 Pager页面取数据时要两样都拿。改成带泛型的 PageResult 让数据和分页参数一次带走复用性直接上一个台阶。public class PageResultT { private ListT list; // 当前页数据 private int pageIndex; // 当前页码 private int pageSize; // 每页条数 private int totalCount; // 总记录数 public int getTotalPage() { return (totalCount pageSize - 1) / pageSize; } public int getStartPos() { return (pageIndex - 1) * pageSize; } }改造思路原来的 Pager 保留作为入参对象新增 PageResult 作为返回对象DaoImp 把查到的 List 塞进去连同分页参数一起返回。Action 里只需要 setAttribute(result, pageResult)JSP 里循环遍历 result.list 渲染表格页码条用 result 里的分页参数渲染。泛型的好处是 Student、Score、Course 都能套用不用为每个实体写一个分页返回类。改完怎么验证写一个 main 方法或者 JUnit 用例测三个边界值totalCount10、pageSize5 时总页数是 2totalCount11 时总页数是 3覆盖向上取整分支pageIndex1 时 startPos0覆盖第一页偏移。这三个值过了分页逻辑基本稳了。这个改法是我在一次课程设计里被逼出来的——那时候三个列表页各写各的分页代码改一处要同步改三处改完第一处忘了第二处页面直接越界。从那以后我每次写列表页都强制先问一句「分页结果能不能复用」再动手写 DAO。这套资源给的不只是能跑通的源码更是一个值得反复折腾的底座希望帮到你。本文还有配套的精品资源点击获取
返回列表