简介这是一份面向Java Web初学者与高校课程设计学生的Servlet基础实践项目聚焦Web层与数据库交互的核心能力训练解决学生信息增删改查的典型业务场景。资源共15个文件包含3个核心Java Servlet类如StudentServlet实现ID查询、5个XML配置文件含web.xml与Maven依赖管理、1个HTML前端页面、1个README说明文档及配套IDEA工程文件iml/.gitignore等整体2.25MB结构简洁便于快速部署运行。已有54人学习下载适合在Tomcat 9 MySQL 8环境下动手调试理解Servlet生命周期、HTTP请求处理、JDBC连接池基础用法及前后端简单交互逻辑。读者可直接导入IDEA运行观察请求路由、参数获取与数据库响应全过程掌握从项目搭建、代码编写到本地调试的完整开发闭环。1. 为什么一个“基于Java Servlet的学生信息管理系统”至今仍是Java Web入门绕不开的硬核练手项目你可能已经刷过几十道Java基础题、背过Spring Boot自动配置原理但只要没亲手用原生ServletJDBCJSP搭过一个能增删改查学生数据、带登录校验、页面跳转不乱码、数据库事务不丢数据的完整系统——那你的Java Web底层肌肉就还没真正长出来。这不是复古情怀而是现实企业面试官翻你简历时看到“熟悉Servlet生命周期”和“独立完成基于Servlet的学生信息管理系统”是两种信任度校招笔试里“写出doGet中如何获取表单参数并转发到success.jsp”的题考的就是这个项目里你踩过的坑。它不炫技但把HTTP协议、容器模型、线程安全、SQL注入防护、字符编码链路这些黑匣子全摊开在你眼皮底下。适合刚学完Java基础、正卡在“知道语法但不会组织Web项目”阶段的开发者也适合想回炉重造、验证自己是否真懂Web容器本质的熟手。别被.zip后缀骗了——源码只是起点跑通、调通、压测通、改通才是这门课的及格线。2. 从零搭建Servlet环境用最简依赖跑通第一个HelloWorldServlet2.1 为什么不用Spring Boot先搞懂容器底座再谈封装新手常问“现在都用Spring Boot了为啥还要学Servlet”答案很直白Spring Boot的RestController本质是Servlet的封装它的DispatcherServlet就是个标准Servlet实例。当你遇到RequestBody解析失败、Filter顺序错乱、或线上CPU飙升却查不到具体Servlet线程栈时所有排查路径最终都会回到javax.servlet.http.HttpServlet的service()方法入口。本项目坚持用原生Servlet不是守旧而是强制你建立“请求进来→容器分发→Servlet处理→响应返回”这条不可跳过的认知链。我们不引入Tomcat Maven插件而是手动部署WAR包因为只有把web.xml、WEB-INF/lib、classes目录结构亲手铺一遍你才会真正理解“为什么静态资源要放webapp下而class文件必须进WEB-INF/classes”。2.2 JDK与Tomcat版本对齐避开“源发行版17需要目标发行版17”这类编译玄学很多同学卡在第一步IDEA里写好Servlet一运行就报错java: 警告: 源发行版 17 需要目标发行版 17。这不是代码问题是环境链断裂。本项目实测稳定组合为JDK 17 Apache Tomcat 10.1.x注意不是9.x。Tomcat 10.0已全面转向Jakarta EE 9命名空间jakarta.servlet.*若误用Tomcat 9仍用javax.servlet.*编译时会提示找不到类若JDK版本低于17而Maven设了source17/source则触发上述警告。正确配置三步走JDK确认终端执行java -version输出必须含17.x.xTomcat下载去官网下载apache-tomcat-10.1.26.zip2024年最新稳定版解压后记下路径如/opt/tomcatIDEA项目SDK绑定File → Project Structure → Project → SDK选JDK 17Project → Project language level选17Modules → Sources → Language level选17提示不要用Mac Homebrew或Linux apt安装的Tomcat它们常被系统包管理器降级。务必手动下载官方二进制包。2.3 手动创建标准Web项目结构拒绝IDE自动生成的“假标准”很多教程教你在IDEA里点“New Project → Java Enterprise → Web Application”结果生成一堆pom.xml和src/main/webapp看似省事实则埋雷——当你要手动添加log4j2.xml或修改web.xml的session-config时IDEA自动生成的结构常把配置文件塞错位置。我们坚持手工建目录确保每个路径都符合Servlet规范student-system/ ├── src/ │ └── main/ │ ├── java/ │ │ └── com/example/student/ # Servlet、DAO、Bean包 │ ├── resources/ # db.properties等配置文件 │ └── webapp/ # 前端资源根目录 │ ├── index.jsp │ ├── WEB-INF/ │ │ ├── web.xml # 核心部署描述符 │ │ └── views/ # JSP视图存放处非必须但推荐 │ └── static/ # CSS/JS/IMG等静态资源 └── pom.xml # Maven依赖管理关键点WEB-INF必须全大写且必须位于webapp/下一级web.xml必须放在WEB-INF/内classes目录由Maven编译后自动生成无需手动创建。2.4 编写第一个Servlet用最简代码验证容器连通性在src/main/java/com/example/student/下新建HelloServlet.javapackage com.example.student; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; WebServlet(/hello) public class HelloServlet 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(h1Hello from Servlet!/h1); out.println(pCurrent time: System.currentTimeMillis() /p); } }注意使用jakarta.servlet.*而非javax.servlet.*Tomcat 10要求WebServlet(/hello)替代web.xml配置更轻量resp.setContentType(text/html;charsetUTF-8)必须显式设置否则中文会乱码编译后将项目打包成WAR# 在student-system根目录执行 mvn clean package -Dmaven.test.skiptrue生成的target/student-system-1.0-SNAPSHOT.war复制到$TOMCAT_HOME/webapps/下启动Tomcat$TOMCAT_HOME/bin/startup.sh # Linux/Mac # 或 $TOMCAT_HOME/bin/startup.bat # Windows访问http://localhost:8080/student-system-1.0-SNAPSHOT/hello看到标题即证明Servlet容器链路打通。3. 数据库层落地MySQL连接池配置与防SQL注入实战3.1 为什么不用H2或Derby真实项目必须直面MySQL字符集与权限坑学生系统看似简单但数据库环节最容易翻车。有人用H2内存数据库跑通就以为OK结果上线MySQL立刻报错Incorrect string value: \xE4\xBD\xA0\xE5\xA5\xBD for column name——这是UTF-8mb4字符集未生效的典型症状。本项目强制使用MySQL 8.0原因有三字符集可控MySQL 8.0默认utf8mb4_0900_as_cs支持emoji和生僻汉字权限模型清晰可精确控制student_db库的SELECT/INSERT/UPDATE/DELETE权限避免开发库用root账号的坏习惯驱动兼容性强mysql-connector-java:8.0.33完美适配JDK 17。建库语句必须带字符集声明CREATE DATABASE student_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs;3.2 手写DBUtil工具类比Druid/Hikari更透彻理解连接池本质虽然HikariCP是当前性能最优连接池但本项目选择手写简易连接池DBUtil.java目的不是造轮子而是让你看清“连接复用”如何实现。核心逻辑只有三步启动时预创建5个Connection存入LinkedListConnectiongetConnection()时从链表取一个取完移除closeConnection()时不是真的关闭而是把Connection加回链表package com.example.student.util; import com.mysql.cj.jdbc.MysqlDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; import java.util.LinkedList; import java.util.Queue; public class DBUtil { private static final int POOL_SIZE 5; private static final QueueConnection connectionPool new LinkedList(); private static DataSource dataSource; static { MysqlDataSource ds new MysqlDataSource(); ds.setUrl(jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue); ds.setUser(student_user); ds.setPassword(Stu2024); dataSource ds; // 预热连接池 for (int i 0; i POOL_SIZE; i) { try { connectionPool.offer(dataSource.getConnection()); } catch (SQLException e) { e.printStackTrace(); } } } public static Connection getConnection() throws SQLException { Connection conn connectionPool.poll(); if (conn null || conn.isClosed()) { return dataSource.getConnection(); // 池空则新建 } return conn; } public static void closeConnection(Connection conn) { if (conn ! null !connectionPool.contains(conn)) { connectionPool.offer(conn); // 归还连接 } } }注意serverTimezoneAsia/Shanghai必须显式指定否则MySQL 8.0会因时区不匹配抛The server time zone value XXX is unrecognized异常。3.3 DAO层防SQL注入PreparedStatement是唯一正解拼接字符串必死学生信息表student含id,name,age,grade,class_name字段。常见错误写法// ❌ 危险直接拼接参数 String sql SELECT * FROM student WHERE name name ; Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql);攻击者输入name zhangsan OR 11SQL变成SELECT * FROM student WHERE name zhangsan OR 11直接绕过校验。正确做法StudentDAO.java片段public ListStudent findByName(String name) throws SQLException { String sql SELECT id, name, age, grade, class_name FROM student WHERE name LIKE ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % name %); // ?占位符setXxx方法自动转义 try (ResultSet rs ps.executeQuery()) { ListStudent list new ArrayList(); while (rs.next()) { Student s new Student(); s.setId(rs.getLong(id)); s.setName(rs.getString(name)); s.setAge(rs.getInt(age)); s.setGrade(rs.getString(grade)); s.setClassName(rs.getString(class_name)); list.add(s); } return list; } } }关键点PreparedStatement的?占位符由JDBC驱动底层转义、;、--等注入字符全部失效try-with-resources确保Connection、PreparedStatement、ResultSet三层自动关闭避免连接泄漏ps.setString(1, ...)中索引从1开始不是0。4. 前端交互与会话管理JSPServlet协作中的编码与状态陷阱4.1 JSP页面三重编码防线从HTML头到request.setCharacterEncoding学生系统前端用JSP而非Vue/React不是技术倒退而是为了暴露HTTP协议的真实约束。一个典型场景用户在add.jsp输入姓名“张三”提交后后台req.getParameter(name)得到“å¼ ä¸”。这不是Bug是编码链路断裂。必须同时守住三道关JSP页面声明add.jsp顶部% page contentTypetext/html;charsetUTF-8 languagejava % !DOCTYPE html html head meta charsetUTF-8 !-- HTML5标准声明 -- title添加学生/title /head表单提交方式必须用methodpostGET方式URL编码不可控Servlet中强制编码AddStudentServlet.javaOverride protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 关键POST请求必须设 String name req.getParameter(name); // ... 处理逻辑 }提示req.setCharacterEncoding()只对POST有效GET参数需在Tomcat的server.xml中配置URIEncodingUTF-8本项目已在$TOMCAT_HOME/conf/server.xml的Connector节点添加该属性。4.2 HttpSession管理学生登录态为什么不能用Cookie存密码学生系统需登录才能操作但绝不能把密码明文存Cookie正确方案是用户登录成功后生成唯一token如UUID存入HttpSessionHttpSession session req.getSession(true); session.setAttribute(user, student); // 存Student对象非密码 session.setMaxInactiveInterval(1800); // 30分钟无操作失效所有受保护Servlet如ListStudentServlet开头校验HttpSession session req.getSession(false); if (session null || session.getAttribute(user) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; }登出时调用session.invalidate()彻底销毁会话。这样设计的好处密码永不离开服务端规避Cookie窃取风险setMaxInactiveInterval可防会话固定攻击session.getAttribute(user)拿到的是内存对象比每次查DB快10倍。4.3 JSP中EL表达式与JSTL标签告别%%脚本片段老式JSP常混用Java代码块% %和表达式% %, 如!-- ❌ 混乱且难维护 -- % ListStudent list (ListStudent) request.getAttribute(studentList); % % for(Student s : list) { % trtd% s.getName() %/td/tr % } %本项目强制使用ELExpression Language JSTLJavaServer Pages Standard Tag Library% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !-- ✅ 清晰分离逻辑与视图 -- c:forEach items${studentList} vars tr td${s.name}/td td${s.age}/td td${s.grade}/td td a hrefdelete?id${s.id}删除/a /td /tr /c:forEach优势${studentList}自动从request/session/page/application作用域查找无需强转c:forEach标签内置空值安全studentList为null时不报错EL表达式${s.name}自动调用getXXX()比% s.getName() %少写一半代码。5. 避坑指南生产环境部署前必须解决的5个致命问题5.1 现象Tomcat启动后访问404控制台无报错原因WAR包名含-SNAPSHOT或-1.0等Maven版本号而浏览器访问路径未同步。例如WAR包名为student-system-1.0-SNAPSHOT.war则应用上下文路径Context Path默认为/student-system-1.0-SNAPSHOT访问http://localhost:8080/hello必然404。解决方案1推荐修改pom.xml中finalName标签build finalNamestudent-system/finalName /build重新mvn package后WAR包名为student-system.war上下文路径即/student-system方案2在$TOMCAT_HOME/conf/server.xml的Host节点内添加Context path/student docBasestudent-system-1.0-SNAPSHOT reloadabletrue/此时访问http://localhost:8080/student/hello即可。5.2 现象中文姓名插入MySQL后显示问号????原因MySQL服务端、数据库、表、连接URL四层字符集未统一为utf8mb4。仅建库时设CHARACTER SET utf8mb4不够表和列也需指定。解决检查MySQL服务端配置/etc/my.cnf或C:\ProgramData\MySQL\MySQL Server 8.0\my.ini[client] default-character-set utf8mb4 [mysql] default-character-set utf8mb4 [mysqld] character-set-server utf8mb4 collation-server utf8mb4_0900_as_cs重启MySQL服务对现有表执行ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_as_cs;5.3 现象登录后跳转到list.jsp但页面显示studentList为空控制台却打印出查询结果原因Servlet中req.setAttribute(studentList, list)后未调用RequestDispatcher.forward()而是用了response.sendRedirect()。后者是客户端重定向request作用域数据丢失。解决// ❌ 错误sendRedirect导致request属性丢失 resp.sendRedirect(list.jsp); // ✅ 正确forward保持request作用域 RequestDispatcher dispatcher req.getRequestDispatcher(/WEB-INF/views/list.jsp); dispatcher.forward(req, resp);注意/WEB-INF/views/list.jsp路径以/WEB-INF/开头外部无法直接访问安全性更高。5.4 现象高并发测试时新增学生ID出现重复如两个请求都生成ID100原因StudentDAO.addStudent()方法中ID生成逻辑为SELECT MAX(id) FROM student在多线程下存在竞态条件Race Condition。解决方案1推荐MySQL表id设为BIGINT AUTO_INCREMENT PRIMARY KEY插入时INSERT INTO student(name,age,...) VALUES(?,?,?)让数据库自增方案2若必须应用层生成用AtomicLong全局计数器private static final AtomicLong idGenerator new AtomicLong(100000); long id idGenerator.incrementAndGet();5.5 现象Tomcat日志中频繁出现SEVERE: Servlet.service() for servlet [jsp] threw exception但页面功能正常原因JSP编译缓存损坏或web.xml中welcome-file-list未配置index.jsp导致Tomcat尝试访问不存在的默认页。解决清空Tomcat临时文件删除$TOMCAT_HOME/work/Catalina/localhost/下对应应用目录确保web.xml含welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list重启Tomcat。6. 进阶技巧用Filter统一处理编码与登录校验让代码瘦30%6.1 编码Filter一行配置终结所有乱码前面在每个Servlet的doPost()里写req.setCharacterEncoding(UTF-8)既重复又易漏。用Filter可全局拦截package com.example.student.filter; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) // 拦截所有请求 public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); // 放行给后续Filter或Servlet } }只需在web.xml中注册或用WebFilter注解所有POST请求自动编码doPost()里再也不用写那行代码。6.2 登录校验Filter用URL模式匹配精准放行免登录资源把登录校验逻辑从每个Servlet中抽离用Filter实现“白名单”机制WebFilter(urlPatterns /*) public class AuthFilter implements Filter { private static final SetString EXCLUDE_PATHS Set.of( /login.jsp, /LoginServlet, /css/, /js/, /images/ ); Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getServletPath(); // 白名单资源直接放行 if (EXCLUDE_PATHS.stream().anyMatch(path::startsWith)) { chain.doFilter(req, resp); return; } // 非白名单资源检查登录态 HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }此Filter将/css/bootstrap.css、/js/jquery.js等静态资源和登录接口排除在外只对/list.jsp、/add等业务路径校验逻辑清晰且可扩展。6.3 Filter链执行顺序Order注解失效用web.xml明确定义当项目中有多个Filter如EncodingFilter、AuthFilter、LogFilter执行顺序至关重要必须先编码再校验否则req.getParameter()会因编码未设而乱码。WebFilter的order属性在部分Tomcat版本中不生效最可靠方式是用web.xml声明filter filter-nameencodingFilter/filter-name filter-classcom.example.student.filter.EncodingFilter/filter-class /filter filter filter-nameauthFilter/filter-name filter-classcom.example.student.filter.AuthFilter/filter-class /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter-mapping filter-nameauthFilter/filter-name url-pattern/*/url-pattern /filter-mappingTomcat按filter-mapping在web.xml中出现的先后顺序执行Filter确保编码Filter永远在前。6.4 实战参数表Filter配置关键参数速查参数名取值示例说明必填url-pattern/*匹配所有请求路径是dispatcherREQUEST,FORWARD,INCLUDE,ERROR指定拦截的请求类型默认仅REQUEST否默认够用async-supportedtrue是否支持异步处理如长轮询需设为true否本项目同步场景设false我当年第一次写Filter时在web.xml里把两个filter-mapping顺序写反导致登录校验总拿不到正确参数debug三天才发现是Filter执行顺序错了。后来养成习惯所有Filter配置必须手写web.xml绝不依赖注解顺序。希望帮到你。本文还有配套的精品资源点击获取