简介面向Java Web课程设计或毕业设计场景这份学生选课管理系统基于JSPServletMySQL开发支持教师与学生双角色登录。教师端可维护学生、课程与选课信息并设置必修学分上下限学生端可在线选课、查看已选课程、修改个人资料系统还会自动统计未修满最低学分的学生功能覆盖常见教务需求。项目采用经典分层结构dao层负责数据访问、servlet处理业务逻辑、JSP负责页面展示代码注释清晰便于二次开发。压缩包共105个文件大小约108.89MB包含Java源码与编译class文件、JSP页面、SQL数据库脚本、项目配置文件、论文文档以及高清运行指导视频。目前已有4703人学习下载配套环境为MySQL5.5~5.7、JDK1.8与Tomcat8兼容常见开发工具附有导入运行教程与视频演示能帮助快速跑通项目并降低环境配置门槛。1. 学生选课管理系统一套能跑起来的 JavaWeb 课设源码意味着什么每年到了毕设季和课程设计节点总有人拿着一份“java基于jspservletmysql开发实现的学生选课管理系统源码”或者类似的标题到处找答案。这类项目最大的特点是老——JSP 页面里嵌 Java 代码、Servlet 处理请求、MySQL 存数据没有 Spring Boot 那一套自动配置但恰恰因为老它离课设评分标准最近多角色登录、CRUD 完整、部署简单、代码量适中。拿到手之后最大的疑问其实只有一个这东西到底能不能跑起来答案是可以但需要你按对的顺序配置 JDK、Tomcat、MySQL 和 IDEA 的部署结构。这篇笔记就按一个课设项目的完整落地路径来拆从数据库设计到 Servlet 路由从 IDEA 启动到 WAR 包部署把每一个可能让你卡住的点说透。2. 为什么是 JSP Servlet多角色登录背后的请求处理与路由逻辑2.1 Servlet 生命周期从请求到响应的完整路径Servlet 的本质是一个 Java 类运行在 Tomcat 这类 Web 容器里负责接收 HTTP 请求并返回响应。理解它的生命周期是你排查所有“请求打不到 Java 代码里”这类问题的前提。一个 Servlet 从诞生到销毁走的是 init、service、destroy 三个阶段。容器启动或第一次收到该 Servlet 的请求时执行 init()只执行一次之后每个请求都会进 service()由它根据 HTTP 方法分发到 doGet() 或 doPost()容器关闭或应用重载时调用 destroy()。你在 IDEA 里改完 Java 代码点重启Tomcat 就会把旧的 Servlet 实例销毁再 new 一个出来这就是为什么有些静态变量会被“神秘重置”——不是被清了是实例被换掉了。framework 选型的理由也藏在这一层。JSP 负责页面展示Servlet 负责业务路由和控制跳转MySQL 负责持久化三者靠 JDBC 和 HTTP 请求串起来。对于课设评审来说这种结构能把 MVC 三层讲得很清楚比 Spring Boot 那种注解满天飞的写法更容易答辩。你要做的是在 web.xml 里声明 Servlet 映射或者用 WebServlet 注解直接标在类上。常见做法是注解优先少写一个配置文件就少一个出错点。但要注意如果你用的 Tomcat 版本太老比如 7.0 之前注解支持不完整老老实实回 web.xml 写。2.2 Session 与过滤器多角色登录的两个关键机制多角色登录是这个项目的核心卖点。学生、教师、管理员三个角色共用同一个登录入口登录后跳转到各自的首页访问权限互不越界。实现这套逻辑依赖两个 JavaWeb 内置机制Session 和 Filter。登录成功后代码从数据库查出用户的角色字段把这个用户对象连同角色信息塞进请求的 Session 里HttpSession session request.getSession(); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { session.setAttribute(user, user); session.setAttribute(role, user.getRole()); // 1学生 2教师 3管理员 // 按角色分发到不同首页 if (1.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /student/index.jsp); } else if (2.equals(user.getRole())) { response.sendRedirect(request.getContextPath() /teacher/index.jsp); } else { response.sendRedirect(request.getContextPath() /admin/index.jsp); } } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); }这段代码里最容易被忽略的是request.getContextPath()。如果你的应用部署名是course_selectionContextPath 就是/course_selection写死成/login.jsp在本地可能没事换一台机器部署名一变就会全部 404。用getContextPath()动态拼是课设阶段就该养成的习惯。Session 只是把登录状态存下来了但要拦截“没登录的人硬闯管理员页面”就得靠过滤器。一个 AuthFilter 把除了 login.jsp 和 LoginServlet 之外的路径全部拦下来检查 Session 里有没有 user 这个属性WebFilter(/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); if (uri.endsWith(/login.jsp) || uri.contains(/LoginServlet)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }过滤器里还要做一层角色校验管理员页面的 Servlet 在执行前判断session.getAttribute(role)是否等于 3不是就抛 403 或重定向回登录页。这里有个很要命的细节——Filter 放行的顺序和注册顺序有关如果第二个 Filter 要求用户已登录而它排在 AuthFilter 前面未登录用户还没被拦截就先被第二个 Filter 处理了直接空指针。多过滤器时用WebFilter的urlPatterns把拦截范围拆细比全填/*然后靠 if 判断更可控。2.3 传统 JavaWeb 项目结构没有 Maven 时依赖怎么管理现在新项目默认 Maven 或 Gradle但这个标题代表的是传统 JavaWeb 工程依赖是手动的把 jar 包丢进WEB-INF/lib目录Tomcat 启动时会自动加载这些包。在 IDEA 里新建或导入这种项目时要确认Project Structure → Libraries里添加的依赖和WEB-INF/lib下实际存在的 jar 包是一致的。一个常见的翻车现场是Libraries 里有 mysql-connector-java但WEB-INF/lib目录下没有物理文件代码编译没问题Tomcat 一运行就报ClassNotFoundException。原因是编译期走的是 IDEA 的 Library运行期走的是 Web 应用的 lib 目录两边是两套体系。需要准备的库就这么几个mysql-connector-java 驱动包、JSTL 标签库如果你用 c:forEach 标签、Servlet APITomcat 自带不要额外引进 lib。驱动版本要和数据库版本匹配MySQL 5.7 用 5.x 驱动没问题MySQL 8.0 就要用 8.x 的驱动类名也从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver这个差异是后面避坑章节的重点。3. 数据库设计选课系统的表结构、外键与容量控制3.1 核心表设计用户表、课程表与选课记录表学生选课系统按最小可用模型拆至少需要五张表用户表、学生信息表、教师信息表、课程表、选课记录表。前面三张承载多角色登录和基本信息后面两张承载业务。先看可运行的建表 SQLMySQL 5.7 和 8.0 通用CREATE DATABASE IF NOT EXISTS course_selection DEFAULT CHARACTER SET utf8mb4; USE course_selection; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role CHAR(1) NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, real_name VARCHAR(50) NOT NULL, class_name VARCHAR(50), phone VARCHAR(20), CONSTRAINT fk_student_user FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_teacher ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, teacher_no VARCHAR(20) NOT NULL UNIQUE, real_name VARCHAR(50) NOT NULL, title VARCHAR(50), department VARCHAR(100), CONSTRAINT fk_teacher_user FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, credit DECIMAL(2,1) NOT NULL, teacher_id INT NOT NULL, capacity INT NOT NULL DEFAULT 30, selected_count INT NOT NULL DEFAULT 0, schedule VARCHAR(100), location VARCHAR(100), CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES t_teacher(id) ); CREATE TABLE t_select_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, status CHAR(1) DEFAULT 1, CONSTRAINT uk_student_course UNIQUE (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES t_course(id) );角色字段role用CHAR(1)而不是VARCHAR(20)存字符串“学生/教师/管理员”是课设项目里常见的设计。好处是校验逻辑简单login.jsp里选一次角色Servlet 里比对一次字符就能决定跳转坏处是别人看数据库时不直观。如果你想让评审老师一眼看懂可以加一个t_role表存角色名然后t_user用role_id关联但接口判断逻辑会复杂一截。我的建议是课设以可运行优先用CHAR(1)加注释就是最优解。3.2 唯一约束与选课人数上限两条 SQL 守住业务底线选课系统最重要的一条业务规则是“一个学生同一门课只能选一次”。单靠 Java 代码判断不可靠——两个请求同时到达时两边都查到记录不存在然后双双插入成功。这种情况在课设演示时很难复现但答辩老师如果问“并发怎么办”你要能答出来。解决方式是建表时那行CONSTRAINT uk_student_course UNIQUE (student_id, course_id)。这样即使 Java 层漏判数据库也会在第二条插入时抛DuplicateEntryException你的选课 Servlet 捕获这个异常后转成友好提示“你已经选过这门课了”业务底线就守住了。选课人数上限capacity和已选人数selected_count的维护要放在同一个数据库事务里执行用一条 UPDATE 语句带条件判断UPDATE t_course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity;这条 UPDATE 返回的影响行数就是选课是否成功的标志。如果返回 0说明当前没有可用的名额可能被占满也可能课程被删Java 端拿到这个结果就回滚事务并提示“选课失败名额不足”。不要写成“先 SELECT 查出 selected_count再在 Java 里判断小于 capacity 后 UPDATE”那个方案在并发下必出超卖。课程设计和真实系统在这一点上没有区别锁和原子更新是标准答案。3.3 初始化数据预置账号与演示数据怎么写源码能“亲测可用”很大程度上取决于初始化数据是否合理。一个能演示多角色登录的数据库至少要有三个账号分别对应三种角色。密码明文存储还是加密存储课设阶段为了展示方便很多人直接存明文但我建议至少用 MD5 存一下然后在代码里加注释说明生产环境要用 BCrypt。不要为了省事输明文答辩时这是会被追着问的点。INSERT INTO t_user (username, password, role) VALUES (admin, MD5(123456), 3), (teacher01, MD5(123456), 2), (student01, MD5(123456), 1); INSERT INTO t_student (user_id, student_no, real_name, class_name, phone) VALUES (3, 20240001, 张三, 计算机2301, 13800000001); INSERT INTO t_teacher (user_id, teacher_no, real_name, title, department) VALUES (2, T10001, 李老师, 副教授, 计算机学院); INSERT INTO t_course (course_no, course_name, credit, teacher_id, capacity, selected_count, schedule, location) VALUES (C001, 数据库原理, 3.0, 1, 30, 0, 周一3-4节, A-101), (C002, JavaWeb开发, 3.5, 1, 30, 0, 周三5-6节, A-203);密码用MD5(123456)写在 SQL 里登录时 Java 端把输入密码MD5后再比对就比明文高档一点。代价是你必须在 Java 里自写 MD5 工具类或者引入了 commons-codec注意 lib 目录别漏包。另外初始化数据的顺序有讲究先插t_user拿到自增 id再插关联表不然外键对不上。SQL 脚本里的 INSERT 顺序错了后面所有账号都建不起来这是很多人第一次跑脚本报错的原因。4. 在 IDEA 里跑通导入、配置与启动的最小步骤4.1 环境版本搭配JDK、Tomcat、MySQL 这样配不容易翻车先确认一组经过大量课设验证的版本组合JDK 81.8181 或更高Tomcat 8.5.xMySQL 5.7 或 8.0IDEA 2020.3 之后的版本均可。如果你用的是 JDK 17建议换成 JDK 8 再跑这个项目因为传统 JSP 项目里很多代码依赖已废弃的内部 API高版本 JDK 下 Tomcat 8.5 会报警告甚至直接起不来。MySQL 安装完以后务必将字符集设置为 utf8mb4。Windows 上修改my.ini的[mysqld]段加一行character-set-serverutf8mb4Linux 上在/etc/my.cnf同位置加改完重启服务。不设 utf8mb4后面你插入中文数据就会看到一堆问号这是数据库字符集的问题不是代码的问题排查方向别搞反。启动 MySQL 后先用命令行确认能连通mysql -uroot -p show variables like character_set_%%;看到character_set_server的值是utf8mb4再继续往下配。否则你写的建表 SQL 里那行DEFAULT CHARACTER SET utf8mb4虽然能生效但表和库之间的字符集不一致依然可能出现乱码。4.2 导入项目到 IDEA 并配置 Artifacts拿到源码或自己按上面的结构建好项目后在 IDEA 里File → Open选到项目根目录。如果项目自带.iml文件IDEA 会直接识别如果只有源码和.idea目录选Import Module让它从存在的源码建项目。传统 JavaWeb 项目在 IDEA 里的部署依赖 Artifacts它的本质是“Web 应用打包后的目录结构”定义。你要做的是File → Project Structure → Artifacts → → Web Application: ExplodedName 填course_selection:war explodedOutput directory 随便选一个 build 目录把WEB-INF目录、JSP 页面、静态资源挂到这个 Artifact 下面确认WEB-INF/lib下有 mysql-connector-java 等 jar 包course_selection/ ├── src/ │ └── main/ │ ├── java/ # Servlet、DAO、实体类 │ └── webapp/ # JSP、静态资源 │ └── WEB-INF/ │ ├── web.xml │ └── lib/ │ ├── mysql-connector-java-8.0.33.jar │ └── jstl-1.2.jar这个目录结构里最容易出问题的就是 lib 目录没有 jar 包。IDEA 的Libraries只是编译期可见运行期 Tomcat 只看WEB-INF/lib。确认方式打开Project Structure → Facets → Web在 Deployment Descriptor 里看 web.xml 路径是否指向正确的WEB-INF/web.xml然后把WEB-INF/lib加到 Artifact 的 WEB-INF 节点下。IDEA 部署时如果不显示 jar 包右键 Artifact 选Include in Project Build强制带上。4.3 数据库连接配置从 JDBC 驱动到连接池参数项目的数据库连接不建议每发一个请求就Class.forName一次再DriverManager.getConnection。课设项目用连接池更稳妥既满足性能要求又能在答辩时讲出“数据库连接池”这个词。常见做法是引入 Apache DBCP或者直接用阿里的 druid-1.2.x.jar。个人写课设我偏好 DBCP包小配置简单。在项目里建一个DBUtil.java统一管理连接import org.apache.commons.dbcp2.BasicDataSource; public class DBUtil { private static final BasicDataSource ds new BasicDataSource(); static { ds.setDriverClassName(com.mysql.cj.jdbc.Driver); ds.setUrl(jdbc:mysql://localhost:3306/course_selection ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai); ds.setUsername(root); ds.setPassword(你的密码); ds.setInitialSize(5); ds.setMaxTotal(10); ds.setMaxIdle(5); ds.setMinIdle(1); } public static Connection getConnection() throws Exception { return ds.getConnection(); } }参数说明driverClassName在 MySQL 8 下必须写com.mysql.cj.jdbc.Driver写旧的com.mysql.jdbc.Driver会报 ClassNotFoundExceptionserverTimezoneAsia/Shanghai不加的话连接时会报时区异常useSSLfalse是为了避免本地连接时 SSL 握手的告警刷屏initialSize5意味着应用一启动就建立 5 条连接能提早暴露数据库连不上的问题不用等到第一次请求才报错。maxTotal控制在 10 以内课设规模 5 个并发顶天了配得再大也只是拖慢启动。4.4 启动、登录与验证多角色怎么切Tomcat 配置完以后在运行配置里选Tomcat Server → LocalDeployment 页签里添加刚才创建的 ArtifactApplication context 填写/course_selection。这里的上下文路径很关键如果你的数据库连接 URL 写的是course_selection这里的路径也要是course_selection否则 Cookie 的 path 对不上登录状态会神秘丢失。启动成功后浏览器访问http://localhost:8080/course_selection/login.jsp能看到登录页。用预置的三组账号分别登录账号密码角色登录后跳转admin123456管理员/admin/index.jspteacher01123456教师/teacher/index.jspstudent01123456学生/student/index.jsp验证多角色是否正确就观察两个点登录后跳转的页面归属是否正确把student/index.jsp的 URL 直接粘贴到浏览器地址栏在未登录状态下是否会被过滤器弹回登录页。第二个点才是多角色权限控制的核心验证很多源码在“能登录”上没问题但在“越权访问”上形同虚设。5. 避坑日记把这套项目跑起来的 5 个高频报错5.1 Tomcat 端口被占用Address already in use现象启动 Tomcat 时控制台报Address already in use: JVM_Bind null:8080页面访问localhost:8080直接连接拒绝。原因上一个未正常关闭的 Tomcat 实例占了 8080 端口或者你电脑上还有其他 Web 服务占用了这个端口。解决先确认占用进程Windows 下执行netstat -ano | findstr 8080拿到 PID 后taskkill /f /pid PID然后把 IDEA 运行配置里 Tomcat 的HTTP port改成 8081 或 9090 都能绕开。改端口只影响本地开发部署到服务器时用 80 或 8080 即可。注意Server port8005和JMX port1099也会被占用排查时三个端口一起看。5.2 驱动类找不到ClassNotFoundException: com.mysql.jdbc.Driver现象运行项目点登录页面报 500控制台最下面写着java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因分两种。一是 lib 目录里根本没有 mysql 驱动 jar二是用 MySQL 8 配了 MySQL 5 时代的驱动类名。第二种更隐蔽6.0 之后的驱动把类改名为com.mysql.cj.jdbc.Driver老的类名只在兼容模式下短时间保留8.0 版本已经不保证可用。解决到 maven 仓库下mysql-connector-java-8.0.33.jar放进WEB-INF/lib把DBUtil.java里的driverClassName同步改成com.mysql.cj.jdbc.Driver。确认 jar 是否真的在运行期被加载可以在DBUtil静态块里加一行Class.forName(com.mysql.cj.jdbc.Driver)启动后看 Tomcat 日志有没有报错这招属于“主动验证依赖”比等请求到了才炸出来省时间。5.3 部署后 404Artifact 没挂到 Tomcat现象Tomcat 正常启动了点运行浏览器自动打开http://localhost:8080/显示 Tomcat 首页但手动访问login.jsp返回 404。原因没有把 Artifact 添加到 Deployment。Tomcat 跑起来了但跑的是空应用IDEA 里最经典的“Tomcat 绿灯了但没部署项目”。还有一种情况是 Artifact 加进去了但Application context路径写错访问时端口对、路径不对同样 404。解决Run → Edit Configurations → Deployment点加号选 ArtifactApplication context填/course_selection。配置完以后重新启动这时 Tomcat 日志里会出现一行Deployment of web application archive ... has finished in xx ms。看到这行字再访问页面不要盯着 IDEA 的浏览器按钮里的端口自己硬试。5.4 中文乱码从数据库到页面全是问号或方块现象登录页显示正常但数据库里查出来是???或 JSP 页面上显示æ±å¥½这种垃圾字符。原因字符集链路断了一环。URL 连接参数、MySQL 表字符集、JSP 页面编码、请求响应编码四处必须一致。最常见的是 JSP 页面写了charsetutf-8但数据库建表时是latin1或者DBUtil里的 URL 没有characterEncodingutf8。解决按顺序检查三处。第一DBUtil的 URL 里characterEncodingutf8是不是在第一个参数符号拼写正确第二JSP 文件首行pageEncodingUTF-8和meta charsetUTF-8对上第三LoginServlet 和 SelectCourseServlet 开头加request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)。数据库那边已经写进脏数据的把表DROP掉重新执行建表 SQL字符集对齐以后再跑初始化脚本。5.5 登录状态反复丢失Cookie Path 上下文不对现象登录成功跳转到首页点一下页面跳转或者刷新一下又回到登录页。在 IDEA 里看 Session 拿到的值是 null。原因Session 靠 Cookie 维持Cookie 的 Path 默认是应用上下文路径/course_selection。如果 Tomcat 的 Deployment 里Application context写的是/course_selection但代码里重定向用的request.getContextPath()拼接出来的 URL 被 IDEA 自动补全成了/course_selection/course_selection/...路径嵌套就会导致 Cookie 丢失。解决统一所有跳转路径。JSP 里用c:url value/student/index.jsp/生成 URLServlet 里用request.getContextPath()拼绝对路径不要再出现手写/course_selection/...这种硬编码。同时确认 Tomcat 的 Deployment 里只有一个 Artifact如果之前部署过旧版本清理 Tomcatwork目录下的缓存再重启。这件事很多源码本身就有问题拿到手先全局搜硬编码路径改完再启动。6. 部署与改造从 IDEA 到独立 Tomcat再往前一步6.1 用Build Artifact打出 WAR 包并部署到独立 Tomcat课设验收经常要求“能部署到服务器”或“提供可运行部署包”。IDEA 里选中 Artifact菜单Build → Build Artifact → Rebuild编译后会在out/artifacts/下生成一个.war文件。把这个文件丢到 Tomcat 的webapps目录下启动 Tomcat它会自动解压部署。部署到 Linux 服务器之前要改三处数据库连接地址从localhost改成服务器的 IPDBUtil里的密码换成目标库的密码Tomcat 端口按服务器环境调整。改完以后重启 Tomcat访问http://服务器IP:8080/course_selection/login.jsp。验证时重点看页面是否能正常加载 CSS以及登录后跳转是否正常——很多部署到新环境的项目挂掉原因不是代码而是数据库没初始化只部署了 WAR 包没跑 SQL 脚本页面打开后一执行 SQL 就报 404。这种情况先看 Tomcat 的logs/catalina.out尾部有 SQL 异常就回去执行course_selection.sql没有异常再查代码。6.2 值得做的三个小改造从“能跑”到“像样”第一把所有 SQL 语句从 Statement 换成 PreparedStatement这能一次性消除掉 SQL 注入的可能性也是答辩时最常被问到的安全点。比如登录处原来写stmt.executeQuery(SELECT * FROM t_user WHERE username username )换成pstmt.setString(1, username)后单引号注入直接失效。第二给学生端的选课列表加一个 Ajax 异步请求页面不刷新就完成选课操作。原理很简单前端用 JavaScript 发起GET到/SelectCourseServlet?courseId1后端返回 JSON 字符串{success:true,msg:选课成功}前端根据返回结果更新按钮状态。这一改动不碰数据库结构只新增一个返回 JSON 的 Servlet工作量约半天但演示效果比传统表单提交好很多。第三加一个简单的 Logger 日志输出。用java.util.logging或 log4j 都行重点是在插入选课记录、登录失败这两处打印带时间戳和参数的日志。答辩时老师问你“怎么排查问题”你能指着日志文件说出用户什么时间做了什么操作比你口头分析代码有说服力得多。我自己的习惯是这种课设源码拿到手先花十分钟看web.xml和DBUtil.java这两个文件能读明白就说明项目结构正规先启动一次把页面跑出来再逐页点功能查逻辑。这些年帮人排查过几十次类似的 JSP Servlet 课设项目绝大多数问题都出在环境配置而不是代码本身。把这篇笔记里第三节的建表脚本、第四节的连接配置、第五节的五个报错排查走一遍你的选课系统就真正从“不能跑”变成“能演示”了。希望帮到你。本文还有配套的精品资源点击获取