
Servlet 是 Java EE(现为 Jakarta EE)规范中的一个重要组件,用于处理客户端请求并生成动态响应。Servlet 通常运行在 Servlet 容器(如 Apache Tomcat、Jetty 等)中,负责接收 HTTP 请求、处理业务逻辑并返回 HTTP 响应。
Servlet 规范有很多版本,目前主流的是 4.x 及 6.x。以下内容引自 廖雪峰的官方网站 - 手写Tomcat - Servlet规范[1]:
Servlet 3.0:支持异步处理的Servlet,支持注解配置Servlet和过滤器,增加了SessionCookieConfig接口; Servlet 3.1:提供了WebSocket的支持,增加了对HTTP请求和响应的流式操作的支持,增加了对HTTP协议的新特性的支持; Servlet 4.0:支持HTTP/2的新特性,提供了HTTP/2的Server Push等特性; Servlet 5.0:主要是把javax.servlet包名改成了jakarta.servlet; Servlet 6.0:继续增加一些新功能,并废除一部分功能。
Servlet 4.0 及之前的规范,可以从 https://jcp.org/en/jsr/summary?id=servlet 获取:
Servlet 5.0 及之后的规范,可直接在线获取:
Maven 坐标从 4.0.2 开始,由
<!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
更换为:
<!-- https://mvnrepository.com/artifact/jakarta.servlet/jakarta.servlet-api -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>4.0.2</version>
<scope>provided</scope>
</dependency>
源码 package 从 5.0 开始由 javax.servlet[7] 更换为 jakarta.servlet[8]。
Servlet 规范中,HttpServletRequest 接口关于 getServletPath() 方法的描述如下。
Servlet 4.0[13]
/**
* Returns the part of this request's URL that calls the servlet. This path
* starts with a "/" character and includes either the servlet name or a
* path to the servlet, but does not include any extra path information or a
* query string. Same as the value of the CGI variable SCRIPT_NAME.
* <p>
* This method will return an empty string ("") if the servlet used to
* process this request was matched using the "/*" pattern.
*
* @return a <code>String</code> containing the name or path of the servlet
* being called, as specified in the request URL, decoded, or an
* empty string if the servlet used to process the request is
* matched using the "/*" pattern.
*/
public String getServletPath();
Servlet 6.1[14]
/**
* Returns the part of this request's URL that calls the servlet. This path starts with a "/" character and includes the
* path to the servlet, but does not include any extra path information or a query string.
*
* <p>
* This method will return an empty string ("") if the servlet used to process this request was matched using the "/*"
* pattern.
*
* @return a <code>String</code> containing the path of the servlet being called, as specified in the request URL, or an
* empty string if the servlet used to process the request is matched using the "/*" pattern. The path will be
* canonicalized as per <a href=
* "https://jakarta.ee/specifications/servlet/6.0/jakarta-servlet-spec-6.0.html#request-uri-path-processing">Servlet
* 6.0, 3.5</a>. This method will not return any encoded characters unless the container is configured specifically to
* allow them.
* @throws IllegalArgumentException In standard configuration, this method will never throw. However, a container may be
* configured to not reject some suspicious sequences identified by <a href=
* "https://jakarta.ee/specifications/servlet/6.0/jakarta-servlet-spec-6.0.html#uri-path-canonicalization">Servlet 6.0,
* 3.5.2<a/>, furthermore the container may be configured to allow such paths to only be accessed via safer methods like
* {@link #getRequestURI()} and to throw IllegalArgumentException if this method is called for such suspicious paths.
*/
String getServletPath();
两个版本细节处有些区别,没有本质差异。
规范文档中,针对 requestURI 的组成部分及各部分含义有明确的说明,以及一个简明的例子:
requestURI = contextPath + servletPath + pathInfo
有三个 Servlet:
Context Path | /catalog |
|---|---|
Servlet Mapping | Pattern: /lawn/*Servlet: LawnServlet |
Servlet Mapping | Pattern: /garden/*Servlet: GardenServlet |
Servlet Mapping | Pattern: *.jspServlet: JSPServlet |
不同请求 URI 对应的各部分值:
Request Path | Path Elements |
|---|---|
/catalog/lawn/index.html | ContextPath: /catalogServletPath: /lawnPathInfo: /index.html |
/catalog/garden/implements/ | ContextPath: /catalogServletPath: /gardenPathInfo: /implements/ |
/catalog/help/feedback.jsp | ContextPath: /catalogServletPath: /help/feedback.jspPathInfo: null |
可以看到其中 ServletPath 与 Servlet Mapping 的 Pattern 是对应的。
但是这个例子中没有提到存在匹配 /* 路径的 Servlet 的情况。按照接口描述中说明,此时返回的 ServletPath 应该是空字符串 ""。让我们看看一些 Servlet 容器实际的实现情况。
延续 还在给每个请求加前缀避免模块间接口冲突呢?[15] 中构造的 demo 工程 multi-dispatcher[16],略微调整以适应本文示例需求。
通过 env 参数切换使用不同的 Servlet 容器:
# 默认使用 tomcat 容器
$ ./gradlew bootRun
# 使用 jetty 容器
$ ./gradlew bootRun -Denv=jetty
# 使用 undertow 容器
$ ./gradlew bootRun -Denv=undertow
添加 --args='--server.servlet.context-path=/demo' 指定上下文根:
$ ./gradlew bootRun -Denv=undertow --args='--server.servlet.context-path=/demo'
启动后可通过类似下面的日志验证使用的容器:
2025-09-06 11:18:52.297 INFO 6679 --- [ main] o.s.b.w.e.u.UndertowServletWebServer : Undertow started on port(s) 8080 (http) with context path '/demo'
之后访问如下地址:
示例响应:
{
"Context Path": "/demo",
"Request URL": "http://localhost:8080/demo/foo/same/path",
"Servlet Path": "/foo",
"Request URI": "/demo/foo/same/path",
"Servlet": "Foo Servlet",
"Path Info": "/same/path",
"URL Mapping": "/foo/*"
}
在 Servlet 注册到了哪?[17] 和 Tomcat 是怎么找到用来处理请求的 Servlet 的?[18] 中,我们梳理过 Tomcat 处理请求的过程。本文继续以 Tomcat 9 为例,说明其实现 Servlet 4.0 规范的情况,Tomcat 版本与 Servlet 版本的对应关系可见:Apache Tomcat Versions[19]。
org.apache.catalina.connector.Request[20] 实现了 HttpServletRequest 接口,对 getServletPath() 方法的实现如下:
@Override
public String getServletPath() {
return mappingData.wrapperPath.toStringType();
}
mappingData 是在 org.apache.catalina.connector.CoyoteAdapter.postParseRequest[21] 方法中映射上的:
connector.getService().getMapper().map(serverName, decodedURI, version, request.getMappingData());
不同类型的 Servlet,对 MappingData[22] 的 wrapperPath 的设置方式不同。
默认 Serlvet:
Mapper.internalMapWrapper[23]
// Rule 7 -- Default servlet
if (mappingData.wrapper == null && !checkJspWelcomeFiles) {
if (contextVersion.defaultWrapper != null) {
mappingData.wrapper = contextVersion.defaultWrapper.object;
mappingData.requestPath.setChars(path.getBuffer(), path.getStart(), path.getLength());
mappingData.wrapperPath.setChars(path.getBuffer(), path.getStart(), path.getLength());
mappingData.matchType = MappingMatch.DEFAULT;
}
...
通配符匹配的 Servlet:
Mapper.internalMapWildcardWrapper[24]
if (found) {
mappingData.wrapperPath.setString(wrappers[pos].name);
if (path.getLength() > length) {
mappingData.pathInfo.setChars(path.getBuffer(), path.getStart() + length,
path.getLength() - length);
}
mappingData.requestPath.setChars(path.getBuffer(), path.getStart(), path.getLength());
mappingData.wrapper = wrappers[pos].object;
mappingData.jspWildCard = wrappers[pos].jspWildCard;
mappingData.matchType = MappingMatch.PATH;
}
$ ./gradlew bootRun --args='--server.servlet.context-path=/demo'
Servlet | Default Servlet | Foo Servlet |
|---|---|---|
url mapping | /* | /foo/* |
request.getContextPath() | /demo | /demo |
request.getServletPath() | /same/path | /foo |
request.PathInfo() | /same/path | |
request.getRequestURI() | /demo/same/path | /demo/foo/same/path |
request.getRequestURL() | http://localhost:8080/demo/same/path | http://localhost:8080/demo/foo/same/path |
由默认 Servlet(URL mapping 是 /*)处理的请求,其 ServletPath 是请求的实际路径,跟 Servlet 规范中描述的情况不一致:
Servlet Path: The path section that directly corresponds to the mapping which activated this request. This path starts with a ’/’ character except in the case where the request is matched with the ‘/*’ or ““ pattern, in which case it is an empty string
以下面命令分别切换容器启动:
$ ./gradlew bootRun -Denv=jetty --args='--server.servlet.context-path=/demo'
$ ./gradlew bootRun -Denv=undertow --args='--server.servlet.context-path=/demo'
结果与使用 Tomcat 容器的情况一致,均是:
Servlet | Default Servlet | Foo Servlet |
|---|---|---|
url mapping | /* | /foo/* |
request.getContextPath() | /demo | /demo |
request.getServletPath() | /same/path | /foo |
request.PathInfo() | /same/path | |
request.getRequestURI() | /demo/same/path | /demo/foo/same/path |
request.getRequestURL() | http://localhost:8080/demo/same/path | http://localhost:8080/demo/foo/same/path |
目前三个主流的开源 Servlet 容器:Tomcat、Jetty 和 Undertow,处理请求的 ServletPath 方式基本一致:
参考资料

还在给每个请求加前缀避免模块间接口冲突呢?

Servlet 注册到了哪?

Tomcat 是怎么找到用来处理请求的 Servlet 的?
[1]
廖雪峰的官方网站 - 手写Tomcat - Servlet规范: https://liaoxuefeng.com/books/jerrymouse/servlet-spec/index.html
[2]
JSR-000369 Java Servlet 4.0 Specification Final Release: https://download.oracle.com/otndocs/jcp/servlet-4-final-spec/index.html
[3]
Online JavaDoc: https://jakarta.ee/specifications/servlet/4.0/apidocs/
[4]
Jakarta Servlet Specification 6.1: https://jakarta.ee/specifications/servlet/6.1/jakarta-servlet-spec-6.1
[5]
Online JavaDoc: https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/
[6]
Project jakartaee/servlet on GitHub: https://github.com/jakartaee/servlet
[7]
javax.servlet: https://github.com/jakartaee/servlet/tree/4.0.4-RELEASE/api/src/main/java/javax/servlet
[8]
jakarta.servlet: https://github.com/jakartaee/servlet/tree/5.0.0-RELEASE/api/src/main/java/jakarta/servlet
[9]
Jakarta Servlet 4.0~6.2: https://jakarta.ee/specifications/servlet/
[10]
Jakarta Servlet GitHub Pages: https://jakartaee.github.io/servlet/
[11]
Java Community Process: https://jcp.org/en/jsr/summary?id=servlet
[12]
Eclipse EE4J Servlet Project: https://projects.eclipse.org/projects/ee4j.servlet
[13]
Servlet 4.0: https://jakarta.ee/specifications/servlet/4.0/apidocs/javax/servlet/http/httpservletrequest#getServletPath--
[14]
Servlet 6.1: https://github.com/jakartaee/servlet/blob/6.1.0-RELEASE/api/src/main/java/jakarta/servlet/http/HttpServletRequest.java#L432-L452
[15]
还在给每个请求加前缀避免模块间接口冲突呢?: https://alphahinex.github.io/2020/04/24/multi-dispatcherservlet/
[16]
multi-dispatcher: https://github.com/AlphaHinex/multi-dispatcher
[17]
Servlet 注册到了哪?: https://alphahinex.github.io/2020/04/30/where-are-servlets/
[18]
Tomcat 是怎么找到用来处理请求的 Servlet 的?: https://alphahinex.github.io/2020/08/02/how-request-find-servlet/
[19]
Apache Tomcat Versions: https://tomcat.apache.org/whichversion.html
[20]
org.apache.catalina.connector.Request: https://github.com/apache/tomcat/blob/9.0.109/java/org/apache/catalina/connector/Request.java#L2273-L2276
[21]
org.apache.catalina.connector.CoyoteAdapter.postParseRequest: https://github.com/apache/tomcat/blob/9.0.109/java/org/apache/catalina/connector/CoyoteAdapter.java#L697
[22]
MappingData: https://github.com/apache/tomcat/blob/9.0.109/java/org/apache/catalina/mapper/MappingData.java
[23]
Mapper.internalMapWrapper: https://github.com/apache/tomcat/blob/9.0.109/java/org/apache/catalina/mapper/Mapper.java#L950-L957
[24]
Mapper.internalMapWildcardWrapper: https://github.com/apache/tomcat/blob/9.0.109/java/org/apache/catalina/mapper/Mapper.java#L1052-L1062