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;charsetUTF-8); resp.getWriter().write(h1Hello Servlet/h1); } }然后在web.xml里注册?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-namehello/servlet-name servlet-classcom.demo.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-namehello/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 62009年发布开始支持注解配置这一改动让开发体验提升了一大截。用注解的话上面的代码可以精简成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;charsetUTF-8); resp.getWriter().write(h1Hello 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请求走doGetPOST请求走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 三个核心APIrequest、response、ServletContextServlet能存东西、能取东西、能跳转靠的是这三个对象。我分别说说它们的常用方法和使用场景。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 contentTypetext/html;charsetUTF-8 languagejava % html head title用户注册/title /head body h2用户注册/h2 form actionregister methodpost 用户名input typetext nameusername /br/ 密码input typepassword namepassword /br/ input typesubmit value注册 / /form /body /html第二步写RegisterServletpackage 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;charsetUTF-8); resp.getWriter().write(h2用户名和密码不能为空/h2); return; } req.setAttribute(username, username); req.getRequestDispatcher(registerSuccess.jsp).forward(req, resp); } }这里有个处理细节你要记住读取POST请求参数之前必须先调用req.setCharacterEncoding(UTF-8)否则中文参数会乱码。这个设置必须在读取任何参数之前生效。第三步写成功页面% page contentTypetext/html;charsetUTF-8 languagejava % 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状态码500null与类转换异常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;charsetUTF-8)这个语句同时设置编码和Content-Type。JSP页面在文件开头写% page contentTypetext/html;charsetUTF-8 languagejava %。三个都做了基本不会乱码。如果还是乱码检查服务器配置文件里URIEncoding是否配置了UTF-8Tomcat的server.xml里Connector节点加URIEncodingUTF-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和ListenerServlet生态的重要拼图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的基础过一遍这时间花得非常值。