接触Servlet,几乎是每个Java Web后端开发绕不开的第一道门槛。我当年看到网上那些“Servlet=服务端小程序”的解释,心里也是半懂不懂,代码倒是能复制能跑,可真正遇到报错就彻底没招了。后来在做一个模拟项目X的时候,我试着把Servlet这一堆核心类按不同功能拆开来看,效果出乎意料地好——原本一团乱麻的逻辑,瞬间就变成了几个各干各活的模块。这篇内容我打算照着这个思路写,零基础的读者跟着走一遍,应该比我当年靠源码硬啃要轻松得多。
先说清楚这篇博文解决什么问题:它不是什么源码级深度解析,而是帮你把“Servlet到底是什么”“里面的类分别负责什么”“一个请求从进入服务器到返回浏览器,中间到底发生了什么”这三件事从头到尾梳理明白。适合刚学完Java基础、正准备进入Java Web阶段的朋友,也适合那些已经跑通了Servlet但在面试、写项目时总觉得哪里没吃透的同学。
1. 别急着写代码:先把 Servlet 在 Web 请求里的位置搞明白
1.1 我当初学 Servlet 时踩过的认知坑
我第一次接触Servlet时,一直在琢磨一个问题:既然我写的类继承了一个叫HttpServlet的东西,那浏览器里的表单到底是怎么跟这个Java类产生关联的?我那时候把Servlet、Tomcat、HTTP协议三个概念混为一谈,导致代码每次一跑就直接404,或者500,可悲的是连排查方向都没有。
后来才意识到,Servlet不是某个独立的软件,也不是一个网页技术,它是服务器上一个专门负责“接收请求—处理逻辑—返回响应”的Java类。而Tomcat这类Web容器,才是真正扮演“中间人”的角色:它负责监听网络端口,把浏览器发来的HTTP请求解析成Java能理解的对象,然后去找对应的Servlet,调用它的方法,最后再把返回结果整理成HTTP响应发回去。
也就是说,你的代码和浏览器之间,隔着容器这层“翻译官”。理解了这个中间层的存在,很多奇怪的配置问题才是从一开始就有法解释的。
1.2 Servlet 在 Web 请求链路中的位置
咱们把整个过程拆成一条清晰的任务链。你打开浏览器,输入地址回车,发起一个HTTP请求,这个请求会先通过网络到达服务器上运行的Web容器。容器收到请求后,会根据URL里的路径信息去一个“路由表”里查找,看有没有哪个Servlet声明了对这个路径负责,这就是我们常说的“URL映射”。
找到对应的Servlet类之后,容器会进行两个动作:如果这个Servlet第一次被访问,它会先被创建并执行一个初始化方法;接着,容器会调用Servlet里处理请求的方法,把请求数据封装成一个请求对象,再把一个用于输出的响应对象一并传进去。你的业务代码就在这个处理方法里执行,最终通过响应对象把HTML、JSON、图片或者一段文本写回去。
链路并不长,但新手总是容易忽略一个关键事实:浏览器和服务器之间通信是“无状态”的,也就是说两次请求之间彼此并不认识。为了让“登录之后能记住用户”,才引入了后面要讲的Session和Cookie。
1.3 把“服务器、容器、Servlet”这三个概念理清
很多教材里,“服务器”和“容器”经常混着说,我建议你从职责上把它们区分开。服务器是一个物理或虚拟的计算环境,负责提供Web服务能力;而Web容器是运行在服务器上的一个程序,专门管理Servlet的生命周期、处理网络通信和并发线程。真正没完没了干活的Servlet,就是你自己写的那一个个类。
我后来在带新人时常用一个比方:容器就是一个呼叫中心的接线员,Servlet是柜台里的业务员。接线员收到客户(浏览器)的电话,根据客户诉求把电话转到对应的业务员那里;业务员处理完,把结果交给接线员,再由接线员报给客户。业务员只需要关心业务本身,不用管电话线路怎么接通、电话信号怎么编码。
这个拆解对新手特别重要,因为一旦你把边界划清楚了,后面学任何Servlet核心类,都只是在回答同一个问题:在这个流程里,这个类到底属于接线员、业务员,还是客户手里的那张账单?
2. 按功能拆解核心类——学会这 6 个类/接口,就等于掌握了 Servlet
2.1 负责人:HttpServlet 类
HttpServlet是Servlet体系的“基层业务员”,我们写的每一个Servlet类几乎都要继承它。它内部定义好了处理HTTP请求的基本框架,尤其是两个核心方法:doGet和doPost。这两个方法分别对应HTTP协议里的GET和POST请求。
我要特别提醒新手一点:在没有特殊需求时,不要重写Servlet的service方法,因为HttpServlet已经在service方法里做好了请求类型的分发,它会把GET请求转发给doGet,把POST请求转发给doPost。如果直接重写service,反而容易把这种自动分流的机制破坏掉。
我自己平时写Servlet时,有个比较稳妥的习惯:如果这个Servlet只接收表单提交,我就只重写doPost;如果处理的是URL上的查询参数或者页面跳转,就只重写doGet。同时我会避免在doGet里写修改数据的逻辑,因为GET请求可能被浏览器预加载、被搜索引擎爬虫访问,背后潜藏着重复提交之类的风险。
2.2 请求数据的搬运工:HttpServletRequest
HttpServletRequest就是那个封装了浏览器请求信息的对象。所有由客户端传来的数据,几乎都要从它身上取。
新手最先要掌握的,是怎么从请求里取值。比如前端传来一个用户名和密码,代码可以这样写:
String username = request.getParameter("username"); String password = request.getParameter("password");如果前端传来的是多个同名参数,比如多选爱好,就要用getParameterValues方法,返回一个字符串数组。
除了取值,还有几个高频方法需要知道:
- getMethod():获取请求方式,是GET还是POST;
- getRequestURI():获取请求的路径,常用于判断用户到底在访问什么资源;
- getRemoteAddr():获取客户端IP地址;
- setAttribute和getAttribute:在当前请求内部存放和取出数据。
这里面的setAttribute非常关键。你在Servlet A里处理完一部分数据,想交给Servlet B继续处理,或者想把数据转发到某个JSP页面去渲染,就可以借助它。需要区分的是,getParameter拿的是浏览器提交过来的参数,getAttribute拿的是你在服务器内部自己放的属性,两者来源完全不同,千万别混用。
2.3 响应数据的出口:HttpServletResponse
如果说请求对象是“入口”,那HttpServletResponse就是“出口”。所有你想返回给浏览器的内容,要么写入它的Writer,要么写入它的OutputStream。
最简单的返回HTML页面写法如下:
response.setContentType("text/html;charset=UTF-8"); response.getWriter().write("<h1>登录成功</h1>");这里有一处很容易踩坑,就是setContentType和字符编码的设置。很多人写代码时不设置编码,结果浏览器显示一堆乱码,原因就在于服务器输出的字节流默认编码和浏览器解析时使用的编码不一致。我的习惯是,在拿到response对象之后、写任何内容之前,先把这两句话写上:
response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8");第一句告诉服务器“你输出的字符串请用UTF-8编码”,第二句告诉浏览器“我发过来的内容类型是HTML,而且已经是UTF-8编码”。两句话缺一不可,这是我在实际项目里吃过亏之后才深刻记住的。
响应对象还有两个不太起眼但很重要的方法:
- sendRedirect(String location):让浏览器重新发起一次请求,跳到另一个地址,这是“重定向”;
- setStatus(int code):手动设置返回的状态码,比如鉴权失败时返回401,资源不存在时返回404。
2.4 保持会话的关键:Cookie 与 HttpSession
HTTP是无状态的,但业务往往需要记住用户。比如你已经登录了一半,刷新一下页面,服务器不能把你当成另一个人。解决办法就是Session和Cookie这套组合拳。
Cookie是存在浏览器端的少量数据,每次请求时浏览器会自动带上对应站点的Cookie。服务端可以创建一个Cookie,设置它的值和存活时间,然后通过response.addCookie接口把它送回浏览器。常见用法是记录用户的登录状态标识。
HttpSession则是存在服务器端的对象,用来保存当前用户的私有数据。要拿到它非常方便:
HttpSession session = request.getSession(); session.setAttribute("currentUser", username);这个对象底层是靠一个特殊的Cookie(默认叫JSESSIONID)来识别用户的。浏览器第一访问时服务器创建Session并生成一个ID,通过Cookie交给浏览器;浏览器第二次请求时带着这个ID过来,服务器就能凭ID找回之前存的那个Session对象。
但是有一点值得注意:Session默认的存活时间是有限制的,在Tomcat里默认是30分钟。如果你做登录功能,发现用户过一会儿就掉线,很可能就是Session超时了,而不是代码逻辑写错。
2.5 全局共享的公共区域:ServletContext
上面几个对象基本是围绕单次请求或单个用户,而ServletContext是围绕整个Web应用存在的。只要Tomcat还在运行,这个对象就一直存在于内存里,所有Servlet都可以访问它。
举个例子,你想在应用启动时加载一份公共配置,或者统计整个网站的访问人数,就可以把数据放到ServletContext里,所有Servlet都能取到。获取方式也非常简单:
ServletContext context = getServletContext(); context.setAttribute("totalCount", 1000); Integer count = (Integer) context.getAttribute("totalCount");它还能用来获取web应用的真实部署路径,比如你要读取项目里某个文件时,getRealPath方法很有用,不过现在更多项目会采用别的方式来管理资源文件。
我要提醒一下:ServletContext虽然是全局共享的好地方,但不建议往里面塞太大的对象或频繁写入数据,因为它是多线程共享的,并发多的时候容易出现并发安全问题,也可能撑爆内存。
2.6 请求的调度员:RequestDispatcher
有时候你不需要通过Servlet返回完整页面,而是希望把请求转交给另一个组件继续处理。RequestDispatcher就是干这件事的。它有两种核心用法:转发和包含。
RequestDispatcher dispatcher = request.getRequestDispatcher("/login.jsp"); dispatcher.forward(request, response);这一段代码的效果是:当前Servlet处理完后,把请求和响应两个对象整个交给login.jsp去继续渲染。地址栏的URL不会变,而且原来是POST请求的话,到了JSP里依然还是POST,整个过程中请求对象里setAttribute的数据都还能拿到。
很多新手分不清转发和重定向的区别,我列一个对比表,后面写代码时直接对照就能少走弯路。
| 对比维度 | 转发 forward | 重定向 sendRedirect |
|---|---|---|
| 发生位置 | 服务器内部完成 | 服务器告诉浏览器重新请求 |
| 请求次数 | 只有1次请求 | 至少2次请求 |
| 地址栏 | 不变 | 会变成新地址 |
| 是否能携带Request数据 | 可以共享 | 无法直接共享 |
| 典型场景 | 页面跳转、配合JSP渲染 | 登录成功后跳转到首页、表单提交后的防重复提交 |
3. 手把手实战:写一个能跑的注册功能 Servlet
3.1 开发环境准备清单
工欲善其事,必先利其器。在动手写代码之前,你至少要把下面这三样准备好:
- JDK,推荐8以上版本;
- 一个支持Web开发的IDE,比如IntelliJ IDEA;
- 一个Servlet容器,常见的有Tomcat,推荐下载支持你JDK版本的稳定版本。
这里提醒一下,Tomcat版本和Servlet规范的版本是有对应关系的。比如Tomcat 9对应Servlet 4.0,包名还是javax.servlet;而Tomcat 10对应Servlet 5.0,包名改成了jakarta.servlet。如果你下载了最新版的Tomcat 10,却按老教程导入javax.servlet的包,代码会大面积报错,这也是新手经常被劝退的重灾区。
3.2 从零创建一个简单的注册功能
我以“用户注册”这个经典场景为例,带你走一遍完整的Servlet开发流程。假设页面已经有一个表单,用户通过POST提交了username和password这两个字段。
先写一个Servlet类,类的结构大致如下:
@WebServlet("/register") public class RegisterServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 解决请求参数和响应输出的编码问题 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); // 2. 接收请求参数 String username = request.getParameter("username"); String password = request.getParameter("password"); // 3. 简单校验参数非空 if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { response.getWriter().write("<h3>用户名和密码不能为空</h3>"); return; } // 4. 模拟保存用户到数据库,真实项目中这里会调用Service层 System.out.println("成功注册用户:" + username); // 5. 跳转到登录页或者给个成功提示 response.getWriter().write("<h3>注册成功,正在跳转登录页...</h3>"); response.setHeader("Refresh", "2; URL=login.html"); } }如果你看不懂上面的第5步,我解释一下:setHeader("Refresh", "2; URL=login.html")是一种轻量级的页面跳转方式,服务器告诉浏览器“2秒后自动跳转到login.html”。这种方式适合做临时提示,正式项目里更常用重定向。
3.3 两种配置方式:@WebServlet 注解与 web.xml
上面的代码中用到了@WebServlet("/register"),这是Servlet 3.0以后推荐的注解式配置,不需要额外写配置文件,注解中的字符串就是当前Servlet对外暴露的访问路径。你在浏览器里输入的地址,比如http://localhost:8080/你的项目名/register,就会命中这个类。
除了注解,另一种老牌配置方式是web.xml。以前的项目都要在web.xml里分别声明Servlet类和URL映射,操作比较繁琐,代码量和出错概率都更高。下面是一个简化的对比:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 注解@WebServlet | 配置和代码在一起,直观便于维护 | 需要Servlet 3.0及以上环境 |
| web.xml | 老项目通用,适合Servlet路径需要外部调整的场景 | 配置分散、代码量多、容易配错 |
个人建议,新项目优先用注解,理解起来也符合“按功能拆解”的思路——访问路径跟着Servlet走,一眼就能看出来。
3.4 生命周期与线程安全:Servlet 为什么“默认不安全”
每个Servlet在被容器加载之后,不是来一次请求就创建一次,而是通常只有一个实例,所有请求共享。这就带来一个非常关键的安全问题:如果我在Servlet里定义了一个成员变量,比如一个计数器,多个请求同时执行时同时读写这个变量,就容易出现并发错乱。
还是拿前面的呼叫中心比喻,业务员只有一个人,所有客户电话都转给他,他在打电话同时当然没法又接待新电话,但在Servlet里,容器会为每个请求开一个线程,让同一个Servlet实例的同一个方法被多个线程同时执行。如果你在方法里用了局部变量,每个线程都有各自的副本,问题不大;可如果用了成员变量,就会互相干扰。
所以实战里的经验是:不要在Servlet里随意定义可变成员变量来存业务数据。确实需要共享状态时,再考虑ServletContext或者数据库。这不算一个多难的问题,但如果没意识到,排查起来会让人相当头疼。
4. 常见问题排查与避坑指南
4.1 新手最容易踩的 5 个报错
我这些年看见过太多人在同一个地方反复栽跟头,下面这几个简直就是“新手套餐”,遇到时不用慌,照着对应思路排查即可。
| 报错现象 | 根本原因 | 排查方向 |
|---|---|---|
| 访问地址404 | URL映射写错、项目没部署成功 | 检查注解路径和项目名是否正确,看容器启动日志 |
| 页面显示500 | 代码运行期异常 | 查看控制台完整异常栈,重点看空指针和转换异常 |
| 中文乱码 | 编码未正确设置 | 在request和response上同时设置UTF-8编码 |
| import javax.servlet报错 | Tomcat版本与包名不匹配 | 检查是Tomcat 10还是旧版本,切换jakarta或javax |
| 改了代码没生效 | 容器没有重新加载 | 重新构建并重启容器,清理浏览器缓存 |
500错误对新手打击最大,但我想说它其实是最友好的错误,因为Tomcat会在后台打出一整段异常日志。只要你养成“看控制台第一行异常的Caused by部分”的习惯,八成问题能自己解决。
4.2 中文乱码问题排查思路
中文乱码是Servlet阶段特别高频的问题,我单独拿出来说一下。乱码的本质就是编码和解码不一致,但背后牵扯到三个环节:请求参数解析、服务器输出编码、浏览器渲染编码。
对于POST请求,在获取任何参数之前调用request.setCharacterEncoding("UTF-8")基本能搞定;对于GET请求,参数通常拼在URL上,Tomcat 8以后URL默认编码就是UTF-8,但老版本还需要额外配置。响应环节则如前所述,先设置response编码再输出。
我最常推荐的排查顺序是:先看浏览器端是否用了UTF-8渲染,再看响应头里的Content-Type是否带charset=UTF-8,最后再看服务器日志里输出的字符串内容。这三个点都对了,乱码基本不会出现。
4.3 转发和重定向用错时怎么识别
新手最常见的操作是把sendRedirect和forward混着用,导致出现两类很典型的异常:一类是“该响应对象已提交,无法转发”,另一类是登录成功后地址栏还停在原来的URL。
如果你是先写了response.getWriter().write再执行forward,就会触发第一类问题,因为响应已经部分提交给浏览器,服务器内部就不能再把请求转发给其他组件了。解决方法是,确认要转发就不要提前写输出内容。
如果你发现登录成功后地址栏没变,而且一刷新页面就重复提交表单,很可能是把转发当成了重定向来用。重定向虽然会多一次请求,但能够避免表单重复提交,也是登录跳转的最佳实践。
4.4 一些容易忽略的实战小习惯
最后分享几个我写Servlet时一直保留的小习惯,算是用教训换来的教训。
- 不要把业务逻辑全写在Servlet里,Servlet只负责接收参数、调用服务、返回结果,否则类会越来越臃肿,后面连维护的欲望都没有。
- 不要直接用字符串拼接拼出大段HTML,宁可转发给JSP去渲染,代码可读性和维护成本完全是两个量级。
- 每次写完Servlet,先想清楚请求是GET还是POST,浏览器地址栏直接访问默认是GET,可不能用doPost逻辑去等一个永远不会来的POST请求。
- request.getParameter永远可能返回null,不要嫌麻烦,关键参数务必做空判断。
我在带新人时发现一个规律:凡是能把Servlet按“请求怎么来、数据怎么拿、结果怎么回、状态怎么存”拆开理解的人,后面学Spring MVC、过滤器、拦截器这些内容都会顺利很多。Servlet看起来类多、概念多,其实每个类只负责一条功能线,拆开之后各自都很简单。下次你对着报错无从下手的时候,可以在心里按这套功能线走一遍:请求到哪里了,我拿到数据没有,我往响应里写没写东西,状态存在哪里。链路捋顺了,问题通常也就现出原形了。