在 Spring Boot 里注册 Servlet,其实有不止一种做法,而且每种方式适合的场景也不太一样:有的更偏配置化,有的更适合注解扫描,还有的更强调运行时动态注册。下面把常见的五种方式完整梳理一遍,方便直接对照使用。

一、Spring Boot 提供了 ServletRegistrationBean、FilterRegistrationBean、ServletListenerRegistrationBean 三个类,分别用于注册 Servlet、Filter、Listener。
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 原生支持,配置集中,也比较符合日常 Spring 项目的使用习惯。需要注册 Servlet 时,直接声明一个 ServletRegistrationBean 就可以了;如果是 Filter 或 Listener,也有对应的 RegistrationBean 可用,整体思路非常统一。
二、Servlet 3.0 之前,Servlet、Filter、Listener 这类组件通常都要在 web.xml 里完成配置。到了 3.0 之后,就不再强依赖 web.xml 了,完全可以通过代码配置,或者使用 @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 规范,组件定义和初始化参数也都能直接放在注解里,阅读起来比较直观。
三、实现 org.springframework.boot.web.servlet.ServletContextInitializer,通过向容器中注入 bean 的方式进行动态注册。
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");
}
}
这一类做法更灵活一些。它不是单纯靠注解扫描,而是在容器启动阶段主动往 ServletContext 里塞注册信息。换句话说,如果需要在启动过程中按条件决定是否注册、注册到哪个路径、带哪些初始化参数,这种方式会更顺手。
四、使用 org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.registerMapping() 方法,在运行时动态注册。
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));
}
这一招严格来说更像是“动态注册请求映射”,重点已经不只是传统意义上的 Servlet 注册了,而是直接把路径和处理方法在运行时挂到 Spring MVC 的映射体系里。它的优势就在“动态”二字上:路径、方法、处理器都可以在程序运行过程中完成装配,适合一些扩展性要求比较高的场景。
五、使用 org.springframework.web.servlet.function.RouterFunction 进行注册。
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();
}
}
这种方式走的是函数式路由的思路,和传统注解式 Controller 不太一样。尤其是在处理流式响应、函数式接口编排这类需求时,会显得更加灵活。上面这个例子里,核心就是通过 RouterFunction 把 /test/stream 这个 POST 路由直接绑定到 handlePost 方法上,并返回 SSE 流式响应。
总结:
Spring Boot 注册 Servlet 的方式,常见的就有这五类:RegistrationBean、Servlet 注解、ServletContextInitializer、RequestMappingHandlerMapping 动态映射,以及 RouterFunction 函数式注册。到底选哪一种,不是看“哪种最高级”,而是看项目当前的技术栈、动态化需求,以及是否需要更贴近原生 Servlet 或 Spring MVC 的编程模型。