ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Servlet核心原理与注解/web.xml配置实战指南

Servlet核心原理与注解/web.xml配置实战指南 聊到Java Web不管你现在用的是Spring Boot还是Spring MVC底层真正处理HTTP请求的核心仍然是Servlet。很多初学者觉得Servlet是“过时技术”一上来就学框架结果遇到请求分发、过滤器、拦截器、会话管理这些概念时完全摸不着头脑。这篇文章就用最直接的方式把Servlet是什么、日常怎么用、注解和web.xml两种配置方式怎么写一次讲清楚同时把我做项目过程中踩过的一些坑也一并分享出来。内容适合刚接触Java Web的同学参考也适合学完框架之后想回头补基础的人就算你已经在写Spring Boot把Servlet这层地基重新看一遍很多之前迷糊的点也会瞬间清晰。1. Servlet是什么理解Java Web的“最小可运行单元”1.1 Servlet在请求处理链中的位置Servlet本质上就是一个运行在服务器端、专门处理HTTP请求的Java类。注意“专门处理HTTP请求”这几个字它跟普通Java类最大的区别是Servlet不需要你自己写main方法去启动它由Web容器最典型的是Tomcat负责创建、调用和销毁。我先把一次完整的请求路径用文字拆开来看浏览器发起请求到Tomcat监听的端口Tomcat解析HTTP报文把它封装成HttpServletRequest和HttpServletResponse两个对象然后根据请求的URL找到匹配的Servlet调用Servlet里的逻辑处理业务代码通过response对象把内容写回最后容器组装成HTTP响应返回给浏览器。这个过程里有一步特别关键Tomcat已经把底层网络通信、协议解析、线程管理全部做完了你写的Servlet只需要关心“请求到了之后我要处理什么”。这个思维方式请牢牢记住因为后边你接触Spring MVC时请求处理的链路完全没有变只是外面的壳换了。1.2 生命周期容器帮你做的四件事Servlet的生命周期是面试高频题也是工作里排查问题的理论基础。它分四个阶段加载与实例化、init()、service()、destroy()。加载与实例化Tomcat启动时如果Servlet配置了load-on-startup且值大于等于1会在启动阶段就创建实例否则等第一次请求到达时才创建。这里有个重要前提Servlet是单例的整个生命周期里容器只会创建一个实例。init()方法实例创建完成后立刻调用且只会执行一次。一般用来读取配置参数、初始化数据库连接池、加载全局资源。service()方法每一次请求都会经过这里容器按请求的method类型把工作分发到doGet、doPost等方法。真正干活的通常是doGet/doPostservice本身默认只做分发。destroy()方法容器关闭或应用卸载时调用用来释放资源比如关闭连接池、写清理日志。我打个比方Servlet就像餐厅后厨的厨师Tomcat是老板加服务员。客人浏览器点菜发请求服务员把菜单Request对象递给厨师厨师做好菜Response对象再让服务员端回去。厨师入职时先熟悉厨房init上班期间集中精力处理每一单service离职前把围裙刀具交还destroy。提示因为是单实例多线程同一个Servlet实例会同时服务多个请求。这意味着千万别在Servlet里写共享的可变成员变量来存业务数据否则并发场景下数据会互相污染。我在刚工作时就因为图省事在成员变量里存了个计数器结果线上数值完全错乱排查了好久才意识到是线程安全问题。2. 配置方式详解注解与web.xml的选型实践2.1 注解配置一行注解替代十几行XMLServlet 3.0规范对应Tomcat 7及以后版本开始支持注解配置。你只需要在Servlet类上添加WebServlet注解容器启动时会自动扫描并注册这个Servlet不需要在web.xml里写任何映射。package com.example.servlet; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.annotation.WebInitParam; 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( name helloServlet, urlPatterns {/hello, /hello.do}, loadOnStartup 1, initParams { WebInitParam(name encoding, value UTF-8) } ) public class HelloServlet extends HttpServlet { Override public void init(ServletConfig config) throws ServletException { super.init(config); System.out.println(init method executed); String encoding config.getInitParameter(encoding); System.out.println(encoding encoding); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html;charsetUTF-8); resp.getWriter().println(h1Hello, Annotation Config/h1); } }这种写法的优势很明显类和配置放在同一处改路径、加初始化参数都不需要去翻XML文件每个Servlet自治团队协作时各自维护自己的类就行。但缺点也存在一旦项目里的Servlet数量多起来所有URL映射关系散落在各个类上你很难一眼看出整个应用的请求路径全貌。这种“集中可见性”的缺失在我维护大型老项目时体会很深。2.2 web.xml配置集中式部署描述符的经典写法web.xml是Servlet规范定义的部署描述符它的本质就是一张“类名到URL”的映射表。即使Servlet类上没有任何注解只要在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 display-nameServletDemo/display-name servlet servlet-namehelloServlet/servlet-name servlet-classcom.example.servlet.HelloServlet/servlet-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namehelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping servlet-mapping servlet-namehelloServlet/servlet-name url-pattern/hello.do/url-pattern /servlet-mapping /web-app拆解web.xml里的核心节点servlet节点声明一个Servlet实例。servlet-name是这个实例的唯一标识servlet-class对应到具体的Java类init-param用来注入初始化参数load-on-startup控制启动优先级。servlet-mapping节点把一个对外访问的URL路径绑定到前面声明的servlet-name上。注意同一个servlet-name可以声明多个servlet-mapping也就是说一个Servlet类可以暴露多个不同的URL路径。初始化参数的读取在Servlet的init方法里通过getInitParameter(encoding)拿到这是给单个Servlet用的参数如果要做全局参数得用context-param加getServletContext().getInitParameter()。web.xml的集中式管理让它很适合“一眼看到全局映射”的场景。但代价也明显每加一个Servlet都要改XML改动后必须重启容器才能生效而且多人同时改一个XML文件很容易产生合并冲突。2.3 两种方式怎么选项目形态决定答案不少初学者会纠结到底学哪种、用哪种。我的判断标准很简单新项目、小团队、追求开发效率用注解老项目、分布式统一治理、需要集中审查URL设计用web.xml。两者本就不冲突很多项目里也确实是混合存在的。对比维度注解配置web.xml配置配置位置写在Servlet类上集中在Web部署描述符中改动成本改一个类重编一次改XML需要重启容器全局可查性较弱分散在类中强一个文件全部可见适用版本Servlet 3.0所有版本通用团队协作减少文件冲突多人改同一文件容易冲突这里提一个实际踩过的坑当我维护一个有些年头的系统时发现某个Servlet既写了WebServlet注解又保留了web.xml里的servlet和servlet-mapping配置。结果该路径请求全部404Tomcat启动日志里能看到这个Servlet被初始化了两次但映射关系异常混乱。排查过程非常折磨最后去掉注解才恢复正常。所以我的建议是同一个Servlet注解和XML配置二选一千万别同时写容器不保证这种情况下的行为是可预期的。3. 入门实战从环境搭建到可运行的登录Servlet3.1 环境准备Maven项目与Servlet API依赖我用Tomcat 9 JDK 8 Maven来演示。项目结构建议直接按Maven Web项目的规范来建因为现在工作里几乎看不到手工拷贝jar包的方式了。先确认本地环境java -version mvn -version在pom.xml里引入Servlet API依赖。注意scope要写成provided因为Tomcat自己已经实现了Servlet规范的所有接口如果打成war包时还把它带上会造成类冲突和启动异常。dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies标准Web项目的目录结构是这样的src/main/java # Java源码 src/main/webapp # Web资源根目录 ├── WEB-INF │ ├── web.xml # 部署描述符 │ └── lib # 第三方jar包Maven项目通常为空或由构建工具管理 └── index.jsp # 静态资源或JSP页面这里有一个容易被忽略的安全概念WEB-INF目录下的所有文件都不能通过浏览器直接URL访问。这个设计是Servlet规范的保护边界你的配置文件、模板资源、class文件都放在里面外部请求会被容器直接拒绝所有动态访问都得通过Servlet或框架入口。这个知识点理解之后你再看框架源码里的资源保护设计会非常顺。3.2 登录校验Servlet核心代码逐行拆解下面写一个最常用的登录处理Servlet把doGet和doPost都重写了方便演示两者的区别。package com.example.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; public class LoginServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // GET请求通常负责页面跳转或查询展示 String username req.getParameter(username); resp.setContentType(text/html;charsetUTF-8); resp.getWriter().println(h1GET request, username username /h1); } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // POST请求通常负责表单提交和状态变更 req.setCharacterEncoding(UTF-8); // 这行必须在getParameter之前调用 String username req.getParameter(username); String password req.getParameter(password); resp.setContentType(text/html;charsetUTF-8); if (admin.equals(username) 123456.equals(password)) { resp.getWriter().println(h1Login Success/h1); } else { resp.getWriter().println(h1Login Failed/h1); } } }代码本身不复杂但有两个细节值得展开说。第一doGet和doPost的分工。HTTP协议里方法很多但业务系统里90%的场景只涉及GET和POST。GET适合无副作用的查询POST适合提交数据、修改状态。如果你只重写了doPost浏览器用GET访问同一个URL时容器会返回405 Method Not Allowed。很多新人第一次碰到405就是这里出了问题。第二req.getParameter()这个方法其实有限制。它只能拿到URL查询串的参数GET和表单以application/x-www-form-urlencoded格式提交的键值对POST。如果前端用fetch或axios提交JSONContent-Type是application/jsongetParameter拿到的永远是null这时候必须改用流式读取BufferedReader reader req.getReader(); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } String jsonBody sb.toString();这个坑在做前后端分离项目时特别常见单独拿出来提醒一下。3.3 参数接收、响应输出与页面跳转的实操细节实际项目里Servlet最常干的活就是三件事拿参数、出响应、做跳转。把这三个场景的操作细节过一遍。参数接收的四种途径// 1. 单个参数 String username req.getParameter(username); // 2. 同名多值参数比如复选框选中的多个爱好 String[] hobbies req.getParameterValues(hobby); if (hobbies ! null) { for (String h : hobbies) { System.out.println(hobby h); } } // 3. 一次性拿到所有参数的Map MapString, String[] paramMap req.getParameterMap(); // 4. JSON体里的参数用流式读取 BufferedReader reader req.getReader();响应输出的两种姿势文本类输出用Writer二进制输出用OutputStream// 输出普通文本或HTML resp.setContentType(text/html;charsetUTF-8); resp.getWriter().println(h1Hello/h1); // 输出JSON前后端分离项目最常用 resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\code\:0,\message\:\success\}); // 输出文件下载比如导出Excel、下载图片 resp.setContentType(application/octet-stream); resp.setHeader(Content-Disposition, attachment;filenamereport.txt); OutputStream os resp.getOutputStream(); os.write(fileBytes);页面跳转的两种方式请求转发Forward服务器内部跳转地址栏不变Request对象可以带着属性传递给目标资源适合需要携带数据到页面渲染的场景。重定向Redirect服务器返回302浏览器重新发起一次请求地址栏变化适合登录成功后跳主页、防止表单重复提交的场景。// 转发 req.setAttribute(user, userInfo); req.getRequestDispatcher(/index.jsp).forward(req, resp); // 重定向 resp.sendRedirect(/login.jsp);我的经验是能把业务数据带到下一页时优先转发如果只是“处理完了你该去另一个页面了”用重定向更干净。特别是表单提交后如果直接转发用户刷新页面会重复提交表单这种情况用重定向能避开。4. 常见问题排查与避坑实录4.1 中文乱码请求、响应、URL三处逐一排查中文乱码是Servlet入门遇到最多的Bug没有之一。我把排查思路总结成三句话请求乱码看“进出”、响应乱码看“声明”、Tomcat乱码看“URI编码”。先看POST请求乱码。解决方法是设置请求体的字符编码req.setCharacterEncoding(UTF-8);这行代码必须在第一次调用getParameter之前执行否则容器已经按默认编码解析了请求体后面再设就晚了。再看GET请求乱码。GET参数是放在URL里的Tomcat 8.5及以上版本默认对GET请求使用UTF-8编码基本不用处理。但如果你的项目部署在Tomcat 7或更早的版本上就需要修改conf/server.xml里的Connector配置Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /最后看响应乱码。响应乱码一般是因为浏览器不知道用什么编码解析内容或者Servlet写数据时用了错误的编码。必须保证以下两行在获取Writer之前执行resp.setContentType(text/html;charsetUTF-8); resp.setCharacterEncoding(UTF-8);注意setCharacterEncoding一旦在getWriter()之后调用就失效了因为Writer对象创建时已经锁定了编码。同样的道理也适用于setContentType要形成“先设置响应头再获取输出流”的习惯。4.2 404与500两类典型报错的诊断思路404表示“路径找不到”优先排查三个方向URL路径是否和servlet-mapping里的url-pattern完全一致注意大小写、是否少了斜杠或拼写错误。项目是否真正部署成功了去Tomcat启动日志里看有没有一堆Exception。访问地址是否正确标准格式是http://localhost:8080/项目名/url-pattern很多人直接访问url-pattern忘了带项目名就卡在404上。500表示“服务器内部出错”说明Servlet找到了但执行过程中抛了异常。处理思路去Tomcat的logs/catalina.out或IDE控制台看完整堆栈根据第一行异常类型定位问题。常见原因NullPointerException多半是getParameter返回值没做判空ClassNotFoundException说明Servlet类没有被编译进class目录还有jar包依赖冲突尤其是把Servlet-api打进了war包。编译版本不匹配也很常见用JDK 17编译的class放到JDK 8的Tomcat里跑会报UnsupportedClassVersionError问题不在代码而在编译JDK版本。建议项目里统一用Maven的maven-compiler-plugin指定source和target。4.3 部署阶段容易忽略的四个细节这几个坑虽然不起眼但每一个都值得单独说一遍。第一同名Servlet重复注册。注解和web.xml混用会造成映射冲突启动日志里能看到同一个Servlet初始化多次请求却莫名404。排查方法检查所有Servlet类上的注解再对照web.xml里的servlet节点。第二web.xml的schema版本写错。老项目里经常能看到老版本的web-app头如果容器版本跟它不兼容启动时会直接失败。新版Tomcat对应schema位置可以查官方文档别随手从网上复制老文件。第三IDE的target目录缓存。很多时候改完代码、点了重启Tomcat加载的还是旧的class。解决顺序是先clean再package最后重启容器。这条看起来像废话但“昨天还好好的今天一来就404”多半就是这个原因。第四端口占用。Tomcat启动报Address already in use时先用命令查端口占用netstat -ano | findstr 8080拿到PID之后在任务管理器里结束进程或者直接把Tomcat的端口改成别的。注意改Tomcat端口要改conf/server.xml里的Connector节点但前端代码里写死的接口地址也要同步改不然本地联调时找不到服务。5. 从Servlet到框架理清Java Web技术演进主线5.1 Spring MVC的本质一个豪华增强版Servlet如果你接下来要学Spring MVC或Spring Boot我提前给你吃颗定心丸Spring MVC底层就是Servlet它最核心的入口DispatcherServlet直接继承自FrameworkServlet再往上就是HttpServletBean和HttpServlet。换句话说Spring MVC只是把Servlet的service方法扩展成了复杂的请求分发引擎把URL映射从web.xml里的servlet-mapping换成了RequestMapping注解把参数绑定、数据校验、视图渲染这些事都做在了这一层里面。理解这件事以后很多“框架难题”会瞬间降级。比如为什么Spring Boot里有Filter配置因为Servlet规范里的Filter本来就和Servlet同级别框架只是在外面包了一层语法糖。再比如为什么拦截器能拿到Handler却拿不到真正的参数因为拦截器工作在DispatcherServlet分发的前后它压根不是Servlet规范里的东西。这些原理性的判断只有对Servlet有概念后才能建立起来。5.2 学完Servlet之后下一步该掌握什么我自己带新人的时候列过一张基础清单学完Servlet阶段最好能全部过一遍HttpServletRequest和HttpServletResponse的常用API包括作用域、请求头、Cookie操作。请求转发与重定向的区别以及各自适合什么场景。Filter过滤器和Listener监听器的基本用法理解过滤链的执行顺序。Session和Cookie的实现原理以及会话保持、失效时间如何控制。Servlet的线程安全问题知道哪些数据能放成员变量、哪些不能。了解JSP的page指令、内置对象知道JSP最终也会被编译成Servlet运行。这张清单不需要背但每个点最好都亲手写个小例子验证。尤其是Filter和Listener实际项目里做登录鉴权、统一日志、全局编码过滤器都离不开它们面试被问到的概率也极高。结尾一点个人实践经验我自己带人从Servlet入门到现在见过太多“直接学Spring Boot结果一脸懵”的例子。直接学框架的人遇到问题第一反应是搜“Spring Boot xxx报错怎么解决”先把Servlet过一遍的人遇到问题会先想“底层的请求到底是怎么走的”。这两种思维方式解决Bug的效率完全不在一个级别。如果你打算系统学Java Web我的建议很简单别跳过Servlet这一课哪怕只是像我上面这样亲手写一个Servlet、配一遍注解和web.xml、再故意制造几个404和乱码问题然后解决掉收获比刷十遍视频大得多。这一个下午的动手时间后面会以超出想象的方式回本。
返回列表