简介:基于Java+MySQL实现的Web学生信息管理系统课程设计项目源码包,面向高校计算机专业学生及Java Web初学者,适合作为课程设计、毕业设计参考或自学练手项目。系统使用IntelliJ IDEA开发,按Servlet+JavaBean+DAO分层架构搭建,Servlet负责业务处理,并配置过滤器校验用户权限;功能分为学生、教师、系统管理员三类角色:学生可进行个人成绩、课程平均成绩、班级开课及教学计划查询;教师可录入与查询成绩、查看任课情况;管理员则统筹管理学生、教师、课程、成绩、学院、专业及班级信息,模块划分完整。压缩包共551个文件,涵盖Java源码、JSP页面、XML配置、HTML/CSS/JS前端样式、图片素材及运行依赖jar包,整体大小约17.88MB,目录结构清晰,便于按功能模块查阅。已有568人学习/下载。通过该项目可系统性理解Web开发流程,掌握分层架构设计、MySQL数据库操作、会话与权限过滤等关键技能,是课程设计或入门实战的高性价比参考。
1. 学生信息管理系统:一套能直接跑通的 Java+MySQL Web 课设源码
期末拿到“学生信息管理系统”这个课设题目,要求 Java+MySQL+Web 三件套,两周时间从零到验收。大多数人的第一反应是直接上 Spring Boot 一把梭,但课程大纲往往卡死了必须用 JSP/Servlet;就算不卡,答辩被问一句“Servlet 生命周期是什么”也容易当场卡壳。这套编号 100010724 的课程设计资源给的就是一个不依赖重量级框架的完整实现:IDEA 2020.3.3 开发,Servlet 负责请求处理和页面转发,DAO 层封装全部 JDBC 操作,JavaBean 承载表单数据,MySQL 存七张业务表,再叠一个过滤器做角色权限拦截。功能上学生能查成绩、课程平均分、开课计划和教学计划;教师能录入与查询成绩、看任课情况;管理员能管理学生、教师、课程、成绩以及学院、专业、班级三类基础数据。适合刚学完 JavaWeb、需要一份能讲清楚原理的课设源码的在校生,也适合接单做同类型小管理系统的开发者。
2. 分层选型逻辑:为什么用 Servlet+JSP+DAO 而不是一上来就 Spring Boot
2.1 先搞懂这套架构在讲什么
这个项目用的是标准的 MVC 分层:JSP 是 View,只负责展示数据;Servlet 是 Controller,接收请求、解析参数、调用业务方法、决定跳转到哪个页面;Model 层拆成两块——JavaBean 做数据载体,DAO 做数据库访问。整个链路是“浏览器发请求 → Tomcat 找到对应 Servlet → Servlet 调 DAO → DAO 执行 SQL 返回 JavaBean 集合 → Servlet 把集合塞进 request → forward 给 JSP 渲染”。
选型逻辑很直接:课设场景下,Servlet+JSP 是 Java Web 官方入门方案,能让你清晰看到一次 HTTP 请求从 Tomcat 到 Servlet 再到 JSP 的完整路径。Spring Boot 确实开发效率更高,但它把请求分发、参数绑定、事务控制全部封装成了黑匣子,答辩时老师问“请求是怎么被 DispatcherServlet 转发的”,没底层功底的人当场就露馅。这套项目的价值恰恰在于把每一层都摊开了:Servlet 里的 doGet/doPost 是你自己写的,DAO 里的 Connection 是你自己拿的,SQL 是你自己拼的。
另外别觉得这套东西过时。把它映射到 Spring Boot 的体系里看:Controller 对应 Servlet,Service+Mapper 对应 DAO,Entity 对应 JavaBean。理解了这套课设,再看 SSM 项目,你只是把“手写 JDBC”换成了“MyBatis 帮你做”,请求链路和分层思想完全一致。
2.2 JavaBean、Servlet、DAO 在系统里各负责什么
我以成绩模块举例,看三个角色怎么配合。先是 JavaBean,用来承载一条成绩记录在页面上要展示的所有字段:
package com.sims.bean; public class Grade { private int id; private String studentNo; private String studentName; private String courseName; private String teacherName; private double score; private String term; // 省略 getter/setter }注意这个类没有 score 对应的课程 ID,而是直接放了 courseName 和 teacherName,因为成绩列表页要展示的是“课程名+教师名+分数”,不是一堆外键 ID。JavaBean 的字段设计可以跟着页面视图走,不必和表结构一一对应,这是新手最容易搞反的地方——有人把数据库七个字段原封不动搬进类里,页面要显示教师姓名还得再查一次表,纯属给自己加活。
DAO 层负责和数据库打交道。比如成绩查询,DAO 里写一个方法接收 studentId,返回 List<Grade>,SQL 里把课程表和教师表 JOIN 出来,一次性把页面要的数据全查齐:
public List<Grade> findGradesByStudent(int studentId) { String sql = "SELECT g.score, g.term, c.course_name, t.teacher_name " + "FROM grade g " + "JOIN course c ON g.course_id = c.id " + "JOIN teacher t ON c.teacher_id = t.id " + "WHERE g.student_id = ? " + "ORDER BY g.term DESC"; // 执行 JDBC 查询,封装成 Grade 对象加入 List }Servlet 层做的事情就三件:从 request 里拿参数、调用 DAO、把结果放到 request 域里转发给 JSP。我见过不少课设把所有代码堆在 Servlet 里,一个方法五六百行,SQL 散落各处。这个项目把增删改查收拢到 DAO,好处是成绩查询、平均分查询多个 Servlet 能复用同一套数据库操作,改表结构时只动 DAO 一处,不用满项目找 SQL。
2.3 过滤器与 Session:权限控制是怎么做成闭环的
权限设计是这个项目值得看的一个点。系统里学生、教师、管理员三类角色,学生不能访问教师的管理页面,教师不能动学生信息,靠的是 Session 加过滤器双层控制。为什么不每个 Servlet 里都写一段 if 判断?因为权限校验是横切逻辑,你希望它在请求到达 Servlet 之前统一拦截,否则十个 Servlet 复制十份 if,漏一个就是一个权限漏洞。
过滤器的核心逻辑是这样:
public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 登录页、登录接口、静态资源直接放行,否则会死循环 if (uri.endsWith("login.jsp") || uri.endsWith("LoginServlet") || uri.contains("/static/")) { chain.doFilter(request, response); return; } HttpSession session = req.getSession(false); Object loginUser = session == null ? null : session.getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } String role = (String) session.getAttribute("role"); if (uri.contains("/admin/") && !"admin".equals(role)) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问"); return; } chain.doFilter(request, response); } }这里有两个细节值得琢磨。第一,req.getSession(false) 传 false 是为了不主动创建 Session——用户没登录时服务器不该白开一个会话对象;第二,未登录跳登录页,已登录但角色不匹配返回 403,两个分支区分开,避免权限不足的用户被来回重定向刷不出页面。为什么角色信息放 Session 而不放 Cookie?Session 存在服务端,用户改不了里面的值,Cookie 是明文存在浏览器里的,改成 role=admin 就能越权,这也是面试里“Session 和 Cookie 区别”的实战对应。
2.4 包结构与 IDEA 运行方式
项目源码包结构是这个样子,拿到手先按这个脉络去读,别上来就翻 JSP:
src/main/java/com/sims │── bean/ # JavaBean 实体类 │── dao/ # JDBC 数据访问层 │── filter/ # 权限过滤器 │── servlet/ # 控制器 src/main/webapp │── admin/ # 管理员功能页面 │── student/ # 学生功能页面 │── teacher/ # 教师功能页面 │── login.jsp │── index.jsp web.xml这种按角色分包的方式是课设里比较清晰的做法:页面目录跟着角色走,过滤器按 URL 前缀判断权限,一眼能看出哪个页面属于哪类用户。在 IDEA 里跑起来的关键点有两个:一是 Project Structure 里把 Web 模块的 Artifacts 配好,二是 Run/Debug Configurations 里 Deployment 标签页一定要把 war exploded 添加进去并写对 Application context,否则启动不报错但浏览器永远 404。Tomcat 8.5 或 9 都可以,JDK 1.8 起步就行。
3. 建库建表:七张表把三个角色的数据视图落到 MySQL
3.1 从需求倒推数据模型
这个项目最值得先做的不是写代码,而是把需求翻译成表结构。三个角色的数据视图完全不同:学生要查成绩、查课程平均分、查班级开课、查教学计划,那学生相关的数据至少要覆盖“谁在哪个班、学了哪些课、考了多少分”;教师要录成绩、查成绩、看任课情况,那课程表里必须能定位到“这门课是哪个老师教的”;管理员要维护六大基础信息,自然引出学院、专业、班级三张字典表。
梳理下来落到七张表:college(学院)、major(专业)、class_info(班级)、student(学生)、teacher(教师)、course(课程)、grade(成绩)。课程和学生的关系通过 grade 表关联,课程和教师通过 course 表里的 teacher_id 关联。至于“教学计划”和“班级开课”这两个查询,课设里常见做法是给 course 表加一个 term 字段表示开课学期,班级通过 class_info 里存的 major_id 关联到专业再关联培养计划,不额外建复杂的关系表,够用且好讲。
3.2 核心建表 SQL:外键和唯一约束一处都不能少
建表语句我按这个模板走,字符集统一用 utf8mb4,避免后面录中文成绩单出现乱码:
CREATE DATABASE IF NOT EXISTS sims_db DEFAULT CHARSET utf8mb4; USE sims_db; CREATE TABLE college ( id INT PRIMARY KEY AUTO_INCREMENT, college_name VARCHAR(50) NOT NULL ); CREATE TABLE major ( id INT PRIMARY KEY AUTO_INCREMENT, major_name VARCHAR(50) NOT NULL, college_id INT, FOREIGN KEY (college_id) REFERENCES college(id) ); CREATE TABLE class_info ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL, major_id INT, grade_year VARCHAR(10), FOREIGN KEY (major_id) REFERENCES major(id) ); CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) UNIQUE NOT NULL, student_name VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL, class_id INT, FOREIGN KEY (class_id) REFERENCES class_info(id) ); CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) UNIQUE NOT NULL, teacher_name VARCHAR(30) NOT NULL, password VARCHAR(64) NOT NULL, college_id INT, FOREIGN KEY (college_id) REFERENCES college(id) ); CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(50) NOT NULL, teacher_id INT, credits DECIMAL(3,1), term VARCHAR(20), FOREIGN KEY (teacher_id) REFERENCES teacher(id) ); CREATE TABLE grade ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT, course_id INT, score DECIMAL(5,2), term VARCHAR(20), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id), UNIQUE KEY uk_stu_course (student_id, course_id) );几个设计决策说下理由。grade 表加了 (student_id, course_id) 联合唯一约束,这是为了防止同一个学生同一门课录两条成绩——没有这个约束,成绩录入页面连续提交两次,数据就花了。score 用 DECIMAL(5,2) 而不是 DOUBLE,因为浮点算平均分会出 89.999999 这种血泪结果。password 字段留 VARCHAR(64),是因为常见做法存 MD5 摘要(32 位 hex),留足长度以后想升级成加盐哈希也不用改表。
外键这里要提醒一句:课设里我建议保留物理外键,因为要的就是数据库层面约束数据完整性。但别加 ON DELETE CASCADE 级联删除,管理员删错一个学院,连带学生成绩全没了,这操作没有后悔药。业务上线项目里很多团队会刻意去掉物理外键改由应用层保证,那是另一套取舍,课设阶段用物理外键反而好解释。
3.3 初始化数据:三个顺序问题导致的外键报错
建完表第一件事是造测试数据。外键在 MySQL 里默认是强校验的,插入顺序错了直接报 “Cannot add or update a child row: a foreign key constraint fails”。常见做法是严格按父表到子表的顺序来:
INSERT INTO college (college_name) VALUES ('计算机学院'); INSERT INTO major (major_name, college_id) VALUES ('软件工程', 1); INSERT INTO class_info (class_name, major_id, grade_year) VALUES ('软工2401', 1, '2024'); INSERT INTO teacher (teacher_no, teacher_name, password, college_id) VALUES ('T001', '王老师', MD5('123456'), 1); INSERT INTO student (student_no, student_name, password, class_id) VALUES ('S001', '张三', MD5('123456'), 1); INSERT INTO course (course_name, teacher_id, credits, term) VALUES ('Java程序设计', 1, 3.5, '2024-2025-1'); INSERT INTO grade (student_id, course_id, score, term) VALUES (1, 1, 88.5, '2024-2025-1');有个坑是命令行客户端里插入中文姓名变成乱码。这通常不是表结构的问题,而是客户端连接字符集没对齐,执行一句 SET NAMES utf8mb4; 再插入基本能解决。涉及 MySQL 安装和字符集配置的细节,网上教程很多,这里只说结论:数据库、表、客户端连接三个层面的字符集必须统一,缺一个都可能翻车。
最后说密码。课设阶段对安全要求不高,但明文存密码在答辩时是明显减分项。常见折中方案是存 MD5 摘要:插入时用 SQL 里的 MD5() 函数,登录时也对用户输入做一次 MD5 再比对。如果你能在答辩时补一句“生产环境一般用 BCrypt 加盐,MD5 已经不够安全”,老师会觉得你不仅会做,还知道边界在哪。
4. 核心链路实现:登录、权限过滤、成绩查询与录入的完整代码
4.1 DBUtil:所有 DAO 的地基
这个项目所有 DAO 都依赖一个 JDBC 工具类,把获取连接和释放资源统一收口。代码不长,但每个参数都有讲究:
package com.sims.dao; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBUtil { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/sims_db" + "?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动加载失败,请检查jar包"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps != null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }静态块里用 Class.forName 注册驱动,保证整个应用生命周期里驱动只加载一次。URL 里的参数逐个说:useUnicode=true 和 characterEncoding=utf8 是让 JDBC 连接以 UTF-8 传输中文,少了它页面就是一堆问号;useSSL=false 是本地开发关掉 SSL 握手,不然 MySQL 8 会多一段警告日志;serverTimezone=Asia/Shanghai 是 MySQL 8 的强制要求,不加会报时区错误。
为什么强制用 PreparedStatement 而不是 Statement?两个原因:一是防 SQL 注入,用户输入的学号如果带单引号,Statement 拼 SQL 直接语法错误甚至被注入;二是参数用 ? 占位,代码可读性好,不需要手动处理字符串转义。close 方法里每个资源单独判空,避免一个关闭失败导致后面的资源泄漏,连接泄漏这个问题在课设里不常见,但工作后迟早会碰到。
4.2 登录、会话与过滤器:把「我只能看我能看的」写死
登录是三个角色进入系统的唯一入口,流程不复杂:接收用户名密码,LoginDao 查库校验,成功就把用户对象和角色写进 Session,失败回到登录页提示。核心代码如下:
@WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { private LoginDao loginDao = new LoginDao(); @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 = loginDao.findUser(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); req.getSession().setAttribute("role", user.getRole()); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }登录成功后用 sendRedirect 做重定向,而不是 forward,这是为了避免用户按 F5 刷新时重复提交登录表单——重定向之后浏览器地址栏变成了 index.jsp,刷新不会重新触发 doPost。失败场景用 forward,因为需要把 msg 属性带给登录页展示,而 redirect 是二次请求,request 域里的数据全丢了。
配合第 2 章的 AuthFilter,权限就闭环了:未登录访问任何业务页面,过滤器直接踢回 login.jsp;登录了但角色不匹配,返回 403。这里有一个实际项目里常见的坑——Session 里存用户对象的 key 必须全局统一。有人登录时 setAttribute("loginUser"),过滤器里却 getAttribute("user"),永远取到 null,怎么都跳不出登录循环。这种问题排查起来特别费时间,属于典型的“代码没错但就是不对”的玄学现场。
web.xml 里过滤器映射要配对所有路径但放行登录相关:
<filter> <filter-name>AuthFilter</filter-name> <filter-class>com.sims.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>url-pattern 用 /* 而不是 /login.jsp 或 /index.jsp,意义在于所有请求统一过闸机,再在过滤器内部细化放行规则。多个过滤器同时存在时,执行顺序按 filter-mapping 在 web.xml 里的声明顺序从前往后,这个顺序问题在日志里经常表现为“权限过滤器先执行了导致后面的过滤器还没机会处理”,排查时先看声明顺序。
4.3 成绩查询链路:QueryGradeServlet 到 StudentDao 到 JSP
成绩查询是学生角色的核心功能,链路完整走一遍能理解 MVC 的数据流动。先是 DAO 层:
public List<Grade> findGradesByStudent(int studentId) { List<Grade> list = new ArrayList<>(); String sql = "SELECT g.score, g.term, c.course_name, t.teacher_name " + "FROM grade g " + "JOIN course c ON g.course_id = c.id " + "JOIN teacher t ON c.teacher_id = t.id " + "WHERE g.student_id = ? " + "ORDER BY g.term DESC, c.course_name ASC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, studentId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Grade g = new Grade(); g.setCourseName(rs.getString("course_name")); g.setTeacherName(rs.getString("teacher_name")); g.setScore(rs.getDouble("score")); g.setTerm(rs.getString("term")); list.add(g); } } } catch (SQLException e) { System.err.println("查询成绩失败: " + e.getMessage()); } return list; }SQL 里 JOIN 了两张表,一次性查出课程名和教师名,页面直接 EL 表达式就能渲染,不用再发起第二次查询。ORDER BY term DESC 让最近的学期排在最前面,这是成绩单页面最自然的展示顺序。Java 7 的 try-with-resources 语法会在代码块结束后自动关闭 PreparedStatement 和 ResultSet,但 Connection 仍然显式交给 DBUtil.close 处理,保持统一。
Servlet 层做的只是搭桥:
@WebServlet("/student/QueryGradeServlet") public class QueryGradeServlet extends HttpServlet { private StudentDao studentDao = new StudentDao(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User loginUser = (User) req.getSession().getAttribute("loginUser"); int studentId = loginUser.getId(); List<Grade> gradeList = studentDao.findGradesByStudent(studentId); req.setAttribute("gradeList", gradeList); req.getRequestDispatcher("/student/gradeList.jsp").forward(req, resp); } }学生 ID 不从请求参数里拿,而是从 Session 里取当前登录用户,这是权限设计的关键——如果从 URL 参数接收 studentId,学生把参数改成别人的 ID 就能查别人的成绩,典型的越权漏洞(IDOR)。课设里能主动意识到这一点,是明显的加分表现。forward 到 JSP 后,页面上用 JSTL 标签循环渲染 gradeList 即可。
4.4 教师录成绩:UPDATE 必须配事务
教师角色录成绩的本质是批量 UPDATE score 字段,但这里有个隐藏问题:如果一次录入十门成绩,执行到第五句时数据库连接断开了,前面四条已经提交,后面五条全丢,数据就处于半更新状态。解决方式就是事务:
public boolean updateScore(int studentId, int courseId, double score) { Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启手动事务 String sql = "UPDATE grade SET score = ? WHERE student_id = ? AND course_id = ?"; ps = conn.prepareStatement(sql); ps.setDouble(1, score); ps.setInt(2, studentId); ps.setInt(3, courseId); int rows = ps.executeUpdate(); conn.commit(); return rows > 0; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { System.err.println("回滚失败: " + ex.getMessage()); } } System.err.println("更新成绩失败: " + e.getMessage()); return false; } finally { DBUtil.close(conn, ps, null); } }setAutoCommit(false) 之后,所有 SQL 都在同一个事务里,只有 commit 才真正落库;任何一步抛异常就 rollback 全部撤销。这段代码对应面试里常问的“事务的 ACID 特性”和“JDBC 事务怎么控制”,能把这两句话讲清楚,比背八股文强得多。
录入前还要做参数校验,比如分数范围 0-100,学号课程号必须大于 0。校验放 Servlet 层做一次,DAO 层只负责执行,这样非法请求在进入数据库之前就被拦住了。如果课程已经录过成绩想更新而不是重录,SQL 可以换成 INSERT ... ON DUPLICATE KEY UPDATE,配合表上的联合唯一索引,一条语句同时处理插入和更新两种场景,这也是实际项目常见的 UPSERT 写法。
5. 避坑指南:IDEA 部署、乱码、驱动与 Session 的五次翻车经历
5.1 现象:Tomcat 启动正常,浏览器访问始终 404
原因几乎每次都出在 IDEA 的 Deployment 配置上:Artifacts 没有添加到 Tomcat 的 Deploy at the server startup,或者 Application context 和实际访问路径不一致。新手最容易遇到的是前一种——在 IDEA 里点运行,Tomcat 日志打完 Started,但访问 http://localhost:8080/ 返回 404,因为服务器上根本没有部署任何工程。
解决方式:Run → Edit Configurations → 找到 Tomcat Server → Deployment 标签页 → 点 + 号 → Artifact → 选择项目对应的 war exploded → Application context 核对为 /sims(或你期望的上下文路径)。改完重启 Tomcat,访问 http://localhost:8080/sims/login.jsp。war exploded 和 war 的区别也要知道:前者是解压目录,IDEA 里改 JSP 能热更新,后者是压缩包,每次改代码都要重新打包,开发阶段一律用 exploded。
5.2 现象:登录后页面中文全是问号或乱码
排查乱码要按三段来看:请求编码、响应编码、数据库连接编码。最隐蔽的是数据库连接 URL 少了 characterEncoding=utf8,JSP 页面和过滤器都设了 UTF-8,但数据写进 MySQL 还是乱码,因为 JDBC 连接本身没告诉 MySQL 用 UTF-8 传输。
解决按顺序检查:JSP 头部确认 <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>;所有 doPost 第一行加 req.setCharacterEncoding("UTF-8");JDBC URL 带上 useUnicode=true&characterEncoding=utf8;数据库建库时指定 DEFAULT CHARSET utf8mb4;如果数据已经乱码写进去了,把连接参数改对后重新插入才能恢复,已经乱码的那批数据基本救不回来,所以这个坑要在一开始就堵住。Tomcat 8.5 及以上 GET 请求的 URI 编码默认就是 UTF-8,不用再改 server.xml。
5.3 现象:启动时 ClassNotFoundException: com.mysql.cj.jdbc.Driver
IDE 里代码能编译但运行报找不到驱动类,九成原因是 JDBC 驱动 jar 只加到了 Module 的 classpath,没有打进最终的 WEB-INF/lib。IDEA 里有个很迷惑人的设计——你在 Project Structure 里添加了库,编译期能过,但 Tomcat 运行时用的是部署目录,WEB-INF/lib 里没有这个 jar 就加载不到。
解决方式:Project Structure → Artifacts → 选中部署的 exploded 目录 → 右侧 Available Elements 里找到 mysql-connector-j 的 jar → 双击添加到 WEB-INF/lib。更稳妥的做法是把 jar 直接复制到项目 webapp/WEB-INF/lib 物理目录下,这样不管怎么重新打包都不会丢。判断问题最直接的办法:打开部署目录看 WEB-INF/lib 里到底有没有这个 jar。
5.4 现象:MySQL 8 连接报 Access denied 或 Communications link failure
MySQL 5.x 和 8.x 在 JDBC 连接上有两个不兼容点:驱动类名从 com.mysql.jdbc.Driver 换成了 com.mysql.cj.jdbc.Driver;连接 URL 强制要求 serverTimezone 参数。如果你本机装的是 8.0 以上版本,驱动 jar 还用的老版本 5.1.x,大概率报 SSL 握手失败或 Access denied。另外 MySQL 8 默认认证插件是 caching_sha2_password,老驱动根本协商不通。
解决方式:驱动包换成 mysql-connector-j 8.0.x,驱动类名写 com.mysql.cj.jdbc.Driver,URL 里加 serverTimezone=Asia/Shanghai,显式声明 useSSL=false。还有个小坑是首次连接时报 “Public Key Retrieval is not allowed”,URL 里加 allowPublicKeyRetrieval=true 即可,这是本地开发的安全妥协,生产环境不要这么干。如果你装了 MySQL 却不知道版本,命令行敲 mysql --version 看一眼,别在这个问题上凭感觉猜。
5.5 现象:登录成功却被过滤器弹回登录页,循环跳转
这问题我在帮人看课设时遇到至少三次起。网上找资料一句话能概括:登录跳转地址没带上下文路径。直接写 resp.sendRedirect("/index.jsp") 在部署路径不是根路径时,浏览器会跳到 localhost:8080/index.jsp,绕过项目和过滤器匹配规则,导致看起来像“登录没生效”。
解决方式:所有重定向统一用 resp.sendRedirect(req.getContextPath() + "/index.jsp"),getContextPath() 会返回 /sims 这样的上下文路径,保证不管部署名是什么都能跳对。另外也要回头检查过滤器里放行规则:如果 AuthFilter 连 login.jsp 和 LoginServlet 也拦,登录请求还没到 DAO 就被踢回登录页,一样是循环。过滤器里放行条件要放在最前面,而且要用 endsWith 或精确匹配来做,别用 contains("login") 这种模糊判断,不然 LoginServlet 以外的、路径里恰好带 login 四个字母的资源也会被放行。
6. 三个改造让这套课设从「交差」变成「作品」
6.1 把 DBUtil 从 DriverManager 换成 Druid 连接池
课设项目里每次查询都新建连接、用完关闭,功能没问题但效率很低——连接创建的代价远高于 SQL 执行本身。面试官问“连接池解决什么问题”时,你需要能说出:复用连接、控制并发、避免频繁握手。改造很简单,引入 Druid 后初始化一个数据源,DAO 里从数据源拿连接:
DruidDataSource ds = new DruidDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/sims_db?serverTimezone=Asia/Shanghai"); ds.setUsername("root"); ds.setPassword("123456"); ds.setInitialSize(5); ds.setMaxActive(20); ds.setMaxWait(60000);DAO 里把 DBUtil.getConnection() 换成 ds.getConnection(),其余代码一行不用改。答辩时能讲出 initialSize 和 maxActive 的区别——最小空闲连接数和最大活跃连接数,说明你对资源管理有意识。
6.2 加一张「挂科统计」报表:JOIN+GROUP BY+HAVING 一次讲透
课程设计里最常见的加分项是给管理员加一个统计报表。比如“哪些学生有挂科记录,挂了几门”,一条 SQL 就能撑起整个功能:
SELECT s.student_no, s.student_name, COUNT(g.course_id) AS fail_count FROM student s LEFT JOIN grade g ON s.id = g.student_id AND g.score < 60 GROUP BY s.id, s.student_no, s.student_name HAVING fail_count > 0 ORDER BY fail_count DESC;这个查询能串起好几个知识点:LEFT JOIN 保证没选课的学生也出现在结果集里(只是挂科数为 0),用 GROUP BY 按学生分组,HAVING 是对分组后的结果做筛选——这也是面试里“HAVING 和 WHERE 区别”的最佳例子:WHERE 在分组前过滤行,HAVING 在分组后过滤组。你把这条 SQL 的每部分拆给老师听,比背十道 mysql 基础面试题都有说服力。
6.3 让 DAO 层接口化:代码规范是课设和工作的分水岭
最后一个小改造:把 StudentDao、TeacherDao 定义成接口,实现类放 dao.impl 包,Servlet 里只依赖接口。这是前面所有改动的收尾——连接池解决性能,接口化解决的是维护性。接口的意义在于把“调用方”和“实现方”解耦:今天用 JDBC 写实现类,明天换成 MyBatis 的实现类,Servlet 的代码完全不用动。答辩时说出“面向接口编程”和“开闭原则”两个词,配合前面几章的实战代码,整份课设的完成度立刻上一个档次。
这套跑通的项目我前后带过几个学弟复现,最大的感受是:网上 Java Web 课设源码不难找,但绝大多数人拿到手只会改个数据库密码就交差,被老师一个问题问倒就露馅。我以前第一次做类似管理系统时也不懂分层,全堆在 Servlet 里,答辩被问“两个人同时录成绩会不会覆盖”直接愣住。从那以后我每次做 Java+MySQL 的 Web 项目,都强制自己走一遍“先拆 DAO、再定权限、最后写业务”的顺序,装完环境先跑通再改代码。这套编号 100010724 的资源把源码、SQL 脚本和部署说明都配齐了,你照着第 4 章的链路走一遍,再挑第 6 章任意一个改造点动手,课设答辩基本稳。希望帮到你。
本文还有配套的精品资源,点击获取