☰
JSP+Servlet+MySQL教务管理系统:老技术栈部署避坑与二次开发实战
2026/10/7 12:46:15 网站建设 项目流程

简介:一份基于JSP、Servlet与MySQL构建的教务管理系统完整源码,面向Java Web初学者、毕业设计选题学生及需要快速搭建后台管理项目的开发者。系统涵盖学生、教师、课程与考试安排等核心模块,演示了从页面展示、请求处理到数据库交互的典型三层结构,并体现MVC设计思想。压缩包共530个文件,包含24个Java源文件、10个JSP页面、24个编译后class文件,以及配套的jar依赖、properties配置、SQL建表脚本和大量gif/jpg界面素材,整体约9.32MB,资源包内目录层级清晰,便于按模块检索查阅。已有719人下载学习。通过研究源码可掌握JSP标签库与EL表达式的使用、Servlet请求分发与业务逻辑编写、JDBC及连接池的调用方式;同时参考页面素材与数据库设计,能从登录认证、信息增删改查出发,快速扩展为毕业设计或实训项目。

1. JSP+Servlet+MySQL 教务管理系统:老技术栈,为什么还值得拆一遍

如果你在找 jsp 源码做课程设计、毕设参考,或者想快速搭一套管理后台练手,这份 JSP+Servlet+MySQL 的教务管理系统是典型得不能再典型的 Java Web 项目。它没有前后端分离、没有微服务,就是一个老老实实的三层结构:JSP 负责展示页面,Servlet 负责接请求和跳转,MySQL 负责存数。从压缩包里的类名能反推出来,它已经具备用户登录、用户增删改查、文件上传和文件管理这几条完整闭环,而且代码量不大,适合新手完整读一遍。但对打算直接部署跑起来的熟手来说,这里面有几个隐藏的老旧环境坑,我下面会按模块、启动、踩坑、改造的顺序一次性说清楚。

2. 从 class 文件名反推架构:MVC 三层是怎么落的

2.1 先看包结构:Action、Manager、Dao、Form 就是一张架构图

解压后在 Java 目录里能看到一堆以 Action、Manager、Dao、Form 结尾的类名,别小看这些名字,它就是整个项目的骨架:

  • LoginAction、AddUserAction、ModifyUserAction、FindUserAction:控制层,处理登录、新增用户、修改用户、查询用户请求。
  • UserManager、FileManager:业务层,干校验、组合逻辑的活。
  • UserDao4MySqlImpl、FileDao3MySqlImpl:数据访问层,并且名字里的4MySql暴露了这是面向 MySQL 的实现。
  • UserActionForm:表单数据封装对象,Struts 1 时代遗留的命名习惯,在这里本质上就是一个 POJO。
  • FileUploadAction:单独拎出来的文件上传入口。

这个项目的技术栈虽然老,但分层确实没偷懒。常见的做法是请求先进 JSP 页面,表单提交给 Action(Servlet),Action 调 Manager 做业务判断,Manager 再调 Dao 去查数据库,最后把结果塞回 request 或 session,转发到另一个 JSP 渲染。即使你后续要换 Spring MVC,这个分层思路也是完全通用的。

如果你把Usermanager、FileManager单独拿出来看,会发现它们其实没有跟 HttpServletRequest 打交道,参数都是普通 Java 类型。这意味着业务逻辑可以脱离 Web 容器测试,不至于必须启一个 Tomcat 才能跑。老项目能做到这个程度,已经算是有意识地做了解耦。

2.2 登录链路:LoginAction 到 UserDao4MySqlImpl 的一次完整请求

拿登录举例,这是我在这类项目里最常看的入口。用户在login.jsp输入用户名密码,表单提交到 LoginAction,这段逻辑的典型写法是这样的:

// LoginAction.java 典型实现片段,用于处理登录请求 @WebServlet("/login") public class LoginAction extends HttpServlet { private UserManager userManager = new UserManager(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 参数从请求里拿出来 String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 调业务层做登录校验 User user = userManager.login(username, password); if (user != null) { // 3. 登录成功:写入 session,重定向到主页面 request.getSession().setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/main.jsp"); } else { // 4. 失败:回到登录页并提示错误,用 forward 保留参数 request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }

这段代码有几个细节值得注意。sendRedirect和forward的区别是:重定向会发起第二次请求,地址栏变成 main.jsp;转发是服务器内部跳转,地址栏不变,request 里的属性还能继续用。老项目里很多人分不清这两个方法,导致登录成功但页面不对,或者错误提示传不过去。

UserManager.login()内部才是真正干活的。常见实现是先校验用户是否存在,再比对密码,最后返回一个 User 对象。密码校验一般用 MD5 或者 SHA 处理后再和库里的值比对,但我看过不少旧项目是明文存库的,如果是拿来学习没问题,真要上线一定要改加密,这一点后面在避坑章我再展开。

