☰
Servlet从入门到实战:原理、配置、生命周期与常见问题排查
2026/10/7 4:46:16 网站建设 项目流程

1. Servlet到底是什么:先建立整体认知

1.1 从一次浏览器请求说起

先别急着看代码,我们用人话捋一遍Servlet在Web开发里的位置。你每天打开浏览器访问网页,输入网址、敲回车,这一瞬间发生了什么?浏览器把请求发到服务器,服务器根据你访问的地址找到对应的处理逻辑,生成HTML响应回给浏览器。这个“处理逻辑”在Java Web世界里,绝大多数情况下就是一个Servlet。

如果接触过Java后端,你一定听过“Servlet是Java Web的基石”这种说法。这话不夸张。现在大家用的Spring MVC、Struts2,底层核心依然是Servlet,框架只不过在Servlet之上做了一层封装和增强。也就是说,你把Servlet学透了,再看框架源码会轻松很多,因为框架帮你干的活本质上就是“把请求路由到某个方法、解析参数、返回结果”这一套。

那Servlet本身糊不复杂?其实就三个核心点:继承HttpServlet、重写doGet/doPost、配置访问路径。把这三点搞明白,你就已经跨进了Java Web开发的门槛。

1.2 为什么现在还要学Servlet

有个现实问题值得说清楚:很多初学者一上来就学Spring Boot,觉得Servlet是老古董、过时了。这种想法我年轻时也有过,后来踩了坑才明白,跳过Servlet直接学框架,就像没学过加减乘除直接学微积分,能照猫画虎写几个接口,但出了问题就抓瞎。

举个例子,你在Spring Boot里写一个Controller,配一个@RequestMapping("/user"),浏览器访问这个地址,请求就到了对应方法。这个映射、分发、调用过程,底层全是Servlet规范在支撑。DispatcherServlet本身就是一个Servlet,只不过它被设计成一个通用的分发器,把请求转给各个Controller方法。

另外,Servlet规范里的Filter(过滤器)、Listener(监听器)这两个概念,在Spring Boot里变成了拦截器、事件监听机制,但核心思想完全一样。你把这个思想搞懂了,学什么都快。

1.3 Servlet的三种角色定位

为了让思路更清晰,我把Servlet在Web应用中的角色归类成三块:

一是请求接收器。浏览器发来的HTTP请求,由Servlet容器(比如Tomcat)接收,然后按照URL映射找到对应的Servlet,把请求封装成HttpServletRequest对象传给Servlet。

二是业务处理器。Servlet拿到请求后,从request里取出参数,调用Service层、Dao层处理业务逻辑,这是整个Java Web后端最核心的一环。

三是响应构造器。业务处理完成后,把结果数据放到HttpServletResponse对象里,设置响应头,写出HTML或者JSON内容,再由容器把响应发回给浏览器。

这三块,其实就是Servlet编程的全部内容。后面所有代码都围绕这三件事展开。

2. 两种配置方式:web.xml与注解,到底怎么选

2.1 传统web.xml配置方式

Servlet的开发流程,早期版本必须依赖web.xml文件。这个文件放在WEB-INF目录下,是Web应用的部署描述符。它的作用就是告诉Tomcat:哪些Servlet类存在、用哪个URL去访问它。

一个典型的Servlet类加web.xml配置,长这样。先写Servlet类:

package com.demo.servlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; import java.io.IOException; public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h1>Hello Servlet</h1>"); } }

然后在web.xml里注册:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>hello</servlet-name> <servlet-class>com.demo.servlet.HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>hello</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> </web-app>

这两个配置是成对出现的。servlet标签负责声明“我有一个Servlet类,取名叫hello”,mapping标签负责规定“你访问/hello这个地址时,就调用hello这个Servlet”。

这种方式的优点很直观:所有映射关系集中在web.xml里,项目大了之后,运维或者后来接手的人打开这个文件,一眼就能看全哪些URL对应哪些处理类。而且改动映射关系不需要重新编译Java代码,改完xml重启容器就行。

