Java Web版学生选课系统源码包:含完整MVC结构、MySQL建表脚本与Tomcat部署说明

本文还有配套的精品资源,点击获取

简介:直接可运行的学生选课Web应用,用Servlet做控制器、JSP渲染页面、MySQL存数据,严格按MVC分层组织。前端有登录页Login.jsp、首页index.jsp、顶部导航Top.jsp、提示页Message.jsp等;后端包含SaveServlet等核心处理类,src下按com.xxx包结构划分业务逻辑,WEB-INF里配好web.xml,还附带init_db.sql一键建库建表(含学生、课程、选课三张关联表)。项目自带pom.xml支持Maven构建,build和classes目录已预设编译路径,扔进Tomcat 7/8/9就能跑。README.md写清楚了启动步骤、账号密码示例和功能入口,适合课程设计快速上手,也方便对照学习请求流转、会话管理、表单提交、重定向跳转、数据库CRUD及外键约束实现。

1. 项目概述:为什么这个选课系统值得你花30分钟认真看一遍

我带过六届计算机专业的课程设计,每年都有学生卡在“怎么把课本上的MVC概念变成一个能点开、能登录、能选课的网页”这一步。不是不会写代码,而是不知道各层之间到底该怎么咬合——Servlet里该不该查数据库?JSP里能不能直接写SQL?Session存什么、什么时候清?这些细节教科书不讲,百度搜出来全是碎片,最后只能抄个半成品改来改去,连自己改了哪都不清楚。这个Java Web版学生选课系统,就是我当年带着学生从零搭起来、又反复拆解重构三遍的“教学锚点”。它不炫技,没用Spring Boot自动装配,没上Redis缓存,就用最朴素的Servlet + JSP + MySQL组合,但每一行代码都在回答一个具体问题:请求进来后,数据怎么流、状态怎么传、错误怎么兜住、页面怎么跳转才不丢数据。比如Login.jsp提交表单,目标是LoginServlet,但实际处理逻辑全在UserDAO里;用户登录成功后,不是简单跳转index.jsp,而是先往session里塞user对象,再重定向,避免F5刷新重复提交;选课操作涉及三张表(student/course/select_course),外键约束怎么设、级联删除要不要开、事务边界划在哪,init_db.sql里每条CREATE TABLE语句都带注释说明设计意图。它不是一个“能跑就行”的Demo,而是一套可追溯、可打断、可逐层调试的Web开发流水线。你打开src/com/edu/dao/UserDAO.java,能看到JDBC连接获取、PreparedStatement参数绑定、ResultSet结果映射的完整链条;你翻web.xml,会发现 里路径和LoginServlet类名严丝合缝;你执行init_db.sql,三张表的ENGINE=InnoDB、CHARSET=utf8mb4、外键ON DELETE RESTRICT都写得明明白白。这不是让你复制粘贴交作业的包,而是给你一把解剖刀——切开任何一个环节,都能看清血肉怎么长、神经怎么连。如果你正被课程设计 deadline 追着跑,它能让你2小时搭起环境、半天跑通流程;如果你想真正吃透Java Web底层逻辑,它就是那个你愿意反复打断点、一行行跟进去看request.getParameter()返回值怎么变的“活体标本”。

2. 整体架构与分层设计:MVC不是三个字母,是三条不能交叉的轨道

2.1 MVC三层如何物理落地:从目录结构读懂设计哲学

