news 2026/9/8 1:25:36

Servlet与web.xml配置详解:从原理到实战踩坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Servlet与web.xml配置详解:从原理到实战踩坑

刚接触Java Web的时候,我最怕的不是写Servlet代码,而是配web.xml。明明核心逻辑就在那个doGet方法里,可每次部署到Tomcat都卡在配置上——要么servlet-name对不上,要么url-pattern少了斜杠,页面直接甩我一个404,连个像样的日志都不给。后来带过几轮新人,发现几乎所有人都绕不开这道坎:注解一个@WebServlet就能搞定的事,为什么还要去啃web.xml?这篇文章我就从Servlet最基础的地方说起,把它的工作原理、容器加载机制、web.xml里每一行配置的实际作用,以及我这些年踩过的坑,一次性讲透。

1. Servlet到底是什么:一次请求背后的完整逻辑

1.1 为什么需要Servlet

要理解Servlet,先回到Web开发的原始场景。早期做动态网站,Java这边最原始的做法是用CGI(Common Gateway Interface):每个请求来了,服务器就启动一个独立进程去处理,处理完进程退出。这种方式的问题很明显——进程的创建和销毁成本太高,并发一上来服务器就扛不住。

Servlet的设计思路完全相反。它运行在Servlet容器(比如Tomcat)内部,以线程的方式处理请求,一个Servlet类在容器中只实例化一次,多个请求复用这同一个实例。请求进来时,容器从线程池中取出一个线程,调用Servlet的service方法去处理。这本质上是"单实例多线程"模型,避免了进程级别的开销,并发能力比CGI高好几个量级。

1.2 Servlet在整个Java Web技术栈里的位置

很多初学者容易把Servlet、JSP、Spring MVC的关系搞混。我的理解是:Servlet是地基,JSP本质上最终也会被容器编译成Servlet,而Spring MVC的DispatcherServlet本身也是一个Servlet,只不过它在这个基础Servlet之上做了路由分发。

所以你去看任何一个Java Web项目,不管用了多花哨的框架,底层一定是若干个Servlet在干活。理解了Servlet,你才算真正看懂了Java Web的底层运转方式。这也是为什么很多大厂面试还在问Servlet的生命周期、线程模型这些基础问题——因为这些才是真正跨框架的通用知识。

1.3 这篇文章的定位

我会以Tomcat 9 + Servlet 4.0为例,手把手演示一个完整的、使用web.xml方式配置的Servlet项目。内容覆盖:项目结构、Servlet类的编写、web.xml配置详解、请求处理流程、核心API使用、URL映射规则,以及我实际开发中遇到的常见错误和排查思路。不管你用的是Eclipse、IDEA还是纯命令行,核心逻辑都是一样的,照做就能跑通。

2. 一个完整的web.xml版Servlet示例:从零到浏览器看到输出

2.1 先搭好项目目录结构

用web.xml方式写Servlet,最标准的Java Web项目结构是这样的:

servlet-demo/ ├── src/ │ └── com/ │ └── demo/ │ └── servlet/ │ └── HelloServlet.java ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ │ └── index.html └── build/ (编译输出目录,部署时就是应用根目录)

这里有一个原则必须记住:WEB-INF目录下的文件是受容器保护的,客户端不能通过URL直接访问。web.xml必须放在WEB-INF下,这没有任何商量余地。lib目录放项目依赖的jar包,Servlet API的jar其实不用放——因为Tomcat本身就提供了,放了反而可能因为版本冲突出问题。

2.2 编写Servlet类

先写一个最简单的Servlet。继承HttpServlet,重写doGet方法,往浏览器输出一段HTML:

package com.demo.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloServlet extends HttpServlet { @Override public void init() throws ServletException { System.out.println("HelloServlet 初始化..."); } @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=UTF-8"); PrintWriter out = response.getWriter(); out.println("<!DOCTYPE html>"); out.println("<html>"); out.println("<head><meta charset=\"UTF-8\"><title>Hello Servlet</title></head>"); out.println("<body>"); out.println("<h1>Hello, Servlet 世界!</h1>"); out.println("<p>请求URI: " + request.getRequestURI() + "</p>"); out.println("<p>请求方法: " + request.getMethod() + "</p>"); out.println("<p>客户端IP: " + request.getRemoteAddr() + "</p>"); out.println("</body>"); out.println("</html>"); } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 简化处理,POST也走doGet的逻辑 doGet(request, response); } @Override public void destroy() { System.out.println("HelloServlet 销毁..."); } }

