☰
Java Web毕业设计选题系统:Servlet+JSP+MySQL三层角色实战
2026/10/7 18:30:33 网站建设 项目流程

简介:这是一套基于Java Web技术栈开发的毕业设计选题管理系统,面向高校计算机类专业师生,解决毕业设计课题申报、分配与过程管理中的多角色协同难题。系统采用JSP+Servlet+MySQL架构,部署于Tomcat环境,完整实现管理员(学生/教师/课题管理及系统维护)、教师(个人信息维护、课题发布与查看)和学生(信息修改、选题浏览与提交)三类角色功能,适合作为课程设计、毕设开发或Java Web入门实战项目。资源包共221个文件,含98个JSP页面(前端交互)、58个GIF图标(界面资源)、24个Class字节码(业务逻辑)、12个Java源码(含TopicManager、TeacherManager等核心管理类)、7个XML配置文件及SQL初始化脚本等,整体压缩后仅5.57MB,结构清晰、模块划分明确。已有2416人学习下载,提供可直接运行的完整工程、数据库脚本及典型管理类代码实现,便于快速理解MVC分层设计与角色权限控制逻辑。

1. 这不是又一个“Java Web 毕业设计模板”,而是一套能真跑起来、真改得动、真交得出去的三层角色选题系统

你手头那份被导师打回来三次的毕业设计,是不是还在用「用户登录→跳转首页→空白页面」的半成品硬撑?是不是每次改个 JSP 表单就报HTTP Status 500 – java.lang.NullPointerException,查日志只看到at org.apache.jsp.index_jsp._jspService(index_jsp.java:87)这种黑匣子报错?别急——这套 Java+JSP+MySQL 实现的「毕业设计选题系统」,不是网上泛滥的空壳 demo,而是经过真实角色权限隔离、数据库事务校验、文件上传路径固化、多层 Manager 类解耦的可交付工程。它不依赖 Spring Boot 自动装配,纯 Servlet+JSP+JDBC 手写逻辑,适合答辩现场被老师追问「你怎么保证学生不能重复选同一个课题」「教师删课题时怎么同步清空已选记录」时,你能立刻打开TopicManager.class指着deleteTopicWithStudents()方法说清楚事务边界。新手能照着部署跑通,老手能快速定位MsgManager.class里消息未读状态更新的竞态条件。如果你正卡在「功能堆砌但逻辑断裂」「数据库建了但增删改查总丢数据」「JSP 页面传参总为空」这三座大山之间,这套资源就是你最后一块能焊实的钢板。


2. 环境筑基:Tomcat 9 + MySQL 5.7 的最小可行组合与版本陷阱

这套系统不是为 Tomcat 10 或 MySQL 8 设计的。它吃的是 Servlet 3.1 和 JDBC 4.2 的兼容性红利,一旦版本越界,轻则ClassNotFoundException: javax.servlet.http.HttpServlet,重则 MySQL 驱动加载失败直接白屏。我拆包验证过,所有.class文件编译目标字节码是52.0(对应 JDK 8),这意味着你必须用 JDK 8u202 或更高但低于 JDK 11 的版本。下面分步带你搭出零干扰环境。

2.1 Tomcat 9.0.83:为什么不是 10.x?Servlet API 断层在哪?

Tomcat 10 引入了 Jakarta EE 9 命名空间迁移,所有javax.*包全换成jakarta.*。而本系统所有 JSP 和 Servlet 类都硬编码import javax.servlet.*,强行升级会触发 20+ 处编译错误。Tomcat 9.0.83 是最后一个支持javax.*且自带完整 JSP 编译器的稳定版。下载地址必须认准 Apache 官网归档页(archive.apache.org/dist/tomcat/tomcat-9/v9.0.83/),避免镜像站混入修改版。

# 解压后立即做两件事: tar -xzf apache-tomcat-9.0.83.tar.gz cd apache-tomcat-9.0.83/bin chmod +x *.sh ./startup.sh # 启动后访问 http://localhost:8080,确认出现 Tomcat 默认首页