2.3 DAO 层与数据库交互:UserDao4MySqlImpl 在做什么

UserDao4MySqlImpl这个命名意味着接口是UserDao,实现是针对 MySQL 的。这个项目里数据访问层的写法,基本上就是 JDBC 的几种经典姿势:获取连接、预编译 SQL、执行查询、遍历结果集、关闭资源。

看一段代表性实现:

// UserDao4MySqlImpl.java 典型片段:按用户名和密码查用户 public class UserDao4MySqlImpl implements UserDao { private DataSource dataSource; public UserDao4MySqlImpl() { // 常见做法:从连接池拿数据源,避免每次 new Connection dataSource = DataSourceFactory.getInstance().getDataSource(); } @Override public User findByUsernameAndPassword(String username, String password) { // 1. 写 SQL,用 ? 占位符而不是字符串拼接 String sql = "SELECT id, username, real_name, role FROM t_user WHERE username = ? AND password = ?"; // 2. try-with-resources 自动关闭连接、语句、结果集 try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { // 3. 参数从 1 开始编号,按顺序绑定 ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRealName(rs.getString("real_name")); user.setRole(rs.getString("role")); return user; } } } catch (SQLException e) { // 日志记录异常,而不是只打印到控制台 logger.error("按用户名密码查询用户失败", e); } return null; } }

重点看PreparedStatement的占位符用法。老项目里最血腥的坑就是把参数直接拼进 SQL 字符串,用户名传个' OR '1'='1就能绕过登录。PreparedStatement 除了防 SQL 注入,还有个好处是 SQL 预编译后可以复用,循环批量操作时性能比 Statement 高出一截。

这个 DAO 里用到的连接池也很关键。如果项目里用的是 C3P0 或 DBCP,配置文件里通常有maxPoolSize、initialPoolSize这些参数。你看到Connection conn = dataSource.getConnection()而不是DriverManager.getConnection(),说明作者已经处理了连接复用的问题。如果日志里频繁出现Too many connections,优先检查连接池有没有正确关闭连接。

另外FileDao3MySqlImpl这个名字里的数字 3,大概率是某个版本的遗留命名,不影响理解。它的职责和 UserDao 是对称的:往文件表里插记录、按条件查文件列表、删除文件记录。这种对称性是老项目里非常难得的优点,你照着 UserDao 的套路去读 FileDao,半小时就能把整个数据层摸完。

3. 本地跑起来:JDK8 + Tomcat8 + MySQL5.7 环境搭建

3.1 环境选型:老项目别乱配新环境

这种 JSP+Servlet+MySQL 的旧项目,最怕一上来就用最新版本的环境,折腾时间比看代码还多。我的建议是直接锁定一套经典搭配,让项目先跑起来再说。

组件推荐版本选择理由
JDK1.8老代码在 JDK9+ 上容易遇到模块化问题和编译警告,8 最省心
Tomcat8.5 或 9.0这两个版本还是javax.servlet命名空间,和项目代码对得上
MySQL5.7默认认证插件是mysql_native_password,和旧驱动兼容最好
IDEIntelliJ IDEA 或 Eclipse按 Web 项目或 Maven 项目导入均可

如果你的机器上已经装了 MySQL 8.0,也不是完全不能用,但驱动版本和认证方式要处理。最稳的办法是去 MySQL 官网下载 5.7 的 zip 包本地解压,或者用 Docker 起一个 5.7 容器。后面避坑章我会细讲 8.0 会踩到哪些雷,这里先说结论:能用 5.7 就别碰 8.0。

Tomcat 一定不要用 10.x,因为从 Tomcat 10 开始,Servlet 规范从javax.servlet迁移到了jakarta.servlet,老项目里所有import javax.servlet.*的代码会直接编译失败或者运行时报NoClassDefFoundError。这不是改一行代码能解决的,涉及整个包名体系,所以老老实实用 Tomcat 9。

3.2 初始化数据库与修改连接配置

解压项目后,在根目录或sql目录下一般能找到一个.sql脚本,类似edu.sql或school.sql。先建库再导入,用命令行操作最直观:

# 登录 MySQL,root 密码换成你自己的 mysql -uroot -p # 在 MySQL 里创建数据库并指定字符集 CREATE DATABASE edu DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; exit;

然后在终端执行导入:

# 把 SQL 脚本导入到 edu 库 mysql -uroot -p edu < edu.sql # 如果脚本里没有建库语句,需要先建库;有的话直接 -p 回车即可

导入完建议查一下表和数据,确认脚本没出问题:

USE edu; SHOW TABLES; SELECT * FROM t_user; SELECT * FROM t_file;

正常情况下能看到用户表、文件表这些核心表。如果SHOW TABLES是空的,说明脚本里可能带了DROP TABLE IF EXISTS或者建库语句被注释掉了,检查一下脚本前几行。