注意几点。第一,*@Override*这些方法名不能拼错,尤其是servicedoGet这类核心方法,拼错了容器不会报编译错误,但它会调用父类的HttpServlet.service,最终给你返回一个405。第二,init()destroy()是生命周期回调,我在这里打印了日志,后面排查问题时会看到它们的作用。第三,重写doPost并转发给doGet,这是开发中很常见的偷懒写法,为了演示方便这没问题,但生产环境中GET和POST各干各的才是规范做法。

2.3 web.xml中声明Servlet并完成URL映射

这一步是用web.xml方式配置的核心。先看完整配置:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <display-name>servlet-demo</display-name> <!-- 1. 声明Servlet --> <servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>com.demo.servlet.HelloServlet</servlet-class> <load-on-startup>1</load-on-startup> <init-param> <param-name>appName</param-name> <param-value>demo</param-value> </init-param> </servlet> <!-- 2. 建立URL映射 --> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>

这里面的逻辑要拆开理解。<servlet>标签做的事情是"注册":向容器声明有一个类叫com.demo.servlet.HelloServlet,我给它的逻辑名字叫helloServlet<servlet-mapping>做的事情是"指路":当浏览器访问/hello这个路径时,容器应该把请求交给名字为helloServlet的那个Servlet去处理。这两个标签通过<servlet-name>对应起来,名字必须完全一致,多一个空格都不行。

为什么不直接在<servlet>里写URL,非要拆成两个标签?原因在于一个Servlet可以配置多个URL映射,比如同一个helloServlet可以同时映射到/hello/hello2,拆开写才能实现这种一对多关系。

2.4 编译部署与验证

编译Servlet需要依赖Servlet API的jar包。如果你用的是Maven,加一个依赖就行:

<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>

注意scopeprovided,意思是编译时需要、但运行时由Tomcat提供,打包时不需要打进去。如果设成compile,你的war包会里塞一份Servlet API,部署时反而可能和容器的类冲突。

如果你是纯命令行操作,可以这样做:

# 解压Tomcat后,使用它自带的servlet-api.jar javac -cp $CATALINA_HOME/lib/servlet-api.jar -d build/WEB-INF/classes \ src/com/demo/servlet/HelloServlet.java # 手动构造部署目录,把web.xml和编译产物放进去 cp -r web/* build/ mkdir -p build/WEB-INF/classes cp -r build/com build/WEB-INF/classes/

部署完成后把build目录改名为servlet-demo,丢到Tomcat的webapps目录下,启动Tomcat,浏览器访问http://localhost:8080/servlet-demo/hello。看到页面输出"Hello, Servlet 世界!",这套流程就通了。

3. 容器在背后做了什么:Servlet的加载、实例化与请求分发

3.1 Servlet容器启动阶段发生了什么

Tomcat启动时,会扫描webapps下所有应用目录。对于每个应用,它会去读取WEB-INF/web.xml,把里面声明的所有Servlet解析成内部数据结构。但注意——此刻Servlet对象还没有被创建,只是"登记"了信息而已。

真正创建实例发生在两种时机:第一种,请求第一次到达这个Servlet的映射路径时;第二种,如果配置了<load-on-startup>,容器启动时就会立即创建。我在web.xml里写了<load-on-startup>1</load-on-startup>,所以Tomcat一启动就会执行HelloServlet的构造函数和init()方法,你会在日志里立刻看到"HelloServlet 初始化..."这行输出。

load-on-startup的数值表示启动顺序,数字越小越先加载。它的实际价值在于:有些Servlet需要在启动时完成重量级资源的初始化,比如加载配置文件、建立数据库连接池。如果不设置,这个初始化会被推迟到第一个请求进来时,用户就会感受到明显的卡顿——第一次访问特别慢,就是这个原因。

3.2 一次HTTP请求的完整旅程

