From 7a55ce48a93f004c7acc0b51c04bb39be2c6b733 Mon Sep 17 00:00:00 2001 From: giampaolo Date: Sat, 5 Apr 2025 14:19:06 +0200 Subject: [PATCH 1/5] Handle CancellationException in JdkClientHttpRequest Handle CancellationException in order to throw an HttpTimeoutException when the timeout handler caused the cancellation. See gh-34721 Signed-off-by: giampaolo fix: use timeoutHandler with a flag isTimeout Closes gh-33973 Signed-off-by: giampaolo --- .../http/client/JdkClientHttpRequest.java | 21 +++- .../http/client/JdkClientHttpRequestTest.java | 117 ++++++++++++++++++ 2 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java diff --git a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java index e2955266ab3..9f5f7740ecd 100644 --- a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java @@ -37,6 +37,7 @@ import java.util.concurrent.ExecutionException; import java.util.concurrent.Executor; import java.util.concurrent.Flow; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; @@ -97,12 +98,13 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { @SuppressWarnings("NullAway") protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException { CompletableFuture> responseFuture = null; + TimeoutHandler timeoutHandler = null; try { HttpRequest request = buildRequest(headers, body); responseFuture = this.httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofInputStream()); if (this.timeout != null) { - TimeoutHandler timeoutHandler = new TimeoutHandler(responseFuture, this.timeout); + timeoutHandler = new TimeoutHandler(responseFuture, this.timeout); HttpResponse response = responseFuture.get(); InputStream inputStream = timeoutHandler.wrapInputStream(response); return new JdkClientHttpResponse(response, inputStream); @@ -121,7 +123,10 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { Throwable cause = ex.getCause(); if (cause instanceof CancellationException) { - throw new HttpTimeoutException("Request timed out"); + if (timeoutHandler != null && timeoutHandler.isTimeout()) { + throw new HttpTimeoutException("Request timed out"); + } + throw new IOException("Request was cancelled"); } if (cause instanceof UncheckedIOException uioEx) { throw uioEx.getCause(); @@ -136,6 +141,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { throw new IOException(cause.getMessage(), cause); } } + catch (CancellationException ex) { + if (timeoutHandler != null && timeoutHandler.isTimeout()) { + throw new HttpTimeoutException("Request timed out"); + } + throw new IOException("Request was cancelled"); + } } private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) { @@ -233,6 +244,7 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { private static final class TimeoutHandler { private final CompletableFuture timeoutFuture; + private final AtomicBoolean isTimeout = new AtomicBoolean(false); private TimeoutHandler(CompletableFuture> future, Duration timeout) { @@ -241,6 +253,7 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { this.timeoutFuture.thenRun(() -> { if (future.cancel(true) || future.isCompletedExceptionally() || !future.isDone()) { + isTimeout.set(true); return; } try { @@ -268,6 +281,10 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { } }; } + + public boolean isTimeout() { + return isTimeout.get(); + } } } diff --git a/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java b/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java new file mode 100644 index 00000000000..86630e1fd37 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java @@ -0,0 +1,117 @@ +package org.springframework.http.client; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; +import java.time.Duration; +import java.util.concurrent.*; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +class JdkClientHttpRequestTest { + + private HttpClient mockHttpClient; + private URI uri = URI.create("http://example.com"); + private HttpMethod method = HttpMethod.GET; + + private ExecutorService executor; + + @BeforeEach + void setup() { + mockHttpClient = mock(HttpClient.class); + executor = Executors.newSingleThreadExecutor(); + } + + @AfterEach + void tearDown() { + executor.shutdownNow(); + } + + @Test + void executeInternal_withTimeout_shouldThrowHttpTimeoutException() throws Exception { + Duration timeout = Duration.ofMillis(10); + + JdkClientHttpRequest request = new JdkClientHttpRequest(mockHttpClient, uri, method, executor, timeout); + + CompletableFuture> future = new CompletableFuture<>(); + + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(future); + + HttpHeaders headers = new HttpHeaders(); + + CountDownLatch startLatch = new CountDownLatch(1); + + // Cancellation thread waits for startLatch, then cancels the future after a delay + Thread canceller = new Thread(() -> { + try { + startLatch.await(); + Thread.sleep(500); + future.cancel(true); + } catch (InterruptedException ignored) { + } + }); + canceller.start(); + + IOException ex = assertThrows(IOException.class, () -> { + startLatch.countDown(); + request.executeInternal(headers, null); + }); + + assertThat(ex) + .isInstanceOf(HttpTimeoutException.class) + .hasMessage("Request timed out"); + + canceller.join(); + } + + @Test + void executeInternal_withTimeout_shouldThrowIOException() throws Exception { + Duration timeout = Duration.ofMillis(500); + + JdkClientHttpRequest request = new JdkClientHttpRequest(mockHttpClient, uri, method, executor, timeout); + + CompletableFuture> future = new CompletableFuture<>(); + + when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) + .thenReturn(future); + + HttpHeaders headers = new HttpHeaders(); + + CountDownLatch startLatch = new CountDownLatch(1); + + Thread canceller = new Thread(() -> { + try { + startLatch.await(); + Thread.sleep(10); + future.cancel(true); + } catch (InterruptedException ignored) { + } + }); + canceller.start(); + + IOException ex = assertThrows(IOException.class, () -> { + startLatch.countDown(); + request.executeInternal(headers, null); + }); + + assertThat(ex) + .isInstanceOf(IOException.class) + .hasMessage("Request was cancelled"); + + canceller.join(); + } + +} From 600d6c6fc0c345d2226aa16c3329ffb077fc9189 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 8 Aug 2025 11:30:57 +0100 Subject: [PATCH 2/5] Update contribution Closes gh-34721 --- .../http/client/JdkClientHttpRequest.java | 26 ++-- .../http/client/JdkClientHttpRequestTest.java | 117 ------------------ .../client/JdkClientHttpRequestTests.java | 87 +++++++++++++ 3 files changed, 101 insertions(+), 129 deletions(-) delete mode 100644 spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java create mode 100644 spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java diff --git a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java index 9f5f7740ecd..0d14667f83a 100644 --- a/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java @@ -122,11 +122,11 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { catch (ExecutionException ex) { Throwable cause = ex.getCause(); - if (cause instanceof CancellationException) { - if (timeoutHandler != null && timeoutHandler.isTimeout()) { - throw new HttpTimeoutException("Request timed out"); + if (cause instanceof CancellationException ce) { + if (timeoutHandler != null) { + timeoutHandler.handleCancellationException(ce); } - throw new IOException("Request was cancelled"); + throw new IOException("Request cancelled", cause); } if (cause instanceof UncheckedIOException uioEx) { throw uioEx.getCause(); @@ -142,10 +142,10 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { } } catch (CancellationException ex) { - if (timeoutHandler != null && timeoutHandler.isTimeout()) { - throw new HttpTimeoutException("Request timed out"); + if (timeoutHandler != null) { + timeoutHandler.handleCancellationException(ex); } - throw new IOException("Request was cancelled"); + throw new IOException("Request cancelled", ex); } } @@ -244,7 +244,8 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { private static final class TimeoutHandler { private final CompletableFuture timeoutFuture; - private final AtomicBoolean isTimeout = new AtomicBoolean(false); + + private final AtomicBoolean timeout = new AtomicBoolean(false); private TimeoutHandler(CompletableFuture> future, Duration timeout) { @@ -252,8 +253,8 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { .completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS); this.timeoutFuture.thenRun(() -> { + this.timeout.set(true); if (future.cancel(true) || future.isCompletedExceptionally() || !future.isDone()) { - isTimeout.set(true); return; } try { @@ -263,7 +264,6 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { // ignore } }); - } @Nullable @@ -282,8 +282,10 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { }; } - public boolean isTimeout() { - return isTimeout.get(); + public void handleCancellationException(CancellationException ex) throws HttpTimeoutException { + if (this.timeout.get()) { + throw new HttpTimeoutException(ex.getMessage()); + } } } diff --git a/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java b/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java deleted file mode 100644 index 86630e1fd37..00000000000 --- a/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTest.java +++ /dev/null @@ -1,117 +0,0 @@ -package org.springframework.http.client; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.Mockito.*; - -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.net.http.HttpTimeoutException; -import java.time.Duration; -import java.util.concurrent.*; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; - -class JdkClientHttpRequestTest { - - private HttpClient mockHttpClient; - private URI uri = URI.create("http://example.com"); - private HttpMethod method = HttpMethod.GET; - - private ExecutorService executor; - - @BeforeEach - void setup() { - mockHttpClient = mock(HttpClient.class); - executor = Executors.newSingleThreadExecutor(); - } - - @AfterEach - void tearDown() { - executor.shutdownNow(); - } - - @Test - void executeInternal_withTimeout_shouldThrowHttpTimeoutException() throws Exception { - Duration timeout = Duration.ofMillis(10); - - JdkClientHttpRequest request = new JdkClientHttpRequest(mockHttpClient, uri, method, executor, timeout); - - CompletableFuture> future = new CompletableFuture<>(); - - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(future); - - HttpHeaders headers = new HttpHeaders(); - - CountDownLatch startLatch = new CountDownLatch(1); - - // Cancellation thread waits for startLatch, then cancels the future after a delay - Thread canceller = new Thread(() -> { - try { - startLatch.await(); - Thread.sleep(500); - future.cancel(true); - } catch (InterruptedException ignored) { - } - }); - canceller.start(); - - IOException ex = assertThrows(IOException.class, () -> { - startLatch.countDown(); - request.executeInternal(headers, null); - }); - - assertThat(ex) - .isInstanceOf(HttpTimeoutException.class) - .hasMessage("Request timed out"); - - canceller.join(); - } - - @Test - void executeInternal_withTimeout_shouldThrowIOException() throws Exception { - Duration timeout = Duration.ofMillis(500); - - JdkClientHttpRequest request = new JdkClientHttpRequest(mockHttpClient, uri, method, executor, timeout); - - CompletableFuture> future = new CompletableFuture<>(); - - when(mockHttpClient.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))) - .thenReturn(future); - - HttpHeaders headers = new HttpHeaders(); - - CountDownLatch startLatch = new CountDownLatch(1); - - Thread canceller = new Thread(() -> { - try { - startLatch.await(); - Thread.sleep(10); - future.cancel(true); - } catch (InterruptedException ignored) { - } - }); - canceller.start(); - - IOException ex = assertThrows(IOException.class, () -> { - startLatch.countDown(); - request.executeInternal(headers, null); - }); - - assertThat(ex) - .isInstanceOf(IOException.class) - .hasMessage("Request was cancelled"); - - canceller.join(); - } - -} diff --git a/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java b/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java new file mode 100644 index 00000000000..1cf59ca5938 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.http.client; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.net.http.HttpTimeoutException; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link JdkClientHttpRequest}. + */ +class JdkClientHttpRequestTests { + + private final HttpClient client = mock(HttpClient.class); + + private ExecutorService executor; + + + @BeforeEach + void setup() { + executor = Executors.newSingleThreadExecutor(); + } + + @AfterEach + void tearDown() { + executor.shutdownNow(); + } + + + @Test + void futureCancelledAfterTimeout() { + CompletableFuture> future = new CompletableFuture<>(); + when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future); + + assertThatThrownBy(() -> createRequest(Duration.ofMillis(10)).executeInternal(new HttpHeaders(), null)) + .isExactlyInstanceOf(HttpTimeoutException.class); + } + + @Test + void futureCancelled() { + CompletableFuture> future = new CompletableFuture<>(); + future.cancel(true); + when(client.sendAsync(any(HttpRequest.class), any(HttpResponse.BodyHandler.class))).thenReturn(future); + + assertThatThrownBy(() -> createRequest(null).executeInternal(new HttpHeaders(), null)) + .isExactlyInstanceOf(IOException.class); + } + + private JdkClientHttpRequest createRequest(Duration timeout) { + return new JdkClientHttpRequest(client, URI.create("http://abc.com"), HttpMethod.GET, executor, timeout); + } + +} From f0a9f649c16218b5fe90969d33472e6a89689516 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 8 Aug 2025 11:36:53 +0100 Subject: [PATCH 3/5] Allow null in ProblemDetail#type See gh-35294 --- .../src/main/java/org/springframework/http/ProblemDetail.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/ProblemDetail.java b/spring-web/src/main/java/org/springframework/http/ProblemDetail.java index f24351ca09f..65968b53a9f 100644 --- a/spring-web/src/main/java/org/springframework/http/ProblemDetail.java +++ b/spring-web/src/main/java/org/springframework/http/ProblemDetail.java @@ -110,7 +110,6 @@ public class ProblemDetail implements Serializable { * @param type the problem type */ public void setType(URI type) { - Assert.notNull(type, "'type' is required"); this.type = type; } @@ -251,7 +250,7 @@ public class ProblemDetail implements Serializable { @Override public boolean equals(@Nullable Object other) { return (this == other || (other instanceof ProblemDetail that && - getType().equals(that.getType()) && + ObjectUtils.nullSafeEquals(getType(), that.getType()) && ObjectUtils.nullSafeEquals(getTitle(), that.getTitle()) && this.status == that.status && ObjectUtils.nullSafeEquals(this.detail, that.detail) && From 968e037503319511e06403cbbd28efe2d394cfd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=A6=E5=88=A9=E6=96=8C?= <68638598+Allan-QLB@users.noreply.github.com> Date: Tue, 22 Jul 2025 23:34:14 +0800 Subject: [PATCH 4/5] Add documentation of RequestMapping about SpEL MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 秦利斌 <68638598+Allan-QLB@users.noreply.github.com> --- .../ROOT/pages/web/webflux/controller/ann-requestmapping.adoc | 2 +- .../pages/web/webmvc/mvc-controller/ann-requestmapping.adoc | 2 +- .../springframework/web/bind/annotation/RequestMapping.java | 4 ++++ 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc index 8230b453bbe..532e6250fe9 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc @@ -237,7 +237,7 @@ Kotlin:: URI path patterns can also have embedded `${...}` placeholders that are resolved on startup by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and other property sources. You can use this, for example, to parameterize a base URL based on -some external configuration. +some external configuration. SpEL expression `#{...}` is also supported in URI path pattern by default. NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support. Both classes are located in `spring-web` and are expressly designed for use with HTTP URL diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc index 902a56ac7f4..1d582f4f49a 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc @@ -220,7 +220,7 @@ Kotlin:: URI path patterns can also have embedded `${...}` placeholders that are resolved on startup by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and other property sources. You can use this, for example, to parameterize a base URL based on -some external configuration. +some external configuration. SpEL expression `#{...}` is also supported in URI path pattern by default. [[mvc-ann-requestmapping-pattern-comparison]] diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index a2f330b74b6..2f0b035eba2 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -109,12 +109,16 @@ public @interface RequestMapping { * At the method level, relative paths (for example, {@code "edit"}) are supported * within the primary mapping expressed at the type level. * Path mapping URIs may contain placeholders (for example, "/${profile_path}"). + * By default, SpEL expression is also supported (for example {@code "/profile/#{@bean.property}"}). *

Supported at the type level as well as at the method level! * When used at the type level, all method-level mappings inherit * this primary mapping, narrowing it for a specific handler method. *

NOTE: A handler method that is not mapped to any path * explicitly is effectively mapped to an empty path. * @since 4.2 + * @see org.springframework.beans.factory.config.EmbeddedValueResolver + * @see org.springframework.context.expression.StandardBeanExpressionResolver + * @see org.springframework.context.support.AbstractApplicationContext */ @AliasFor("value") String[] path() default {}; From 6e2fbfe10813e671283ccacbd60096e2f90bdaad Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 8 Aug 2025 11:50:07 +0100 Subject: [PATCH 5/5] Polishing contribution Closes gh-35232 --- .../web/webflux/controller/ann-requestmapping.adoc | 11 +++++++---- .../web/webmvc/mvc-controller/ann-requestmapping.adoc | 11 +++++++---- .../web/bind/annotation/RequestMapping.java | 8 ++++---- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc index 532e6250fe9..f15948bea14 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc @@ -234,10 +234,13 @@ Kotlin:: ====== -- -URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and -other property sources. You can use this, for example, to parameterize a base URL based on -some external configuration. SpEL expression `#{...}` is also supported in URI path pattern by default. +URI path patterns can also have: + +- Embedded `${...}` placeholders that are resolved on startup via +`PropertySourcesPlaceholderConfigurer` against local, system, environment, and +other property sources. This is useful, for example, to parameterize a base URL based on +external configuration. +- SpEL expressions `#{...}`. NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support. Both classes are located in `spring-web` and are expressly designed for use with HTTP URL diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc index 1d582f4f49a..64f5abec6c4 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc @@ -217,10 +217,13 @@ Kotlin:: ---- ====== -URI path patterns can also have embedded `${...}` placeholders that are resolved on startup -by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and -other property sources. You can use this, for example, to parameterize a base URL based on -some external configuration. SpEL expression `#{...}` is also supported in URI path pattern by default. +URI path patterns can also have: + +- Embedded `${...}` placeholders that are resolved on startup via +`PropertySourcesPlaceholderConfigurer` against local, system, environment, and +other property sources. This is useful, for example, to parameterize a base URL based on +external configuration. +- SpEL expression `#{...}`. [[mvc-ann-requestmapping-pattern-comparison]] diff --git a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java index 2f0b035eba2..3e3dba15a02 100644 --- a/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java +++ b/spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java @@ -105,11 +105,11 @@ public @interface RequestMapping { /** * The path mapping URIs — for example, {@code "/profile"}. - *

Ant-style path patterns are also supported (for example, {@code "/profile/**"}). - * At the method level, relative paths (for example, {@code "edit"}) are supported + *

Ant-style path patterns are also supported, e.g. {@code "/profile/**"}. + * At the method level, relative paths, e.g., {@code "edit"} are supported * within the primary mapping expressed at the type level. - * Path mapping URIs may contain placeholders (for example, "/${profile_path}"). - * By default, SpEL expression is also supported (for example {@code "/profile/#{@bean.property}"}). + * Path mapping URIs may contain property placeholders, e.g. "/${profile_path}", + * and SpEL expressions, e.g. {@code "/profile/#{@bean.property}"}. *

Supported at the type level as well as at the method level! * When used at the type level, all method-level mappings inherit * this primary mapping, narrowing it for a specific handler method.