知識庫 / Spring / Spring Boot RSS 訂閱

Spring Boot 中 URL 匹配

Spring Boot
HongKong
7
11:41 AM · Dec 06 ,2025

1. 概述

在本教程中,我們將探討 Spring Boot 3 (Spring 6) 中 URL 匹配的變更。 Spring 6.0 棄用了透明的尾部斜槓支持,轉而要求通過代理、Servlet 或控制器配置顯式的重定向。

URL 匹配是 Spring Boot 中一項強大的特性,它允許開發人員將特定的 URL 映射到 Web 應用程序中的控制器和操作。 該特性簡化了應用程序的組織和導航,從而提升了用户體驗。

為了處理 URL 映射,Spring Boot 使用一個強大的機制,稱為 DispatcherServlet,它作為應用程序的前控制器。 Servlet 根據 URL 將請求轉發到適當的控制器。 DispatcherServlet 使用一組規則,稱為映射,以確定給定請求應由哪個控制器處理。

我們可以先了解一下 Spring Boot 2 (Spring 5) 中舊版本的 URL 匹配的更多細節。

2. Spring MVC 和 Webflux URL 匹配更改

Spring Boot 3 顯著更改了尾部斜槓匹配配置選項。此選項確定是否將帶有尾部斜槓的 URL 與不帶尾部斜槓的 URL 視為相同。以前版本的 Spring Boot 將此選項默認為 true,這意味着一個控制器將默認匹配 “GET /some/greeting” 和 “GET /some/greeting/”。

@RestController
public class GreetingsController {

    @GetMapping("/some/greeting")
    public String greeting {
        return "Hello";
    } 

}

因此,如果嘗試訪問帶尾部斜槓的 URL,除非控制器明確配置為處理帶尾部斜槓的 URL,否則將收到 404 錯誤。這可能導致混淆和失效鏈接,如果未正確處理。

讓我們探討一下適應此更改的一些選項。

3. 配置 UrlHandlerFilter 以實現尾部斜槓匹配

Spring Framework 6.2 引入了新的 UrlHandlerFilter,以幫助重定向或重寫帶有尾部斜槓的傳入 URL。例如,它會將請求 “/some/greeting/” 重定向到 “/some/greeting”UrlHandlerFilter 可以使用兩種方法進行實例化。

第一種方法是在請求 “/greetings/some-greeting/” 時,能夠返回 HTTP 308 重定向狀態碼,並將瀏覽器發送到不帶尾部斜槓的 URL “/greetings/some-greeting”

UrlHandlerFilter urlHandlerFilter = 
  UrlHandlerFilter.trailingSlashHandler("/greetings/**").
  redirect(HttpStatus.PERMANENT_REDIRECT).
  build();

第二種方法是,當我們請求 “/greetings/some-greeting/” 時,它能夠將請求包裝起來,使其看起來像是發送到了 “/greetings/some-greeting”,並處理該請求。

UrlHandlerFilter urlHandlerFilter = 
  UrlHandlerFilter.trailingSlashHandler("/greetings/**").
  wrapRequest()
  .build();

UrlHandlerFilter 可與 Spring MVC Servlet API 和 Spring WebFlux Reactive API 配合使用。通過添加一個 Filter,我們使用 Spring Bean,並在配置中提供@Bean定義:

@Bean
    public FilterRegistrationBean urlHandlerFilterRegistrationBean() {
        FilterRegistrationBean<OncePerRequestFilter> registrationBean = new FilterRegistrationBean<>();
        UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
          .trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
	  .trailingSlashHandler("/greetings/**").wrapRequest()
          .build();
        registrationBean.setFilter(urlHandlerFilter);

        return registrationBean;
    }

我們配置了兩個請求路徑,在示例 Filter bean 中。 Servlet 容器會自動註冊 Filter bean。

4. 額外路由

為了確保應用程序正確處理 URL,並適應這一變更,我們需要更新現有應用程序。 這可以通過為每個處理帶有尾部斜槓的 URL 的控制器添加特定的 URL 映射來實現。

例如,在處理 URL “/some/greeting” 的先前控制器中,我們需要添加一個單獨的映射 “/some/greeting/”。 這將確保即使用户在 URL 中包含尾部斜槓,也能訪問所需頁面。

@RestController
public class GreetingsController {

    @GetMapping("/some/greeting")
    public String greeting {
        return "Hello";
    } 

    @GetMapping("/some/greeting/")
    public String greeting {
        return "Hello";
    } 

}

這是一個反應式 @RestController,使用 Webflux:

@RestController
public class GreetingsControllerReactive {

    @GetMapping("/some/reactive/greeting")
    public Mono<String> greeting() {
        return Mono.just("Hello reactive");
    }

    @GetMapping("/some/reactive/greeting/")
    public Mono<String> greetingTrailingSlash() {
        return Mono.just("Hello with slash reactive");
    }
}

