学生选课系统JavaWeb课程设计全攻略:从源码到部署避坑指南
发布时间:2026/9/24 19:17:26 锦皓数字建站

简介基于JavaWeb课程设计的学生选课系统源码与数据库打包适用对象为正在完成JavaWeb课程设计的在校学生以及希望动手实践ServletJSP开发流程的初学者。项目围绕学生选课业务完整提供课程浏览、学生选课、退课管理等典型功能模块整体按照分层设计思路组织代码便于阅读参考与二次扩展。压缩包共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. 先立住技术底座从 JSPServlet 到 SSM选课系统的选型逻辑2.1 为什么课程设计首选 JSPServlet而不是 Spring Boot很多同学拿到题目第一反应是“直接上 Spring Boot 不就行了”但这恰恰是课程设计里最容易翻车的思路。Spring Boot 把配置全部封装好了你写几行注解就能跑起来但老师看的是你懂不懂底层原理。用 Spring Boot 做完的选课系统答辩时被问“Servlet 生命周期”“请求转发和重定向的区别”你大概率答不上来。常见的做法是课程设计阶段用 JSPServletJDBC 这套组合等做到毕业设计再上 Spring Boot把地基打牢之后再谈省事。JSPServlet 这套组合的好处在于它把 JavaWeb 的完整链路暴露在你面前浏览器发请求 → Tomcat 解析 → Servlet 接收 → 调用 Service/DAO → JDBC 操作 MySQL → 返回结果 → JSP 渲染页面。每一步你都能看到、能调试、能讲清楚。相比之下SSMSpringSpringMVCMyBatis是三层架构的经典落地方案适合团队协作和后期维护但学习曲线陡峭。对于课程设计时间通常只有两到四周用 JSPServlet 把一套 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 只负责参数接收和页面跳转不写 SQLDAO 只负责和数据库打交道不处理业务判断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 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课关系表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表 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 doesnt 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 连接串末尾加上allowPublicKeyRetrievaltrueuseSSLfalse即可解决。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快捷键 CtrlAltShiftS在 Libraries 里把 WebContent/WEB-INF/libEclipse 项目或 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?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; 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.DriverMySQL 8.x 的驱动类是com.mysql.cj.jdbc.Driver两个类名不一样。如果你用的是 8.x 的驱动 jar 但写的是 5.x 的类名会直接报 ClassNotFoundException。同理URL 里的serverTimezone参数在 8.x 驱动下是必填的不填会报“The server time zone value is unrecognized”。参数说明useSSLfalse表示不启用加密连接本地开发就这么写避免证书报错characterEncodingutf8保证中文参数正常传输配合 request 的 setCharacterEncoding(UTF-8) 使用这是中文乱码问题的第一道防线allowPublicKeyRetrievaltrue是 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 管理、请求转发、密码安全这些知识点的集中考验点。常见的实现方式是登录页面提交表单到 LoginServletServlet 里查询数据库校验用户名密码成功则把用户信息放进 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 注解但 JSPServlet 项目里事务是手动管理的。选课的逻辑是先判断课程人数是否已满再判断学生是否已经选过这门课这两个判断都通过后往选课表插入记录同时把课程表的已选人数加一。这两步操作必须在一个事务里否则会出现“插入选课记录成功但课程人数没加”的脏数据。// 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 contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table border1 cellpadding8 cellspacing0 tr th课程编号/th th课程名称/th th授课教师/th th学分/th th已选/上限/th th上课时间/th th操作/th /tr c:forEach items${courseList} varcourse varStatusstatus 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 stylecolor:red已满/span /c:if /td td${course.schedule}/td td c:choose c:when test${course.selected} a hrefjavascript:void(0) onclickalert(你已经选过这门课)已选/a /c:when c:when test${course.selectedNum course.maxStudents} span stylecolor:gray不可选/span /c:when c:otherwise a href${pageContext.request.contextPath}/selectCourse?courseId${course.courseId} onclickreturn 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} patternyyyy-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 页面 404IDEA 中 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.DriverMySQL 8.x 对应com.mysql.cj.jdbc.Driver不建议换版本直接统一到 8.x 然后改代码里的类名。5.3 中文乱码从数据库到页面全链路排查的思路现象页面上显示的中文全是问号或者往数据库里插入的中文变成??。原因乱码是 JavaWeb 里最玄学的问题之一根源在于“编码不一致”——浏览器是 UTF-8 编码Tomcat 默认用 ISO-8859-1 解码请求参数数据库连接没指定编码MySQL 表又建成了 latin1 字符集四层里只要有一层对不上就乱。解决按顺序排查四层别盲目改。第一层是 JSP 页面在文件头部加% page contentTypetext/html;charsetUTF-8 languagejava %第二层是请求参数在 Servlet 的 doPost 方法里加request.setCharacterEncoding(UTF-8)doGet 方法在 Tomcat 8 之后默认是 UTF-8但保险起见也可以在 web.xml 里配一个 CharacterEncodingFilter第三层是 JDBC 连接串在 URL 上加?characterEncodingutf8第四层是数据库表确认建表时CHARSETutf8mb4而不是建完再改。四层都对齐后如果还乱看一下 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设为 truevalidationQuery设为SELECT 1如果用的是博客里手写的“假连接池”或者直连最简单的方案是配置一台数据库同步软件的本地副本做容灾或者修改 MySQL 的wait_timeout为更大的值。但治标不治本课程设计答辩后要上生产还是得换 Druid 连接池它自带空闲连接检测和回收机制一句配置就能搞定这个问题。Druid 的配置示例// druid.properties - Druid 连接池最小配置 driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/course_system?useSSLfalseserverTimezoneAsia/Shanghai usernameroot password你的密码 # 关键参数空闲连接检测 testWhileIdletrue validationQuerySELECT 1 timeBetweenEvictionRunsMillis60000testWhileIdle的原理是连接池后台每 60 秒由timeBetweenEvictionRunsMillis控制对空闲连接执行一次SELECT 1这条 SQL 如果执行失败连接就会被判定为失效并移除避免把坏连接交给业务代码。这个参数的效果是你在前台完全感知不到但系统的长时稳定性会明显提升。6. 从课程设计到毕业设计这几个进阶技巧让系统上一个档次课程设计做到系统能跑、功能完整已经及格了但如果想让老师觉得你的代码有想法、有扩展性还有几个小改动值得做工作量不大但能显著提升系统的工程味道。第一个改动是加一个登录拦截器。现在的课程设计项目大多在每个 Servlet 里手工判断 Session 是否为空代码重复率高而且容易漏掉某个页面。用一个 Filter 统一处理在 web.xml 里配置拦截路径就能把校验逻辑收拢到一处。这个 Filter 也顺带做了编码处理一箭双雕。对应配置大概是!-- web.xml 中的过滤器配置 -- filter filter-nameLoginFilter/filter-name filter-classcom.course.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern /filter-mappingFilter 类里逻辑很简单放行 login.jsp 和 /login 路径其余请求都检查 Session 中是否有用户对象没有就重定向到登录页。这个模式是 JavaWeb 面试的高频考点你写出来就说明你理解“横切关注点”这回事。第二个改动是所有列表页加分页。课程设计里课程列表一般就几十行不分页看不出问题但分页本身是一个完整的计算链路总记录数、总页数、当前页、每页条数、偏移量。自己写一个 PageBean 工具类把分页逻辑封装起来一次做好后面的订单列表、成绩列表都能复用。这个改进在答辩时非常加分。分页计算的核心 SQL 是LIMIT offset, pageSize其中offset (currentPage - 1) * pageSize要注意 offset 不能填负数。第三个改动是把密码加密从 MD5 升级到加盐哈希。课程设计大部分用 MD5但答辩时老师很可能会问“MD5 现在还能保证安全吗”。你可以回答MD5 已被证明可碰撞更稳妥的方案是 BCrypt 或 PBKDF2每次加密生成一个随机盐值存储在密码字段中校验时重新计算。虽然 JSPServlet 项目里引入 Spring Security 的 BCrypt 有点重但单独引入一个 jBCrypt 库才几十 KB改动量两三行代码。这个改动虽然小却能证明你跟得上安全形势不是只会抄代码。做完这三个改动你的选课系统从结构、性能、安全三个角度看都已经不是纯粹的“课程设计水平”了。我自己的经验是课程设计就是给毕业设计打前站的你在这里把 Filter 和事务搞明白了做毕设时切到 Spring Boot 会发现思路完全一致只是操作方式从 XML 变成了注解。很多人感觉 JavaWeb 难不是原理难是手写配置的细节太碎但恰恰是这些碎逼着你把每个步骤都搞清楚往后用框架时才不会被黑匣子卡住。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。