Compare commits

...

36 Commits

Author SHA1 Message Date
Stéphane Nicoll 16ecbfc9fe Release v7.0.5 2026-02-18 13:31:03 +01:00
rstoyanchev 85c18caf25 Update docs on supported versions with baseline notation
Closes gh-36316
2026-02-18 11:33:08 +00:00
Juergen Hoeller 22bd8bd704 Skip serialization of potentially non-serializable cached state
Closes gh-36346
2026-02-17 22:13:57 +01:00
Juergen Hoeller 9b10bb5e08 Polishing 2026-02-17 18:57:09 +01:00
Juergen Hoeller 727ccd04ef Avoid setCharacterEncoding(Charset) call with null value
Includes consistent content length check for functional response.

See gh-36343
2026-02-17 18:56:59 +01:00
Juergen Hoeller a9f447e8d7 Consistent adaptation of HTTP headers on Servlet responses
Includes use of Servlet 6.1 setCharacterEncoding(Charset)

Closes gh-36343
2026-02-17 17:38:50 +01:00
海子 Yang 0841e79e32 Copy methodAnnotations in MethodParameter copy constructor
Signed-off-by: 海子 Yang <i.take.today@gmail.com>
2026-02-17 17:17:22 +01:00
Niravil a1868d3e9e fix TransactionAspectSupport#currentTransactionStatus javadoc
Signed-off-by: Niravil <messerignacius@gmail.com>
2026-02-17 16:26:02 +01:00
rstoyanchev e5aac66157 ServletResponseHeadersAdapter checks contentType property
Issue gh-36334
2026-02-17 11:55:34 +00:00
Juergen Hoeller 8bf85d2596 Polishing 2026-02-17 12:27:00 +01:00
Juergen Hoeller 508b31da4f Optimize Entry hashCode in HttpHeaders (plus related polishing) 2026-02-17 11:55:10 +01:00
Juergen Hoeller d291272736 Use HandlerMethod-determined validation groups
Closes gh-36336
2026-02-17 11:50:07 +01:00
海子 Yang db01f07037 Remove unused EMPTY_GROUPS in InvocableHandlerMethod
Signed-off-by: 海子 Yang <i.take.today@gmail.com>
2026-02-17 11:30:54 +01:00
Brian Clozel b9e190e313 Fix HttpMessageConverters configurers support
This commit fixes the `configureMessageConverters` and
`configureMessageConvertersList` behavior.

`configureMessageConverters` was not executing consumers in their order
of registration (but in the reverse order).
`configureMessageConvertersList` was not executing multiple consumers
and was instead executing the first consumer multiple times.

This commit fixes both issues.

Fixes gh-36332
2026-02-16 18:48:26 +01:00
rstoyanchev d712ec3d49 Minor optimization in Tomcat header adapters
get method skips containsKey and instead checks if the enumeration
has elements, which should give the same behavior other than for
headers without values.

See gh-36334
2026-02-16 17:38:01 +00:00
rstoyanchev 5baa4fdd69 Further optimize Servlet header adapters
- Optimize get method for request headers
- Update keySet methods to use custom extension of AbstractSet
- Drop use of native Tomcat headers, which could be an issue for
request and response wrappers that override header methods.
The performance of Servlet adapters should be similar for the
commonly used methods, and it should be possible to optimize
further for the future in HttpHeaders (e.g. by adding a
set alternative to put), and requesting Servlet API refinements.

Closes gh-36334
2026-02-16 17:37:51 +00:00
rstoyanchev 9273a11a2c Stub Servlet headers adapter methods not needed in HttpHeaders
See gh-36334
2026-02-16 17:37:27 +00:00
rstoyanchev 7ea11baff9 Pass-through handling of Servlet headers
See gh-36334
2026-02-16 17:37:09 +00:00
Sam Brannen 655fd2e2bf Document that SpEL expressions using Optional w/ null-safe & Elvis operators are not compilable
Closes gh-36331
2026-02-15 12:26:54 +01:00
Brian Clozel a31574a0a2 Fix MultipartParser & PartGenerator memory leak
Prior to this commit, the reactive `MultipartParser` and `PartGenerator`
types were leaking memory at runtime in specific cases:

* many HTTP clients must send multipart requests to be parsed and close
  the connection while uploading
* the `PartGenerator` must be configured to write file parts to
  temporary files on disk
* concurrency, upload speed must be important to trigger cases where the
  file system is not fast enough to consume incoming buffers

The `MultipartParser` parses and emits `BodyToken` to its sink
(here, the `PartGenerator`). By definition, Reactor's `FluxSink` when
created with `Flux.create(FluxSink)` will use a "buffer" strategy and
will queue emitted elements if they cannot be consumed.

Here, the cancellation signal does dispose internal states in the
`MultiPartParser` and `PartGenerator` but does not clear the internal
queue in `FluxSink`.

This commit ensures that an operation is registered to release buffers
on the discard event.

Fixes gh-36262
2026-02-13 18:33:47 +01:00
Juergen Hoeller 44f3e7b427 Fix Checkstyle violation
See gh-36317
2026-02-13 16:34:58 +01:00
Juergen Hoeller e8e24e65d2 Detect all common size exceptions from Tomcat and Commons FileUpload 2.x
Closes gh-36317
2026-02-13 16:19:46 +01:00
Sam Brannen 9a002a77b2 Stop referring to "Java-8" features in documentation
See gh-36310
2026-02-13 15:21:31 +01:00
Brian Clozel 97e96895db Optimize MediaType(MediaType, Charset) constructor
Prior to this commit, the `MediaType` and `MimeType` "copy" constructors
would not leverage the fact that the existing instance has been
validated already (types, subtype and parameters have been checked
already for errors) and the entire validation would be performed again.
This would also allocate map instances in the process.

This commit ensures that the already validated information is reused
directly and that we avoid unnessecary operations and allocations for
such constructors.

Closes gh-36318
2026-02-13 14:49:34 +01:00
Juergen Hoeller e98c2fe488 Reuse AnnotatedMethod annotation cache in derived instances
Closes gh-36322
2026-02-13 13:55:24 +01:00
Juergen Hoeller 75705bcbb1 Restore early MessageConsumer creation for temporary queue
Closes gh-36321
2026-02-13 13:55:08 +01:00
rstoyanchev 47a2e7059e Optimal charset handling in AbstractHttpMessageConverter
Closes gh-36320
2026-02-13 12:39:03 +00:00
Sam Brannen 14b6339351 Consistently support @⁠Autowired as a meta-annotation
Prior to this commit, the findAutowiredAnnotation() method in
AutowiredAnnotationBeanPostProcessor fully supported finding
@⁠Autowired as a meta-annotation; however, the isRequired() method in
QualifierAnnotationAutowireCandidateResolver only found @⁠Autowired as
a "directly present" annotation without any support for
meta-annotations.

For consistency and to avoid bugs, this commit revises
QualifierAnnotationAutowireCandidateResolver so that we always support
@⁠Autowired as a meta-annotation.

Closes gh-36315
2026-02-12 16:38:24 +01:00
Juergen Hoeller bc2e89a786 Cache ResponseBody annotation presence per controller class
See gh-36311
2026-02-12 13:44:54 +01:00
Sam Brannen 4abbddf601 Avoid duplicate required attribute lookup for @⁠Autowired annotations
This commit revises AutowiredAnnotationBeanPostProcessor so that
determineRequiredStatus(MergedAnnotation<?>) only looks up the required
attribute once.

Closes gh-36314
2026-02-12 13:19:06 +01:00
Sam Brannen 926bcbd9a6 Stop referring to obsolete ListenableFuture in documentation
ListenableFuture as deprecated for removal in 6.0 and removed in 7.0.