缺点也明显:每写一个Servlet,都要维护两处配置,类多了之后web.xml会越来越臃肿。更麻烦的是,如果配置了servlet-class但类名写错,或者映射路径和别的Servlet冲突,启动时直接报错,排查起来比较费劲。

2.2 注解配置方式

Servlet 3.0规范(对应Java EE 6,2009年发布)开始支持注解配置,这一改动让开发体验提升了一大截。用注解的话,上面的代码可以精简成:

package com.demo.servlet; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.ServletException; 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;charset=UTF-8"); resp.getWriter().write("<h1>Hello Servlet</h1>"); } }

就这么简单。@WebServlet注解放在类上,括号里写URL映射路径,Tomcat启动时会自动扫描项目中所有带这个注解的类,完成注册。不用再碰web.xml。

@WebServlet注解里还可以配置更多参数,比如initParams(初始化参数)、loadOnStartup(启动时是否立即加载)、asyncSupported(是否支持异步),日常开发中用得最多还是urlPatterns或者value属性,两者等价,用来指定访问路径。

2.3 两种方式的对比与混用规则

我把两种方案的差异整理成一个表格,方便你对照:

对比维度web.xml配置注解配置
配置位置WEB-INF/web.xmlJava类上
开发效率较低,类与配置分离较高,一处搞定
可维护性集中管理,全局可见分散在各类中,需翻代码
热修改改映射无需重新编译改注解需重新编译
适用项目老项目、大型多模块项目中小型项目、新项目
规范版本所有版本都支持需Servlet 3.0及以上

一个常见的误解是,用了注解就不能用web.xml,或者用了web.xml就不能用注解。实际上两者完全可以共存。Servlet容器加载的时候,先扫描注解注册Servlet,再读取web.xml里的配置,两者不冲突。同名映射的情况下,web.xml的优先级更高,会覆盖注解配置。

我个人的实践建议是:中小型项目、个人学习、微服务项目,直接用注解,效率高、代码集中;大型企业级老项目,或者有多套环境、需要运维统一管理映射关系的场景,继续沿用web.xml更稳妥。另外注意一点,如果你的项目里web.xml已经存在,并且版本声明是3.0以上,那注解和xml混用完全没问题;如果版本很低(2.5及以下),注解是不生效的,需要升级声明。

3. Servlet运行机制与生命周期详解

3.1 请求的完整旅行路径

光会写类和配置还不行,你得搞清楚一个请求从浏览器发出到Servlet执行完,中间到底经历了什么。只有理解了这条链路,遇到问题时你才知道该往哪个环节排查。

一次完整的Servlet请求处理过程,一共六步:

第一步,浏览器发起HTTP请求,比如访问http://localhost:8080/app/hello。这个请求通过网络到达Tomcat监听的端口。

第二步,Tomcat接收到请求后,解析出请求行、请求头、请求体,创建HttpServletRequest和HttpServletResponse两个对象。你可以把request理解成“快递包裹”,里面装着浏览器带来的所有信息;response是“回执单”,你往里面写什么,浏览器就能看到什么。

第三步,Tomcat根据URL里的上下文路径(/app)定位到具体应用,再根据剩余路径(/hello)查找匹配的Servlet映射。这一步就是路由分发。

第四步,如果Servlet实例还没创建,容器会加载Servlet类并实例化,然后调用init方法初始化。

第五步,调用service方法。HttpServlet的service方法会依据请求的HTTP方法(GET/POST等)自动分发,GET请求走doGet,POST请求走doPost,你可以选择重写其中一个或两个都重写。

第六步,Servlet处理完业务,把结果写入response对象,方法返回。随后容器把响应内容转成字节流发回浏览器,最后销毁request和response对象。

这条链路,每次请求都会完整走一遍。理解它,你就知道为什么Servlet是单实例多线程的设计,为什么不能把用户相关状态存在实例变量里,后面会展开讲。

3.2 生命周期:init、service、destroy的时序与陷阱

Servlet的生命周期由容器管理,一共三个阶段:初始化、服务、销毁。我逐一说。

