☰
高校选课系统教学实践:JSP+Servlet+JavaBean全栈开发指南
2026/10/6 21:03:59 网站建设 项目流程

简介:本资源是一份完整的高校数据库课程设计实践项目,面向计算机专业本科生及Web开发初学者,聚焦JSP+Servlet+JavaBean三层架构的选课系统开发全流程。项目覆盖管理员、教师、学生三类角色全部核心功能,包含E-R图、实体关系模型、详细说明文档及完整可运行代码,适用于课程设计答辩、毕业设计参考或Java Web技术栈综合实训。压缩包共237个文件,5.95MB,含35个Java业务逻辑类、18个JSP页面、66个JS交互脚本、27个CSS样式文件及22个PNG/JPG界面素材,结构清晰,便于模块化学习与二次开发。已有1052人学习下载,提供Tomcat 8.5部署方案、SQL Server 2017建库脚本、Eclipse 2019-09工程配置说明,以及Ace前端UI框架集成细节(如ace.min.css、bootstrap.min.css等),开箱即用,显著降低环境搭建与调试门槛。

1. 高校选课系统为什么非得用 JSP + Servlet + JavaBean 这套“老架构”?——不是怀旧,是教学闭环的刚性需求

你打开一份高校《数据库设计》课程设计任务书,看到“基于 JSP + Servlet + JavaBean + Tomcat 实现选课管理系统”,第一反应可能是:这都 2024 年了,Spring Boot 都卷到自动装配源码级了,怎么还在写<%= request.getAttribute("msg") %>?但真相是:这不是技术倒退,而是教学场景下最扎实的“能力锚点”。这套组合拳不追求高并发、不拼云原生,它强制你亲手画 E-R 图、手写建表语句、手动拼接 SQL、在web.xml里配servlet-mapping、在doPost()里解析request.getParameter()、把Student对象从 DAO 层一层层托举到 JSP 页面——每一步都暴露逻辑断点,每一处都可打断点调试。学生交作业时,老师能一眼看出:E-R 图里“课程-教师”是 1:N 还是 M:N?Servlet 是否做了空值校验?JavaBean 的setCourseId()是不是漏了Integer.parseInt()?Tomcat 日志里有没有ClassNotFoundException: com.mysql.cj.jdbc.Driver?这种“全链路裸奔式开发”,恰恰是数据库设计能力落地的唯一验证路径。它不教你怎么用 MyBatis 自动生成 mapper,它教你为什么需要 mapper——因为手写 JDBC 太容易出错。如果你正被课程设计卡在“数据库连不上”或“JSP 页面显示 null”,别急着换框架,先把这个栈的每个环节钉死。


2. 从零搭起选课系统骨架:Tomcat + JDK + MySQL 环境三件套实操指南

2.1 Tomcat 安装与 JDK 版本强绑定:为什么必须用 JDK 8(而非 JDK 21)?

很多同学在 IDEA 里配 Tomcat 时卡在“Application Server not configured”,根源常是 JDK 版本错配。Tomcat 8.5(课程设计最常用版本)官方明确要求JDK 1.8+,但实际运行中,若用 JDK 17 或 JDK 21,会触发java.lang.UnsupportedClassVersionError—— 因为 Tomcat 8.5 编译目标字节码版本是 52(对应 JDK 8),而 JDK 17 编译出的是 61。更隐蔽的坑是:JDK 21 的javax.servlet.*包已被移除,而 JSP/Servlet 规范依赖它。
正确做法:

  • 下载 Adoptium Temurin JDK 8u392 (LTS 版本,兼容性最稳)
  • 设置JAVA_HOME指向 JDK 8 根目录(如C:\Program Files\Eclipse Adoptium\jdk-8.0.392.9-hotspot)
  • 在 Tomcatbin\setenv.bat(Windows)或setenv.sh(Linux)中显式声明:
@echo off set JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-8.0.392.9-hotspot set JRE_HOME=%JAVA_HOME%

提示:不要依赖系统默认 JDK。IDEA 中需在File → Project Structure → Project和Project Settings → Modules两级都设为 JDK 8;Tomcat 配置页里的 JRE 也必须单独指定,否则 IDEA 会偷偷用项目 SDK 覆盖。

2.2 MySQL 数据库初始化:按选课业务建模的 5 张核心表及字段约束

选课系统不是 CRUD 练习,它的表结构直指数据库设计核心能力——关系识别、范式控制、约束落地。我们按真实教学场景设计以下 5 张表(含外键、非空、唯一等约束):

