
简介一份完整的基于J2EE与MySQL的学生课程管理系统课程设计项目面向初学Java Web或准备毕业设计、课程设计、工程实训的开发者。系统采用MVC分层架构整合Spring、Struts与Hibernate三大框架围绕学生、教师、管理员三种角色展开涵盖用户注册登录、个人资料设置、课程查询与选课、作业布置提交批改、助教安排及教学数据统计等功能几乎覆盖高校教务管理中的常见场景。项目代码结构清晰按模块分包便于理解业务逻辑与框架配置。压缩包内共90个文件主体为46个Java源文件、12个JSP页面、13个CSS样式表、3个XML配置文件及若干JS脚本同时附带设计文档、演示PPT与README说明能够帮助学习者快速掌握从源码到文档的完整课程设计流程资源包仅356KB轻量紧凑已有79人学习下载。代码作为参考资料需要一定基础后自行调试并二次开发依然适用于初期项目立项和功能扩展。1. 学生课程管理系统课程设计为什么还选 J2EE“学生课程管理系统”这个题目放到今天来看确实带着一股教材味但如果你拿到的课程设计任务书上写的是“基于 J2EE MySQL”那要交出来的就不是一个能点击的页面而是一条从浏览器到 Servlet、再到业务层、DAO最后落到 MySQL 的完整调用链。这个系统解决的是“学生在线选课、教师维护课程、管理员管理学生”这一组典型业务技术选型上刻意不走 Spring Boot为的是把 Java Web 最底层的请求分发、JDBC 事务和 SQL 设计全部暴露出来。适合正在做数据库或 Java 课程设计的人也适合想回头补一遍原生 Java Web 机制的开发。这里面的每一层都值得单独抠开看因为答辩老师最容易问的就是“你这层为什么这么分”和“这条数据到底是怎么查出来的”。2. J2EE 三层架构拆分与课程管理系统的包结构设计2.1 课程设计用 J2EE核心是 Servlet JSP JDBC 的调用链J2EE 是一个很大的目录课程设计真正用到的只是其中 Web 容器相关的部分Servlet 负责接收 HTTP 请求JSP 负责渲染页面JDBC 负责访问 MySQLTomcat 负责把这几个东西跑起来。很多人一开始纠结要不要上 Spring MVC我的建议是不要理由不是学不会而是课程设计评审看你的事务边界和 SQL 功底原生 J2EE 反而能把“谁调谁”讲得最清楚。一次典型的选课请求会这样走浏览器提交学号和课程号Tomcat 根据 web.xml 或注解把请求交给对应的 ServletServlet 调用业务层接口业务层校验课程容量后调用 DAODAO 用 JDBC 执行 INSERT 和 UPDATE最后把结果返回给 JSP 或直接重定向到列表页。这条链路里每一个环节都涉及一个独立类类的边界就是架构的边界。我这里给出一份可直接对照落地的包结构课程设计项目通常会比企业项目少很多抽象但分包的原则不能丢。course-system/ ├── pom.xml └── src/main/ ├── java/com/course/ │ ├── controller/ # Servlet 层只做收参、转发 │ ├── service/ # 业务接口与实现事务边界在这里 │ ├── dao/ # JDBC 操作只负责 SQL 和结果集映射 │ ├── entity/ # Student、Course、SC 实体类 │ └── util/ # DBUtil、StringUtil 等工具类 ├── resources/ │ └── jdbc.properties # 数据库连接参数 └── webapp/ ├── WEB-INF/web.xml ├── jsp/student/ ├── jsp/course/ └── index.jsp这个结构的核心约束是依赖方向controller 依赖 serviceservice 依赖 daodao 不依赖上层。实体类可以被所有层引用但它只是字段容器不应该带任何业务方法。最容易被忽视的是 util 包DBUtil 这类工具类在课程设计里承担连接创建和关闭它的质量直接影响事务是否生效后面专门讲。2.2 课程管理系统的三层分包与依赖方向各层职责需要落到一张表上这张表可以直接写进课程设计文档的“系统设计”章节也可以当作你写代码时的边界检查清单。层典型类名核心职责允许依赖controllerSelectCourseServlet接收请求、参数校验、页面转发service 接口serviceCourseServiceImpl业务规则、事务开启与提交dao 接口daoStudentDaoSQL 执行、ResultSet 到实体映射自身、entityentityStudent、Course、SC携带数据库字段无utilDBUtil创建 Connection、关闭资源jdbc.properties我一般要求学生把校验和业务规则放 service 而不是放 servlet原因是 Servlet 层的代码写多了以后事务和校验逻辑会纠缠在一起。比如“选课时先判断课程是否已满”这条规则放在 service 里意味着你可以在不启动 Tomcat 的情况下单独测试这个逻辑而放在 Servlet 里就得每次发起 HTTP 请求才能验证。2.3 各层代码的命名规范与参数传递约定命名这件事在课程设计里占了印象分的一半。controller 用 Servlet 结尾例如 StudentServlet、CourseServletservice 接口用 CourseService实现类用 CourseServiceImpldao 层用 CourseDao不搞泛型基类那一套因为课程设计代码量小过度抽象反而让答辩时说不清楚。参数传递上controller 从 request 拿到的都是字符串必须先做类型转换和空值判断再传给 service不要寄希望于 DAO 层帮你容错。service 返回实体对象或 List不要返回 ResultSetResultSet 依赖数据库连接连接关闭后数据就读不出来了。DAO 层返回 List 或单个实体基本类型只适合用在 count 这类聚合查询上。3. MySQL 数据库设计与 JDBC 连接的配置细节3.1 学生、课程、选课三张表的关系与字段设计学生和课程之间是多对多关系所以数据库必须拆出三张表student、course、sc。sc 是中间表里面存 student_id 和 course_id 两个外键外加选课时间和成绩字段。这个设计几乎是学生课程管理系统的标准答案因为不拆中间表的话一个学生只能选一门课或者一门课只能被一个学生选都无法表达真实业务。字段类型上不要全部无脑用 VARCHAR。学号用 VARCHAR(20) 是合理的因为可能存在“2024CS001”这类带字母的编号姓名用 VARCHAR(50)别用 VARCHAR(255)入学年份专门用 YEAR 类型比用字符串更能避免“2024年”这种脏数据。成绩用 DECIMAL(5,2)float 在比较时会引入精度问题DECIMAL 在 MySQL 中按字符串存储比较和排序都符合直觉。3.2 建表 SQL 与索引设计要点建表语句可以直接在 MySQL Workbench 的 SQL 编辑器中执行也可以保存成 .sql 文件后在命令行用mysql -u root -p create.sql导入。下面这份 SQL 是按 MySQL 5.7 及以上版本写的8.0 也能直接跑。CREATE DATABASE course_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_system; CREATE TABLE student ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, student_no VARCHAR(20) NOT NULL COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, major VARCHAR(100) DEFAULT NULL COMMENT 专业, enroll_year YEAR DEFAULT NULL COMMENT 入学年份, PRIMARY KEY (id), UNIQUE KEY uk_student_no (student_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE course ( id INT NOT NULL AUTO_INCREMENT COMMENT 主键, course_no VARCHAR(20) NOT NULL COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) DEFAULT 0.0 COMMENT 学分, capacity INT NOT NULL DEFAULT 50 COMMENT 课程容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, PRIMARY KEY (id), UNIQUE KEY uk_course_no (course_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; CREATE TABLE sc ( id INT NOT NULL AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩, PRIMARY KEY (id), UNIQUE KEY uk_student_course (student_id, course_id), KEY idx_course_id (course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课表;这里有两个细节值得在文档里单独说明。第一sc 表的联合唯一索引uk_student_course是业务兜底就算应用层忘了判断“是否已选过”数据库也会拒绝重复选课。第二外键约束在 InnoDB 下是强制性的如果你希望保持中间表数据完整性就保留 FK如果担心导入测试数据时外键检查麻烦可以只保留普通索引在应用层维护一致性但答辩时要能说清楚取舍。索引设计上sc 表的 student_id 是查询“某个学生的所有选课”的入口course_id 是查询“某门课被哪些学生选”的入口所以各建一个普通索引。联合唯一索引本身也可以覆盖 student_id 的前缀查询不需要单独为 student_id 再建一次索引。3.3 JDBC 驱动的 URL 参数与连接池配置MySQL 5.7 之后官方驱动类从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver旧写法在 MySQL 8.0 下会直接报 ClassNotFoundException。连接 URL 里有一批参数会影响行为和编码课程设计最常见的问题是中文乱码根源就是 URL 没带 characterEncoding。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.passwordyour_passworduseUnicodetruecharacterEncodingutf8解决插入中文变成问号的问题serverTimezoneAsia/Shanghai解决 8.0 驱动读取时间类型时的时区报错useSSLfalse是因为本地开发环境一般没有配置证书MySQL 8.0 默认开启 SSL 会打出 warning不影响运行但看着烦。allowPublicKeyRetrievaltrue是 8.0 连接时使用 caching_sha2_password 认证需要的参数不设置会报 Public Key Retrieval is not allowed。连接管理上课程设计没必要引入重型的连接池但也不能不用。最简单的做法是在 DBUtil 里用静态代码块加载驱动每次从 DriverManager 获取连接。这样写的好处是代码直观、答辩容易讲坏处是每一次请求都建立一个新的 TCP 连接在高并发下性能很差。如果课程设计文档里想体现进阶能力可以用 Druid 或 DBCP2 配置一个最小连接池核心配置是 initialSize5、maxActive20、maxWait6000这几个参数的意思是初始连接数、最大活跃连接数和获得连接的等待超时时间。4. 学生选课功能从 DAO 到 Servlet 的实现与事务控制4.1 DAO 层用 PreparedStatement 完成学生列表查询DAO 层直接面对 JDBC最容易踩的坑是把 SQL 字符串拼接成SELECT * FROM student WHERE name name 。这种写法在课程设计里还能跑通但答辩老师只要追问一句 SQL 注入就会很难看。用 PreparedStatement 有两个实际收益参数通过 setString 等方法绑定特殊字符会被转义另外 MySQL 服务端会缓存相同的预编译 SQL重复执行时少一步 SQL 解析。下面这段代码完成带分页的学生列表查询课程设计里管理端经常会用到。public ListStudent findStudentsByPage(int offset, int limit) { String sql SELECT id, student_no, name, major, enroll_year FROM student ORDER BY id LIMIT ?, ?; ListStudent list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, limit); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Student s new Student(); s.setId(rs.getInt(id)); s.setStudentNo(rs.getString(student_no)); s.setName(rs.getString(name)); s.setMajor(rs.getString(major)); if (rs.getDate(enroll_year) ! null) { s.setEnrollYear(rs.getInt(enroll_year)); } list.add(s); } } } catch (SQLException e) { throw new RuntimeException(查询学生列表失败, e); } return list; }代码里的 try-with-resources 是 Java 7 以后的语法Connection、PreparedStatement、ResultSet 会在语句块结束时自动关闭省去了 finally 里手动 close 的样板代码。LIMIT 后的两个参数用 setInt 绑定offset 表示从第几条开始取limit 表示取多少条页面上传的页码要先转换成 offset规则是 offset (page - 1) * pageSize。enroll_year 字段是 YEAR 类型MySQL JDBC 驱动返回 java.sql.Date 而不是整数必须先判空再取值否则 NULL 年份会直接触发 NullPointerException。4.2 业务层把选课动作切成一个事务选课不是一条 SQL 就能完成的动作至少包含两件事向 sc 表插入选课记录把 course 表的 selected_count 加一。这两条 SQL 必须同时成功或同时失败否则会出现选了课但人数没加、或者人数加了但记录没插进去的脏数据所以 service 层需要手动控制事务。业务层事务控制的关键是保证两条 SQL 使用同一个 Connection。最常见的问题是 DAO 层每个方法都自己 getConnection查询时没问题但到事务就失效因为两个 DAO 方法各用各的连接谁也没法控制对方的提交回滚。所以我把选课的核心逻辑放在 CourseServiceImpl 中用一条连接执行完整流程。public void selectCourse(int studentId, int courseId) { String lockCourseSql SELECT capacity, selected_count FROM course WHERE id ? FOR UPDATE; String insertScSql INSERT INTO sc(student_id, course_id) VALUES (?, ?); String updateCourseSql UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(lockCourseSql)) { ps1.setInt(1, courseId); ResultSet rs ps1.executeQuery(); if (!rs.next()) { throw new RuntimeException(课程不存在); } if (rs.getInt(selected_count) rs.getInt(capacity)) { throw new RuntimeException(课程容量已满); } } try (PreparedStatement ps2 conn.prepareStatement(insertScSql)) { ps2.setInt(1, studentId); ps2.setInt(2, courseId); ps2.executeUpdate(); } try (PreparedStatement ps3 conn.prepareStatement(updateCourseSql)) { ps3.setInt(1, courseId); int rows ps3.executeUpdate(); if (rows 0) { throw new RuntimeException(选课人数已满更新失败); } } conn.commit(); } catch (SQLException e) { try (Connection conn DBUtil.getConnection()) { conn.rollback(); } catch (SQLException ex) { throw new RuntimeException(回滚失败, ex); } throw new RuntimeException(选课失败, e); } }setAutoCommit(false)表示关闭自动提交之后的 SQL 都缓存在当前事务里直到 commit 才真正落库。SELECT 语句末尾的FOR UPDATE是行级锁把这条 course 记录锁住防止两个学生同时选最后一门课时都判断“容量未满”这个技巧在并发场景下是必须的。UPDATE 里的selected_count capacity是第二重保护即使前面判断失效这里也不让超卖。注意我在 catch 里重新获取了一个 Connection 做 rollback这种做法不推荐因为获取新连接本身也可能失败。更稳妥的方式是把 conn 定义在 try 块外面在 catch 中使用同一个 conn 回滚。课程设计里可以把原代码写成外层 Connection 模式把选择和回滚保持在同一个连接会话内这也是答辩时很容易被追问的一个细节。4.3 Servlet 控制层接收请求与参数校验Servlet 层的代码量应该最少它只做三件事设置编码、读取参数、调用 service 并决定页面走向。参数校验放在这里是因为它离 HTTP 最近可以在进入业务层之前把空值拦截掉。WebServlet(/selectCourse) public class SelectCourseServlet extends HttpServlet { private CourseService courseService new CourseServiceImpl(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String studentIdParam request.getParameter(studentId); String courseIdParam request.getParameter(courseId); if (studentIdParam null || courseIdParam null || studentIdParam.isEmpty() || courseIdParam.isEmpty()) { request.setAttribute(error, 学号和课程号不能为空); request.getRequestDispatcher(/jsp/error.jsp).forward(request, response); return; } try { int studentId Integer.parseInt(studentIdParam); int courseId Integer.parseInt(courseIdParam); courseService.selectCourse(studentId, courseId); response.sendRedirect(request.getContextPath() /courseList?page1); } catch (NumberFormatException e) { request.setAttribute(error, 参数格式错误); request.getRequestDispatcher(/jsp/error.jsp).forward(request, response); } catch (RuntimeException e) { request.setAttribute(error, e.getMessage()); request.getRequestDispatcher(/jsp/error.jsp).forward(request, response); } } }WebServlet(/selectCourse)是 Servlet 3.0 之后提供的注解注册方式省去了在 web.xml 里写servlet和servlet-mapping两段配置。如果你的 Tomcat 版本较老或者指导老师要求必须体现 web.xml 配置功底就把注解去掉在 web.xml 中补充等价配置。sendRedirect是重定向浏览器地址栏会变成新的 URL并产生第二次请求forward是服务端转发浏览器地址不变request 域中的属性可以带到 JSP。表单提交失败时用 forward 保留错误信息成功时用 sendRedirect 防止刷新页面重复提交选课请求。5. J2EE 项目部署到 Tomcat 的运行问题与排查5.1 本机环境准备与版本匹配部署一个 J2EE 课程设计项目环境匹配是第一道门槛。JDK 8 配 Tomcat 8.5 或 9.0 是最稳妥的组合JDK 11 也可以跑 Tomcat 9.0但如果你用的是更高的 Tomcat 版本要注意 Servlet 规范变化对 web.xml 头部的版本声明是否兼容。MySQL 你可以在官网选 8.x 的安装包安装配置教程里最关键的一步是选认证方式时选 Legacy 还是 Strong选了 Strong 的话 JDBC URL 必须带 allowPublicKeyRetrievaltrue。组件推荐版本注意事项JDK8 或 11Tomcat 9.0 支持 JDK 8Tomcat8.5 / 9.0端口默认 8080注意被占用MySQL5.7 / 8.0驱动类必须用 com.mysql.cj.jdbc.DriverMaven3.6打包 war 用也可以直接用 IDEA 导出MySQL 新建数据库时直接在 Workbench 的左边栏点 Create Schema 更直观数据库名和 jdbc.properties 里的保持一致。字符集默认就是 utf8mb4不需要额外改。如果你更习惯命令行mysql -u root -p登录后用SHOW VARIABLES LIKE character_set%查看编码确认不是 latin1 就能往下走。5.2 部署到 Tomcat 的完整步骤课程设计不一定要用 Maven但用 Maven 可以把整个生命周期标准化。项目根目录下执行打包命令生成 war 后直接放到 Tomcat 的 webapps 目录下启动后 Tomcat 会自动解压并部署。mvn clean package -DskipTests cp target/course-system.war $TOMCAT_HOME/webapps/ $TOMCAT_HOME/bin/startup.sh tail -f $TOMCAT_HOME/logs/catalina.out curl http://localhost:8080/course-system/mvn clean package -DskipTests会清理旧的编译结果并重新打包-DskipTests跳过测试课程设计没有写单元测试的话不加也可以。cp把 war 复制到 webappsTomcat 启动时发现 war 文件会自动解压成同名目录。startup.sh在 Windows 下对应 startup.bat启动日志并不在控制台打印所以用tail -f catalina.out实时查看异常。最后一条 curl 验证根路径能否访问返回 HTTP 200 就说明部署成功。如果 war 包之前部署过必须先把 webapps 下的同名目录删除再重新复制否则 Tomcat 可能不会解压覆盖。5.3 404、ClassNotFoundException、Access denied 的排查思路运行阶段的大部分报错都有固定套路下面这张表是我在带课程设计时经常用的排查清单。报错现象可能原因处理方式访问路径 404war 没部署成功或上下文路径写错看 webapps 下是否生成同名目录URL 必须带 /course-systemClassNotFoundException: com.mysql.cj.jdbc.Driver没有把 MySQL 驱动 jar 放进 WEB-INF/lib下载 mysql-connector-j 的 jar 放入 lib或用 Maven 依赖Access denied for user rootlocalhost密码错误或认证方式不兼容检查 jdbc.propertiesMySQL 8 确认 allowPublicKeyRetrievaltrueUnknown database course_system库名不一致Workbench 中确认创建了数据库且名称与 URL 相同中文乱码JSP 或 JDBC 编码不一致JSP 顶部加 pageEncodingUTF-8URL 带 characterEncodingutf8ClassNotFoundException 有一个隐蔽场景jar 明明在本地 Tomcat 的 lib 下存在但运行时报错。原因是 Tomcat 的 lib 目录对所有应用可见但如果你用 IDEA 的 Artifacts 打包Maven 依赖没选 “exploded”war 里的 WEB-INF/lib 就是空的。排查时先jar tf course-system.war | grep mysql看驱动是否真的打进了包。6. 验证选课结果时把 MySQL 索引与事务的坑补上6.1 用重复选课验证事务是否真的生效验证事务是否生效最直接的方法就是人为制造一次失败。对同一个 student_id 和 course_id 连续提交两次选课请求第二次会撞上 sc 表的联合唯一索引 uk_student_course抛出 SQLIntegrityConstraintViolationException。如果事务控制正确第一次选课插入的 sc 记录和 course 表 selected_count 的 1 会一起回滚数据库里查不到任何残留。做完异常场景后再用一条 SQL 回查最终状态SELECT s.student_no, c.course_name, sc.select_time, c.selected_count, c.capacity FROM sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.id WHERE s.student_no 20240001;联合查询是把三张表串起来的标准写法也是课程设计文档里必须有的内容。如果这里查出了两条相同学生和课程的记录说明要么唯一索引没建成功要么插入时绕过了约束检查。6.2 答辨时绕不开的三个数据库细节第一个是为什么 sc 表要用中间表而不是直接在 course 表加 student_id 字段多对多关系需要中间表来维持。第二个是为什么课程容量判断和更新人数要用selected_count capacity做条件而不是应用层先 SELECT 再 UPDATE并发下两段式判断会超卖条件更新是原子操作。第三个是为什么推荐使用 InnoDB它支持行级锁和事务回滚MyISAM 在选课这种写多的场景下容易出问题。针对第二个点可以在连接池的最小资源上再补一个动作代码里所有 try-with-resources 的块结束时一定要确保连接回调回连接池而不是被垃圾回收。使用连接池时close 方法实际是归还连接只要 URL 参数和连接池配置正确这个动作是自动的但要理解它和普通 JDBC 的 close 语义不同。最后再看一眼 jdbc.properties 里的 URL 是否带全了 utf8mb4 相关的参数这一步能避免答辩现场演示中文乱码的尴尬。本文还有配套的精品资源点击获取