位置:首页 > Java > Spring Boot 注册 Servlet 的五种方法,怎么选更合适?

Spring Boot 注册 Servlet 的五种方法,怎么选更合适?

时间:2026-08-22  |  作者:宇宙开黑者  |  阅读:0

目录

  1. 用 RegistrationBean 注册:Spring Boot 原生、最常见
  2. 用 @WebServlet 等注解注册:适合从传统 Servlet 项目迁移
  3. 用 ServletContextInitializer 动态注册:适合启动期按条件装配
  4. 用 RequestMappingHandlerMapping.registerMapping():本质上是动态注册请求映射
  5. 用 RouterFunction 注册:函数式路由更适合流式接口
  6. 五种方式怎么选,先看你解决的是哪类问题

前言

在 Spring Boot 里,所谓“注册 Servlet”常常被混用成好几类能力:既有传统 Servlet 组件注册,也有 Spring MVC 层的动态路由装配。本文按使用场景把 5 种常见方案拆开说明,保留关键代码和注意点,帮助你判断什么时候该选 RegistrationBean,什么时候该转向动态映射或函数式路由。

在 Spring Boot 项目里,很多人第一次接触 Servlet 注册时,往往只知道 ServletRegistrationBean,但实际可选方案远不止这一种。不同做法背后对应的是不同的编程模型:有的偏 Spring Boot 配置化,有的沿用 Servlet 规范注解,有的适合启动期动态装配,还有的其实已经转向 Spring MVC 的动态映射或函数式路由。

这篇文章把常见的 5 种方式按“什么时候用、代码怎么写、边界在哪里”重新梳理一遍。读完后,你可以更快判断:当前需求究竟是在注册传统 Servlet,还是已经进入更适合用 MVC 路由或函数式接口处理的场景。

1. 用 RegistrationBean 注册:Spring Boot 原生、最常见

Spring Boot 提供了 ServletRegistrationBeanFilterRegistrationBeanServletListenerRegistrationBean 三个类,分别用来注册 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

ServletContextInitializer 在容器启动阶段向 ServletContext 动态注册 Servlet 的流程图
启动期动态注册流程这一节的重点不是注解,而是启动阶段如何通过 ServletContext 主动完成注册。
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 不同,走的是函数式路由模型。

RouterFunction 处理 SSE 流式响应的路由与响应链路示意图
RouterFunction 与 SSE 响函数式路由这一节最适合用流程图说明:请求进入、校验 Accept 头、绑定处理函数,再持续输出。
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/streamPOST 请求直接绑定到 handlePost,并返回 SSE 流式响应。相比传统 Controller,这种写法在以下场景更有优势:

  • 函数式接口编排更清晰;
  • 流式响应,例如 SSE,更容易集中处理;
  • 希望弱化注解式 Controller,直接用路由对象组织入口。

所以,从分类上看,它更像“请求处理模型的替代方案”,而不是对传统 Servlet 注册方式的简单补充。

6. 五种方式怎么选,先看你解决的是哪类问题

把这 5 种方式放在一起,最容易混淆的地方在于:它们不完全处于同一个层面。

对比 Spring Boot 中五种注册方式的定位、动态能力与适用层次
五种注册方式定位对比把五种方案放到同一张图里,可以快速看清哪些属于传统 Servlet 注册,哪些已经转向。

如果你需要的是传统 Servlet 组件注册

  • 优先考虑 ServletRegistrationBean
  • 如果项目偏原生 Servlet 风格,或者是老项目迁移,考虑 @WebServlet + @ServletComponentScan
  • 如果要在启动期按条件决定注册内容,考虑 ServletContextInitializer

如果你需要的是 Spring MVC 层的动态路由能力

  • 运行时挂载请求入口,可考虑 RequestMappingHandlerMapping.registerMapping()
  • 如果你更倾向函数式模型,尤其要处理流式接口,可考虑 RouterFunction

简单说,别只看“哪个更高级”,而要先区分:你是在注册 Servlet,还是在扩展 Spring MVC 的请求处理能力。问题分清楚之后,选型会容易很多。

免责声明:文中图文均来自网络,如有侵权请联系删除,心愿游戏发布此文仅为传递信息,不代表心愿游戏认同其观点或证实其描述。

相关文章

更多

精选合集

更多

大家都在玩

热门话题

大家都在看

更多