1、概览 {#1概览}
在本教程中,我们将学习 Spring Boot 3(Spring 6)在 URL 匹配方面引入的变化。
Spring Boot 使用 DispatcherServlet
处理 URL 映射,它会根据 URL 将请求转发到相应的 controller。DispatcherServlet
使用一组称为映射(mapping)的规则来确定使用哪个 controller 来处理请求。
2、Spring MVC 和 Webflux URL 匹配的更改 {#2spring-mvc-和-webflux-url-匹配的更改}
Spring Boot 3 对"尾斜线匹配"配置选项进行了重大修改。该选项决定是否将带尾斜线的 URL 与不带尾斜线的 URL 作相同处理。以前版本的 Spring Boot 默认将此选项设置为 true
。这意味着 controller 默认会同时匹配 GET /some/greeting
和 GET /some/greeting/
:
@RestController
public class GreetingsController {
@GetMapping("/some/greeting")
public String greeting {
return "Hello";
}
}
如上,如果我们尝试访问带有尾斜线的 URL,就会收到 404 错误。
让我们来探讨一下如何适应这种变化。
3、添加额外的路由 {#3添加额外的路由}
要处理这种问题,可以额外添加一个专门处理带尾斜线的路由:
@RestController
public class GreetingsController {
@GetMapping("/some/greeting")
public String greeting {
return "Hello";
}
@GetMapping("/some/greeting/")
public String greeting {
return "Hello";
}
}
下面是一个使用 Webflux 的响应式 @RestController
:
@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");
}
}
4、覆写默认配置 {#4覆写默认配置}
我们可以通过复写 Spring MVC 的 WebMvcConfigurer:configurePathMatch
方法来实现:
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true);
}
}
如果我们使用 Webflux,配置更改与此类似:
@Configuration
class WebConfiguration implements WebFluxConfigurer {
@Override
public void configurePathMatching(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true);
}
}
5、自定义 Filter 修改 URL {#5自定义-filter-修改-url}
首先,创建一个实现 javax.servlet.Filter
接口的 Filter:
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 中实现了 Filter
接口,并覆写了 doFilter
方法。首先,我们将 ServletRequest
转换为 HttpServletRequest
,以访问请求 URI。然后,我们检查 URI 是否以斜线结尾。如果有,我们就使用新的 CustomHttpServletRequestWrapper
(一个继承 HttpServletRequestWrapper
的私有静态类)删除尾斜线。该类覆写了 getRequestURI
和 getRequestURL
方法,以返回修改后的 URI 和 URL。
最后,要将自定义 filter 应用到所有端点,我们可以使用 URL pattern 为 "/*"
的 FilterRegistrationBean
进行注册。如下::
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;
}
}
注意,将 filter 应用到所有端点可能会影响性能,而且如果我们的自定义端点不遵循标准 RESTful URL 模式,还可能导致意外行为。
最后,进行测试验证 filter 是否生效:
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
修改 URL {#6自定义-webfilter-修改-url}
对于响应式端点,我们可以创建一个实现 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
参数中提取 request。我们使用 getPath
方法获取传入请求的路径,并检查其是否以斜线结尾。如果有,我们就删除尾部斜线,并使用 mutate
方法创建一个新的 ServerHttpRequest
。然后,我们将修改后的 exchange 对象传递给 WebFilterChain
参数上的 filter
方法。如果路径不是以斜线结尾,我们就用原始 exchange 对象调用 filter
方法。
要注册 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、通过代理配置重定向 {#7通过代理配置重定向}
将以斜线结尾的 URL 请求重定向到不带斜线的 URL 是配置 web 服务器时的一项常见需求。这有助于确保网站上的所有 URL 具有一致的结构,并提高搜索引擎优化(SEO)效果。
大多数 web 服务器都内置了对 URL 重定向的支持。
接下来,让我们学习如何在两个常用的代理服务器,Apache 和 Nginx 中配置重定向。
7.1、Nginx {#71nginx}
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;
}
在本例中,我们在根 location 块中添加了 if
块。if
块会检查请求 URI 是否以斜线结尾。如果 URI 以斜线结尾,就会使用 301
重定向将请求重定向到没有尾斜线的同一 URI。$request_uri
是一个预定义的 Nginx 变量,包含从客户端接收到的原始请求 URI,包括查询字符串(如果有)。正则表达式 "^(.+)/$"
有一个捕获组 "(.+)"
,可捕获尾斜线的任何字符。返回指令中的 $1
指的是第一个捕获组,即不含尾斜线的匹配 URI。
然后,我们使用 proxy_pass
指令指定处理请求的后端服务器的 URL,并使用 proxy_set_header
指令设置必要的请求头。注意,需要将 URL 替换为后端服务器的实际 URL。
7.2、Apache {#72apache}
RewriteEngine On
RewriteRule ^(.+)/$ $1 [L,R=301]
ProxyPass / http://localhost:8080/
ProxyPassReverse / http://localhost:8080/
在本例中,我们使用了一个 RewriteRule
,其中包含了我们在 Nginx 配置中使用的正则表达式。执行 RewriteRule
时,它会将带斜线的匹配 URL 替换为第一组捕获的值(不带斜线的 URL),并执行 301 重定向。
然后,我们使用 ProxyPass
和 ProxyPassReverse
指令指定处理请求的后端服务器的 URL。我们可以在 <VirtualHost>
块中添加此配置,将其应用于整个网站。
8、总结 {#8总结}
在本文中,我们讨论了 Spring Boot 3 弃用"尾斜线匹配"配置选项的问题,这对框架中的 URL 映射产生了重大影响,虽然需要付出一些努力,但却为应用程序提供了稳定一致的基础。通过了解这一变化并相应地更新我们的应用程序,我们可以确保无缝和一致的用户体验。
参考:https://www.baeldung.com/spring-boot-3-url-matching