初始化对应init方法。有两种触发时机,默认是第一次收到请求时才创建实例并调用init,这叫懒加载;也可以在web.xml里配置load-on-startup(或注解里写loadOnStartup),让应用启动时就初始化。数值越小优先级越高。

init方法在整个生命周期里只调用一次。它适合做那些“只做一次”的事情,比如加载数据库连接池、读取配置文件、初始化全局数据。我把一个常见错误给你说一下:很多人把耗时操作放在init里,导致应用启动慢,其实init里只放轻量级准备工作就行,真正重的资源延迟到使用时再加载更合理。

服务阶段对应service方法。HttpServlet默认的service方法会根据请求类型分发给doGet、doPost等。这里有个很重要的设计细节:Servlet是单实例多线程的。Tomcat为每个请求分配一个工作线程,多个线程可能同时执行同一个Servlet实例的service方法。这就意味着,你在Servlet类里定义的实例变量是共享的,如果有线程安全问题,需要加锁或者尽量不定义可变的实例变量。

销毁阶段对应destroy方法。应用关闭或容器回收Servlet时调用,用来释放资源。这个方法实际开发中覆盖得不多,但如果你在init里开了数据库连接、线程池,最好在destroy里关闭,避免资源泄露。

3.3 三个核心API:request、response、ServletContext

Servlet能存东西、能取东西、能跳转,靠的是这三个对象。我分别说说它们的常用方法和使用场景。

HttpServletRequest,最重要的方法是getParameter,从请求里取参数,不管参数来自URL查询字符串还是表单体,都用它。还有getRequestDispatcher("路径").forward(req, resp),实现服务器内部转发,地址栏URL不变。setAttribute和getAttribute配合使用,用于在request范围内传递数据,转发之前把数据塞进request,转发到另一个Servlet或JSP就能取出来。

HttpServletResponse,常用的是setContentType设置响应类型,getWriter().write()输出文本内容,sendRedirect("路径")做重定向,地址栏会变化。还有setHeader设置响应头,比如控制浏览器缓存。

ServletContext,代表整个Web应用,它是全局唯一的。用getServletContext()可以拿到。常用场景:getRealPath("/")获取应用部署的物理路径、setAttribute/getAttribute做应用级数据共享、getInitParameter读取应用初始化参数。

关于forward和sendRedirect的区别,我多说两句。forward是服务器内部行为,整个过程只有一次请求,效率高,适合带数据跳转到页面;sendRedirect是服务器告诉浏览器“你去另一个地址访问”,会产生第二次请求,效率低一些,但适合跳转到另一个应用或者需要改变URL的场景。选哪个,取决于你的业务需求。

4. 入门实战:从零搭建一个Servlet项目

4.1 环境准备:JDK、Tomcat、IDE怎么配

正式写代码之前,先把环境搞定。我用的是JDK 8及以上、Tomcat 9或10、IntelliJ IDEA,但你用Eclipse或者别的IDE也没问题,思路一样。

第一步,安装JDK。配置JAVA_HOME环境变量,把%JAVA_HOME%\bin加到Path里。命令行敲java -version,能输出版本号就说明成功了。

第二步,下载Tomcat。注意版本差异:Tomcat 9及以下对应javax.servlet包名,Tomcat 10及以上对应jakarta.servlet包名。我们这次示例用Tomcat 9的javax命名空间,这是老项目最广泛的写法。如果你的教程里看到import javax.servlet.http.HttpServlet,就用Tomcat 9;看到import jakarta.servlet.http.HttpServlet,就用Tomcat 10或11。这个坑每年都有新人踩,代码明明对着教程写的,却编译报错找不到类,八成就是Tomcat版本不匹配。

第三步,配置Tomcat。在IDE里添加Tomcat服务器,指定Tomcat安装目录,部署时选择war exploded模式。开发阶段用exploded模式更快,修改代码后不需要重新打包war。

4.2 创建动态Web项目与目录结构

在IDEA里新建项目,选Java Enterprise,勾选Web Application,这样会自动生成标准的Web项目结构。一个标准的Servlet项目目录长这样:

