Browse Source

Merge branch '6.2.x'

pull/35303/head
rstoyanchev 7 months ago
parent
commit
a0542f023c
  1. 11
      framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc
  2. 11
      framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc
  3. 3
      spring-web/src/main/java/org/springframework/http/ProblemDetail.java
  4. 27
      spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java
  5. 10
      spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java
  6. 87
      spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java

11
framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc

@ -256,10 +256,13 @@ Kotlin:: @@ -256,10 +256,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.
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

11
framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-requestmapping.adoc

@ -251,10 +251,13 @@ Kotlin:: @@ -251,10 +251,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.
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]]

3
spring-web/src/main/java/org/springframework/http/ProblemDetail.java

@ -108,7 +108,6 @@ public class ProblemDetail implements Serializable { @@ -108,7 +108,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;
}
@ -245,7 +244,7 @@ public class ProblemDetail implements Serializable { @@ -245,7 +244,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) &&

27
spring-web/src/main/java/org/springframework/http/client/JdkClientHttpRequest.java

@ -37,6 +37,7 @@ import java.util.concurrent.ExecutionException; @@ -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.jspecify.annotations.Nullable;
@ -96,12 +97,13 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { @@ -96,12 +97,13 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
@Override
protected ClientHttpResponse executeInternal(HttpHeaders headers, @Nullable Body body) throws IOException {
CompletableFuture<HttpResponse<InputStream>> 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<InputStream> response = responseFuture.get();
InputStream inputStream = timeoutHandler.wrapInputStream(response);
return new JdkClientHttpResponse(response, inputStream);
@ -119,8 +121,11 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { @@ -119,8 +121,11 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
catch (ExecutionException ex) {
Throwable cause = ex.getCause();
if (cause instanceof CancellationException) {
throw new HttpTimeoutException("Request timed out");
if (cause instanceof CancellationException ce) {
if (timeoutHandler != null) {
timeoutHandler.handleCancellationException(ce);
}
throw new IOException("Request cancelled", cause);
}
if (cause instanceof UncheckedIOException uioEx) {
throw uioEx.getCause();
@ -136,6 +141,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { @@ -136,6 +141,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
throw (message == null ? new IOException(cause) : new IOException(message, cause));
}
}
catch (CancellationException ex) {
if (timeoutHandler != null) {
timeoutHandler.handleCancellationException(ex);
}
throw new IOException("Request cancelled", ex);
}
}
private HttpRequest buildRequest(HttpHeaders headers, @Nullable Body body) {
@ -234,12 +245,15 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { @@ -234,12 +245,15 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
private final CompletableFuture<Void> timeoutFuture;
private final AtomicBoolean timeout = new AtomicBoolean(false);
private TimeoutHandler(CompletableFuture<HttpResponse<InputStream>> future, Duration timeout) {
this.timeoutFuture = new CompletableFuture<Void>()
.completeOnTimeout(null, timeout.toMillis(), TimeUnit.MILLISECONDS);
this.timeoutFuture.thenRun(() -> {
this.timeout.set(true);
if (future.cancel(true) || future.isCompletedExceptionally() || !future.isDone()) {
return;
}
@ -250,7 +264,6 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { @@ -250,7 +264,6 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
// ignore
}
});
}
public @Nullable InputStream wrapInputStream(HttpResponse<InputStream> response) {
@ -267,6 +280,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest { @@ -267,6 +280,12 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
}
};
}
public void handleCancellationException(CancellationException ex) throws HttpTimeoutException {
if (this.timeout.get()) {
throw new HttpTimeoutException(ex.getMessage());
}
}
}
}

10
spring-web/src/main/java/org/springframework/web/bind/annotation/RequestMapping.java

@ -105,16 +105,20 @@ public @interface RequestMapping { @@ -105,16 +105,20 @@ public @interface RequestMapping {
/**
* The path mapping URIs &mdash; for example, {@code "/profile"}.
* <p>Ant-style path patterns are also supported (for example, {@code "/profile/**"}).
* At the method level, relative paths (for example, {@code "edit"}) are supported
* <p>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, <code>"/${profile_path}"</code>).
* Path mapping URIs may contain property placeholders, e.g. <code>"/${profile_path}"</code>,
* and SpEL expressions, e.g. {@code "/profile/#{@bean.property}"}.
* <p><b>Supported at the type level as well as at the method level!</b>
* When used at the type level, all method-level mappings inherit
* this primary mapping, narrowing it for a specific handler method.
* <p><strong>NOTE</strong>: 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 {};

87
spring-web/src/test/java/org/springframework/http/client/JdkClientHttpRequestTests.java

@ -0,0 +1,87 @@ @@ -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<HttpResponse<InputStream>> 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<HttpResponse<InputStream>> 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);
}
}
Loading…
Cancel
Save