
简介一份面向Java Web课程设计的学生信息管理系统项目源码采用JSPServlet经典技术栈前端配合Layui与jQuery实现交互数据库使用MySQL。运行环境为JDK7Tomcat8IntelliJ IDEA系统支持学生自主注册登记、教师对学生的增删改查等功能模块划分清晰适合初学者理解Java Web开发的完整流程。压缩包共含2个文件包括zip项目源码与htm说明文档整体大小仅2.13MB。zip内提供完整工程代码及lib目录下的jar包依赖数据库文件获取方式一并写入htm文档中整体工程具备良好的完整度。已有6433人浏览学习属于被广泛验证过的课程设计可用资源。通过这套资源可以快速获得一个可运行的学生信息管理系统模板涵盖登录注册、信息维护、角色权限等典型模块既能支撑课设报告的技术实现论述也适合在此基础上自行扩展成绩管理、分页检索等新功能是上手Java Web项目实践的有效参考。1. 基于java web的简单学生信息管理系统jspservlet这个项目到底在解决什么问题如果你正在学Java Web或者正在为课程设计、毕业设计找切入点你大概率会撞见这个标题。它没有Spring Boot没有MyBatis没有前后端分离甚至没有Maven——只有JSP、Servlet和一个Tomcat。看起来“古老”但恰恰是这套组合把Java Web最底层的请求响应模型、Session管理、JDBC交互、页面渲染全部讲透了。很多人在简历里写“熟悉Spring Boot”但一问Servlet生命周期就卡壳就是因为当初跳过了这一步。本文要做的不是再贴一遍别人的源码而是把一个JSPServlet的学生信息管理系统从环境搭建到CRUD实现、再到部署和避坑完整地捋一遍。目标是让你不光能跑起来还能回答“为什么这么写”“哪里容易翻车”。适合三类人刚学完Java基础想找第一个完整项目的人、正在做课设但不想直接抄代码的人、准备Java面试想补底层链路的人。2. 选型背后的考量为什么2025年了还要用JSPServlet做管理系统2.1 这套技术栈到底在扮演什么角色很多初学者有个误区以为学了Spring Boot就可以跳过Servlet和JSP。实际上Spring Boot的Web模块底层就是Spring MVC而Spring MVC的核心分发器DispatcherServlet本身就是一个Servlet只是它替你把请求路由、参数绑定、视图解析都封装好了。你写Spring Boot时感受不到Servlet的存在但它一直在底层跑。JSP和Servlet的分工是“各管一摊”Servlet负责处理请求和业务逻辑接收表单参数、调用JDBC操作数据库、把结果存进request或sessionJSP负责展示从request或session中取出数据用Java代码片段脚本或EL表达式渲染成HTML。管理系统的典型流程——查学生列表、点编辑回填表单、保存更新——用这套组合写一遍你才理解什么叫“请求-响应”循环什么叫转发和重定向的区别。我的建议很直接如果你时间只够做一个项目做这个“简单学生信息管理系统”比做一个半吊子的Spring Boot项目有价值得多。因为它小到你能看懂每一行代码又完整覆盖了Web开发的核心链路。做完这个再学Spring Boot你会发现很多概念是相通的。2.2 对比三条技术路线的取舍选技术栈不是越新越好而是“当前目标下成本最低”。下表是我接触过的三组常见组合对比它们的门槛和适配场景技术组合学习成本调试难度适合场景JSPServletJDBC低只需Java基础和SQL低出错能直接看到课设、毕设、理解Java Web底层JSPServletMyBatis中需要额外学ORM中SQL和Java分开调有数据库设计要求的课设Spring BootMyBatis Plus高需要理解IOC/AOP中问题被框架掩盖找工作、真实企业项目我不反对用Spring Boot但反对第一次做Web项目就直接上Spring Boot。框架帮你做了太多事反而把“请求怎么进来、数据怎么存、页面怎么渲染”这件事变成了黑匣子。JSPServlet方案下你写的每一个方法都是自己调的用的每个对象都是自己new出来的翻了车能顺着调用链找到根因——这种体验是框架给不了的。2.3 学生信息管理系统的功能边界怎么划定标题里有个关键词叫“简单”。很多课设喜欢把系统做大加权限角色、加课程管理、加成绩统计、加Excel导出……但功能越多代码越乱最后变成“临时需求驱动的代码堆”失去教学意义。我建议功能边界就定在四条学生信息的增删改查、按条件检索、登录校验、分页展示。这四项覆盖了Web开发最核心的操作模式也足够支撑一篇像样的设计文档。数据库表也不宜复杂。学生表student、用户表user就够。学生表字段控制在id学号、name、gender、age、class_name、phone、create_time。登录表字段控制在username、password。这样建库、写SQL、写实体类都不用纠结把精力集中在Servlet和JSP的交互逻辑上。提示功能越少越容易把每个功能写扎实。加需求永远比减需求容易先跑通主链路再谈扩展。3. 从零搭出可运行环境JDK、Tomcat与IDEA的配置细节3.1 装对版本组合省掉一半玄学报错先聊版本。JSPServlet对版本要求不苛刻但“最新版”往往会带来不必要的兼容麻烦。我在实际搭建中踩过Tomcat 10的坑Jakarta EE改名后javax.servlet包变成了jakarta.servlet旧代码直接编译失败。这个坑对初学者来说相当隐蔽报错信息还不直观。所以这里给你一份明确的选择清单照着装基本不会卡JDK1.8 或 11二者选一。JDK 8 兼容性最好网上能找到的绝大多数资料都以它为准。Tomcat8.5.x 或 9.0.x。只要不选Tomcat 10及以上javax.servlet包就还在。IDEACommunity版即可不需要Ultimate。使用社区版时手动配置Web骨架和Tomcat反而能看清项目结构。MySQL5.7 或 8.0。8.0的驱动类名是com.mysql.cj.jdbc.Driver5.7是com.mysql.jdbc.Driver这个差异后面会重点提。3.2 创建Web项目的三种方式与手工配置步骤如果你用IDEA Ultimate新建项目时选Java Enterprise勾选Web Application和Servlet骨架就出来了。但社区版没有这个选项需要手工建。很多教程绕过去了这里我给出一个能落地的做法第一步用IDEA新建一个普通Java项目然后在src/main目录下手动创建java和webapp两个目录。第二步右键webapp目录选择“将目录标记为Web资源目录”相当于手动告诉IDEA“这是Web根目录”。第三步在webapp/WEB-INF下创建web.xml并补下面的内容?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_3_1.xsd version3.1 display-nameStudent Management System/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-app逻辑说明这个web.xml声明了项目是一个Java Web应用并指定了首页文件。注意版本号写3.1对应Servlet 3.1规范这个版本支持注解配置后面可以用WebServlet代替在XML里写Servlet映射。welcome-file-list的作用是当浏览器访问项目根路径时Tomcat自动帮你跳转到login.jsp。参数说明第一步的两个目录位置不能错Java源码必须在src/main/java下JSP必须放在webapp下。WEB-INF目录是个特殊目录浏览器不能直接访问里面的文件但JSP放在它外面如webapp根目录是可以被直接访问的。很多初学者的页面放在WEB-INF里然后访问404就是没理解这个约束。第四步配置Tomcat。IDEA社区版没有现成的Tomcat集成按钮需要手动下载Tomcat压缩包解压到本地。然后在IDEA的Run Configuration中添加“Tomcat Server - Local”指定Tomcat目录Deployment里添加Artifact。如果你没有使用Maven构建这里需要把编译后的classes目录和webapp目录打包成一个“Exploded Artifact”。3.3 验证环境第一段Servlet代码跑通链路环境配好后先别急着写业务写一个最简Servlet验证链路是否通畅。在java目录下建一个DemoServlet.javapackage com.demo; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; WebServlet(/demo) public class DemoServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html;charsetUTF-8); PrintWriter out resp.getWriter(); out.println(h1环境OK路径访问成功/h1); } }逻辑说明WebServlet(/demo)是Servlet 3.0之后的注解写法等价于在web.xml中配置一段servlet-mapping。它的作用是把/demo这个URL路径绑定到当前类上。当浏览器访问http://localhost:8080/项目名/demo时Tomcat找到这个类并调用doGet方法。resp.setContentType设置了响应类型和编码里面的charsetUTF-8是中文不乱码的关键。参数说明WebServlet的urlPatterns属性写的是相对路径必须以/开头。如果你配的是/demo/*那/demo/anything都会路由到这个Servlet。这里有个高频坑项目名要跟在端口后面写成http://localhost:8080/项目名/demo很多新手漏掉项目名一直404还找不到原因。启动Tomcat后浏览器访问对应的URL看到“环境OK”就说明JDK、Tomcat、IDEA三者的协作已经打通。这一步是整个项目的地基宁可多花半小时在这里也不要急着写代码。4. 核心实现拆解从数据库表到Servlet与JSP的完整链路4.1 设计数据库建表SQL与JDBC工具类的封装学生管理系统的数据层不需要复杂设计。下面是我的建表SQL直接在MySQL命令行或Navicat中执行即可CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE student ( id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender VARCHAR(10) COMMENT 性别, age INT COMMENT 年龄, class_name VARCHAR(50) COMMENT 班级, phone VARCHAR(20) COMMENT 联系电话, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO user (username, password) VALUES (admin, 123456);逻辑说明student表的主键用VARCHAR类型的学号而不是自增INT。这是“学生信息管理系统”业务决定的学号本身有业务含义比如2023001适合直接做主键。user表先固定插一条管理员记录用于登录验证。数据库字符集用utf8mb4比utf8更稳妥能存emoji和生僻字避免后面乱码翻车。接下来是JDBC工具类这是整个项目里被复用次数最多的代码。我习惯写一个DBUtil.java统一管连接和关闭package com.demo.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的数据库密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); if (ps ! null) ps.close(); if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明static块里的Class.forName用于加载数据库驱动类。MySQL 8.0的驱动类是com.mysql.cj.jdbc.Driver如果你本地是MySQL 5.x要改成com.mysql.jdbc.Driver否则运行时报找不到类。URL里的characterEncodingutf8解决中文乱码serverTimezoneAsia/Shanghai解决MySQL 8.0时区报错。参数说明USER字段里root是MySQL默认用户密码部分必须替换成本地实际值。不要在生产环境这么写密码但课设项目为了简单硬编码可接受。close方法连写三个关闭是有讲究的ResultSet不关可能让连接无法归还连接池PreparedStatement不关可能造成游标泄漏。一个工具类把三件套都收干净业务代码就不用重复写了。4.2 登录模块Session校验与过滤器拦截登录是管理系统的第一道门。它的实现模式在Java Web里非常典型表单提交到ServletServlet查数据库查到就写Session并跳转主页查不到就返回错误信息。先看LoginServlet的核心代码package com.demo.servlet; import com.demo.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( SELECT * FROM user WHERE username? AND password?)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { HttpSession session req.getSession(); session.setAttribute(username, username); resp.sendRedirect(list); } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); } } } catch (SQLException e) { e.printStackTrace(); req.setAttribute(msg, 系统异常请稍后再试); req.getRequestDispatcher(login.jsp).forward(req, resp); } } }逻辑说明req.getParameter从表单里取用户输入这是Servlet接收参数的通用方式。登录成功用resp.sendRedirect(list)做重定向浏览器地址栏会变成/list并重新发起一次GET请求失败则用forward转发回login.jsp。重定向和转发的区别是关键考点转发是服务器内部跳转浏览器无感知request里的属性可以传递到目标页面重定向是浏览器重新请求request属性丢失但URL会变化可以防止表单重复提交。登录成功后重定向到列表页是避免刷新页面时弹“确认重新提交表单”的标准做法。在拦截未登录访问时我加了一个Filter来实现。很多教科书没讲清楚Filter的优先级这里用注解直接配置package com.demo.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); String uri req.getRequestURI(); boolean isLoginRequest uri.endsWith(login.jsp) || uri.endsWith(/login); boolean isStaticResource uri.contains(/css/) || uri.contains(/js/); if (session ! null session.getAttribute(username) ! null) { chain.doFilter(request, response); } else if (isLoginRequest || isStaticResource) { chain.doFilter(request, response); } else { resp.sendRedirect(login.jsp); } } }逻辑说明WebFilter(/*)表示拦截所有URL。代码里放行了两类请求登录页本身和静态资源其余请求在Session里没有username就强制回登录页。req.getSession(false)传false是关键——如果当前没有Session就直接返回null而不是新创建一个避免每访问一次静态资源就被塞一个Session。注意过滤器URL映射用/*时JSP页面也会被拦截。如果登录页里引用了CSS必须在过滤器中放行静态资源否则页面会一片白板并且看不到任何报错。4.3 学生列表与分页请求参数传递与前端渲染登录后最核心的功能就是学生列表页。这里我选择用一个StudentServlet统一处理list、add、update、delete四个操作用req.getParameter(action)区分。这种写法代码集中适合“简单”定位。先看列表和分页package com.demo.servlet; import com.demo.entity.Student; import com.demo.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.sql.*; import java.util.ArrayList; import java.util.List; WebServlet(/list) public class StudentListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int pageNum 1; int pageSize 5; String pageStr req.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) { pageNum Integer.parseInt(pageStr); } String keyword req.getParameter(keyword); if (keyword null) { keyword ; } ListStudent list new ArrayList(); int totalCount 0; try (Connection conn DBUtil.getConnection()) { String countSql SELECT COUNT(*) FROM student WHERE name LIKE ? ; try (PreparedStatement countPs conn.prepareStatement(countSql)) { countPs.setString(1, % keyword %); try (ResultSet rs countPs.executeQuery()) { if (rs.next()) { totalCount rs.getInt(1); } } } String sql SELECT * FROM student WHERE name LIKE ? ORDER BY id LIMIT ?, ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, (pageNum - 1) * pageSize); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Student s new Student(); s.setId(rs.getString(id)); s.setName(rs.getString(name)); s.setGender(rs.getString(gender)); s.setAge(rs.getInt(age)); s.setClassName(rs.getString(class_name)); s.setPhone(rs.getString(phone)); list.add(s); } } } } catch (SQLException | NumberFormatException e) { e.printStackTrace(); } req.setAttribute(list, list); req.setAttribute(pageNum, pageNum); req.setAttribute(totalCount, totalCount); req.setAttribute(totalPages, (totalCount pageSize - 1) / pageSize); req.setAttribute(keyword, keyword); req.getRequestDispatcher(list.jsp).forward(req, resp); } }逻辑说明分页两个参数pageNum是当前页码pageSize是每页条数。SQL串里LIMIT ?, ?两个占位符分别是偏移量和条数。偏移量公式是(pageNum - 1) * pageSize比如第2页每页5条就是跳过前5条取第6到第10条。先查COUNT再查数据是分页的标准两步总记录数用来算totalPages。(totalCount pageSize - 1) / pageSize这个写法是向上取整避免最后一页因为除不尽被漏掉。参数说明LIMIT的占位符不能用字符串拼接否则会有SQL注入风险。这里用PreparedStatement的setInt赋值既安全又能在数据库层做类型验证。keyword参数默认空字符串查出来的结果就是全表数据这样搜索和列表共用一套逻辑不用写两个方法。4.4 JSP页面里的数据渲染JSTL与EL表达式怎么配合JSP页面要从request域取出Student列表并渲染成表格。如果你在JSP里用% %写循环页面会迅速变成一锅粥。更好的做法是用EL表达式加JSTL标签库。先引入依赖或直接在WEB-INF/lib里放jstl-1.2.jar文件然后在JSP顶部写% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core %列表页的核心表格代码如下table border1 cellpadding8 cellspacing0 tr th学号/th th姓名/th th性别/th th年龄/th th班级/th th电话/th th操作/th /tr c:forEach items${list} varstu tr td${stu.id}/td td${stu.name}/td td${stu.gender}/td td${stu.age}/td td${stu.className}/td td${stu.phone}/td td a hrefedit?id${stu.id}编辑/a a hrefdelete?id${stu.id} onclickreturn confirm(确定删除该学生吗)删除/a /td /tr /c:forEach /table逻辑说明${stu.id}会调用Student类的getId方法这就是为什么实体类的setter/getter必须写全。如果实体类里是class_name这种数据库字段Java属性名要写成classNameJSP里用${stu.className}对应。这里有个常见混淆数据库字段下划线命名Java属性驼峰命名JSP页面跟Java属性对齐而不是跟数据库字段对齐。分页导航放在表格后面直接拼接URL参数div 共 ${totalCount} 条记录第 ${pageNum}/${totalPages} 页 c:if test${pageNum 1} a hreflist?page${pageNum-1}keyword${keyword}上一页/a /c:if c:if test${pageNum totalPages} a hreflist?page${pageNum1}keyword${keyword}下一页/a /c:if /divJSP页面的逻辑越少越好把判断交给c:if把取值交给ELJava脚本代码尽量别出现在页面上。这不仅是代码美观的问题也是后续维护的分水岭。你会发现同样一个列表页用脚本写的和用JSTL写的维护成本差出好几倍。4.5 新增与编辑的复用同一个form页面处理两种场景新增和编辑的动作都属于表单提交。我习惯共用一个student_form.jsp通过URL参数区分是新增还是编辑。页面顶部加一段JSTL判断c:set varisEdit value${not empty student} / h2${isEdit ? 编辑学生信息 : 新增学生}/h2 form actionsave methodpost c:if test${isEdit} input typehidden nameid value${student.id} / /c:if p姓名input typetext namename value${student.name} //p p性别input typeradio namegender value男 ${student.gender 男 ? checked : }/男 input typeradio namegender value女 ${student.gender 女 ? checked : }/女/p p年龄input typetext nameage value${student.age} //p p班级input typetext nameclassName value${student.className} //p p电话input typetext namephone value${student.phone} //p button typesubmit保存/button /form逻辑说明${student}是从request域传过来的实体。新增时Servlet不设置这个属性not empty student为falseisEdit为false表单标题显示“新增”提交后Servlet走insert逻辑。编辑时Servlet通过id查出完整Student对象放进requestisEdit为true表单自动回填并且多了个隐藏的id字段用于更新时定位。参数说明隐藏的input是关键。表单提交后SaveServlet需要根据id判断是新增还是更新id为空就insertid非空就update。这样两个功能共用一套页面构建逻辑后期加字段只需改一处表单、一处SQL。更新操作的核心SQL如下String sql isEdit ? UPDATE student SET name?, gender?, age?, class_name?, phone? WHERE id? : INSERT INTO student (id, name, gender, age, class_name, phone) VALUES (?,?,?,?,?,?);这段SQL放在SaveServlet里配合req.getParameter(id)来判断走哪个分支。增删改查到这里就全部闭环了。5. 避坑手册新手做JSPServlet项目最常见的5个翻车点5.1 现象Tomcat启动后访问项目404原因排查大概率是你访问的URL里没有带项目上下文路径。Tomcat默认部署后路径是/项目名不是/。如果项目Artifact名字叫student_war_explodedURL就必须是http://localhost:8080/student_war_exploded/login.jsp。另一种可能web.xml的welcome-file指定的文件名拼错了大小写。解决先打开Tomcat控制台看部署后的上下文路径。IDEA的Deployment标签页里Application context那一栏写的是啥浏览器里就必须带啥。建议把这个值改成/这样访问http://localhost:8080/login.jsp就能直接进省去每次输入长路径的麻烦。5.2 现象页面中文全部变成问号或者乱码这是JSPServlet项目的经典坑。乱码有三个来源你大概率是其中一个JSP文件本身的编码、Servlet读取请求参数的编码、数据库连接的编码。解决三层都要看到。JSP文件顶部必须有% page contentTypetext/html;charsetUTF-8 %这一行同时IDEA右下角文件编码设置为UTF-8。Servlet里如果是POST提交在req.getParameter之前调用req.setCharacterEncoding(UTF-8)。数据库连接URL里的characterEncodingutf8也不能少。三层都对了乱码才能彻底根治单独改任一层都是按下葫芦浮起瓢。5.3 现象JDBC连接数据库报ClassNotFoundException原因MySQL驱动jar包没放进WEB-INF/lib目录。很多教程告诉你下载mysql-connector-java下载了也拷贝了但拷贝到了src/main/lib这种自定义目录Tomcat根本不会去那里找类。驱动jar必须放在WEB-INF/lib下这个目录下的jar会在应用启动时被自动加载到类路径。解决将驱动jar复制到webapp/WEB-INF/lib/下然后重启Tomcat。如果是Maven项目检查pom.xml里依赖的scope不能是provided否则运行环境中没有该依赖。5.4 现象更改了Java代码但浏览器访问的还是旧页面原因JSP页面在Tomcat中默认是开发模式自动更新的但Java类必须重新编译并热部署。如果你用IDEA的exploded方式部署改Java类后IDEA有时不会自动把编译后的class文件同步到Tomcat的部署目录。解决检查IDEA的Build菜单选择Rebuild Project后再重启Tomcat。一个更可靠的检查方式是到Tomcat部署目录看实际运行的class文件修改时间如果时间不对说明没有成功同步。这种“改了没生效”比报错更折磨人建议改一次代码就重启一次Tomcat不要让热部署的黑匣子坑你。5.5 现象明明登录成功了点任何链接又跳回登录页原因Session在Tomcat重启后丢失或者过滤器的放行逻辑有漏洞。最常见的情况是浏览器直接访问list路径时过滤器发现Session中没有username就直接重定向到login.jsp。但如果你在JSP页面里通过相对路径跳转比如hreflist而不是href/项目名/list浏览器解析出来的路径就会丢失上下文。解决修改Servlet转发和重定向的路径统一加前缀或使用${pageContext.request.contextPath}表达式。在JSP页面里所有跳转链接写成a href${pageContext.request.contextPath}/list学生列表/a这个表达式会自动替换成当前项目的根路径无论部署名怎么改都不会出错。这是JSP页面里最实用的一个技巧没有之一。6. 让项目能拿得出手日志、SQL注入排查与面试追问的准备当项目功能全部跑通后下一步不是急着往上加功能而是打磨两个关键点安全性和代码可解释性。先说SQL注入。前面在StudentListServlet里用了PreparedStatement已经把主要的注入风险堵住了。但你要能讲清楚为什么它安全PreparedStatement会把SQL语句的结构和参数值分开传给数据库参数中的单引号会被转义成普通字符不会改变SQL的执行逻辑。如果面试官问“字符串拼接SQL会有什么后果”你能说出“1 OR 11在登录SQL里直接绕过密码校验”这个例子比背八股文有效得多。日志是另一个容易被忽略的加分项。现在系统里的e.printStackTrace()虽然能让控制台打印堆栈但在真实部署环境里没人盯着控制台看。一个更实用的做法是把异常信息写到日志文件我用Java原生的java.util.logging写了一个最轻量的版本import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; public class LogUtil { private static final Logger LOGGER Logger.getLogger(StudentManagement); static { try { FileHandler fh new FileHandler(student.log, true); fh.setFormatter(new SimpleFormatter()); LOGGER.addHandler(fh); } catch (Exception e) { e.printStackTrace(); } } public static void error(String msg, Exception e) { LOGGER.log(Level.SEVERE, msg, e); } }逻辑说明FileHandler的第二个参数true表示追加写文件不会每次启动覆盖旧日志。SimpleFormatter输出可读的文本格式。以后在任何Service或Servlet里把e.printStackTrace()替换成LogUtil.error(查询学生列表失败, e)运行一天后再去日志文件里定位问题效率完全不同。部署环节也有技巧。IDEA里直接Run Tomcat是开发模式但演示或答辩时需要打一个war包扔到独立Tomcat里运行。在IDEA中执行Build Artifacts选student:war生成的war包放到Tomcat的webapps目录下启动Tomcat后war自动解压。这里有个细节war包的文件名就是访问Path比如student.war对应http://localhost:8080/student/xxx答辩前记得把war包改名成好记的短名。最后是面试追问准备。针对这个项目下面几个问题几乎是必问的Servlet的生命周期是什么三个方法init、service、destroy分别在什么时候被调用转发和重定向的区别网络层面发生了什么差异Session和Cookie到底是怎么协同的JSP和Servlet的本质区别是什么。这些问题都能在代码里找到真实案例支撑把代码和原理对照着讲比空背概念扎实得多。我自己的习惯是每次做完一个“简单”项目都会把代码从零重写一遍不复制不带抄。第二遍写的时候不再需要搜“servlet demo”也不再被空洞的框架概念困住反而能把代码写得更简单。这个系统也一样把本文的样例代码敲一遍你会发现后来的Spring MVC分层思想在这套代码里早有影子。希望帮到你。本文还有配套的精品资源点击获取