src/ main/ java/ -- 源码目录,放Servlet类和业务类 webapp/ -- Web根目录 WEB-INF/ -- 存放web.xml和依赖jar包 web.xml static/ -- 静态资源目录 index.jsp -- 默认首页

注意,WEB-INF是受保护的目录,浏览器无法直接访问里面的文件,但容器可以通过getRequestDispatcher转发到里面的JSP。这个机制就是用来保护内部JSP、配置文件不被外部直接访问的。

4.3 编写一个完整案例:用户注册

前面都是原理,现在写一个能跑起来的完整案例。我设计一个用户注册场景:浏览器提交用户名和密码,Servlet接收参数、做些简单校验,然后把结果显示给用户。

第一步,写注册页面register.jsp,放在webapp根目录:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>用户注册</title> </head> <body> <h2>用户注册</h2> <form action="register" method="post"> 用户名:<input type="text" name="username" /><br/> 密码:<input type="password" name="password" /><br/> <input type="submit" value="注册" /> </form> </body> </html>

第二步,写RegisterServlet:

package com.demo.servlet; 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("/register") public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) { resp.setContentType("text/html;charset=UTF-8"); resp.getWriter().write("<h2>用户名和密码不能为空</h2>"); return; } req.setAttribute("username", username); req.getRequestDispatcher("registerSuccess.jsp").forward(req, resp); } }

这里有个处理细节你要记住:读取POST请求参数之前,必须先调用req.setCharacterEncoding("UTF-8"),否则中文参数会乱码。这个设置必须在读取任何参数之前生效。

第三步,写成功页面:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>注册成功</title> </head> <body> <h2>注册成功,欢迎 ${username}!</h2> </body> </html>

启动Tomcat,浏览器访问http://localhost:8080/项目名/register.jsp,填表单提交,就会看到注册成功页面。这个案例麻雀虽小五脏俱全,覆盖了POST请求处理、参数获取、中文乱码处理、请求转发、EL表达式取值这几个核心点。

4.4 关键路径说明:URL和@WebServlet的对应关系

很多新手搞不清楚URL映射里的项目名问题。我举个例子:部署到Tomcat的应用上下文路径(也就是项目名)是servlet-demo,你在@WebServlet里写的映射路径是/register,那么完整访问地址是:

http://localhost:8080/servlet-demo/register

上下文路径可以在IDEA的部署配置里改,也可以在Tomcat的conf/server.xml里配,但一般不建议手动改server.xml,在IDE里配置Application context即可。项目名加Servlet映射路径,这个规则适用于所有Servlet访问。

5. 高频问题与排查技巧:我踩过的坑都在这

5.1 HTTP状态码404:路径问题排查

404是最常见的错误,意思是你访问的URL找不到对应资源。遇到404,按这三步排查:

第一步,确认URL对不对。http://localhost:8080/项目名/Servlet映射路径,三部分缺一不可。尤其容易漏项目名,或者大小写不对。URL区分大小写。

第二步,确认Servlet映射有没有配好。注解方式检查@WebServlet("/xxx")里的路径和浏览器访问的路径是否完全一致;xml方式检查servlet-name和servlet-mapping里的名字是否一致,url-pattern是否匹配。

第三步,确认Servlet类有没有被编译到classes目录。IDE里看target或out目录,找不到class文件就在IDE里重新Build一下项目。

5.2 HTTP状态码500:null与类转换异常

500表示服务器内部异常,看控制台堆栈日志就行。新手最常见的两个原因:

一是业务层或Servlet里直接操作没有初始化的对象,报NullPointerException。比如忘了在init方法里初始化某个Service对象,或者request.getParameter不存在的参数名返回null,直接去调用它的方法就炸了。

二是request.getAttribute取出的对象类型不匹配,强转时报ClassCastException。解决办法是getAttribute之后先instanceof判断一下再强转,更稳妥。

5.3 中文乱码:三个地方要统一

