简介:本资源是一份完整的高校数据库课程设计实践项目,面向计算机专业本科生及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) - 在 Tomcat
bin\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 张表(含外键、非空、唯一等约束):
| 表名 | 字段说明 | 关键约束 | 设计意图 |
|---|---|---|---|
student | stu_id(PK, CHAR(10)),name(NOT NULL),major,grade | stu_id主键,name非空 | 学生实体,学号定长便于对齐 |
teacher | tea_id(PK, CHAR(8)),name,dept | tea_id主键,dept默认 '计算机系' | 教师实体,院系可为空但有默认值 |
course | course_id(PK, VARCHAR(10)),course_name,credit,tea_id(FK) | course_id主键,creditCHECK(credit BETWEEN 1 AND 8) | 课程实体,学分必须 1~8,外键关联教师 |
select_course | stu_id(FK),course_id(FK),score(TINYINT),select_time | 联合主键(stu_id,course_id),scoreCHECK(score BETWEEN 0 AND 100) | 选课关系表,成绩范围校验,时间戳记录选课时刻 |
admin | admin_id(PK),username,password | usernameUNIQUE,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 是否真跑通:
- 进入
tomcat/bin目录,双击startup.bat(Windows)或执行./startup.sh(Linux) - 观察控制台输出:出现
INFO [main] org.apache.catalina.startup.Catalina.start Server startup in [xxx] milliseconds即成功 - 浏览器访问
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.jspwebapps/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> |
页面显示null | Servlet 中未将 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:
解决步骤:
- 编辑
tomcat/conf/tomcat-users.xml,添加角色和用户:
<role rolename="manager-gui"/> <role rolename="manager-script"/> <user username="admin" password="123456" roles="manager-gui,manager-script"/>- 编辑
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" /> -->- 重启 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.Driver | catalina.out 开头 | MySQL 驱动 JAR 未放入WEB-INF/lib/ | ls webapps/course/WEB-INF/lib/ | grep mysql |
SEVERE: Servlet.service() for servlet [LoginServlet] threw exception | catalina.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 occurred | catalina.out 中间 | JDK 版本与 Tomcat 不兼容(如 JDK 17 + Tomcat 8.5) | java -version && cat VERSION.txt |
INFO: Server startup in [xxx] milliseconds | catalina.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,只忠实地记录每一行字节的生死。课程设计不是比谁代码多,是比谁离真相最近。希望帮到你。
本文还有配套的精品资源,点击获取