提示:启动失败常见原因是JAVA_HOME未指向 JDK 8。执行echo $JAVA_HOME确认路径,若为/usr/lib/jvm/java-11-openjdk-amd64,需先卸载 JDK 11,再安装openjdk-8-jdk(Ubuntu)或从 Oracle 官网下载 JDK 8u202。

2.2 MySQL 5.7.44:驱动、字符集、SQL_MODE 三件套配置

系统 SQL 文件夹下的topic_db.sql使用了utf8mb4字符集和STRICT_TRANS_TABLES模式。MySQL 8 默认开启ONLY_FULL_GROUP_BY,会导致SELECT * FROM student GROUP BY major类查询直接报错——而本系统StudentManager.class里恰有这类统计语句。必须降级到 5.7.44 并手动加固配置:

# /etc/mysql/my.cnf 中 [mysqld] 段落追加: [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci sql_mode = STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
-- 创建数据库时强制指定字符集 CREATE DATABASE topic_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 导入前确认连接字符集 SET NAMES utf8mb4; SOURCE /path/to/sql/topic_db.sql;

注意:topic_db.sql文件末尾有INSERT INTO admin (username, password) VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e');—— 这是 MD5 加密的123456,密码明文存储是毕业设计常见妥协,但务必在答辩前用UPDATE admin SET password=MD5('新密码') WHERE username='admin';替换。

2.3 JDBC 驱动:mysql-connector-java-5.1.47.jar 的不可替代性

系统WEB-INF/lib/下自带mysql-connector-java-5.1.47.jar,这是专为 MySQL 5.7 优化的最后稳定版。若替换为 8.x 驱动,DriverManager.getConnection("jdbc:mysql://localhost:3306/topic_db?useSSL=false", ...)会因 URL 参数变更(如useSSL→sslMode=DISABLED)直接抛异常。绝对不要删除或覆盖这个 jar。验证方式:启动 Tomcat 后查看catalina.out日志,应出现Loading classcom.mysql.jdbc.Driver'. This is deprecated. The new driver class iscom.mysql.cj.jdbc.Driver—— 这条警告可忽略,只要后续无java.sql.SQLException: No suitable driver found即可。


3. 代码结构解剖:从 .class 文件反推三层 Manager 的职责边界与调用链

项目正文列出的.class文件不是随意堆砌,而是按「角色-功能」严格分层的。TopicManager.class不处理学生信息,StudentManager.class不碰课题状态——这种解耦让修改「教师只能删自己发布的课题」逻辑时,只需动TeacherManager.class里的deleteOwnTopic(),不会波及学生选题流程。我们用javap -c TopicManager.class反编译关键方法,还原真实调用关系。

3.1 TopicManager.class:课题生命周期的四道关卡

反编译发现其核心方法addTopic(TopicBean tb)包含完整事务控制:

public void addTopic(TopicBean tb) { Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); // 复用连接池?不,这里是直连! conn.setAutoCommit(false); // 关键:手动开启事务 String sql = "INSERT INTO topic (title, teacher_id, description, status) VALUES (?, ?, ?, ?)"; ps = conn.prepareStatement(sql); ps.setString(1, tb.getTitle()); ps.setInt(2, tb.getTeacherId()); ps.setString(3, tb.getDescription()); ps.setInt(4, 1); // status=1 表示待审核 ps.executeUpdate(); conn.commit(); // 成功才提交 } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ex) {} throw new RuntimeException(e); } finally { DBUtil.close(ps, conn); } }

逻辑说明:status=1是审核中状态,管理员后台AdminServlet会调用TopicManager.approveTopic(int id)将其改为status=2(已发布)。学生只能看到status=2的课题,这层过滤在TopicManager.getAvailableTopics()的 SQLWHERE status=2中实现,而非前端隐藏——安全边界在 DAO 层,不在 JSP。