See gh-33808
See gh-33809
See commit cb8ed43be1
Closes gh-36313
2026-02-12 13:19:06 +01:00
Sam Brannen cb8ed43be1 Stop referring to "JDK 8" features in documentation
See gh-36310
2026-02-12 12:27:51 +01:00
Juergen Hoeller 1703388074 Cache ResponseBody annotation presence per controller class
Closes gh-36311
2026-02-12 12:20:14 +01:00
Sam Brannen d84c4a39e2 Stop referring to "Java 8" features in documentation
Closes gh-36310
2026-02-12 12:11:02 +01:00
Sam Brannen 6f03c186b9 Polishing 2026-02-12 11:51:39 +01:00
Brian Clozel e10d37ad54 Next development version (v7.0.5-SNAPSHOT) 2026-02-12 10:39:15 +01:00
96 changed files with 1424 additions and 436 deletions
@@ -727,7 +727,7 @@ of determining parameter names, an exception will be thrown.
parameter names. This discoverer is only used if such APIs are present on the classpath.
`StandardReflectionParameterNameDiscoverer` :: Uses the standard `java.lang.reflect.Parameter`
API to determine parameter names. Requires that code be compiled with the `-parameters`
flag for `javac`. Recommended approach on Java 8+.
flag for `javac`. Recommended approach.
`AspectJAdviceParameterNameDiscoverer` :: Deduces parameter names from the pointcut
expression, `returning`, and `throwing` clauses. See the
{spring-framework-api}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc]
@@ -531,6 +531,7 @@ following kinds of expressions cannot be compiled.
* Expressions relying on the conversion service
* Expressions using custom resolvers
* Expressions using overloaded operators
* Expressions using `Optional` with the null-safe or Elvis operator
* Expressions using array construction syntax
* Expressions using selection or projection
* Expressions using bean references
@@ -35,7 +35,7 @@ xref:integration/cache/store-configuration.adoc#cache-store-configuration-jsr107
[[cache-store-configuration-caffeine]]
== Caffeine Cache
Caffeine is a Java 8 rewrite of Guava's cache, and its implementation is located in the
Caffeine is a rewrite of Guava's cache, and its implementation is located in the
`org.springframework.cache.caffeine` package and provides access to several features
of Caffeine.
@@ -26,7 +26,7 @@ behind the scenes for each annotated method, by using a `JmsListenerContainerFac
Such a container is not registered against the application context but can be easily
located for management purposes by using the `JmsListenerEndpointRegistry` bean.
TIP: `@JmsListener` is a repeatable annotation on Java 8, so you can associate
TIP: `@JmsListener` is a repeatable annotation, so you can associate
several JMS destinations with the same method by adding additional `@JmsListener`
declarations to it.
@@ -466,7 +466,7 @@ synchronous, asynchronous, and streaming scenarios.
* Non-blocking I/O
* Reactive Streams back pressure
* High concurrency with fewer hardware resources
* Functional-style, fluent API that takes advantage of Java 8 lambdas
* Functional-style, fluent API that takes advantage of lambda expressions
* Synchronous and asynchronous interactions
* Streaming up to or streaming down from a server
@@ -533,8 +533,7 @@ that returns a value:
----
TIP: `@Async` methods may not only declare a regular `java.util.concurrent.Future` return
type but also Spring's `org.springframework.util.concurrent.ListenableFuture` or, as of
Spring 4.2, JDK 8's `java.util.concurrent.CompletableFuture`, for richer interaction with
type but also `java.util.concurrent.CompletableFuture`, for richer interaction with
the asynchronous task and for immediate composition with further processing steps.
You can not use `@Async` in conjunction with lifecycle callbacks such as `@PostConstruct`.
@@ -3,13 +3,15 @@
:page-section-summary-toc: 1
The Spring Framework supports various Kotlin constructs, such as instantiating Kotlin classes
through primary constructors, immutable classes data binding, and function optional parameters
with default values.
through primary constructors, data binding for immutable classes, and optional parameters
with default values for functions.
Kotlin parameter names are recognized through a dedicated `KotlinReflectionParameterNameDiscoverer`,
which allows finding interface method parameter names without requiring the Java 8 `-parameters`
compiler flag to be enabled during compilation. (For completeness, we nevertheless recommend
running the Kotlin compiler with its `-java-parameters` flag for standard Java parameter exposure.)
which allows finding interface method parameter names without requiring the Java `-parameters`
compiler flag to be enabled during compilation.
TIP: For completeness, we nevertheless recommend running the Kotlin compiler with its
`-java-parameters` flag for standard Java parameter exposure.
You can declare configuration classes as
{kotlin-docs}/nested-classes.html[top level or nested but not inner],
@@ -4,8 +4,7 @@
`@AfterTransaction` indicates that the annotated `void` method should be run after a
transaction is ended, for test methods that have been configured to run within a
transaction by using Spring's `@Transactional` annotation. `@AfterTransaction` methods
are not required to be `public` and may be declared on Java 8-based interface default
methods.
are not required to be `public` and may be declared on interface default methods.
[tabs]
======
@@ -4,8 +4,7 @@
`@BeforeTransaction` indicates that the annotated `void` method should be run before a
transaction is started, for test methods that have been configured to run within a
transaction by using Spring's `@Transactional` annotation. `@BeforeTransaction` methods
are not required to be `public` and may be declared on Java 8-based interface default
methods.
are not required to be `public` and may be declared on interface default methods.
The following example shows how to use the `@BeforeTransaction` annotation:
@@ -3,7 +3,7 @@
`@SqlGroup` is a container annotation that aggregates several `@Sql` annotations. You can
use `@SqlGroup` natively to declare several nested `@Sql` annotations, or you can use it
in conjunction with Java 8's support for repeatable annotations, where `@Sql` can be
in conjunction with Java's support for repeatable annotations, where `@Sql` can be
declared several times on the same class or method, implicitly generating this container
annotation. The following example shows how to declare an SQL group:
@@ -11,7 +11,7 @@ https://testng.org/[TestNG] ::
testing, distributed testing, and other features. Supported in the
xref:testing/testcontext-framework.adoc[Spring TestContext Framework].
{assertj-docs}[AssertJ] ::
"Fluent assertions for Java", including support for Java 8 lambdas, streams, and
"Fluent assertions for Java", including support for lambda expressions, streams, and
numerous other features. Supported in Spring's
xref:testing/mockmvc/assertj.adoc[MockMvc testing support].
https://en.wikipedia.org/wiki/Mock_Object[Mock Objects] ::
@@ -13,9 +13,9 @@ the same xref:web/webflux/reactive-spring.adoc[Reactive Core] foundation.
== Overview
[.small]#xref:web/webmvc-functional.adoc#webmvc-fn-overview[See equivalent in the Servlet stack]#
In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes a
`ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono<ServerResponse>`).
Both the request and the response object have immutable contracts that offer JDK 8-friendly
Both the request and the response object have immutable contracts that offer convenient
access to the HTTP request and response.
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
annotation-based programming model.
@@ -117,7 +117,7 @@ Most applications can run through the WebFlux Java configuration, see xref:web/w
== HandlerFunction
[.small]#xref:web/webmvc-functional.adoc#webmvc-fn-handler-functions[See equivalent in the Servlet stack]#
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient
access to the HTTP request and response.
Both request and response provide {reactive-streams-site}[Reactive Streams] back pressure
against the body streams.
@@ -394,7 +394,8 @@ Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` `version` attribute supports the following:
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above
- Baseline version ("1.2+") -- matches the given and
xref:web/webflux/config.adoc#webflux-config-api-version[supported versions] above
- No value -- matches any version, but is superseded by a more specific version match
If multiple controller methods have a version less than or equal to the request version,
@@ -443,6 +444,9 @@ For request with version `"1.3"`:
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
- (4) is higher and does not match
NOTE: Version 1.3 must be present in the mappings, or be
xref:web/webflux/config.adoc#webflux-config-api-version[configured as supported].
For request with version `"1.5"`:
- (1) matches as it matches any version
@@ -459,10 +463,6 @@ versioned alternative was introduced. Therefore, even though an unversioned cont
method is considered a match for any version, it is in fact given the lowest priority,
and is effectively superseded by any alternative controller method with a version.
NOTE: The above assumes the request version is a
xref:web/webflux/config.adoc#webflux-config-api-version["supported" version],
or otherwise it would fail.
See xref:web/webflux-versioning.adoc[API Versioning] for more details on underlying
infrastructure and support for API Versioning.
@@ -58,7 +58,7 @@ If a publisher cannot slow down, it has to decide whether to buffer, drop, or fa
Reactive Streams plays an important role for interoperability. It is of interest to libraries
and infrastructure components but less useful as an application API, because it is too
low-level. Applications need a higher-level and richer, functional API to
compose async logic -- similar to the Java 8 `Stream` API but not only for collections.
compose async logic -- similar to the Java `Stream` API but not only for collections.
This is the role that reactive libraries play.
{reactor-github-org}/reactor[Reactor] is the reactive library of choice for
@@ -131,7 +131,7 @@ execution model benefits as others in this space and also provides a choice of s
(annotated controllers and functional web endpoints), and a choice of reactive libraries
(Reactor, RxJava, or other).
* If you are interested in a lightweight, functional web framework for use with Java 8 lambdas
* If you are interested in a lightweight, functional web framework for use with Java
or Kotlin, you can use the Spring WebFlux functional web endpoints. That can also be a good choice
for smaller applications or microservices with less complex requirements that can benefit
from greater transparency and control.
@@ -15,7 +15,7 @@ the same xref:web/webmvc/mvc-servlet.adoc[DispatcherServlet].
In WebMvc.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
`ServerRequest` and returns a `ServerResponse`.
Both the request and the response object have immutable contracts that offer JDK 8-friendly
Both the request and the response object have immutable contracts that offer convenient
access to the HTTP request and response.
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
annotation-based programming model.
@@ -116,7 +116,7 @@ xref:web/webmvc-functional.adoc#webmvc-fn-running[Running a Server].
== HandlerFunction
[.small]#xref:web/webflux-functional.adoc#webflux-fn-handler-functions[See equivalent in the Reactive stack]#
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient
access to the HTTP request and response, including headers, body, method, and status code.
[[webmvc-fn-request]]
@@ -6,7 +6,7 @@
The next table describes the supported controller method arguments. Reactive types are not supported
for any arguments.
JDK 8's `java.util.Optional` is supported as a method argument in combination with
`java.util.Optional` is supported as a method argument in combination with
annotations that have a `required` attribute (for example, `@RequestParam`, `@RequestHeader`,
and others) and is equivalent to `required=false`.
@@ -425,7 +425,7 @@ Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` `version` attribute supports the following:
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above
- Baseline version ("1.2+") -- matches the given and xref:web/webmvc/mvc-config/api-version.adoc[supported versions] above
- No value -- matches any version, but is superseded by a more specific version match
If multiple controller methods have a version less than or equal to the request version,
@@ -463,7 +463,7 @@ Java::
----
<1> match any version
<2> match version 1.1
<3> match version 1.2 and above
<3> match version 1.2 and supported versions above
<4> match version 1.5
======
@@ -474,6 +474,9 @@ For request with version `"1.3"`:
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
- (4) is higher and does not match
NOTE: Version 1.3 must be present in the mappings, or be
xref:web/webmvc/mvc-config/api-version.adoc[configured as supported].
For request with version `"1.5"`:
- (1) matches as it matches any version
@@ -490,10 +493,6 @@ versioned alternative was introduced. Therefore, even though an unversioned cont
method is considered a match for any version, it is in fact given the lowest priority,
and is effectively superseded by any alternative controller method with a version.
NOTE: The above assumes the request version is a
xref:web/webmvc/mvc-config/api-version.adoc["supported" version], or otherwise it
would fail.
See xref:web/webmvc-versioning.adoc[API Versioning] for more details on underlying
infrastructure and support for API Versioning.
+1 -1
View File
@@ -1,4 +1,4 @@
version=7.0.4-SNAPSHOT
version=7.0.5
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -283,7 +283,7 @@ public final class CachedIntrospectionResults {
}
// Explicitly check implemented interfaces for setter/getter methods as well,
// in particular for Java 8 default methods...
// in particular for interface default methods.
Class<?> currClass = beanClass;
while (currClass != null && currClass != Object.class) {
introspectInterfaces(beanClass, currClass, readMethodNames);
@@ -62,9 +62,9 @@ import java.lang.annotation.Target;
*
* <h3>Multiple Arguments and 'required' Semantics</h3>
* <p>In the case of a multi-arg constructor or method, the {@link #required} attribute
* is applicable to all arguments. Individual parameters may be declared as Java-8 style
* {@link java.util.Optional} as well as {@code @Nullable} or a not-null parameter
* type in Kotlin, overriding the base 'required' semantics.
* is applicable to all arguments. Individual parameters may be declared as
* {@link java.util.Optional}, {@code @Nullable}, or a not-null parameter type in
* Kotlin, overriding the base 'required' semantics.
*
* <h3>Autowiring Arrays, Collections, and Maps</h3>
* <p>In case of an array, {@link java.util.Collection}, or {@link java.util.Map}
@@ -33,6 +33,7 @@ import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -621,12 +622,12 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
* <p>A 'required' dependency means that autowiring should fail when no beans
* are found. Otherwise, the autowiring process will simply bypass the field
* or method when no beans are found.
* @param ann the Autowired annotation
* @param ann a {@link MergedAnnotation} representing the Autowired annotation
* @return whether the annotation indicates that a dependency is required
*/
protected boolean determineRequiredStatus(MergedAnnotation<?> ann) {
return (ann.getValue(this.requiredParameterName).isEmpty() ||
this.requiredParameterValue == ann.getBoolean(this.requiredParameterName));
Optional<Boolean> requiredAttribute = ann.getValue(this.requiredParameterName, Boolean.class);
return (requiredAttribute.isEmpty() || this.requiredParameterValue == requiredAttribute.get());
}
/**
@@ -348,8 +348,20 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
if (!super.isRequired(descriptor)) {
return false;
}
Autowired autowired = descriptor.getAnnotation(Autowired.class);
return (autowired == null || autowired.required());
for (Annotation ann : descriptor.getAnnotations()) {
// Directly present?
if (ann instanceof Autowired autowired) {
return autowired.required();
}
// Meta-present?
Autowired autowired = AnnotationUtils.findAnnotation(ann.annotationType(), Autowired.class);
if (autowired != null) {
return autowired.required();
}
}
// No @Autowired annotation present: default to true.
return true;
}
/**
@@ -149,10 +149,10 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable
/**
* Return whether this dependency is required.
* <p>Optional semantics are derived from Java 8's {@link java.util.Optional},
* any variant of a parameter-level {@code Nullable} annotation (such as from
* JSR-305 or the FindBugs set of annotations), or a language-level nullable
* type declaration in Kotlin.
* <p>Optional semantics are derived from Java's {@link java.util.Optional},
* any variant of a parameter-level {@code @Nullable} annotation (such as from
* JSpecify, JSR-305, or the FindBugs set of annotations), or a language-level
* nullable type declaration in Kotlin.
*/
public boolean isRequired() {
if (!this.required) {
@@ -0,0 +1,179 @@
/*
* 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.beans.factory.annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.core.MethodParameter;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
/**
* Unit tests for {@link QualifierAnnotationAutowireCandidateResolver}.
*
* @author Sam Brannen
* @since 7.0.5
*/
class QualifierAnnotationAutowireCandidateResolverTests {
final QualifierAnnotationAutowireCandidateResolver resolver = new QualifierAnnotationAutowireCandidateResolver();
Method testMethod;
@RegisterExtension
BeforeTestExecutionCallback extension = context -> this.testMethod = context.getRequiredTestMethod();
@Test
void isNotAutowired() {
assertRequired();
}
@Test
void isAutowiredRequired() {
assertRequired();
}
@Test
void isAutowiredOptional() {
assertNotRequired();
}
@Test
void isMetaAutowiredRequired() {
assertRequired();
}
@Test
void isMetaAutowiredOptional() {
assertNotRequired();
}
@Test
void isMetaMetaAutowiredRequired() {
assertRequired();
}
@Test
void isMetaMetaAutowiredOptional() {
assertNotRequired();
}
private void assertRequired() {
assertSoftly(softly -> {
softly.assertThat(this.resolver.isRequired(getFieldDescriptor()))
.as("%sField is required", this.testMethod.getName()).isTrue();
softly.assertThat(this.resolver.isRequired(getParameterDescriptor()))
.as("parameter in %sParameter() is required", this.testMethod.getName()).isTrue();
});
}
private void assertNotRequired() {
assertSoftly(softly -> {
softly.assertThat(this.resolver.isRequired(getFieldDescriptor()))
.as("%sField is not required", this.testMethod.getName()).isFalse();
softly.assertThat(this.resolver.isRequired(getParameterDescriptor()))
.as("parameter in %sParameter() is not required", this.testMethod.getName()).isFalse();
});
}
private DependencyDescriptor getFieldDescriptor() {
var field = ReflectionUtils.findField(getClass(), this.testMethod.getName() + "Field");
return new DependencyDescriptor(field, true);
}
private DependencyDescriptor getParameterDescriptor() {
var method = ReflectionUtils.findMethod(getClass(), this.testMethod.getName() + "Parameter", String.class);
var methodParameter = MethodParameter.forExecutable(method, 0);
return new DependencyDescriptor(methodParameter, true);
}
String isNotAutowiredField;
@Autowired
String isAutowiredRequiredField;
@Autowired(required = false)
String isAutowiredOptionalField;
@MetaAutowiredRequired
String isMetaAutowiredRequiredField;
@MetaAutowiredOptional
String isMetaAutowiredOptionalField;
@MetaMetaAutowiredRequired
String isMetaMetaAutowiredRequiredField;
@MetaMetaAutowiredOptional
String isMetaMetaAutowiredOptionalField;
void isNotAutowiredParameter(String enigma) {
}
void isAutowiredRequiredParameter(@Autowired String enigma) {
}
void isAutowiredOptionalParameter(@Autowired(required = false) String enigma) {
}
void isMetaAutowiredRequiredParameter(@MetaAutowiredRequired String enigma) {
}
void isMetaAutowiredOptionalParameter(@MetaAutowiredOptional String enigma) {
}
void isMetaMetaAutowiredRequiredParameter(@MetaMetaAutowiredRequired String enigma) {
}
void isMetaMetaAutowiredOptionalParameter(@MetaMetaAutowiredOptional String enigma) {
}
@Retention(RetentionPolicy.RUNTIME)
@Autowired
@interface MetaAutowiredRequired {
}
@Retention(RetentionPolicy.RUNTIME)
@Autowired(required = false)
@interface MetaAutowiredOptional {
}
@Retention(RetentionPolicy.RUNTIME)
@MetaAutowiredRequired
@interface MetaMetaAutowiredRequired {
}
@Retention(RetentionPolicy.RUNTIME)
@MetaAutowiredOptional
@interface MetaMetaAutowiredOptional {
}
}
@@ -26,9 +26,9 @@ import java.lang.annotation.Target;
* Container annotation that aggregates several {@link ComponentScan} annotations.
*
* <p>Can be used natively, declaring several nested {@link ComponentScan} annotations.
* Can also be used in conjunction with Java 8's support for repeatable annotations,
* where {@link ComponentScan} can simply be declared several times on the same method,
* implicitly generating this container annotation.
* Can also be used in conjunction with Java's support for repeatable annotations,
* where {@link ComponentScan @ComponentScan} can simply be declared several times
* on the same method, implicitly generating this container annotation.
*
* @author Juergen Hoeller
* @since 4.3
@@ -443,7 +443,7 @@ class ConfigurationClassParser {
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(ifc);
for (MethodMetadata methodMetadata : beanMethods) {
if (!methodMetadata.isAbstract()) {
// A default method or other concrete method on a Java 8+ interface...
// A default method or other concrete method on a Java interface...
configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}
}
@@ -26,9 +26,10 @@ import java.lang.annotation.Target;
* Container annotation that aggregates several {@link PropertySource} annotations.
*
* <p>Can be used natively, declaring several nested {@link PropertySource} annotations.
* Can also be used in conjunction with Java 8's support for <em>repeatable annotations</em>,
* where {@link PropertySource} can simply be declared several times on the same
* {@linkplain ElementType#TYPE type}, implicitly generating this container annotation.
* Can also be used in conjunction with Java's support for <em>repeatable annotations</em>,
* where {@link PropertySource @PropertySource} can simply be declared several
* times on the same {@linkplain ElementType#TYPE type}, implicitly generating
* this container annotation.
*
* @author Phillip Webb
* @since 4.0
@@ -40,7 +40,7 @@ import org.springframework.util.StringUtils;
/**
* Formats fields annotated with the {@link DateTimeFormat} annotation using the
* JSR-310 <code>java.time</code> package in JDK 8.
* JSR-310 <code>java.time</code> package.
*
* @author Juergen Hoeller
* @author Sam Brannen
@@ -1,5 +1,5 @@
/**
* Integration with the JSR-310 <code>java.time</code> package in JDK 8.
* Integration with the JSR-310 <code>java.time</code> package.
*/
@NullMarked
package org.springframework.format.datetime.standard;
@@ -28,9 +28,9 @@ import org.springframework.aot.hint.annotation.Reflective;
* Container annotation that aggregates several {@link Scheduled} annotations.
*
* <p>Can be used natively, declaring several nested {@link Scheduled} annotations.
* Can also be used in conjunction with Java 8's support for repeatable annotations,
* where {@link Scheduled} can simply be declared several times on the same method,
* implicitly generating this container annotation.
* Can also be used in conjunction with Java's support for repeatable annotations,
* where {@link Scheduled @Scheduled} can simply be declared several times on the
* same method, implicitly generating this container annotation.
*
* <p>This annotation may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em>.
@@ -49,14 +49,15 @@ public class ForkJoinPoolFactoryBean implements FactoryBean<ForkJoinPool>, Initi
/**
* Set whether to expose JDK 8's 'common' {@link ForkJoinPool}.
* <p>Default is "false", creating a local {@link ForkJoinPool} instance based on the
* {@link #setParallelism "parallelism"}, {@link #setThreadFactory "threadFactory"},
* {@link #setUncaughtExceptionHandler "uncaughtExceptionHandler"} and
* {@link #setAsyncMode "asyncMode"} properties on this FactoryBean.
* <p><b>NOTE:</b> Setting this flag to "true" effectively ignores all other
* Set whether to expose Java's 'common' {@link ForkJoinPool}.
* <p>Default is {@code false} , creating a local {@link ForkJoinPool} instance
* based on the {@link #setParallelism parallelism},
* {@link #setThreadFactory threadFactory},
* {@link #setUncaughtExceptionHandler uncaughtExceptionHandler}, and
* {@link #setAsyncMode asyncMode} properties on this FactoryBean.
* <p><b>NOTE:</b> Setting this flag to {@code true} effectively ignores all other
* properties on this FactoryBean, reusing the shared common JDK {@link ForkJoinPool}
* instead. This is a fine choice on JDK 8 but does remove the application's ability
* instead. This is a fine choice but does remove the application's ability
* to customize ForkJoinPool behavior, in particular the use of custom threads.
* @since 3.2
* @see java.util.concurrent.ForkJoinPool#commonPool()
@@ -20,7 +20,7 @@ import org.jspecify.annotations.Nullable;
/**
* Default implementation of the {@link ParameterNameDiscoverer} strategy interface,
* delegating to the Java 8 standard reflection mechanism.
* delegating to Java's standard reflection mechanism.
*
* <p>If a Kotlin reflection implementation is present,
* {@link KotlinReflectionParameterNameDiscoverer} is added first in the list and
@@ -47,7 +47,7 @@ public class DefaultParameterNameDiscoverer extends PrioritizedParameterNameDisc
addDiscoverer(new KotlinReflectionParameterNameDiscoverer());
}
// Recommended approach on Java 8+: compilation with -parameters.
// Recommended approach on Java: compilation with -parameters.
addDiscoverer(new StandardReflectionParameterNameDiscoverer());
}
@@ -26,11 +26,12 @@ import kotlin.reflect.jvm.ReflectJvmMapping;
import org.jspecify.annotations.Nullable;
/**
* {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection facilities
* for introspecting parameter names.
* {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection
* facilities for introspecting parameter names.
*
* <p>Compared to {@link StandardReflectionParameterNameDiscoverer}, it allows in addition to
* determine interface parameter names without requiring Java 8 -parameters compiler flag.
* <p>In contrast to {@link StandardReflectionParameterNameDiscoverer}, this
* discoverer can also determine interface parameter names without requiring Java's
* {@code -parameters} compiler flag.
*
* @author Sebastien Deleuze
* @since 5.0
@@ -24,8 +24,8 @@ import org.springframework.util.ObjectUtils;
/**
* A common key class for a method against a specific target class,
* including {@link #toString()} representation and {@link Comparable}
* support (as suggested for custom {@code HashMap} keys as of Java 8).
* including a {@link #toString()} representation and {@link Comparable}
* support (as suggested for custom {@code HashMap} keys in Java).
*
* @author Juergen Hoeller
* @since 4.3
@@ -45,13 +45,13 @@ import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Helper class that encapsulates the specification of a method parameter, i.e. a {@link Method}
* or {@link Constructor} plus a parameter index and a nested type index for a declared generic
* type. Useful as a specification object to pass along.
* Helper class that encapsulates the specification of a method parameter: a
* {@link Method} or {@link Constructor} plus a parameter index and a nested type
* index for a declared generic type. Useful as a specification object to pass along.
*
* <p>As of 4.2, there is a {@link org.springframework.core.annotation.SynthesizingMethodParameter}
* subclass available which synthesizes annotations with attribute aliases. That subclass is used
* for web and message endpoint processing, in particular.
* <p>There is also a {@link org.springframework.core.annotation.SynthesizingMethodParameter}
* subclass available which synthesizes annotations with attribute aliases. That
* subclass is used for web and message endpoint processing, in particular.
*
* @author Juergen Hoeller
* @author Rob Harrop
@@ -128,7 +128,8 @@ public class MethodParameter {
}
/**
* Create a new MethodParameter for the given constructor, with nesting level 1.
* Create a new {@code MethodParameter} for the given constructor, with nesting
* level 1.
* @param constructor the Constructor to specify a parameter for
* @param parameterIndex the index of the parameter
*/
@@ -137,7 +138,7 @@ public class MethodParameter {
}
/**
* Create a new MethodParameter for the given constructor.
* Create a new {@code MethodParameter} for the given constructor.
* @param constructor the Constructor to specify a parameter for
* @param parameterIndex the index of the parameter
* @param nestingLevel the nesting level of the target type
@@ -152,7 +153,7 @@ public class MethodParameter {
}
/**
* Internal constructor used to create a {@link MethodParameter} with a
* Internal constructor used to create a {@code MethodParameter} with a
* containing class already set.
* @param executable the Executable to specify a parameter for
* @param parameterIndex the index of the parameter
@@ -168,9 +169,9 @@ public class MethodParameter {
}
/**
* Copy constructor, resulting in an independent MethodParameter object
* Copy constructor, resulting in an independent {@code MethodParameter} object
* based on the same metadata and cache state that the original object was in.
* @param original the original MethodParameter object to copy from
* @param original the original {@code MethodParameter} object to copy from
*/
public MethodParameter(MethodParameter original) {
Assert.notNull(original, "Original must not be null");
@@ -182,6 +183,7 @@ public class MethodParameter {
this.containingClass = original.containingClass;
this.parameterType = original.parameterType;
this.genericParameterType = original.genericParameterType;
this.methodAnnotations = original.methodAnnotations;
this.parameterAnnotations = original.parameterAnnotations;
this.parameterNameDiscoverer = original.parameterNameDiscoverer;
this.parameterName = original.parameterName;
@@ -279,7 +281,7 @@ public class MethodParameter {
/**
* Decrease this parameter's nesting level.
* @see #getNestingLevel()
* @deprecated in favor of retaining the original MethodParameter and
* @deprecated in favor of retaining the original {@code MethodParameter} and
* using {@link #nested(Integer)} if nesting is required
*/
@Deprecated(since = "5.2")
@@ -392,10 +394,10 @@ public class MethodParameter {
/**
* Return whether this method indicates a parameter which is not required:
* either in the form of Java 8's {@link java.util.Optional}, JSpecify annotations,
* any variant of a parameter-level {@code @Nullable} annotation (such as from Spring,
* JSR-305 or Jakarta set of annotations), a language-level nullable type
* declaration or {@code Continuation} parameter in Kotlin.
* either in the form of {@link java.util.Optional}, JSpecify annotations,
* any variant of a parameter-level {@code @Nullable} annotation (such as
* from Spring, JSR-305, or Jakarta annotations), or a language-level
* nullable type declaration or {@code Continuation} parameter in Kotlin.
* @since 4.3
* @see Nullness#forMethodParameter(MethodParameter)
*/
@@ -773,9 +775,9 @@ public class MethodParameter {
/**
* Create a new MethodParameter for the given method or constructor.
* <p>This is a convenience factory method for scenarios where a
* Method or Constructor reference is treated in a generic fashion.
* Create a new {@code MethodParameter} for the given method or constructor.
* <p>This is a convenience factory method for scenarios where a {@link Method}
* or {@link Constructor} reference is treated in a generic fashion.
* @param methodOrConstructor the Method or Constructor to specify a parameter for
* @param parameterIndex the index of the parameter
* @return the corresponding MethodParameter instance
@@ -791,9 +793,9 @@ public class MethodParameter {
}
/**
* Create a new MethodParameter for the given method or constructor.
* <p>This is a convenience factory method for scenarios where a
* Method or Constructor reference is treated in a generic fashion.
* Create a new {@code MethodParameter} for the given method or constructor.
* <p>This is a convenience factory method for scenarios where a {@link Method}
* or {@link Constructor} reference is treated in a generic fashion.
* @param executable the Method or Constructor to specify a parameter for
* @param parameterIndex the index of the parameter
* @return the corresponding MethodParameter instance
@@ -812,11 +814,11 @@ public class MethodParameter {
}
/**
* Create a new MethodParameter for the given parameter descriptor.
* <p>This is a convenience factory method for scenarios where a
* Java 8 {@link Parameter} descriptor is already available.
* Create a new {@code MethodParameter} for the given parameter descriptor.
* <p>This is a convenience factory method for scenarios where a {@link Parameter}
* descriptor is already available.
* @param parameter the parameter descriptor
* @return the corresponding MethodParameter instance
* @return the corresponding {@code MethodParameter} instance
* @since 5.0
*/
public static MethodParameter forParameter(Parameter parameter) {
@@ -851,7 +853,7 @@ public class MethodParameter {
}
/**
* Create a new MethodParameter for the given field-aware constructor,
* Create a new {@code MethodParameter} for the given field-aware constructor,
* for example, on a data class or record type.
* <p>A field-aware method parameter will detect field annotations as well,
* as long as the field name matches the parameter name.
@@ -859,7 +861,7 @@ public class MethodParameter {
* @param parameterIndex the index of the parameter
* @param fieldName the name of the underlying field,
* matching the constructor's parameter name
* @return the corresponding MethodParameter instance
* @return the corresponding {@code MethodParameter} instance
* @since 6.1
*/
public static MethodParameter forFieldAwareConstructor(Constructor<?> ctor, int parameterIndex, @Nullable String fieldName) {
@@ -123,13 +123,13 @@ public class ResolvableType implements Serializable {
private @Nullable Class<?> resolved;
private volatile @Nullable ResolvableType superType;
private transient volatile @Nullable ResolvableType superType;
private volatile ResolvableType @Nullable [] interfaces;
private transient volatile ResolvableType @Nullable [] interfaces;
private volatile ResolvableType @Nullable [] generics;
private transient volatile ResolvableType @Nullable [] generics;
private volatile @Nullable Boolean unresolvableGenerics;
private transient volatile @Nullable Boolean unresolvableGenerics;
/**
@@ -23,7 +23,7 @@ import java.lang.reflect.Parameter;
import org.jspecify.annotations.Nullable;
/**
* {@link ParameterNameDiscoverer} implementation which uses JDK 8's reflection facilities
* {@link ParameterNameDiscoverer} implementation which uses Java's reflection facilities
* for introspecting parameter names (based on the "-parameters" compiler flag).
*
* <p>This is a key element of {@link DefaultParameterNameDiscoverer} where it is being
@@ -58,7 +58,7 @@ public class AnnotatedMethod {
private final MethodParameter[] parameters;
private final Map<Class<? extends Annotation>, Object> annotations = new ConcurrentHashMap<>(4);
private final Map<Class<? extends Annotation>, Object> annotations;
private volatile @Nullable List<Annotation[][]> inheritedParameterAnnotations;
@@ -73,6 +73,7 @@ public class AnnotatedMethod {
this.bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
ReflectionUtils.makeAccessible(this.bridgedMethod);
this.parameters = initMethodParameters();
this.annotations = new ConcurrentHashMap<>(4);
}
/**
@@ -83,6 +84,7 @@ public class AnnotatedMethod {
this.method = annotatedMethod.method;
this.bridgedMethod = annotatedMethod.bridgedMethod;
this.parameters = annotatedMethod.parameters;
this.annotations = annotatedMethod.annotations;
this.inheritedParameterAnnotations = annotatedMethod.inheritedParameterAnnotations;
}
@@ -87,7 +87,7 @@ public class SynthesizingMethodParameter extends MethodParameter {
/**
* Copy constructor, resulting in an independent {@code SynthesizingMethodParameter}
* based on the same metadata and cache state that the original object was in.
* @param original the original SynthesizingMethodParameter object to copy from
* @param original the original {@code SynthesizingMethodParameter} object to copy from
*/
protected SynthesizingMethodParameter(SynthesizingMethodParameter original) {
super(original);
@@ -111,12 +111,14 @@ public class SynthesizingMethodParameter extends MethodParameter {
/**
* Create a new SynthesizingMethodParameter for the given method or constructor.
* <p>This is a convenience factory method for scenarios where a
* Method or Constructor reference is treated in a generic fashion.
* @param executable the Method or Constructor to specify a parameter for
* Create a new {@code SynthesizingMethodParameter} for the given method or
* constructor.
* <p>This is a convenience factory method for scenarios where a {@link Method}
* or {@link Constructor} reference is treated in a generic fashion.
* @param executable the {@code Method} or {@code Constructor} to specify a
* parameter for
* @param parameterIndex the index of the parameter
* @return the corresponding SynthesizingMethodParameter instance
* @return the corresponding {@code SynthesizingMethodParameter} instance
* @since 5.0
*/
public static SynthesizingMethodParameter forExecutable(Executable executable, int parameterIndex) {
@@ -132,11 +134,12 @@ public class SynthesizingMethodParameter extends MethodParameter {
}
/**
* Create a new SynthesizingMethodParameter for the given parameter descriptor.
* <p>This is a convenience factory method for scenarios where a
* Java 8 {@link Parameter} descriptor is already available.
* Create a new {@code SynthesizingMethodParameter} for the given parameter
* descriptor.
* <p>This is a convenience factory method for scenarios where a Java
* {@link Parameter} descriptor is already available.
* @param parameter the parameter descriptor
* @return the corresponding SynthesizingMethodParameter instance
* @return the corresponding {@code SynthesizingMethodParameter} instance
* @since 5.0
*/
public static SynthesizingMethodParameter forParameter(Parameter parameter) {
@@ -194,10 +194,11 @@ public class GenericConversionService implements ConfigurableConversionService {
/**
* Template method to convert a {@code null} source.
* <p>The default implementation returns {@code null} or the Java 8
* {@link java.util.Optional#empty()} instance if the target type is
* {@code java.util.Optional}. Subclasses may override this to return
* custom {@code null} objects for specific target types.
* <p>The default implementation returns {@code null} or the
* {@link Optional#empty()} instance if the target type is
* {@code java.util.Optional}.
* <p>Subclasses may override this to return custom {@code null} objects for
* specific target types.
* @param sourceType the source type to convert from
* @param targetType the target type to convert to
* @return the converted null object
@@ -22,7 +22,7 @@ import java.util.TimeZone;
import org.springframework.core.convert.converter.Converter;
/**
* Simple converter from Java 8's {@link java.time.ZoneId} to {@link java.util.TimeZone}.
* Simple converter from Java's {@link java.time.ZoneId} to {@link java.util.TimeZone}.
*
* <p>Note that Spring's default ConversionService setup understands the 'from'/'to' convention
* that the JSR-310 {@code java.time} package consistently uses. That convention is implemented
@@ -23,7 +23,7 @@ import java.util.GregorianCalendar;
import org.springframework.core.convert.converter.Converter;
/**
* Simple converter from Java 8's {@link java.time.ZonedDateTime} to {@link java.util.Calendar}.
* Simple converter from Java's {@link java.time.ZonedDateTime} to {@link java.util.Calendar}.
*
* <p>Note that Spring's default ConversionService setup understands the 'from'/'to' convention
* that the JSR-310 {@code java.time} package consistently uses. That convention is implemented
@@ -24,7 +24,7 @@ import org.apache.commons.logging.LogFactory;
/**
* A convenient accessor for Commons Logging, providing not only
* {@code CharSequence} based log methods but also {@code Supplier}
* based variants for use with Java 8 lambda expressions.
* based variants for use with Java lambda expressions.
*
* @author Juergen Hoeller
* @since 5.2
@@ -140,31 +140,6 @@ public class MimeType implements Comparable<MimeType>, Serializable {
*/
public MimeType(String type, String subtype, Charset charset) {
this(type, subtype, Collections.singletonMap(PARAM_CHARSET, charset.name()));
this.resolvedCharset = charset;
}
/**
* Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType},
* and allows to set the specified character set.
* @param other the other MimeType
* @param charset the character set
* @throws IllegalArgumentException if any of the parameters contains illegal characters
* @since 4.3
*/
public MimeType(MimeType other, Charset charset) {
this(other.getType(), other.getSubtype(), addCharsetParameter(charset, other.getParameters()));
this.resolvedCharset = charset;
}
/**
* Copy-constructor that copies the type and subtype of the given {@code MimeType},
* and allows for different parameter.
* @param other the other MimeType
* @param parameters the parameters (may be {@code null})
* @throws IllegalArgumentException if any of the parameters contains illegal characters
*/
public MimeType(MimeType other, @Nullable Map<String, String> parameters) {
this(other.getType(), other.getSubtype(), parameters);
}
/**
@@ -181,16 +156,43 @@ public class MimeType implements Comparable<MimeType>, Serializable {
checkToken(subtype);
this.type = type.toLowerCase(Locale.ROOT);
this.subtype = subtype.toLowerCase(Locale.ROOT);
if (!CollectionUtils.isEmpty(parameters)) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ROOT);
parameters.forEach((parameter, value) -> {
checkParameters(parameter, value);
map.put(parameter, value);
});
this.parameters = Collections.unmodifiableMap(map);
this.parameters = createParametersMap(parameters);
if (this.parameters.containsKey(PARAM_CHARSET)) {
this.resolvedCharset = Charset.forName(unquote(this.parameters.get(PARAM_CHARSET)));
}
else {
this.parameters = Collections.emptyMap();
}
/**
* Copy-constructor that copies the type, subtype, parameters of the given {@code MimeType},
* and allows to set the specified character set.
* @param other the other MimeType
* @param charset the character set
* @throws IllegalArgumentException if any of the parameters contains illegal characters
* @since 4.3
*/
public MimeType(MimeType other, Charset charset) {
this.type = other.type;
this.subtype = other.subtype;
Map<String, String> map = new LinkedCaseInsensitiveMap<>(other.parameters.size() + 1, Locale.ROOT);
map.putAll(other.parameters);
map.put(PARAM_CHARSET, charset.name());
this.parameters = Collections.unmodifiableMap(map);
this.resolvedCharset = charset;
}
/**
* Copy-constructor that copies the type and subtype of the given {@code MimeType},
* and allows for different parameter.
* @param other the other MimeType
* @param parameters the parameters (may be {@code null})
* @throws IllegalArgumentException if any of the parameters contains illegal characters
*/
public MimeType(MimeType other, @Nullable Map<String, String> parameters) {
this.type = other.type;
this.subtype = other.subtype;
this.parameters = createParametersMap(parameters);
if (this.parameters.containsKey(PARAM_CHARSET)) {
this.resolvedCharset = Charset.forName(unquote(this.parameters.get(PARAM_CHARSET)));
}
}
@@ -223,16 +225,25 @@ public class MimeType implements Comparable<MimeType>, Serializable {
}
}
private Map<String, String> createParametersMap(@Nullable Map<String, String> parameters) {
if (!CollectionUtils.isEmpty(parameters)) {
Map<String, String> map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ROOT);
parameters.forEach((parameter, value) -> {
checkParameters(parameter, value);
map.put(parameter, value);
});
return Collections.unmodifiableMap(map);
}
else {
return Collections.emptyMap();
}
}
protected void checkParameters(String parameter, String value) {
Assert.hasLength(parameter, "'parameter' must not be empty");
Assert.hasLength(value, "'value' must not be empty");
checkToken(parameter);
if (PARAM_CHARSET.equals(parameter)) {
if (this.resolvedCharset == null) {
this.resolvedCharset = Charset.forName(unquote(value));
}
}
else if (!isQuotedString(value)) {
if (!isQuotedString(value)) {
checkToken(value);
}
}
@@ -147,7 +147,7 @@ public abstract class NumberUtils {
else if (number instanceof BigDecimal bigDecimal) {
bigInt = bigDecimal.toBigInteger();
}
// Effectively analogous to JDK 8's BigInteger.longValueExact()
// Effectively analogous to Java's BigInteger.longValueExact()
if (bigInt != null && (bigInt.compareTo(LONG_MIN) < 0 || bigInt.compareTo(LONG_MAX) > 0)) {
raiseOverflowException(number, targetClass);
}
@@ -78,8 +78,8 @@ public abstract class ReflectionUtils {
/**
* Cache for {@link Class#getDeclaredMethods()} plus equivalent default methods
* from Java 8 based interfaces, allowing for fast iteration.
* Cache for {@link Class#getDeclaredMethods()} plus equivalent interface
* default methods, allowing for fast iteration.
*/
private static final Map<Class<?>, Method[]> declaredMethodsCache = new ConcurrentReferenceHashMap<>(256);
@@ -309,7 +309,7 @@ public abstract class ReflectionUtils {
/**
* Perform the given callback operation on all matching methods of the given
* class, as locally declared or equivalent thereof (such as default methods
* on Java 8 based interfaces that the given class implements).
* from interfaces that the given class implements).
* @param clazz the class to introspect
* @param mc the callback to invoke for each method
* @throws IllegalStateException if introspection fails
@@ -444,7 +444,7 @@ public abstract class ReflectionUtils {
/**
* Variant of {@link Class#getDeclaredMethods()} that uses a local cache in
* order to avoid new Method instances. In addition, it also includes Java 8
* order to avoid new {@link Method} instances. In addition, it also includes
* default methods from locally implemented interfaces, since those are
* effectively to be treated just like declared methods.
* @param clazz the class to introspect
@@ -231,8 +231,7 @@ class BridgeMethodResolverTests {
}
}
assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue();
boolean condition = bridgedMethod != null && !bridgedMethod.isBridge();
assertThat(condition).isTrue();
assertThat(bridgedMethod != null && !bridgedMethod.isBridge()).isTrue();
assertThat(BridgeMethodResolver.findBridgedMethod(bridgeMethod)).isEqualTo(bridgedMethod);
}
@@ -1335,6 +1335,22 @@ class ResolvableTypeTests {
assertThat(deserializedNone).isSameAs(ResolvableType.NONE);
}
@Test
void serializeWithCachedState() throws Exception {
ResolvableType type = ResolvableType.forClass(List.class);
testSerialization(type);
type.getSuperType();
type.getInterfaces();
type.getGenerics();
type.hasUnresolvableGenerics();
testSerialization(type);
type.getSuperType();
type.getInterfaces();
type.getGenerics();
type.hasUnresolvableGenerics();
testSerialization(type);
}
@Test
void canResolveVoid() {
ResolvableType type = ResolvableType.forClass(void.class);
@@ -133,7 +133,7 @@ class SortedPropertiesTests {
String[] lines = lines(baos);
assertThat(lines).isNotEmpty();
// Leniently match first line due to differences between JDK 8 and JDK 9+.
// Leniently match first line due to potential differences between JDK versions.
String regex = "<\\?xml .*\\?>";
assertThat(lines[0]).matches(regex);
assertThat(lines).filteredOn(line -> !line.matches(regex)).containsExactly( //
@@ -160,10 +160,11 @@ class AnnotationUtilsTests {
assertThat(getAnnotation(bridgeMethod, Order.class)).isNull();
assertThat(findAnnotation(bridgeMethod, Order.class)).isNotNull();
// As of JDK 8, invoking getAnnotation() on a bridge method actually finds an
// annotation on its 'bridged' method [1]; however, the Eclipse compiler does
// not support this [2]. Thus, we effectively ignore the following
// assertion if the test is currently executing within the Eclipse IDE.
// For code compiled with OpenJDK, invoking getAnnotation() on a bridge
// method actually finds an annotation on its 'bridged' method [1]; however,
// the Eclipse compiler does not support this [2]. Thus, we effectively
// ignore the following assertion if the test is currently executing within
// the Eclipse IDE.
//
// [1] https://bugs.openjdk.java.net/browse/JDK-6695379
// [2] https://bugs.eclipse.org/bugs/show_bug.cgi?id=495396
@@ -576,7 +577,7 @@ class AnnotationUtilsTests {
final List<String> expectedValuesJava = asList("A", "B", "C");
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
// Java 8
// Java
MyRepeatable[] array = MyRepeatableClass.class.getAnnotationsByType(MyRepeatable.class);
assertThat(array).isNotNull();
List<String> values = stream(array).map(MyRepeatable::value).collect(toList());
@@ -601,7 +602,7 @@ class AnnotationUtilsTests {
final List<String> expectedValuesJava = asList("A", "B", "C");
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
// Java 8
// Java
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
assertThat(array).isNotNull();
List<String> values = stream(array).map(MyRepeatable::value).collect(toList());
@@ -626,7 +627,7 @@ class AnnotationUtilsTests {
final List<String> expectedValuesJava = asList("X", "Y", "Z");
final List<String> expectedValuesSpring = asList("X", "Y", "Z", "meta2");
// Java 8
// Java
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
assertThat(array).isNotNull();
List<String> values = stream(array).map(MyRepeatable::value).collect(toList());
@@ -651,7 +652,7 @@ class AnnotationUtilsTests {
final List<String> expectedValuesJava = asList("X", "Y", "Z");
final List<String> expectedValuesSpring = asList("X", "Y", "Z", "meta2");
// Java 8
// Java
MyRepeatable[] array = clazz.getAnnotationsByType(MyRepeatable.class);
assertThat(array).isNotNull();
List<String> values = stream(array).map(MyRepeatable::value).collect(toList());
@@ -675,7 +676,7 @@ class AnnotationUtilsTests {
final List<String> expectedValuesJava = asList("A", "B", "C");
final List<String> expectedValuesSpring = asList("A", "B", "C", "meta1");
// Java 8
// Java
MyRepeatable[] array = MyRepeatableClass.class.getDeclaredAnnotationsByType(MyRepeatable.class);
assertThat(array).isNotNull();
List<String> values = stream(array).map(MyRepeatable::value).collect(toList());
@@ -699,7 +700,7 @@ class AnnotationUtilsTests {
void getDeclaredRepeatableAnnotationsDeclaredOnSuperclass() {
final Class<?> clazz = SubMyRepeatableClass.class;
// Java 8
// Java
MyRepeatable[] array = clazz.getDeclaredAnnotationsByType(MyRepeatable.class);
assertThat(array).isNotNull();
assertThat(array).isEmpty();
@@ -893,10 +893,11 @@ class MergedAnnotationsTests {
assertThat(MergedAnnotations.from(method).get(Order.class).getDistance()).isEqualTo(-1);
assertThat(MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY).get(
Order.class).getDistance()).isEqualTo(0);
// As of JDK 8, invoking getAnnotation() on a bridge method actually finds an
// annotation on its 'bridged' method [1]; however, the Eclipse compiler does
// not support this [2]. Thus, we effectively ignore the following
// assertion if the test is currently executing within the Eclipse IDE.
// For code compiled with OpenJDK, invoking getAnnotation() on a bridge
// method actually finds an annotation on its 'bridged' method [1]; however,
// the Eclipse compiler does not support this [2]. Thus, we effectively
// ignore the following assertion if the test is currently executing within
// the Eclipse IDE.
//
// [1] https://bugs.openjdk.java.net/browse/JDK-6695379
// [2] https://bugs.eclipse.org/bugs/show_bug.cgi?id=495396
@@ -105,7 +105,7 @@ class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {
}
@Test
@Disabled("Disabled since some Java 8 updates handle the bridge method differently")
@Disabled("Disabled since some Java versions/compilers handle the bridge method differently")
void getMultipleComposedAnnotationsOnBridgeMethod() {
Set<Cacheable> cacheables = getAllMergedAnnotations(getBridgeMethod(), Cacheable.class);
assertThat(cacheables).isNotNull();
@@ -202,8 +202,7 @@ class MultipleComposedAnnotationsOnSingleAnnotatedElementTests {
}
}
assertThat(bridgeMethod != null && bridgeMethod.isBridge()).isTrue();
boolean condition = bridgedMethod != null && !bridgedMethod.isBridge();
assertThat(condition).isTrue();
assertThat(bridgedMethod != null && !bridgedMethod.isBridge()).isTrue();
return bridgeMethod;
}
@@ -957,20 +957,22 @@ public class JmsTemplate extends JmsDestinationAccessor implements JmsOperations
try {
Message requestMessage = messageCreator.createMessage(session);
producer = session.createProducer(destination);
if (!useCorrelationId) {
consumer = session.createConsumer(responseQueue);
}
requestMessage.setJMSReplyTo(responseQueue);
if (logger.isDebugEnabled()) {
logger.debug("Sending created message: " + requestMessage);
}
doSend(producer, requestMessage);
String messageSelector = null;
if (useCorrelationId) {
if (consumer == null) { // useCorrelationId=true
String correlationId = requestMessage.getJMSCorrelationID();
if (correlationId == null) {
correlationId = requestMessage.getJMSMessageID();
}
messageSelector = "JMSCorrelationID='" + correlationId + "'";
String messageSelector = "JMSCorrelationID='" + correlationId + "'";
consumer = session.createConsumer(responseQueue, messageSelector);
}
consumer = session.createConsumer(responseQueue, messageSelector);
return receiveFromConsumer(consumer, getReceiveTimeout());
}
finally {
@@ -648,7 +648,7 @@ class JmsTemplateTests {
given(localSession.createTemporaryQueue()).willReturn(replyDestination);
MessageConsumer messageConsumer = mock();
given(localSession.createConsumer(replyDestination, null)).willReturn(messageConsumer);
given(localSession.createConsumer(replyDestination)).willReturn(messageConsumer);
TextMessage request = mock();
MessageCreator messageCreator = mock();
@@ -208,7 +208,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
return handleShuttingDownConnectFailure(handler);
}
// Report first connect to the ListenableFuture
// Report first connect to the CompletableFuture
CompletableFuture<@Nullable Void> connectFuture = new CompletableFuture<>();
extendTcpClient(this.tcpClient, handler)
@@ -27,7 +27,7 @@ import java.lang.annotation.Target;
* Container annotation that aggregates several {@link Sql @Sql} annotations.
*
* <p>Can be used natively, declaring several nested {@code @Sql} annotations.
* Can also be used in conjunction with Java 8's support for repeatable
* Can also be used in conjunction with Java's support for repeatable
* annotations, where {@code @Sql} can simply be declared several times on the
* same class or method, implicitly generating this container annotation.
*
@@ -100,9 +100,8 @@ import org.springframework.util.StringUtils;
* execute certain <em>set up</em> or <em>tear down</em> code outside a
* transaction. {@code TransactionalTestExecutionListener} provides such
* support for methods annotated with {@link BeforeTransaction @BeforeTransaction}
* or {@link AfterTransaction @AfterTransaction}. As of Spring Framework 4.3,
* {@code @BeforeTransaction} and {@code @AfterTransaction} may also be declared
* on Java 8 based interface default methods.
* or {@link AfterTransaction @AfterTransaction}. {@code @BeforeTransaction} and
* {@code @AfterTransaction} may also be declared on interface default methods.
*
* <h3>Configuring a Transaction Manager</h3>
* <p>{@code TransactionalTestExecutionListener} expects a
@@ -151,7 +151,7 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
* Mainly intended for code that wants to set the current transaction
* rollback-only but not throw an application exception.
* <p>This exposes the locally declared transaction boundary with its declared name
* and characteristics, as managed by the aspect. Ar runtime, the local boundary may
* and characteristics, as managed by the aspect. At runtime, the local boundary may
* participate in an outer transaction: If you need transaction metadata from such
* an outer transaction (the actual resource transaction) instead, consider using
* {@link org.springframework.transaction.support.TransactionSynchronizationManager}.
@@ -54,8 +54,8 @@ public interface TransactionOperations {
* {@link org.springframework.transaction.TransactionStatus} from within the callback,
* use {@link #execute(TransactionCallback)} instead.
* <p>This variant is analogous to using a {@link TransactionCallbackWithoutResult}
* but with a simplified signature for common cases - and conveniently usable with
* Java 8 lambda expressions.
* but with a simplified signature for common cases and conveniently usable with
* lambda expressions.
* @param action the Runnable that specifies the transactional action
* @throws TransactionException in case of initialization, rollback, or system errors
* @throws RuntimeException if thrown by the Runnable
@@ -491,7 +491,12 @@ public class HttpHeaders implements Serializable {
*/
public static HttpHeaders copyOf(MultiValueMap<String, String> headers) {
HttpHeaders httpHeadersCopy = new HttpHeaders();
headers.forEach((key, values) -> httpHeadersCopy.put(key, new ArrayList<>(values)));
for (String name : headers.keySet()) {
List<String> values = headers.get(name);
if (values != null) {
httpHeadersCopy.put(name, new ArrayList<>(values));
}
}
return httpHeadersCopy;
}
@@ -1984,7 +1989,9 @@ public class HttpHeaders implements Serializable {
* @see #put(String, List)
*/
public void putAll(Map<? extends String, ? extends List<String>> headers) {
headers.forEach(this::put);
for (String name : headers.keySet()) {
put(name, headers.get(name));
}
}
/**
@@ -2181,6 +2188,7 @@ public class HttpHeaders implements Serializable {
private static final Object VALUE = new Object();
private final MultiValueMap<String, String> headers;
private final Map<String, Object> deduplicatedNames;
public CaseInsensitiveHeaderNameSet(MultiValueMap<String, String> headers) {
@@ -2222,13 +2230,15 @@ public class HttpHeaders implements Serializable {
}
}
private static class HeaderNamesIterator implements Iterator<String> {
private @Nullable String currentName;
private final MultiValueMap<String, String> headers;
private final Iterator<String> namesIterator;
private @Nullable String currentName;
public HeaderNamesIterator(MultiValueMap<String, String> headers, Map<String, Object> caseInsensitiveNames) {
this.headers = headers;
this.namesIterator = caseInsensitiveNames.keySet().iterator();
@@ -2262,6 +2272,7 @@ public class HttpHeaders implements Serializable {
private static final class CaseInsensitiveEntrySet extends AbstractSet<Entry<String, List<String>>> {
private final MultiValueMap<String, String> headers;
private final CaseInsensitiveHeaderNameSet nameSet;
public CaseInsensitiveEntrySet(MultiValueMap<String, String> headers) {
@@ -2279,6 +2290,7 @@ public class HttpHeaders implements Serializable {
return this.nameSet.size();
}
private final class CaseInsensitiveIterator implements Iterator<Entry<String, List<String>>> {
private final Iterator<String> namesIterator;
@@ -2303,6 +2315,7 @@ public class HttpHeaders implements Serializable {
}
}
private final class CaseInsensitiveEntry implements Entry<String, List<String>> {
private final String key;
@@ -2323,26 +2336,21 @@ public class HttpHeaders implements Serializable {
@Override
public List<String> setValue(List<String> value) {
List<String> previousValues = Objects.requireNonNull(
CaseInsensitiveEntrySet.this.headers.get(this.key));
List<String> previous = Objects.requireNonNull(CaseInsensitiveEntrySet.this.headers.get(this.key));
CaseInsensitiveEntrySet.this.headers.put(this.key, value);
return previousValues;
return previous;
}
@Override
public boolean equals(@Nullable Object o) {
if (this == o) {
return true;
}
if (!(o instanceof Map.Entry<?,?> that)) {
return false;
}
return ObjectUtils.nullSafeEquals(getKey(), that.getKey()) && ObjectUtils.nullSafeEquals(getValue(), that.getValue());
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof Map.Entry<?, ?> that &&
ObjectUtils.nullSafeEquals(getKey(), that.getKey()) &&
ObjectUtils.nullSafeEquals(getValue(), that.getValue())));
}
@Override
public int hashCode() {
return ObjectUtils.nullSafeHash(getKey(), getValue());
return this.key.hashCode(); // avoid value lookup for hashCode
}
}
}
@@ -458,7 +458,7 @@ public class MediaType extends MimeType implements Serializable {
* @throws IllegalArgumentException if any of the parameters contain illegal characters
*/
public MediaType(MediaType other, @Nullable Map<String, String> parameters) {
super(other.getType(), other.getSubtype(), parameters);
super(other, parameters);
}
/**
@@ -181,7 +181,10 @@ class ReadOnlyHttpHeaders extends HttpHeaders {
@Override
public void forEach(BiConsumer<? super String, ? super List<String>> action) {
this.headers.forEach((k, vs) -> action.accept(k, Collections.unmodifiableList(vs)));
for (String name : this.headers.keySet()) {
List<String> values = this.headers.get(name);
action.accept(name, (values != null ? Collections.unmodifiableList(values) : Collections.emptyList()));
}
}
}
@@ -100,7 +100,8 @@ final class PartGenerator extends BaseSubscriber<MultipartParser.Token> {
sink.onCancel(generator);
sink.onRequest(l -> generator.requestToken());
tokens.subscribe(generator);
tokens.doOnDiscard(MultipartParser.BodyToken.class, bodyToken -> DataBufferUtils.release(bodyToken.buffer()))
.subscribe(generator);
});
}
@@ -252,13 +252,14 @@ public abstract class AbstractHttpMessageConverter<T> implements HttpMessageConv
contentTypeToUse = (mediaType != null ? mediaType : contentTypeToUse);
}
if (contentTypeToUse != null) {
String value = contentTypeToUse.toString();
if (contentTypeToUse.getCharset() == null) {
Charset defaultCharset = getDefaultCharset();
if (defaultCharset != null) {
contentTypeToUse = new MediaType(contentTypeToUse, defaultCharset);
value += ";charset=" + defaultCharset.name();
}
}
headers.setContentType(contentTypeToUse);
headers.set(HttpHeaders.CONTENT_TYPE, value);
}
}
if (headers.getContentLength() < 0 && !headers.containsHeader(HttpHeaders.TRANSFER_ENCODING)) {
@@ -225,12 +225,12 @@ class DefaultHttpMessageConverters implements HttpMessageConverters {
}
void addMessageConverterConfigurer(Consumer<HttpMessageConverter<?>> configurer) {
this.configurer = (this.configurer != null) ? configurer.andThen(this.configurer) : configurer;
this.configurer = (this.configurer != null) ? this.configurer.andThen(configurer) : configurer;
}
void addMessageConvertersListConfigurer(Consumer<List<HttpMessageConverter<?>>> configurer) {
this.convertersListConfigurer = (this.convertersListConfigurer != null) ?
this.convertersListConfigurer.andThen(this.convertersListConfigurer) : configurer;
this.convertersListConfigurer.andThen(configurer) : configurer;
}
List<HttpMessageConverter<?>> getBaseConverters() {
@@ -83,9 +83,9 @@ import org.springframework.util.xml.StaxUtils;
* detected on the classpath:
* <ul>
* <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>:
* support for other Java 8 types like {@link java.util.Optional}</li>
* support for Java 8 types like {@link java.util.Optional}</li>
* <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>:
* support for Java 8 Date &amp; Time API types</li>
* support for Java Date &amp; Time API types</li>
* <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>:
* support for Kotlin classes and data classes</li>
* <li><a href="https://github.com/FasterXML/jackson-modules-java8/tree/2.18/parameter-names">jackson-modules-java8/parameter-names</a>:
@@ -114,9 +114,9 @@ import org.springframework.context.ApplicationContextAware;
* <li><a href="https://github.com/FasterXML/jackson-datatype-jdk7">jackson-datatype-jdk7</a>:
* support for Java 7 types like {@link java.nio.file.Path}</li>
* <li><a href="https://github.com/FasterXML/jackson-datatype-jdk8">jackson-datatype-jdk8</a>:
* support for other Java 8 types like {@link java.util.Optional}</li>
* support for Java 8 types like {@link java.util.Optional}</li>
* <li><a href="https://github.com/FasterXML/jackson-datatype-jsr310">jackson-datatype-jsr310</a>:
* support for Java 8 Date &amp; Time API types</li>
* support for Java Date &amp; Time API types</li>
* <li><a href="https://github.com/FasterXML/jackson-module-kotlin">jackson-module-kotlin</a>:
* support for Kotlin classes and data classes</li>
* </ul>
@@ -0,0 +1,431 @@
/*
* 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.server;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedCaseInsensitiveMap;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@code MultiValueMap} implementation for wrapping Servlet request headers.
*
* @author Rossen Stoyanchev
* @since 7.0.5
*/
final class ServletRequestHeadersAdapter implements MultiValueMap<String, String> {
private final HttpServletRequest request;
private ServletRequestHeadersAdapter(HttpServletRequest request) {
this.request = request;
}
@Override
public @Nullable String getFirst(String key) {
return this.request.getHeader(key);
}
@Override
public void add(String key, @Nullable String value) {
throw immutableRequestException();
}
@Override
public void addAll(String key, List<? extends String> values) {
throw immutableRequestException();
}
@Override
public void addAll(MultiValueMap<String, String> map) {
throw httpHeadersMapException();
}
@Override
public void set(String key, @Nullable String value) {
throw immutableRequestException();
}
@Override
public void setAll(Map<String, String> map) {
throw immutableRequestException();
}
@Override
public Map<String, String> toSingleValueMap() {
Map<String, String> map = new LinkedHashMap<>();
Enumeration<String> names = this.request.getHeaderNames();
while (names.hasMoreElements()) {
String name = names.nextElement();
map.put(name, this.request.getHeader(name));
}
return map;
}
@Override
public int size() {
return keySet().size();
}
@Override
public boolean isEmpty() {
return !this.request.getHeaderNames().hasMoreElements();
}
@Override
public boolean containsKey(Object key) {
if (key instanceof String headerName) {
Enumeration<String> names = this.request.getHeaderNames();
while (names.hasMoreElements()) {
if (headerName.equalsIgnoreCase(names.nextElement())) {
return true;
}
}
}
return false;
}
@Override
public boolean containsValue(Object rawValue) {
throw httpHeadersMapException();
}
@Override
public @Nullable List<String> get(Object key) {
if (key instanceof String headerName) {
Enumeration<String> values = this.request.getHeaders(headerName);
if (values.hasMoreElements()) {
String value = values.nextElement();
if (!values.hasMoreElements()) {
return Collections.singletonList(value);
}
List<String> result = new ArrayList<>(4);
result.add(value);
while (values.hasMoreElements()) {
result.add(values.nextElement());
}
return result;
}
}
return null;
}
@Override
public @Nullable List<String> put(String key, List<String> value) {
throw immutableRequestException();
}
@Override
public @Nullable List<String> remove(Object key) {
throw immutableRequestException();
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> map) {
throw httpHeadersMapException();
}
@Override
public void clear() {
throw immutableRequestException();
}
@Override
public Set<String> keySet() {
return new HeaderNames();
}
@Override
public Collection<List<String>> values() {
throw httpHeadersMapException();
}
@Override
public Set<Entry<String, List<String>>> entrySet() {
throw httpHeadersMapException();
}
private static UnsupportedOperationException immutableRequestException() {
return new UnsupportedOperationException("Request headers are immutable");
}
private static UnsupportedOperationException httpHeadersMapException() {
return new UnsupportedOperationException("HttpHeaders does not support all Map operations");
}
@Override
public String toString() {
return HttpHeaders.formatHeaders(this);
}
/**
* Factory method to create a Servlet request headers adapter.
* @param request the request to access headers from
* @return the created adapter instance
*/
static MultiValueMap<String, String> create(HttpServletRequest request) {
return new RequestHeaderOverrideWrapper(new ServletRequestHeadersAdapter(request));
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(request.getHeaderNames());
}
@Override
public int size() {
Enumeration<String> names = request.getHeaderNames();
int size = 0;
while (names.hasMoreElements()) {
names.nextElement();
size++;
}
return size;
}
}
private static final class HeaderNamesIterator implements Iterator<String> {
private final Enumeration<String> enumeration;
private HeaderNamesIterator(Enumeration<String> enumeration) {
this.enumeration = enumeration;
}
@Override
public boolean hasNext() {
return this.enumeration.hasMoreElements();
}
@Override
public String next() {
return this.enumeration.nextElement();
}
@Override
public void remove() {
throw immutableRequestException();
}
}
/**
* Wrapper that holds override values.
*/
private static class RequestHeaderOverrideWrapper implements MultiValueMap<String, String> {
private final MultiValueMap<String, String> delegate;
private @Nullable MultiValueMap<String, String> overrideMap;
RequestHeaderOverrideWrapper(MultiValueMap<String, String> delegate) {
this.delegate = delegate;
}
@Override
public @Nullable String getFirst(String key) {
String value = (this.overrideMap != null ? this.overrideMap.getFirst(key) : null);
return (value != null ? value : this.delegate.getFirst(key));
}
@Override
public void add(String key, @Nullable String value) {
initOverrideMap().add(key, value);
}
@Override
public void addAll(String key, List<? extends String> values) {
initOverrideMap().addAll(key, values);
}
@Override
public void addAll(MultiValueMap<String, String> map) {
throw httpHeadersMapException();
}
@Override
public void set(String key, @Nullable String value) {
initOverrideMap().set(key, value);
}
@Override
public void setAll(Map<String, String> map) {
initOverrideMap().setAll(map);
}
@Override
public Map<String, String> toSingleValueMap() {
Map<String, String> map = this.delegate.toSingleValueMap();
if (this.overrideMap != null) {
this.overrideMap.forEach((key, values) -> map.put(key, values.get(0)));
}
return map;
}
@Override
public int size() {
if (this.overrideMap == null) {
return this.delegate.size();
}
Set<String> set = new LinkedHashSet<>();
for (String name : this.delegate.keySet()) {
set.add(name.toLowerCase(Locale.ROOT));
}
this.overrideMap.keySet().forEach(key -> set.add(key.toLowerCase(Locale.ROOT)));
return set.size();
}
@Override
public boolean isEmpty() {
return (this.delegate.isEmpty() && (this.overrideMap == null || this.overrideMap.isEmpty()));
}
@Override
public boolean containsKey(Object key) {
if (key instanceof String headerName) {
if (this.delegate.containsKey(headerName)) {
return true;
}
if (this.overrideMap != null) {
return this.overrideMap.containsKey(headerName);
}
}
return false;
}
@Override
public boolean containsValue(Object rawValue) {
throw httpHeadersMapException();
}
@Override
public @Nullable List<String> get(Object key) {
if (key instanceof String headerName) {
if (this.overrideMap != null) {
List<String> values = this.overrideMap.get(headerName);
if (values != null) {
return values;
}
}
return this.delegate.get(headerName);
}
return null;
}
@Override
public @Nullable List<String> put(String key, List<String> value) {
return initOverrideMap().put(key, value);
}
@Override
public @Nullable List<String> remove(Object key) {
return initOverrideMap().remove(key);
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> map) {
throw httpHeadersMapException();
}
@Override
public void clear() {
if (this.overrideMap != null) {
this.overrideMap.clear();
}
}
@Override
public Set<String> keySet() {
if (this.overrideMap != null) {
Set<String> set = new LinkedHashSet<>(this.delegate.keySet());
set.addAll(this.overrideMap.keySet());
return set;
}
return this.delegate.keySet();
}
@Override
public Collection<List<String>> values() {
throw httpHeadersMapException();
}
@Override
public Set<Entry<String, List<String>>> entrySet() {
throw httpHeadersMapException();
}
private MultiValueMap<String, String> initOverrideMap() {
if (this.overrideMap == null) {
this.overrideMap = CollectionUtils.toMultiValueMap(new LinkedCaseInsensitiveMap<>(8, Locale.ROOT));
}
return this.overrideMap;
}
@Override
public int hashCode() {
return toMultiValueMap().hashCode();
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof MultiValueMap<?,?> that && toMultiValueMap().equals(that)));
}
private MultiValueMap<String, String> toMultiValueMap() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
for (String name : keySet()) {
List<String> values = get(name);
if (values != null) {
for (String value : values) {
map.add(name, value);
}
}
}
return map;
}
@Override
public String toString() {
return HttpHeaders.formatHeaders(this);
}
}
}
@@ -0,0 +1,255 @@
/*
* 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.server;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* {@code MultiValueMap} implementation for wrapping Servlet response headers.
*
* @author Rossen Stoyanchev
* @since 7.0.5
*/
class ServletResponseHeadersAdapter implements MultiValueMap<String, String> {
private final HttpServletResponse response;
ServletResponseHeadersAdapter(HttpServletResponse response) {
this.response = response;
}
@Override
public @Nullable String getFirst(String key) {
String header = this.response.getHeader(key);
if (header == null && key.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE)) {
header = this.response.getContentType();
}
return header;
}
@Override
public void add(String key, @Nullable String value) {
this.response.addHeader(key, value);
}
@Override
public void addAll(String key, List<? extends String> values) {
for (String value : values) {
this.response.addHeader(key, value);
}
}
@Override
public void addAll(MultiValueMap<String, String> map) {
throw httpHeadersUnsupportedOperationException();
}
@Override
public void set(String key, @Nullable String value) {
this.response.setHeader(key, value);
}
@Override
public void setAll(Map<String, String> map) {
for (Entry<String, String> entry : map.entrySet()) {
this.response.setHeader(entry.getKey(), entry.getValue());
}
}
@Override
public Map<String, String> toSingleValueMap() {
Map<String, String> map = new LinkedHashMap<>();
Collection<String> names = this.response.getHeaderNames();
for (String name : names) {
map.put(name, this.response.getHeader(name));
}
return map;
}
@Override
public int size() {
return this.response.getHeaderNames().size();
}
@Override
public boolean isEmpty() {
return this.response.getHeaderNames().isEmpty();
}
@Override
public boolean containsKey(Object key) {
if (key instanceof String headerName) {
return this.response.containsHeader(headerName);
}
return false;
}
@Override
public boolean containsValue(Object rawValue) {
throw httpHeadersUnsupportedOperationException();
}
@Override
public @Nullable List<String> get(Object key) {
if (key instanceof String headerName) {
Collection<String> values = this.response.getHeaders(headerName);
if (values.isEmpty() && headerName.equalsIgnoreCase(HttpHeaders.CONTENT_TYPE)) {
String contentType = this.response.getContentType();
return (contentType != null ? Collections.singletonList(contentType) : null);
}
if (!values.isEmpty()) {
return new ArrayList<>(values);
}
}
return null;
}
@Override
public @Nullable List<String> put(String key, List<String> values) {
List<String> previous = remove(key);
for (String value : values) {
this.response.addHeader(key, value);
}
return previous;
}
@Override
public @Nullable List<String> remove(Object key) {
if (key instanceof String headerName) {
List<String> previous = get(headerName);
if (previous != null) {
this.response.setHeader(headerName, null);
}
return previous;
}
return null;
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> map) {
throw httpHeadersUnsupportedOperationException();
}
@Override
public void clear() {
for (String headerName : this.response.getHeaderNames()) {
this.response.setHeader(headerName, null);
}
}
@Override
public Set<String> keySet() {
return new HeaderNames();
}
@Override
public Collection<List<String>> values() {
throw httpHeadersUnsupportedOperationException();
}
@Override
public Set<Entry<String, List<String>>> entrySet() {
throw httpHeadersUnsupportedOperationException();
}
private static UnsupportedOperationException httpHeadersUnsupportedOperationException() {
return new UnsupportedOperationException("HttpHeaders does not support all Map operations");
}
@Override
public int hashCode() {
return toMultiValueMap().hashCode();
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof MultiValueMap<?,?> that && toMultiValueMap().equals(that)));
}
private MultiValueMap<String, String> toMultiValueMap() {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
for (String name : this.response.getHeaderNames()) {
for (String value : this.response.getHeaders(name)) {
map.add(name, value);
}
}
return map;
}
@Override
public String toString() {
return HttpHeaders.formatHeaders(this);
}
private class HeaderNames extends AbstractSet<String> {
@Override
public Iterator<String> iterator() {
return new HeaderNamesIterator(response.getHeaderNames());
}
@Override
public int size() {
return ServletResponseHeadersAdapter.this.size();
}
}
private static final class HeaderNamesIterator implements Iterator<String> {
private final Iterator<String> values;
private HeaderNamesIterator(Collection<String> values) {
this.values = values.iterator();
}
@Override
public boolean hasNext() {
return this.values.hasNext();
}
@Override
public String next() {
return this.values.next();
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
}
}
@@ -156,16 +156,7 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
@Override
public HttpHeaders getHeaders() {
if (this.headers == null) {
this.headers = new HttpHeaders();
for (Enumeration<?> names = this.servletRequest.getHeaderNames(); names.hasMoreElements();) {
String headerName = (String) names.nextElement();
for (Enumeration<?> headerValues = this.servletRequest.getHeaders(headerName);
headerValues.hasMoreElements();) {
String headerValue = (String) headerValues.nextElement();
this.headers.add(headerName, headerValue);
}
}
this.headers = new HttpHeaders(ServletRequestHeadersAdapter.create(this.servletRequest));
// HttpServletRequest exposes some headers as properties:
// we should include those if not already present
@@ -183,10 +174,10 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
if (contentType != null && contentType.getCharset() == null) {
String requestEncoding = this.servletRequest.getCharacterEncoding();
if (StringUtils.hasLength(requestEncoding)) {
Charset charSet = Charset.forName(requestEncoding);
Charset charset = Charset.forName(requestEncoding);
Map<String, String> params = new LinkedCaseInsensitiveMap<>();
params.putAll(contentType.getParameters());
params.put("charset", charSet.toString());
params.put("charset", charset.toString());
MediaType mediaType = new MediaType(contentType.getType(), contentType.getSubtype(), params);
this.headers.setContentType(mediaType);
}
@@ -203,11 +194,9 @@ public class ServletServerHttpRequest implements ServerHttpRequest {
}
}
}
return this.headers;
}
@Override
public @Nullable Principal getPrincipal() {
return this.servletRequest.getUserPrincipal();
}
@@ -18,10 +18,7 @@ package org.springframework.http.server;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.nio.charset.Charset;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.Nullable;
@@ -30,7 +27,6 @@ import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* {@link ServerHttpResponse} implementation that is based on a {@link HttpServletResponse}.
@@ -59,7 +55,7 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
public ServletServerHttpResponse(HttpServletResponse servletResponse) {
Assert.notNull(servletResponse, "HttpServletResponse must not be null");
this.servletResponse = servletResponse;
this.headers = new ServletResponseHttpHeaders();
this.headers = new HttpHeaders(new ServletResponseHeadersAdapter(servletResponse));
}
@@ -112,92 +108,27 @@ public class ServletServerHttpResponse implements ServerHttpResponse {
private void writeHeaders() {
if (!this.headersWritten) {
getHeaders().forEach((headerName, headerValues) -> {
for (String headerValue : headerValues) {
this.servletResponse.addHeader(headerName, headerValue);
}
});
// HttpServletResponse exposes some headers as properties: we should include those if not already present
MediaType contentTypeHeader = this.headers.getContentType();
if (this.servletResponse.getContentType() == null && contentTypeHeader != null) {
this.servletResponse.setContentType(contentTypeHeader.toString());
if (this.servletResponse.getContentType() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) {
this.servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
}
if (this.servletResponse.getCharacterEncoding() == null && contentTypeHeader != null &&
contentTypeHeader.getCharset() != null) {
this.servletResponse.setCharacterEncoding(contentTypeHeader.getCharset().name());
}
long contentLength = getHeaders().getContentLength();
if (contentLength != -1) {
this.servletResponse.setContentLengthLong(contentLength);
if (this.servletResponse.getCharacterEncoding() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) {
try {
// Lazy parsing into MediaType
MediaType contentType = this.headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
this.servletResponse.setCharacterEncoding(charset);
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
this.headersWritten = true;
}
}
/**
* Extends HttpHeaders with the ability to look up headers already present in
* the underlying HttpServletResponse.
*
* <p>The intent is merely to expose what is available through the HttpServletResponse
* i.e. the ability to look up specific header values by name. All other
* map-related operations (for example, iteration, removal, etc) apply only to values
* added directly through HttpHeaders methods.
*
* @since 4.0.3
*/
private class ServletResponseHttpHeaders extends HttpHeaders {
private static final long serialVersionUID = 3410708522401046302L;
@Override
public boolean containsHeader(String key) {
return (super.containsHeader(key) || (get(key) != null));
}
@Override
public @Nullable String getFirst(String headerName) {
if (headerName.equalsIgnoreCase(CONTENT_TYPE)) {
// Content-Type is written as an override so check super first
String value = super.getFirst(headerName);
return (value != null ? value : servletResponse.getContentType());
}
else {
String value = servletResponse.getHeader(headerName);
return (value != null ? value : super.getFirst(headerName));
}
}
@Override
public @Nullable List<String> get(String headerName) {
if (headerName.equalsIgnoreCase(CONTENT_TYPE)) {
// Content-Type is written as an override so don't merge
String value = getFirst(headerName);
return (value != null ? Collections.singletonList(value) : null);
}
Collection<String> values1 = servletResponse.getHeaders(headerName);
if (headersWritten) {
return new ArrayList<>(values1);
}
boolean isEmpty1 = CollectionUtils.isEmpty(values1);
List<String> values2 = super.get(headerName);
boolean isEmpty2 = CollectionUtils.isEmpty(values2);
if (isEmpty1 && isEmpty2) {
return null;
}
List<String> values = new ArrayList<>();
if (!isEmpty1) {
values.addAll(values1);
}
if (!isEmpty2) {
values.addAll(values2);
}
return values;
}
}
}
@@ -122,31 +122,35 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
}
protected void adaptHeaders(boolean removeAdaptedHeaders) {
MediaType contentType = null;
try {
contentType = getHeaders().getContentType();
}
catch (Exception ex) {
String rawContentType = getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
this.response.setContentType(rawContentType);
}
if (this.response.getContentType() == null && contentType != null) {
this.response.setContentType(contentType.toString());
}
HttpHeaders headers = getHeaders();
Charset charset = (contentType != null ? contentType.getCharset() : null);
if (this.response.getCharacterEncoding() == null && charset != null) {
this.response.setCharacterEncoding(charset.name());
// HttpServletResponse exposes some headers as properties: we should include those if not already present
if (this.response.getContentType() == null && headers.containsHeader(HttpHeaders.CONTENT_TYPE)) {
this.response.setContentType(headers.getFirst(HttpHeaders.CONTENT_TYPE));
}
long contentLength = getHeaders().getContentLength();
if (this.response.getCharacterEncoding() == null && headers.containsHeader(HttpHeaders.CONTENT_TYPE)) {
try {
// Lazy parsing into MediaType
MediaType contentType = headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
this.response.setCharacterEncoding(charset);
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
long contentLength = headers.getContentLength();
if (contentLength != -1) {
this.response.setContentLengthLong(contentLength);
}
if (removeAdaptedHeaders) {
getHeaders().remove(HttpHeaders.CONTENT_TYPE);
getHeaders().remove(HttpHeaders.CONTENT_LENGTH);
headers.remove(HttpHeaders.CONTENT_TYPE);
headers.remove(HttpHeaders.CONTENT_LENGTH);
}
}
@@ -352,7 +356,6 @@ class ServletServerHttpResponse extends AbstractListenerServerHttpResponse {
private class ResponseBodyProcessor extends AbstractListenerWriteProcessor<DataBuffer> {
public ResponseBodyProcessor() {
super(request.getLogPrefix());
}
@@ -65,12 +65,16 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
@Override
public void addAll(String key, List<? extends String> values) {
values.forEach(value -> add(key, value));
for (String value : values) {
add(key, value);
}
}
@Override
public void addAll(MultiValueMap<String, String> values) {
values.forEach(this::addAll);
for (Entry<String, List<String>> entry : values.entrySet()) {
addAll(entry.getKey(), entry.getValue());
}
}
@Override
@@ -80,24 +84,28 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
@Override
public void setAll(Map<String, String> values) {
values.forEach(this::set);
for (Entry<String, String> entry : values.entrySet()) {
set(entry.getKey(), entry.getValue());
}
}
@Override
public Map<String, String> toSingleValueMap() {
Map<String, String> singleValueMap = CollectionUtils.newLinkedHashMap(this.headers.size());
this.keySet().forEach(key -> singleValueMap.put(key, getFirst(key)));
return singleValueMap;
Map<String, String> map = CollectionUtils.newLinkedHashMap(this.headers.size());
for (String name : this.keySet()) {
map.put(name, getFirst(name));
}
return map;
}
@Override
public int size() {
Enumeration<String> names = this.headers.names();
Set<String> deduplicated = new LinkedHashSet<>();
Set<String> set = new LinkedHashSet<>(this.headers.size());
while (names.hasMoreElements()) {
deduplicated.add(names.nextElement().toLowerCase(Locale.ROOT));
set.add(names.nextElement().toLowerCase(Locale.ROOT));
}
return deduplicated.size();
return set.size();
}
@Override
@@ -116,10 +124,10 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
@Override
public boolean containsValue(Object value) {
if (value instanceof String text) {
MessageBytes messageBytes = MessageBytes.newInstance();
messageBytes.setString(text);
MessageBytes bytes = MessageBytes.newInstance();
bytes.setString(text);
for (int i = 0; i < this.headers.size(); i++) {
if (this.headers.getValue(i).equals(messageBytes)) {
if (this.headers.getValue(i).equals(bytes)) {
return true;
}
}
@@ -129,33 +137,38 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
@Override
public @Nullable List<String> get(Object key) {
if (containsKey(key)) {
return Collections.list(this.headers.values((String) key));
if (key instanceof String headerName) {
Enumeration<String> values = this.headers.values(headerName);
if (values.hasMoreElements()) {
return Collections.list(values);
}
}
return null;
}
@Override
public @Nullable List<String> put(String key, List<String> value) {
List<String> previousValues = get(key);
List<String> previous = get(key);
this.headers.removeHeader(key);
value.forEach(v -> this.headers.addValue(key).setString(v));
return previousValues;
return previous;
}
@Override
public @Nullable List<String> remove(Object key) {
if (key instanceof String headerName) {
List<String> previousValues = get(key);
List<String> previous = get(key);
this.headers.removeHeader(headerName);
return previousValues;
return previous;
}
return null;
}
@Override
public void putAll(Map<? extends String, ? extends List<String>> map) {
map.forEach(this::put);
for (Entry<? extends String, ? extends List<String>> entry : map.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
@@ -258,6 +271,7 @@ class TomcatHeadersAdapter implements MultiValueMap<String, String> {
}
}
private final class HeaderNamesIterator implements Iterator<String> {
private final Enumeration<String> enumeration;
@@ -52,14 +52,14 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
/**
* Creates a new {@code JettyHeadersAdapter} based on the given
* Construct a new {@code JettyHeadersAdapter} based on the given
* {@code HttpFields} instance.
* @param headers the {@code HttpFields} to base this adapter on
*/
public JettyHeadersAdapter(HttpFields headers) {
Assert.notNull(headers, "Headers must not be null");
this.headers = headers;
this.mutable = headers instanceof HttpFields.Mutable m ? m : null;
this.mutable = (headers instanceof HttpFields.Mutable m ? m : null);
}
@@ -146,12 +146,12 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
public @Nullable List<String> get(Object key) {
List<String> list = null;
if (key instanceof String name) {
for (HttpField f : this.headers) {
if (f.is(name)) {
for (HttpField field : this.headers) {
if (field.is(name)) {
if (list == null) {
list = new ArrayList<>();
}
list.add(f.getValue());
list.add(field.getValue());
}
}
}
@@ -162,7 +162,6 @@ public final class JettyHeadersAdapter implements MultiValueMap<String, String>
public @Nullable List<String> put(String key, List<String> value) {
HttpFields.Mutable mutableHttpFields = mutableFields();
List<String> oldValues = get(key);
if (oldValues == null) {
switch (value.size()) {
case 0 -> {}
@@ -57,10 +57,12 @@ public @interface PathVariable {
/**
* Whether the path variable is required.
* <p>Defaults to {@code true}, leading to an exception being thrown if the path
* variable is missing in the incoming request. Switch this to {@code false} if
* you prefer a {@code null} or Java 8 {@code java.util.Optional} in this case.
* for example, on a {@code ModelAttribute} method which serves for different requests.
* <p>Defaults to {@code true}, leading to an exception being thrown if the
* path variable is missing in the incoming request.
* <p>Switch this to {@code false} if you prefer a {@code null} or
* {@code java.util.Optional} if the path variable does not exist &mdash;
* for example, on a {@code ModelAttribute} method which serves for different
* requests.
* @since 4.3.3
*/
boolean required() default true;
@@ -57,9 +57,9 @@ public @interface RequestAttribute {
/**
* Whether the request attribute is required.
* <p>Defaults to {@code true}, leading to an exception being thrown if
* the attribute is missing. Switch this to {@code false} if you prefer
* a {@code null} or Java 8 {@code java.util.Optional} if the attribute
* doesn't exist.
* the attribute is missing.
* <p>Switch this to {@code false} if you prefer a {@code null} or
* {@code java.util.Optional} if the attribute does not exist.
*/
boolean required() default true;
@@ -66,8 +66,8 @@ public @interface SessionAttribute {
* Whether the session attribute is required.
* <p>Defaults to {@code true}, leading to an exception being thrown
* if the attribute is missing in the session or there is no session.
* Switch this to {@code false} if you prefer a {@code null} or Java 8
* {@code java.util.Optional} if the attribute doesn't exist.
* <p>Switch this to {@code false} if you prefer a {@code null} or
* {@code java.util.Optional} if the attribute does not exist.
*/
boolean required() default true;
@@ -41,8 +41,8 @@ import org.springframework.util.Assert;
* @since 15.03.2004
* @see #setEncoding
* @see #setForceEncoding
* @see jakarta.servlet.http.HttpServletRequest#setCharacterEncoding
* @see jakarta.servlet.http.HttpServletResponse#setCharacterEncoding
* @see jakarta.servlet.http.HttpServletRequest#setCharacterEncoding(String)
* @see jakarta.servlet.http.HttpServletResponse#setCharacterEncoding(String)
*/
public class CharacterEncodingFilter extends OncePerRequestFilter {
@@ -64,8 +64,6 @@ public class InvocableHandlerMethod extends HandlerMethod {
private static final Object[] EMPTY_ARGS = new Object[0];
private static final Class<?>[] EMPTY_GROUPS = new Class<?>[0];
private static final boolean KOTLIN_REFLECT_PRESENT = KotlinDetector.isKotlinReflectPresent();
@@ -117,10 +117,11 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
// MaxUploadSizeExceededException ?
Throwable cause = ex;
do {
String msg = cause.getMessage();
String msg = cause.toString();
if (msg != null) {
msg = msg.toLowerCase(Locale.ROOT);
if ((msg.contains("exceed") && (msg.contains("size") || msg.contains("length"))) ||
if (((msg.contains("exceed") || msg.contains("limit")) &&
(msg.contains("size") || msg.contains("length") || msg.contains("count"))) ||
(msg.contains("request") && (msg.contains("big") || msg.contains("large")))) {
throw new MaxUploadSizeExceededException(-1, ex);
}
@@ -266,7 +267,7 @@ public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpSe
if (dest.isAbsolute() && !dest.exists()) {
// Servlet Part.write is not guaranteed to support absolute file paths:
// may translate the given path to a relative location within a temp dir
// (for example, on Jetty whereas Tomcat detects absolute paths).
// (for example, on Jetty whereas Tomcat and Undertow detect absolute paths).
// At least we offloaded the file from memory storage; it'll get deleted
// from the temp dir eventually in any case. And for our user's purposes,
// we can manually copy it to the requested location as a fallback.
@@ -210,22 +210,31 @@ class DefaultHttpMessageConvertersTests {
.addCustomConverter(customConverter)
.configureMessageConverters(converter -> {
if (converter instanceof CustomHttpMessageConverter custom) {
custom.processed = true;
assertThat(custom.processCount).isZero();
custom.processCount++;
}
})
.configureMessageConverters(converter -> {
if (converter instanceof CustomHttpMessageConverter custom) {
assertThat(custom.processCount).isEqualTo(1);
custom.processCount++;
}
}).build();
assertThat(customConverter.processed).isTrue();
assertThat(customConverter.processCount).isEqualTo(2);
}
@Test
void shouldAppendCustomConverterToList() {
var customConverter = new CustomHttpMessageConverter();
var firstCustom = new CustomHttpMessageConverter();
var secondCustom = new CustomHttpMessageConverter();
var messageConverters = HttpMessageConverters.forClient()
.registerDefaults()
.configureMessageConvertersList(converters -> converters.add(customConverter))
.configureMessageConvertersList(converters -> converters.add(firstCustom))
.configureMessageConvertersList(converters -> converters.add(secondCustom))
.build();
assertThat(messageConverters).last().isInstanceOf(CustomHttpMessageConverter.class);
assertThat(messageConverters).containsSequence(firstCustom, secondCustom);
}
}
@@ -336,22 +345,31 @@ class DefaultHttpMessageConvertersTests {
.addCustomConverter(customConverter)
.configureMessageConverters(converter -> {
if (converter instanceof CustomHttpMessageConverter custom) {
custom.processed = true;
assertThat(custom.processCount).isZero();
custom.processCount++;
}
})
.configureMessageConverters(converter -> {
if (converter instanceof CustomHttpMessageConverter custom) {
assertThat(custom.processCount).isEqualTo(1);
custom.processCount++;
}
}).build();
assertThat(customConverter.processed).isTrue();
assertThat(customConverter.processCount).isEqualTo(2);
}
@Test
void shouldAppendCustomConverterToList() {
var customConverter = new CustomHttpMessageConverter();
var firstCustom = new CustomHttpMessageConverter();
var secondCustom = new CustomHttpMessageConverter();
var messageConverters = HttpMessageConverters.forServer()
.registerDefaults()
.configureMessageConvertersList(converters -> converters.add(customConverter))
.configureMessageConvertersList(converters -> converters.add(firstCustom))
.configureMessageConvertersList(converters -> converters.add(secondCustom))
.build();
assertThat(messageConverters).last().isInstanceOf(CustomHttpMessageConverter.class);
assertThat(messageConverters).containsSequence(firstCustom, secondCustom);
}
}
@@ -365,7 +383,7 @@ class DefaultHttpMessageConvertersTests {
static class CustomHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
boolean processed = false;
int processCount;
@Override
protected boolean supports(Class<?> clazz) {
@@ -298,12 +298,14 @@ class HeadersAdaptersTests {
static Stream<Arguments> nativeHeadersWithCasedEntries() {
return Stream.of(
argumentSet("Netty", new Netty4HeadersAdapter(withHeaders(new DefaultHttpHeaders(), h -> h::add))),
argumentSet("Tomcat", new TomcatHeadersAdapter(withHeaders(new MimeHeaders(),
h -> (k, v) -> h.addValue(k).setString(v)))),
argumentSet("Jetty", new JettyHeadersAdapter(withHeaders(HttpFields.build(), h -> h::add))),
argumentSet("HttpComponents", new HttpComponentsHeadersAdapter(withHeaders(new HttpGet("https://example.com"),
h -> h::addHeader)))
argumentSet("Netty", new Netty4HeadersAdapter(
withHeaders(new DefaultHttpHeaders(), h -> h::add))),
argumentSet("Tomcat", new TomcatHeadersAdapter(
withHeaders(new MimeHeaders(), h -> (k, v) -> h.addValue(k).setString(v)))),
argumentSet("Jetty", new JettyHeadersAdapter(
withHeaders(HttpFields.build(), h -> h::add))),
argumentSet("HttpComponents", new HttpComponentsHeadersAdapter(
withHeaders(new HttpGet("https://example.com"), h -> h::addHeader)))
);
}
@@ -123,6 +123,47 @@ class StandardMultipartHttpServletRequestTests {
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-32549
void undertowRequestTooBigException() {
IOException ex = new IOException("Connection terminated as request was larger than 10000");
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Tomcat's Commons FileUpload
void commonsFileSizeLimitExceededException() {
IOException ex = new FileSizeLimitExceededException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Tomcat's Commons FileUpload
void commonsFileCountLimitExceededException() {
IOException ex = new FileCountLimitExceededException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Commons FileUpload 2.x
void commonsFileUploadByteCountLimitException() {
IOException ex = new FileUploadByteCountLimitException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
@Test // gh-36317: Commons FileUpload 2.x
void commonsFileUploadFileCountLimitException() {
IOException ex = new FileUploadFileCountLimitException();
assertThatExceptionOfType(MaxUploadSizeExceededException.class)
.isThrownBy(() -> requestWithException(ex)).withCause(ex);
}
private static StandardMultipartHttpServletRequest requestWithPart(String name, String disposition, String content) {
MockHttpServletRequest request = new MockHttpServletRequest();
MockPart part = new MockPart(name, null, content.getBytes(StandardCharsets.UTF_8));
@@ -141,4 +182,31 @@ class StandardMultipartHttpServletRequestTests {
return new StandardMultipartHttpServletRequest(request);
}
private static StandardMultipartHttpServletRequest requestWithException(IOException ex) {
MockHttpServletRequest request = new MockHttpServletRequest() {
@Override
public Collection<Part> getParts() throws IOException {
throw ex;
}
};
return new StandardMultipartHttpServletRequest(request);
}
@SuppressWarnings("serial")
private static class FileSizeLimitExceededException extends IOException {
}
@SuppressWarnings("serial")
private static class FileCountLimitExceededException extends IOException {
}
@SuppressWarnings("serial")
private static class FileUploadByteCountLimitException extends IOException {
}
@SuppressWarnings("serial")
private static class FileUploadFileCountLimitException extends IOException {
}
}
@@ -55,7 +55,6 @@ import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Contract;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.ValidationAnnotationUtils;
import org.springframework.validation.method.MethodValidator;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.BindingContext;
@@ -97,8 +96,6 @@ public class InvocableHandlerMethod extends HandlerMethod {
private @Nullable MethodValidator methodValidator;
private Class<?>[] validationGroups = EMPTY_GROUPS;
private @Nullable Scheduler invocationScheduler;
@@ -151,7 +148,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
/**
* Configure a reactive adapter registry. This is needed for cases where the response is
* fully handled within the controller in combination with an async void return value.
* <p>By default this is a {@link ReactiveAdapterRegistry} with default settings.
* <p>By default, this is a {@link ReactiveAdapterRegistry} with default settings.
*/
public void setReactiveAdapterRegistry(ReactiveAdapterRegistry registry) {
this.reactiveAdapterRegistry = registry;
@@ -165,8 +162,6 @@ public class InvocableHandlerMethod extends HandlerMethod {
*/
public void setMethodValidator(@Nullable MethodValidator methodValidator) {
this.methodValidator = methodValidator;
this.validationGroups = (methodValidator != null ?
ValidationAnnotationUtils.determineValidationGroups(getBean(), getBridgedMethod()) : EMPTY_GROUPS);
}
/**
@@ -193,7 +188,7 @@ public class InvocableHandlerMethod extends HandlerMethod {
try {
LocaleContextHolder.setLocaleContext(exchange.getLocaleContext());
this.methodValidator.applyArgumentValidation(
getBean(), getBridgedMethod(), getMethodParameters(), args, this.validationGroups);
getBean(), getBridgedMethod(), getMethodParameters(), args, getValidationGroups());
}
finally {
LocaleContextHolder.resetLocaleContext();
@@ -19,6 +19,8 @@ package org.springframework.web.reactive.result.method.annotation;
import java.net.URI;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import reactor.core.publisher.Mono;
@@ -53,6 +55,9 @@ import org.springframework.web.server.ServerWebExchange;
*/
public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandler implements HandlerResultHandler {
private final Map<Class<?>, Boolean> responseBodyControllerCache = new ConcurrentHashMap<>();
/**
* Basic constructor with a default {@link ReactiveAdapterRegistry}.
* @param writers the writers for serializing to the response body
@@ -93,8 +98,8 @@ public class ResponseBodyResultHandler extends AbstractMessageWriterResultHandle
@Override
public boolean supports(HandlerResult result) {
MethodParameter returnType = result.getReturnTypeSource();
Class<?> containingClass = returnType.getContainingClass();
return (AnnotatedElementUtils.hasAnnotation(containingClass, ResponseBody.class) ||
return (this.responseBodyControllerCache.computeIfAbsent(returnType.getContainingClass(),
clazz -> AnnotatedElementUtils.hasAnnotation(clazz, ResponseBody.class)) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
@@ -59,7 +59,6 @@ class WebFluxViewResolutionIntegrationTests {
private static final MediaType TEXT_HTML_ISO_8859_1 = MediaType.parseMediaType("text/html;charset=ISO-8859-1");
@Nested
class FreeMarkerTests {
@@ -115,6 +114,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class FreeMarkerWebFluxConfig extends AbstractWebFluxConfig {
@@ -131,6 +131,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class ExplicitDefaultEncodingConfig extends AbstractWebFluxConfig {
@@ -148,6 +149,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Configuration(proxyBeanMethods = false)
static class ExplicitDefaultEncodingAndContentTypeConfig extends AbstractWebFluxConfig {
@@ -196,6 +198,7 @@ class WebFluxViewResolutionIntegrationTests {
}
}
@Controller
static class SampleController {
@@ -17,6 +17,7 @@
package org.springframework.web.servlet.function;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Set;
@@ -29,6 +30,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.MediaType;
import org.springframework.util.CollectionUtils;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -51,15 +53,16 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
private final MultiValueMap<String, Cookie> cookies;
protected AbstractServerResponse(
HttpStatusCode statusCode, HttpHeaders headers, MultiValueMap<String, Cookie> cookies) {
this.statusCode = statusCode;
this.headers = HttpHeaders.readOnlyHttpHeaders(headers);
this.cookies =
CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
this.cookies = CollectionUtils.unmodifiableMultiValueMap(new LinkedMultiValueMap<>(cookies));
}
@Override
public final HttpStatusCode statusCode() {
return this.statusCode;
@@ -110,14 +113,29 @@ abstract class AbstractServerResponse extends ErrorHandlingServerResponse {
servletResponse.addHeader(headerName, headerValue);
}
});
// HttpServletResponse exposes some headers as properties: we should include those if not already present
if (servletResponse.getContentType() == null && this.headers.getContentType() != null) {
servletResponse.setContentType(this.headers.getContentType().toString());
if (servletResponse.getContentType() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) {
servletResponse.setContentType(this.headers.getFirst(HttpHeaders.CONTENT_TYPE));
}
if (servletResponse.getCharacterEncoding() == null &&
this.headers.getContentType() != null &&
this.headers.getContentType().getCharset() != null) {
servletResponse.setCharacterEncoding(this.headers.getContentType().getCharset().name());
if (servletResponse.getCharacterEncoding() == null && this.headers.containsHeader(HttpHeaders.CONTENT_TYPE)) {
try {
// Lazy parsing into MediaType
MediaType contentType = this.headers.getContentType();
if (contentType != null) {
Charset charset = contentType.getCharset();
if (charset != null) {
servletResponse.setCharacterEncoding(charset);
}
}
}
catch (Exception ex) {
// Leave character encoding unspecified
}
}
long contentLength = this.headers.getContentLength();
if (contentLength != -1) {
servletResponse.setContentLengthLong(contentLength);
}
}
@@ -34,6 +34,7 @@ import org.springframework.web.servlet.ModelAndView;
/**
* Base class for {@link ServerResponse} implementations with error handling.
*
* @author Arjen Poutsma
* @since 5.3
*/
@@ -81,6 +82,7 @@ abstract class ErrorHandlingServerResponse implements ServerResponse {
return null;
}
private static class ErrorHandler<T extends ServerResponse> {
private final Predicate<Throwable> predicate;
@@ -229,7 +229,7 @@ public class HttpEntityMethodProcessor extends AbstractMessageConverterMethodPro
if (HttpHeaders.VARY.equals(key) && outputHeaders.containsHeader(HttpHeaders.VARY)) {
List<String> values = getVaryRequestHeadersToAdd(outputHeaders, entityHeaders);
if (!values.isEmpty()) {
outputHeaders.setVary(values);
outputHeaders.addAll(HttpHeaders.VARY, values);
}
}
else {
@@ -20,6 +20,8 @@ import java.io.IOException;
import java.lang.reflect.Type;
import java.net.URI;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.jspecify.annotations.Nullable;
@@ -67,6 +69,9 @@ import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolv
*/
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
private final Map<Class<?>, Boolean> responseBodyControllerCache = new ConcurrentHashMap<>();
/**
* Basic constructor with converters only. Suitable for resolving
* {@code @RequestBody}. For handling {@code @ResponseBody} consider also
@@ -132,7 +137,8 @@ public class RequestResponseBodyMethodProcessor extends AbstractMessageConverter
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
return (this.responseBodyControllerCache.computeIfAbsent(returnType.getContainingClass(),
clazz -> AnnotatedElementUtils.hasAnnotation(clazz, ResponseBody.class)) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
@@ -458,6 +458,11 @@ public class ResponseBodyEmitterReturnValueHandler implements HandlerMethodRetur
// ignore
}
@Override
public void setCharacterEncoding(Charset encoding) {
// ignore
}
@Override
public void setContentLength(int len) {
// ignore
@@ -197,7 +197,7 @@ public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
* Create a nested ServletInvocableHandlerMethod subclass that returns the
* given value (or raises an Exception if the value is one) rather than
* actually invoking the controller method. This is useful when processing
* async return values (for example, Callable, DeferredResult, ListenableFuture).
* async return values (for example, Callable, DeferredResult, CompletableFuture).
*/
ServletInvocableHandlerMethod wrapConcurrentResult(@Nullable Object result) {
return new ConcurrentResultHandlerMethod(result, new ConcurrentResultMethodParameter(result));
@@ -374,7 +374,7 @@ public class ScriptTemplateView extends AbstractUrlBasedView {
setResponseContentType(request, response);
if (this.charset != null) {
response.setCharacterEncoding(this.charset.name());
response.setCharacterEncoding(this.charset);
}
}
@@ -395,7 +395,7 @@ public class XsltView extends AbstractUrlBasedView {
* Configure the supplied {@link HttpServletResponse}.
* <p>The default implementation of this method sets the
* {@link HttpServletResponse#setContentType content type} and
* {@link HttpServletResponse#setCharacterEncoding encoding}
* {@link HttpServletResponse#setCharacterEncoding(String) encoding}
* from the "media-type" and "encoding" output properties
* specified in the {@link Transformer}.
* @param model merged output Map (never {@code null})
@@ -288,7 +288,7 @@ class ResponseBodyEmitterReturnValueHandlerTests {
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.webRequest);
assertThat(asyncManager.getConcurrentResult()).isSameAs(ex);
assertThat(this.response.getContentType()).isNull();
assertThat(this.response.getContentType()).isEqualTo(MediaType.TEXT_EVENT_STREAM_VALUE);
}
@Test
@@ -247,7 +247,7 @@ public class WebSocketStompClient extends StompClientSupport implements SmartLif
* @param handshakeHeaders the headers for the WebSocket handshake
* @param handler the session handler
* @param uriVariables the URI variables to expand into the URL
* @return a {@code ListenableFuture} for access to the session when ready for use
* @return a {@code CompletableFuture} for access to the session when ready for use
* @since 6.0
*/
public CompletableFuture<StompSession> connectAsync(String url, @Nullable WebSocketHttpHeaders handshakeHeaders,