表名字段说明关键约束设计意图
studentstu_id(PK, CHAR(10)),name(NOT NULL),major,gradestu_id主键,name非空学生实体,学号定长便于对齐
teachertea_id(PK, CHAR(8)),name,depttea_id主键,dept默认 '计算机系'教师实体,院系可为空但有默认值
coursecourse_id(PK, VARCHAR(10)),course_name,credit,tea_id(FK)course_id主键,creditCHECK(credit BETWEEN 1 AND 8)课程实体,学分必须 1~8,外键关联教师
select_coursestu_id(FK),course_id(FK),score(TINYINT),select_time联合主键(stu_id,course_id),scoreCHECK(score BETWEEN 0 AND 100)选课关系表,成绩范围校验,时间戳记录选课时刻
adminadmin_id(PK),username,passwordusernameUNIQUE,password加密存储(MD5)管理员表,密码绝不明文

建表 SQL(MySQL 5.7+):

-- 创建数据库并设字符集 CREATE DATABASE course_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE course_db; -- 学生表 CREATE TABLE student ( stu_id CHAR(10) PRIMARY KEY, name VARCHAR(20) NOT NULL, major VARCHAR(30), grade INT ); -- 教师表 CREATE TABLE teacher ( tea_id CHAR(8) PRIMARY KEY, name VARCHAR(20), dept VARCHAR(30) DEFAULT '计算机系' ); -- 课程表(外键指向教师) CREATE TABLE course ( course_id VARCHAR(10) PRIMARY KEY, course_name VARCHAR(50) NOT NULL, credit TINYINT CHECK (credit BETWEEN 1 AND 8), tea_id CHAR(8), FOREIGN KEY (tea_id) REFERENCES teacher(tea_id) ON DELETE SET NULL ); -- 选课关系表(联合主键 + 成绩校验) CREATE TABLE select_course ( stu_id CHAR(10), course_id VARCHAR(10), score TINYINT CHECK (score BETWEEN 0 AND 100), select_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (stu_id, course_id), FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE ); -- 管理员表(密码 MD5 加密) CREATE TABLE admin ( admin_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) UNIQUE NOT NULL, password VARCHAR(32) NOT NULL -- 存储 MD5 值 );

注意:ON DELETE CASCADE和ON DELETE SET NULL不是炫技,而是体现数据库设计中的“业务一致性”意识——删教师时课程教师字段置空(保留课程记录),删学生则自动清空其所有选课记录(避免孤儿数据)。这些约束必须在建表时写死,而不是靠 Java 代码补救。

2.3 Tomcat 启动验证与 WAR 包部署:从localhost:8080到http://localhost:8080/course的关键跳转

环境配好后,别急着写代码,先验证 Tomcat 是否真跑通:

  1. 进入tomcat/bin目录,双击startup.bat(Windows)或执行./startup.sh(Linux)
  2. 观察控制台输出:出现INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [xxx] milliseconds即成功
  3. 浏览器访问http://localhost:8080,看到 Apache Tomcat 欢迎页(注意:不是 404!)

部署你的选课系统 WAR 包:

  • 将项目导出为 WAR(IDEA:File → Project Structure → Artifacts → + → Web Application: Archive)
  • 把生成的course.war文件复制到tomcat/webapps/目录下
  • Tomcat 会自动解压成course/文件夹(无需手动解压)
  • 访问http://localhost:8080/course—— 此时若报 404,90% 是web.xml配置错误或index.jsp路径不对

关键检查点:

  • webapps/course/WEB-INF/web.xml必须存在且<welcome-file-list>指向index.jsp
  • webapps/course/index.jsp必须在根目录(不是webapps/course/pages/index.jsp)
  • 若启动时报SEVERE: Error listenerStart,大概率是web.xml里 servlet-class 类名写错(如com.servlet.LoginServlet写成com.servlet.loginServlet,Java 区分大小写!)

3. JavaBean + Servlet 分层实现:把数据库操作从 JSP 里彻底剥离

3.1 JavaBean 设计原则:不是 POJO,而是“可序列化、有业务语义”的数据载体

很多同学把 JavaBean 当作简单的 getter/setter 容器,这是致命误区。在选课系统中,JavaBean 必须承载业务规则和状态合法性。例如Student类不能只存字段,还要封装校验逻辑:

