1. 概述
本文將介紹新的 <em ><a href="https://docs.spring.io/spring-boot/docs/3.1.12/api/org/springframework/boot/web/servlet/ServletComponentScan.html">@ServletComponentScan</a></em> 註解在 Spring Boot 中的使用。
目標是支持以下 Servlet 3.0 註解:
- jakarta.servlet.annotation.WebFilter
- jakarta.servlet.annotation.WebListener
- jakarta.servlet.annotation.WebServlet
<em >@WebServlet</em >,@WebFilter, 和 <em >@WebListener</em > 註解的類可以通過在 <em >@Configuration</em > 類上註解 <em >@ServletComponentScan</em > 並指定包名時,自動註冊到嵌入式 Servlet 容器中。
我們已在《Java Servlet 簡介》和《Java 中攔截器模式簡介》中介紹了基本 <em >@WebServlet</em > 的使用方法。對於 <em >@WebListener</em >,您可以參考本文,它展示了 Web Listeners 的典型用例。
2. Servlet、Filter 和 Listener
在深入瞭解 @ServletComponentScan 之前,讓我們先看看在 @ServletComponentScan 出現之前,這些註解是如何使用的:@WebServlet、@WebFilter 和 @WebListener。
2.1. @WebServlet
現在,我們首先定義一個 Servlet,它處理 GET 請求並返回 “hello”:
@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. <em @WebFilter@
然後是一個過濾器,用於過濾請求,使其針對 ,並將 前綴添加到輸出。
@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 應用程序的基本組件,可以將其打包並部署到 Servlet 容器中。通過將打包好的 WAR 文件部署到 Jetty、 Tomcat 或任何支持 Servlet 3.0 的 Servlet 容器中,可以輕鬆驗證每個組件的行為。
3. 在 Spring Boot 中使用 @ServletComponentScan
你可能會想,既然我們可以使用這些註解在大多數 Servlet 容器中而無需任何配置,那為什麼我們需要 @ServletComponentScan 呢?問題在於嵌入式 Servlet 容器。
由於嵌入式容器不支持 @WebServlet、@WebFilter 和 @WebListener 註解,Spring Boot,大量依賴嵌入式容器,因此引入了新的註解 @ServletComponentScan,以支持使用這三種註解的某些依賴 jar 包。
詳細討論請參考 GitHub 上的 Issue。
3.1. Maven 依賴
為了使用 <em @ServletComponentScan</em>,我們需要使用 Spring Boot 版本 1.3.0 或更高版本。 讓我們將最新版本的 <a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent"><em>spring-boot-starter-parent</em></a> 和 <a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web"><em>spring-boot-starter-web</em></a> 添加到<em pom` 中:
<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. 指定掃描包
默認情況下,<em @ServletComponentScan</em>> 會從標註類的包中進行掃描。要指定掃描哪些包,可以使用其屬性:
- value
- basePackages
- basePackageClasses
默認的 <em>value</em> 屬性是 <em>basePackages</em> 的別名。
假設我們的 <em>SpringBootAnnotatedApp</em> 位於 <em>com.baeldung.annotation</em> 包下,並且我們想要掃描位於上文 Web 應用程序中創建的 <em>com.baeldung.annotation.components</em> 包中的類,以下配置等效:
@ServletComponentScan@ServletComponentScan("com.baeldung.annotation.components")@ServletComponentScan(basePackages = "com.baeldung.annotation.components")@ServletComponentScan(
basePackageClasses =
{AttrListener.class, HelloFilter.class, HelloServlet.class})4. 源碼解析
<em @ServletComponentScan</em>> 註解由 <a href="https://github.com/spring-projects/spring-boot/blob/main/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/context/ServletComponentRegisteringPostProcessor.java"><em>ServletComponentRegisteringPostProcessor</em></a> 處理。 該處理器掃描指定包中帶有 <em @WebFilter</em>>, <em @WebListener</em>> 和 <em @WebServlet</em>> 註解的類,並生成一個 <a href="https://github.com/spring-projects/spring-boot/blob/main/module/spring-boot-web-server/src/main/java/org/springframework/boot/web/server/servlet/context/ServletComponentHandler.java"><em>ServletComponentHandlers</em></a> 列表,用於處理這些註解的屬性,並註冊掃描到的 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。