接下来改数据库连接配置。这类项目一般有一个jdbc.properties或db.properties,也可能是DBUtil.java里硬编码,打开后改成你自己的库名、用户名、密码:

# jdbc.properties 典型配置 jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/edu?useUnicode=true&characterEncoding=utf8&useSSL=false jdbc.username=root jdbc.password=123456

这里有两个关键参数。characterEncoding=utf8保证数据库连接时中文不乱码;useSSL=false是为了避免老驱动连接 MySQL5.7 时报 SSL 警告,省事。useUnicode=true配合characterEncoding使用,是 Java Web 项目里约定俗成的写法。

3.3 部署到 Tomcat 与验证访问

把这个项目部署到 Tomcat,有几种做法。最简单粗暴的是把解压后的整个项目目录复制到 Tomcat 的webapps/下,然后启动 Tomcat。如果你用 IDEA,直接配置一个 Tomcat Server,Deployment 里加war exploded,点运行即可,这里演示命令行方式:

# 把项目复制到 webapps 下,重命名为 edu cp -r edu_project $CATALINA_HOME/webapps/edu # 启动 Tomcat $CATALINA_HOME/bin/startup.sh # 实时查看日志,确认没有异常 tail -f $CATALINA_HOME/logs/catalina.out

访问地址是http://localhost:8080/edu/login.jsp(项目名不同就把路径换成你的实际名字)。如果看到登录页,说明项目已经能访问了。如果 404,先确认项目目录名是否正确、webapps/edu下有没有WEB-INF/web.xml;如果 500,看catalina.out里的堆栈信息,一般是数据库连接失败或驱动类找不到。

Windows 环境下启动用的是startup.bat,日志在logs目录下的stdout.log和catalina.*.log。可以先用默认账号密码试登录,大部分这种项目的默认账号写在t_user表里,直接SELECT * FROM t_user;就能看到。如果表里全是密文看不出密码,翻一下 SQL 脚本,通常有 INSERT 语句带初始密码。

4. 避坑指南:这个项目最常见的几个翻车点

4.1 MySQL 8.0 认证方式:Access denied 与 allowPublicKeyRetrieval

现象:用 JDBC 连 MySQL 8.0 时报Public Key Retrieval is not allowed,或者直接Access denied for user 'root'@'localhost'。

原因:MySQL 8.0 把默认认证插件改成了caching_sha2_password,而老项目里的mysql-connector-java5.x 驱动默认只认mysql_native_password。驱动版本落后,认证协议对接不上,就出现上面两种报错。

解决:两个办法选一个。第一个办法是把数据库用户的认证方式改回老格式:

-- 在 MySQL 8.0 中执行,把 root 的认证改回 mysql_native_password ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;

第二个办法是升级驱动到 8.0.x,并在 JDBC URL 里加两个参数:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/edu?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

注意驱动类也从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver。serverTimezone不设的话有时会报时区相关的异常。总之一句话:老项目配老数据库,新项目配新数据库,交叉使用必踩坑。

4.2 Tomcat 10 的 Jakarta 改名:NoClassDefFoundError 的真相

现象:项目部署到 Tomcat 10.1 后,启动时或访问页面时报NoClassDefFoundError: javax/servlet/ServletException,或者ClassNotFoundException。

原因:从 Tomcat 10 开始,Servlet API 的包名从javax.servlet改成了jakarta.servlet。老项目代码里全部是javax.servlet开头的 import,放到 Tomcat 10 里找不着对应的类,直接翻车。

解决:不用纠结,直接把 Tomcat 换成 9.0.x 或者 8.5.x。这是最省时间的做法。如果你非要用 Tomcat 10,需要把所有import javax.servlet批量改成import jakarta.servlet,改完还不一定一次过,因为某些第三方库内部也引用了javax.servlet,那才是无底洞。血的教训:老项目永远配老容器。

4.3 中文乱码:从 Tomcat、JDBC URL 到 JSP 一个都不能少

现象:登录表单输入中文用户名,数据库里存进去变成???,或者页面显示乱码。

原因:乱码往往是连锁反应。第一环是 HTTP 请求没指定编码,Tomcat 默认按 ISO-8859-1 解码;第二环是 JDBC URL 没带characterEncoding=utf8,数据到 MySQL 时转码失败;第三环是 JSP 页面本身的pageEncoding没设置。

解决:按顺序排查三处。先改 Tomcat 的server.xml,给 Connector 加 URIEncoding:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />

然后在 web.xml 里加一个编码过滤器,或者在每个 Servlet 的doPost开头加一行:

request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8");

最后确认每张 JSP 页面头部是:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>

三层都对了,中文基本不会再翻车。我还见过一种情况是数据库表本身是 latin1 编码,那是建库时的问题,对表单独执行ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4就能救回来。

