恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
HttpServlet深度剖析:生命周期、方法详解与实战踩坑指南
首页
资讯中心
/
HttpServlet深度剖析:生命周期、方法详解与实战踩坑指南
HttpServlet深度剖析:生命周期、方法详解与实战踩坑指南
发布时间:2026/9/9 17:49:17
1. 初识HttpServletJava Web开发绕不开的基石1.1 没有框架的年代我们怎么处理HTTP请求如果你入行比较晚可能一上来学的就是Spring Boot见过最多的就是RestController和RequestMapping。但如果你真的想弄清楚一个HTTP请求从浏览器发出之后后端到底是怎么把它接住、怎么解析、怎么分发给对应代码的那HttpServlet就是绕不开的第一课。HttpServlet是Java Servlet规范里专门处理HTTP协议的一个抽象类。它不在JDK里而是在Servlet API里也就是javax.servlet.http.HttpServlet。你在Maven里加依赖时经常看到的javax.servlet-api它定义的就是这一整套东西。任何Java Web应用不管是传统的JSP项目、Spring MVC项目还是Spring Boot内嵌Tomcat跑起来的服务底层请求处理链路中都有Servlet的身影。在Spring MVC这类框架出现之前Java Web开发的标准姿势就是写一个类继承HttpServlet重写doGet、doPost这些方法然后把请求参数、业务逻辑、页面跳转全部自己管起来。框架出现之后DispatcherServlet替我们挡在了前面但我们依然可以基于HttpServlet做很多底层的事比如写过滤器、写拦截器、做文件上传的入口控制甚至接手维护遗留系统。1.2 Servlet容器与HttpServlet的分工逻辑要说清楚HttpServlet必须先搞清楚它和Tomcat或者说Servlet容器的关系。很多人在这一层就糊了以为Tomcat就是Servlet其实它们是两回事。Servlet容器是负责运行Servlet的环境最典型的有Tomcat、Jetty、Undertow。它的职责包括监听端口、接收TCP连接、解析HTTP报文、把请求封装成HttpServletRequest对象、把响应封装成HttpServletResponse对象然后找到对应的Servlet去调用最后再把Servlet写出的数据转成HTTP响应发回浏览器。而HttpServlet本身只是业务代码与HTTP协议之间的桥。它不关心你监听哪个端口不关心TCP粘包拆包它只关心一件事——当容器把一个封装好的HTTP请求对象丢给你的时候你怎么把里面有用的信息拿出来处理业务再把结果填到响应对象里还回去。这个分工其实很像餐厅的运作逻辑。Tomcat是餐厅本身负责接客、安排座位、传菜HttpServlet是厨师只负责把客人点的菜做出来。客人不会直接跑进后厨点菜厨师也不用管餐厅的选址和装潢。2. 核心方法拆解生命周期和请求处理的底层逻辑2.1 生命周期三件套init、service、destroyHttpServlet的生命周期方法一共就那么几个但每一个背后都有准确的调用时机。我见过不少开发写了两三年Java被问init方法什么时候执行、执行几次仍然答不上来。先看顺序。Servlet从被容器加载到被销毁会经历三个时间点。首先容器在启动时或者第一次接收到请求时创建Servlet实例紧接着调用init()方法做初始化。之后每一次请求进来容器都会调用service()方法去处理。最后应用停止或Servlet被卸载时容器调用destroy()方法让你做清理工作。init()默认是空实现你可以在里面做读取配置、初始化连接池、启动后台线程这类事情。它只执行一次所以适合放重量级的初始化逻辑。destroy()同样只执行一次适合释放资源。service()是每次请求都会触发的也就是整个Servlet里最核心的方法。这里有个很多人没注意过的细节init()方法有重载版本。init()是无参默认方法实际是被init(ServletConfig config)调用的而ServletConfig里存的是这个Servlet的web.xml配置信息。如果你在web.xml里给Servlet配置了init-param就可以在init(ServletConfig)里通过getInitParameter()读取。我见过有人把配置项写在静态变量里到处传其实用init-param是最基础也最规范的解法。service()方法的默认实现会做一次HTTP方法分发。它会根据请求的method字段GET、POST、PUT、DELETE、HEAD、OPTIONS、TRACE自动调用对应的doXxx方法。所以如果你只重写了doGet没有重写doPost那么POST请求进来时service()会直接返回405错误因为默认的doPost实现就是返回405。2.2 doGet与doPostHTTP谓词如何映射到Java方法doGet对应HTTP的GET请求doPost对应POST请求。这个映射关系听起来简单但实际使用中有很多容易踩的坑。GET请求通常用于查询操作参数拼在URL后面。POST请求通常用于提交数据参数放在请求体里。但这不是一个强制的技术约束你可以用GET传大量数据也可以用POST做查询但从规范和请求长度限制来看还是应该遵循常规用法。打开浏览器的开发者工具你会发现很多场景下页面表单的提交方式五花八门。比如form标签不指定method时默认是GET提交浏览器会把表单字段拼成URL query string发出去。如果你后端的doGet里写了修改数据的逻辑别人只要构造一个GET请求链接发给你你就中招了。所以GET请求的处理方法里不要写有副作用的业务逻辑这是安全和规范的双重要求。doHead、doPut、doDelete这些方法在实际开发中用得少但有一点需要知道如果客户端发了OPTIONS请求service()方法会根据Web应用配置自动判断允许哪些方法并在响应头里写入Allow字段。如果你自己重写了service()却忘了调用super.service()那这个自动分发机制就没了。2.3 HttpServletRequest和HttpServletResponse把输入输出搞清楚HttpServletRequest和HttpServletResponse是Servlet世界里最常接触的两个接口很多框架底层本质上是帮你封装了它们。请求对象里有几类信息要分清楚。第一类是请求行信息包括请求方法、请求URI、协议版本对应的方法是getMethod()、getRequestURI()、getProtocol()。第二类是请求头包括Content-Type、User-Agent、Cookie等对应方法getHeader()、getCookies()。第三类是参数分为URL query参数和表单提交的body参数。注意getParameter()会把URL参数和表单参数合并在一起返回也就是说不管参数是放在URL上还是表单里你用同一个方法都能拿到。响应对象的核心逻辑是设置状态码、写入响应头、输出响应体。状态码通过setStatus()设置响应头通过setHeader()设置响应体通过getWriter()写文本或getOutputStream()写二进制来写。有一点非常关键一旦你开始往响应Body里写入内容就不能再修改状态码和响应头了。因为HTTP协议里响应头必须在响应体之前发送。如果你在写入之后又调用sendRedirect()或者setStatus()实际上会抛出异常或失效。3. 手写一个HttpServlet从配置到运行全流程实操3.1 环境准备和第一个Servlet理论讲了这么多现在真正动手写一个。我用的是最传统的组合Tomcat 9 Servlet API 4.0。虽然用Spring Boot的人很多但为了看清Servlet底层机制建议你还是手动配一遍Web应用。创建项目时不用IDE的向导可以直接手动建一个标准Maven Web项目目录结构servlet-demo/ ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/demo/servlet/ │ │ │ └── HelloServlet.java │ │ └── webapp │ │ └── WEB-INF │ │ └── web.xmlpom.xml里加一个Servlet API依赖注意scope要设为provided因为Tomcat本身带了一套Servlet API如果你的包冲突了反而会报ClassCastException。dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency然后写第一个Servletpackage com.demo.servlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType(text/html;charsetUTF-8); PrintWriter writer resp.getWriter(); writer.println(html); writer.println(headtitleServlet Demo/title/head); writer.println(body); writer.println(h1Hello from HttpServlet/h1); writer.println(/body); writer.println(/html); } }web.xml里注册这个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 servlet servlet-namehelloServlet/servlet-name servlet-classcom.demo.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-namehelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping /web-app这里url-pattern的写法和Spring MVC里的WebServlet(/hello)本质上是一回事。3.2 处理请求参数GET、POST两种姿势参数处理是Servlet日常开发里最频繁的操作。我先说GET请求。Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String name req.getParameter(name); String age req.getParameter(age); // 处理业务... }浏览器请求/hello?namezhangsanage25时getParameter(name)返回zhangsangetParameter(age)返回25——注意这里是字符串。如果你要算年龄需要自己转成int类型并且做好异常处理。不要假设前端传的参数一定存在也不要想当然地认为格式一定正确。稳妥的做法是写一个工具方法先判空再转换。POST请求的区别在于参数在请求体里。如果前端用application/x-www-form-urlencoded格式提交那么getParameter()依然可以正常获取。提交上来的表单字段会在容器层被解析好放进同一个参数Map里。但如果是JSON格式也就是Content-Type: application/json那么getParameter()就拿不到数据了因为你得自己从getInputStream()里读出来再用JSON工具解析。我见过不少新手在这上面栽跟头。前端用Axios默认的Content-Type发JSON字符串后端Servlet里用getParameter(xxx)取怎么取都是null。正确做法是Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { StringBuilder sb new StringBuilder(); BufferedReader reader req.getReader(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } String json sb.toString(); // 然后用 Jackson 或 Gson 解析 }还有一个容易被忽略的点POST请求的body只能读一次。如果你在过滤器里读了一次输入流到Servlet里再读就变成空了。要解决这个问题可以做请求包装——用一个HttpServletRequestWrapper把body缓存到内存里重写getInputStream()和getReader()让它们支持重复读取。这个后面在排查问题里再展开说。3.3 请求转发与重定向这两种跳转别搞混Servlet里做页面跳转有两种方式每种都有适合的场景很多人分不清或者混着用。请求转发是服务端内部行为。代码形式是req.getRequestDispatcher(/otherServlet).forward(req, resp);转发发生时浏览器地址栏不会变客户端只发了一次请求。请求对象和响应对象被继续传递给了下一个Servlet或JSP因此请求域里存的属性通过req.setAttribute()设置在目标组件里还能读到。重定向是客户端行为。代码形式是resp.sendRedirect(/contextPath/otherServlet);重定向发生时服务器会返回一个302状态码并在Location响应头里写上目标地址。浏览器收到302后会重新发起一次GET请求到新地址。所以地址栏会变最多会出现两次网络请求而且request对象会丢失——注意如果你用sendRedirect跳转后还想带点参数只能手动拼在URL上或者用Session、Cookie来传递。怎么选如果是登录成功后跳转到首页推荐重定向避免刷新页面时重复提交表单。如果是查询详情页渲染数据推荐转发效率更高。如果业务上要求跳转到外部网站那就只能是重定向了。3.4 Session让HTTP记住你的用户HTTP协议是无状态的每个请求之间谁也不认识谁。Session机制就是在这个前提下发明出来让服务器记住用户的方案。HttpSession的使用方式很简单HttpSession session req.getSession(); // 获取当前会话如果没有则创建 session.setAttribute(userId, 1001); session.setAttribute(cart, cartList);之后在另一个请求里通过req.getSession()拿到同一个Session对象时就可以用getAttribute(userId)取出之前存的数据。这里有个底层的映射逻辑要说清楚。当你第一次调用getSession()时容器会创建一个HttpSession对象并且生成一个唯一的JSESSIONID字符串通过Set-Cookie: JSESSIONIDxxxxx写到浏览器里。浏览器后续请求会自动带上这个Cookie服务器就能根据JSESSIONID找到对应的Session对象。Session最头疼的问题是过期时间和分布式环境下的会话共享。默认情况下Tomcat的Session默认超时时间是30分钟你可以在web.xml里配置session-config下的session-timeout来修改单位是分钟。分布式部署时如果负载均衡轮询到不同节点Session会丢失常见的解决方案有Session黏滞、Session复制、Redis集中存储等。此外在前后端分离的项目里如果前端没有启用Cookie还可以用URL重写encodeURL的方式传递Session ID但这种方式已经不推荐了安全性也差。4. 踩坑经验那些年我们一起调试过的Servlet问题4.1 中文乱码一套组合拳解决中文乱码是Servlet开发里出现频率最高的坑没有之一。乱码问题的本质是编码和解码不一致。浏览器按A编码发出服务器按B解码结果就乱套了。分两个方向排查。请求方面POST请求的中文参数需要在读取参数之前就设置编码代码是req.setCharacterEncoding(UTF-8)。但这只对POST表单的body有效对URL query参数无效。URL里的中文参数由Tomcat的URIEncoding属性控制需要在Tomcat的server.xml里配置URIEncodingUTF-8。在我接触过的Tomcat 8.5以上版本URIEncoding默认已经是UTF-8了但老项目从旧版本迁移上来时还是要确认。响应方面resp.setContentType(text/html;charsetUTF-8)这行代码必须在getWriter()之前调用否则可能不生效。还有一种更稳妥的姿态是结合resp.setCharacterEncoding(UTF-8)一起设置。实际开发中现在的通用做法是在最前面加一个字符编码过滤器统一设置请求和响应的编码。Spring Boot里也有CharacterEncodingFilter默认就是UTF-8。但如果你在原生Servlet项目里没加这个过滤器每个Servlet里都手动设置一遍就会很麻烦——说了多少次了这种重复代码应该尽早抽成一个Filter。4.2 404和500到底怎么排查遇到404时先别急着怀疑代码写错了分三步排查。第一步看请求的URL路径对不对第二步看web.xml里的url-pattern是否匹配第三步看项目有没有编译成功尤其是新增了类、修改了包名之后忘了重新部署会导致新旧class文件混在一起。遇到500时重点看Tomcat本地日志。默认部署方式下日志在logs目录下catalina.out和localhost.yyyy-MM-dd.log是最常看的两个文件。500的原因太多了最常见的是NPE和类型转换异常。前者是因为参数为null、Session里取不到对象、或者getAttribute返回null后直接调用方法。后者是前端传来字符串但你强行用Integer.parseInt转int时没有做容错。还有一个比较容易迷惑的是ClassNotFoundException和NoClassDefFoundError。前者是类路径上根本没有这个类通常是缺少依赖JAR后者是类路径上有这个类但初始化失败——比如静态变量初始化抛异常了。你在IDE里能运行部署到Tomcat就报NoClassDefFoundError先检查JAR包是否真的被打进了WEB-INF/lib。4.3 线程安全问题共享变量的坑HttpServlet默认是单例多线程的。什么意思就是容器只创建一个Servlet实例所有请求都在这个实例上调用service()。如果这个实例里定义了可变的成员变量多个线程同时读写就可能出现数据混乱。典型错误示范public class LoginServlet extends HttpServlet { private int count 0; Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) { count; // 非线程安全 } }两个请求同时进来count的加减可能互相覆盖。正确做法是尽量使用局部变量请求相关数据都从req里取如果必须共享状态用AtomicInteger、ConcurrentHashMap这类线程安全的数据结构或者加volatile配合锁如果只是每个用户自己的字段用Session或ThreadLocal保存不要放成员变量。这个问题的隐蔽性在于开发环境并发量低测不出来上线后流量一大问题就出现了。而且复现非常困难因为它是偶发性的。所以从源头守规矩写Servlet时先问自己一句这个成员变量要不要被所有线程共享5. 走向生产环境Servlet性能优化与架构演进5.1 异步Servlet与性能提升传统Servlet模型是一个请求占用一个线程。在Tomcat 9版本里如果不配置线程池默认最多200个线程。如果某个请求处理的业务比较慢——比如调用外部API耗时3秒——那么这个线程在这3秒内就被占用了其他请求只能排队等待。为了解决这类问题Servlet 3.0引入了异步处理能力。核心思路是先用req.startAsync()把当前请求标记为异步模式把耗时任务丢给另一个工作线程执行这时候Tomcat的调度线程可以马上去接下一个请求。等任务完成后通过AsyncContext.complete()或dispatch()把结果返回给客户端。我用一个实际场景来说明。假设一个报表导出功能要查数据库还要组装Excel平均耗时8秒。传统模式下Tomcat的200个线程很快就会被这类请求占满造成页面卡死用异步Servlet之后Tomcat的200个线程只负责接收请求和返回响应真正的耗时计算全部交给独立的线程池Tomcat线程可以快速释放出去服务更多请求。异步Servlet的使用要注意两点一是异步任务的线程池要单独管理不然还是堵二是异步请求必须在规定时间内完成否则容器会报AsyncTimeoutException。生产上我建议只在确实需要长时间处理的请求里使用异步不是所有接口都要异步化——异步化也会增加代码的复杂度和出错的概率。5.2 从HttpServlet到Spring MVC框架究竟封装了什么既然现在项目大多是Spring Boot那搞清楚Spring MVC和HttpServlet的关系就特别重要。说白了Spring MVC的核心入口DispatcherServlet本身就是一个HttpServlet的子类。它在doDispatch()方法里帮我们完成了请求到Controller方法的映射、参数绑定、JSON序列化、异常处理这一整套流水线。框架封装了什么封装了重复。一个HTTP请求进来在原生Servlet里你要手动做这些事情从request里取值、做校验、转换类型、绑定到对象、调用业务、手动写成JSON响应。在Spring MVC里你只需要写一个Controller方法参数自动绑定返回值自动序列化。但框架再好底层出问题的时候还是要回到Servlet层面去查。比如你发现Spring Boot项目里getParameter()遇到了乱码其实追根溯源还是Tomcat的编码配置问题。比如你发现某个请求的Header被吞了还是得回到Servlet API里看它的getHeaderNames()的语义。我个人的建议是不要因为用了高级框架就跳过Servlet这一层的基础学习。你会写GetMapping不稀奇但如果你能解释清楚DispatcherServlet为什么是一个HttpServlet、请求参数从HTTP报文到你方法参数之间经过了哪些环节那你对这个系统的掌控力是完全不一样的。5.3 给初学者的实战建议如果你想真正练熟HttpServlet我建议你做一个手动挡的小项目不用Spring、不用MyBatis只用原生Servlet JSP JDBC写一个简单的用户管理系统。功能就做登录、注册、列表展示、删除用户这几个基本操作。这个练习的价值在于你会在过程中被迫处理Session存用户信息、GET/POST的写法差异、参数校验、请求转发和重定向的跳转、中文乱码、数据库连接关闭等问题。这些问题每一个都非常底层每一个都是面试常问更重要的是——它们都是真实项目里一定会遇到的问题。当你把这些问题都亲手扛过去一遍再回头看Spring MVC你会自然而然地明白框架帮你做了什么、你又该怎么去驾驭它。还有个小建议多读HttpServlet源码。这个类的源码写在JDK的servlet-api包里实际上并不复杂核心就是service()方法和那一组doXxx方法。读懂一个类胜过你背十篇网上的总结。最后再分享一个我在实际项目中反复用到的技巧排查Servlet相关问题时不要只盯着自己写的类先把请求链路从头到尾捋一遍。浏览器发出请求到Tomcat接收到Filter到Servlet到Service层到DAO层每一层都可能出问题。带着这个链路图去定位远比盲目打日志快得多。