直接实现Servlet接口:从生命周期到请求分发全解析

发布时间:2026/10/24 0:41:48
直接实现Servlet接口:从生命周期到请求分发全解析 Servlet是Java Web开发绕不开的第一道门槛。哪怕现在Spring MVC、Spring Boot满天飞底层请求处理的核心机制依然是Servlet规范。这次要讲的就是最原始的Servlet使用方式直接实现Servlet接口手动在service方法里判断请求类型做分发再用注解完成映射配置。这套路子虽然在实际项目中不会直接这么用但把请求从浏览器到服务器再到业务代码的完整链路看得清清楚楚理解之后再看框架源码会轻松得多。适合刚接触Java Web的初学者也适合想补基础的老手。1. 为什么直接实现Servlet接口——设计与思路拆解1.1 先搞清楚Servlet到底是个什么东西Servlet本质上是一个运行在服务器端的Java类它遵守一套约定好的规范也就是Servlet规范能够接收浏览器的HTTP请求处理之后把结果通过HTTP响应返回给客户端。用生活化的类比来说Servlet就像餐厅的服务员客人浏览器下单发请求服务员Servlet记录需求、转给后厨业务逻辑再把做好的菜响应结果端上桌。这套机制从Java Web诞生之初就存在至今没有变过。Servlet规范定义了一套标准接口只要实现了这套接口你的类就有资格被Servlet容器比如Tomcat识别、加载、调用。容器负责管理Servlet的创建、初始化、请求调用、销毁整个生命周期开发者只需要关心业务逻辑怎么处理请求。1.2 三条实现路径的对比实现Servlet有三种常见方式各有各的使用场景实现方式代码复杂度请求方法处理使用场景直接实现Servlet接口最高需自己在service中判断教学演示、理解底层机制继承GenericServlet中等仍需自己在service中判断协议无关的服务端组件继承HttpServlet最低自动分发到doGet/doPost日常项目开发为什么这次选最原始的第一种方式因为它的过程最透明。继承HttpServlet的时候容器帮你把很多细节隐藏了——比如请求方法怎么判断的、doGet和doPost是怎么被调用的、service方法为什么能自动识别GET和POST。而直接实现Servlet接口所有东西都要自己写反而能把原理看得清清楚楚。等你在service方法里亲手写上if判断、自己手动调出doGet和doPost逻辑的时候HttpServlet那层封装的面纱就自然揭开了。1.3 这种方式解决了什么问题直接实现Servlet接口至少带来三个层面的收获。首先是理解生命周期接口里定义了init、service、destroy等方法这些方法什么时候被调用、调用顺序是什么不亲手实现一遍很难真正内化。其次是理解请求分发HTTP协议里GET、POST、PUT、DELETE等方法在Java代码中是怎样的存在为什么框架能自动分发核心逻辑其实就是一个简单的字符串判断。最后是为框架学习打基础Spring MVC的DispatcherServlet本身就是个Servlet只是它把这个接口玩到了极致。2. Servlet生命周期与核心方法详解2.1 生命周期全流程从启动到销毁Servlet的生命周期由容器管理标准流程是加载→实例化→初始化→处理请求→销毁五步。容器启动或者第一次收到该Servlet的请求时会创建Servlet实例并调用init方法完成初始化。之后每一次请求到达容器都会创建一个新线程调用service方法。当应用停止或容器关闭时调用destroy方法做清理工作。这个生命周期有几个细节值得注意。init方法只会执行一次Servlet实例在整个生命周期里只有一个单例模式多个请求并发访问的是同一个实例。所以你在Servlet里写成员变量的时候要格外小心并发问题。destroy方法也不是一定能执行到比如容器被强制kill掉就来不及调了。日常开发中很少会用到destroy做事情但了解这个流程对理解容器工作机制很重要。2.2 接口的五个核心方法逐一说透Servlet接口一共定义了五个方法每个方法都有明确的职责分工init(ServletConfig config)初始化方法容器在创建实例后调用用于加载配置参数、建立资源连接。注意它接收一个ServletConfig对象这个对象里封装了Servlet的初始化参数需要保存起来供后续使用。getServletConfig()返回容器传给init方法的ServletConfig对象。开发工具链比如IDE和容器需要通过这个方法获取Servlet的配置信息。service(ServletRequest req, ServletResponse res)核心处理逻辑所在。接收请求、处理业务、返回响应的所有操作都在这里完成。容器每收到一个请求就调一次这个方法。getServletInfo()返回Servlet的基本信息字符串比如作者、版本等仅用于展示不影响逻辑。destroy()销毁方法容器在移除Servlet之前调用Release占用的资源。这五个方法里真正写业务逻辑的只有service。但其他四个也必须实现因为这是接口的约束。假如不实现编译器就会报错。这就引出一个实际问题每次写Servlet都要把这五个方法全部写一遍太啰嗦了。2.3 请求分发核心service方法是怎么工作的当浏览器发送一个HTTP请求到服务器容器解析请求后会根据请求的URL找到匹配的Servlet创建一个ServletRequest对象封装请求数据参数、头信息、方法类型等创建一个ServletResponse对象用于写入响应内容然后调用service方法并传入这两个对象。service方法内部的第一步就是判断请求类型。ServletRequest接口里有一个getMethod方法返回的是请求的HTTP方法名称字符串比如GET、POST、PUT、DELETE。分发逻辑本质上就是对这个字符串做判断String method req.getMethod(); if (GET.equals(method)) { // 处理GET请求 } else if (POST.equals(method)) { // 处理POST请求 } else { // 其他请求方法 }这就是HttpServlet类做的事情。HttpServlet重写了service方法在内部先判断是HTTP请求还是普通请求然后根据getMethod的返回值分别调用doGet、doPost、doPut等方法。所以理解了这个简单的字符串判断你就理解了HttpServlet分发机制的全部本质。3. 手把手实操从零搭建一个注解配置的Servlet3.1 环境准备版本选型和项目结构实战之前先把环境准备好。我用的是最经典的组合JDK 8 Tomcat 9 Maven用IDEA开发。这里有个非常重要的版本兼容问题要提前说清楚Tomcat 9及之前的版本用的是javax.servlet包Tomcat 10开始迁移到了jakarta.servlet包。如果选错了依赖代码会直接编译报错。建议初学者用Tomcat 9网上资料多、踩坑少。项目结构用Maven标准布局servlet-demo/ ├── pom.xml └── src/main/java/com/example/servlet/ └── HelloServlet.javaMaven的pom.xml里加上Servlet API依赖注意scope设为provided因为Tomcat本身自带Servlet API打包的时候不需要打进去避免冲突dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency3.2 编写Servlet类实现接口的完整代码接下来是核心代码。直接实现Servlet接口把五个方法全部写上在service方法里做请求分发package com.example.servlet; import javax.servlet.*; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; WebServlet(/hello) public class HelloServlet implements Servlet { private ServletConfig servletConfig; Override public void init(ServletConfig config) throws ServletException { this.servletConfig config; System.out.println( init方法被调用Servlet初始化完成 ); } Override public ServletConfig getServletConfig() { return servletConfig; } Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { System.out.println( service方法被调用开始处理请求 ); res.setContentType(text/html;charsetUTF-8); PrintWriter writer res.getWriter(); String method req.getMethod(); if (GET.equals(method)) { writer.write(h1接收到GET请求/h1); writer.write(p请求URL req.getScheme() :// req.getServerName() : req.getServerPort() req.getRequestURI() /p); } else if (POST.equals(method)) { writer.write(h1接收到POST请求/h1); } else { writer.write(h1接收到 method 请求/h1); } } Override public String getServletInfo() { return My HelloServlet, version 1.0; } Override public void destroy() { System.out.println( destroy方法被调用Servlet销毁 ); } }这段代码有几个关键点需要说明。init方法里把ServletConfig保存到了成员变量getServletConfig才能返回它这是标准写法。service方法里先调用setContentType设置响应内容的类型和编码这一步不做的话中文会乱码。getMethod方法拿到的是大写的HTTP方法名所以判断的时候用GET、POST这样的字符串。有人可能会问直接实现Servlet接口service方法拿到的参数是ServletRequest和ServletResponse为什么不直接用HttpServletRequest因为ServletRequest是通用的不限于HTTP协议。如果直接用HttpServletRequest需要强转类型HttpServletRequest httpReq (HttpServletRequest) req; HttpServletResponse httpResp (HttpServletResponse) res;强转的安全性取决于容器传入的实际对象类型。Tomcat这个Servlet容器下传给service方法的就是HttpServletRequestWrapper的实现类所以强转安全。但直接实现Servlet接口的意义在于兼容所有协议场景所以不推荐强转。理解这一层你就明白为什么GenericServlet和HttpServlet这些抽象类逐步演化出来了。3.3 用注解配置映射一行搞定URL匹配代码写完之后需要告诉容器哪些URL路径应该由这个Servlet处理。传统方式是写web.xml配置文件servlet servlet-nameHelloServlet/servlet-name servlet-classcom.example.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-nameHelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mappingJava Web 3.0之后引入了WebServlet注解一行搞定WebServlet(/hello)也可以写完整形式WebServlet(name HelloServlet, urlPatterns {/hello, /hi})urlPatterns支持数组意味着可以配置多个路径指向同一个Servlet。注解的本质是让容器在启动时扫描classpath下的注解自动完成注册和映射省去了写XML的繁琐。对于单一Servlet的映射来说WebServlet的价值非常明显配置量从八行降到了一行。不过底层原理要清楚注解只是Web.xml的替代写法最终容器拿到的注册信息和XML方式完全一致。也就是说注解并没有改变Servlet规范本身只是换了一种更简洁的声明方式。3.4 部署运行与请求链路实测把项目部署到Tomcat启动后观察控制台输出你会看到init方法的打印信息。这一步说明Servlet实例已经被创建并完成初始化。然后浏览器访问http://localhost:8080/servlet-demo/hello页面显示接收到GET请求控制台打印service方法的信息。这里有个非常重要的时序概念第一次访问时容器才创建Servlet实例并调用init然后调用service。之后再次访问不再调用init只有service被调用。你可以刷新页面多次控制台只有一条init日志但有多条service日志。这就是生命周期单例模式的具体表现。再做一个POST请求测试。用Postman或者写个简单的HTML表单发起POST请求你会看到service方法里进入了POST分支。这一步很直观地验证了分发逻辑的正确性。要注意Maven打包之后的版本号路径问题。默认打包的WAR文件名是servlet-demo-1.0-SNAPSHOT.war访问路径要带上这个版本号。如果想简化在pom.xml里配置finalName为servlet-demo访问路径就干净了。4. 常见问题与排查实录4.1 访问出现404先查映射路径再查部署状态404是最常见的错误。原因通常有三个URL路径写错了、Servlet注解配置的路径和实际访问不匹配、项目没有正确部署到Tomcat。排查顺序先确认访问URL里的上下文路径是否正确——就是部署之后的WAR包名称再确认Servlet的urlPatterns是否和访问路径一致最后看Tomcat启动日志里有没有报错。我自己踩过的坑是上下文路径。IDEA部署的时候默认会带Artifact名称比如项目叫servlet-demo访问路径就是/servlet-demo_war_exploded/hello。如果你直接访问/hello必然404。解决方式是在IDEA的Deployment配置里把Application context改成/。4.2 500错误看控制台堆栈信息500表示服务器内部错误通常是代码执行异常。比如service方法里调用了req.getParameter()方法在非HTTP请求上使用、强转类型失败、业务逻辑抛异常等。排查方式很简单看Tomcat控制台的异常堆栈定位到出错的行号。有一个容易被忽略的坑是getWriter和getOutputStream不能同时调用。如果你想在service里既用PrintWriter又用OutputStream会抛出IllegalStateException。解决方式是一次只能选一种方式输出。4.3 中文乱码问题漫不经心就会踩的坑中文乱码分两种场景响应乱码和请求参数乱码。响应乱码是因为服务器返回的数据编码和浏览器解析的编码不一致。在service方法里加上这一行res.setContentType(text/html;charsetUTF-8);这句话让服务器以UTF-8编码输出内容同时告诉浏览器用UTF-8解码。注意setContentType要在getWriter之前调用否则写入的数据可能拿不到正确的编码设置。请求参数乱码发生在表单提交的时候。GET请求的参数在URL上POST请求的参数在请求体里。对于POST请求浏览器以页面编码提交数据服务器需要同样编码解析解决办法是获取参数之前调用req.setCharacterEncoding(UTF-8);这个方法只对POST请求体有效GET请求乱码需要在Tomcat配置里处理。4.4 包名对不上javax和jakarta的前世今生新手最容易困惑的是导入的包名。用Tomcat 9导入的是import javax.servlet.Servlet; import javax.servlet.annotation.WebServlet;如果用Tomcat 10或者Tomcat 11导入的变成import jakarta.servlet.Servlet; import jakarta.servlet.annotation.WebServlet;不只是Servlet接口所有相关类HttpServletRequest、HttpServletResponse、ServletContext等都从javax迁移到了jakarta。这背后的原因是Java EE把相关规范捐给了Eclipse基金会品牌改名导致包名变更。不确定性因素在于你用的IDE或教程可能默认Tomcat版本不同导致代码导入的包名和容器自带的包名不一致。排查方式很简单看编译报错提示的包是否存在或者看Tomcat lib目录下的servlet-api.jar里是javax还是jakarta。4.5 注解不生效忘了开启注解扫描在支持Servlet 3.0以上规范的容器里注解默认就是开启的一般不需要额外配置。但如果你用了web.xml并且web.xml里的版本声明是2.5或者更低容器会认为你不支持注解从而忽略WebServlet。解决方法是在web.xml的根节点把版本改到3.0以上web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 /web-app如果项目完全没有web.xml默认就是最新版本规范注解必然生效。5. 实操经验总结与进阶建议直接实现Servlet接口这件事看起来代码写得多、效率低但恰恰因为这个慢你才能真正理解容器和Servlet之间的信任关系。init方法为什么只执行一次service为什么每请求调用一次请求分发本质是什么这些不问清楚之后学Spring MVC的一堆抽象概念会非常吃力。进阶方向上建议按顺序做三件事。第一学完这次内容后去看GenericServlet的源码你会发现它主要是把init的参数做保存、提供了一些简便方法但service还是抽象的。第二再看HttpServlet的源码盯着service方法看它怎么调用doGet、doPost的你会发现自己写的if判断和它内部逻辑几乎一致。第三之后学Spring MVC的时候带着DispatcherServlet也是一个Servlet这个视角去看理解路径解析、HandlerMapping这类概念会轻松很多。最后分享一个排查技巧在init、service、destroy三个方法里各加一条System.out.println观察日志打印的顺序和时间点这是理解Java Web请求全链路最直观的方式。等这些基础颗粒归仓之后后续学框架、读源码都会顺畅得多。