在 Spring Boot 项目里,很多人第一次接触 Servlet 注册时,往往只知道 ServletRegistrationBean,但实际可选方案远不止这一种。不同做法背后对应的是不同的编程模型:有的偏 Spring Boot 配置化,有的沿用 Servlet 规范注解,有的适合启动期动态装配,还有的其实已经转向 Spring MVC 的动态映射或函数式路由。
这篇文章把常见的 5 种方式按“什么时候用、代码怎么写、边界在哪里”重新梳理一遍。读完后,你可以更快判断:当前需求究竟是在注册传统 Servlet,还是已经进入更适合用 MVC 路由或函数式接口处理的场景。
1. 用 RegistrationBean 注册:Spring Boot 原生、最常见
Spring Boot 提供了 ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean 三个类,分别用来注册 Servlet、Filter 和 Listener。对大多数 Spring Boot 项目来说,这是最顺手的一种写法。
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class RegisterServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String name = getServletConfig().getInitParameter("name");
String sex = getServletConfig().getInitParameter("sex");
resp.getOutputStream().println("name is " + name);
resp.getOutputStream().println("sex is " + sex);
}
}
@Bean
public ServletRegistrationBean registerServlet() {
ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new RegisterServlet(), "/registerServlet");
servletRegistrationBean.addInitParameter("name", "javastack");
servletRegistrationBean.addInitParameter("sex", "man");
return servletRegistrationBean;
}
这种方式的优点比较明确:
- Spring Boot 原生支持,和项目配置体系一致;
- URL 映射、初始化参数都集中在一个 Bean 里;
- 如果后续还要注册 Filter、Listener,思路也完全统一。
如果你的目标就是在 Spring Boot 中接入一个传统 Servlet,并且不需要复杂的动态控制,这通常就是优先选择。
2. 用 @WebServlet 等注解注册:适合从传统 Servlet 项目迁移
在 Servlet 3.0 之前,Servlet、Filter、Listener 通常依赖 web.xml 配置。到了 3.0 之后,这类组件可以通过注解方式声明,例如 @WebServlet、@WebFilter、@WebListener。
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 = "javaServlet", urlPatterns = "/javastack.cn", asyncSupported = true,
initParams = {
@WebInitParam(name = "name", value = "javastack"),
@WebInitParam(name = "sex", value = "man") })
public class JavaServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String name = getServletConfig().getInitParameter("name");
String sex = getServletConfig().getInitParameter("sex");
resp.getOutputStream().println("name is " + name);
resp.getOutputStream().println("sex is " + sex);
}
}
注意:启动类上需要加上注解 @ServletComponentScan,用于扫描这些 Servlet 组件注解。也就是说,@ServletComponentScan 会扫描 @WebServlet、@WebFilter 和 @WebListener。
这类方案更接近原生 Servlet 规范,适合两种情况:
- 从传统 Servlet 项目迁移到 Spring Boot,希望保留原有组件定义风格;
- 团队本身就习惯在组件类上直接声明 URL、初始化参数和异步支持能力。
它的优点是“定义即配置”,阅读成本低;但如果你希望把注册逻辑统一收口到 Spring 配置类,RegistrationBean 往往更容易维护。
3. 用 ServletContextInitializer 动态注册:适合启动期按条件装配
如果需求不只是“写死一个 Servlet 和一个路径”,而是希望在容器启动时按条件决定是否注册、注册到哪里、带什么参数,那么可以实现 org.springframework.boot.web.servlet.ServletContextInitializer。

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 = "javaServlet", urlPatterns = "/javastack.cn", asyncSupported = true,
initParams = {
@WebInitParam(name = "name", value = "javastack"),
@WebInitParam(name = "sex", value = "man") })
public class JavaServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String name = getServletConfig().getInitParameter("name");
String sex = getServletConfig().getInitParameter("sex");
resp.getOutputStream().println("name is " + name);
resp.getOutputStream().println("sex is " + sex);
}
}
import cn.javastack.springbootbestpractice.servlet.InitServlet;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.stereotype.Component;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
@Component
public class ServletConfig implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) {
ServletRegistration initServlet = servletContext.addServlet("initServlet", InitServlet.class);
initServlet.addMapping("/initServlet");
initServlet.setInitParameter("name", "javastack");
initServlet.setInitParameter("sex", "man");
}
}
这里的关键点不在注解扫描,而在于 onStartup(ServletContext servletContext) 这个时机。你可以在容器启动阶段直接操作 ServletContext,把 Servlet 注册信息主动塞进去。
这种方式适合什么场景
- 是否注册要根据配置、环境或外部条件判断;
- 路径和初始化参数需要在启动时动态计算;
- 你希望更贴近底层 Servlet 容器生命周期来做控制。
和前两种相比,它的灵活性更高,但代码也更偏底层。对于普通固定注册需求,没有必要优先走到这一步。
4. 用 RequestMappingHandlerMapping.registerMapping():本质上是动态注册请求映射
第四种常被放进“注册 Servlet”一起讨论,但严格来说,它更像是在运行时动态注册 Spring MVC 的请求映射,而不是传统意义上的 Servlet 注册。
private void registerServerEndpoint(String path, Object bean) throws NoSuchMethodException {
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
RequestMappingInfo.BuilderConfiguration config = new RequestMappingInfo.BuilderConfiguration();
if (webMvcProperties.getPathmatch().getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) {
config.setPatternParser(new PathPatternParser());
} else {
config.setPathMatcher(new AntPathMatcher());
}
RequestMappingInfo handleGet = RequestMappingInfo.paths(path).methods(RequestMethod.GET).options(config).build();
mapping.registerMapping(handleGet, bean, ServletHttpHandler.class.getMethod("handleGet"));
RequestMappingInfo handlePost = RequestMappingInfo.paths(path).methods(RequestMethod.POST).options(config).build();
mapping.registerMapping(handlePost, bean, ServletHttpHandler.class.getMethod("handlePost", String.class));
}
这段代码做的事情是:把某个路径的 GET、POST 请求,在运行时挂到 Spring MVC 的处理器方法上。重点在于:
- 路径可以动态决定;
- HTTP 方法可以分别注册;
- 最终接入的是 Spring MVC 映射体系,而不是原生 Servlet 入口。
因此,如果你的系统已经是典型的 Spring MVC 应用,并且需要运行时扩展接口能力,比如插件式路由、动态开放接口、按配置生成请求入口,这种方式会更合适。
但如果你真正需要的是一个标准 Servlet 生命周期对象,那么这并不是最直接的方案。
5. 用 RouterFunction 注册:函数式路由更适合流式接口
第五种方式是通过 org.springframework.web.servlet.function.RouterFunction 来注册处理逻辑。它和传统注解式 Controller 不同,走的是函数式路由模型。

