做学生成绩管理系统几乎是每一个学Java的人都会碰到的课程设计题。我自己当年也被这个项目折磨过——IDE里报错报得怀疑人生数据库中文乱码乱到想摔键盘前脚刚把成绩录入页面写完后脚一刷新全没了。这次干脆把它从控制台版本升级成一套完整的Web项目基于JavaMySQL实现的学生成绩管理系统把登录认证、学生信息管理、课程管理、成绩录入与统计这些功能全部串起来。这篇就把整套东西的拆解思路、数据库设计、后端分层、前端交互和部署排错全部讲透适合正在做课程设计、准备Java面试或者想入门Web开发的人直接抄作业。1. 项目整体设计与技术选型思路1.1 为什么选JavaMySQLWeb这套组合很多人上来就问现在都Spring Boot了怎么还抱着Servlet和JSP不放手我讲句实在话学生成绩管理系统这种典型CRUD项目最佳目标从来不是“用最酷的框架”而是“把Web开发的底层链路彻底搞明白”。用Java做后端是因为Java在Web服务端的生态太成熟了不管是将来走Java开发岗面试还是继续做更复杂的电商系统、后台管理系统Java那套类加载、Servlet容器、JDBC操作数据库的机制都是根基。很多Java面试题问来问去就是“JDBC连接数据库有哪几步”“Servlet生命周期是怎样的”“Session和Cookie区别是什么”这套系统一个小项目全都能串起来。MySQL做存储理由更直接它免费、轻量、资料多学生时代用得最多。而且MySQL 8.0之后性能、窗口函数、JSON支持都跟上来了做成绩统计、排名、平均分这类计算写SQL非常顺手。加上Navicat可视化工具一配建表、看数据、调SQL都比命令行舒服太多。至于Web指的是从浏览器访问、而不是桌面客户端。浏览器发HTTP请求到TomcatTomcat把请求交给Servlet处理Servlet调Service再调DAO操作MySQL结果渲染成JSP页面返回给浏览器。这条链路虽然老但它把HTTP协议、请求响应模型、Servlet生命周期、JDBC这些Web开发最核心的东西全部暴露在你面前。用这套技术栈做完一遍再去学Spring Boot、MyBatis那些封装框架你会知道它们到底帮你省了什么。1.2 功能模块拆解从登录到成绩统计我设计这个系统时没有一上来就堆功能而是按“一条完整业务线”去拆。一个老师要用的成绩管理系统最核心的路径是什么登进来看到学生列表录成绩改成绩查成绩看统计。所以我把系统拆成五个模块登录认证模块区分管理员和普通教师两种角色未登录用户一律拦截到登录页。学生管理模块对学生信息做增删改查包括学号、姓名、性别、班级、入学年份。课程管理模块维护课程信息比如课程编号、课程名称、学分、授课教师。成绩管理模块按课程录入成绩、修改成绩、删除成绩录入时校验分数范围。成绩统计模块按课程统计最高分、最低分、平均分、及格率按总分排名。这个拆法有一个好处每个模块之间的边界清晰后面写代码时DAO、Service、Servlet对应关系一目了然。比如“成绩录入”绝对不写在StudentServlet里而是单独拆出ScoreServlet。别人看你代码时不用猜就知道某个页面上的请求该找哪个类这也是项目评分里的一个隐形加分项。2. 数据库设计成绩系统的地基2.1 数据表结构设计数据库设计是整个系统的地基。我见过太多人上来就建一张表全塞进去结果后期改需求改到崩溃。成绩管理系统的核心实体有四张表用户表、学生表、课程表、成绩表。用户表存登录账号区分角色用role字段1表示管理员、0表示普通教师。学生表侧重维护学生基本信息学号设成唯一索引因为业务上不允许两个学生共用一个学号。课程表关联教师和学分课程编号设成唯一键。成绩表是核心中的核心它不能直接存学生姓名和课程名称而是存student_id和course_id两个外键这样符合数据库第三范式避免数据冗余同时也能防止删一个学生导致大量重复数据残留。建表SQL大概长这样CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, stu_no VARCHAR(20) NOT NULL COMMENT 学号, stu_name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, class_name VARCHAR(50) COMMENT 班级, grade_year INT COMMENT 入学年份, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_stu_no (stu_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表;成绩表加一个联合唯一索引uk_student_course(student_id, course_id)这个细节很多人会漏。加了它同一个学生同一门课只能有一条成绩记录重复录入时数据库直接报错比你在Java代码里先查询再判断要靠谱得多。我当年没加这个索引结果系统跑了一阵子成绩表里出现同一个人同一门课两条分数排名统计直接乱了。2.2 JDBC连接管理从DriverManager到连接池Java操作MySQL最原始的方式是JDBC核心步骤固定在五步加载驱动、获取连接、创建Statement/PreparedStatement、执行SQL处理结果集、关闭资源。很多Java基础面试题会直接让你背这五步所以即使后面用了连接池我还是建议你先手写一遍纯JDBC感受一下每条代码到底在干嘛。我项目里一开始用的是DriverManager直接获取连接写起来确实简单Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/score_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai; Connection conn DriverManager.getConnection(url, root, password);但跑起来就发现不对——Tomcat每次请求都new一个物理连接系统稍微一多并发MySQL的报错就开始刷屏。后来我换成Druid连接池启动时初始化一批连接放着谁用谁借用完归还性能立刻稳了。连接池初始化代码网上很多我直接用的Druid的简单配置DruidDataSource dataSource new DruidDataSource(); dataSource.setUrl(url); dataSource.setUsername(root); dataSource.setPassword(password); dataSource.setInitialSize(5); dataSource.setMaxActive(20);注意一点MySQL 5.x和8.x的驱动类不一样。5.x是com.mysql.jdbc.Driver8.x是com.mysql.cj.jdbc.Driver。如果你本地装的是MySQL 8还在网上复制老代码用5.x的驱动类启动时必然报ClassNotFoundException。这个坑我帮朋友排过不止一次。2.3 核心SQL平均分、及格率与排名成绩管理系统的核心价值在于统计。我在统计模块里写了三条比较关键的SQL都是工作中高频会用到的。按课程统计平均分、最高分、最低分和及格率用聚合函数加条件计数SELECT c.course_name, COUNT(s.id) AS total_count, AVG(sc.score) AS avg_score, MAX(sc.score) AS max_score, MIN(sc.score) AS min_score, SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) / COUNT(s.id) * 100 AS pass_rate FROM score sc JOIN course c ON sc.course_id c.id GROUP BY c.course_name;按学生总分排名MySQL 8.0直接用窗口函数比老版本的变量写法清爽太多SELECT stu_no, stu_name, SUM(score) AS total_score, RANK() OVER (ORDER BY SUM(score) DESC) AS rank_no FROM score GROUP BY stu_no ORDER BY total_score DESC;如果你还在用MySQL 5.7窗口函数用不了只能用rank这种用户变量实现排名。这里插一句排名查询里用RANK()和DENSE_RANK()是有区别的——同分并列时RANK()会跳号比如两名第二名下一个直接是第四名DENSE_RANK()不会跳号。课程设计里一般用RANK()就行但在面试被问到这个区别你得说清楚。3. 后端与前端实现细节3.1 分层架构DAO、Service、Servlet各司其职整个后端我采用的是经典三层结构Servlet负责接收请求和返回页面Service负责业务逻辑DAO负责数据库操作。很多课程设计代码喜欢把所有东西塞进Servlet一个类几百行方法多到离谱。那确实能跑但扩展性极差。分层能让你改业务时不动数据库代码改数据库时不动前端代码这是工程化的基本素养。以“修改学生信息”为例请求链路是这样的浏览器POST表单到StudentServletServlet调用StudentService的update方法Service先做一遍逻辑校验比如学号不能为空、姓名不能为空然后把更新任务交给StudentDAODAO执行UPDATE语句返回受影响行数Service再把结果返回给ServletServlet决定是跳转成功页还是带着错误信息回填表单。这么一拆每一层都能单独测。我在开发时习惯先写DAO用JUnit测通SQL再写Service最后接Servlet。如果上来就一把梭把三层全写完再启动Tomcat出问题你根本不知道是SQL写错、业务逻辑写错还是请求映射写错排查效率会低很多。3.2 登录与权限控制用Filter统一拦会话登录模块看起来简单但逃课最多。我见过某同学的系统登录页面就是个摆设把浏览器地址栏直接改成/list.jsp就能跳过登录进主页。我自己的做法是写一个全局Filter拦截所有/admin/*路径下的请求Session里没有登录标记的直接重定向到登录页。核心代码很简单WebFilter(/admin/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session ! null session.getAttribute(loginUser) ! null) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() /login.jsp); } } }Filter是一个面试高频考点。Servlet容器在请求到达Servlet之前先把请求交给FilterFilter决定是放行还是拦截。用一个Filter统一做登录鉴权好过在每个Servlet里复制粘贴几十行判断逻辑。角色权限我是在登录成功时把用户对象放进Session在JSP或者后端判断loginUser.role管理员能看到“用户管理”菜单普通教师只能操作成绩和查询。3.3 前端页面与交互JSPELJSTL的组合前端我选的是JSPEL表达式JSTL标签库这在一门课程设计里是标准答案。JSP能直接写Java代码但我不建议你在页面上写一堆% %那会让页面变成意大利面。正确的做法是用EL表达式取request/session域里的数据用JSTL的c:forEach遍历成绩列表页面里几乎看不到Java代码。成绩列表页的骨架table classtable thead trth学号/thth姓名/thth课程/thth成绩/thth操作/th/tr /thead tbody c:forEach items${scoreList} varsc tr td${sc.stuNo}/td td${sc.stuName}/td td${sc.courseName}/td td${sc.score}/td tda href/score/edit?id${sc.id}编辑/a/td /tr /c:forEach /tbody /table交互层面我做了一个比较实用的动作录入成绩时选择课程下拉框后自动加载该课程学生列表。这个用原生JS的fetch就能搞定不需要任何框架。插一句我经常被问“Java Web还要不要学JavaScript”我的答案是要学。后台管理系统再重前端也多多少少要处理动态交互。你把原生JS的fetch、事件、DOM操作弄明白后面学Vue、React都是降维打击。4. Web系统部署运行与常见问题实录4.1 环境准备从JDK到Tomcat的完整配置一套可复现的环境是这样的JDK 1.8或11MySQL 8.0Tomcat 9Navicat或者MySQL Workbench作为可视化工具Maven负责依赖管理。很多人栽在环境上尤其是Java环境变量配置那个经典的三连——JAVA_HOME、Path、CLASSPATH我建议直接搜“Java环境变量配置”的图文教程按图操作不要跳步。配好后命令行输入java -version能输出版本号才算过。Tomcat部署有两种方式。一种是把项目打包成war包丢到Tomcat的webapps目录下启动Tomcat后自动解压发布。另一种是使用IDEA的Tomcat插件直接跑调试改完Java代码热部署一下就能看效果适合开发阶段。我个人习惯是开发期用IDEA内置Tomcat上线演示时打成war包。部署完成后访问http://localhost:8080/能看到Tomcat首页说明servlet容器已经起来了再访问项目的登录页才算真正通。MySQL安装有两个容易翻车的点。第一个是root密码安装过程中会让你设置千万别手滑设完就忘后面连接数据库报Access denied for user多半就是这里出了问题。第二个是字符集安装时建议直接选utf8mb4或者在my.ini里配置character-set-serverutf8mb4。如果库表建完才发现字符集不对中文会一直乱码非常折磨。4.2 常见问题排查速查表我把实际调试中最常遇到的几个报错整理成一张表配合排查思路遇到问题先对照看自己属于哪一种。报错现象根本原因解决方案ClassNotFoundException: com.mysql.jdbc.Driver驱动包没引入或驱动类版本不对确认mysql-connector-java依赖MySQL 8用com.mysql.cj.jdbc.DriverCommunications link failure数据库没启动、IP端口不对、防火墙拦截命令行先测mysql -u root -p能否连接再检查url里的端口3306Access denied for user用户名或密码错误重置MySQL root密码或用Navicat重新连接测试Invalid value for serverTimezoneMySQL连接url缺时区参数url中加serverTimezoneAsia/ShanghaiPort 8080 already in use8080被其他进程占用换Tomcat端口到8090或找出占用进程kill掉中文全部变成??连接url没加characterEncodingurl加useUnicodetruecharacterEncodingutf8且表字段用utf8mb4HTTP 404找不到jsp访问路径写错或web.xml没配置欢迎页检查请求路径、webapp目录结构是否标准、部署是否成功No suitable driver found驱动代码写错或用错连接方式检查Class.forName是否执行连接url前缀是否jdbc:mysql://排查Web项目问题有个基本顺序先看Tomcat日志再看MySQL是否正常再看浏览器请求路径和状态码。日志是黑纸白字的真相比瞎猜靠谱太多。遇到500错误直接把IDEA控制台里的堆栈信息复制出来搜索十有八九能找到现成解决方案。4.3 踩坑实录字符集、端口与数据一致性第一个坑是乱码。乱码这个事同一个现象可能来自三个不同的源头页面显示乱码是JSP头部没加pageEncoding请求参数乱码是Servlet里没设request.setCharacterEncoding(utf-8)数据库存进去就是乱码则是连接url或库表字符集不对。我建议在系统开发第一天就把三处全部统一成utf8JSP文件保存编码、Servlet过滤器字符集、数据库连接参数一并在登录模块里测一遍。别等录入中文成绩单了再改那时候数据已经脏了。第二个坑是端口占用。Tomcat默认8080如果机器上装了Nginx或者别的服务占了这个端口启动会直接抛出Port 8080 already in use。我现在的做法是用一个随机端口跑调试比如8081、8082省得每次都要去查是谁占了8080。Windows下用netstat -ano | findstr 8080能找到占用进程PID再到任务管理器杀掉这个操作在项目演示前一晚特别有用。第三个坑是数据一致性。删除学生的时候如果不同步删成绩表里的记录成绩统计就会查到一堆挂在空壳学生名下的幽灵分数。我在Service里用事务解决了这个问题删除学生时先删成绩表再删学生表两层操作放在同一个事务里任何一个失败就整体回滚。Transactional public void deleteStudent(int studentId) { scoreDao.deleteByStudentId(studentId); studentDao.deleteById(studentId); }这里我用的Transactional在Spring里是标配但在纯Servlet项目里需要自己管理事务用conn.setAutoCommit(false)手动控制提交和回滚。我甚至建议你在课程设计里刻意写一段手动事务管理代码因为面试官问“事务你是怎么控制的”时你能说出提交、回滚、手动控制这三个关键词比只会答“加个注解”要加分。项目做完之后我最大的感受是学生成绩管理系统看着是个老掉牙的CRUD但它把Java Web开发最核心的能力——数据库设计、JDBC、事务、Servlet生命周期、Filter鉴权、JSP模板渲染——全部串起来了。真正动手做完一遍很多东西不用背也记住了。如果还想继续扩展可以往这几个方向走引入Maven整合Spring MVCMyBatis重构加入Excel导出成绩单功能或者把前端升级成VueElementUI做前后端分离。每一次扩展都是在简历上多写一行扎实的项目经历。