
接触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 个类/接口就等于掌握了 Servlet2.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 请求数据的搬运工HttpServletRequestHttpServletRequest就是那个封装了浏览器请求信息的对象。所有由客户端传来的数据几乎都要从它身上取。新手最先要掌握的是怎么从请求里取值。比如前端传来一个用户名和密码代码可以这样写String username request.getParameter(username); String password request.getParameter(password);如果前端传来的是多个同名参数比如多选爱好就要用getParameterValues方法返回一个字符串数组。除了取值还有几个高频方法需要知道getMethod()获取请求方式是GET还是POSTgetRequestURI()获取请求的路径常用于判断用户到底在访问什么资源getRemoteAddr()获取客户端IP地址setAttribute和getAttribute在当前请求内部存放和取出数据。这里面的setAttribute非常关键。你在Servlet A里处理完一部分数据想交给Servlet B继续处理或者想把数据转发到某个JSP页面去渲染就可以借助它。需要区分的是getParameter拿的是浏览器提交过来的参数getAttribute拿的是你在服务器内部自己放的属性两者来源完全不同千万别混用。2.3 响应数据的出口HttpServletResponse如果说请求对象是“入口”那HttpServletResponse就是“出口”。所有你想返回给浏览器的内容要么写入它的Writer要么写入它的OutputStream。最简单的返回HTML页面写法如下response.setContentType(text/html;charsetUTF-8); response.getWriter().write(h1登录成功/h1);这里有一处很容易踩坑就是setContentType和字符编码的设置。很多人写代码时不设置编码结果浏览器显示一堆乱码原因就在于服务器输出的字节流默认编码和浏览器解析时使用的编码不一致。我的习惯是在拿到response对象之后、写任何内容之前先把这两句话写上response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8);第一句告诉服务器“你输出的字符串请用UTF-8编码”第二句告诉浏览器“我发过来的内容类型是HTML而且已经是UTF-8编码”。两句话缺一不可这是我在实际项目里吃过亏之后才深刻记住的。响应对象还有两个不太起眼但很重要的方法sendRedirect(String location)让浏览器重新发起一次请求跳到另一个地址这是“重定向”;setStatus(int code)手动设置返回的状态码比如鉴权失败时返回401资源不存在时返回404。2.4 保持会话的关键Cookie 与 HttpSessionHTTP是无状态的但业务往往需要记住用户。比如你已经登录了一半刷新一下页面服务器不能把你当成另一个人。解决办法就是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. 手把手实战写一个能跑的注册功能 Servlet3.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;charsetUTF-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; URLlogin.html); } }如果你看不懂上面的第5步我解释一下setHeader(Refresh, 2; URLlogin.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 个报错我这些年看见过太多人在同一个地方反复栽跟头下面这几个简直就是“新手套餐”遇到时不用慌照着对应思路排查即可。报错现象根本原因排查方向访问地址404URL映射写错、项目没部署成功检查注解路径和项目名是否正确看容器启动日志页面显示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是否带charsetUTF-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看起来类多、概念多其实每个类只负责一条功能线拆开之后各自都很简单。下次你对着报错无从下手的时候可以在心里按这套功能线走一遍请求到哪里了我拿到数据没有我往响应里写没写东西状态存在哪里。链路捋顺了问题通常也就现出原形了。