ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

教务管理系统JavaWeb项目实战:从JSP+Servlet到SpringBoot的完整案例

教务管理系统JavaWeb项目实战:从JSP+Servlet到SpringBoot的完整案例 简介这是一份面向JavaWeb初学者与课程设计学习者的教务管理系统项目源码基于MVC设计模式与DAO模式搭建采用Servlet、JSP、Filter与MySQL数据库实现运行于Windows系统下的Tomcat 7与JDK1.8环境。系统按角色划分为登录、学生、教师、教务员与系统管理员四大部分涵盖登录、找回与修改密码、注销以及学生成绩查询、选修与考级报名、学籍信息查看修改、教师个人信息维护、教务员成绩管理与报名名单管理、管理员用户管理等功能结构完整适合作为课程设计或毕业设计的参考模板。资源包共223个文件以82个class、82个java、38个jsp为核心另含6个jar、4个xml及sql脚本等压缩包约2.6MB目录层次清晰。目前已有11656人学习下载。下载后建议先阅读README.txt了解启动方式读者可借此掌握分层开发思路、数据库建表与Servlet跳转逻辑并在此基础上继续完善待实现功能扩展新的业务模块。1. 教务管理系统 JavaWeb 项目从课程设计到简历项目的分水岭在哪很多同学做教务管理系统 JavaWeb 项目最后交出来的东西长一个样登录、查课表、增删改查页面能跑数据库里躺着几张表然后就没有然后了。答辩时老师问一句“选课冲突怎么处理的”当场卡壳。问题不在技术栈而在于大多数人把它当成了 JSPServlet 的练习册而不是一个需要处理并发、权限、数据一致性的真实系统。教务管理系统的核心难点其实很集中角色多管理员、教师、学生、业务交叉选课、排课、成绩、学籍、数据敏感成绩不能乱改、选课不能超员。这三个特点决定了它天然适合拿来练权限设计和事务控制而不是单纯堆 CRUD 页面。如果你正在找 JavaWeb 项目完整案例或者跟着黑马程序员的笔记学完了 JSP 和 Servlet 想找个东西练手教务管理系统是个不错的靶子——但前提是你得知道哪些地方该较真哪些地方可以先用最朴素的方式跑通。这篇内容面向两类人一是课程设计需要交东西、但不想交个玩具的在校生二是自学 JavaWeb 想往 SpringBoot 过渡、需要一个完整项目串知识点的转行者。我会按“先跑通最小闭环再逐个替换成更靠谱的方案”的思路来讲中间会给出可复制的配置和代码片段也会说清楚哪些坑我踩过、哪些参数你最好别照抄。2. 技术选型与最小可运行环境别一上来就 SpringBoot2.1 为什么先用 JSPServletMySQL 跑通而不是直接上框架我见过太多人打开 IDEA 第一件事就是 Spring Initializr勾一堆依赖然后卡在配置文件上三天。对于教务管理系统这个体量的项目如果你对 JavaWeb 的请求流转还没有肌肉记忆直接上 SpringBoot 会让你把“框架帮你做了什么”和“业务本身要做什么”搅在一起。常见做法是先用 JSPServletJDBC 把登录、查课表、选课三个功能跑通感受一下 request、session、转发重定向、连接池这些概念然后再用 SpringBoot 重写一遍。这样你对“为什么需要框架”的理解会扎实很多。热搜词里“idea 运行 javaweb 项目配置”出现频率很高说明很多人的第一道坎是环境。我一般会建议用 Tomcat 9 JDK 8 或 11 的组合MySQL 用 5.7 或 8.0 都行但驱动包版本要对上。IDEA 里配置 Tomcat 的坑集中在 Artifact 和 Context Path 上下面会给具体步骤。2.2 数据库表设计五张核心表就能撑起最小闭环教务系统的表可以很多但最小闭环只需要五张用户表、角色表、课程表、选课记录表、成绩表。下面是我常用的建表语句字段做了精简但约束该有的都有。-- 用户表统一存三种角色用 role 字段区分 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 或 BCrypt别存明文 real_name VARCHAR(32) NOT NULL, role TINYINT NOT NULL COMMENT 1管理员 2教师 3学生, class_id INT DEFAULT NULL COMMENT 学生所属班级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表capacity 是选课人数上限后面并发控制要用 CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_name VARCHAR(64) NOT NULL, teacher_id INT NOT NULL, credit DECIMAL(3,1) NOT NULL, capacity INT NOT NULL DEFAULT 50, selected_count INT NOT NULL DEFAULT 0, semester VARCHAR(16) NOT NULL, FOREIGN KEY (teacher_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表student_id course_id 建唯一索引防止重复选课 CREATE TABLE t_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES t_user(id), FOREIGN KEY (course_id) REFERENCES t_course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表和选课记录一对一用 selection_id 关联 CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, selection_id INT NOT NULL UNIQUE, score DECIMAL(5,2) DEFAULT NULL, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (selection_id) REFERENCES t_selection(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个参数值得说明。t_selection上的uk_student_course唯一索引是防止同一学生重复选同一门课的第一道防线比在 Java 代码里查一遍再插入要可靠得多。t_course里的selected_count是冗余字段用空间换查询性能但必须和t_selection的插入放在同一个事务里更新否则会出现“记录插进去了但计数没加”的脏数据。字符集统一用utf8mb4不然后面存学生姓名里的生僻字会出问题。2.3 IDEA 里跑通第一个 Servlet配置步骤和常见报错在 IDEA 里新建一个 Java Enterprise 项目勾选 Web Application然后配置 Tomcat。具体路径是 Run → Edit Configurations → 加号 → Tomcat Server → Local在 Deployment 标签页里把 Artifact 加进去Application context 设成/edu。这一步如果 Artifact 是空的说明你没在 Project Structure → Artifacts 里创建 Web Application: Exploded。// LoginServlet.java最朴素的登录逻辑先跑通再说 WebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // 实际项目里这里要做 MD5 或 BCrypt先明文跑通流程 User user userDao.findByUsernameAndPassword(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } req.getSession().setAttribute(currentUser, user); // 按角色跳不同首页 switch (user.getRole()) { case 1: resp.sendRedirect(req.getContextPath() /admin/index.jsp); break; case 2: resp.sendRedirect(req.getContextPath() /teacher/index.jsp); break; default: resp.sendRedirect(req.getContextPath() /student/index.jsp); } } }这段代码的逻辑很直白拿参数、查库、写 session、按角色跳转。参数说明上req.getContextPath()返回的是/edu拼在重定向路径前面能避免部署路径变化后 404。forward和sendRedirect的区别在这里体现得很清楚登录失败用 forward 是为了保留 request 里的错误提示登录成功用 redirect 是为了防止刷新页面重复提交表单。这个细节答辩时经常被问。跑起来之后如果报ClassNotFoundException: com.mysql.cj.jdbc.Driver检查WEB-INF/lib下有没有 mysql-connector-java 的 jar 包或者 Maven 里 scope 是不是写成了 provided。如果报 404先看 Tomcat 启动日志里 Context path 是不是/edu再看WebServlet的路径有没有拼错。3. 选课并发与权限拦截教务系统真正值得较真的两个地方3.1 选课超员问题从“查了再插”到“更新时判断”选课是教务系统里唯一有并发压力的场景。假设一门课容量 50 人第 50 个和第 51 个学生同时点选课如果你先SELECT selected_count FROM t_course WHERE id?查出 49然后判断小于 50再UPDATE加一两个线程可能都查到 49都判断通过最后变成 51 人。这就是典型的竞态条件。常见做法有两种。第一种是用数据库的行锁在事务里用SELECT ... FOR UPDATE锁住课程行再判断再更新。第二种是用一条带条件的 UPDATE 语句把判断和更新合并成原子操作。// 方案二原子更新推荐 public boolean selectCourse(int studentId, int courseId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 先插选课记录唯一索引会拦住重复选课 String insertSql INSERT INTO t_selection(student_id, course_id) VALUES(?, ?); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); } // 再原子更新已选人数只有 selected_count capacity 时才成功 String updateSql UPDATE t_course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, courseId); int rows ps.executeUpdate(); if (rows 0) { conn.rollback(); // 人数已满回滚掉刚才插入的选课记录 return false; } } conn.commit(); return true; } catch (SQLException e) { // 唯一索引冲突也会走到这里说明重复选课 if (conn ! null) try { conn.rollback(); } catch (SQLException ignored) {} return false; } finally { if (conn ! null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } }这段代码的关键在于UPDATE语句里的AND selected_count capacity。数据库在执行这条 UPDATE 时会对目标行加写锁两个并发事务只有一个能先拿到锁并完成更新另一个会等锁释放后再执行此时selected_count已经等于capacity更新影响行数为 0触发回滚。参数上capacity和selected_count都必须是 NOT NULL 且有默认值否则比较会出意外。另外t_selection的唯一索引在这里也起了作用如果同一个学生重复提交INSERT 会直接抛异常被 catch 捕获后返回 false不会走到后面的更新。注意如果你的 MySQL 隔离级别是 REPEATABLE READ默认上面的 UPDATE 行为是可靠的。但如果有人把隔离级别改成 READ COMMITTED逻辑依然成立因为 UPDATE 本身会加锁。真正要避免的是在 Java 代码里先查后改那种写法在任何隔离级别下都不可靠。3.2 权限拦截用 Filter 统一处理别在每个 Servlet 里写 if教务系统有三种角色每个角色能访问的页面不一样。新手容易在每个 Servlet 开头写if (user.getRole() ! 1) { resp.sendRedirect(...); return; }代码重复且容易漏。正确做法是用 Filter 做统一拦截。// AuthFilter.java拦截所有请求按角色和路径判断 WebFilter(/*) public class AuthFilter implements Filter { // 不需要登录就能访问的路径 private static final String[] WHITE_LIST {/login, /login.jsp, /css/, /js/}; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String contextPath request.getContextPath(); String path uri.substring(contextPath.length()); // 白名单直接放行 for (String white : WHITE_LIST) { if (path.startsWith(white)) { chain.doFilter(req, resp); return; } } User user (User) request.getSession().getAttribute(currentUser); if (user null) { response.sendRedirect(contextPath /login.jsp); return; } // 按角色限制路径前缀 if (path.startsWith(/admin/) user.getRole() ! 1) { response.sendError(403, 无权访问); return; } if (path.startsWith(/teacher/) user.getRole() ! 2) { response.sendError(403, 无权访问); return; } if (path.startsWith(/student/) user.getRole() ! 3) { response.sendError(403, 无权访问); return; } chain.doFilter(req, resp); } }这个 Filter 的逻辑分三层白名单放行、未登录跳转、已登录按路径前缀校验角色。参数上WHITE_LIST里的路径要写全比如/css/后面的斜杠不能少否则/cssfoo也会被放行。path.startsWith(/admin/)这种判断方式要求你的目录结构必须规整管理员页面全放在/admin/下教师页面全放在/teacher/下。如果目录结构乱了这个 Filter 就形同虚设。提示Filter 的WebFilter(/*)会拦截所有请求包括静态资源。如果你发现 CSS 加载不出来检查白名单里有没有把/css/和/js/加进去。另一个常见问题是 Filter 里用了request.getSession()但没配置 session 超时导致用户登录后长时间不操作再点页面就跳回登录页这个在web.xml里配session-configsession-timeout30/session-timeout/session-config就行。4. 从 JSP 到前后端分离什么时候该换换了之后怎么改4.1 判断信号出现这三个迹象就该动手了JSP 不是不能用但当你发现页面里% %脚本块比 HTML 标签还多、同一个查询逻辑在三个 JSP 里各写了一遍、前端同学想调你的接口但只能拿到渲染好的 HTML 时就该考虑前后端分离了。教务管理系统里最典型的信号是成绩录入页面教师需要动态加载学生名单、批量提交分数、实时校验分数范围这些用 JSP 写会非常别扭。换的路径不是推倒重来而是渐进式替换。先把 Servlet 改造成返回 JSON 的接口JSP 页面暂时保留但改成用 AJAX 调接口。这样你可以一个模块一个模块地迁移不会出现“改到一半项目跑不起来”的尴尬。4.2 用 Servlet Gson 返回 JSON最小改造示例不需要引入 SpringMVC用最朴素的 Servlet 加 Gson 就能返回 JSON。下面以“查询某课程的学生名单”为例。// CourseStudentServlet.java返回 JSON 而不是转发到 JSP WebServlet(/api/course/students) public class CourseStudentServlet extends HttpServlet { private SelectionDao selectionDao new SelectionDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置响应类型和编码这两行顺序不能反 resp.setContentType(application/json;charsetUTF-8); resp.setCharacterEncoding(UTF-8); int courseId Integer.parseInt(req.getParameter(courseId)); ListStudentScoreVO list selectionDao.findStudentsByCourse(courseId); // 用 Gson 序列化日期格式要单独处理 Gson gson new GsonBuilder() .setDateFormat(yyyy-MM-dd HH:mm:ss) .create(); resp.getWriter().write(gson.toJson(list)); } }参数说明setContentType必须在getWriter()之前调用否则编码设置不生效返回的中文会变成乱码。GsonBuilder里的setDateFormat是为了让java.util.Date类型的字段按可读格式输出默认 Gson 会输出时间戳。StudentScoreVO是一个普通的 JavaBean字段名就是 JSON 的 key前端拿到的数据结构是[{studentName: 张三, score: 85.5}, ...]。前端用 fetch 调这个接口的代码大概长这样// 教师端成绩录入页面的加载逻辑 async function loadStudents(courseId) { const resp await fetch(/edu/api/course/students?courseId${courseId}); if (!resp.ok) { alert(加载失败请检查登录状态); return; } const data await resp.json(); const tbody document.querySelector(#studentTable tbody); tbody.innerHTML data.map(s tr td${s.studentName}/td tdinput typenumber min0 max100 step0.5 value${s.score ?? }># application.yml 关键配置 spring: datasource: url: jdbc:mysql://localhost:3306/edu_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver servlet: session: timeout: 30m mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.edu.entityserverTimezoneAsia/Shanghai这个参数在 MySQL 8.0 里必须加否则连接会报时区错误。session.timeout设成 30 分钟和之前web.xml里的配置对应。MyBatis 的mapper-locations指向resources/mapper目录XML 文件里的 namespace 要对应 Mapper 接口的全限定名。重写完之后你会发现原来 JSP 里那些% %脚本块全变成了 Controller 返回 JSON、前端用 Vue 或 React 渲染。这时候你的项目就从“课程设计”变成了“能写进简历的前后端分离项目”。面试官问起来你可以从 JSP 的痛点讲到为什么要分离从选课的竞态条件讲到数据库行锁和原子更新从 Filter 讲到 Spring 的 Interceptor 和 AOP。这些细节比“我用了 SpringBoot”有说服力得多。我自己的习惯是每做完一个版本把踩过的坑和关键决策记在一个 Markdown 文件里答辩前翻一遍比背稿子管用。教务管理系统这个题目看起来普通但把并发、权限、事务、前后端分离这几块讲透足够撑起一场不心虚的答辩。希望帮到你。本文还有配套的精品资源点击获取
返回列表