ARTICLE DETAIL

资讯详情

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

基于JSP+Servlet的学生信息管理系统:从环境配置到答辩加分全攻略

基于JSP+Servlet的学生信息管理系统:从环境配置到答辩加分全攻略 简介基于Java Web的学生信息管理系统完整源码采用JSPServletlayuijQuery实现面向正在做课程设计的计算机专业学生及Java Web入门开发者可解决学生自主注册、教师统一管理学生信息等典型场景问题。压缩包共2个文件以zip源码包和htm说明文档为主整体仅2.13MB源码内含数据库脚本及lib目录下jar包依赖数据库文件可通过配套网盘链接另行获取。运行环境为JDK7Tomcat8MySQLIntelliJ IDEA按登录注册、学生信息管理、教师后台等模块组织结构清晰便于导入IDE直接运行和二次扩展。已有6433人学习下载可从中掌握Servlet请求处理、JSP页面渲染、Layui表格交互及登录注册模块的完整落地思路对完成课程设计或理解Java Web基础分层开发具有实用参考价值。1. 基于JSPServlet的学生信息管理系统为什么我劝你别一上来就上Spring Boot做课设或者刚学完Java基础想练手的同学十有八九被布置过“基于java web的简单学生信息管理系统”这类题目。不少人第一反应是都2025年了谁还写JSPServlet直接上Spring Boot不香吗但从我带过的十几个课设项目来看JSPServlet这套组合在“理解Web本质”这个目标上依然没有替代品。几十个类的Servlet里你能亲眼看到request怎么进来、response怎么出去、session怎么维持这些在Spring Boot中全被封进了注解背后。这个系统也不只是课设专用很多老企业的内部OA系统至今还在跑类似架构。适合的人群很明确准备Java面试被“Servlet生命周期”问懵的、用IDEA新建Web工程不知道从哪里下手的、以及想要一个能快速答辩又能讲清楚原理的JavaWeb项目的人。2. 从JDK到Tomcat把运行环境一次配齐版本搭配与项目骨架2.1 为什么还在用JSPServlet而不是Spring Boot选型边界先说服自己为什么值得花精力把它搭出来。JSPServlet作为Java Web最初的完整形态把HTTP协议的处理过程暴露得干干净净Servlet负责接收请求、处理逻辑、返回响应JSP负责在服务端生成动态HTML页面。你写下的每一行代码都能直接映射到HTTP报文上这种透明度是框架无法给你的。另外从实际需求来看这类“简单学生信息管理系统”通常只有登录、增删改查、列表查询几个功能数据量也就是一个班几十号人。这种体量用Spring Boot加MyBatis Plus光起步配置就比业务代码多。学框架之前先用标准JavaWeb把业务跑通后续理解Spring MVC的DispatcherServlet、拦截器机制时会有一种“原来如此”的通透感。选择这套技术栈还有一个非常现实的理由期末答辩时面试官和评委老师最爱问的“Servlet是单例吗线程安全吗”“JSP和Servlet什么关系”这些问题用这套项目实战过的人才能答出细节只背面试题的人一追问就翻车。2.2 IDEA 2024创建Web项目的正确流程别被Gradle和Maven带偏用IDEA 2024新建JavaWeb项目时不少新人会在Gradle和Maven之间纠结半天其实这个阶段都不需要。常见做法是直接用IDEA的“Java Enterprise”模板创建或者干脆创建一个普通Java项目手动加Web目录结构。我一般会走这个流程第一步确认JDK版本。这里有一个重要的版本匹配问题如果你要用经典的javax.servlet包下的Servlet API请务必选择JDK 8或JDK 11配合Tomcat 9及以下版本。Tomcat 10及以上已经把包名改成了jakarta.servlet代码写法完全不同。标题里的JSPServlet通常暗示的是javax.servlet那一套所以选Tomcat 9最稳妥。# 验证JDK版本 java -version # 期望输出类似: java version 1.8.0_202 或 11.0.x第二步在IDEA中配置Tomcat。打开“Run”菜单下的“Edit Configurations”点击“”加号选择“Tomcat Server”下的“Local”然后在“Application server”中选择本地的Tomcat 9解压目录。部署选项卡里点击“”选择“Artifact”把Web项目挂上去Application context填/student这样启动后访问路径就是http://localhost:8080/student。第三步校验项目的Web目录结构。一个标准的JavaWeb工程必须有src/main/java存放源码、src/main/webapp存放JSP和静态资源。如果用的是普通Java项目改造需要手动在webapp/WEB-INF下创建web.xml文件。这个文件是Web应用的“注册中心”Servlet和过滤器都要在这里声明IDEA只会帮你建骨架不会帮你写映射。student-management/ ├── src/ │ ├── main/ │ │ ├── java/ # 源码目录 │ │ │ └── com/student/ │ │ │ ├── dao/ # 数据访问层 │ │ │ ├── entity/ # 实体类 │ │ │ ├── servlet/ # Servlet控制器 │ │ │ └── util/ # 工具类 │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ └── web.xml │ │ ├── login.jsp │ │ ├── studentList.jsp │ │ └── static/ │ │ ├── css/ │ │ └── js/ │ └── resources/ # 数据库配置文件2.3 web.xml关键配置逐行拆解Servlet映射和欢迎页web.xml是整个项目的总控开关。在这份配置里你需要告诉Tomcat哪个URL路径交给哪个Servlet类处理以及默认访问哪个页面。?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 System/display-name !-- 登录Servlet映射 -- servlet servlet-nameloginServlet/servlet-name servlet-classcom.student.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameloginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping !-- 学生列表Servlet映射 -- servlet servlet-namestudentListServlet/servlet-name servlet-classcom.student.servlet.StudentListServlet/servlet-class /servlet servlet-mapping servlet-namestudentListServlet/servlet-name url-pattern/studentList/url-pattern /servlet-mapping !-- 欢迎页 -- welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list !-- 字符编码过滤器防止中文乱码 -- filter filter-nameencodingFilter/filter-name filter-classcom.student.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping /web-app这份配置的核心逻辑是URL到Java类的映射关系。/login这个路径被访问时Tomcat会实例化LoginServlet并调用其doGet或doPost方法。注意url-pattern的写法/login是精确匹配/*是通配所有路径。很多人在过滤器里写/*没问题但在Servlet映射里如果用/*就会拦截掉所有JSP页面的访问这是最常见的翻车点之一。3. 核心链路落地登录校验、学生表CRUD与JSP页面渲染3.1 JDBC连接封装类一次封装全程复用学生信息管理系统的数据操作离不开数据库。这里选择MySQL配合JDBC连接信息写在src/main/resources/db.properties配置文件中方便换环境时只改配置不改代码。jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456连接封装类是整套代码的地基。我一般会写一个静态工具类把驱动加载和连接获取统一管理package com.student.util; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String driver; private static String url; private static String username; private static String password; // 静态代码块: 类加载时只执行一次, 避免反复注册驱动 static { try { InputStream is DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties props new Properties(); props.load(is); driver props.getProperty(jdbc.driver); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); Class.forName(driver); // 注册驱动 } catch (Exception e) { throw new ExceptionInInitializerError(数据库配置初始化失败); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable rc : resources) { if (rc ! null) { try { rc.close(); } catch (Exception ignored) { // 关闭失败不影响主流程 } } } } }这段代码值得注意的有两个点一是Class.forName(driver)在JDBC 4.0之后其实可以省略但在Java 8配合MySQL 8.x驱动时保留更稳妥二是close方法用可变参数接收Connection、PreparedStatement、ResultSet三类资源调用时按rs, pstmt, conn的顺序从里往外关。这个习惯能避免连接泄漏——别小看这个Tomcat运行两天后突然报连接池耗尽多半就是资源没关干净。3.2 登录流程的Servlet实现request、session和重定向登录是学生信息管理系统的第一道门。用户在login.jsp输入用户名和密码表单以POST方式提交到/loginLoginServlet负责校验。校验成功后把用户信息存进session再重定向到学生列表页失败则携带错误信息回到登录页。package com.student.servlet; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; public class LoginServlet extends HttpServlet { // doGet直接转发给doPost, 地址栏手动访问也能正确处理 Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 从请求参数中获取用户输入 String username request.getParameter(username); String password request.getParameter(password); // 非空校验: 前端做了校验, 后端必须再做一次 if (username null || username.isEmpty() || password null || password.isEmpty()) { request.setAttribute(errorMsg, 用户名和密码不能为空); request.getRequestDispatcher(login.jsp).forward(request, response); return; } // 简单模拟校验: 实际项目中改为查数据库admin表 if (admin.equals(username) 123456.equals(password)) { // 登录成功, 把用户写入Session并设置过期时间 HttpSession session request.getSession(); session.setAttribute(currentUser, username); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作失效 // 使用重定向防表单重复提交 response.sendRedirect(request.getContextPath() /studentList); } else { // 登录失败, 回显错误消息 request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } }这段登录逻辑当中有三个细节处理值得说明。request.getParameter拿到的是字符串后端判空能防掉那些绕过前端直接构造请求的情况。登录成功后用sendRedirect而不用forward是为了符合PRGPost/Redirect/Get模式——刷新列表页时不会把登录请求再提交一次。失败时用forward并携带errorMsg属性JSP页面就能通过${errorMsg}把这句提示渲染出来。你还可以为密码存储预留一个升级空间数据库里存MD5或BCrypt哈希后的密文比明文密码稳妥得多。3.3 学生信息DAO层PreparedStatement的增删改查样板DAO层是业务逻辑和数据库之间的隔离带。学生实体类Student包含id、studentNo学号、name、gender、age、className班级、phone这些字段。StudentDAO提供add、update、delete、findById、findAll5个方法。package com.student.dao; import com.student.entity.Student; import com.student.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDAO { // 查询所有学生 public ListStudent findAll() { String sql SELECT id, student_no, name, gender, age, class_name, phone FROM student ORDER BY id; ListStudent list new ArrayList(); Connection conn null; PreparedStatement pstmt null; ResultSet rs null; try { conn DBUtil.getConnection(); pstmt conn.prepareStatement(sql); rs pstmt.executeQuery(); while (rs.next()) { list.add(mapRowToStudent(rs)); } } catch (SQLException e) { throw new RuntimeException(查询学生列表失败, e); } finally { DBUtil.close(rs, pstmt, conn); } return list; } // 新增学生 public boolean add(Student student) { String sql INSERT INTO student (student_no, name, gender, age, class_name, phone) VALUES (?, ?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, student.getStudentNo()); pstmt.setString(2, student.getName()); pstmt.setString(3, student.getGender()); pstmt.setInt(4, student.getAge()); pstmt.setString(5, student.getClassName()); pstmt.setString(6, student.getPhone()); return pstmt.executeUpdate() 0; } catch (SQLException e) { throw new RuntimeException(新增学生失败, e); } } // 根据ID删除学生 public boolean delete(int id) { String sql DELETE FROM student WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setInt(1, id); return pstmt.executeUpdate() 0; } catch (SQLException e) { throw new RuntimeException(删除学生失败, e); } } // 更新学生信息 public boolean update(Student student) { String sql UPDATE student SET student_no ?, name ?, gender ?, age ?, class_name ?, phone ? WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { pstmt.setString(1, student.getStudentNo()); pstmt.setString(2, student.getName()); pstmt.setString(3, student.getGender()); pstmt.setInt(4, student.getAge()); pstmt.setString(5, student.getClassName()); pstmt.setString(6, student.getPhone()); pstmt.setInt(7, student.getId()); return pstmt.executeUpdate() 0; } catch (SQLException e) { throw new RuntimeException(更新学生失败, e); } } // 结果集映射为实体对象 private Student mapRowToStudent(ResultSet rs) throws SQLException { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setStudentNo(rs.getString(student_no)); stu.setName(rs.getString(name)); stu.setGender(rs.getString(gender)); stu.setAge(rs.getInt(age)); stu.setClassName(rs.getString(class_name)); stu.setPhone(rs.getString(phone)); return stu; } }所有SQL都用PreparedStatement的?占位符拼参数而不是用字符串拼接。这不是风格问题而是一条安全红线用SELECT * FROM student WHERE name name 这种写法用户输入 OR 11 --就能把整个表拖出来这就是SQL注入。setString和setInt这些方法会自动处理特殊字符转义相当于把用户输入当成数据而非SQL片段。另一个值得学习的是try-with-resources写法只在add和delete方法中使用了因为这两个方法不需要ResultSet而findAll需要承接查询结果所以保留了传统finally块手动关闭资源。3.4 学生列表页JSP渲染JSTL和EL表达式让页面告别小脚本早期写JSP的老程序员习惯在页面里塞% while(rs.next()) { %这种脚本片段页面上Java代码掺着HTML标签改一行样式都可能破坏业务逻辑。现在更规范的做法是用EL表达式取数据、JSTL标签做循环判断。在studentList.jsp的顶部引入标签库% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生信息列表/title /head body h2学生信息管理/h2 p欢迎c:out value${sessionScope.currentUser}//p table border1 cellpadding8 cellspacing0 tr thID/th th学号/th th姓名/th th性别/th th年龄/th th班级/th th联系电话/th th操作/th /tr c:forEach items${studentList} varstudent tr td${student.id}/td td${student.studentNo}/td td${student.name}/td td${student.gender}/td td${student.age}/td td${student.className}/td td${student.phone}/td td a hrefeditStudent?id${student.id}编辑/a a hrefdeleteStudent?id${student.id} onclickreturn confirm(确认删除该学生)删除/a /td /tr /c:forEach /table /body /html这段JSP的关键在于请求转发时往request域里塞了一个名为studentList的ListStudent对象。StudentListServlet中的写法是request.setAttribute(studentList, dao.findAll());然后request.getRequestDispatcher(studentList.jsp).forward(request, response);。JSP里的${studentList}会自动调用toString或者按JavaBean规范调getter方法所以实体类必须保留getId()、getName()这类命名方式。${sessionScope.currentUser}前面加sessionScope是为了限定取值范围。HttpServletRequest、HttpSession、ServletContext三个作用域都有setAttribute方法如果不限定前缀EL会按照page、request、session、application的顺序逐层查找。刚学的时候觉得不加也能出结果一旦项目里同名属性多了排查起来像在黑匣子里摸。养成写清作用域前缀的习惯能省掉不少调试时间。4. 数据表设计与分页模糊查询让系统从玩具变成结课设计4.1 三张核心表怎么拆学生表、班级表、成绩表学生信息管理系统如果只有一张student表功能会显得单薄也撑不起结课设计要求的“业务复杂度”。这里使用三张表设计学生表student、班级表class_info、成绩表score。班级表解决学生所属班级信息的冗余成绩表则让系统具备“按学号查询考试成绩”的扩展能力。CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; -- 班级表 CREATE TABLE class_info ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL UNIQUE, grade VARCHAR(20) COMMENT 年级, 如: 2024级, head_teacher VARCHAR(20) COMMENT 班主任 ); -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, gender ENUM(男, 女) DEFAULT 男, age INT, class_id INT COMMENT 关联班级表的ID, phone VARCHAR(15), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 成绩表 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT COMMENT 关联学生表的ID, subject VARCHAR(30) COMMENT 科目, score DECIMAL(5, 1), exam_date DATE, FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE );三张表之间用外键关联student.class_id指向class_info.idscore.student_id指向student.id。外键约束能保证数据完整性但也不是越多越好。比如score表对学生表的外键设置了ON DELETE CASCADE意味着删除学生时成绩自动级联删除这在演示系统里很方便实际生产系统中通常不会启用级联删除而是用逻辑删除加deleted标记字段保护历史数据。gender字段用ENUM类型在MySQL中没问题但如果后续要迁移到SQL Server就需要改成CHAR(1)这一点在做数据库选型时要想清楚。4.2 分页查询的参数对照表LIMIT ? OFFSET ?的实际用法数据量小的时候findAll一把梭还看不出问题等往课程设计里插入50条以上演示数据时列表页会被拉得很长。分页是整个系统里性价比最高的一项优化。前端页面传递两个参数页码pageNum和每页条数pageSize。DAO层的分页查询方法public ListStudent findByPage(int pageNum, int pageSize) { String sql SELECT s.id, s.student_no, s.name, s.gender, s.age, s.phone, c.class_name FROM student s LEFT JOIN class_info c ON s.class_id c.id ORDER BY s.id LIMIT ?, ?; ListStudent list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql)) { // MySQL的LIMIT从0开始, 第1页的offset是0 int offset (pageNum - 1) * pageSize; pstmt.setInt(1, offset); pstmt.setInt(2, pageSize); ResultSet rs pstmt.executeQuery(); while (rs.next()) { list.add(mapRowToStudent(rs)); } } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } return list; }同时查询总数来算总页数public int count() { String sql SELECT COUNT(*) FROM student; try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql); ResultSet rs pstmt.executeQuery()) { rs.next(); return rs.getInt(1); } catch (SQLException e) { throw new RuntimeException(统计学生总数失败, e); } }分页参数需要特别说明的是LIMIT ? ?的含义第一个参数是跳过的行数第二个参数是返回的最大行数。代码中offset (pageNum - 1) * pageSize当pageSize10时第1页的offset是0第2页的offset是10第3页是20以此类推。很多新手会把第一页的offset写成1结果第一条数据被跳过。另一个常见问题是页码越界——用户手动在URL里传入pageNum99时查询会返回空列表。所以Servlet里必须对页码做防御性校验小于1时重设为1大于总页数时重设为总页数。不处理的话演示时极可能因为随意改动URL参数而当场翻车。4.3 多条件模糊查询动态SQL的雏形分页之后课程设计的高频需求就是按姓名、学号、班级模糊查询。这三个条件可能组合出现也可能是空条件——SQL需要根据用户实际输入动态拼接。这是MyBatis动态SQL要解决的问题在纯Servlet时代只能靠StringBuilder手动拼接。public ListStudent searchStudents(String keyword, int classId, int pageNum, int pageSize) { StringBuilder sql new StringBuilder(); sql.append(SELECT s.id, s.student_no, s.name, s.gender, s.age, s.phone, c.class_name ); sql.append(FROM student s LEFT JOIN class_info c ON s.class_id c.id WHERE 11 ); ListObject params new ArrayList(); // 动态拼关键字条件 if (keyword ! null !keyword.trim().isEmpty()) { sql.append(AND (s.name LIKE ? OR s.student_no LIKE ?) ); String likeKeyword % keyword.trim() %; params.add(likeKeyword); params.add(likeKeyword); } // 动态拼班级条件 if (classId 0) { sql.append(AND s.class_id ? ); params.add(classId); } sql.append(ORDER BY s.id LIMIT ?, ?); params.add((pageNum - 1) * pageSize); params.add(pageSize); ListStudent list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement pstmt conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { pstmt.setObject(i 1, params.get(i)); } ResultSet rs pstmt.executeQuery(); while (rs.next()) { list.add(mapRowToStudent(rs)); } } catch (SQLException e) { throw new RuntimeException(条件查询失败, e); } return list; }这段代码有一个经典技巧WHERE 11。这个恒真条件看起来没用却让后面的AND拼接变得无脑——有条件就接AND ...不用判断“当前是不是第一个条件”。参数列表用ListObject收集再统一通过pstmt.setObject(i 1, params.get(i))设值顺序必须和SQL中的占位符严格一致。值得注意的是LIKE关键字在MySQL中默认不区分大小写但下划线_和百分号%如果出现在用户输入内容中会被当成通配符需要转义的话可以加ESCAPE子句。对课设场景不处理影响也不大。5. 基于JSPServlet的5个翻车现场从中文乱码到4045.1 POST请求中文乱码filter配置了还是没用现象登录成功后跳转到学生列表页面上所有中文姓名全部变成问号???。新增一条包含中文的记录后数据库里存进去的也是乱码。原因Tomcat 9的server.xml中默认的URIEncoding是UTF-8但POST请求的表单参数编码取自请求头的Content-Type字符集。如果JSP页面没有指定pageEncodingUTF-8表单提交的数据在浏览器端就用系统默认编码Windows下是GBK发送了出去。只写% page contentTypetext/html;charsetUTF-8 %还不够acceptcharset或filter里的setCharacterEncoding调用顺序有讲究。解决在EncodingFilter里对所有请求统一设置编码public class EncodingFilter implements Filter { private String encoding UTF-8; Override public void init(FilterConfig filterConfig) { String enc filterConfig.getInitParameter(encoding); if (enc ! null) { encoding enc; } } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(encoding); // 只对POST请求体生效 response.setCharacterEncoding(encoding); // 响应内容编码 response.setContentType(text/html;charset encoding); chain.doFilter(request, response); } }关键点是request.setCharacterEncoding必须在读取任何请求参数之前调用。HttpServletRequest第一次调用getParameter时会按当前编码解析请求体之后再设置就晚了。所以我建议EncodingFilter的url-pattern写/*并置于web.xml最前面保证它是第一个执行过滤器。现代浏览器和Tomcat 9组合下这样的配置可以同时解决POST和GET请求的乱码问题。5.2 Servlet映射写成/*导致JSP样式全部丢失现象输入http://localhost:8080/student/studentList地址原本正常的页面变成一片空白按F12看Network面板发现所有CSS、JS文件全部404。原因web.xml里某个Servlet的url-pattern写成了/*。这个通配符匹配所有路径包括/css/style.css和JSP页面本身。Servlet优先于默认Servlet处理请求本应由Tomcat内置的DefaultServlet处理的静态资源和JSP编译请求全部被截胡所以样式文件全部返回404。解决检查所有Servlet映射的url-pattern。精确匹配/studentList、/login只拦截指定路径不会影响静态资源。如果确实需要通配某个前缀用/api/*这种带路径前缀的写法让JSP和静态资源路径如/css/*、/static/*不在匹配范围内。5.3 启动时报ClassNotFoundException: javax.servlet.http.HttpServlet现象Tomcat启动后访问项目地址出现java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet的异常堆栈页面直接报500。原因pom.xml或lib目录下没有引入Servlet API依赖。有些人用的是JDK 11及以上版本创建项目IDEA默认解析的模块依赖里没有Tomcat运行环境提供的servlet-api.jar。解决在pom.xml中引入Servlet API依赖注意使用Tomcat 9对应的javax.servlet坐标dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency这里的scope设为provided很重要编译时需要Servlet API但部署到Tomcat时容器自带实现如果改成compile会把Tomcat自带的类再打包一遍版本冲突时反而会出诡异问题。5.4 JDBC驱动加载失败MySQL 8.x驱动的class名坑现象启动项目后调用DAO层方法控制台打印ClassNotFoundException: com.mysql.jdbc.Driver。原因数据库驱动MySQL Connector/J 8.x已经把驱动类名从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver。老教程里的com.mysql.jdbc.Driver在MySQL 8及以上版本中已移除。同时新版驱动要求url中必须指定serverTimezone否则连接时抛出时区相关的异常。解决db.properties中已写明com.mysql.cj.jdbc.Driverurl带上了serverTimezoneAsia/Shanghai。数据库驱动jar包通过Maven依赖引入dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency5.5 修改代码后不生效Tomcat缓存和重启玄学现象改了Java代码后重启Tomcat访问效果还是旧逻辑。有时改了JSP代码刷新页面也看不到变化。原因Tomcat默认开启JSP编译缓存部署时工作目录下的work/Catalina保存了已编译的class文件。IDEA的热部署Update classes and resources对Java方法体内部的改动有效但对新增方法、修改类签名、修改web.xml不生效。更隐蔽的是IDEA的Build输出目录和Tomcat部署目录不同步。解决最稳妥的做法是在IDEA的Tomcat配置中启用On frame deactivation为Update classes and resources同时在Deployment选项卡选择“Exploded”而不是“Archive”。如果修改后依然不生效手动删除target目录和Tomcat的work目录再重启基本能解决90%的“改动不生效”问题。6. 给课设加分的三个小动作Filter日志、事务回滚和URL重写项目骨架和核心功能跑通后想让答辩评分往上走一个档次这三个优化值得花半天时间加进去。第一个是Filter统一访问日志。在现有过滤器基础上再写一个AccessLogFilter拦截所有请求并记录IP地址、访问路径、处理耗时。这个功能技术上不复杂却非常有答辩表现力——评委问“系统有什么亮点”时你可以直接打开控制台展示每一条请求的处理时间证明自己考虑了可观测性问题。public class AccessLogFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; String ip request.getRemoteAddr(); String uri request.getRequestURI(); long start System.currentTimeMillis(); chain.doFilter(req, resp); long cost System.currentTimeMillis() - start; System.out.println([AccessLog] ip uri cost ms); } }第二个是数据库事务回滚。当前add和update方法各自独立提交事务如果“新增学生后同步往score表插入默认成绩”这两步操作中途发生异常数据库里会出现只有学生没有成绩的脏数据。正确做法是在一个业务方法里拿到同一个连接手动控制提交和回滚Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); studentDAO.addWithConn(conn, student); scoreDAO.initScoreWithConn(conn, student.getId()); conn.commit(); } catch (Exception e) { conn.rollback(); } finally { conn.setAutoCommit(true); DBUtil.close(conn); }setAutoCommit(false)是事务开始的分界线后续所有SQL都在同一个数据库连接上执行直到commit才真正落盘。只要过程中任何一步抛出异常rollback会把之前的所有操作全部撤销。这个“事务管理器”模式在面试题里出现的频率极高亲手实现一次比背十遍八股文都管用。第三个是URL重写与页面上编辑、删除链接的规范传参。/editStudent?id18这种方式把参数放在查询字符串里对数据修改类操作不够安全——别人把地址改成id17就能操作别人的数据。你可以做一个简单的权限校验确认session中有currentUser才允许执行修改操作。删除用POST加confirm弹窗将链接改为带隐藏表单的方式提交。做过这几个毕业设计周期后我最深的体会是JSPServlet这套老技术栈真正磨炼人的地方不在于“能不能跑”而在于你能不能讲清楚为什么这么搭建。如果能把从环境配置、数据库设计、Servlet控制流程、Filter解决乱码到事务回滚的完整链路给评委讲明白哪怕代码规模才两千行也比一万行“CtrlC/CtrlV”的Spring Boot项目更有含金量。希望帮到你。本文还有配套的精品资源点击获取
返回列表