ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Servlet核心原理与JavaWeb实战:从生命周期到增删改查

2026/9/30 9:27:56 拓冰建站 浏览量
Servlet核心原理与JavaWeb实战:从生命周期到增删改查 1. 先把Servlet放回它该在的位置这几年不少同学是直接从SpringBoot入门JavaWeb开发的习惯了写一个Controller然后丢一个GetMapping对Servlet反而有些陌生。但只要你往后翻一翻框架的底层或者哪天真被拉去维护一个老系统、接手一个课设项目你就会发现JavaWeb这套东西从来就没有消失过Servlet就是整个JavaWeb处理请求的那条主线。这篇内容我围绕JavaWeb里的核心组件Servlet来写会讲清楚它到底是干什么的Tomcat容器和它之间是什么关系Servlet生命周期里那几个方法分别在什么时候被调用然后我会完整走一遍在IDEA里把一个Servlet项目跑起来、连上MySQL、实现增删改查的流程最后把这些年踩过的坑和排查方法整理成一份速查表。适合正在学JavaWeb的学员、准备做课设或毕设项目的同学以及那些用SpringBoot但一直没搞懂底层原理的人。我尽量少讲空泛的概念多给可以直接照做的东西。基础薄弱的读者也不用担心遇到关键概念我会用生活化的类比拆开讲保证你能看懂每一步在干什么。2. Servlet在JavaWeb里到底是干什么的2.1 一次HTTP请求的旅程先把问题简单化你在浏览器里输入一个网址回车发生了什么请求先是到了Web服务器比如TomcatTomcat拿到这个HTTP请求之后并不会自己去处理业务逻辑它只负责把HTTP报文解析成Java能理解的对象然后找到对应的Servlet把活派出去。Servlet处理完再把结果封装成HTTP响应还给TomcatTomcat最后把响应报文发送回浏览器。换句话说Servlet就是Java世界中专门处理HTTP请求的那一层组件。你在SpringBoot里写的Controller本质上也是通过一个叫DispatcherServlet的核心Servlet转发过去的。SpringMVC不管包装得多花哨底下干的还是Servlet的活接收请求、解析参数、调用业务、输出响应。2.2 Tomcat和Servlet是雇佣关系Tomcat严格来说是一个Servlet容器。它负责的就是Servlet的“生老病死”什么时候创建Servlet实例、什么时候调用哪个方法、请求结束之后实例要不要销毁全由容器说了算。程序员只需要把Servlet类写好然后在web.xml里声明一下或者用WebServlet注解标记一下剩下的交给容器。这个关系可以理解成酒店前台和客房服务员Tomcat是酒店的大堂和调度系统Servlet是每个客房服务员。客人HTTP请求到了前台Tomcat根据房间号找到对应的服务员服务员干完活之后回到待命状态前台继续接待下一位客人。服务员不是每次客人来都重新招一个也不是酒店一开始营业就把所有服务员全部招齐而是有需要才安排这批服务员还能同时服务不同客人——这就是后面要说的Servlet生命周期和线程模型。2.3 为什么现在学SpringBoot也要懂Servlet我之前带过几个实习生他们SpringBoot用得挺溜但问到“Filter和Interceptor有什么区别”就开始含糊。其实Filter就是Servlet规范里定义的东西它和Servlet一样需要容器来管理只是它的职责是请求的“前置过滤”。你配SpringSecurity、配跨域过滤器底层都是Servlet Filter在工作。另一个很现实的场景排查线上问题的时候你打开Tomcat日志里面全是http-nio-8080-exec-10这种线程名看不懂这些线程怎么来的就很难定位并发问题。Tomcat每收到一个HTTP请求就会从线程池里取一个线程在这个线程里调用Servlet的service方法。如果你不理解Servlet的线程模型写出来的代码很容易在多线程环境下出事故。后面实战部分我会单独讲这个坑。所以结论很简单Servlet不是过时技术它是JavaWeb的地基。地基没打牢楼层再高也是危房。3. Servlet生命周期面试必考实战必踩3.1 三个关键方法一次说清Servlet规范规定了一个Servlet从出生到销毁要经历三个阶段对应三个方法init(ServletConfig config)初始化阶段service(ServletRequest req, ServletResponse res)服务阶段destroy()销毁阶段这个生命周期全程由Servlet容器来调用程序员自己不要直接去调用。你只需要继承HttpServlet重写其中需要的方法即可。初始化只做一次。HttpServlet这个类在第一次被请求访问时由Tomcat实例化随后立即调用init完成初始化。这也是为什么init方法适合做数据库连接、加载配置资源这类只需要执行一次的操作。如果你有多个Servlet实例它们各自有各自的init别把公共资源初始化塞到每个Servlet里那样会重复创建连接浪费资源。3.2 用“入职—上班—离职”的类比理解我把Servlet生命周期拉到现实场景里帮你记忆init好比一个员工入职公司会给他分配工位、配好电脑这相当于容器创建Servlet实例并调用initservice就像每天的日常工作客户有需要就通过前台转给你处理你处理一个客户再处理下一个一天可能处理几百个客户destroy就是员工离职公司会回收电脑、工牌做最后的收尾工作之后这个员工就不再存在了。有一个细节很多人没注意Servlet实例化并不一定发生在服务器启动时。默认情况下Tomcat是在Servlet第一次被请求时才创建实例也就是说第一个访问该Servlet的用户会承担额外的创建耗时。如果你想在服务器启动的时候就完成Servlet的创建可以在web.xml里加一个load-on-startup标签或者注解里写loadOnStartup值越小启动优先级越高。3.3 单实例多线程模型是最大的坑Servlet是“单实例多线程”的。什么意思一个Servlet在整个生命周期里只有一个对象实例但Tomcat会同时用很多线程来调用这个对象的service方法。也就是说你这个Servlet里的成员变量是所有请求共享的。一旦你写出了这样的代码public class UserServlet extends HttpServlet { private String name; // 注意这个成员变量 Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { name req.getParameter(name); // 模拟耗时操作 Thread.sleep(100); resp.getWriter().write(name); } }看起来没什么问题但两个线程同时进来线程A把name设成了“张三”线程B把name改成“李四”线程A再去读取name时读到的是“李四”数据就串了。解决方法是把变量定义到方法内部变成局部变量或者用ThreadLocal做线程隔离再不行就加锁但加锁会影响性能一般不建议优先考虑。事务、分页这些常见的需求也要注意写Servlet代码时的习惯是能不用成员变量就不用成员变量。如果确实要共享状态用ServletContext存应用级数据但要清楚它的生命周期是整个应用级别的这样才不会遇到“数据莫名其妙变来变去”这种诡异问题。3.4 HTTP协议相关的其他生命周期细节HttpServlet在service方法内部做了分派它会根据请求方法自动调用doGet、doPost这组对应的方法。如果你没有重写某个方法默认会返回405。所以写Servlet的时候你要想清楚自己的接口支持哪些请求方式别只重写doGet不管doPost前端一换成POST请求就报错。另外还要注意一个Servlet是可以映射到多个URL路径的也可以在注解里配置多个urlPatterns。真正决定调用哪个Servlet的是Tomcat的URL匹配规则有精确匹配、通配符匹配、扩展名匹配优先级依次递减。遇到请求路径匹配不对的情况先检查自己的映射规则是不是和请求路径对得上。4. IDEA里跑通一个Servlet项目配置实操4.1 环境版本选择动手之前先打好版本组合。我这边测试下来比较稳定的一套是JDK 8 或 JDK 11Tomcat 8.5 或 Tomcat 9IntelliJ IDEA 2021 及以上版本Maven 3.6 以上如果你用JDK 17建议对应Tomcat 10以上因为Tomcat 10里Servlet包名从javax.servlet改成了jakarta.servlet很多老代码直接迁移会报编译错误。对于初学者我的建议是直接用Tomcat 9加JDK 8或11的组合网上案例最多踩坑最少。4.2 创建方式Maven Webapp是首选我在教学中见过不少同学还在手动往WEB-INF/lib里拷贝jar包这种做法不是不行但jar包冲突、版本管理会让你非常痛苦。现在最推荐用Maven创建JavaWeb项目依赖全部写在pom.xml里交给Maven统一下载管理。具体步骤IDEA里选择新建项目左侧选Maven勾选Create from archetype选中maven-archetype-webapp。填好GroupId和ArtifactIdGroupId一般用公司域名倒写比如com.exampleArtifactId就是项目名。项目生成后的目录结构里会自带一个src/main/webapp目录JSP和静态资源放在这里。在pom.xml里添加Servlet依赖和Tomcat插件。依赖这样加dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency注意provided这个scope意思是编译时需要、运行不需要因为Tomcat自己已经带了一套Servlet API。如果你是Tomcat 10及以后版本把groupId换成jakarta.servletartifactId换成jakarta.servlet-api。4.3 配置Tomcat运行环境IDEA里跑JavaWeb项目有两种方式一种是用外部安装的Tomcat另一种是用Maven自带的Tomcat插件。我比较推荐第一种因为更贴近真实部署流程。步骤是先下载一个Tomcat压缩包解压到一个固定目录然后回到IDEA选择Run菜单里的Edit Configurations点击加号找到Tomcat Server里的Local在弹出的窗口里指定Tomcat的安装目录部署那一栏点加号选择Artifact选择你项目的war包或war exploded格式。这里有个细节war和war exploded的区别很大。war是打包好的压缩包相当于真实部署时会用到的形式war exploded是解压后的目录IDEA可以直接把编译后的class和资源文件增量同步到Tomcat里修改代码后热加载速度会快很多。开发阶段建议用war exploded。启动后如果浏览器访问会404大概率是Application context路径没设置对。默认是http://localhost:8080/项目名/你要访问具体Servlet就需要在路径后面拼上Servlet的映射地址。4.4 第一个Servlet从代码到页面在项目里新建一个类继承HttpServlet加上WebServlet注解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; WebServlet(/hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html;charsetUTF-8); resp.getWriter().write(h1JavaWeb 的第一个 Servlet/h1); resp.getWriter().write(p当前请求地址 req.getRequestURL() /p); } }然后启动Tomcat在浏览器输入http://localhost:8080/项目名/hello看到页面输出说明你的Servlet环境已经完整跑通了。顺便说一句这个resp.setContentType(text/html;charsetUTF-8)别省。不设置编码的话中文会乱码。后面案例里我会专门展开讲编码问题。5. 完整案例Servlet MySQL 实现用户管理5.1 案例需求和数据库设计只写一个输出Hello的Servlet意义不大我带你做一个能连数据库的用户管理小系统这也是很多学校课设项目的经典原型用户管理包含用户列表展示、新增用户、删除用户、修改用户。先设计数据库表用MySQL建表语句CREATE DATABASE IF NOT EXISTS user_db DEFAULT CHARACTER SET utf8mb4; USE user_db; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码, email VARCHAR(100) COMMENT 邮箱, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里我特意设置了utf8mb4字符集能存真正的emoji表情和更多字符比老的utf8兼容性更好。MySQL 5.7以上都支持直接照用就行。5.2 项目分层别把代码全堆在Servlet里一个规范的小项目至少分成三层实体类、数据访问层DAO、控制层Servlet。如果你后面要接JSP做页面渲染还可以加一个JSP展示层。我见过很多初学者把数据库查询代码直接写在Servlet里一个Servlet几百行看起来也能跑但后面改需求就会想哭。正确做法是把职责拆开entity包放与数据库表字段对应的JavaBeandao包负责数据库操作封装增删改查servlet包接收请求、调用DAO、跳转页面或返回JSONutil包放数据库连接工具类这个分层模型不光是Servlet阶段用你后面学Spring的时候会发现SpringMVC的Controller、Service、Mapper分层思路和这个是一脉相承的。实体类如下public class User { private Integer id; private String username; private String password; private String email; private Date createTime; // getter、setter 方法省略IDE 会自动生成 }5.3 JDBC连接工具类关键参数详解这里用纯JDBC写数据库访问不引入MyBatis因为重点是要让你看清底层每一步在做什么。先写一个JDBC工具类import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/user_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USERNAME root; private static final String PASSWORD 你的数据库密码; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }JDBC连接串里几个参数非常关键useSSLfalse本地开发没必要启用SSL加密去掉可以省去建立连接的握手时间serverTimezoneAsia/Shanghai是必加项MySQL 8.0以后不指定时区会直接报错characterEncodingutf8确保字符的传输编码正确。密码我没写成配置文件真实项目中建议放到properties文件里再把文件加入.gitignore防止密码泄露。5.4 DAO层代码增删改查逐一走读DAO层写一个UserDao类用JDBC操作t_user表。我挑两个典型方法展开讲。查询所有用户public ListUser findAll() { ListUser list new ArrayList(); String sql SELECT id, username, password, email, create_time FROM t_user; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setPassword(rs.getString(password)); user.setEmail(rs.getString(email)); user.setCreateTime(rs.getTimestamp(create_time)); list.add(user); } } catch (SQLException e) { throw new RuntimeException(查询用户列表失败, e); } return list; }注意我这里用的是PreparedStatement而不是直接拼SQL字符串的Statement这是重要的习惯PreparedStatement预编译SQL可以防止SQL注入而且执行效率更高。你以后写持久层框架它们内部底层也是PreparedStatement。新增用户方法public int insert(User user) { String sql INSERT INTO t_user (username, password, email) VALUES (?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getEmail()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(新增用户失败, e); } }这里有一个细节通过ps.setString传入值不要用字符串拼接的方式写SQL比如INSERT INTO t_user VALUES( username )这种写法非常危险一旦用户名里出现单引号就能破坏SQL语法遇到恶意输入就直接被注入。用?占位符加setString既安全又清晰。5.5 Servlet里如何调用DAOUserServlet接收浏览器的请求根据action参数判断要执行什么操作WebServlet(/user) public class UserServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String action req.getParameter(action); if (list.equals(action)) { ListUser users userDao.findAll(); req.setAttribute(users, users); req.getRequestDispatcher(/user_list.jsp).forward(req, resp); } else if (add.equals(action)) { User user new User(); user.setUsername(req.getParameter(username)); user.setPassword(req.getParameter(password)); user.setEmail(req.getParameter(email)); userDao.insert(user); resp.sendRedirect(user?actionlist); } else if (delete.equals(action)) { int id Integer.parseInt(req.getParameter(id)); userDao.deleteById(id); resp.sendRedirect(user?actionlist); } } }这里有两个容易出错但很关键的细节。第一个是req.setCharacterEncoding(UTF-8)必须放在获取参数之前否则POST提交的中文参数会乱码。第二个是新增和删除数据之后要用sendRedirect做重定向避免用户刷新浏览器时重复提交。如果你直接forward到列表页面刷新一下就会把刚才的插入操作再执行一遍数据就重复了。这是Web开发里经典的PRG模式Post-Redirect-Get。5.6 JSP页面做展示JSP放webapp目录下简单写一个list页面% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title用户列表/title /head body h3用户列表/h3 table border1 tr thID/th th用户名/th th邮箱/th th操作/th /tr c:forEach items${users} varu tr td${u.id}/td td${u.username}/td td${u.email}/td tda hrefuser?actiondeleteid${u.id}删除/a/td /tr /c:forEach /table a hrefuser_add.jsp新增用户/a /body /htmlJSTL标签库的引入要提前在pom.xml里加依赖如果你觉得麻烦这一步可以退回去用Java代码在Servlet里把HTML拼接输出。虽然JSPJSTL是常见组合但我更建议新手先搞清楚JSP的本质它就是被容器解析翻译成Servlet的模板文件页面里能写HTML也能嵌Java代码。等你理解透这个后面用模板引擎或前后端分离方案都会轻松很多。5.7 MySQL驱动加载和运行的连贯流程数据库驱动jar包要通过Maven添加写pom.xml里dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency你如果数据库是MySQL 5.x这个驱动版本也能用。整个过程跑下来的顺序是启动MySQL创建数据库和表然后IDEA里启动Tomcat浏览器访问http://localhost:8080/项目名/user?actionlist列表加载出来说明全链路通了。如果页面报500打开IDEA控制台看堆栈绝大多数错误是驱动类找不到或连接串写错。6. 常见问题速查对号入座解决报错6.1 高频错误查找表我直接给你一份问题对照表遇到报错先来这里找答案报错现象常见原因解决方案访问URL返回404项目context路径不对或Servlet映射写错检查浏览器地址和web.xml/WebServlet映射启动Tomcat就端口占用8080端口被占用找到占用进程关掉或改Tomcat端口中文显示乱码请求或响应编码未设置添加req.setCharacterEncoding和resp.setContentTypeClassNotFoundException: com.mysql.cj.jdbc.DriverMySQL驱动jar未引入pom.xml添加mysql-connector依赖Access denied for user数据库账号密码错误检查DBUtil里的USERNAME和PASSWORDCommunications link failureMySQL没启动或连接串错误确认MySQL服务已启动检查URL里的端口方法返回405Servlet没有重写对应的doGet/doPost补上对应方法或重写service6.2 Servlet项目排错方法论报错之后不要只盯着那一行红字看先分清楚是哪个环节出了问题。我的排错顺序是先看Tomcat日志里有没有异常堆栈有堆栈信息就说明代码或依赖有问题没有堆栈但访问404则是映射或者路径问题浏览器直接显示无法连接才是服务没启动。排查请求环节的时候我习惯先用浏览器开发者工具看Network标签页把请求URL、请求方法、状态码看清楚。404和405的区别很大404是没有这个资源405是方法不允许。很多时候前端报错是POST请求没配对后端doPost方法或者URL路径多了一层斜杠。6.3 几条保命题的实操经验调试Servlet的时候用System.out.println输出关键变量是最直观的方式开发阶段不要觉得Low很多老程序员排查复杂问题也是先加日志。IDEA里给Servlet打断点非常方便但你要确保启动方式选的是Tomcat的Debug模式而不是普通Run。断点打在service方法入口就能看到请求参数和数据的具体值。这个方法比反复猜测强太多。一个容易被忽略的问题修改了web.xml或注解后有时需要重启Tomcat才生效。IDEA的Hot Swap不能帮你在运行中加载新的Servlet映射遇到改了映射不生效的情况不要怀疑编码问题重启一下服务就好。7. 我对Servlet学习路线的几点实在话从Servlet到SpringBoot这条路线绕不过去我建议你把这个小项目完完整整做一遍遇到报错自己排查不要一卡住就抄答案。头歌实训里的答案能帮你过任务但做项目时没有人替你踩坑踩过的坑才是真正学到的东西。我个人在带新人时有一个要求先不碰框架纯Servlet JSP MySQL可以实现一个增删改查项目然后在这个基础上过渡到Spring SpringMVC。你会发现框架的本质就是帮你把原来手工做的事封装好了参数注入替代了手动getParameterORM替代了手写JDBCIOC容器帮你管理了那些对象的创建与销毁。但理解底层之后再接触封装你才清楚每一层解决的是什么问题。后续你还可以从这个项目继续扩展给密码加上MD5或BCrypt加密存储、用Filter写一个登录校验拦截未登录用户、用Session维持登录状态、把JSP换成AJAX前后端交互。这些扩展点每一个都能单独撑起一篇技术笔记也正好是JavaWeb课设里最常出现的要求。最后再补一句实在话Servlet生命周期那套东西面试的时候经常被问到但更重要的是它直接决定了你怎么写出线程安全的Web代码。把前面的单实例多线程模型理解透以后并发编程、框架设计学起来都会顺畅很多。