JavaWeb教务管理系统实战:Servlet+JSP+JDBC从环境配置到二次开发

发布时间:2026/10/17 2:56:36
JavaWeb教务管理系统实战:Servlet+JSP+JDBC从环境配置到二次开发
简介这是一套教务管理系统Java Web项目源代码面向正在学习Servlet、JSP与MVC分层开发的初学者也适合用作毕业设计或课程实训参考。项目围绕登录、学生、教师、教务员、系统管理员五个板块展开内置登录注销、找回与修改密码、成绩查询、选修与考级报名、学籍信息查看修改、教师个人信息维护、教务员成绩管理、管理员用户管理等功能技术上采用MVC设计模式与DAO数据访问模式Servlet处理请求控制JSP负责页面渲染Filter统一过滤公共逻辑MySQL作为后端数据库整体层次清晰容易对照学习项目基于J2EE Eclipse环境开发适配Apache Tomcat与JDK1.8运行环境。压缩包为rar格式共223个文件包含82个Java源码及配套class、38个JSP页面、6个依赖jar包、4个XML配置和SQL建库脚本包体仅2.6MB同时内含README.txt便于快速理解目录结构与启动方式项目专门留有一定扩展空间可继续完善成绩管理、报名审核、权限控制等模块适合在二次开发中训练排错与调试思路。目前已有11657人学习下载是Java Web入门练手和课程设计的实用参考。1. 教务管理系统一个把 JavaWeb 基础全部落地成代码的完整项目“选课系统崩了”这件事多半不是系统的问题而是环境没搭对。我第一次拿到这套 javaweb 教务管理系统源码时图省事装了最新的 Tomcat 和 MySQL结果页面全是 500 和乱码折腾到凌晨才意识到是版本代差。把环境老老实实降到 JDK8 Tomcat8.5 MySQL5.7 之后登录、选课、成绩录入、学生管理四条主链路一次跑通。它的定位很明确一个用 Servlet JSP JDBC 写出来的完整教务管理系统覆盖管理员、教师、学生三种身份不扛高并发但把连接池、过滤器、事务、分页这些 JavaWeb 必考基础全部落地了。适合准备课设、或者想把 Servlet 基础串成一个完整系统的人。2. 技术栈与运行环境JDK8 Tomcat8.5 MySQL5.7 这套组合为什么不能乱动2.1 技术栈清单没有框架反而暴露了 JavaWeb 的全部细节这套源码的核心依赖就五样Servlet 3.1、JSP 2.3、JSTL 1.2、JDBC、C3P0 连接池前端用的 Bootstrap 3 jQuery。没有 Spring没有 MyBatis所有请求都走 HttpServlet 的子类所有数据库操作都是手写 PreparedStatement。如果源码包是用 IDEA 打开的 Maven 结构pom 里的依赖一只手数得完不会有那种拉一屏 jar 包的情况。我拆这套代码时最大的感受是它把 JavaWeb 里最容易糊弄过去的东西全部暴露出来了。比如登录校验要自己维护 session编码问题要靠 Filter 去统一处理选课那两条 SQL 要手动开启事务。这些在 SSM 项目里被框架藏起来的东西在这里每一行都看得见。刚学完 Servlet 但没写过完整系统的人看这种“看得见”的代码比看框架黑匣子有用得多。从技术选型的角度说这种老组合也有现实意义。很多学校的课设要求里明确写了“禁止使用框架”或者“仅限 Servlet JSP”这项目恰好就是照着这个要求写的。你拿它应付课设不用改技术栈你拿它入门也不会被框架的概念先入为主地带偏。2.2 版本搭配为什么是 JDK1.8 Tomcat8.5 MySQL5.7拿到源码先别急着升级环境。我见过不少人一上来就用 JDK17 Tomcat10 MySQL8折腾一晚上全在给版本擦屁股。这个项目是按老 JavaWeb 规范写的最稳的组合如下组件推荐版本注意点JDK1.8JDK17 跑老项目偶发反射访问报错Tomcat8.5仍是 javax.servlet 命名空间MySQL5.7与 5.1 驱动直接配套无时区问题mysql-connector-java5.1.49驱动类名 com.mysql.jdbc.DriverC3P00.9.5配置参数在 c3p0-config.xml 里提示如果是 MySQL 8.x 用户驱动要换成 com.mysql.cj.jdbc.Driver连接串还要加上 serverTimezoneAsia/Shanghai这个坑在第 5 章专门讲。版本确认之后的启动顺序很简单先用 IDEA 以 war exploded 方式部署Application context 填/system再修改源码里的db.properties把库名、账号、密码改成你自己本地的执行school.sql初始化数据库最后启动 Tomcat浏览器访问http://localhost:8080/system/login.jsp。如果不想用 IDEA把项目打成 war 丢进 Tomcat 的 webapps 目录也能跑访问路径就是 war 包名二选一即可。db.properties 里通常有 driver、url、username、password 四项。我一般会把 url 写成jdbc:mysql://localhost:3306/school?characterEncodingutf8这个characterEncodingutf8参数很关键后面乱码部分还会再提它。2.3 数据库设计六张表怎么支撑起教务闭环这个项目的库名通常叫 school核心是六张表。先把表结构列出来再去对照代码就非常清晰表名关键字段职责t_userid, username, password, role, realname登录账号username 存学号或工号t_studentstu_id, name, class_name, major_name学生基本信息t_teacherteacher_id, name, dept_name, title教师基本信息t_coursecourse_id, name, credit, teacher_id, capacity, selected课程与容量控制t_electiveid, stu_id, course_id, create_time学生选课记录t_scoreid, stu_id, course_id, score成绩记录这里有一个设计细节值得注意账号密码统一收在 t_user 里而不是散落在学生表和教师表中登录时只查一张表通过 role 字段决定跳转去向。t_course 里的 capacity 和 selected 两列是配套的selected capacity的判断承担了选课容量控制这个逻辑落到代码里就是第 4 章的选课事务。t_user 的表结构大致是这样CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(20) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role TINYINT NOT NULL COMMENT 1管理员 2教师 3学生, realname VARCHAR(50) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8;说明一下password 字段在老版本里常见直接存明文这对课设来说能跑通但如果要给导师演示安全规范建议二次开发时加 MD5 加盐。角色用数字而不是字符串是为了减少存储开销代价是需要看注释或者代码才知道 1、2、3 分别代表什么代码里通常有常量类做映射。2.4 分包设计servlet / service / dao 是手工搭出来的三层源码结构基本是标准的三层分包com.school.bean 放实体类com.school.dao 做数据库访问com.school.service 组织业务逻辑com.school.servlet 处理请求和跳转再加上 com.school.filter 和 com.school.utils 两个辅助包。这个分包一眼就能看出是照着 MVC 经典分层搭的。dao 里不写业务判断service 拿到多个 dao 方法拼装成一个完整操作servlet 只做参数解析和重定向。后面拆代码时会看到选课事务在 service 层而不是散落到 dao 里。从架构角度讲这个分包放在课程设计里是能拿得出手的没有过度设计没有动辄接口实现类双份文件读起来很直白。3. 目录结构与模块地图六个包、三个角色、一条请求链路3.1 源码目录先整体看一遍再动手拿到源码后第一件事打开目录结构。典型结构长这样src/main/java/com/school/ ├── bean/ 实体类User、Student、Course 等 ├── servlet/ 控制层LoginServlet、CourseServlet 等 ├── service/ 业务层选课事务在这里 ├── dao/ JDBC 数据访问 ├── filter/ 编码过滤器、登录过滤器 └── utils/ DBUtils、PageUtils src/main/webapp/ ├── css/ js/ images/ 静态资源 ├── WEB-INF/ │ ├── web.xml │ └── lib/ ├── login.jsp ├── index.jsp ├── admin/ 管理员页面 ├── teacher/ 教师页面 └── student/ 学生页面注意 webapp 下按角色分了三个目录这是整个项目组织页面的核心思路。每个角色登录后被重定向到自己的目录后续所有操作都在自己那套页面里完成不用在代码里反复判断权限。页面之间的跳转统一用相对路径加前缀的方式JSP 里的链接大多写成${pageContext.request.contextPath}/xxx这种形式。3.2 三个角色三套功能地图角色登录后首页核心功能管理员admin/index.jsp学生管理、教师管理、课程管理、班级维护教师teacher/index.jsp查看任课课程、导出学生名单、录入成绩学生student/index.jsp在线选课、退课、查询个人成绩这三套功能不是各写各的底层数据是打通的。管理员在课程管理里新建的课程教师登录后能在“我的课程”里看到学生选课后教师端的学生名单实时多一个人学生端“我的成绩”里也能看到已选课程的成绩。一个学期的教务闭环被切成三个视角这是这类项目最值得学习的地方。3.3 一次登录请求走完全程从 login.jsp 表单触发到 admin/index.jsp 渲染完成完整链路是这样的浏览器 POST/system/login→ web.xml 或 WebServlet 注解匹配到 LoginServlet → doPost 里取参数 → UserDao.findByUsernameAndPwd 查库 → 校验通过后 session 里放 user 和 role → sendRedirect 到/system/admin/index.jsp→ 浏览器发起第二次 GET 请求 → LoginFilter 检查 session放行 → JSP 页面用 EL 表达式渲染数据。这条链路里有两个关键点。第一校验失败时用的是 forward 转发到 login.jsp 并带一个 msg 参数校验成功时用的是 sendRedirect 重定向区别在于刷新页面时不会重复提交表单。第二LoginFilter 拦截的是所有带/system前缀的请求但静态资源和登录页本身必须放行否则会出现“css 加载不出来”或者“登录页自己把自己拦了”的怪问题。3.4 页面为什么全是 JSP JSTL打开任意一个列表页看到的不是满屏 Java 代码而是c:forEach和${}这种标签。这是利用 JSTL 替代传统 scriptlet 的做法好处是页面里没有 Java 代码块前端样式和后端逻辑能分开改。列表页的循环输出基本都长这样c:forEach items${studentList} vars tr td${s.stuId}/td td${s.name}/td td${s.className}/td /tr /c:forEach对应的 servlet 里只需要request.setAttribute(studentList, list)就能把数据送到页面上。这里要注意的是如果你在页面里看到${studentList}取不到值十有八九是 setAttribute 的 key 和页面里写的 key 不一致这种错不报异常但页面就是空白排起来比较费眼。4. 核心代码逐段拆解登录鉴权、登录过滤、选课事务与分页4.1 LoginServlet校验、session、重定向三步走登录是整个系统的大门处理逻辑也是最标准的。先看这段WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPwd(username, password); if (user null) { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } HttpSession session request.getSession(); session.setAttribute(user, user); session.setAttribute(role, user.getRole()); // 1管理员 2教师 3学生 if (user.getRole() 1) { response.sendRedirect(request.getContextPath() /admin/index.jsp); } else if (user.getRole() 2) { response.sendRedirect(request.getContextPath() /teacher/index.jsp); } else { response.sendRedirect(request.getContextPath() /student/index.jsp); } } }逻辑说明这段代码先做字符编码处理再取表单参数查库判断账号密码失败转发回登录页并带提示成功则把 user 对象存进 session最后按角色重定向到不同首页。这里两个细节值得注意失败用 forward能保留 request 里的 msg 参数并在登录页显示成功用 sendRedirect避免刷新页面时表单重复提交。参数说明username 对管理员来说是管理员账号对学生是学号对教师是工号三者都查 t_user 表role 是数字1、2、3 分别对应管理员、教师、学生跳转路径和这个数字一一对应。如果后续要加角色只需要改这里的分支判断。对应的 UserDao 方法长这样注意用了 PreparedStatement 而不是字符串拼接 SQLpublic User findByUsernameAndPwd(String username, String password) { String sql SELECT * FROM t_user WHERE username? AND password?; try (Connection conn DBUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setPassword(rs.getString(password)); u.setRole(rs.getInt(role)); u.setRealname(rs.getString(realname)); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这段是 JDBC 的标准写法。用 PreparedStatement 的好处是 SQL 注入的隐患小而且?占位符的传参顺序不会因为字符串拼接出错。try-with-resources 写在 JDK7 之后都能用连接会在代码块结束后自动关闭不用手动写 finally close。4.2 LoginFilter静态资源放行与登录拦截登录之后的每一次页面访问都要经过这道过滤器。它解决的是“没登录能不能访问内部页面”的问题WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) res; String uri request.getRequestURI(); // 登录接口、登录页、静态资源直接放行 if (uri.endsWith(/login) || uri.endsWith(login.jsp) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/)) { chain.doFilter(request, response); return; } HttpSession session request.getSession(false); Object user (session null) ? null : session.getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }逻辑说明过滤器先判断请求的 URI 是否属于放行名单登录页和静态资源必须放行否则会出现“没登录访问登录页也被拦截”的死循环然后取 session 判断 user 是否存在不存在就重定向回登录页。这里用request.getSession(false)有个讲究false 表示如果没有 session 就返回 null 而不是自动创建一个避免了“没登录反而生成一堆空 session”的浪费。注意点如果你以后给项目加了图片上传功能记得在上传图片的目录也加进放行名单否则img标签请求图片会被拦截页面上全是裂图还不报错。这种问题最坑因为 Tomcat 日志里干干净净只能靠浏览器 F12 看响应状态码才能发现。4.3 选课服务容量判断与事务回滚避免并发超选选课是教务系统里最核心的业务因为它涉及两条 SQL 操作一条更新课程已选人数一条插入选课记录。这两条必须在一个事务里不然会出现“名额加了但记录没插上”的脏数据。更关键的是容量判断老版本常见的写法是先用 SELECT 查人数再判断是否小于容量这个写法在高并发下会超选。这个项目里的写法是直接把判断放进 UPDATEpublic boolean electiveCourse(int stuId, int courseId) { // selected capacity 是容量判断也是行级锁 String lockSql UPDATE t_course SET selected selected 1 WHERE course_id? AND selected capacity; String insertSql INSERT INTO t_elective(stu_id, course_id) VALUES(?, ?); Connection conn null; try { conn DBUtils.getConnection(); conn.setAutoCommit(false); // 开启事务 PreparedStatement ps1 conn.prepareStatement(lockSql); ps1.setInt(1, courseId); int rows ps1.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 课程不存在或已选满 } PreparedStatement ps2 conn.prepareStatement(insertSql); ps2.setInt(1, stuId); ps2.setInt(2, courseId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { DBUtils.close(conn); } }逻辑说明核心在这一行UPDATE t_course SET selected selected 1 WHERE course_id? AND selected capacity。MySQL 的 InnoDB 引擎执行 UPDATE 时会对命中的行加行锁selected capacity条件如果不成立影响行数是 0事务直接回滚。这比“先 select 再判断”方式安全得多两个学生同时选最后一门课只有一个能 update 成功另一个影响行数为 0 直接失败。参数说明stuId 是 t_user 里学生的 id 或学号courseId 是课程 id返回的 boolean 会直接被 servlet 拿到true 跳转成功页false 回到选课列表并提示“该课程已满”。事务的三个关键动作是 setAutoCommit(false)、commit、rollback漏掉任何一个都会出问题特别是忘记 rollback出现异常时连接归还连接池但事务没结束下一个拿到这个连接的人会看到一堆奇怪的行锁。4.4 成绩录入与分页工具两个通用件成绩录入用的是批量提交思路。教师在页面上填一个班的学生成绩提交过来的是一个数组servlet 里这样取String[] stuIds request.getParameterValues(stuId); String[] scores request.getParameterValues(score); for (int i 0; i stuIds.length; i) { Score score new Score(); score.setStuId(Integer.parseInt(stuIds[i])); score.setScore(Double.parseDouble(scores[i])); scoreDao.insert(score); }逻辑说明getParameterValues取到的是同名表单元素的数组HTML 里多个input namestuId就会得到多个值。遍历数组逐条插入注意数组长度要一致否则会数组越界。如果课程有 50 个学生这一页表单就有 50 组输入框提交时一次性写入成绩表。分页工具类是最容易复用到别的页面的代码public class PageUtils { public static final int PAGE_SIZE 5; public static int getTotalPage(int totalCount) { // 向上取整余数不足一页的按一页算 return (totalCount PAGE_SIZE - 1) / PAGE_SIZE; } public static int getOffset(int currentPage) { // MySQL limit 的起点第一页 offset 是 0 return (currentPage - 1) * PAGE_SIZE; } }调用方在 servlet 里算出 totalPage 和 offset然后把当前页数据和页码一起塞进 requestint currentPage Integer.parseInt(request.getParameter(currentPage)); int totalCount courseDao.count(); int offset PageUtils.getOffset(currentPage); ListCourse list courseDao.findPage(offset, PageUtils.PAGE_SIZE); request.setAttribute(list, list); request.setAttribute(totalPage, PageUtils.getTotalPage(totalCount)); request.setAttribute(currentPage, currentPage);分页的 SQL 在 dao 里就是一句LIMIT ?, ?第一个参数是 offset第二个是每页条数。这里最容易踩的坑是 currentPage 参数没传或传了非数字页面直接 500所以在实际项目里我会给参数解析包一层 try-catch取不到就默认 1。这个项目里没有加这层保护你改的时候可以顺手补上。5. 部署避坑与常见问题排查驱动、乱码、404、版本代差四道坎5.1 访问首页 404先看 Application context再看 war 包名现象Tomcat 启动没有任何报错但访问http://localhost:8080/login.jsp直接 404。原因IDEA 里部署时 Application context 设置的不是/或者 war 包名和访问路径不一致。解决IDEA 的 Run Configuration 里找到 Deployment把 Application context 改成/system然后访问http://localhost:8080/system/login.jsp。如果是直接丢 war 包到 webapps访问路径必须带 war 包名不含 .war比如school.war就要访问/school/login.jsp。这个错不报异常新手最容易在 Tomcat 日志没错误但页面 404 这一步卡住。5.2 ClassNotFoundException驱动类名和连接串必须匹配现象项目启动后访问任意页面控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因mysql-connector-java 的 jar 没进 WEB-INF/lib或者用的是 MySQL 8.x 驱动但代码里还在 Class.forName 老驱动名。解决如果是 5.x 驱动确认 jar 在 WEB-INF/lib 或 Maven 依赖里驱动类名写com.mysql.jdbc.DriverURL 写jdbc:mysql://localhost:3306/school?characterEncodingutf8如果数据库是 MySQL 8.x驱动类名要换com.mysql.cj.jdbc.DriverURL 再加serverTimezoneAsia/Shanghai。在 pom 里我一般用 5.1.49 版本配合这个项目省心。5.3 中文乱码三处编码必须对齐现象登录后页面上所有中文都是问号或者往数据库里插的中文全乱。原因JSP 文件编码、请求编码、数据库连接编码三个地方不一致。解决JSP 第一行确认是% page contentTypetext/html;charsetUTF-8 %在 Filter 或每个 doPost 开头加request.setCharacterEncoding(UTF-8)数据库连接串上加characterEncodingutf8创建表时用DEFAULT CHARSETutf8。这三处只要有一处漏了中文就保不住。最隐蔽的是数据库表本身建成了 latin1 编码这种情况页面怎么改都没用只能重建表或用ALTER TABLE t_user CONVERT TO CHARACTER SET utf8补救。5.4 Tomcat 10 的 javax.servlet 兼容坑现象项目在 Tomcat 8.5 跑得好好的换了 Tomcat 10 启动直接报NoClassDefFoundError: javax/servlet/ServletException或者类加载异常。原因Tomcat 10 把javax.servlet命名空间整体迁移成了jakarta.servlet老项目里所有 import javax.servlet 的类都找不到包。解决最省事的是换回 Tomcat 8.5 或 Tomcat 9如果一定要用 Tomcat 10需要全局搜索替换javax.servlet为jakarta.servletJSP 和 web.xml 里的配置也要跟着改。我的建议是别折腾老项目配老 Tomcat这不是胆小是把时间花在业务上。5.5 排查方法论500 页面前先看 catalina 日志的 StackTrace现象页面显示 500但页面上的报错信息看不懂或者干脆是空白页。原因JSP 编译异常或 servlet 运行时异常真实错误堆栈在 Tomcat 的logs/catalina.out或 IDEA 控制台里浏览器页面上只显示一段简略描述。解决打开 Tomcat 日志目录按时间找到最新一条SEVERE或Exception从第一行Caused by看起大多数问题要么是 SQL 语法错误要么是空指针要么是类找不到。定位到具体类名和行号后再去源码里看那一段逻辑。这个习惯我每次跑老项目都会强制走一遍先看日志再改代码绝不在页面上瞎猜。6. 二次开发从零加一个公告管理模块的完整改造流程6.1 表结构和实体类给系统加公告模块第一步是建表CREATE TABLE t_notice ( notice_id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8;对应实体类 Notice.java 只需要四个字段注意字段名和表列名保持驼峰对应方便后面写 DAO 时手写映射不晕。6.2 数据层和控制器DAO 里加两个方法findAll()返回公告列表按时间倒序insert(Notice)用于管理员发布新公告。Servlet 用一个 NoticeServlet 处理列表和新增两个动作通过隐藏字段或者请求路径区分。最省事的做法是复用现有 servlet 的跳转模式取到列表后 setAttribute 到 request再 forward 到目标 JSP。6.3 首页接入在 index.jsp 的公告区域用 JSTL 循环输出ul c:forEach items${noticeList} varn li${n.title} fmt:formatDate value${n.createTime} patternyyyy-MM-dd //li /c:forEach /ul注意首页 index.jsp 是直接访问的不会经过 NoticeServlet所以要在对应的入口页加一个request.setAttribute(noticeList, noticeDao.findAll())或者把首页改成一个轻量 servlet 转发。我一般会选后者把首页数据的准备统一放在一个入口 servlet 里而不是散落在 JSP 中用自定义标签调用 DAO那样会让页面和数据库耦合。6.4 我的改造习惯与验证路径这套项目我前后拆过两遍。第一遍图快用了最新的 Tomcat 和 MySQL翻车之后才意识到 JavaWeb 老项目的稳定组合是有道理的。从那以后我每次拿到新的源码包都会先做三件事检查 JDK 与 Tomcat 版本、统一三处 UTF-8、把 SQL 过一遍再启动。改项目时也习惯每次只动一条链路改表就补 SQL改后端就补接口测试改页面就刷新看数据。这个公告模块的改动虽小但登录、查库、渲染、权限过滤全链路都涉及了能完整跑通一遍你对这套代码结构才算真正上手。希望帮到你。本文还有配套的精品资源点击获取