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

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 的编程模型。

本文转载于:https://www.jb51.net/program/368464cy8.htm 如有侵犯,请联系zhengruancom@outlook.com删除。
免责声明:正软商城发布此文仅为传递信息,不代表正软商城认同其观点或证实其描述。