5. 使用自定義過濾器配置重定向

當請求到達時,Spring Boot 過濾器鏈會根據請求和已註冊的過濾器確定要應用哪些過濾器。如果請求被傳統阻塞 I/O 端點(例如,<em @RestController</em><em @Controller</em>)接收,則過濾器鏈將應用實現 <em Filter</em> 接口的任何過濾器。這些過濾器會阻塞 I/O 並可能與 Servlet API 交互以讀取或修改請求或響應。

要使用自定義過濾器配置 URL 重定向以處理阻塞請求,可以按照以下步驟操作:

首先,我們需要創建一個實現 <em jakarta.servlet.Filter</em> 接口的新類:

public class TrailingSlashRedirectFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        String path = httpRequest.getRequestURI();

        if (path.endsWith("/")) {
            String newPath = path.substring(0, path.length() - 1);
            HttpServletRequest newRequest = new CustomHttpServletRequestWrapper(httpRequest, newPath);
            chain.doFilter(newRequest, response);
        } else {
            chain.doFilter(request, response);
        }
    }

    private static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {

        private final String newPath;

        public CustomHttpServletRequestWrapper(HttpServletRequest request, String newPath) {
            super(request);
            this.newPath = newPath;
        }

        @Override
        public String getRequestURI() {
            return newPath;
        }

        @Override
        public StringBuffer getRequestURL() {
            StringBuffer url = new StringBuffer();
            url.append(getScheme()).append("://").append(getServerName()).append(":").append(getServerPort())
              .append(newPath);
            return url;
        }
    }
}

我們在此自定義過濾器中實現了Filter接口,並覆蓋了doFilter方法。首先,我們把ServletRequest轉換為HttpServletRequest以訪問請求 URI。然後,我們檢查 URI 是否以斜槓結尾。如果以斜槓結尾,我們使用一個新的CustomHttpServletRequestWrapper(一個私有的靜態類,繼承自HttpServletRequestWrapper)來移除尾部斜槓。該類覆蓋了getRequestURIgetRequestURL方法,以返回修改後的 URI 和 URL。