// src/com/entity/Student.java package com.entity; import java.io.Serializable; public class Student implements Serializable { private static final long serialVersionUID = 1L; // 防止序列化版本冲突 private String stu_id; private String name; private String major; private Integer grade; // 构造方法强制校验学号格式(10位数字) public Student(String stu_id, String name) { if (stu_id == null || !stu_id.matches("\\d{10}")) { throw new IllegalArgumentException("学号必须为10位数字"); } this.stu_id = stu_id; this.name = name == null ? "" : name.trim(); } // getter/setter(IDEA 自动生成即可) public String getStu_id() { return stu_id; } public void setStu_id(String stu_id) { this.stu_id = stu_id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getMajor() { return major; } public void setMajor(String major) { this.major = major; } public Integer getGrade() { return grade; } public void setGrade(Integer grade) { this.grade = grade; } }

关键点:Serializable接口不是摆设——当 Student 对象存入 session(如session.setAttribute("user", student))时,Tomcat 需要序列化它;serialVersionUID避免类升级后反序列化失败;构造函数内嵌校验,比在 Servlet 里if (stu_id.length()!=10)更早拦截错误。

3.2 DAO 层手写 JDBC:为什么不用 Hibernate?因为你要亲手看见 SQL

课程设计禁用 ORM,目的就是逼你写原生 JDBC。DAO 层是数据库与业务逻辑的唯一通道,必须严格遵循“一个方法一个 SQL”原则:

// src/com/dao/StudentDAO.java package com.dao; import com.entity.Student; import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDAO { private static final String URL = "jdbc:mysql://localhost:3306/course_db?useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; // 生产环境必须加密 // 查询所有学生(演示 ResultSet 映射) public List<Student> findAll() throws SQLException { List<Student> list = new ArrayList<>(); String sql = "SELECT stu_id, name, major, grade FROM student ORDER BY stu_id"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student s = new Student( rs.getString("stu_id"), rs.getString("name") ); s.setMajor(rs.getString("major")); s.setGrade(rs.getInt("grade")); list.add(s); } } return list; } // 插入学生(演示参数化防止 SQL 注入) public boolean insert(Student student) throws SQLException { String sql = "INSERT INTO student (stu_id, name, major, grade) VALUES (?, ?, ?, ?)"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, student.getStu_id()); ps.setString(2, student.getName()); ps.setString(3, student.getMajor()); ps.setInt(4, student.getGrade() != null ? student.getGrade() : 0); return ps.executeUpdate() > 0; } } }

注意:PreparedStatement的?占位符是防 SQL 注入的唯一防线;try-with-resources自动关闭连接,避免Connection leak;serverTimezone=Asia/Shanghai解决 MySQL 8.0+ 时区报错;密码明文仅限本地测试,课程设计验收后必须改成配置文件读取。

3.3 Servlet 控制器:HTTP 请求到业务逻辑的精准路由

Servlet 不是“写个 doPost 就完事”,它是 HTTP 协议与 Java 业务的翻译官。以登录为例,LoginServlet必须处理三种请求分支:

// src/com/servlet/LoginServlet.java package com.servlet; import com.dao.AdminDAO; import com.entity.Admin; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 基础校验(空值、长度) if (username == null || username.trim().isEmpty() || password == null || password.length() < 6) { request.setAttribute("msg", "用户名或密码格式错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 2. 密码 MD5 加密(与数据库存储一致) String md5Pass = md5(password); // 3. 查询管理员(DAO 层) AdminDAO dao = new AdminDAO(); Admin admin = dao.findByUsername(username); // 4. 业务判断:用户存在且密码匹配 if (admin != null && admin.getPassword().equals(md5Pass)) { HttpSession session = request.getSession(); session.setAttribute("admin", admin); // 登录态存入 session response.sendRedirect(request.getContextPath() + "/admin/main.jsp"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } // MD5 工具方法(课程设计允许直接写,不必引入 Apache Commons Codec) private String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.getBytes()); StringBuilder sb = new StringBuilder(); for (byte b : digest) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }

关键细节:response.sendRedirect()用于登录成功后的页面跳转(避免重复提交);request.getRequestDispatcher().forward()用于登录失败时回传错误信息到login.jsp;session.setAttribute()是维持登录态的核心;md5()方法必须与数据库插入时的加密方式完全一致(否则永远登不上)。


4. JSP 页面开发避坑指南:从“页面空白”到“数据精准渲染”的 5 个血泪经验

4.1 JSP 页面加载后刷新一次:不是玄学,是解决表单重复提交的务实方案

