简介:基于JavaWeb课程设计的学生选课系统源码与数据库打包,适用对象为正在完成JavaWeb课程设计的在校学生,以及希望动手实践Servlet+JSP开发流程的初学者。项目围绕学生选课业务,完整提供课程浏览、学生选课、退课管理等典型功能模块,整体按照分层设计思路组织代码,便于阅读参考与二次扩展。压缩包共477个文件,体量约5.26MB,核心部分包括70个Java类、76个JSP页面、15个XML配置和1个SQL数据库脚本;同时配有CSS样式、JavaScript交互、SVG图标以及字体文件,前端页面资源较齐全,目录结构清晰,可快速定位代码、页面和数据库脚本。通过学习这套项目,读者既能获得可直接运行的课程设计成果,也能加深对JavaWeb分层架构、数据访问及前后端协作方式的理解。已有786人浏览学习,适合作为课程设计选题参考或JavaWeb入门实战练习。
1. 学生选课系统,JavaWeb 课程设计里绕不开的“硬骨头”
如果你正在为 JavaWeb 课程设计发愁,或者手头刚拿到一份名为“学生选课系统源码+数据库”的压缩包却不知道怎么跑起来,这篇文章就是给你写的。学生选课系统几乎是 JavaWeb 教学案例里出现频率最高的题目,因为它麻雀虽小五脏俱全:前端页面、Servlet 控制层、JDBC 数据库访问、事务处理、外键约束、多表联查,一个不落。它不像商城那样业务庞大,但足够把 JavaWeb 的核心能力完整展示一遍,这也是为什么很多老师反复拿它当课程设计题目的原因。本文会把这个系统拆开讲透:从技术选型、项目结构、数据库设计,到环境搭建、核心代码实现,再到那些让你半夜抓狂的踩坑点,最后给你几个从课程设计升级到毕设水平的技巧。无论你是第一次做 JavaWeb 项目,还是想把手里的代码改得能上台答辩,都能照着一步步做出来。
2. 先立住技术底座:从 JSP+Servlet 到 SSM,选课系统的选型逻辑
2.1 为什么课程设计首选 JSP+Servlet,而不是 Spring Boot
很多同学拿到题目第一反应是“直接上 Spring Boot 不就行了”,但这恰恰是课程设计里最容易翻车的思路。Spring Boot 把配置全部封装好了,你写几行注解就能跑起来,但老师看的是你懂不懂底层原理。用 Spring Boot 做完的选课系统,答辩时被问“Servlet 生命周期”“请求转发和重定向的区别”,你大概率答不上来。常见的做法是,课程设计阶段用 JSP+Servlet+JDBC 这套组合,等做到毕业设计再上 Spring Boot,把地基打牢之后再谈省事。
JSP+Servlet 这套组合的好处在于,它把 JavaWeb 的完整链路暴露在你面前:浏览器发请求 → Tomcat 解析 → Servlet 接收 → 调用 Service/DAO → JDBC 操作 MySQL → 返回结果 → JSP 渲染页面。每一步你都能看到、能调试、能讲清楚。相比之下,SSM(Spring+SpringMVC+MyBatis)是三层架构的经典落地方案,适合团队协作和后期维护,但学习曲线陡峭。对于课程设计,时间通常只有两到四周,用 JSP+Servlet 把一套 MVC 写明白,比用框架堆一个黑匣子更能拿到分数。
2.2 MVC 分层与目录结构:让老师一眼看出你会工程化
选课系统的目录结构虽然没有硬性规定,但一个规范的包结构能让代码的可读性和答辩印象分提升不少。我一般会按 controller、service、dao、entity、filter、util 这六个包去组织,对应的职责分别是接收请求、业务逻辑、数据库操作、实体映射、拦截过滤和工具方法。
src/main/java ├── com.course.controller # Servlet 控制层 ├── com.course.service # 业务逻辑接口 + 实现 ├── com.course.dao # 数据访问层 ├── com.course.entity # 实体类(Student/Course/SelectCourse) ├── com.course.filter # 编码过滤器、登录过滤器 ├── com.course.util # DBUtil、字符串工具类 src/main/webapp ├── WEB-INF │ ├── web.xml # 部署描述符,配置 Servlet 映射 │ └── jsp # 存放 JSP 页面 ├── css / js / images # 静态资源这种分层的核心思想是:Servlet 只负责参数接收和页面跳转,不写 SQL;DAO 只负责和数据库打交道,不处理业务判断;Service 居中调度,把两者串起来。后面要加一个“选课上限 20 人”的业务规则时,你只需要改 Service 层,页面和控制层都不用动。这就是分层的价值,也是老师在代码审查时最关注的工程化意识。
2.3 数据库设计:学生表、课程表、选课表,三张表就够了
选课系统的核心业务是“学生选课”,围绕这个业务,最小可行的表结构就是三张表:学生表、课程表、选课关系表。其中选课关系表是典型的多对多中间表,一个学生可以选多门课,一门课可以被多个学生选,学生和课程之间的映射关系就落在选课表里。下面这段 SQL 是这套表结构的常用写法,你要导入的数据库脚本上半部分通常就是这一类建表语句。
-- 学生表 CREATE TABLE tb_student ( student_id VARCHAR(20) PRIMARY KEY COMMENT '学号', student_name VARCHAR(50) NOT NULL COMMENT '姓名', password VARCHAR(64) NOT NULL COMMENT '密码,建议MD5加密存储', gender CHAR(1) DEFAULT '男' COMMENT '性别', class_name VARCHAR(50) COMMENT '班级', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表 CREATE TABLE tb_course ( course_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程编号', course_name VARCHAR(100) NOT NULL COMMENT '课程名称', teacher_name VARCHAR(50) COMMENT '授课教师', credit DECIMAL(3,1) DEFAULT 2.0 COMMENT '学分', max_students INT DEFAULT 50 COMMENT '选课人数上限', selected_num INT DEFAULT 0 COMMENT '已选人数', schedule VARCHAR(100) COMMENT '上课时间地点', CONSTRAINT chk_selected CHECK (selected_num >= 0 AND selected_num <= max_students) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选课关系表 CREATE TABLE tb_select_course ( select_id INT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(20) NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', score DECIMAL(4,1) DEFAULT NULL COMMENT '成绩,录入后填写', UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES tb_student(student_id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES tb_course(course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这段建表 SQL 里有几个细节值得注意。第一,选课表设置了联合唯一索引uk_student_course,保证同一个学生不能重复选同一门课;第二,课程表的selected_num字段配合 CHECK 约束,从数据库层面杜绝了“选课人数超过上限”这种脏数据;第三,字符集用 utf8mb4 而不是 utf8,因为 utf8 在 MySQL 里存不下 emoji 和生僻字,虽然选课系统用不到,但这个习惯在数据库同步软件和后期扩展时能少踩很多坑。
如果你的源码包里还有角色表、管理员表,那通常是额外扩展了“后台管理”模块。课程设计阶段,管理员登录可以复用学生表,加一个role字段区分,没必要单独开表,后期真要拆也很容易。
3. 把源码跑起来:从导入数据库到 IDEA 部署 Tomcat 的完整步骤
3.1 导入数据库脚本:半小时就能完成的起步操作
拿到“源码+数据库.zip”压缩包,解压后第一件事不是打开 IDEA,而是先把数据库脚本导进 MySQL。很多同学一上来就启动 Tomcat,结果页面全是 500 错误,报错一大堆“Table 'xxx.database' doesn't exist”,就是因为数据库脚本没导入。
打开你的 MySQL 客户端(Navicat、DataGrip、或者命令行都行),先创建一个数据库,名字建议和你项目里 JDBC 连接串保持一致,常见的有course_system或者student_course,具体看压缩包里数据库目录下的 SQL 文件名或项目里的 db.properties 配置。接着选择“运行 SQL 文件”,把解压出来的 .sql 文件导进去。执行完后检查一下表是否创建成功:
-- 在 MySQL 命令行或客户端中执行 USE course_system; SHOW TABLES; -- 期望输出:tb_student, tb_course, tb_select_course 等表名 -- 确认初始数据是否存在 SELECT * FROM tb_student LIMIT 5; SELECT * FROM tb_course LIMIT 10;逻辑说明:SHOW TABLES用来确认建表语句执行成功,SELECT用来确认初始化数据存在。注意,如果你导入的 SQL 文件里有DROP DATABASE IF EXISTS开头,那它会在导入时自动重建数据库,你手动创建的库名和脚本里的库名可能不一致,最终以脚本内定义的为准。
参数说明:导入前确认 MySQL 版本在 5.7 以上,8.0 更佳。如果用的是 5.5 或更老的版本,utf8mb4 字符集和 CHECK 约束可能不被完整支持,出现语法错就替换成 utf8。连接 MySQL 时如果报“Public Key Retrieval is not allowed”,这是 8.0 驱动的安全限制,在 JDBC 连接串末尾加上allowPublicKeyRetrieval=true&useSSL=false即可解决。
3.2 配置 IDEA 的运行环境:JDK 版本和 Web 依赖这一步别偷懒
数据库导完,接着配置项目环境。如果你的压缩包里有完整的 .idea 目录,IDEA 打开后可能能直接识别;但如果源码是别人用 Eclipse 写的,或者压缩包里只有 src 和 webapp 目录,就需要手动配置。常见做法是在 IDEA 里新建一个 JavaWeb 项目,然后把源码目录拷进 src 里,再把 webapp 目录设置成 Web 资源目录。
第一步,确认你的 JDK 版本。这个项目的代码如果是十几年前的老写法,基于 JDK 8 开发,那一定要装 JDK 8,不要图新鲜装 JDK 17 或 21。Servlet 和 JSP 的老 API 在高版本 JDK 下虽然能编译,但 Tomcat 版本不匹配时会出现 “UnsupportedClassVersionError”,这个错很经典,低版本 Tomcat 跑不了高版本 JDK 编译的 class 文件。
第二步,把所有 jar 包加到项目依赖里。打开 Project Structure(快捷键 Ctrl+Alt+Shift+S),在 Libraries 里把 WebContent/WEB-INF/lib(Eclipse 项目)或 src/main/webapp/WEB-INF/lib 下的 mysql-connector-java、jstl、servlet-api 等 jar 全部添加进去。如果你发现压缩包里没有 lib 目录,那大概率是导出的源码做了依赖瘦身,推荐安装 Maven 后手动补依赖,或者直接从本地仓库复制一份 jar 进来。
3.3 启动前的最后 100 米:配置 Tomcat 和 JDBC 连接参数
环境配置好之后,最核心的一步是确认项目的 JDBC 数据库连接信息指向你的本地数据库。找到项目里的 db.properties 或 DBUtil.java,内容通常是这样的:
// DBUtil.java - JDBC 连接工具类 package com.course.util; import java.sql.*; public class DBUtil { // 数据库连接参数,不同项目的 key 名可能不同 private static final String URL = "jdbc:mysql://localhost:3306/course_system?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; // 注意:启动前先确认数据库名、用户名、密码这三项和本地环境一致 static { try { // 常见的坑:驱动类名写错或 jar 包缺失会导致 ClassNotFoundException Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败", e); } } public static Connection getConnection() throws SQLException { // 不要在循环里调用这个方法,连接池另有用途 return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 关闭资源时注意顺序:先 rs,再 stmt,最后 conn if (rs != null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt != null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn != null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码里最容易踩坑的地方是Class.forName这一行。MySQL 5.x 的驱动类是com.mysql.jdbc.Driver,MySQL 8.x 的驱动类是com.mysql.cj.jdbc.Driver,两个类名不一样。如果你用的是 8.x 的驱动 jar 但写的是 5.x 的类名,会直接报 ClassNotFoundException。同理,URL 里的serverTimezone参数在 8.x 驱动下是必填的,不填会报“The server time zone value is unrecognized”。
参数说明:useSSL=false表示不启用加密连接,本地开发就这么写,避免证书报错;characterEncoding=utf8保证中文参数正常传输,配合 request 的 setCharacterEncoding("UTF-8") 使用,这是中文乱码问题的第一道防线;allowPublicKeyRetrieval=true是 MySQL 8.0 的专属参数,认证时允许客户端向服务器获取公钥。
Tomcat 的配置相对简单:在 IDEA 右上角打开 Run/Debug Configurations,点“+”新增 Tomcat Server → Local,选择本地 Tomcat 安装目录,然后在 Deployment 标签页把当前项目以 war exploded 方式部署进去。这里有个参数建议设定:Application context 填/而不是默认的项目名,这样访问地址就是http://localhost:8080/而不是http://localhost:8080/course_system_war_exploded/,课程设计答辩时页面上的跳转链接全是写死的相对路径,部署路径不匹配就会 404。
4. 核心功能实现拆解:选课、退课、列表展示的前后端联动
4.1 登录模块:Session 会话管理,别把密码明文存数据库
选课系统第一个功能通常就是登录。登录模块看起来简单,但它是 Session 管理、请求转发、密码安全这些知识点的集中考验点。常见的实现方式是:登录页面提交表单到 LoginServlet,Servlet 里查询数据库校验用户名密码,成功则把用户信息放进 Session,失败则跳回登录页并带上错误提示。
// LoginServlet.java - 登录处理核心逻辑 @WebServlet("/login") public class LoginServlet extends HttpServlet { private StudentService studentService = new StudentService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数,注意先设置编码,否则中文名乱码 request.setCharacterEncoding("UTF-8"); String studentId = request.getParameter("studentId"); String password = request.getParameter("password"); // 2. 简单校验空值:前端校验只是体验,后端校验才是安全 if (studentId == null || studentId.trim().isEmpty() || password == null || password.isEmpty()) { request.setAttribute("error", "学号和密码不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 3. 调用 Service 层验证,不要把 SQL 写在这里 Student student = studentService.login(studentId, MD5Util.md5(password)); if (student == null) { request.setAttribute("error", "学号或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 4. 登录成功,把用户信息写入 Session HttpSession session = request.getSession(); session.setAttribute("currentStudent", student); // 5. 重定向到主页,避免刷新页面时表单重复提交 response.sendRedirect(request.getContextPath() + "/courseList"); } }这段代码值得注意的点有三个。第一,密码存储用的是 MD5 加密,虽然 MD5 在现在看加密强度已经不够,但课程设计阶段比明文存储已经进了一步,如果你做的是毕设,建议换成 BCrypt 加盐哈希,这个改进答辩时能说清楚就加分。第二,登录失败用的是forward而不是redirect,因为 forward 可以把 request 里的错误信息带给 JSP 页面渲染,而 redirect 是重新发起一次请求,request 里的数据就丢了。第三,登录成功后必须用sendRedirect而不是forward,否则用户刷新页面,浏览器会重复提交最后一次 POST 请求,系统里可能出现重复选课记录。
4.2 选课模块:事务 + 防重复提交,这两道保险必须上
选课是整个系统的核心事务。一听到“事务”,很多同学会想到 Spring 的 @Transactional 注解,但 JSP+Servlet 项目里事务是手动管理的。选课的逻辑是:先判断课程人数是否已满,再判断学生是否已经选过这门课,这两个判断都通过后,往选课表插入记录,同时把课程表的已选人数加一。这两步操作必须在一个事务里,否则会出现“插入选课记录成功但课程人数没加”的脏数据。
// SelectCourseServlet.java - 处理选课请求 @WebServlet("/selectCourse") public class SelectCourseServlet extends HttpServlet { private CourseService courseService = new CourseService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从 Session 中获取当前登录学生信息 HttpSession session = request.getSession(false); Student student = (Student) session.getAttribute("currentStudent"); if (student == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } int courseId = Integer.parseInt(request.getParameter("courseId")); Connection conn = null; try { conn = DBUtil.getConnection(); // 2. 开启事务,默认 autocommit 为 true,必须先关掉 conn.setAutoCommit(false); // 3. 查询课程信息,加行级锁,防止并发选课导致超选 Course course = courseDao.findByIdForUpdate(conn, courseId); if (course == null) { request.setAttribute("error", "课程不存在"); request.getRequestDispatcher("/courseList").forward(request, response); return; } if (course.getSelectedNum() >= course.getMaxStudents()) { request.setAttribute("error", "选课人数已满"); request.getRequestDispatcher("/courseList").forward(request, response); return; } // 4. 判断是否已选过 boolean exists = selectCourseDao.exists(conn, student.getStudentId(), courseId); if (exists) { request.setAttribute("error", "你已经选过这门课了"); request.getRequestDispatcher("/courseList").forward(request, response); return; } // 5. 插入选课记录 + 课程人数加 1,两步视为一个原子操作 selectCourseDao.insert(conn, student.getStudentId(), courseId); courseDao.increaseSelectedNum(conn, courseId); conn.commit(); // 6. 成功跳转回选课列表 response.sendRedirect(request.getContextPath() + "/courseList"); } catch (Exception e) { // 7. 任何一步出错,回滚整个事务 if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); request.setAttribute("error", "选课失败,请稍后重试"); request.getRequestDispatcher("/courseList").forward(request, response); } finally { // 8. 关闭连接,恢复 autocommit 默认值 if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }代码里findByIdForUpdate这个 DAO 方法的实现值得展开说说,它的核心是SELECT ... FOR UPDATE语句。基础做法是先普通查询再更新,但高并发下两个请求同时查到 available 状态,就可能出现超选。加FOR UPDATE后,事务在更新前先锁定该行,另一个事务必须等第一个事务提交或回滚才能读这行,从根上解决了并发问题。这个点很多课程设计里都不会实现,你写进去并且能讲清楚原理,答辩时的技术含量直接拉开一个身位。
第二个防重复提交隐患,是用户点击“选课”按钮后网络延迟时反复点击,或者选课成功后按了刷新键,这些操作都会导致同一个选课请求被发送多次。除了用数据库的唯一索引兜底,还可以在页面上做一个“提交后禁用按钮”的前端控制,JavaWeb 里常见的做法是用 Session 里的 token 做幂等校验,但课程设计阶段,靠唯一索引兜底已经能保证数据不会错,只是会报“重复选课”的异常提示而已。
4.3 列表展示:JSP + JSTL 渲染,别在页面里写 Java 脚本片段
课程列表页和数据展示是 JSP 的重头戏,这块也是老师和同学看得最多的地方。很多初学写法是在 JSP 里写<%%>Java 脚本片段,直接连数据库取数据循环输出,这种写法在课程设计里拿不到好分。规范的做法是:Servlet 里把查询结果放进 request 或 session,再用 EL 表达式配合 JSTL 标签库在 JSP 页面里渲染。
<%-- courseList.jsp - 课程列表页面核心片段 --%> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <table border="1" cellpadding="8" cellspacing="0"> <tr> <th>课程编号</th> <th>课程名称</th> <th>授课教师</th> <th>学分</th> <th>已选/上限</th> <th>上课时间</th> <th>操作</th> </tr> <c:forEach items="${courseList}" var="course" varStatus="status"> <tr> <td>${course.courseId}</td> <td>${course.courseName}</td> <td>${course.teacherName}</td> <td>${course.credit}</td> <td> ${course.selectedNum} / ${course.maxStudents} <c:if test="${course.selectedNum >= course.maxStudents}"> <span style="color:red">(已满)</span> </c:if> </td> <td>${course.schedule}</td> <td> <c:choose> <c:when test="${course.selected}"> <a href="javascript:void(0)" onclick="alert('你已经选过这门课')">已选</a> </c:when> <c:when test="${course.selectedNum >= course.maxStudents}"> <span style="color:gray">不可选</span> </c:when> <c:otherwise> <a href="${pageContext.request.contextPath}/selectCourse?courseId=${course.courseId}" onclick="return confirm('确认选这门课?')">选课</a> </c:otherwise> </c:choose> </td> </tr> </c:forEach> </table>这个 JSP 用 JSTL 的c:forEach循环渲染课程列表,用c:choose做分支判断,页面上完全没有 Java 代码,体现的是“视图层只负责展示”的思想。${course.selected}这个属性在 JSP 里直接通过 getter 方法读取,所以你的 Course 实体类里需要有一个selected字段,它的值是在 Servlet 里针对当前登录学生判断“是否已选”后塞进去的。
关于时间格式,如果课程表里有开课时间字段,建议在 JSP 顶部引入<fmt>标签库,用<fmt:formatDate value="${course.startTime}" pattern="yyyy-MM-dd HH:mm"/>格式化日期,而不是在 Java 代码里把 Date 转成 String 再传出去。前者更符合 MVC 的分工定位,后者会在实体类和页面之间强耦合。
4.4 退课模块:权限校验 + 删除级联,一个都不能省
退课的逻辑和选课是镜像关系:先校验这门课是不是当前学生选的,然后删除选课记录,同时把课程表的已选人数减一,同样需要事务控制。这里最容易犯的错是只做了删除操作,没有把selectedNum减回去,导致课程人数显示虚高。
退课的权限校验比选课更重要,因为选课只能给“当前登录学生”自己选,而退课请求里如果只传课程 ID,不法分子完全可以直接构造一个退课请求把所有学生的选课记录删掉。所以 DAO 层的 delete 方法一定要把 studentId 也作为查询条件,也就是DELETE FROM tb_select_course WHERE course_id=? AND student_id=?,这样即使有人伪造请求,也只能删除自己的选课记录。
5. 运行期避坑指南:那些让 JavaWeb 新手集体翻车的 5 个坑
5.1 页面 404,IDEA 中 Tomcat 部署路径和项目路径对不上
现象:启动 Tomcat 后浏览器访问 http://localhost:8080/login.jsp,页面报 404,但 IDEA 控制台显示 Tomcat 启动成功没有任何报错。
原因:这是部署时 Application context 没配置对。IDEA 默认的 context 是/项目名_war_exploded,你要访问登录页就得输入http://localhost:8080/你的项目名_war_exploded/login.jsp才能打开。但页面里的表单提交地址和超链接通常都是写死的相对路径,比如/login、/courseList,它们默认从根目录开始解析,和实际部署路径对不上,于是请求全部 404。
解决:打开 Run/Debug Configurations 里的 Deployment 标签页,把 Application context 改成/,重启 Tomcat。改完之后,所有写死的路径虽然还是从根开始,但根路径已经被你接管了,就能正常访问。
5.2 ClassNotFoundException: com.mysql.jdbc.Driver,驱动类找不到
现象:启动项目后第一次访问数据库时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。
原因:两种常见情况。第一种是 lib 目录里放的 mysql-connector-java 版本是 8.x,但代码里写的驱动类名是 5.x 的com.mysql.jdbc.Driver;第二种是 jar 包虽然在项目文件目录里存在,但没被发布到 Tomcat 的 WEB-INF/lib 下,IDEA 里有个“打包时是否包含依赖”的选项没勾上。
解决:先看你的项目里 jar 包版本。右键 jar 包选“Add as Library”确认编译阶段没问题,再去 IDEA 的 Artifacts 配置里确认 Output Layout 下 WEB-INF/lib 列表中包含该 jar。代码里的驱动类名和版本对应关系是:MySQL 5.x 对应com.mysql.jdbc.Driver,MySQL 8.x 对应com.mysql.cj.jdbc.Driver,不建议换版本,直接统一到 8.x 然后改代码里的类名。
5.3 中文乱码,从数据库到页面全链路排查的思路
现象:页面上显示的中文全是问号,或者往数据库里插入的中文变成??。
原因:乱码是 JavaWeb 里最玄学的问题之一,根源在于“编码不一致”——浏览器是 UTF-8 编码,Tomcat 默认用 ISO-8859-1 解码请求参数,数据库连接没指定编码,MySQL 表又建成了 latin1 字符集,四层里只要有一层对不上就乱。
解决:按顺序排查四层,别盲目改。第一层是 JSP 页面,在文件头部加<%@ page contentType="text/html;charset=UTF-8" language="java" %>;第二层是请求参数,在 Servlet 的 doPost 方法里加request.setCharacterEncoding("UTF-8"),doGet 方法在 Tomcat 8 之后默认是 UTF-8,但保险起见也可以在 web.xml 里配一个 CharacterEncodingFilter;第三层是 JDBC 连接串,在 URL 上加?characterEncoding=utf8;第四层是数据库表,确认建表时CHARSET=utf8mb4而不是建完再改。四层都对齐后,如果还乱,看一下 MySQL 的SHOW VARIABLES LIKE 'character_set%'输出,把 character_set_server 也设成 utf8mb4。
5.4 选课提交后页面刷新,同一门课选了两次
现象:用户在选课界面点了两次“选课”按钮,或者选课成功后按 F5 刷新,数据库里出现了同一个人同一门课的两条选课记录。
原因:这是经典的表单重复提交问题。第一次 POST 请求成功后,浏览器地址栏停留在提交地址上,按 F5 时浏览器会重新发送上一次的 POST 请求。如果代码里选课成功后用的是 forward 跳转,这个重复提交就会直接再次执行业务逻辑。
解决:三个层面套着用。第一层,选课成功后用response.sendRedirect(request.getContextPath() + "/courseList")做重定向,这是最有效的做法,重定向后地址栏变成 GET 请求的地址,刷新就不会重放 POST;第二层,数据库层加联合唯一索引,即便请求重复了插入也会报“Duplicate entry”而不会产生脏数据;第三层,前端在按钮的 onclick 里加this.disabled = true,点击后立即禁用按钮,降低人为重复点击的概率。三层都做好,选课流程才算稳。
5.5 数据库连接超时:“Connection is not available, request timed out”
现象:系统刚启动时用着正常,放了十分钟没操作,再点选课就报Cannot get a connection, pool exhausted或者Connection is not available,重启 Tomcat 又恢复。
原因:这是 JDBC 直连方式的经典毛病。DriverManager.getConnection 每次都创建一个物理连接,用完后调用 close 虽然释放了连接,但 MySQL 的wait_timeout默认 8 小时,如果连接池(或你代码里手动维护的短连接)闲置时间过长,MySQL 会主动掐断连接。下一次请求拿到的是已经被 MySQL 丢掉的连接,等于是拿着一把已经换了锁芯的钥匙去开门。
解决:如果代码里用的是 C3P0、Druid 这类连接池,把连接池的testWhileIdle设为 true,validationQuery设为SELECT 1;如果用的是博客里手写的“假连接池”或者直连,最简单的方案是配置一台数据库同步软件的本地副本做容灾,或者修改 MySQL 的wait_timeout为更大的值。但治标不治本,课程设计答辩后要上生产,还是得换 Druid 连接池,它自带空闲连接检测和回收机制,一句配置就能搞定这个问题。Druid 的配置示例:
// druid.properties - Druid 连接池最小配置 driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/course_system?useSSL=false&serverTimezone=Asia/Shanghai username=root password=你的密码 # 关键参数:空闲连接检测 testWhileIdle=true validationQuery=SELECT 1 timeBetweenEvictionRunsMillis=60000testWhileIdle的原理是:连接池后台每 60 秒(由timeBetweenEvictionRunsMillis控制)对空闲连接执行一次SELECT 1,这条 SQL 如果执行失败,连接就会被判定为失效并移除,避免把坏连接交给业务代码。这个参数的效果是你在前台完全感知不到,但系统的长时稳定性会明显提升。
6. 从课程设计到毕业设计:这几个进阶技巧让系统上一个档次
课程设计做到系统能跑、功能完整,已经及格了,但如果想让老师觉得你的代码有想法、有扩展性,还有几个小改动值得做,工作量不大但能显著提升系统的工程味道。
第一个改动是加一个登录拦截器。现在的课程设计项目大多在每个 Servlet 里手工判断 Session 是否为空,代码重复率高,而且容易漏掉某个页面。用一个 Filter 统一处理,在 web.xml 里配置拦截路径,就能把校验逻辑收拢到一处。这个 Filter 也顺带做了编码处理,一箭双雕。对应配置大概是:
<!-- web.xml 中的过滤器配置 --> <filter> <filter-name>LoginFilter</filter-name> <filter-class>com.course.filter.LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>Filter 类里逻辑很简单:放行 login.jsp 和 /login 路径,其余请求都检查 Session 中是否有用户对象,没有就重定向到登录页。这个模式是 JavaWeb 面试的高频考点,你写出来就说明你理解“横切关注点”这回事。
第二个改动是所有列表页加分页。课程设计里课程列表一般就几十行,不分页看不出问题,但分页本身是一个完整的计算链路:总记录数、总页数、当前页、每页条数、偏移量。自己写一个 PageBean 工具类,把分页逻辑封装起来,一次做好,后面的订单列表、成绩列表都能复用。这个改进在答辩时非常加分。分页计算的核心 SQL 是LIMIT offset, pageSize,其中offset = (currentPage - 1) * pageSize,要注意 offset 不能填负数。
第三个改动是把密码加密从 MD5 升级到加盐哈希。课程设计大部分用 MD5,但答辩时老师很可能会问“MD5 现在还能保证安全吗”。你可以回答:MD5 已被证明可碰撞,更稳妥的方案是 BCrypt 或 PBKDF2,每次加密生成一个随机盐值,存储在密码字段中,校验时重新计算。虽然 JSP+Servlet 项目里引入 Spring Security 的 BCrypt 有点重,但单独引入一个 jBCrypt 库才几十 KB,改动量两三行代码。这个改动虽然小,却能证明你跟得上安全形势,不是只会抄代码。
做完这三个改动,你的选课系统从结构、性能、安全三个角度看,都已经不是纯粹的“课程设计水平”了。我自己的经验是,课程设计就是给毕业设计打前站的,你在这里把 Filter 和事务搞明白了,做毕设时切到 Spring Boot 会发现思路完全一致,只是操作方式从 XML 变成了注解。很多人感觉 JavaWeb 难,不是原理难,是手写配置的细节太碎,但恰恰是这些碎,逼着你把每个步骤都搞清楚,往后用框架时才不会被黑匣子卡住。希望帮到你。
本文还有配套的精品资源,点击获取