简介这是一份基于 JavaServletJSPBootstrapMySQL 的学生信息管理系统项目源码面向正在进行 Java Web 期末大作业、课程设计或毕业设计的本专科学生也适合初学 Servlet/JSP 分层开发的读者。项目采用 ServletDAOVO 分层结构涵盖登录注册、学生与教师模块、验证码、图片上传等常见功能并配有 MySQL 建表脚本和说明文档源码均已本地编译验证通过可直接导入 IDE 配合本地数据库运行。压缩包共 82 个文件以 Java 源码、JSP 页面、class 编译文件、CSS/JS 前端资源、SQL 脚本和依赖 Jar 包为主整体约 12.03MB目录划分清晰便于按模块阅读和复用。该资源评审分达到 98 分难度适中内容经过助教老师审定适合期末大作业和课程设计参考目前已有 157 人学习下载。下载后不仅可完成课程项目还可作为理解 Java Web 请求处理、页面交互与数据库操作的完整实践模板。1. 学生信息管理系统为什么ServletJSPBootstrapMySQL这套组合仍值得动手复现学生信息管理系统在Java Web阶段的课程设计里几乎是一道必做题登录、增删改查、分页搜索几个模块走完Servlet和JSP的请求流转也就吃透了。这个项目标题里的四个技术点组合起来就是一份非常典型的Java Web项目完整案例——没有Spring Boot的自动配置帮你遮住细节你得手动处理数据库连接、Filter过滤、页面渲染和数据回显反而能把Web应用的底层链路看得更清楚。这篇文章按这套技术组合从头拆到落地覆盖数据表设计、后端功能实现、前端页面整合和常见踩坑记录。如果你正在找一份能跑通、能讲清楚、能应付答辩的高分项目这篇笔记能把每个环节的来龙去脉和坑位都补上。2. 先定架构再写代码技术分工、数据表设计和项目骨架搭建2.1 四个技术的职责边界一次HTTP请求从浏览器到MySQL的完整旅程做这个项目之前先理解四个角色分别管什么否则后面联调翻车时很难定位是前端问题还是后端问题。Bootstrap跑在浏览器端只负责HTML和CSS的样式渲染它不参与业务逻辑甚至不关心页面上的数据是从哪儿来的。JSP是视图层模板由Tomcat编译成Servlet再输出HTML适合做动态页面。Servlet是控制层接收浏览器请求、调用业务对象、打包数据、选择视图转发。MySQL在最底层负责持久化把学生、用户、成绩这些数据存起来并提供增删改查的SQL能力。一次完整流程是这样的浏览器输入地址 → Tomcat根据url-pattern找到对应的Servlet → Servlet调用DAO访问MySQL → 把查询结果放进request作用域 → forward到JSP → JSP渲染HTML → 浏览器展示带Bootstrap样式的页面。你在地址栏看到的每一个动作都被这一条链路串起来。为什么不用Spring Boot不是Spring Boot不好而是课程设计评分的重点不在“功能能不能跑”而在“你能不能讲清楚Servlet生命周期、doGet和doPost区别、Session原理、JDBC流程”。Spring Boot把这些细节全自动配置了项目是容易写了但被问到底层时往往答不上来。反而是ServletJSP这套组合能让你把所有底层概念暴露出来这也是高分项目最常见的呈现方式之一。2.2 数据库设计学生、用户、成绩三张表的建表SQL一个能拿高分的学生管理系统数据库设计不能只有一张学生表。我一般会至少建三张表用户登录表、学生信息表、成绩表。其中成绩表通过外键关联学生表这样既能演示一对多关系的建模也能在讲解时证明你理解外键、索引、级联这些基础概念。下面这份建表SQL可以直接抄CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_db; -- 用户登录表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5加密后的密文, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 学生信息表 CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT NOT NULL DEFAULT 1 COMMENT 1-男2-女, age INT NOT NULL COMMENT 年龄, class_name VARCHAR(50) COMMENT 班级, major VARCHAR(50) COMMENT 专业, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_class_name (class_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 成绩表 CREATE TABLE t_score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL COMMENT 学生ID, course_name VARCHAR(50) NOT NULL COMMENT 课程名, score DECIMAL(5,2) NOT NULL COMMENT 分数, KEY idx_student_id (student_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES t_student (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表; -- 初始数据登录账号 admin / 123456 INSERT INTO t_user (username, password) VALUES (admin, MD5(123456)); INSERT INTO t_student (student_no, name, gender, age, class_name, major) VALUES (2024001, 张伟, 1, 20, 计科2401, 计算机科学与技术), (2024002, 李娜, 2, 21, 软工2402, 软件工程);需要注意的细节有三个。一是字符集必须选utf8mb4而不是utf8否则遇到生僻字会被MySQL直接截断报错二是学号虽然加了UNIQUE约束但主键仍用自增id因为学号不是真正稳定的业务主键万一学校改学号规则改主键会牵连所有外键三是class_name上建普通索引是为了后面模糊搜索班级时能用上索引不要嫌数据少就不建答辩时老师会看你的SHOW CREATE TABLE输出。2.3 Maven项目骨架pom.xml、web.xml与分包命名项目骨架建议直接用Maven webapp结构包名用com.example.student这类标准三段式命名方便后续扩展。分包按照filter、servlet、dao、entity、util划分每个包只放一类东西这是课程设计里最容易拿分的好习惯。project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdstudent-management/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source8/maven.compiler.source maven.compiler.target8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies build finalNamestudent-management/finalName /build /project两个地方要注意。第一javax.servlet-api的scope是provided因为Tomcat自带Servlet实现war包里重复打包容易和容器冲突第二版本组合上推荐JDK 8 Tomcat 9 mysql-connector-java 8.0.x这是目前兼容性最稳的组合。如果你用了Tomcat 10以上代码里的javax.servlet.*包名要全部改成jakarta.servlet.*否则部署时会直接报找不到类。再看web.xml这个项目不需要在web.xml里逐个注册Servlet用注解方式更简洁但web.xml里的欢迎页和Session超时还是要配?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-namestudent-management/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list session-config session-timeout30/session-timeout /session-config /web-app如果你的Tomcat是9.0这个web.xml直接能用。选择版本的时候别光看“最新”Tomcat 9配合javax.*包名是大量Java Web课程设计经过验证的组合换到Tomcat 10或11包名变更带来的兼容问题会让新手心态崩掉。3. 后端实现链路登录认证、学生CRUD、分页与模糊查询的代码落地3.1 登录认证与Session管理从LoginServlet到拦截过滤器登录是整个系统的入口也是后台管理的第一道安全防线。这个问题在Java面试题里常年出现Session怎么创建的、什么时候失效、为什么关掉浏览器再打开就要重新登录。先写DAO层用PreparedStatement查用户public class UserDAO { private final Connection conn; public UserDAO(Connection conn) { this.conn conn; } public User findByUsername(String username) throws SQLException { String sql SELECT id, username, password, create_time FROM t_user WHERE username ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); 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)); return u; } } } return null; } }这里用PreparedStatement而不是Statement拼字符串是防止SQL注入的最基本手段。你可以在参数里传 OR 11 --试一下Statement方式会把SQL语义改掉PreparedStatement会把整个字符串当成一个字面量值。对应的LoginServletWebServlet(/login) public class LoginServlet extends HttpServlet { private static final long serialVersionUID 1L; 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); if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { request.setAttribute(error, 用户名和密码不能为空); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } try (Connection conn DBUtil.getConnection()) { UserDAO userDAO new UserDAO(conn); User user userDAO.findByUsername(username.trim()); if (user ! null MD5Util.md5(password).equals(user.getPassword())) { HttpSession session request.getSession(); session.setAttribute(user, user); // 登录成功跳转到列表页 response.sendRedirect(request.getContextPath() /student/list); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } catch (SQLException e) { e.printStackTrace(); request.setAttribute(error, 系统繁忙请稍后重试); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }注意登录接口有两个细节要说。第一我这里先按username把用户查出来再在Java代码里比对MD5密文而不是直接SELECT * FROM t_user WHERE username? AND passwordMD5(?)原因是为了让你拿到查出来的User对象后续如果要扩展“修改密码”“记住登录状态”都会方便很多第二密码加密用的MD5虽然已经不算强但课程设计里能写出MD5加盐已经比明文密码高一个档次了你可以说盐值是username 固定字符串再哈希答辩老师基本认这个思路。登录之后要拦截未登录请求写一个AuthFilter过滤器WebFilter(/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String path req.getRequestURI().substring(req.getContextPath().length()); // 放行登录页、登录接口、静态资源 if (path.equals(/login.jsp) || path.equals(/login) || path.startsWith(/static/)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); if (session null || session.getAttribute(user) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这里最容易被忽略的就是静态资源放行。过滤器路径是/*会把CSS、JS、图片全部拦住如果你没放行/static/页面会显示成纯HTMLBootstrap完全不起作用。很多同学折腾半天以为Bootstrap引入方式错了实际上就是Filter把所有静态请求都重定向到了登录页。3.2 学生信息CRUDDAO模式下的增删改查完整代码学生信息的增删改查是整个项目的核心模块也是评分老师最关注的代码位置。我用StudentDAO承载所有数据库操作用StudentListServlet处理列表请求。先建Student实体字段对应t_student表id用Integer而不是int这样新增时id为null才能和修改用同一个save方法public class Student { private Integer id; private String studentNo; private String name; private Integer gender; private Integer age; private String className; private String major; // 省略 getter / setter }StudentDAO里的列表和统计方法public class StudentDAO { private final Connection conn; public StudentDAO(Connection conn) { this.conn conn; } public ListStudent list(int offset, int pageSize, String keyword) throws SQLException { StringBuilder sql new StringBuilder( SELECT id, student_no, name, gender, age, class_name, major, create_time ); sql.append(FROM t_student WHERE 11 ); ListObject args new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (name LIKE ? OR student_no LIKE ? OR class_name LIKE ?) ); String kw % keyword.trim() %; args.add(kw); args.add(kw); args.add(kw); } sql.append(ORDER BY student_no LIMIT ?, ?); args.add(offset); args.add(pageSize); try (PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i args.size(); i) { ps.setObject(i 1, args.get(i)); } try (ResultSet rs ps.executeQuery()) { ListStudent list new ArrayList(); while (rs.next()) { list.add(mapRow(rs)); } return list; } } } public int count(String keyword) throws SQLException { StringBuilder sql new StringBuilder(SELECT COUNT(*) FROM t_student WHERE 11 ); ListObject args new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (name LIKE ? OR student_no LIKE ? OR class_name LIKE ?) ); String kw % keyword.trim() %; args.add(kw); args.add(kw); args.add(kw); } try (PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i args.size(); i) { ps.setObject(i 1, args.get(i)); } try (ResultSet rs ps.executeQuery()) { rs.next(); return rs.getInt(1); } } } public int delete(int id) throws SQLException { try (PreparedStatement ps conn.prepareStatement(DELETE FROM t_student WHERE id?)) { ps.setInt(1, id); return ps.executeUpdate(); } } public int save(Student s) throws SQLException { if (s.getId() null) { String sql INSERT INTO t_student (student_no, name, gender, age, class_name, major) VALUES (?,?,?,?,?,?); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setInt(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setString(5, s.getClassName()); ps.setString(6, s.getMajor()); return ps.executeUpdate(); } } else { String sql UPDATE t_student SET student_no?, name?, gender?, age?, class_name?, major? WHERE id?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setInt(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setString(5, s.getClassName()); ps.setString(6, s.getMajor()); ps.setInt(7, s.getId()); return ps.executeUpdate(); } } } }这套DAO有两点可以拿出来讲。一是为什么sql里要写WHERE 11这是动态拼接条件的常用技巧后面追加AND ...时不需要判断是不是第一条代码简洁且不会拼出多余AND二是所有SQL都用PreparedStatement其中分页的LIMIT ?, ?两个参数也走预编译没有任何字符串拼接SQL的痕迹。StudentListServlet接收分页参数和搜索关键字WebServlet(/student/list) public class StudentListServlet extends HttpServlet { private static final long serialVersionUID 1L; Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String pageParam request.getParameter(page); String keyword request.getParameter(keyword); int page 1; int pageSize 10; if (pageParam ! null pageParam.matches(\\d)) { page Integer.parseInt(pageParam); } try (Connection conn DBUtil.getConnection()) { StudentDAO dao new StudentDAO(conn); ListStudent students dao.list((page - 1) * pageSize, pageSize, keyword); int total dao.count(keyword); int totalPage (int) Math.ceil(total * 1.0 / pageSize); if (totalPage 0) { totalPage 1; } request.setAttribute(students, students); request.setAttribute(currentPage, page); request.setAttribute(totalPage, totalPage); request.setAttribute(total, total); request.setAttribute(keyword, keyword); request.getRequestDispatcher(/student_list.jsp).forward(request, response); } catch (SQLException e) { e.printStackTrace(); response.sendError(500, 数据库访问失败); } } }分页算法的核心就两行offset (page - 1) * pageSizetotalPage ceil(total / pageSize)。page从1开始是人的直觉但SQL的LIMIT从0开始所以需要减一。当总记录数为0时totalPage会算成0不处理的话页面分页条会显示“1 / 0”所以强制把totalPage置1。3.3 编辑、删除和表单提交请求参数映射到实体编辑功能需要一个加载单条记录的方法在StudentDAO里补一个findById然后由StudentEditServlet做两步先根据id查出Student放到request域再forward到student_form.jsp表单里所有input都回填对应值。提交时StudentSaveServlet接收表单参数封装成Student对象后调用dao.save(student)。这里有一个常见的坑页面表单里有一个隐藏域input typehidden nameid value${student.id}负责告诉后台这条记录是新增还是修改。很多同学忘了写隐藏域导致编辑时id丢失save方法判断idnull把更新记录变成了插入记录。逻辑很简单但确实每年都有人在这上面翻车。DELETE请求在后面的前端章节里会改成POST方式现在是标准做法删除按钮只是一个地址为/student/delete?id...的链接用GET访问会触发删除。这种写法在单机演示时没问题但如果部署到公网环境搜索引擎爬虫或浏览器预加载都可能把记录删掉。扣分点是老师会追问“能不能把删除改成POST”所以在实现时直接就把删除接口限制成POST方法才是稳妥的。4. 前端页面整合Bootstrap表格与表单、JSP渲染和Ajax异步删除4.1 Bootstrap页面搭建导航栏、表格、表单和分页组件的落地前端部分的目标是让页面看起来像一个正经后台管理系统而不是一行一行的数据堆叠。Bootstrap 4是课程设计的主流选择官方文档里对表格、导航、表单、分页都有现成类名直接查文档比背CSS快得多。不建议在JSP里引远程CDN。因为答辩教室的电脑可能没有外网页面加载到一半样式全崩那是灾难现场。正确的做法是提前把bootstrap.min.css、jquery.min.js、bootstrap.min.js下载到项目的webapp/static目录下在JSP里用相对路径引用% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html langzh-CN head meta charsetUTF-8 title学生列表 - 学生信息管理系统/title link relstylesheet href${pageContext.request.contextPath}/static/css/bootstrap.min.css /head body nav classnavbar navbar-expand-lg navbar-dark bg-primary div classcontainer a classnavbar-brand href${pageContext.request.contextPath}/student/list学生信息管理系统/a div classml-auto text-white 欢迎${sessionScope.user.username} a classbtn btn-outline-light btn-sm ml-2 href${pageContext.request.contextPath}/logout退出/a /div /div /nav div classcontainer mt-4 form classform-inline mb-3 action${pageContext.request.contextPath}/student/list methodget input typetext namekeyword value${keyword} classform-control mr-2 placeholder学号 / 姓名 / 班级 button typesubmit classbtn btn-info搜索/button /form table classtable table-bordered table-hover thead classthead-dark tr th学号/thth姓名/thth性别/thth年龄/th th班级/thth专业/thth操作/th /tr /thead tbody c:forEach items${students} vars tr td${s.studentNo}/td td${s.name}/td td${s.gender 1 ? 男 : 女}/td td${s.age}/td td${s.className}/td td${s.major}/td td a classbtn btn-sm btn-warning href${pageContext.request.contextPath}/student/edit?id${s.id}编辑/a button classbtn btn-sm btn-danger btn-delete>$(function () { $(.btn-delete).on(click, function () { var id $(this).data(id); if (!confirm(确定要删除这条学生记录吗)) { return; } $.ajax({ url: ${pageContext.request.contextPath}/student/delete?id id, type: POST, dataType: json, success: function (res) { if (res.code 200) { alert(删除成功); location.reload(); } else { alert(删除失败: res.message); } }, error: function () { alert(网络异常请稍后重试); } }); }); });对应StudentDeleteServlet只接受POST请求WebServlet(/student/delete) public class StudentDeleteServlet extends HttpServlet { private static final long serialVersionUID 1L; Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(application/json;charsetUTF-8); String idParam request.getParameter(id); if (idParam null || !idParam.matches(\\d)) { response.getWriter().write({\code\:400,\message\:\参数不合法\}); return; } try (Connection conn DBUtil.getConnection()) { StudentDAO dao new StudentDAO(conn); int rows dao.delete(Integer.parseInt(idParam)); if (rows 0) { response.getWriter().write({\code\:200,\message\:\success\}); } else { response.getWriter().write({\code\:404,\message\:\记录不存在\}); } } catch (SQLException e) { e.printStackTrace(); response.getWriter().write({\code\:500,\message\:\系统异常\}); } } }改造之后删除动作只能通过浏览器里的JavaScript触发地址栏直接访问/student/delete?id1会返回405。这块内容可以在答辩时主动讲“为什么不用GET做删除”安全意识的得分点就在这里。5. 排查与避坑端口冲突、MySQL时区、中文乱码与Servlet 404的经典记录5.1 启动与环境类Tomcat端口占用和MySQL 8驱动踩坑1Tomcat启动秒退控制台报端口被占用现象IDEA里启动Tomcat控制台输出Port 8080 required by Tomcat v9.0 Server is already in use服务起不来。原因上一次Debug没有正常停止残留的Tomcat进程还在占用8080端口或者本机装了别的服务占用了8080常见的还有Nginx、另一个Tomcat实例。解决Windows下打开命令行执行netstat -ano | findstr 8080找到占用8080的PID再用taskkill /pid pid /f结束进程。如果想彻底避免可以去Tomcat的conf/server.xml里把端口改成8081但改完以后访问地址也要跟着变项目的JDBC URL不涉及端口所以不受影响。IDEA里还需要养成习惯结束调试用右下方Tomcat的停止按钮而不是直接关浏览器。踩坑2MySQL 8.x连接报ClassNotFound或者时区错误现象项目启动或第一次访问数据库时报ClassNotFoundException: com.mysql.jdbc.Driver或者换用正确驱动后连接时报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因mysql-connector-java 5.x的驱动类名是com.mysql.jdbc.Driver8.x改成了com.mysql.cj.jdbc.Driver。导致连接失败后部分同学在互联网上复制到一段旧的JDBC配置文件把驱动类名又改回5.x的写法问题交叉叠加。时区报错的根因是MySQL 8.x驱动要求JDBC URL里显式说明serverTimezone不写就默认取数据库系统时区中文系统时区名无法解析。解决驱动类固定写成com.mysql.cj.jdbc.DriverJDBC URL加上完整参数jdbc:mysql://localhost:3306/student_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue其中useSSLfalse是跳过SSL握手allowPublicKeyRetrievaltrue是配合MySQL 8的caching_sha2_password认证插件不加这个参数也可能分析失败。这个组合适用于MySQL 8.0.x全系列。5.2 中文乱码类JSP、Servlet、数据库三层编码对齐踩坑3页面上所有中文变成问号或者一串乱码现象登录后学生列表里姓名、班级显示成????或者出现类似æ··的UTF-8被当成GBK解码的乱码。原因中文乱码是Java Web联调里的老熟人本质是三层编码不一致。常见组合JSP页面没声明pageEncodingUTF-8Servlet接收参数时没调request.setCharacterEncoding(UTF-8)或者JDBC URL少了characterEncodingutf8以及MySQL建库时字符集用了latin1。解决三层逐一检查按这个顺序排查。第一JSP页面顶部必须有% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %第二所有接收参数的ServletdoGet和doPost第一行都写request.setCharacterEncoding(UTF-8)第三JDBC URL确认带useUnicodetruecharacterEncodingutf8第四数据库表和库都确认是utf8mb4。如果做到了全部四点POST提交的中文不会乱。GET请求的中文参数此时可能还乱那就需要在Tomcat的server.xml里给Connector加一行URIEncodingUTF-8或者手动把参数按ISO-8859-1解码再转成UTF-8。课程设计环境里我建议直接用POST提交搜索表单绕开GET中文参数的坑。5.3 访问异常类Servlet 404、Filter拦静态资源和删除误触踩坑4访问/student/list返回404Tomcat控制台没有任何异常现象浏览器输入http://localhost:8080/student-management/student/list页面白屏报404IDEA控制台也不打日志。原因表面上是“路径不对”实际常见原因有两个。一是WebServlet注解没生效Tomcat的注解扫描默认是开启的如果你在web.xml里写了metadata-completetrue/metadata-complete就会关闭注解扫描所有Servlet注解全部失效二是编译产物里根本没有这个Servlet类IDEA的Artifact没有把最新代码同步进Tomcat的部署目录这个问题在刚新建Servlet后特别容易出现。解决先检查web.xml有没有写入metadata-complete有就删掉再打开IDEA的Project Structure - Artifacts查看Output Layout里classes目录是否存在最新编译的class文件不确定就执行一次Build - Rebuild Project。最后用浏览器开发者工具看Network请求的URL确认没有少写Context Path。这三步走完99%的Servlet 404都能定位。踩坑5删除操作被GET访问触发数据误删现象页面里删除功能原本是链接浏览器地址栏输入删除URL或者外部爬虫抓取页面时一句话没点删除按钮记录就没了。原因把删除做成了GET接口浏览器的预取、收藏夹、爬虫都会发起GET请求服务器端收到GET就执行delete根本拦不住。解决把删除接口限定为POST方法改成doPost前端用Ajax或者form methodpost提交。如果项目里兼顾老浏览器可以在Servlet里写一句校验if (!POST.equalsIgnoreCase(request.getMethod())) { response.sendError(405, Method Not Allowed); return; }删除这种不可逆操作永远不要放在GET里这是Java Web项目里最基础的安全红线。6. 部署验证与答辩准备打war包换机跑通和三个高频追问6.1 先把项目打成war包换一台没有IDEA的机器也能演示课程设计答辩经常要换到教室或机房电脑上演示这时候IDEA里的运行配置帮不了你最稳妥的办法是提前把项目打成war包。Maven项目一行命令搞定mvn clean package产出物在target/student-management.war把war包复制到任意一台装了JDK和Tomcat的机器丢进Tomcat的webapps目录启动Tomcat后会自动解压部署。访问地址是http://localhost:8080/student-management/注意必须带上下文路径。换机部署最容易翻车的点有三个数据库有没有导进去、MySQL账号密码是否跟工程配置一致、JDK版本是否满足。我的习惯是包里放一份完整的建库SQL脚本和README到现场先用Navicat或命令行执行SQL再改DBUtil里的连接账号最后启动Tomcat。整个过程五分钟能跑完比现场打开IDEA等依赖下载靠谱得多。6.2 答辩时老师最常追问的三个技术点第一个是Servlet生命周期。你要能说出Tomcat启动时加载Servlet、实例化后调用init、每次请求走service分发到doGet或doPost、关闭时调用destroy。Servlet是单实例多线程多个请求共享同一个Servlet对象所以成员变量不要放有状态的数据。第二个是为什么用PreparedStatement。三个得分点预编译提升性能、参数化防SQL注入、代码可读性高。可以顺手举一个之前提过的 OR 11 --注入例子说明Statement拼接会改SQL语义。第三个是分页查询怎么实现的。说出两个核心参数LIMIT offset, pageSize其中offset(currentPage-1)*pageSize总页数totalPageMath.ceil(total*1.0/pageSize)。再解释一下为什么页码从1开始但offset从0开始这个细节能证明你真的看懂了自己的代码。我最初带课程设计项目时最反感的就是学生做完但讲不清。能把这几个点准备到位比你额外多写一百行没用的代码都加分。希望帮到你。本文还有配套的精品资源点击获取