课程设计常见需求:“学生提交选课后,页面自动刷新显示最新已选课程列表”。很多同学用 JavaScriptwindow.location.reload(),结果导致表单重复提交(F5 刷新时重发 POST)。正确解法是 PRG 模式(Post-Redirect-Get):

  • Servlet 处理完选课逻辑后,用response.sendRedirect("select_list.jsp")跳转
  • select_list.jsp用GET请求加载数据,此时刷新页面只是重新 GET,不会重复选课
<!-- select_list.jsp --> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>我的选课</title></head> <body> <h2>已选课程</h2> <c:choose> <c:when test="${empty requestScope.courseList}"> <p>暂无选课记录</p> </c:when> <c:otherwise> <table border="1"> <tr><th>课程编号</th><th>课程名称</th><th>学分</th><th>教师</th><th>成绩</th></tr> <c:forEach items="${requestScope.courseList}" var="c"> <tr> <td>${c.course_id}</td> <td>${c.course_name}</td> <td>${c.credit}</td> <td>${c.teacher_name}</td> <td>${c.score == null ? '未录入' : c.score}</td> </tr> </c:forEach> </table> </c:otherwise> </c:choose> </body> </html>

注意:${requestScope.courseList}显式指定作用域,避免 EL 表达式因作用域混乱取不到值;<c:forEach>循环前必须用<%@ taglib %>引入 JSTL;<c:choose>替代 Java 脚本<% if(...) %>,更安全易读。

4.2 JSP 图片坐标定位:不是 CSS 万能,而是 HTML 表单控件的像素级对齐

“JSP 页面让图片对坐标定位”常被误解为 CSS 定位,实则是表单元素与图片的视觉对齐问题。例如选课系统首页需在“课程列表”旁放一张“选课指南”图片,要求图片右上角紧贴表格右边界:

<!-- index.jsp 片段 --> <div style="position: relative; width: 100%;"> <table style="float: left; margin-right: 20px;"> <!-- 课程表格内容 --> </table> <img src="images/guide.png" style="position: absolute; top: 0; right: 0; width: 150px;" alt="选课指南"/> </div>

关键:position: relative为父容器建立定位上下文;position: absolute让图片脱离文档流;top: 0; right: 0锚定右上角;width: 150px强制缩放(避免大图撑破布局)。纯 CSS 方案比<img align="right">更可控,且兼容所有浏览器。

4.3 JSP 个人信息展示页面:EL 表达式失效的 3 个排查路径

学生登录后进入profile.jsp,期望显示欢迎,张三!,却只看到空白或${student.name}原样输出。这不是 JSP 语法错,而是环境链断裂:

现象原因解决方案
页面显示${student.name}字面量web.xml 中缺失<jsp-config>或 JSP 版本过低在web.xml中添加:
<jsp-config><jsp-property-group><url-pattern>*.jsp</url-pattern><el-ignored>false</el-ignored></jsp-property-group></jsp-config>
页面显示nullServlet 中未将 Student 对象存入 request/session检查request.setAttribute("student", student)是否执行,且forward()前调用
页面部分字段为空(如student.major为空)数据库该字段为 NULL,而 JavaBean 未处理 null 安全在 JSP 中用<c:out value="${student.major}" default="未填写"/>或 JavaBean 的 getter 返回空字符串

血泪经验:EL 表达式失效 80% 是web.xml配置缺失,不是代码错。Tomcat 8.5 默认支持 EL,但老式web.xmlDTD 声明(如<!DOCTYPE web-app PUBLIC "-//Sun Microsystems...">)可能禁用 EL,改用web.xml3.1+ schema 声明可根治。

4.4 Tomcat 后台上传 WAR 被限制 IP:不是安全策略,而是 manager-gui 的默认访问控制

课程设计需用 Tomcat Manager 上传 WAR,但访问http://localhost:8080/manager/html时提示 403。这不是端口错,而是 Tomcat 8.5+ 默认禁止远程 IP 访问 Manager:

解决步骤:

  1. 编辑tomcat/conf/tomcat-users.xml,添加角色和用户:
<role rolename="manager-gui"/> <role rolename="manager-script"/> <user username="admin" password="123456" roles="manager-gui,manager-script"/>
  1. 编辑tomcat/webapps/manager/META-INF/context.xml,注释掉 IP 过滤段:
<!-- <Valve className="org.apache.catalina.valves.RemoteAddrValve" allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" /> -->
  1. 重启 Tomcat,用admin/123456登录 Manager 页面上传 WAR

注意:生产环境绝不可注释 IP 限制,课程设计本地开发时才可临时放开。manager-script角色支持 CURL 上传,比网页更稳定。