最後,為了將自定義過濾器應用於所有端點,我們可以使用一個FilterRegistrationBean,其 URL 模式為“/*”進行註冊。以下是一個示例:

@Configuration
public class WebConfig {

    @Bean
    public Filter trailingSlashRedirectFilter() {
        return new TrailingSlashRedirectFilter();
    }

    @Bean
    public FilterRegistrationBean<Filter> trailingSlashFilter() {
        FilterRegistrationBean<Filter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(trailingSlashRedirectFilter());
        registrationBean.addUrlPatterns("/*");
        return registrationBean;
    }
}

請注意,將過濾器應用於所有端點可能會產生性能影響,並且如果我們的自定義端點不遵循標準 RESTful URL 模式,則可能導致意外行為。通常,僅將過濾器應用於需要它們的情況下的端點是推薦的做法。

最後,讓我們看看在過濾器已啓用的情況下的一些測試:

private static final String BASEURL = "/some";

@Autowired
MockMvc mvc;

@Test
public void testGreeting() throws Exception {
    mvc.perform(get(BASEURL + "/greeting").accept(MediaType.APPLICATION_JSON_VALUE))
      .andExpect(status().isOk())
      .andExpect(content().string("Hello"));
}

@Test
public void testGreetingTrailingSlashWithFilter() throws Exception {
    mvc.perform(get(BASEURL + "/greeting/").accept(MediaType.APPLICATION_JSON_VALUE))
      .andExpect(status().isOk())
      .andExpect(content().string("Hello"));
}

6. 使用自定義 WebFilter 進行重定向

對於反應式端點,我們可以創建一個自定義類,該類實現 WebFilter 接口並覆蓋其 filter 方法:

public class TrailingSlashRedirectFilterReactive implements WebFilter {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String path = request.getPath().value();

        if (path.endsWith("/")) {
            String newPath = path.substring(0, path.length() - 1);
            ServerHttpRequest newRequest = request.mutate().path(newPath).build();
            return chain.filter(exchange.mutate().request(newRequest).build());
        }

        return chain.filter(exchange);
    }
}

首先,我們從 ServerWebExchange 參數中提取請求。我們使用 getPath 方法獲取傳入請求的路徑,並檢查它是否以斜槓結尾。如果以斜槓結尾,我們移除尾部斜槓並使用 mutate 方法創建新的 ServerHttpRequest。然後,我們將修改後的 exchange 對象傳遞給 WebFilterChain 參數中的 filter 方法。如果路徑沒有以斜槓結尾,則我們調用 filter 方法時傳遞原始的 exchange 對象。

為了註冊 WebFilter,我們使用 @Component 註解,Spring Boot 將自動將其註冊到適當的 WebFilterChain 中。

要指定 TrailingSlashRedirectFilterReactive 應該應用的路徑,我們可以使用 @WebFilter 註解並設置 urlPatterns 屬性為一個 URL 模式列表。

最後,讓我們看看在過濾器生效的情況下的一些測試:

private static final String BASEURL = "/some/reactive";

@Autowired
private WebTestClient webClient;

@Test
public void testGreeting() {
    webClient.get().uri( BASEURL + "/greeting")
      .exchange()
      .expectStatus().isOk()
      .expectBody().consumeWith(result -> {
          String responseBody = new String(result.getResponseBody());
          assertTrue(responseBody.contains("Hello reactive"));
      });
}
   
@Test
public void testGreetingTrailingSlashWithFilter() {
    webClient.get().uri(BASEURL +  "/greeting/")
      .exchange()
      .expectStatus().isOk()
      .expectBody().consumeWith(result -> {
          String responseBody = new String(result.getResponseBody());
          assertTrue(responseBody.contains("Hello reactive"));
      });
}

7. 通過代理配置重定向

將以尾部斜槓結尾的 URL 請求重定向為不帶尾部斜槓的 URL 是一種常見的任務,在配置 Web 服務器時經常需要執行。 這有助於確保網站上的所有 URL 具有一致的結構,並提高搜索引擎優化 (SEO)。

此外,大多數 Web 服務器都內置了 URL 重定向支持。 可以利用此支持將帶有尾部斜槓的請求重定向到不帶尾部斜槓的 URL。

讓我們探討如何使用代理配置 Apache 和 Nginx 兩種流行的 Web 服務器的重定向。

7.1. Nginx

location / {
    if ($request_uri ~ ^(.+)/$) {
        return 301 $1;
    }
    
    proxy_pass http://localhost:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

在此示例中,我們向根位置塊添加了 if 塊——此外,if 塊檢查請求 URI 是否以尾部斜槓結尾。如果 URI 以尾部斜槓結尾,則使用 301 重定向將請求重定向到不帶尾部斜槓的相同 URI。$request_uri 是一個預定義的 Nginx 變量,它包含客户端接收到的原始請求 URI,包括查詢字符串(如果有)。正則表達式 “^(.+)/$” 包含一個捕獲組 “(.+)”,該組捕獲任何字符序列後跟一個尾部斜槓。$1return 指令中引用第一個捕獲組,即不帶尾部斜槓的匹配 URI。

然後,我們使用 proxy_pass 指令指定後端服務器的 URL,用於處理請求,並使用 proxy_set_header 指令設置必要的標頭。請注意,我們需要用實際的後端服務器 URL 替換 URL。

7.2. Apache

Apache 是一個流行的開源 HTTP 服務器軟件,以及一個用於 Web 開發的開源項目。它以其穩定性和靈活性而聞名,被廣泛應用於各種 Web 應用程序和網站。

主要特性:

  • 高性能: Apache 能夠處理大量的併發請求,提供快速的響應速度。
  • 模塊化: Apache 具有強大的模塊化架構,允許用户根據需要添加各種功能模塊,例如身份驗證、安全、緩存等。
  • 可配置性: Apache 提供了豐富的配置選項,允許用户自定義服務器的行為,以滿足特定的需求。
  • 廣泛的支持: Apache 擁有龐大的用户社區和活躍的開發團隊,提供廣泛的支持和文檔。

常用模塊:

  • mod_rewrite:用於 URL 重寫,可以簡化 URL 結構,提高 SEO 效果。
  • mod_proxy:用於代理請求,可以實現負載均衡、反向代理等功能。
  • mod_ssl:用於啓用 SSL/TLS 加密,保護網站和用户數據的安全。

示例代碼:

<?php
// 這是一個簡單的 PHP 代碼示例
echo "Hello, World!";
?>
RewriteEngine On
RewriteRule ^(.+)/$ $1 [L,R=301]

ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/

在此示例中,我們使用了一個 RewriteRule,該規則使用了我們在 Nginx 配置中使用的正則表達式。當 RewriteRule 執行時,它會將匹配的 URL 替換為帶有第一個捕獲組(URL 中不帶尾部斜槓的值)的尾部斜槓,並執行 301 重定向。

然後,我們使用 ProxyPassProxyPassReverse 指令來指定處理請求的後端服務器的 URL。可以將此配置添加到 <VirtualHost> 塊中,從而將其應用於整個站點。

8. 結論

在本文中,我們討論了 Spring Boot 3 中廢棄的尾部斜槓匹配配置選項,該選項對框架中的 URL 映射產生重大影響,需要一定的調整,但為應用程序提供了穩定且一致的基礎。通過理解這一變更並相應地更新我們的應用程序,我們可以確保用户體驗的無縫和一致。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.