当一个请求GET /servlet-demo/hello到达Tomcat时,完整的调用链是这样的:

  1. Tomcat的Connector组件(负责监听端口的线程)接收TCP连接,解析HTTP报文,封装成HttpServletRequestHttpServletResponse对象。
  2. Tomcat的Engine、Host、Context逐级定位应用,最终找到servlet-demo这个Context。
  3. Context根据URL路径/hello去匹配web.xml中配置的<url-pattern>,找到对应的<servlet-name>
  4. 容器检查helloServlet实例是否已存在,不存在则通过反射创建实例,调用init()
  5. 容器从线程池中取出一个工作线程,调用service(request, response)方法。
  6. HttpServletservice方法根据请求方法(GET还是POST)自动分发,最终调用我们重写的doGetdoPost
  7. 我们的代码往response里写了HTML,容器把响应刷回浏览器。
  8. 这个requestresponse对象的使命结束,被回收。

这套流程没有一步是多余的,理解它之后你再去看框架源码,你会发现Spring MVC、Struts这些最终都逃不出这套机制。

3.3 生命周期方法:init、service、destroy的三个阶段

Servlet的生命周期是面试高频考点,我用一句话概括:实例化一次,初始化一次,服务多次,销毁一次。

阶段调用时机调用次数典型用途
init()实例创建后、处理首个请求前1次加载配置、初始化连接池
service()每次请求到达多次根据HTTP方法分发到doGet/doPost
destroy()容器关闭或应用卸载1次释放资源、关闭连接

有个细节值得注意:init()destroy()默认是同步执行的,容器保证它们在多线程下只被调用一次。但service()是赤裸裸的多线程并发调用——同一个Servlet实例,同一时刻可能被5个线程同时执行doGet。如果doGet里面访问了共享的实例变量,而你又没做同步处理,就可能出现数据错乱。这是Servlet线程安全问题的根源,写代码时必须把共享状态设计成无状态,或者用局部变量。

3.4 destroy()在什么情况下不会执行

有经验的开发都知道,destroy()并不总是可靠。强制杀掉Tomcat进程(kill -9)、系统崩溃、断电,destroy()都不会执行。所以"在destroy里关闭数据库连接"这种写法只能作为优雅停机的补充,真正关键的资源释放,还是得依赖连接池的自动回收机制,不能把宝全押在destroy上。

4. Servlet核心API实战:请求、响应与参数处理的细节

4.1 HttpServletRequest:从请求里拿到一切

HttpServletRequest封装了整个HTTP请求,按内容分成三部分:请求行、请求头、请求体。

请求行包含请求方法、URI和协议版本,对应的方法是getMethod()getRequestURI()getProtocol()。请求头用getHeader("User-Agent")这类方法获取,可以用来判断客户端是浏览器还是爬虫。请求体是POST方式提交的数据,通过getParameter()获取。

参数获取是开发中用到最多的操作。三种方式:

// 方式一:GET请求的查询参数,也适用于POST的urlencoded格式 String name = request.getParameter("name"); // 方式二:拿到所有参数名,再逐一遍历(适合动态表单) Enumeration<String> names = request.getParameterNames(); while (names.hasMoreElements()) { String paramName = names.nextElement(); String paramValue = request.getParameter(paramName); } // 方式三:Map形式一次性获取(适合批量处理) Map<String, String[]> paramMap = request.getParameterMap();

有个坑提前说:对于同一个参数名,如果表单里出现了多次(比如多选框checkbox),getParameter()只会返回第一个值,这时必须用getParameterValues("hobby")拿String数组。

4.2 HttpServletResponse:控制输出的每一处细节

响应对象的核心职责是告诉容器"你要回给客户端什么"。最常用的是设置响应头和写响应体:

response.setContentType("text/html;charset=UTF-8"); response.setCharacterEncoding("UTF-8"); response.setHeader("Cache-Control", "no-cache"); response.setStatus(HttpServletResponse.SC_OK); PrintWriter out = response.getWriter(); out.write("<h1>响应内容</h1>");

那两行编码设置是重灾区,我详细说一下。setContentType("text/html;charset=UTF-8")同时设置了MIME类型和字符集,它会让响应头里带Content-Type: text/html;charset=UTF-8,浏览器据此用UTF-8解码。setCharacterEncoding("UTF-8")设置的是PrintWriter写出去字符时用的编码。实战中我建议两个都写上,顺序无所谓的,但必须在第一次调用getWriter()之前设置,否则容器已经根据默认编码写入响应头了,后面再改不生效。

如果需要返回JSON数据,setContentType("application/json;charset=UTF-8"),然后直接写JSON字符串。返回文件下载,则要设置Content-Disposition响应头。

4.3 中文乱码的根源:两头编码要一致