import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.RouterFunctions;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;
import javax.servlet.ServletException;
import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
public class StreamServer {
private final ObjectMapper objectMapper = new ObjectMapper();
private final String endpoint;
private final RouterFunction router;
public StreamServer(String endpoint) {
this.endpoint = endpoint;
this.router = RouterFunctions.route()
.POST(this.endpoint, this::handlePost)
.build();
}
public RouterFunction getRouter() {
return this.router;
}
private ServerResponse handlePost(ServerRequest request) throws ServletException, IOException {
List acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM)
|| !acceptHeaders.contains(MediaType.APPLICATION_JSON)) {
return ServerResponse.badRequest()
.body("Invalid Accept headers. Expected TEXT_EVENT_STREAM and APPLICATION_JSON");
}
String body = request.body(String.class);
log.info("body:{}", body);
String sessionId = UUID.randomUUID().toString();
return ServerResponse.sse(sseBuilder -> {
sseBuilder.onComplete(() -> {
log.debug("Request response stream completed for session: {}", sessionId);
});
sseBuilder.onTimeout(() -> {
log.debug("Request response stream timed out for session: {}", sessionId);
});
try {
for (int i = 0; i < 100; ++i) {
Map row = new HashMap<>();
row.put("rowId", i + 1);
row.put("rowName", i + 1);
String json = objectMapper.writeValueAsString(row);
sseBuilder.id(sessionId)
.event("message")
.data(json);
}
sseBuilder.complete();
} catch (Exception e) {
log.error("Failed to handle request stream: {}", e.getMessage());
sseBuilder.error(e);
}
}, Duration.ZERO);
}
}
import com.github.kylewka.smartai.demo.stream.StreamServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;
@Configuration
public class StreamServerConfig {
@Bean
public StreamServer streamServer() {
return new StreamServer("/test/stream");
}
@Bean
public RouterFunction sseRoute(StreamServer streamServer) {
return streamServer.getRouter();
}
}
上面这个例子里,核心是把 /test/stream 的 POST 请求直接绑定到 handlePost,并返回 SSE 流式响应。相比传统 Controller,这种写法在以下场景更有优势:
- 函数式接口编排更清晰;
- 流式响应,例如 SSE,更容易集中处理;
- 希望弱化注解式 Controller,直接用路由对象组织入口。
所以,从分类上看,它更像“请求处理模型的替代方案”,而不是对传统 Servlet 注册方式的简单补充。
6. 五种方式怎么选,先看你解决的是哪类问题
把这 5 种方式放在一起,最容易混淆的地方在于:它们不完全处于同一个层面。

如果你需要的是传统 Servlet 组件注册
- 优先考虑
ServletRegistrationBean; - 如果项目偏原生 Servlet 风格,或者是老项目迁移,考虑
@WebServlet+@ServletComponentScan; - 如果要在启动期按条件决定注册内容,考虑
ServletContextInitializer。
如果你需要的是 Spring MVC 层的动态路由能力
- 运行时挂载请求入口,可考虑
RequestMappingHandlerMapping.registerMapping(); - 如果你更倾向函数式模型,尤其要处理流式接口,可考虑
RouterFunction。
简单说,别只看“哪个更高级”,而要先区分:你是在注册 Servlet,还是在扩展 Spring MVC 的请求处理能力。问题分清楚之后,选型会容易很多。