乱码问题,我单独拎出来说,因为真是重灾区。记住一个原则:客户端、服务器、页面三个环节的编码必须统一为UTF-8。

POST请求,在读取参数前执行req.setCharacterEncoding("UTF-8")。

响应输出,在写内容前执行resp.setContentType("text/html;charset=UTF-8"),这个语句同时设置编码和Content-Type。

JSP页面,在文件开头写<%@ page contentType="text/html;charset=UTF-8" language="java" %>。

三个都做了,基本不会乱码。如果还是乱码,检查服务器配置文件里URIEncoding是否配置了UTF-8,Tomcat的server.xml里Connector节点加URIEncoding="UTF-8"即可。

5.4 修改代码不生效:重启还是热部署

我开发时经常遇到“改了代码但效果没变”的情况。先说结论:修改了Java类,需要重启Tomcat或者让IDE重新编译并热部署;修改了JSP、HTML、CSS,不用重启,刷新浏览器就行。

IDEA里可以开启热部署,修改Java类后IDE会自动编译并更新到运行中的容器。但这个热部署有时不靠谱,特别是新增方法或改了静态配置时。我的经验是:小改动先试试热部署,如果生效就继续;改了web.xml或Servlet注解,建议直接重启,因为容器读取映射信息是在启动阶段完成的,热部署不一定重新加载映射。

5.5 一个容易被忽略的坑:getWriter多次调用

有些同学在doGet里多处调用resp.getWriter().write(),看起来没什么问题,实际上多次调用getWriter()可能抛出IllegalStateException,因为response获取writer之后,流的状态发生了变化,再次获取可能会报错。

规范做法是:先获取一次PrintWriter对象,保存到局部变量,后续所有输出都用同一个引用。这不算大问题,但能提前规避就规避。

6. 进阶必知:Servlet的延伸能力

6.1 一个Servlet中doGet与doPost的互相调用

实际开发中,很多Servlet处理GET和POST的逻辑是相同的,比如都是查询数据然后返回JSON。这时候不用把逻辑写两遍,你可以在doGet方法里直接调用doPost,或者反过来:

@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { this.doPost(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 统一的业务处理逻辑 }

但要注意,如果你的GET请求和POST请求语义差别很大,比如GET是查详情、POST是修改,那就不该互相调用,老老实实分开写。互相调用的前提是逻辑确实一致。

6.2 Filter和Listener:Servlet生态的重要拼图

Servlet规范不是只有Servlet,还包括Filter(过滤器)和Listener(监听器)。Filter可以在请求到达Servlet之前拦截请求,也可以在响应返回客户端之前拦截响应。典型的应用场景有:登录校验、字符编码设置、日志记录、权限控制。

Listener用来监听Web应用中的事件,常见的有ServletContextListener(应用启动和关闭时触发,常用于初始化全局资源)、HttpSessionListener(会话创建和销毁)、ServletRequestListener(请求创建和销毁)。我自己的经验是,把数据库连接池的初始化放在ServletContextListener的实现里,比放在某个Servlet的init里更规范,因为不依赖任何具体Servlet。

6.3 下一步:往Spring MVC平滑过渡

学完Servlet,你已经有能力阅读Spring MVC的源码了。你会发现DispatcherServlet的核心流程其实就是:接收请求、根据URL找到HandlerMapping、调用Controller方法、处理返回值、渲染结果。这一切的基础,都源于你对Servlet工作机制的理解。

我建议的学习路径是:先把Servlet的请求生命周期和Filter机制吃透,然后手写一个Mini版的MVC框架,把请求分发、参数绑定这些逻辑亲手实现一遍。这个过程比你直接啃Spring源码有效得多。等你写完Mini框架,再去看Spring MVC源码,很多原来看不懂的类,你会一眼认出它们的职责。

我个人一直觉得,Servlet是Java后端开发中最值得花时间打基础的组件。它不花哨,但它是你排查问题、阅读框架底层逻辑、理解Web容器原理的地基。学Spring Boot之前,花几天时间把Servlet和JSP的基础过一遍,这时间花得非常值。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询