ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Servlet配置全解析:web.xml与@WebServlet注解的实战选择

2026/10/6 13:50:11 拓冰建站 浏览量
Servlet配置全解析:web.xml与@WebServlet注解的实战选择 当年我学 Servlet 的时候最迷惑的不是那些 Doget、Dopost 方法反而是配置这件事。明明写了一个 Java 类为什么访问不到为什么有人往 web.xml 里加几行 XML有人在类上写个 WebServlet 也能跑后来才意识到配置不是实验课上的附加题而是 Servlet 的核心机制之一。今天这篇就专门讲清楚 Servlet 的两种配置方法看完你就理解容器到底是怎么找到你写的那个类的也知道自己该选哪种方式。这篇适合刚入门 Java Web、被 web.xml 和注解两套写法搞晕的初学者也适合那些会写代码但说不清“为什么这么配”的朋友。我把整个知识点拆成五块配置的本质、web.xml 写法、注解写法、选型逻辑、以及新手容易踩的坑。尽量讲得实在你能直接照着敲。1. 先搞明白Servlet 为什么要“配置”两种方式到底在配什么1.1 容器怎么知道你的 Servlet 类存在很多人第一次写 Servlet 会用 IDE 自动生成一个类比如public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setCharacterEncoding(UTF-8); response.getWriter().write(Hello, Servlet World!); } }你在浏览器里输入http://localhost:8080/你的项目名/helloTomcat 会调用这个类的 doGet 方法。但这个过程中有一个关键问题Tomcat 怎么知道HelloServlet这个类存在怎么知道它对应的访问路径是/hello答案是不配置它就不知道。Tomcat 启动后只会扫描WEB-INF/classes目录下的编译产物和WEB-INF/lib下的依赖包但把一个 Java 类和一段 URL 匹配关系对应起来必须由开发者显式地告诉它。这个动作就是“配置 Servlet”。如果没配你把项目部署到 Tomcat 后控制台不会报错浏览器会给你一个 404页面提示找不到资源。新手在这步最容易怀疑是不是 Tomcat 装坏了其实问题往往是“类写了但容器不认识它”。1.2 web.xml 与注解集中登记和就地登记的区别我打个比方。Servlet 像公司里的员工容器像人事系统URL 规则像员工的工牌。要让员工开工干活你得在人事系统里给这个员工登记他叫什么名字servlet-name真实身份是什么servlet-class工牌上写哪个门禁编号url-pattern。那登记方式呢有两种web.xml 方式所有员工的信息统一填在一张登记表上交给 HR 统一录入。人事系统只要查表就能知道每个人的信息。注解方式员工自己把工牌挂在胸口走到哪都能亮出来。系统只要扫描人脸类上的注解就能识别出谁是谁。两种方式最终达到的效果一样让容器明白“哪个类处理哪个 URL”。但实现路径不同代码组织方式也不同。web.xml 把配置和代码分开集中管理注解把配置和代码放在一起就地声明。理解了这一点后面所有的细节都建立在它上面。提示一个 Servlet 不是配了就能访问它只是被容器“认识”了。真正响应请求还涉及生命周期但配置这一步是入口。先把入口搞对再谈后面的东西。2. 第一种配置法web.xml 里写清楚“类”与“URL”的对应关系2.1 一套能跑通的最小示例用 web.xml 配置 Servlet核心动作是“两步走”第一步写 Servlet 类第二步在 web.xml 里加两组 XML 标签。下面是一个完整的最小示例。先写 Servlet 类放在src/main/java/com/example/servlet/HelloServlet.javapackage com.example.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 request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(text/html); response.setCharacterEncoding(UTF-8); response.getWriter().write(h1Hello from web.xml Servlet/h1); } }然后编辑src/main/webapp/WEB-INF/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-nameHelloServlet/servlet-name servlet-classcom.example.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-nameHelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping /web-app部署到 Tomcat 后启动项目浏览器访问http://localhost:8080/你的项目名/hello能看到那个一级标题说明配置生效了。从这套最小示例里可以提炼出最关键的一句话servlet 标签只是“注册”这个类servlet-mapping 标签才负责把 URL 绑上去。很多新手以为 servlet-class 写对了就完事少了 mapping 那段照样 404。2.2 web.xml 的结构拆解四个元素一个都不能少web.xml 中 Servlet 相关的配置其实只有两类标签、四个重点字段。我用表格列出来照着填不容易错XML 标签位置作用初学阶段最容易犯的错servlet-name在servlet和servlet-mapping中各出现一次定义一个逻辑名字是两组标签之间的“连接暗号”两个标签里的名字没保持一致servlet-class只在servlet中填写 Servlet 类的完整限定名含包名只写了类名没写包名或路径写错url-pattern只在servlet-mapping中定义访问路径规则忘记以/开头用绝对 URLservlet与servlet-mapping平级前后关系前者完成类注册后者完成 URL 绑定只写注册忘记绑定反之亦然这里有三个细节必须敲黑板强调第一servlet-name 本身没有任何技术含义。它不是 Java 类名也不是 URL 路径它只是一段内部标识。你叫它 hello、hs、第一个Servlet 都行只要servlet里的名字和servlet-mapping里的名字严格一致。容器靠这个名字对得上号哦用户访问 /hello 时我该去找那个名字叫 HelloServlet 的配置然后实例化它下面写的那个类。第二servlet-class 一定是全限定名。你写了package com.example.servlet;这里就要写com.example.servlet.HelloServlet。少写包名容器在启动时会直接抛ClassNotFoundException项目起不来这算好的能让你立刻发现问题最怕的是类名写错但和别的类碰巧同名那种情况排查起来更头疼。第三url-pattern 必须以中文语境下说的“斜杠”开头。/hello合法hello不合法/hello/*合法hello/*不合法。这个规则比较死但记不住就会踩很基础的坑。后面第四章我会专门展开讲 url-pattern 的匹配规则这里先记住最基础的一条。2.3 进阶配置初始化参数与启动时机web.xml 的配置能力不止“类 URL”这么简单。两个非常常见的需求给 Servlet 传递参数、让 Servlet 在容器启动时就初始化。初始化参数像给员工发入职手册有些资源或参数比如数据库连接串、文件路径、版本号你想写成可配置的不硬编码在 Java 类里。可以用init-paramservlet servlet-nameConfigServlet/servlet-name servlet-classcom.example.servlet.ConfigServlet/servlet-class init-param param-nameappName/param-name param-valueMyFirstApp/param-value /init-param init-param param-namemaxUploadSize/param-name param-value10485760/param-value /init-param /servlet在 Servlet 里通过getServletConfig().getInitParameter(appName)读取public class ConfigServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String appName this.getServletConfig().getInitParameter(appName); response.setCharacterEncoding(UTF-8); response.setContentType(text/html); response.getWriter().write(AppName: appName); } }好处是改参数不用改代码重新编译改 XML 再重启容器就行。这在老式企业项目里非常常见。load-on-startup让Servlet提前“上岗”默认情况下Servlet 是在第一次被请求时才实例化的这种叫懒加载。但如果 Servlet 初始化要加载大量缓存、建立长连接、预热数据第一次访问就会特别慢用户会明显感觉到卡顿。这时可以让它在容器启动阶段就初始化servlet servlet-nameInitServlet/servlet-name servlet-classcom.example.servlet.InitServlet/servlet-class load-on-startup1/load-on-startup /servletload-on-startup的值是个整数数字越小优先级越高。这里填 1意思就是项目一启动容器就帮我创建这个 Servlet 的实例并调用 init() 方法。我在实际项目里见过一种很典型的误用把所有 Servlet 都配上load-on-startup觉得“提前加载总归没坏处”。其实没必要如果你这个 Servlet 只处理简单请求没有重型初始化逻辑懒加载完全够用还省内存。这个配置属于“用到才加”不用追求全员启动。3. 第二种配置法WebServlet 注解让配置跟着代码走3.1 Servlet 3.0 之后的新玩法web.xml 配置虽然清晰但你一定也感受到了它的繁琐每写一个 Servlet就要回 web.xml 里加五六个标签类一多web.xml 膨胀得比代码还长页面滚动都得半天。Servlet 3.0 规范发布后Java Web 开发多了一个选项注解配置。在 Servlet 类上直接写WebServlet配置信息就内嵌在代码里省去了在 web.xml 中注册和映射的过程。用注解改写刚才的 HelloServletpackage com.example.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(urlPatterns /hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(text/html); response.setCharacterEncoding(UTF-8); response.getWriter().write(h1Hello from Annotation Servlet/h1); } }部署后访问http://localhost:8080/你的项目名/hello效果和 web.xml 方式一模一样。web.xml 里什么都不用加甚至可以是一个空配置。我当时第一次用注解时心里嘀咕这也太偷懒了吧一行注解就完事后来明白了容器启动时会在指定目录下扫描类上的注解发现有WebServlet就自动完成注册和映射。本质上做的事情和 web.xml 相同只不过信息源从“外部配置文件”变成了“类本身”。这里有一个关键前提你的容器必须支持 Servlet 3.0 规范。Tomcat 7 及以上版本才支持注解扫描Tomcat 6 就只能老老实实用 web.xml。3.2 WebServlet 的各属性到底能配什么注解的好处是配置就近但坏处是很多人只会写一个urlPatterns其他属性根本不知道。我把常用的属性整理成表格属性作用举例urlPatterns映射路径可写多个WebServlet(urlPatterns {/hello, /hi})value与urlPatterns等价两者不能同时用WebServlet(/hello)nameServlet 的逻辑名称类似 web.xml 的 servlet-nameWebServlet(name HelloServlet, urlPatterns /hello)loadOnStartup容器启动时初始化和 XML 里的 load-on-startup 一致WebServlet(urlPatterns /init, loadOnStartup 1)initParams指定初始化参数是WebInitParam数组见下方综合示例asyncSupported是否支持异步处理WebServlet(urlPatterns /async, asyncSupported true)description描述信息相当于 XML 里的 descriptionWebServlet(urlPatterns /hello, description 登录接口)两个容易搞混的属性放在一起说value和urlPatterns。WebServlet(/hello)这种写法用的是valueWebServlet(urlPatterns /hello)用的是urlPatterns功能完全一样但不要同时写会直接报编译错误。官方建议多路径映射时用urlPatterns单路径怎么顺手怎么来。3.3 注解方式下的一个综合示例为了让你直观看到注解能配多少东西我写一个带初始化参数的例子package com.example.servlet; 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 javax.servlet.ServletException; import java.io.IOException; WebServlet( name UserServlet, urlPatterns {/user, /u}, loadOnStartup 0, initParams { WebInitParam(name defaultPageSize, value 20), WebInitParam(name appEnv, value dev) } ) public class UserServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pageSize this.getServletConfig().getInitParameter(defaultPageSize); String env this.getServletConfig().getInitParameter(appEnv); response.setContentType(text/html); response.setCharacterEncoding(UTF-8); response.getWriter().write(pageSize pageSize , env env); } }访问/user和/u两个路径都能命中同一个 Servlet。这是注解方式特别方便的一点一个 Servlet 挂多个路径只需要把数组写全不用像 web.xml 那样重复写多组servlet-mapping。注意注解的 initParams 和 web.xml 的 init-param 在最终效果上没有区别。但如果一个 Servlet 在两个地方都配了初始化参数以 web.xml 为准这一点在很多容器上是有约定的生产环境别依赖这种歧义统一定在一处最省心。4. 两种方式的选型逻辑什么场景用什么会不会互相冲突4.1 现实项目里哪种更常见这个问题几乎每个初学者都会问。我的观察是分阶段的传统 SSM / SSH 时代的老项目web.xml 几乎雷打不动。因为那时候项目里 Servlet 较多而且大量相关配置就是集中在 web.xml 里比如过滤器 Filter、监听器 Listener、Spring 的上下文加载监听器、欢迎页、错误页全都在这个文件里。散落各处的注解反而不好统一审查。现代 Spring Boot 时代Spring Boot 内置了 Spring MVC 的 DispatcherServlet你写的控制器不再是 HttpServlet 子类而是RestController标注的普通类。因此“Servlet 怎么配”这个问题在 Boot 型项目里被框架封装掉了。但如果你用的是原生 Servlet 项目、或者让 Boot 和外部 Servlet 容器配合两种方式依旧都在用。个人项目和快速原型注解是首选。代码和配置在一起改一个类就是改一处不用在两个文件之间来回跳心智负担小。所以我的建议很直接如果是学习理解机制必须玩透 web.xml如果是要快速交付功能用注解。两条腿走路缺一条都别扭。4.2 同一 Servlet 同时在 web.xml 和注解里配置会怎样这个问题很现实。比如你在 HelloServlet 类上写了WebServlet(/hello)又在 web.xml 里写了同样的/hello映射会不会报错结论分情况如果类名不同、URL 相同容器可能启动失败出现java.lang.IllegalArgumentException: The servlet name xxx is already defined之类的异常因为容器认为产生了冲突。如果完全相同的 Servlet 在注解和 web.xml 各配一遍部分容器以 web.xml 的配置为准注解会被忽略或覆盖。但不同容器之间表现有差异不要拿生产环境去赌这个行为。更常见的坑是“同名 Servlet、不同路径”。比如WebServlet(/hello) public class HelloServlet extends HttpServlet { ... }web.xml 里却又这样写servlet servlet-nameHelloServlet/servlet-name servlet-classcom.example.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-nameHelloServlet/servlet-name url-pattern/sayHello/url-pattern /servlet-mapping容器里会出现两个逻辑名称都是 HelloServlet 的 Servlet严格讲这属于定义了两次相同的名字是不规范的做法可能直接启动失败也可能只有一个生效。我踩过这个坑之后养成一个习惯一个 Servlet 的配置方式全项目只选一种绝不混着开。要么统一注解要么统一 web.xml这个规矩能让很多奇怪的问题在源头就消失。4.3 url-pattern 的匹配细节别让映射埋雷配置方法学会了路径规则同样重要。url-pattern 不是只能写一个具体的/hello它支持几类写法每种写法的匹配范围不同写法匹配规则示例/hello精确匹配只匹配这个路径/hello能访问/hello/可能都不行/hello/*目录匹配匹配/hello/下的所有路径/hello/a、/hello/b/c都能访问*.do扩展名匹配匹配以.do结尾的路径/user/list.do可以访问/缺省路径匹配所有没有其它规则匹配的请求常用于把请求交给核心分发器一个重要的匹配优先级规则精确匹配 目录匹配 扩展名匹配 缺省匹配。容器收到/hello/a时先找有没有精确的/hello/a没有再看/hello/*再看*.do最后才落到/。这个优先级机制对纯 Servlet 项目颇为关键一个非常经典的坑是把某个 Servlet 的 url-pattern 配成/*结果所有路径都进了同一个 Servlet连 JSP 页面请求也被它拦了页面全部变成一串源码或空白。原因就是/和/*的区别没搞明白——/是缺省兜底/*是目录匹配且优先级极高会抢先拦住所有请求。5. 新手最容易踩的配置坑和完整排查链路5.1 404 最常见映射看似写了实则没生效浏览器输入 URL 后长时间 404这个问题在配置里占比最高。我建议按下面顺序排查每步都能排除一类原因确认 URL 完整是否少了项目上下文路径访问 URL 的完整格式是http://ip:端口/上下文路径/url-pattern。在 IDEA 的 Tomcat 配置里默认上下文路径可能是/项目名_war_exploded和你以为的/项目名不一致URL 就会 404。确认类真的编译到了 classes 目录项目构建产物里有没有WEB-INF/classes/com/example/servlet/HelloServlet.class有时代码保存了但没编译Tomcat 运行的还是旧产物。确认 web.xml 的映射真的存在打开现在的 web.xml看servlet-name和url-pattern是否完整。新手很容易在复制模板时把 mapping 标签落到注释里或者放到了别的servlet内部结构嵌套错了。确认 Tomcat 的 catalina.out / 控制台输出有没有报错如果类名写错或ClassNotFoundException控制台一定有异常堆栈找org.apache.catalina相关的关键词。确认重启过容器web.xml 的修改必须在重启后才生效。如果是热部署没触发手动重启一次再试。按这个链路走完能解决大约九成的 404。剩下的一成是静态资源和动态映射重叠或者权限拦截器等更深层的问题。还有一个小细节IDE 中创建的 web.xml 不一定会被自动打包到 WAR 里。看构建产物时重点确认WEB-INF/web.xml这个文件到底在不在部署目录中。之前遇到过 IDEA 把 web.xml 放进了src/main/resources结果部署时根本没把它当 Web 配置读取启动倒是正常请求全部 404。5.2 启动报错servlet-name 不一致web.xml 里servlet和servlet-mapping的 servlet-name 不一致时Tomcat 启动通常直接报错The servlet named xxx is not defined in this web application这个报错说得很明白在 mapping 里引用了一个不存在的 Servlet 名字。解决办法就是把两个名字改到完全一致。这里要特别留意空格和大小写HelloServlet和HelloServlet之间有空格看起来没问题但对容器来说就是两个不同的字符串。排查这类问题的小技巧把 web.xml 里所有的servlet-name值列出来和所有servlet-mapping里引用的名字做一次“配对”。多配一个不成对立刻就能发现。5.3 重复映射与静态资源冲突我见过一个项目一个 Servlet 对应/user另一个 Servlet 也对应/user。Tomcat 启动时通常会提示类似 “Multiple mappings” 或直接覆盖具体表现因版本而异。这种重复映射的根因往往是一个人加了注解配置另一个人加了 web.xml 配置两个人互相不知道。另外一类冲突是动态映射抢了静态资源。比如配了url-pattern /之后你会发现访问项目里的静态图片、CSS、JS 全部失效因为它们都被 Servlet 接口拦走了没有资源处理器来响应。这是新手理解文件的 Servlet 流程时最强烈的挫败感来源之一明明配得“没错”页面就是乱样。解决方案是不要轻易改写/或/*这种级别的路径除非你很清楚自己在做请求入口分发。5.4 改了配置没生效关于重新部署Servlet 映射变了、初始化参数变了但浏览器访问还是旧行为。这有三个常见原因容器没有重新部署要看到 web.xml 修改后的效果必须重启 Tomcat 或触发一次 redeploy。浏览器缓存了响应尽管改了逻辑但 HTTP 响应被浏览器缓存按 F12 打开开发者工具勾选 Disable cache 再试。IDE 缓存问题IntelliJ IDEA 偶发没有同步最新 web.xml 到 target 目录手动执行 Build Build Artifacts Rebuild或者直接 Clean 后再启动。我个人的习惯是改 web.xml 这种关键配置后先直接重启 Tomcat不依赖热部署。热部署对类文件友好但对 XML 配置的支持不是所有环境都可靠与其来回试不如一次重启干净利落。6. 我的实操建议两种方法一个都不能少写到这里你应该能把握这两种方式的核心了。web.xml 是集中登记、外部管理适合项目整体把控注解是就地绑定、代码自带适合快速迭代。两者背后服务的是同一个目标让容器在正确的时间找到正确的类处理正确的 URL 请求。如果你刚入门我建议你先拿 web.xml 手写几个 Servlet理解类和 URL 的对应关系再用注解来写对比两者的差异。这个过程会让你真正理解容器的工作方式而不是只知道在某一个类上抄一段注解就能跑。以后你再看 Spring MVC、Spring Boot 里的请求映射也不会觉得它们神秘本质上都是在做“谁能处理哪个 URL”这件事的登记。最后分享一个实际经验我在团队里带新人时判断对方有没有真正理解 Servlet 配置就看一个问题——“为什么注解方式不用写 web.xml 也能被访问”能答出“容器扫描注解完成注册”这句话的人后面学什么框架都不太会卡壳。你现在看到这句了说明你也懂了。