中文乱码这个问题几乎每个新手都遇到过。用一句话说清楚:乱码的本质是客户端和服务端在某个环节用了不同的编码规则。

POST请求的乱码,是因为Tomcat 8以前默认用ISO-8859-1解析请求体。解决办法是在读取任何参数之前执行:

request.setCharacterEncoding("UTF-8");

注意这个方法的限定条件:只对POST请求的请求体有效,必须在getParameter()之前调用,而且必须在第一次调用之后就不能再改。Tomcat 8及以上版本默认请求体编码已经是UTF-8了,所以新项目里POST乱码通常不出现。

GET请求的乱码是另一个原因——Tomcat对URL中的查询参数用URI编码解析,默认也是ISO-8859-1。解决办法是修改Tomcat的server.xml,在Connector节点加一行:

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />

改完重启Tomcat,GET中文参数就正常了。现在的Tomcat 8+默认URIEncoding就是UTF-8,所以新环境很少踩这个坑,但你要是维护老项目,碰到GET乱码第一时间查这里。

4.4 转发和重定向:路径语义完全不同

request.getRequestDispatcher("/target").forward(request, response)是转发,response.sendRedirect("target")是重定向。两者最大的区别有三个维度:

  • URL变化:转发后浏览器地址栏不变,重定向会变成新地址。
  • 请求次数:转发是服务器内部一次请求,重定向是两次请求。
  • 数据共享:转发的request对象可以带参数到目标Servlet,重定向的两次请求完全独立,参数必须通过URL或session传递。
// 转发:URL不变,request里的属性可以带到下一个Servlet request.setAttribute("user", userInfo); request.getRequestDispatcher("/user/detail").forward(request, response); // 重定向:URL变成新地址,参数只能拼在URL上 response.sendRedirect(request.getContextPath() + "/user/detail?uid=1001");

一个很容易犯的错误是重定向时忘记加request.getContextPath()。如果你的应用部署在/servlet-demo这个上下文下,直接写sendRedirect("/user/detail")会跳到http://localhost:8080/user/detail,丢失了应用名,导致404。加上了getContextPath()才会变成/servlet-demo/user/detail

5. URL映射规则与web.xml配置进阶

5.1 四种匹配规则与优先级顺序

web.xml里<url-pattern>支持四种写法:

