1. 概述

本文将探讨Spring Boot中的新注解@ServletComponentScan。其目标是支持以下Servlet 3.0注解:

  • jakarta.servlet.annotation.WebFilter
  • jakarta.servlet.annotation.WebListener
  • jakarta.servlet.annotation.WebServlet

我们已经在Java Servlets入门中介绍了@WebServlet的基本用法,在Java拦截器模式入门中讨论了@WebFilter。对于@WebListener,可以参考这篇文章,它展示了一个典型的web监听器用例。

2. Servlet、过滤器和监听器

在深入讨论@ServletComponentScan之前,先来看看在它出现之前,如何使用这些注解:@WebServlet@WebFilter@WebListener

2.1. @WebServlet

首先,定义一个处理GET请求并返回"hello"的Servlet:

@WebServlet("/hello")
public class HelloServlet extends HttpServlet {

    @Override
    public void doGet(HttpServletRequest request, HttpServletResponse response) {
        try {
            response
              .getOutputStream()
              .write("hello");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

2.2. @WebFilter

然后,创建一个过滤器,针对"/hello"请求,并在输出前添加"filtering ":

@WebFilter("/hello")
public class HelloFilter implements Filter {

    //...
    @Override
    public void doFilter(
      ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) 
      throws IOException, ServletException {
        servletResponse
          .getOutputStream()
          .print("filtering ");
        filterChain.doFilter(servletRequest, servletResponse);
    }
    //...

}

2.3. @WebListener

最后,定义一个监听器,在ServletContext中设置自定义属性:

@WebListener
public class AttrListener implements ServletContextListener {

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        servletContextEvent
          .getServletContext()
          .setAttribute("servlet-context-attr", "test");
    }
    //...
}

2.4. 部署到Servlet容器

现在我们构建了一个简单web应用的基础组件,可以将打包后的war文件部署到诸如Jetty、Tomcat等支持Servlet 3.0的Servlet容器中,以验证每个组件的行为。

3. 在Spring Boot中使用@ServletComponentScan

你可能会问,既然大多数Servlet容器无需配置即可使用这些注解,为什么还需要@ServletComponentScan?问题出在嵌入式Servlet容器上。

由于嵌入式容器不支持@WebServlet@WebFilter@WebListener注解,依赖于嵌入式容器的Spring Boot引入了这个新的@ServletComponentScan注解,以支持使用这三种注解的一些依赖库。

详细讨论可在GitHub上的相关issue中找到。

3.1. Maven依赖

要使用@ServletComponentScan,需要Spring Boot 1.3.0或更高版本。在pom.xml中添加最新的spring-boot-starter-parentspring-boot-starter-web

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.0</version>
    <relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.4.0</version>
    </dependency>
</dependencies>

3.2. 使用@ServletComponentScan

Spring Boot应用程序非常简单。只需在类上添加@ServletComponentScan,即可启用对@WebFilter@WebListener@WebServlet的扫描:

@ServletComponentScan
@SpringBootApplication
public class SpringBootAnnotatedApp {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootAnnotatedApp.class, args);
    }

}

无需修改之前的web应用,它就能正常工作:

@Autowired private TestRestTemplate restTemplate;

@Test
public void givenServletFilter_whenGetHello_thenRequestFiltered() {
 
    ResponseEntity<String> responseEntity = 
      restTemplate.getForEntity("/hello", String.class);
 
    assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
    assertEquals("filtering hello", responseEntity.getBody());
}
@Autowired private ServletContext servletContext;

@Test
public void givenServletContext_whenAccessAttrs_thenFoundAttrsPutInServletListner() {
 
    assertNotNull(servletContext);
    assertNotNull(servletContext.getAttribute("servlet-context-attr"));
    assertEquals("test", servletContext.getAttribute("servlet-context-attr"));
}

3.3. 指定扫描包

默认情况下,@ServletComponentScan会从注解类所在的包开始扫描。要指定扫描哪些包,可以使用它的属性:

  • value
  • basePackages
  • basePackageClasses

value属性是basePackages的别名。

假设我们的SpringBootAnnotatedApp位于com.baeldung.annotation包下,而我们希望扫描上述web应用中创建在com.baeldung.annotation.components包下的类,以下配置等效:

@ServletComponentScan
@ServletComponentScan("com.baeldung.annotation.components")
@ServletComponentScan(basePackages = "com.baeldung.annotation.components")
@ServletComponentScan(
  basePackageClasses = 
    {AttrListener.class, HelloFilter.class, HelloServlet.class})

4. 内部实现

@ServletComponentScan注解由ServletComponentRegisteringPostProcessorGitHub代码)处理。扫描指定包中的@WebFilter@WebListener@WebServlet注解后,会生成一个ServletComponentHandler列表,处理注解属性并将扫描到的bean注册:

class ServletComponentRegisteringPostProcessor
  implements BeanFactoryPostProcessor, ApplicationContextAware {
  
    private static final List<ServletComponentHandler> HANDLERS;

    static {
        List<ServletComponentHandler> handlers = new ArrayList<>();
        handlers.add(new WebServletHandler());
        handlers.add(new WebFilterHandler());
        handlers.add(new WebListenerHandler());
        HANDLERS = Collections.unmodifiableList(handlers);
    }
    
    //...
    
    private void scanPackage(
      ClassPathScanningCandidateComponentProvider componentProvider, 
      String packageToScan){
        //...
        for (ServletComponentHandler handler : HANDLERS) {
            handler.handle(((ScannedGenericBeanDefinition) candidate),
              (BeanDefinitionRegistry) this.applicationContext);
        }
    }
}

正如官方Javadoc所说,@ServletComponentScan注解仅在嵌入式Servlet容器中有效,这是Spring Boot默认提供的。

5. 总结

本文介绍了@ServletComponentScan及其如何用于支持依赖于@WebServlet@WebFilter@WebListener的任何应用。

示例的实现和代码可以在GitHub项目中找到。