5. 数据库设计验证:用 3 个真实业务场景检验你的 E-R 图是否“活”着

5.1 场景一:学生跨专业选课——检验外键约束与 NULL 处理

业务规则:计算机系学生可选“数学分析”(数学系开课),此时course.tea_id应允许为 NULL(因数学系教师不在teacher表中)。
验证动作:

  • 手动执行INSERT INTO course (course_id, course_name, credit, tea_id) VALUES ('MA101', '数学分析', 4, NULL);
  • 若报错Cannot add or update a child row: a foreign key constraint fails,说明tea_id外键未设ON DELETE SET NULL或字段不允许 NULL
  • 修正后,再插入一条select_course记录,确认stu_id和course_id联合主键能唯一标识选课行为

5.2 场景二:教师离职后课程归属——检验级联删除与数据完整性

业务规则:张教授退休,其开设的“数据结构”课程由李教授接手,原课程记录保留,仅更新tea_id。
验证动作:

  • 执行UPDATE course SET tea_id='T002' WHERE course_id='CS201';
  • 查看select_course表中该课程的选课记录是否完整保留(不应被删)
  • 若误设ON DELETE CASCADE,会导致学生选课记录丢失——这是范式设计失误的铁证

5.3 场景三:期末成绩批量导入——检验 CHECK 约束与事务原子性

业务规则:教务员上传 Excel 成绩,需保证每门课成绩在 0~100 之间,且同一学生同一课程只能有一条成绩记录。
验证动作:

  • 写一个批量更新 Servlet,用PreparedStatement批量执行:
String sql = "INSERT INTO select_course (stu_id, course_id, score) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE score = VALUES(score)"; // 使用 ON DUPLICATE KEY UPDATE 处理重复主键(联合主键)
  • 故意插入score=105,观察是否触发CHECK约束报错Data truncation: Data too long for column 'score'
  • 若未报错,说明建表时CHECK未生效(MySQL 5.7 默认忽略 CHECK,需升级到 8.0+ 或用触发器替代)

这些场景不是考试题,而是数据库设计是否“活”着的试金石。E-R 图画得再漂亮,挡不住一条INSERT就崩的现实。我带过 12 届课程设计,学生交稿前必做这三步验证——不是为了拿高分,是确保答辩时老师问“如果教师离职,数据怎么保证?”你能指着ON DELETE SET NULL的 SQL 说:“老师,这里已经处理了。”


6. 最后一道关卡:Tomcat 日志里藏了所有答案,学会读它比写代码更重要

课程设计最后 24 小时,你卡在“点击登录按钮没反应”,浏览器控制台干净,页面静默。别猜,直接翻tomcat/logs/catalina.out—— 这里才是真相发生地。我整理了 5 类高频日志线索,按出现频率排序:

日志关键词典型位置代表问题速查命令(Linux)
ClassNotFoundException: com.mysql.cj.jdbc.Drivercatalina.out 开头MySQL 驱动 JAR 未放入WEB-INF/lib/ls webapps/course/WEB-INF/lib/ | grep mysql
SEVERE: Servlet.service() for servlet [LoginServlet] threw exceptioncatalina.out 中段Servlet 代码抛异常(如空指针、SQL 语法错)grep -A 10 "LoginServlet" catalina.out
Caused by: java.sql.SQLException: Access denied for user 'root'@'localhost'catalina.out 尾部数据库账号密码错或权限不足mysql -u root -p -e "SHOW GRANTS FOR 'root'@'localhost';"
WARNING: An illegal reflective access operation has occurredcatalina.out 中间JDK 版本与 Tomcat 不兼容(如 JDK 17 + Tomcat 8.5)java -version && cat VERSION.txt
INFO: Server startup in [xxx] millisecondscatalina.out 结尾Tomcat 启动成功,问题在应用层tail -n 50 catalina.out | grep -i "course"

实战技巧:

  • Windows 用户用notepad++打开catalina.out,Ctrl+F 搜Exception或Caused by
  • Linux 用户用tail -f catalina.out实时监控,同时在浏览器触发操作,日志秒级刷新
  • 若日志无报错,检查localhost_access_log.*.txt—— 这里记录每次 HTTP 请求,看POST /course/login是否 200 还是 404/500

我带学生答辩时,常问:“你昨天遇到的最大问题是什么?怎么解决的?”答“看日志”者,基本都能过。因为日志不撒谎,它不关心你写了多漂亮的 JSP,只忠实地记录每一行字节的生死。课程设计不是比谁代码多,是比谁离真相最近。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询