写法示例匹配规则
精确匹配/hello完全路径一致才命中
路径匹配/user/*匹配以/user开头的所有路径
扩展名匹配*.do匹配所有以.do结尾的URL
默认匹配/匹配所有未被其他规则命中的请求

优先级顺序是:精确匹配 > 路径匹配 > 扩展名匹配 > 默认匹配。注意/user/*这种路径匹配比*.do优先级高,即使在web.xml里先写了*.do也一样——优先级和声明顺序无关,只看规则类型。

这里有个容易踩的坑://*的区别。/是默认Servlet,当所有其他映射都没命中时兜底,同时它还负责处理webapps下的静态资源。/*则是通配所有路径,它会拦截包括JSP在内的所有请求。如果你把某个Servlet映射到/*,你会发现JSP页面全都不渲染了,全部进入这个Servlet处理——这种匪夷所思的事故,十有八九是/*干的好事。

5.2 一个Servlet配置多个URL模式

前面提过,一个Servlet可以对应多个url-pattern。web.xml是这么写的:

<servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/hello</url-pattern> <url-pattern>/hello2</url-pattern> <url-pattern>/greeting/*</url-pattern> </servlet-mapping>

这样配置之后,/hello/hello2/greeting/anything都会进入同一个Servlet。这个特性在处理"同一套逻辑对外暴露多个入口"时很实用,比如一个API同时支持/api/v1/users/api/v2/users,可以映射到同一个Servlet,在Servlet内部再根据URI做版本区分。

5.3 init-param:在web.xml中给Servlet传参

Servlet内部可以通过getInitParameter读取web.xml里配置的初始化参数:

String appName = getInitParameter("appName");

我在示例的web.xml里配置了<init-param>,对应的就是这段代码读取的值。这个机制的价值在于配置与代码分离:数据库地址、告警开关、环境标识这些可能因环境而变的值,不需要修改代码、重新编译,只改web.xml里的参数就行。

<context-param>则是更高一级的配置,作用范围是整个应用,所有Servlet都能通过getServletContext().getInitParameter("globalKey")读取。适合放全局配置,比如全局字符编码、全局版本号。

5.4 welcome-file与错误页面配置

welcome-file-list指定了用户访问应用根路径时默认展示的页面:

<welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.jsp</welcome-file> </welcome-file-list>

注意这里有个顺序逻辑:Tomcat会从上到下依次检查文件是否存在,用第一个存在的文件。如果文件都不存在,会返回404。

错误页面配置也很有用,可以让404、500这类错误展示友好的提示页,而不是丑陋的默认报错页:

<error-page> <error-code>404</error-code> <location>/error404.html</location> </error-page> <error-page> <error-code>500</error-code> <location>/error500.html</location> </error-page>

我接手过的老项目里,有一类很经典的场景:线上用户反馈"页面白屏报错",开发一看就是个500,但用户不知道发生了什么。配好错误页面之后,至少用户看到的是一句友好的提示,运维排查也更有抓手。

6. 踩坑实录:web.xml方式最常见的五个错误与排查思路

6.1 404错误:先分清是路径问题还是映射问题

浏览器访问Servlet的URL返回404时,我的排查顺序是这样的:

第一,确认URL里的应用名对不对。http://localhost:8080/servlet-demo/hello,其中servlet-demo是部署目录的名字。如果你改过war包名或目录名,应用名就变了,URL也要跟着变。

第二,确认<url-pattern>写的对不对。/hello前面必须有斜杠,写成hello容器会在启动时报错或者根本匹配不上。检查web.xml的时候我还会顺带看一眼<servlet-mapping>里的<servlet-name><servlet>里的<servlet-name>是否完全一致。

第三,确认应用有没有正常发布。看Tomcat日志里有没有Deploying web application directoryDeployment of web application这两个关键日志。如果没有,说明应用压根没加载,多半是web.xml格式有问题导致解析失败。

6.2 500错误:初始化失败的完整排查链路

500错误比404复杂得多,因为它可能出在任何环节。以一个典型场景为例:java.lang.ClassNotFoundException: com.demo.servlet.HelloServlet

这个错误说明容器找不到Servlet类。为什么找不到?最常见的三个原因:

  • 编译产物没放到正确位置。记得我前文强调过的目录结构吗?类文件必须放在WEB-INF/classes对应包路径下,也就是WEB-INF/classes/com/demo/servlet/HelloServlet.class。位置错了,类加载器就找不到。
  • 编译时用的包名和web.xml里<servlet-class>写的包名不一致。这种情况经常发生在新手改代码的时候,包名从com.demo改成com.example,忘了同步改web.xml。
  • jar包依赖的类缺失。如果Servlet里用到第三方库(比如JSON解析库),这些jar必须放在WEB-INF/lib下。放在别的地方,应用部署时不会把它加到类路径里。

排查时看Tomcat的localhost.log,里面的异常堆栈会精确告诉你是哪个类加载失败,定位速度比猜快得多。

6.3 改了代码不生效:缓存和重启的先后关系

开发阶段最让人抓狂的bug之一:改了Servlet代码,重新编译部署了,但浏览器访问还是旧逻辑。原因多半是没注意这几点:

  • Tomcat没有真正重启。webapps目录下如果你直接用解压目录部署,容器可能还缓存着旧类。稳妥做法是先把应用从webapps下移走或删掉,停Tomcat,再把新版本放进去,启动。
  • 浏览器缓存了页面。尤其是Content-Type: text/html的响应,浏览器经常缓存。可以开一个无痕窗口验证,或者在响应头上加Cache-Control: no-cache
  • 编译没成功但没报错。javac如果编译失败会有错误输出,但你连续执行命令时容易看漏。编译完用ls确认一下class文件的时间戳是不是最新的。

6.4 javax和jakarta:包名引起的连锁故障

这是一个新老项目交替时期特有的坑。Tomcat 9及以下版本,Servlet API的包名是javax.servlet;从Tomcat 10开始,包名改成了jakarta.servlet。如果你用Tomcat 10跑一个基于javax.servlet的老项目,启动时会报ClassNotFoundException或者NoClassDefFoundError

你在网上搜Servlet教程,看到代码里import javax.servlet.http.HttpServlet,先确认自己用的Tomcat版本。如果是Tomcat 10+,需要把导入改成import jakarta.servlet.http.HttpServlet。同时,web.xml的<web-app>头部也要同步改(xmlns="https://jakarta.ee/xml/ns/jakartaee"并对应版本号),否则容器不识别。

我的建议是:新项目直接上Tomcat 10+和jakarta包名,老项目就老老实实用Tomcat 9。两边切换是最容易出幺蛾子的。

6.5 定位问题的终极手段:开日志、看堆栈、翻目录

遇到实在排查不出来的问题,我会做三件事:

第一,看Tomcat的控制台输出和logs目录下的catalina.outlocalhost.log。Java Web的绝大多数问题都会在这里留下痕迹。第二,确认部署目录的实际内容,用find . -name "*.class"看看编译产物到底在不在预期位置。第三,确认访问的URL和web.xml映射是否完全吻合,包括大小写和斜杠。

有一次我排查了很久,最后发现是HelloServlet这个名字的大小写问题——Linux文件系统区分大小写,HelloServlet.classhelloServlet.class是两个完全不同的文件。这种问题Windows上不会出现,一上Linux就暴露了。

7. 我的一点实际体会:web.xml和注解,到底怎么选

最后说点掏心窝的话。现在开发新项目,几乎没人会纯手工去写web.xml了,@WebServlet("/hello")确实方便太多。但我依然建议你至少完整走一遍web.xml方式的流程,理由有三个。

第一,理解"声明-映射"这个分离思想。注解方式把两者压缩成了一个注解,隐藏了背后的设计逻辑。你亲手写一遍web.xml,就会真正明白Servlet名和URL是两个维度的事,这对后续理解框架路由有巨大帮助。

第二,维护老项目的必备技能。大量的存量Web应用还是web.xml配置方式,尤其是企业级系统、银行保险项目里的老系统,可能还要改个接口、加个Servlet。这时候你不会web.xml,连门都进不去。

第三,web.xml不止配Servlet。Filter、Listener、welcome-file、error-page、context-param这些全局配置,在web.xml里都能看到全貌。注解方式虽然也有对应API,但配置分散在各个类上,审视全局时反而不如一份web.xml来得直观。

在实际项目中我见到最多的做法是注解为主、web.xml为辅:日常Servlet用注解开发,但涉及安全过滤链顺序、全局参数、错误页这类需要全局视角的配置,还是会集中放在web.xml里管理。所以别把两者对立起来,它们从来不是二选一的关系。

回到文章开头那个问题——为什么明明注解能搞定,还要啃web.xml?我的答案是:学习Servlet不是为了用某个特定方式去写配置,而是为了理解容器和 Servlet 之间的协作契约。web.xml只是这份契约最完整、最显式的一种表达形式。把这份契约吃透了,以后无论是注解、还是框架,你看到的都是同一个底层的套路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 1:24:43

公司充值ChatGPT服务支付方式系统指南

2026年AI技术在企业场景的应用持续深化&#xff0c;中泰证券《Token 经济学&#xff1a;AI 时代的新生产要素与产业重构》研究显示&#xff0c;Token已成为AI时代核心生产要素与价值载体&#xff0c;企业在ChatGPT等大模型API调用、AI工具订阅、算力采购等方面的支出规模快速增…

作者头像 李华
网站建设 2026/9/8 1:24:33

毕业设计全程AI工具链:从论文写作到代码开发的实战组合

每年到毕业季前后&#xff0c;我总能收到大量学弟学妹的私信&#xff0c;问的无外乎是“论文怎么写才能不被导师连环打回”“毕业设计的系统到底怎么搭”“代码跑不通怎么办”。说实话&#xff0c;过去几年大家还在靠纯手工肝文档、熬夜调代码&#xff0c;但今年这批人手里已经…

作者头像 李华
网站建设 2026/9/8 1:23:26

nvCOMP实战指南:用GPU将压缩吞吐提升一个量级

做数据处理和存储这行的朋友&#xff0c;对LZ4、Snappy、Zstd这些压缩库应该都不陌生。但如果你接触过大规模数据的在线导入、列式存储落盘&#xff0c;或者AI训练前的数据预处理链路&#xff0c;大概率会遇到一个尴尬场景&#xff1a;CPU核数堆得很高&#xff0c;压缩吞吐还是…

作者头像 李华
网站建设 2026/9/8 1:23:17

ComfyUI本地部署与AI漫剧工作流搭建实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 1:22:49

秋叶ComfyUI整合包评测:中文界面一键部署AI绘画本地方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/8 1:21:52

正整数构造算法:贪心策略与数字拆分实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华