很多人以为MVC就是“Servlet放control、JSP放view、DAO放model”,但真正在Tomcat里跑起来时,目录结构才是第一道防线。这个项目的目录树不是随便拍脑袋定的,每一层都对应着Servlet容器的加载规则和开发者的认知边界:

  • WebContent(或标准叫法WebRoot)是View的绝对领地:Login.jsp、index.jsp、Top.jsp这些文件必须放在这个根目录下,因为Tomcat默认只把这里当静态资源和JSP的发布源。你不能把Login.jsp塞进src里——Servlet容器根本找不到它。更关键的是,Message.jsp这类提示页被设计成纯展示组件:它不处理任何业务逻辑,只通过${requestScope.message}或${sessionScope.user}取值渲染,就像乐高积木的凹槽,只负责接其他模块传来的数据块。

  • WEB-INF是Controller和配置的保险柜:web.xml在这里,所有 和 定义都在此。为什么必须放这里?因为Servlet规范规定,WEB-INF及其子目录下的内容永远不被客户端直接访问。你浏览器输http://localhost:8080/WEB-INF/web.xml,Tomcat会直接返回404。这保证了配置不被暴露,也强制开发者把路由逻辑集中管理。SaveServlet.class编译后放在WEB-INF/classes/com/edu/servlet/下,正是遵循“类路径(classpath)优先级”原则——Tomcat启动时,先加载WEB-INF/classes,再加载WEB-INF/lib里的jar,确保你的自定义Servlet永远优先于第三方库同名类。

  • src下的com包结构是Model的战场:src/com/edu/dao/UserDAO.java、src/com/edu/service/UserService.java、src/com/edu/entity/Student.java,这三个文件构成Model层的铁三角。Student.java是纯粹的数据载体(POJO),只有private字段和getter/setter;UserDAO.java是数据访问的“挖掘机”,只管和MySQL对话,用JDBC执行CRUD,绝不碰业务规则;UserService.java是“调度员”,它调用UserDAO查数据,再根据业务逻辑(比如密码加密校验、学号格式检查)做判断,最后把结果打包给Servlet。这种拆分让修改成本降到最低:如果学校要求密码从明文改成BCrypt加密,你只改UserService里的checkPassword()方法,DAO和Servlet一行不动。

提示:注意pom.xml里 war 的声明。这意味着Maven构建时,会自动把src编译后的class、WEB-INF/web.xml、WebContent下的JSP和静态资源,按WAR包标准结构打包。你不用手动复制class文件到WEB-INF/classes,Maven clean package一条命令搞定。

2.2 请求流转全景图:一次登录背后的七步心跳

以用户输入账号密码点击登录为例,真实流程比教科书写的更“啰嗦”,而这恰恰是理解Web本质的关键:

  1. 浏览器发起POST请求:Login.jsp表单action=”/LoginServlet”,method=”post”,数据以application/x-www-form-urlencoded编码发往Tomcat。
  2. Tomcat路由匹配:根据web.xml中 的url-pattern,识别出/LoginServlet对应LoginServlet类。
  3. Servlet容器实例化并调用doPost():Tomcat创建LoginServlet实例(单例),执行其doPost(HttpServletRequest req, HttpServletResponse resp)方法。
  4. Controller提取参数并委托Service:LoginServlet从req.getParameter(“username”)拿到账号,调用UserService.login(username, password)。
  5. Service协调DAO执行查询:UserService内部new UserDAO(),调用dao.findByUsername(username),生成PreparedStatement:”SELECT * FROM student WHERE stu_no = ?”,执行查询。
  6. DAO封装结果并返回:UserDAO将ResultSet映射为Student对象,返回给Service;Service校验密码,成功则返回Student对象,失败返回null。
  7. Controller决定响应策略:LoginServlet收到非null Student,调用req.getSession().setAttribute(“user”, student),然后resp.sendRedirect(“index.jsp”);若失败,则req.setAttribute(“message”, “账号或密码错误”),再req.getRequestDispatcher(“Message.jsp”).forward(req, resp)。

看到没?forward和sendRedirect的区别不是语法差异,而是HTTP协议层面的生死线。forward是服务器内部跳转,request对象全程复用,所以Message.jsp能直接读到req.getAttribute(“message”);而sendRedirect是告诉浏览器“你去访问另一个URL”,浏览器发起新请求,原request彻底丢失——这就是为什么登录成功后必须用session存user,而不是request。这个项目里所有跳转都严格遵循此原则,没有一处用错。

2.3 数据库设计的现实妥协:三张表如何撑起选课业务

init_db.sql里的建表语句,表面看是基础SQL,实则藏着教学设计的精妙取舍:

-- 学生表:主键stu_no用VARCHAR(12),而非INT CREATE TABLE student ( stu_no VARCHAR(12) PRIMARY KEY COMMENT '学号,如20210001', name VARCHAR(20) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 'BCrypt加密后的密码', major VARCHAR(30) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 课程表:course_id用INT自增,但course_code用VARCHAR(10) CREATE TABLE course ( course_id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(10) UNIQUE NOT NULL COMMENT '课程代码,如CS101', title VARCHAR(50) NOT NULL, credit TINYINT NOT NULL DEFAULT 2 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 选课中间表:联合主键+外键约束 CREATE TABLE select_course ( stu_no VARCHAR(12) NOT NULL, course_id INT NOT NULL, PRIMARY KEY (stu_no, course_id), FOREIGN KEY (stu_no) REFERENCES student(stu_no) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE RESTRICT ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

为什么学生表主键不用INT?因为高校学号是字符串(20210001),强行转INT会丢失前导零,且INT范围(21亿)远超高校学生数,没必要。为什么课程表要course_code和course_id两个ID?course_id是数据库内关联用的整数主键,高效;course_code是业务系统识别用的字符串编码,人眼友好。最关键的是select_course表的外键设计:ON DELETE CASCADE意味着删学生时自动清空其所有选课记录,避免孤儿数据;而ON DELETE RESTRICT意味着删课程前必须先清空选课表,防止课程被删后选课记录指向不存在的course_id。这种“一边松一边紧”的约束,正是真实业务场景的缩影——学生毕业离校是常态,课程停开是例外。

3. 核心模块详解与实操要点:手把手带你抠懂每一行关键代码

3.1 登录模块:Session管理与安全边界的建立

LoginServlet.java是整个系统的入口守门员,它的代码量不大,但每行都是安全红线:

public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 获取参数(注意:getParameter()对中文需设置req.setCharacterEncoding("UTF-8")) String username = req.getParameter("username"); String password = req.getParameter("password"); // 2. 调用Service层验证(此处省略密码加密逻辑,实际应调用BCrypt.checkpw) UserService service = new UserService(); Student user = service.login(username, password); if (user != null) { // 3. 成功:存入Session,注意key名统一用"user",所有页面都认这个 HttpSession session = req.getSession(true); // true表示不存在则创建 session.setAttribute("user", user); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作自动失效 // 4. 重定向到首页,避免F5刷新重复提交 resp.sendRedirect("index.jsp"); } else { // 5. 失败:设置错误信息到request,forward到提示页 req.setAttribute("message", "用户名或密码错误,请重试!"); req.getRequestDispatcher("Message.jsp").forward(req, resp); } } }

实操要点
-字符编码陷阱:如果Login.jsp表单提交中文用户名出现乱码,在LoginServlet开头必须加req.setCharacterEncoding("UTF-8")。这是因为Tomcat 8.5+默认用ISO-8859-1解码POST数据,而JSP页面meta声明UTF-8,两边不一致必然乱码。这个坑90%的新手都踩过。
-Session生命周期控制setMaxInactiveInterval(1800)不是可有可无的装饰。不设的话,Tomcat默认30分钟过期,但用户可能正在填选课表单,突然Session失效,提交后跳回登录页,所有已填数据丢失。设成30分钟是平衡安全与体验的折中——既防长期空闲会话被劫持,又保操作连续性。
-forward vs redirect的生死抉择:失败时用forward(),因为Message.jsp需要读取req.getAttribute("message");成功时用sendRedirect(),因为index.jsp需要读取session.getAttribute("user")。如果登录成功也用forward,会导致浏览器地址栏仍是/LoginServlet,用户刷新时会重新提交表单,触发二次登录逻辑(虽然不影响功能,但用户体验极差)。

3.2 选课核心逻辑:事务边界与并发安全的朴素实现

SaveServlet.java处理选课提交,是业务复杂度最高的控制器。它必须保证“学生选课”是一个原子操作:要么全部成功(插入select_course记录+更新课程余量),要么全部失败(不插入记录,余量不变)。项目采用最直白的JDBC事务控制:

public class SaveServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String stuNo = (String) req.getSession().getAttribute("user"); String courseIdStr = req.getParameter("courseId"); Connection conn = null; PreparedStatement psInsert = null; PreparedStatement psUpdate = null; try { conn = JDBCUtil.getConnection(); // 工具类获取连接 conn.setAutoCommit(false); // 关键!关闭自动提交,开启事务 // 步骤1:插入选课记录 String sqlInsert = "INSERT INTO select_course(stu_no, course_id) VALUES(?, ?)"; psInsert = conn.prepareStatement(sqlInsert); psInsert.setString(1, stuNo); psInsert.setInt(2, Integer.parseInt(courseIdStr)); psInsert.executeUpdate(); // 步骤2:更新课程余量(假设course表有remain字段) String sqlUpdate = "UPDATE course SET remain = remain - 1 WHERE course_id = ?"; psUpdate = conn.prepareStatement(sqlUpdate); psUpdate.setInt(1, Integer.parseInt(courseIdStr)); psUpdate.executeUpdate(); conn.commit(); // 全部成功,提交事务 req.setAttribute("message", "选课成功!"); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 任一失败,回滚所有操作 } catch (SQLException ex) { ex.printStackTrace(); } } req.setAttribute("message", "选课失败:" + e.getMessage()); } finally { // 关闭资源(此处省略JDBCUtil.close()调用) JDBCUtil.close(psInsert, psUpdate, conn); } req.getRequestDispatcher("Message.jsp").forward(req, resp); } }

为什么不用Spring声明式事务?因为这是教学项目,必须让学生看到事务的“肌肉”在哪里。conn.setAutoCommit(false)是开关,conn.commit()是确认键,conn.rollback()是紧急刹车。如果去掉事务控制,可能出现:插入选课记录成功,但更新余量时因课程已满(remain=0)失败,导致数据库出现“学生已选课但课程余量没减”的数据不一致。这个代码片段里,JDBCUtil.getConnection()封装了驱动加载、URL、用户名密码,避免在每个DAO里硬编码,是初学者最容易忽略的“解耦第一步”。

3.3 JSP页面协作机制:如何让多个JSP共享同一套导航与样式

项目中的Top.jsp、Message.jsp不是孤立页面,而是通过JSP标准动作<jsp:include>组装成完整视图:

index.jsp主体结构

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head><title>学生首页</title></head> <body> <!-- 动态包含顶部导航 --> <jsp:include page="Top.jsp"/> <!-- 主体内容 --> <div class="main-content"> <h2>欢迎回来,${sessionScope.user.name}!</h2> <p>您当前已选 ${fn:length(sessionScope.courses)} 门课程。</p> <!-- 选课表单 --> <form action="SaveServlet" method="post"> <select name="courseId"> <c:forEach items="${requestScope.courses}" var="c"> <option value="${c.course_id}">${c.title} (${c.course_code})</option> </c:forEach> </select> <input type="submit" value="提交选课"/> </form> </div> <!-- 底部版权信息(可另建Bottom.jsp) --> <jsp:include page="Bottom.jsp"/> </body> </html>

Top.jsp内容

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <div class="top-nav"> <a href="index.jsp">首页</a> | <a href="SelectListServlet">我的选课</a> | <a href="Logout.jsp">退出登录</a> <span style="float:right;">欢迎 ${sessionScope.user.name}</span> </div>

关键技巧
-<jsp:include>运行时包含,每次请求index.jsp都会重新执行Top.jsp,所以${sessionScope.user.name}能实时显示当前登录用户。这和<%@ include file="Top.jsp"%>(编译时包含)有本质区别——后者在JSP编译成Servlet时就把Top.jsp代码拷贝进来了,无法动态读取session。
- EL表达式${sessionScope.user.name}<%=session.getAttribute("user")!=null?((Student)session.getAttribute("user")).getName():""%>简洁十倍,且自动处理null安全。但要注意:JSP默认不启用EL,需在page指令加isELIgnored="false"(Tomcat 7+默认true,通常不用显式写)。
-fn:length()是JSTL函数库的长度计算,用于遍历课程列表前判断是否为空,避免空指针。使用前需在JSP顶部声明<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>,并在pom.xml中添加jstl依赖。

4. Tomcat部署全流程与环境适配:从零开始到页面弹出的每一步

4.1 环境准备清单:版本兼容性不是玄学,是血泪教训

这个项目明确支持Tomcat 7/8/9,但不同版本对Servlet/JSP规范的支持有细微差别,必须精准匹配:

组件推荐版本必须规避的坑
JDKJDK 8u202 或 JDK 11Tomcat 7必须用JDK 8,Tomcat 9.0.x最低要求JDK 8,9.1.x起支持JDK 17,但项目未测试。用JDK 17跑Tomcat 7会报UnsupportedClassVersionError
TomcatTomcat 8.5.94(稳定版)Tomcat 10+全面转向Jakarta EE命名空间(jakarta.servlet.*替代javax.servlet.*),项目中所有import javax.servlet.*的代码会编译失败。必须用Tomcat 9及以下。
MySQLMySQL 5.7 或 8.0MySQL 8.0默认认证插件改为caching_sha2_password,老JDBC驱动不兼容。解决方案:安装时选“Use Legacy Authentication Method”,或在jdbc.url末尾加?serverTimezone=UTC&useSSL=false

实操步骤
1.下载解压Tomcat:去archive.apache.org/dist/tomcat/找Tomcat 8.5.94,解压到无中文、无空格路径(如D:\tomcat8)。
2.配置JDK环境变量JAVA_HOME=D:\jdk8PATH追加%JAVA_HOME%\bin,命令行输入java -version确认输出1.8.0_202
3.启动Tomcat验证:双击D:\tomcat8\bin\startup.bat,浏览器访问http://localhost:8080,看到Tomcat欢迎页即成功。

4.2 项目导入与编译:Maven不是银弹,手动编译才是根基

虽然有pom.xml,但教学项目必须掌握手动编译,因为这是理解classpath的第一课:

方案一:Maven一键构建(推荐新手)

# 进入项目根目录(含pom.xml处) mvn clean package # 生成target/student-course.war # 将war包复制到D:\tomcat8\webapps\下 # Tomcat自动解压部署,访问http://localhost:8080/student-course/Login.jsp

方案二:手动编译(理解原理必做)

# 1. 创建编译输出目录 mkdir D:\project\build mkdir D:\project\classes # 2. 编译Java文件(需指定classpath包含servlet-api.jar) javac -encoding UTF-8 -d D:\project\classes \ -cp "D:\tomcat8\lib\servlet-api.jar;D:\project\lib\mysql-connector-java-5.1.47.jar" \ src/com/edu/servlet/*.java \ src/com/edu/dao/*.java \ src/com/edu/service/*.java \ src/com/edu/entity/*.java # 3. 复制编译后的class到WEB-INF/classes xcopy /E /I D:\project\classes D:\project\WebContent\WEB-INF\classes # 4. 复制依赖jar到WEB-INF/lib copy D:\project\lib\mysql-connector-java-5.1.47.jar D:\project\WebContent\WEB-INF\lib\

关键点-cp参数里的servlet-api.jar是Tomcat提供的,绝不能打包进WAR,否则会和Tomcat自带的冲突。mysql-connector-java.jar则必须放lib里,因为它是应用依赖,Tomcat不提供。

4.3 数据库初始化:init_db.sql执行的三个致命细节

执行init_db.sql不是简单复制粘贴,有三个细节决定成败:

  1. 字符集必须显式指定:MySQL客户端默认字符集可能不是utf8mb4,导致中文建表注释乱码。执行前先运行:
    sql SET NAMES utf8mb4; CREATE DATABASE IF NOT EXISTS student_course DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_course;
  2. 外键检查临时关闭:如果数据库已有数据,建表时可能因外键约束失败。安全做法是:
    sql SET FOREIGN_KEY_CHECKS = 0; -- 关闭外键检查 -- 执行所有CREATE TABLE语句 SET FOREIGN_KEY_CHECKS = 1; -- 重新开启
  3. 初始数据插入要避开主键冲突:init_db.sql末尾通常有INSERT语句填充示例数据。如果表已存在,INSERT会因主键重复报错。解决方案:在INSERT前加TRUNCATE TABLE student;清空,或用INSERT IGNORE忽略重复。

执行完成后,用MySQL Workbench连接,检查三张表结构是否完全匹配init_db.sql,特别核对外键箭头是否正确指向student和course表。

5. 常见问题与排查技巧实录:那些让你抓狂半小时的“低级错误”

5.1 HTTP 404错误:不是代码错了,是路径没对齐

现象:浏览器访问http://localhost:8080/Login.jsp显示404,但http://localhost:8080/能打开Tomcat首页。

排查链路
- 第一步:确认Login.jsp物理位置。它必须在D:\tomcat8\webapps\ROOT\Login.jsp(如果部署在ROOT)或D:\tomcat8\webapps\student-course\Login.jsp(如果部署为student-course应用)。常见错误是把项目文件夹整个拖进webapps,导致路径变成webapps\student-course-master\Login.jsp,多了一层目录。
- 第二步:检查web.xml的servlet-mapping。如果LoginServlet映射为<url-pattern>/login</url-pattern>,那么浏览器必须访问/login,而不是/LoginServlet。URL路径和xml配置必须字节级一致。
- 第三步:查看Tomcat日志。启动时logs/catalina.out会打印部署日志:“Deployment of web application archive [D:\tomcat8\webapps\student-course.war] has finished in [X] ms”。如果没有这行,说明WAR包根本没被识别,可能是文件损坏或名字含非法字符(如空格)。

实操心得:在Tomcat启动后,立刻打开D:\tomcat8\webapps\目录,看是否有student-course文件夹(解压后的WAR)或student-course.war文件。没有?说明部署失败。有文件夹但没内容?说明WAR包损坏,重新生成。

5.2 中文乱码:字符集战争的三个战场

现象:Login.jsp输入中文用户名,后台打印出来是????;或者数据库存入中文,查询出来是??

三战场定位法
-浏览器到Tomcat(GET请求):URL参数乱码。解决方案:在LoginServlet开头加req.setCharacterEncoding("UTF-8"),但这对GET无效(GET参数由Tomcat在解析URL时解码)。必须在D:\tomcat8\conf\server.xml的Connector节点加URIEncoding="UTF-8"
xml <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" /> <!-- 加这一行 -->
-Tomcat到JSP(响应输出):JSP页面显示乱码。解决方案:JSP页面第一行必须是<%@ page contentType="text/html;charset=UTF-8" %>,且HTML meta标签<meta charset="UTF-8">
-Java到MySQL(数据库交互):JDBC连接乱码。解决方案:jdbc.url必须包含?characterEncoding=utf8mb4&serverTimezone=UTC,例如:
java String url = "jdbc:mysql://localhost:3306/student_course?characterEncoding=utf8mb4&serverTimezone=UTC";

终极验证:在LoginServlet里加一行System.out.println("用户名:" + username);,看控制台输出是否正常。如果控制台正常但页面乱码,问题在响应编码;如果控制台就是????,问题在请求解码或JDBC连接。

5.3 Session失效:用户登着登着就回到登录页

现象:用户登录后浏览几个页面,突然跳转到Login.jsp,F12看Network里请求的Cookie没有JSESSIONID。

根因分析
-Cookie路径不匹配:Tomcat默认为应用上下文路径设置Cookie Path。如果部署在ROOT,Cookie Path是/;如果部署在student-course,Path是/student-course。浏览器访问/student-course/index.jsp时发送Cookie,但访问/Login.jsp(ROOT路径)时不发送,导致Session丢失。解决方案:统一部署在ROOT,或确保所有链接都带上下文路径(<a href="${pageContext.request.contextPath}/index.jsp">)。
-浏览器隐私模式:某些浏览器隐私模式会阻止第三方Cookie,而跨域请求时可能触发。解决方案:换普通窗口测试。
-Session超时时间过短:web.xml中可全局配置:
xml <session-config> <session-timeout>60</session-timeout> <!-- 单位:分钟 --> </session-config>

快速诊断:在任意JSP页面加<%=session.getId()%>,刷新页面看ID是否变化。如果变化,说明Session重建了,问题出在Cookie传递环节。

5.4 数据库连接失败:ClassNotFoundException与CommunicationsException

现象:启动Tomcat时报java.lang.ClassNotFoundException: com.mysql.jdbc.DriverCommunications link failure

分层排查
-ClassNotFoundException:JDBC驱动类找不到。原因:mysql-connector-java.jar没放到WEB-INF/lib,或jar包版本太新(MySQL 8+驱动类名变为com.mysql.cj.jdbc.Driver)。解决方案:下载mysql-connector-java-5.1.47.jar(兼容MySQL 5.7/8.0),放入lib目录。
-Communications link failure:网络连不通。原因:MySQL服务没启动、端口被占用、防火墙拦截、JDBC URL写错。解决方案:
1. 命令行执行netstat -ano | findstr :3306,确认MySQL进程在运行;
2. 检查JDBC URL是否为jdbc:mysql://localhost:3306/student_course(不是127.0.0.1,某些MySQL配置拒绝localhost解析);
3. 在MySQL命令行执行SHOW VARIABLES LIKE 'bind_address';,如果是127.0.0.1,则URL必须用127.0.0.1,不能用localhost

终极技巧:在JDBCUtil.java的getConnection()方法里,加一行System.out.println("Connecting to: " + url);,确认程序读取的URL和你预期的一致。有时候IDE缓存了旧配置,重启IDE才能生效。

6. 教学延伸与能力跃迁:从跑通项目到独立开发的三阶跃迁

这个项目的价值,绝不仅限于交一份课程设计报告。它是一块跳板,帮你完成从“照着抄”到“自己造”的三级跃迁:

第一阶:解剖式学习(1-2天)
目标:搞懂每一行代码的“为什么”。建议你打开IDEA,把项目导入后,对LoginServlet打三个断点:req.getParameter()后、service.login()后、session.setAttribute()后。启动Debug模式,观察Variables窗口里username、user、session的值如何变化。重点跟踪JDBCUtil.getConnection()DriverManager.getConnection(url, user, pwd)的执行过程,看Connection对象怎么从DriverManager工厂里诞生。这阶段不做任何修改,只问一个问题:“如果我把session.setAttribute("user", user)改成req.setAttribute("user", user),会发生什么?”然后动手试。

第二阶:缝合式改造(3-5天)
目标:把项目模块像乐高一样拆开重组。例如:
- 把JSP页面全部换成Bootstrap 5样式,替换Top.jsp为响应式导航栏;
- 在选课成功后,不再跳Message.jsp,而是用AJAX异步提交,返回JSON{success:true, message:"选课成功"},前端用JavaScript动态更新页面;
- 给学生表增加email字段,在Login.jsp加邮箱登录选项,修改UserService.login()支持邮箱或学号登录。
这个阶段的关键是“小步快跑”:每次只改一个点,改完立刻测试,确保破坏可控。

第三阶:架构式升级(1周+)
目标:用现代技术栈重构核心模块,理解演进逻辑。例如:
- 用Spring MVC替换原生Servlet:把LoginServlet改成@Controller类,@RequestMapping(“/login”),用@RequestParam接收参数,ModelAndView返回视图;
- 用MyBatis替换JDBC:编写StudentMapper.xml,用<select id="findByStuNo" resultType="Student">SELECT * FROM student WHERE stu_no = #{stuNo}</select>
- 用Thymeleaf替换JSP:把index.jsp重写为index.html,用<span th:text="${session.user.name}">Name</span>
这时你会发现,原来LoginServlet里繁琐的req.getParameter()session.setAttribute()resp.sendRedirect(),在Spring里变成一行return "redirect:/index";原来JDBC里手动管理Connection、PreparedStatement、ResultSet的模板代码,在MyBatis里消失不见。这种对比,比任何教程都深刻——它告诉你,框架不是魔法,而是对重复劳动的抽象封装。

最后分享一个真实案例:去年有个学生,在这个项目基础上增加了“课程评价”功能。他没急着写代码,而是先画了四张ER图:评价表(id, stu_no, course_id, score, comment, create_time)、评分统计视图(course_id, avg_score, total_count)。然后对照init_db.sql,写出CREATE TABLE语句,再补全DAO、Service、Servlet。答辩时老师问他“评价表为什么要加create_time索引”,他脱口而出:“因为按时间倒序查最新评价是高频操作,索引能避免全表扫描。”——那一刻我知道,他已经把知识变成了肌肉记忆。这个项目真正的终点,不是页面弹出“选课成功”,而是你关掉IDE,脑子里还回响着那句conn.commit()的回声。

本文还有配套的精品资源,点击获取

简介:直接可运行的学生选课Web应用,用Servlet做控制器、JSP渲染页面、MySQL存数据,严格按MVC分层组织。前端有登录页Login.jsp、首页index.jsp、顶部导航Top.jsp、提示页Message.jsp等;后端包含SaveServlet等核心处理类,src下按com.xxx包结构划分业务逻辑,WEB-INF里配好web.xml,还附带init_db.sql一键建库建表(含学生、课程、选课三张关联表)。项目自带pom.xml支持Maven构建,build和classes目录已预设编译路径,扔进Tomcat 7/8/9就能跑。README.md写清楚了启动步骤、账号密码示例和功能入口,适合课程设计快速上手,也方便对照学习请求流转、会话管理、表单提交、重定向跳转、数据库CRUD及外键约束实现。


本文还有配套的精品资源,点击获取