3.2 StudentManager.class:选题动作的原子性保障

学生选题不是简单INSERT INTO selection,而是带锁的双表更新:

public boolean selectTopic(int studentId, int topicId) { Connection conn = null; PreparedStatement ps1 = null, ps2 = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 步骤1:检查课题是否已被选满(max_student 字段) ps1 = conn.prepareStatement("SELECT max_student, current_student FROM topic WHERE id=? FOR UPDATE"); ps1.setInt(1, topicId); ResultSet rs = ps1.executeQuery(); if (!rs.next() || rs.getInt("current_student") >= rs.getInt("max_student")) { return false; // 已满,拒绝选题 } // 步骤2:插入选题记录 ps2 = conn.prepareStatement("INSERT INTO selection (student_id, topic_id) VALUES (?, ?)"); ps2.setInt(1, studentId); ps2.setInt(2, topicId); ps2.executeUpdate(); // 步骤3:更新课题当前人数 ps1 = conn.prepareStatement("UPDATE topic SET current_student=current_student+1 WHERE id=?"); ps1.setInt(1, topicId); ps1.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ex) {} return false; } }

参数说明:FOR UPDATE是关键,它对topic表该行加写锁,防止并发选题时超限。max_student在建表时默认为3,若需调整,直接改topic表结构即可,无需动 Java 代码。

3.3 FileUpLoad.class:JSP 上传文件的真实路径落地逻辑

FileUpLoad.class不是调用 Commons-FileUpload,而是用request.getInputStream()手动解析 multipart 数据——这是 JDK 8 原生方案,避开第三方库冲突。它把文件存到webapps/ROOT/upload/目录下,并在数据库topic表的file_path字段存相对路径upload/20240515_abc.pdf。注意:Tomcat 默认禁止写 webapps 目录,必须在conf/context.xml中添加:

<Context> <Resources className="org.apache.naming.resources.FileDirContext" allowLinking="true" readOnly="false"/> </Context>

然后创建目录并赋权:

mkdir -p /path/to/tomcat/webapps/ROOT/upload chmod 755 /path/to/tomcat/webapps/ROOT/upload

4. 角色权限落地:管理员、教师、学生三套登录态如何穿透 JSP 与 Servlet

系统没用 Filter 做统一鉴权,而是每个 Servlet 开头硬编码if (session.getAttribute("role") == null) { response.sendRedirect("login.jsp"); }。这种「朴素但有效」的方式,让权限逻辑完全暴露在源码里,方便答辩时讲解。我们以TeacherServlet.java(反编译自TeacherManager.class)为例,看角色如何流转。

4.1 登录认证:密码 MD5 + 角色字段直取

LoginServlet.class的doPost方法核心逻辑:

String username = request.getParameter("username"); String password = request.getParameter("password"); String md5Pass = MD5Util.getMD5(password); // 调用工具类 MD5Util.class // 查询三张表:admin/teacher/student,用 UNION ALL 合并 String sql = "SELECT 'admin' as role, id, username FROM admin WHERE username=? AND password=? " + "UNION ALL SELECT 'teacher' as role, id, username FROM teacher WHERE username=? AND password=? " + "UNION ALL SELECT 'student' as role, id, username FROM student WHERE username=? AND password=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, md5Pass); ps.setString(3, username); ps.setString(4, md5Pass); ps.setString(5, username); ps.setString(6, md5Pass); ResultSet rs = ps.executeQuery(); if (rs.next()) { String role = rs.getString("role"); session.setAttribute("role", role); session.setAttribute("id", rs.getInt("id")); session.setAttribute("username", rs.getString("username")); response.sendRedirect(role + "_main.jsp"); // 跳转到 teacher_main.jsp 等 }

关键点:UNION ALL保证一次查询覆盖三角色,role字段决定后续跳转页面。teacher_main.jsp顶部有<% if (!"teacher".equals(session.getAttribute("role"))) response.sendRedirect("login.jsp"); %>,双重保险。

4.2 教师主页:动态菜单与数据隔离

teacher_main.jsp的导航栏用 JSTL 判断角色生成:

<c:choose> <c:when test="${sessionScope.role == 'admin'}"> <a href="admin_student.jsp">学生管理</a> <a href="admin_teacher.jsp">教师管理</a> <a href="admin_topic.jsp">课题管理</a> </c:when> <c:when test="${sessionScope.role == 'teacher'}"> <a href="teacher_info.jsp">个人信息</a> <a href="teacher_addtopic.jsp">添加课题</a> <a href="teacher_mytopic.jsp">我的课题</a> </c:when> <c:otherwise> <a href="student_info.jsp">个人信息</a> <a href="student_select.jsp">选择课题</a> <a href="student_mytopic.jsp">我的课题</a> </c:otherwise> </c:choose>

数据隔离:teacher_mytopic.jsp调用TopicManager.getTopicsByTeacherId((Integer)session.getAttribute("id")),SQL 为SELECT * FROM topic WHERE teacher_id=?,绝不出现WHERE teacher_id=${session.id}这种拼接漏洞。

4.3 管理员特殊操作:系统维护背后的 SQL 批量执行

AdminServlet.class的systemMaintain方法执行TRUNCATE TABLE selection清空所有选题记录,但加了二次确认:

String confirm = request.getParameter("confirm"); if ("YES".equals(confirm)) { Statement stmt = conn.createStatement(); stmt.execute("TRUNCATE TABLE selection"); // 注意:TRUNCATE 比 DELETE 快,且重置自增ID stmt.execute("UPDATE topic SET current_student=0"); // 同步重置人数 request.setAttribute("msg", "系统维护完成:已清空选题记录"); } else { request.setAttribute("msg", "维护取消"); }

提示:TRUNCATE在 MySQL 5.7 中需DROP权限,若报错Access denied for user 'topic_user'@'localhost' to database 'topic_db',需给数据库用户授权:GRANT DROP ON topic_db.* TO 'topic_user'@'localhost';


5. 避坑指南:部署调试阶段最常踩的五个深坑与血泪解法

这套系统看似简单,但实际部署时 80% 的失败源于环境细节。以下是我用三台不同配置机器(Ubuntu 20.04 / Windows 10 / macOS Monterey)反复验证的典型问题,每一条都附带现象、根因和可复制的解决命令。

5.1 现象:JSP 页面中文乱码,显示为??或方框

原因:Tomcat 默认使用 ISO-8859-1 解析请求参数,而 JSP 页面<%@ page contentType="text/html;charset=UTF-8"%>仅设置响应编码,未设请求编码。
解决:在conf/web.xml的<filter>段落前插入:

<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

验证:重启 Tomcat,在login.jsp输入中文用户名,request.getParameter("username")输出正常中文。

5.2 现象:FileUpLoad.class上传文件后,数据库file_path存的是C:\fakepath\abc.pdf

原因:现代浏览器出于安全限制,<input type="file">的value属性只返回文件名,不返回完整路径。FileUpLoad.class错误地用了request.getParameter("file")获取路径。
解决:必须用Part接口(Servlet 3.1+)重写上传逻辑。找到FileUpLoad.class对应的 JSP,将表单改为:

<form action="uploadServlet" method="post" enctype="multipart/form-data"> <input type="file" name="uploadFile" /> <input type="submit" value="上传" /> </form>

然后新建UploadServlet.java(非FileUpLoad.class),用request.getPart("uploadFile")获取文件流。

5.3 现象:学生选题成功,但topic.current_student数值没增加

原因:StudentManager.selectTopic()中UPDATE topic SET current_student=current_student+1语句未生效,通常因topic表的current_student字段类型为VARCHAR而非INT。
解决:检查建表 SQL,确保字段定义为current_student INT DEFAULT 0。若已建错,执行:

ALTER TABLE topic MODIFY COLUMN current_student INT DEFAULT 0; UPDATE topic SET current_student = 0 WHERE current_student = '';

5.4 现象:管理员登录后点击「学生管理」,页面报HTTP Status 404 – /admin_student.jsp

原因:admin_student.jsp文件在webapps/ROOT/目录下,但 Tomcat 默认部署路径是webapps/yourappname/。系统假设部署在 ROOT 上下文。
解决:将整个项目文件夹(含WEB-INF,admin_student.jsp等)全部拷贝到webapps/ROOT/目录下,而非新建子目录。或者修改conf/server.xml,将<Host>的appBase改为你的项目路径。

5.5 现象:MsgManager.class发送站内信后,收件人刷新页面看不到新消息

原因:消息未读状态存在msg表的is_read字段,但student_main.jsp查询时漏加WHERE is_read=0,导致未读数恒为 0。
解决:找到MsgManager.getUnreadCount(int userId)方法,确认 SQL 为SELECT COUNT(*) FROM msg WHERE receiver_id=? AND is_read=0。若无此条件,手动补上。


6. 进阶技巧:用 JUnit 4 快速验证 Manager 类逻辑,告别「改完不敢测」

这套系统没配单元测试,但Manager类全是 POJO,完全可以脱离 Tomcat 用 JUnit 4 黑盒验证。我给StudentManager.class写了个最小验证套件,5 分钟就能跑通核心逻辑,比启动 Tomcat 快 10 倍。

6.1 准备测试环境:H2 内存数据库模拟 MySQL

Maven 添加依赖:

<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency>

6.2 编写 StudentManagerTest:验证选题超限拦截

public class StudentManagerTest { private StudentManager manager; private Connection conn; @Before public void setUp() throws Exception { // 使用 H2 内存库,自动建表 conn = DriverManager.getConnection("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1", "sa", ""); ScriptRunner runner = new ScriptRunner(conn); runner.runScript(new FileReader("src/test/resources/topic_db_h2.sql")); // 用 H2 兼容版建表SQL manager = new StudentManager(); manager.setConnection(conn); // 通过 setter 注入连接,避免改原代码 } @Test public void testSelectTopicWhenFull() throws Exception { // 给课题 ID=1 设置 max_student=1,并已有一人选了 conn.createStatement().execute("INSERT INTO selection (student_id, topic_id) VALUES (1001, 1)"); conn.createStatement().execute("UPDATE topic SET current_student=1 WHERE id=1"); // 尝试第二人选 boolean result = manager.selectTopic(1002, 1); assertFalse(result); // 应该失败 // 验证 current_student 仍为 1 ResultSet rs = conn.createStatement().executeQuery("SELECT current_student FROM topic WHERE id=1"); rs.next(); assertEquals(1, rs.getInt(1)); } }

关键点:topic_db_h2.sql是从原topic_db.sql修改而来,把ENGINE=InnoDB换成ENGINE=H2,AUTO_INCREMENT换成AUTO_INCREMENT=1,去掉 MySQL 特有函数。这样测试不依赖真实 MySQL,秒级启动。

6.3 用断点调试反向定位 JSP 传参问题

当teacher_addtopic.jsp提交后TopicManager.addTopic()的tb.getTitle()为空,不要盲目改 JSP。在TopicManager.class的addTopic方法第一行加断点,用 IDE 远程调试(Tomcat 启动参数加-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000),观察request.getParameter("title")的值。90% 的情况是 JSP 表单name="title"拼错,或request.setCharacterEncoding("UTF-8")没在doPost开头执行。

从那以后我每次改完 JSP 表单,都强制走一遍curl -X POST -F "title=test" -F "description=ok" http://localhost:8080/addTopicServlet,用命令行绕过浏览器缓存和编码干扰,直接验证后端接收逻辑。这招帮我避开了至少 7 次「页面看着没问题,后台死活收不到参」的玄学翻车。

希望帮到你。

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

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

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

立即咨询