4.4 文件上传后查不到文件:别只信数据库记录

现象:文件上传提示成功,数据库里也有记录,但点预览或下载时 404,或者磁盘上根本找不到文件。

原因:FileUploadAction把文件保存到了相对路径,比如upload/,但 Tomcat 的工作目录不是项目部署目录,相对路径解析到了别的地方。另一种更隐蔽的情况是上传目录本身不存在,代码里没有做mkdirs(),保存时抛异常被吞掉,界面还提示成功。

解决:在文件上传代码里,保存路径改成绝对路径,并且保存前确保目录存在:

// FileUploadAction.java 中处理文件保存的片段 // 常见做法:读取配置或固定路径,这里以项目下落为例 String uploadBasePath = request.getServletContext().getRealPath("/upload"); File uploadDir = new File(uploadBasePath); if (!uploadDir.exists()) { uploadDir.mkdirs(); // 目录不存在就创建,防止 FileNotFoundException } String fileName = System.currentTimeMillis() + "_" + item.getName(); File savedFile = new File(uploadDir, fileName); item.write(savedFile);

注意getRealPath("/upload")拿到的是当前 Web 应用部署后的真实磁盘路径,这样存进数据库的相对路径就能被后续请求正确解析。如果你把应用打成 WAR 包部署,Tomcat 解压后的目录可能每次重启会变,这种情况更建议配置一个绝对路径存储到固定目录,比如 Linux 下的/data/edu_upload/,然后数据库里直接存完整路径。这个问题在文件管理类系统里特别常见,我用这套姿势改完,基本再没遇到过文件上传后找不到的情况。

5. 二次开发:从跑起来到改得动

5.1 新增课程管理模块的完整链路

老项目的代码结构虽然不够新潮,但好在套路统一,你完全可以把用户模块复制一份改成课程模块。我一般会按这个顺序来:先建t_course表,再写Course实体,接着写CourseDao和CourseDao4MySqlImpl,然后写CourseManager,再写控制层AddCourseAction,最后加 JSP 页面和菜单链接。

控制层的写法直接套用 AddUserAction 的模式,任何模块的添加逻辑都长一个样:

// AddCourseAction.java 片段:新增课程入口 @WebServlet("/courseAdd") public class AddCourseAction extends HttpServlet { private CourseManager courseManager = new CourseManager(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 防止中文乱码,提交前前端表单也要是 utf-8 request.setCharacterEncoding("UTF-8"); String courseName = request.getParameter("courseName"); String credit = request.getParameter("credit"); if (courseName == null || courseName.trim().isEmpty()) { request.setAttribute("error", "课程名不能为空"); request.getRequestDispatcher("/courseAdd.jsp").forward(request, response); return; } Course course = new Course(); course.setCourseName(courseName.trim()); course.setCredit(Double.parseDouble(credit)); if (courseManager.addCourse(course)) { response.sendRedirect("courseList"); // 添加成功后走查询列表 } else { request.setAttribute("error", "添加失败"); request.getRequestDispatcher("/courseAdd.jsp").forward(request, response); } } }

核心技术点不在 CRUD 本身,而在“复用已有模式”。把 User 相关的文件全部浏览一遍,课程模块照着写,大概率一次通过。老项目的好处就是结构高度相似,你只需要注意把表名、字段名、跳转路径全部替换干净。

5.2 让 FileUploadAction 支持图片预览

文件管理模块目前大概率只提供下载链接,不够直观。一个低成本的改造方案是在查询文件列表时,判断文件扩展名,如果是jpg、png、gif,就生成<img>标签直接预览,否则还走下载。判断逻辑很简单:

// FileManager.java 中追加一个方法:根据扩展名决定展示方式 public boolean isImage(String fileName) { String lower = fileName.toLowerCase(); return lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".png") || lower.endsWith(".gif") || lower.endsWith(".bmp"); }

然后在 JSP 的列表页面对应位置加一个分支,图片文件用img标签输出,其他文件保持原有的下载链接。这个小改动能让整个文件模块的实用性强很多,而且不需要动数据库表结构。

另外还有一个值得做的进阶点:把密码加密从明文改成 MD5 或加盐哈希。老项目里最危险的就是密码明文存在t_user表里。改造时先写一个工具类生成哈希值,再在 AddUserAction 和 LoginAction 里调用它,注意历史数据需要做一次批量转换。这个改造不复杂,但对系统的安全性提升立竿见影。

说实话,这种老项目我拆过不是一个两个了,现在养成的习惯是拿到压缩包先不去碰 IDE,先把 SQL 脚本、web.xml、jdbc.properties这三个文件过一遍,再启动配置文件改成本机环境,然后才部署。这个顺序帮我绕过了至少一半的环境坑。希望帮到你,动手时慢一点,踩出来的坑才值钱。

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

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

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

立即咨询