本文还有配套的精品资源点击获取简介直接可运行的学生选课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里能不能直接写SQLSession存什么、什么时候清这些细节教科书不讲百度搜出来全是碎片最后只能抄个半成品改来改去连自己改了哪都不清楚。这个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三张表的ENGINEInnoDB、CHARSETutf8mb4、外键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.xmlTomcat会直接返回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/setterUserDAO.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/classesMaven clean package一条命令搞定。2.2 请求流转全景图一次登录背后的七步心跳以用户输入账号密码点击登录为例真实流程比教科书写的更“啰嗦”而这恰恰是理解Web本质的关键浏览器发起POST请求Login.jsp表单action”/LoginServlet”method”post”数据以application/x-www-form-urlencoded编码发往Tomcat。Tomcat路由匹配根据web.xml中 的url-pattern识别出/LoginServlet对应LoginServlet类。Servlet容器实例化并调用doPost()Tomcat创建LoginServlet实例单例执行其doPost(HttpServletRequest req, HttpServletResponse resp)方法。Controller提取参数并委托ServiceLoginServlet从req.getParameter(“username”)拿到账号调用UserService.login(username, password)。Service协调DAO执行查询UserService内部new UserDAO()调用dao.findByUsername(username)生成PreparedStatement”SELECT * FROM student WHERE stu_no ?”执行查询。DAO封装结果并返回UserDAO将ResultSet映射为Student对象返回给ServiceService校验密码成功则返回Student对象失败返回null。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课中间表联合主键外键约束 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么学生表主键不用INT因为高校学号是字符串20210001强行转INT会丢失前导零且INT范围21亿远超高校学生数没必要。为什么课程表要course_code和course_id两个IDcourse_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. 失败设置错误信息到requestforward到提示页 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()是紧急刹车。如果去掉事务控制可能出现插入选课记录成功但更新余量时因课程已满remain0失败导致数据库出现“学生已选课但课程余量没减”的数据不一致。这个代码片段里JDBCUtil.getConnection()封装了驱动加载、URL、用户名密码避免在每个DAO里硬编码是初学者最容易忽略的“解耦第一步”。3.3 JSP页面协作机制如何让多个JSP共享同一套导航与样式项目中的Top.jsp、Message.jsp不是孤立页面而是通过JSP标准动作jsp:include组装成完整视图index.jsp主体结构% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle学生首页/title/head body !-- 动态包含顶部导航 -- jsp:include pageTop.jsp/ !-- 主体内容 -- div classmain-content h2欢迎回来${sessionScope.user.name}/h2 p您当前已选 ${fn:length(sessionScope.courses)} 门课程。/p !-- 选课表单 -- form actionSaveServlet methodpost select namecourseId c:forEach items${requestScope.courses} varc option value${c.course_id}${c.title} (${c.course_code})/option /c:forEach /select input typesubmit value提交选课/ /form /div !-- 底部版权信息可另建Bottom.jsp -- jsp:include pageBottom.jsp/ /body /htmlTop.jsp内容% page contentTypetext/html;charsetUTF-8 languagejava % div classtop-nav a hrefindex.jsp首页/a | a hrefSelectListServlet我的选课/a | a hrefLogout.jsp退出登录/a span stylefloat:right;欢迎 ${sessionScope.user.name}/span /div关键技巧-jsp:include是运行时包含每次请求index.jsp都会重新执行Top.jsp所以${sessionScope.user.name}能实时显示当前登录用户。这和% include fileTop.jsp%编译时包含有本质区别——后者在JSP编译成Servlet时就把Top.jsp代码拷贝进来了无法动态读取session。- EL表达式${sessionScope.user.name}比%session.getAttribute(user)!null?((Student)session.getAttribute(user)).getName():%简洁十倍且自动处理null安全。但要注意JSP默认不启用EL需在page指令加isELIgnoredfalseTomcat 7默认true通常不用显式写。-fn:length()是JSTL函数库的长度计算用于遍历课程列表前判断是否为空避免空指针。使用前需在JSP顶部声明% taglib prefixfn urihttp://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 8Tomcat 9.0.x最低要求JDK 89.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末尾加?serverTimezoneUTCuseSSLfalse。实操步骤1.下载解压Tomcat去archive.apache.org/dist/tomcat/找Tomcat 8.5.94解压到无中文、无空格路径如D:\tomcat8。2.配置JDK环境变量JAVA_HOMED:\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不是简单复制粘贴有三个细节决定成败字符集必须显式指定MySQL客户端默认字符集可能不是utf8mb4导致中文建表注释乱码。执行前先运行sql SET NAMES utf8mb4; CREATE DATABASE IF NOT EXISTS student_course DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_course;外键检查临时关闭如果数据库已有数据建表时可能因外键约束失败。安全做法是sql SET FOREIGN_KEY_CHECKS 0; -- 关闭外键检查 -- 执行所有CREATE TABLE语句 SET FOREIGN_KEY_CHECKS 1; -- 重新开启初始数据插入要避开主键冲突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输入中文用户名后台打印出来是????或者数据库存入中文查询出来是??。三战场定位法-浏览器到TomcatGET请求URL参数乱码。解决方案在LoginServlet开头加req.setCharacterEncoding(UTF-8)但这对GET无效GET参数由Tomcat在解析URL时解码。必须在D:\tomcat8\conf\server.xml的Connector节点加URIEncodingUTF-8xml Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 / !-- 加这一行 ---Tomcat到JSP响应输出JSP页面显示乱码。解决方案JSP页面第一行必须是% page contentTypetext/html;charsetUTF-8 %且HTML meta标签meta charsetUTF-8。-Java到MySQL数据库交互JDBC连接乱码。解决方案jdbc.url必须包含?characterEncodingutf8mb4serverTimezoneUTC例如java String url jdbc:mysql://localhost:3306/student_course?characterEncodingutf8mb4serverTimezoneUTC;终极验证在LoginServlet里加一行System.out.println(用户名 username);看控制台输出是否正常。如果控制台正常但页面乱码问题在响应编码如果控制台就是????问题在请求解码或JDBC连接。5.3 Session失效用户登着登着就回到登录页现象用户登录后浏览几个页面突然跳转到Login.jspF12看Network里请求的Cookie没有JSESSIONID。根因分析-Cookie路径不匹配Tomcat默认为应用上下文路径设置Cookie Path。如果部署在ROOTCookie Path是/如果部署在student-coursePath是/student-course。浏览器访问/student-course/index.jsp时发送Cookie但访问/Login.jspROOT路径时不发送导致Session丢失。解决方案统一部署在ROOT或确保所有链接都带上下文路径a href${pageContext.request.contextPath}/index.jsp。-浏览器隐私模式某些浏览器隐私模式会阻止第三方Cookie而跨域请求时可能触发。解决方案换普通窗口测试。-Session超时时间过短web.xml中可全局配置xml session-config session-timeout60/session-timeout !-- 单位分钟 -- /session-config快速诊断在任意JSP页面加%session.getId()%刷新页面看ID是否变化。如果变化说明Session重建了问题出在Cookie传递环节。5.4 数据库连接失败ClassNotFoundException与CommunicationsException现象启动Tomcat时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或Communications link failure。分层排查-ClassNotFoundExceptionJDBC驱动类找不到。原因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 idfindByStuNo resultTypeStudentSELECT * 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及外键约束实现。本文还有配套的精品资源点击获取
Java Web版学生选课系统源码包:含完整MVC结构、MySQL建表脚本与Tomcat部署说明
发布时间:2026/7/8 18:22:15
本文还有配套的精品资源点击获取简介直接可运行的学生选课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里能不能直接写SQLSession存什么、什么时候清这些细节教科书不讲百度搜出来全是碎片最后只能抄个半成品改来改去连自己改了哪都不清楚。这个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三张表的ENGINEInnoDB、CHARSETutf8mb4、外键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.xmlTomcat会直接返回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/setterUserDAO.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/classesMaven clean package一条命令搞定。2.2 请求流转全景图一次登录背后的七步心跳以用户输入账号密码点击登录为例真实流程比教科书写的更“啰嗦”而这恰恰是理解Web本质的关键浏览器发起POST请求Login.jsp表单action”/LoginServlet”method”post”数据以application/x-www-form-urlencoded编码发往Tomcat。Tomcat路由匹配根据web.xml中 的url-pattern识别出/LoginServlet对应LoginServlet类。Servlet容器实例化并调用doPost()Tomcat创建LoginServlet实例单例执行其doPost(HttpServletRequest req, HttpServletResponse resp)方法。Controller提取参数并委托ServiceLoginServlet从req.getParameter(“username”)拿到账号调用UserService.login(username, password)。Service协调DAO执行查询UserService内部new UserDAO()调用dao.findByUsername(username)生成PreparedStatement”SELECT * FROM student WHERE stu_no ?”执行查询。DAO封装结果并返回UserDAO将ResultSet映射为Student对象返回给ServiceService校验密码成功则返回Student对象失败返回null。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课中间表联合主键外键约束 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么学生表主键不用INT因为高校学号是字符串20210001强行转INT会丢失前导零且INT范围21亿远超高校学生数没必要。为什么课程表要course_code和course_id两个IDcourse_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. 失败设置错误信息到requestforward到提示页 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()是紧急刹车。如果去掉事务控制可能出现插入选课记录成功但更新余量时因课程已满remain0失败导致数据库出现“学生已选课但课程余量没减”的数据不一致。这个代码片段里JDBCUtil.getConnection()封装了驱动加载、URL、用户名密码避免在每个DAO里硬编码是初学者最容易忽略的“解耦第一步”。3.3 JSP页面协作机制如何让多个JSP共享同一套导航与样式项目中的Top.jsp、Message.jsp不是孤立页面而是通过JSP标准动作jsp:include组装成完整视图index.jsp主体结构% page contentTypetext/html;charsetUTF-8 languagejava % html headtitle学生首页/title/head body !-- 动态包含顶部导航 -- jsp:include pageTop.jsp/ !-- 主体内容 -- div classmain-content h2欢迎回来${sessionScope.user.name}/h2 p您当前已选 ${fn:length(sessionScope.courses)} 门课程。/p !-- 选课表单 -- form actionSaveServlet methodpost select namecourseId c:forEach items${requestScope.courses} varc option value${c.course_id}${c.title} (${c.course_code})/option /c:forEach /select input typesubmit value提交选课/ /form /div !-- 底部版权信息可另建Bottom.jsp -- jsp:include pageBottom.jsp/ /body /htmlTop.jsp内容% page contentTypetext/html;charsetUTF-8 languagejava % div classtop-nav a hrefindex.jsp首页/a | a hrefSelectListServlet我的选课/a | a hrefLogout.jsp退出登录/a span stylefloat:right;欢迎 ${sessionScope.user.name}/span /div关键技巧-jsp:include是运行时包含每次请求index.jsp都会重新执行Top.jsp所以${sessionScope.user.name}能实时显示当前登录用户。这和% include fileTop.jsp%编译时包含有本质区别——后者在JSP编译成Servlet时就把Top.jsp代码拷贝进来了无法动态读取session。- EL表达式${sessionScope.user.name}比%session.getAttribute(user)!null?((Student)session.getAttribute(user)).getName():%简洁十倍且自动处理null安全。但要注意JSP默认不启用EL需在page指令加isELIgnoredfalseTomcat 7默认true通常不用显式写。-fn:length()是JSTL函数库的长度计算用于遍历课程列表前判断是否为空避免空指针。使用前需在JSP顶部声明% taglib prefixfn urihttp://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 8Tomcat 9.0.x最低要求JDK 89.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末尾加?serverTimezoneUTCuseSSLfalse。实操步骤1.下载解压Tomcat去archive.apache.org/dist/tomcat/找Tomcat 8.5.94解压到无中文、无空格路径如D:\tomcat8。2.配置JDK环境变量JAVA_HOMED:\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不是简单复制粘贴有三个细节决定成败字符集必须显式指定MySQL客户端默认字符集可能不是utf8mb4导致中文建表注释乱码。执行前先运行sql SET NAMES utf8mb4; CREATE DATABASE IF NOT EXISTS student_course DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_course;外键检查临时关闭如果数据库已有数据建表时可能因外键约束失败。安全做法是sql SET FOREIGN_KEY_CHECKS 0; -- 关闭外键检查 -- 执行所有CREATE TABLE语句 SET FOREIGN_KEY_CHECKS 1; -- 重新开启初始数据插入要避开主键冲突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输入中文用户名后台打印出来是????或者数据库存入中文查询出来是??。三战场定位法-浏览器到TomcatGET请求URL参数乱码。解决方案在LoginServlet开头加req.setCharacterEncoding(UTF-8)但这对GET无效GET参数由Tomcat在解析URL时解码。必须在D:\tomcat8\conf\server.xml的Connector节点加URIEncodingUTF-8xml Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 / !-- 加这一行 ---Tomcat到JSP响应输出JSP页面显示乱码。解决方案JSP页面第一行必须是% page contentTypetext/html;charsetUTF-8 %且HTML meta标签meta charsetUTF-8。-Java到MySQL数据库交互JDBC连接乱码。解决方案jdbc.url必须包含?characterEncodingutf8mb4serverTimezoneUTC例如java String url jdbc:mysql://localhost:3306/student_course?characterEncodingutf8mb4serverTimezoneUTC;终极验证在LoginServlet里加一行System.out.println(用户名 username);看控制台输出是否正常。如果控制台正常但页面乱码问题在响应编码如果控制台就是????问题在请求解码或JDBC连接。5.3 Session失效用户登着登着就回到登录页现象用户登录后浏览几个页面突然跳转到Login.jspF12看Network里请求的Cookie没有JSESSIONID。根因分析-Cookie路径不匹配Tomcat默认为应用上下文路径设置Cookie Path。如果部署在ROOTCookie Path是/如果部署在student-coursePath是/student-course。浏览器访问/student-course/index.jsp时发送Cookie但访问/Login.jspROOT路径时不发送导致Session丢失。解决方案统一部署在ROOT或确保所有链接都带上下文路径a href${pageContext.request.contextPath}/index.jsp。-浏览器隐私模式某些浏览器隐私模式会阻止第三方Cookie而跨域请求时可能触发。解决方案换普通窗口测试。-Session超时时间过短web.xml中可全局配置xml session-config session-timeout60/session-timeout !-- 单位分钟 -- /session-config快速诊断在任意JSP页面加%session.getId()%刷新页面看ID是否变化。如果变化说明Session重建了问题出在Cookie传递环节。5.4 数据库连接失败ClassNotFoundException与CommunicationsException现象启动Tomcat时报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或Communications link failure。分层排查-ClassNotFoundExceptionJDBC驱动类找不到。原因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 idfindByStuNo resultTypeStudentSELECT * 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及外键约束实现。本文还有配套的精品资源点击获取