Compare commits

...

74 Commits

Author SHA1 Message Date
Brian Clozel 502b31a7f2 Release v6.2.8 2025-06-12 11:02:39 +02:00
Brian Clozel f0e7b42704 Encode non-printable character in Content-Disposition parameter
Prior to this commit, the "filename" parameter value for the
"Content-Disposition" header would contain non-printable characters,
causing parsing issues for HTTP clients.
This commit ensures that all non-printable characters are encoded.

Fixes gh-35034
2025-06-12 08:39:29 +02:00
Johnny Lim e86dc9ad95 Apply gh-34856 to MockClientHttpRequest in testfixture package
Closes gh-35031

Signed-off-by: Johnny Lim <izeye@naver.com>
2025-06-11 17:40:54 +02:00
Sébastien Deleuze 05c3f56ec7 Rely on default retention in @Contract
Closes gh-35027
2025-06-11 10:14:59 +02:00
Brian Clozel dee80ab1da Upgrade to Reactor 2024.0.7
Closes gh-35021
2025-06-11 09:23:17 +02:00
Brian Clozel 07fd835ea9 Upgrade to Micrometer 1.14.8
Closes gh-35020
2025-06-11 09:22:50 +02:00
Brian Clozel 0d6c6eb2d5 Use Micrometer BOM for Context Propagation dependency
Closes gh-35026
2025-06-11 09:15:30 +02:00
Sam Brannen 4d2cc4ae97 Polish contribution
See gh-35013
2025-06-10 11:45:20 +02:00
Mohammad Saeed Nouri c04902fefb Allow update of existing WebSession after max sessions limit is reached
Previously, when saving a WebSession, the system did not check whether
the session ID already existed. As a result, even if the session being
saved was an update to an existing one, it was incorrectly treated as a
new session, and a "maximum sessions exceeded" error was triggered.

This fix ensures that if a WebSession with the same ID already exists,
it will be updated rather than counted as a new session, thereby
preventing unnecessary session limit violations.

Closes gh-35013

Signed-off-by: Mohammad Saeed Nouri <msnsaeed71@gmail.com>
2025-06-10 11:44:59 +02:00
Sam Brannen 3c265e1044 Fix InMemoryWebSessionStoreTests.startsSessionImplicitly() test 2025-06-10 11:44:00 +02:00
Sam Brannen 222702f750 Polish WebSession support and tests 2025-06-10 11:43:56 +02:00
Sébastien Deleuze 7bb19fcde8 Refine Kotlin Serialization hint registration
This commit adds support for serializer methods with a parameter.

Closes gh-34979
2025-06-09 16:48:15 +02:00
Sam Brannen 18d6a55e3e Polishing and removal of "this." for method invocations 2025-06-09 14:10:30 +02:00
秦利斌 99890b6147 Fix ResourceHttpRequestHandler#setHeaders JavaDoc
Closes gh-35004
Signed-off-by: 秦利斌 <68638598+Allan-QLB@users.noreply.github.com>
2025-06-09 10:55:21 +02:00
Sam Brannen 0afdb08099 Upgrade to JUnit 5.13.1
Closes gh-34969
2025-06-07 12:03:10 +02:00
Sébastien Deleuze 40058ef875 Disable Gradle auto-provisioning
Closes gh-35007
2025-06-06 18:18:52 +02:00
rstoyanchev c769f43b3e Update docs on testing client code
Closes gh-34892
2025-06-06 15:28:28 +01:00
rstoyanchev 4782c697b8 Improve RestControllerAdvice documentation
Closes gh-34866
2025-06-06 15:28:28 +01:00
rstoyanchev de52090959 Polishing contribution
Closes gh-34554
2025-06-06 15:28:28 +01:00
Vedran Pavic 4d862b871d Improve Javadoc for @ExceptionHandler
This commit adds `ProblemDetail` and `ErrorResponse` to the list of
supported return types for `@ExceptionHandler` methods.

Closes gh-34554

Signed-off-by: Vedran Pavic <vedran@vedranpavic.com>
2025-06-06 15:28:28 +01:00
Jimmy Axenhus fa781c8390 Correct name of Selenium WebDriver artifact
The selenium-htmlunit3-driver artifact does not exist.

Closes gh-34840

Signed-off-by: Jimmy Axenhus <github@axenhus.com>
2025-06-06 15:28:28 +01:00
Sam Brannen 88cd7addda Upgrade to Gradle 8.14.2
Closes gh-34937
2025-06-06 15:06:06 +02:00
Brian Clozel 332c7f770c Remove Link to AspectJ Javadoc
Closes gh-35000
2025-06-06 12:05:44 +02:00
Juergen Hoeller 9f76ea13e3 Upgrade to Groovy 4.0.27, Mockito 5.18, Checkstyle 10.25 2025-06-06 09:21:37 +02:00
Juergen Hoeller d9c103b7ff Remove unnecessary fallback to DefaultConversionService
See gh-34936
2025-06-06 09:21:21 +02:00
Juergen Hoeller f1ddd051a3 Restore synchronization against AspectJ race condition behind PointcutExpression
Closes gh-34735
2025-06-05 20:36:10 +02:00
Juergen Hoeller a266e1b403 Close JarFile only in case of useCaches=false
Closes gh-34955
2025-06-05 20:20:47 +02:00
Sébastien Deleuze 4d09eb569b Refine RequestResponseBodyMethodProcessorKotlinTests
Improve read tests and fix copyright.

See gh-34992
2025-06-05 14:07:27 +02:00
Sébastien Deleuze a439e9030f Fix collection support in AbstractKotlinSerializationHttpMessageConverter
AbstractKotlinSerializationHttpMessageConverter#getSupportedMediaTypes(Class<?>)
currently invokes transitively supports(Class<?>) which always return false
with generic types.

This commit adds an override that just invokes getSupportedMediaTypes().

Closes gh-34992
2025-06-05 12:02:22 +02:00
James Yuzawa aa5c0dcd72 Add caching headers to unmodified static resources
per https://www.rfc-editor.org/rfc/rfc7232#section-4.1

The server generating a 304 response MUST generate any of the
following header fields that would have been sent in a 200 (OK)
response to the same request: Cache-Control, Content-Location, Date,
ETag, Expires, and Vary.

Closes gh-34614

Signed-off-by: James Yuzawa <jtyuzawa@gmail.com>
2025-06-05 10:58:24 +02:00
Brian Clozel 4a46d957f3 Fix out of bounds exception for PathPattern#combine
Prior to this commit, combining the "/*" and  "/x/y" path patterns
would result in a `StringIndexOutOfBoundsException`.

This commit fixes this problem and revisits the implementation for
better consistency:

* "/*" + "/x/y" is now "/x/y"
* "/x/*.html" + "/y/file.*" is now rejected because they don't share the
  same prefix.

This change also adds the relevant Javadoc to the `PathPattern#combine`
method.

Fixes gh-34986
2025-06-04 20:18:33 +02:00
Sam Brannen 4df93a825b Update copyright headers and fix test method name 2025-06-04 11:28:55 +02:00
rstoyanchev be17315f63 Add mention of CompletionStage in "Async Requests"
Closes gh-34991
2025-06-03 19:10:03 +01:00
DongNyoung Lee 31903a9d92 Fix typo in MockClientHttpRequest Javadoc
Closes gh-34856

Signed-off-by: DongNyoung Lee <121621378+Dongnyoung@users.noreply.github.com>
2025-06-03 19:05:32 +01:00
rstoyanchev 06f69915cb Polishing contribution
Closes gh-34885
2025-06-03 19:04:17 +01:00
addoDev d9459bdc74 Improve mvc-ann-async.adoc
Added section for WebAsyncTask return type along with java and
kotlin example code

See gh-34885

Signed-off-by: addoDev <adityaraochokkadi@gmail.com>
2025-06-03 18:57:40 +01:00
Tran Ngoc Nhan 3f0892b42c Fix typos
Closes gh-34876

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2025-06-03 18:55:15 +01:00
Patrick Strawderman 182d654fa8 Add optimized DataBufferInputStream overrides
Add optimized DataBufferInputStream overrides for readNBytes, skip, and transferTo; all of them
allocate byte buffers which we can either avoid (in the case of skip) or size more precisely since
the number of remaining bytes is known.

Closes gh-34799

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
2025-06-03 18:06:43 +02:00
Johnny Lim bbae625850 Add Javadoc since for gh-34745
Closes gh-34940

Signed-off-by: Johnny Lim <izeye@naver.com>
2025-06-03 18:06:24 +02:00
Sam Brannen f376d1b525 Remove @⁠Contract declaration for CodeFlow.isIntegerForNumericOp()
Since the Number parameter is not @⁠Nullable, the @⁠Contract declaration
is unnecessary.

Closes gh-34985
2025-06-03 12:58:45 +02:00
Sam Brannen 707b7698ce Consistent @⁠Contract expression formatting 2025-06-02 17:00:28 +02:00
Johannes Jank 5b9cb8291e Fix exception name in ModelAttribute docs
Closes gh-34980

Signed-off-by: Johannes Jank <johannes.wengert@googlemail.com>
2025-06-02 16:30:16 +02:00
Brian Clozel 659472f9e3 Use HTTP methods in JdkClientHttpRequest when possible
Prior to this commit, we would use the
`java.net.http.HttpRequest.Builder#method(String, BodyPublisher)` to
create HTTP requests for the JDK HttpClient. This method requires a
non-null body publisher; providing an empty publisher writes a
"Content-Length: 0" header to all requests.

As of Java 19, this behavior changes for `HttpRequest.Builder#GET` and
similar methods, where the body publisher is considered as null and no
"Content-Length" header is written.

This commit aligns with this behavior and favors dedicated HTTP methods
whenever available.`

Closes gh-34971
2025-06-02 15:36:20 +02:00
Dmytro Nosan c2d678879f Fix AOT code generation for autowired inner class constructor
Prior to this commit, argument index handling in
AutowiredArgumentsCodeGenerator suffered from an off-by-one error when
generating code for an autowired inner class constructor.

Since the startIndex is already properly calculated for an inner class in
InstanceSupplierCodeGenerator.buildGetInstanceMethodForConstructor(...),
there is no need to adjust the argument indexes within
AutowiredArgumentsCodeGenerator.generateCode(...).

Closes gh-34974

Signed-off-by: Dmytro Nosan <dimanosan@gmail.com>
2025-06-02 10:43:21 +02:00
Sam Brannen f207c0ed5c Polish contribution
See gh-34949
2025-06-02 10:10:46 +02:00
Mengqi Xu bbf61c74ab Fix REPLY_CHANNEL header check in MessageHeaderAccessor
Prior to this commit, the verifyType() method in MessageHeaderAccessor
checked if REPLY_CHANNEL ended with the given header name which does
not make much sense and is inconsistent with the ERROR_CHANNEL check.

This commit therefore checks if the REPLY_CHANNEL is equal to the given
header name, analogous to the ERROR_CHANNEL check.

See gh-34881
Closes gh-34949

Signed-off-by: Mengqi Xu <2663479778@qq.com>
2025-06-02 10:00:01 +02:00
Aurh1l aea66265f7 Fix syntax in @⁠SqlGroup example
Closes gh-34972
2025-05-31 12:15:50 +02:00
Sam Brannen 016294b067 Avoid overriding methods deprecated in JUnit Jupiter 5.13
See gh-34969
2025-05-30 14:54:57 +02:00
Sam Brannen 8201193650 Upgrade to JUnit 5.13
Closes gh-34969
2025-05-30 13:57:18 +02:00
Dhruv 562157d2c0 Update X-Forwarded-Proto doc to say "https / http"
Closes gh-34959

Signed-off-by: Dhruv <dhruv2015@hotmail.co.uk>
2025-05-28 16:41:16 +02:00
Dmitry Sulman 63db3d06ab Fix method links in Javadoc of WebClient
Closes gh-34938

Signed-off-by: Dmitry Sulman <dmitry.sulman@gmail.com>
2025-05-28 11:05:00 +01:00
rstoyanchev dc52042b85 Polishing contribution
Closes gh-34939
2025-05-28 11:05:00 +01:00
dujiabao cff6e02162 Correct sample in multipart-forms.adoc
See gh-34939

Signed-off-by: dujiabao <42103826+WayneDu98@users.noreply.github.com>
2025-05-28 11:05:00 +01:00
rstoyanchev 983af78352 Configurable limit on HandlerMappingIntrospector
Closes gh-34918
2025-05-28 11:05:00 +01:00
Sébastien Deleuze 67ed64a41d Remove not null checks in BeanUtilsKotlinTests 2025-05-28 10:31:07 +02:00
Sébastien Deleuze d777b70889 Ignore DefaultConstructorMarker in BeanUtils#getParameterNames
Closes gh-34760
2025-05-28 10:12:09 +02:00
Sam Brannen 472c4012ca Remove obsolete note from Javadoc for BeanDefinitionReader 2025-05-27 11:32:05 +02:00
Sam Brannen 90be94a4a5 Use ConversionService from Environment in PropertySourcesPlaceholderConfigurer
This commit fixes a regression in PropertySourcesPlaceholderConfigurer
that was introduced in Spring Framework 6.2.7.

Specifically, this commit reinstates automatic String-conversion of
values from PropertySources in the Environment using the
ConversionService configured in the Environment.

See gh-34861
Closes gh-34936
2025-05-27 11:20:09 +02:00
Juergen Hoeller d9e261aecd Enforce backwards compatibility for null bean in shortcut code path
Closes gh-34929
2025-05-27 09:47:39 +02:00
Juergen Hoeller 15d1455acb Check for package-visible constructor in case of ClassLoader mismatch
Closes gh-34950
2025-05-27 09:47:29 +02:00
Sam Brannen 53844b068c Test conversion support in PropertySourcesPlaceholderConfigurer
This commit introduces a @⁠Disabled "regression test" which demonstrates
that PropertySourcesPlaceholderConfigurer uses the ConversionService
from the Environment.

See gh-34936
2025-05-26 18:18:52 +02:00
Sam Brannen d78264756e Support only Object property values in MockEnvironment test fixture
The setProperty() and withProperty() methods in MockEnvironment were
originally introduced with (String, String) signatures; however, they
should have always had (String, Object) signatures in order to comply
with the MockPropertySource and PropertySource APIs.

To address that, this commit changes the signatures of these methods so
that they only accept Object values for properties.

NOTE: this commit only affects the internal MockEnvironment used as a
test fixture. This commit does not affect the official, public
MockEnvironment implementation in spring-test.

See gh-34947
See gh-34948
2025-05-26 17:29:16 +02:00
Sam Brannen d0efc2230c Support Object property values in MockEnvironment
The setProperty() and withProperty() methods in MockEnvironment were
originally introduced with (String, String) signatures; however, they
should have always had (String, Object) signatures in order to comply
with the MockPropertySource and PropertySource APIs.

To address that, this commit introduces variants of these methods that
accept Object values for properties.

Closes gh-34947
2025-05-26 17:22:52 +02:00
Sam Brannen 6a6abac003 Polish Javadoc for MockPropertySource 2025-05-26 17:21:31 +02:00
Sébastien Deleuze 66f8eb02bd Upgrade to HttpComponents HttpClient 5.5
Closes gh-34941
2025-05-26 10:25:00 +02:00
Sam Brannen 274b97ca14 Upgrade to Gradle 8.14.1
Closes gh-34937
2025-05-23 17:32:53 +02:00
Sam Brannen e4b4512bcb Update code due to change in nullability contract
See gh-34933
2025-05-22 17:43:38 +02:00
Sam Brannen db5809f968 Remove obsolete @⁠Contract declaration for StreamUtils.drain()
Closes gh-34933
2025-05-22 17:23:35 +02:00
Sam Brannen d890a38f3c Support registration of non-public BeanDefinitionReader via @⁠ImportResource
Prior to this commit, a BeanDefinitionReader registered via
@⁠ImportResource was required to be public and have a public
constructor that accepts a single BeanDefinitionRegistry. However, the
public visibility requirements are not necessary, and the requirements
for the constructor's formal parameter list is not documented.

To address those issues, this commit removes the public visibility
restrictions and documents that a BeanDefinitionReader registered via
@⁠ImportResource must declare a constructor that accepts a single
BeanDefinitionRegistry.

In addition, this commit includes the cause of the instantiation
failure in case the registered BeanDefinitionReader cannot be
instantiated.

Closes gh-34928
2025-05-21 15:37:04 +02:00
Sam Brannen 98cef503fb Remove obsolete TODO
See gh-10976
2025-05-21 15:37:04 +02:00
Sam Brannen 362fe12704 Polishing 2025-05-21 15:36:58 +02:00
Brian Clozel 2af0323c21 Use Content-Type charset in JAXB message converters
Prior to this commit, the JAXB message converters would only rely on the
encoding declaration inside the XML document for reading the document.
This would then use the default UTF-8 encoding, even if the HTTP message
has the `"application/xml;charset=iso-8859-1"` Content-Type.

This commit ensures that both `Jaxb2CollectionHttpMessageConverter` and
`Jaxb2RootElementHttpMessageConverter` use the encoding declared in the
HTTP Content-Type, if present.

Fixes gh-34745
2025-05-19 16:57:57 +02:00
Brian Clozel fdab8fabd2 Avoid duplicate Content-Type in MockHttpServletRequest
Fixes gh-34913
2025-05-19 12:04:46 +02:00
Brian Clozel ae8b45a290 Next development version (v6.2.8-SNAPSHOT) 2025-05-15 11:07:25 +02:00
126 changed files with 1475 additions and 612 deletions
+2 -3
View File
@@ -91,18 +91,17 @@ configure([rootProject] + javaProjects) { project ->
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jakarta.ee/specifications/platform/9/apidocs/",
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
"https://eclipse.dev/aspectj/doc/latest/runtime-api/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.14/",
"https://hc.apache.org/httpcomponents-client-5.4.x/current/httpclient5/apidocs/",
"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.12.2/api/",
// "https://junit.org/junit5/docs/5.13.1/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("10.23.1");
checkstyle.setToolVersion("10.25.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -166,7 +166,7 @@ following sections to make this pattern much easier to implement.
== MockMvc and WebDriver Setup
To use Selenium WebDriver with `MockMvc`, make sure that your project includes a test
dependency on `org.seleniumhq.selenium:selenium-htmlunit3-driver`.
dependency on `org.seleniumhq.selenium:htmlunit3-driver`.
We can easily create a Selenium WebDriver that integrates with MockMvc by using the
`MockMvcHtmlUnitDriverBuilder` as the following example shows:
@@ -1,10 +1,26 @@
[[spring-mvc-test-client]]
= Testing Client Applications
You can use client-side tests to test code that internally uses the `RestTemplate`. The
idea is to declare expected requests and to provide "`stub`" responses so that you can
focus on testing the code in isolation (that is, without running a server). The following
example shows how to do so:
To test code that uses the `RestClient` or `RestTemplate`, you can use a mock web server, such as
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or
https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular
server, and that means you can test with the same HTTP client that is also configured in
the same way as in production, which is important because there are often subtle
differences in the way different clients handle network I/O. Another advantage of mock
web servers is the ability to simulate specific network issues and conditions at the
transport level, in combination with the client used in production.
In addition to dedicated mock web servers, historically the Spring Framework has provided
a built-in option to test `RestClient` or `RestTemplate` through `MockRestServiceServer`.
This relies on configuring the client under test with a custom `ClientHttpRequestFactory`
backed by the mock server that is in turn set up to expect requests and send "`stub`"
responses so that you can focus on testing the code in isolation, without running a server.
TIP: `MockRestServiceServer` predates the existence of mock web servers. At present, we
recommend using mock web servers for more complete testing of the transport layer and
network conditions.
The following example shows an example of using `MockRestServiceServer`:
[tabs]
======
@@ -250,7 +250,7 @@ Java::
@SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql")
)}
})
void userTest() {
// run code that uses the test schema and test data
}
@@ -2,9 +2,16 @@
= Testing
:page-section-summary-toc: 1
To test code that uses the `WebClient`, you can use a mock web server, such as the
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example
of its use, check out
To test code that uses the `WebClient`, you can use a mock web server, such as
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or
https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular
server, and that means you can test with the same HTTP client that is also configured in
the same way as in production, which is important because there are often subtle
differences in the way different clients handle network I/O. Another advantage of mock
web servers is the ability to simulate specific network issues and conditions at the
transport level, in combination with the client used in production.
For example use of MockWebServer, see
{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`]
in the Spring Framework test suite or the
https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`]
@@ -19,7 +19,7 @@ Java::
private String name;
private MultipartFile file;
private FilePart file;
// ...
@@ -42,7 +42,7 @@ Kotlin::
----
class MyForm(
val name: String,
val file: MultipartFile)
val file: FilePart)
@Controller
class FileUploadController {
@@ -34,11 +34,7 @@ Controllers can then return a `Flux<List<B>>`; Reactor provides a dedicated oper
| `HttpHeaders`
| For returning a response with headers and no body.
| `ErrorResponse`
| To render an RFC 9457 error response with details in the body,
see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses]
| `ProblemDetail`
| `ErrorResponse`, `ProblemDetail`
| To render an RFC 9457 error response with details in the body,
see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses]
@@ -4,11 +4,13 @@
Spring MVC has an extensive integration with Servlet asynchronous request
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-processing[processing]:
* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`]
return values in controller methods provide basic support for a single asynchronous
return value.
* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`],
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`], and
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-webasynctask[`WebAsyncTask`] return values
in controller methods provide support for a single asynchronous return value.
* Controllers can xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[stream] multiple values, including
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data].
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data].
* Controllers can use reactive clients and return
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive types] for response handling.
@@ -96,6 +98,47 @@ xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[config
[[mvc-ann-async-webasynctask]]
== `WebAsyncTask`
`WebAsyncTask` is comparable to using xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[Callable]
but allows customizing additional settings such a request timeout value, and the
`AsyncTaskExecutor` to execute the `java.util.concurrent.Callable` with instead
of the defaults set up globally for Spring MVC. Below is an example of using `WebAsyncTask`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@GetMapping("/callable")
WebAsyncTask<String> handle() {
return new WebAsyncTask<String>(20000L,()->{
Thread.sleep(10000); //simulate long-running task
return "asynchronous request completed";
});
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@GetMapping("/callable")
fun handle(): WebAsyncTask<String> {
return WebAsyncTask(20000L) {
Thread.sleep(10000) // simulate long-running task
"asynchronous request completed"
}
}
----
======
[[mvc-ann-async-processing]]
== Processing
@@ -390,7 +433,7 @@ reactive types from the controller method.
Reactive return values are handled as follows:
* A single-value promise is adapted to, similar to using `DeferredResult`. Examples
include `Mono` (Reactor) or `Single` (RxJava).
include `CompletionStage` (JDK), Mono` (Reactor), and `Single` (RxJava).
* A multi-value stream with a streaming media type (such as `application/x-ndjson`
or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
`SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava).
@@ -10,18 +10,20 @@ to any controller. Moreover, as of 5.3, `@ExceptionHandler` methods in `@Control
can be used to handle exceptions from any `@Controller` or any other handler.
`@ControllerAdvice` is meta-annotated with `@Component` and therefore can be registered as
a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning]
. `@RestControllerAdvice` is meta-annotated with `@ControllerAdvice`
and `@ResponseBody`, and that means `@ExceptionHandler` methods will have their return
value rendered via response body message conversion, rather than via HTML views.
a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning].
`@RestControllerAdvice` is a shortcut annotation that combines `@ControllerAdvice`
with `@ResponseBody`, in effect simply an `@ControllerAdvice` whose exception handler
methods render to the response body.
On startup, `RequestMappingHandlerMapping` and `ExceptionHandlerExceptionResolver` detect
controller advice beans and apply them at runtime. Global `@ExceptionHandler` methods,
from an `@ControllerAdvice`, are applied _after_ local ones, from the `@Controller`.
By contrast, global `@ModelAttribute` and `@InitBinder` methods are applied _before_ local ones.
The `@ControllerAdvice` annotation has attributes that let you narrow the set of controllers
and handlers that they apply to. For example:
By default, both `@ControllerAdvice` and `@RestControllerAdvice` apply to any controller,
including `@Controller` and `@RestController`. Use attributes of the annotation to narrow
the set of controllers and handlers that they apply to. For example:
[tabs]
======
@@ -177,13 +177,9 @@ the content negotiation during the error handling phase will decide which conten
be converted through `HttpMessageConverter` instances and written to the response.
See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity].
| `ErrorResponse`
| `ErrorResponse`, `ProblemDetail`
| To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
| `ProblemDetail`
| To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
| `String`
| A view name to be resolved with `ViewResolver` implementations and used together with the
@@ -243,7 +243,7 @@ Kotlin::
======
If there is no `BindingResult` parameter after the `@ModelAttribute`, then
`MethodArgumentNotValueException` is raised with the validation errors. However, if method
a `MethodArgumentNotValidException` is raised with the validation errors. However, if method
validation applies because other parameters have `@jakarta.validation.Constraint` annotations,
then `HandlerMethodValidationException` is raised instead. For more details, see the section
xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
@@ -22,11 +22,7 @@ supported for all return values.
| `HttpHeaders`
| For returning a response with headers and no body.
| `ErrorResponse`
| To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
| `ProblemDetail`
| `ErrorResponse`, `ProblemDetail`
| To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
@@ -38,7 +38,7 @@ to inform the server that the original port was `443`.
==== X-Forwarded-Proto
While not standard, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto[`X-Forwarded-Proto: (https|http)`]
is a de-facto standard header that is used to communicate the original protocol (for example, https / https)
is a de-facto standard header that is used to communicate the original protocol (for example, https / http)
to a downstream server. For example, if a request of `https://example.com/resource` is sent to
a proxy which forwards the request to `http://localhost:8080/resource`, then a header of
`X-Forwarded-Proto: https` can be sent to inform the server that the original protocol was `https`.
@@ -119,4 +119,4 @@ https://example.com/api/app1/{path} -> http://localhost:8080/app1/{path}
In this case, the proxy has a prefix of `/api/app1` and the server has a prefix of
`/app1`. The proxy can send `X-Forwarded-Prefix: /api/app1` to have the original prefix
`/api/app1` override the server prefix `/app1`.
`/api/app1` override the server prefix `/app1`.
+6 -7
View File
@@ -8,20 +8,20 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
api(platform("io.micrometer:micrometer-bom:1.14.7"))
api(platform("io.micrometer:micrometer-bom:1.14.8"))
api(platform("io.netty:netty-bom:4.1.121.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.6"))
api(platform("io.projectreactor:reactor-bom:2024.0.7"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.26"))
api(platform("org.apache.groovy:groovy-bom:4.0.27"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.21"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.12.2"))
api(platform("org.mockito:mockito-bom:5.17.0"))
api(platform("org.junit:junit-bom:5.13.1"))
api(platform("org.mockito:mockito-bom:5.18.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.2")
@@ -47,7 +47,6 @@ dependencies {
api("com.thoughtworks.xstream:xstream:1.4.21")
api("commons-io:commons-io:2.15.0")
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
api("io.micrometer:context-propagation:1.1.1")
api("io.mockk:mockk:1.13.4")
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
@@ -100,7 +99,7 @@ dependencies {
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.4.4")
api("org.apache.httpcomponents.client5:httpclient5:5.5")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.7-SNAPSHOT
version=6.2.8
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
-1
View File
@@ -1,7 +1,6 @@
plugins {
id "com.gradle.develocity" version "3.19"
id "io.spring.ge.conventions" version "0.0.17"
id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0"
}
include "spring-aop"
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -112,12 +112,12 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
private BeanFactory beanFactory;
@Nullable
private transient ClassLoader pointcutClassLoader;
private transient volatile ClassLoader pointcutClassLoader;
@Nullable
private transient PointcutExpression pointcutExpression;
private transient volatile PointcutExpression pointcutExpression;
private transient boolean pointcutParsingFailed = false;
private transient volatile boolean pointcutParsingFailed;
/**
@@ -197,11 +197,14 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
* Lazily build the underlying AspectJ pointcut expression.
*/
private PointcutExpression obtainPointcutExpression() {
if (this.pointcutExpression == null) {
this.pointcutClassLoader = determinePointcutClassLoader();
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
PointcutExpression pointcutExpression = this.pointcutExpression;
if (pointcutExpression == null) {
ClassLoader pointcutClassLoader = determinePointcutClassLoader();
pointcutExpression = buildPointcutExpression(pointcutClassLoader);
this.pointcutClassLoader = pointcutClassLoader;
this.pointcutExpression = pointcutExpression;
}
return this.pointcutExpression;
return pointcutExpression;
}
/**
@@ -467,40 +470,24 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
}
private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) {
ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(this, targetMethod);
ShadowMatchKey key = new ShadowMatchKey(this, targetMethod);
ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(key);
if (shadowMatch == null) {
PointcutExpression fallbackExpression = null;
Method methodToMatch = targetMethod;
try {
PointcutExpression pointcutExpression = obtainPointcutExpression();
synchronized (pointcutExpression) {
shadowMatch = ShadowMatchUtils.getShadowMatch(key);
if (shadowMatch != null) {
return shadowMatch;
}
PointcutExpression fallbackExpression = null;
Method methodToMatch = targetMethod;
try {
shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch);
}
catch (ReflectionWorldException ex) {
// Failed to introspect target method, probably because it has been loaded
// in a special ClassLoader. Let's try the declaring ClassLoader instead...
try {
fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass());
if (fallbackExpression != null) {
shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch);
}
}
catch (ReflectionWorldException ex2) {
fallbackExpression = null;
}
}
if (targetMethod != originalMethod && (shadowMatch == null ||
(Proxy.isProxyClass(targetMethod.getDeclaringClass()) &&
(shadowMatch.neverMatches() || containsAnnotationPointcut())))) {
// Fall back to the plain original method in case of no resolvable match or a
// negative match on a proxy class (which doesn't carry any annotations on its
// redeclared methods), as well as for annotation pointcuts.
methodToMatch = originalMethod;
try {
shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch);
shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch);
}
catch (ReflectionWorldException ex) {
// Could neither introspect the target class nor the proxy class ->
// let's try the original method's declaring class before we give up...
// Failed to introspect target method, probably because it has been loaded
// in a special ClassLoader. Let's try the declaring ClassLoader instead...
try {
fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass());
if (fallbackExpression != null) {
@@ -511,21 +498,45 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
fallbackExpression = null;
}
}
if (targetMethod != originalMethod && (shadowMatch == null ||
(Proxy.isProxyClass(targetMethod.getDeclaringClass()) &&
(shadowMatch.neverMatches() || containsAnnotationPointcut())))) {
// Fall back to the plain original method in case of no resolvable match or a
// negative match on a proxy class (which doesn't carry any annotations on its
// redeclared methods), as well as for annotation pointcuts.
methodToMatch = originalMethod;
try {
shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch);
}
catch (ReflectionWorldException ex) {
// Could neither introspect the target class nor the proxy class ->
// let's try the original method's declaring class before we give up...
try {
fallbackExpression = getFallbackPointcutExpression(methodToMatch.getDeclaringClass());
if (fallbackExpression != null) {
shadowMatch = fallbackExpression.matchesMethodExecution(methodToMatch);
}
}
catch (ReflectionWorldException ex2) {
fallbackExpression = null;
}
}
}
}
catch (Throwable ex) {
// Possibly AspectJ 1.8.10 encountering an invalid signature
logger.debug("PointcutExpression matching rejected target method", ex);
fallbackExpression = null;
}
if (shadowMatch == null) {
shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null);
}
else if (shadowMatch.maybeMatches() && fallbackExpression != null) {
shadowMatch = new DefensiveShadowMatch(shadowMatch,
fallbackExpression.matchesMethodExecution(methodToMatch));
}
shadowMatch = ShadowMatchUtils.setShadowMatch(key, shadowMatch);
}
catch (Throwable ex) {
// Possibly AspectJ 1.8.10 encountering an invalid signature
logger.debug("PointcutExpression matching rejected target method", ex);
fallbackExpression = null;
}
if (shadowMatch == null) {
shadowMatch = new ShadowMatchImpl(org.aspectj.util.FuzzyBoolean.NO, null, null, null);
}
else if (shadowMatch.maybeMatches() && fallbackExpression != null) {
shadowMatch = new DefensiveShadowMatch(shadowMatch,
fallbackExpression.matchesMethodExecution(methodToMatch));
}
shadowMatch = ShadowMatchUtils.setShadowMatch(this, targetMethod, shadowMatch);
}
return shadowMatch;
}
@@ -720,4 +731,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
}
}
private record ShadowMatchKey(AspectJExpressionPointcut expression, Method method) {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -16,24 +16,48 @@
package org.springframework.aop.aspectj;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.aspectj.weaver.tools.ShadowMatch;
import org.springframework.aop.support.ExpressionPointcut;
import org.springframework.lang.Nullable;
/**
* Internal {@link ShadowMatch} utilities.
*
* @author Stephane Nicoll
* @author Juergen Hoeller
* @since 6.2
*/
public abstract class ShadowMatchUtils {
private static final Map<Key, ShadowMatch> shadowMatchCache = new ConcurrentHashMap<>(256);
private static final Map<Object, ShadowMatch> shadowMatchCache = new ConcurrentHashMap<>(256);
/**
* Find a {@link ShadowMatch} for the specified key.
* @param key the key to use
* @return the {@code ShadowMatch} to use for the specified key,
* or {@code null} if none found
*/
@Nullable
static ShadowMatch getShadowMatch(Object key) {
return shadowMatchCache.get(key);
}
/**
* Associate the {@link ShadowMatch} with the specified key.
* If an entry already exists, the given {@code shadowMatch} is ignored.
* @param key the key to use
* @param shadowMatch the shadow match to use for this key
* if none already exists
* @return the shadow match to use for the specified key
*/
static ShadowMatch setShadowMatch(Object key, ShadowMatch shadowMatch) {
ShadowMatch existing = shadowMatchCache.putIfAbsent(key, shadowMatch);
return (existing != null ? existing : shadowMatch);
}
/**
* Clear the cache of computed {@link ShadowMatch} instances.
@@ -42,34 +66,4 @@ public abstract class ShadowMatchUtils {
shadowMatchCache.clear();
}
/**
* Return the {@link ShadowMatch} for the specified {@link ExpressionPointcut}
* and {@link Method} or {@code null} if none is found.
* @param expression the expression
* @param method the method
* @return the {@code ShadowMatch} to use for the specified expression and method
*/
@Nullable
static ShadowMatch getShadowMatch(ExpressionPointcut expression, Method method) {
return shadowMatchCache.get(new Key(expression, method));
}
/**
* Associate the {@link ShadowMatch} to the specified {@link ExpressionPointcut}
* and method. If an entry already exists, the given {@code shadowMatch} is
* ignored.
* @param expression the expression
* @param method the method
* @param shadowMatch the shadow match to use for this expression and method
* if none already exists
* @return the shadow match to use for the specified expression and method
*/
static ShadowMatch setShadowMatch(ExpressionPointcut expression, Method method, ShadowMatch shadowMatch) {
ShadowMatch existing = shadowMatchCache.putIfAbsent(new Key(expression, method), shadowMatch);
return (existing != null ? existing : shadowMatch);
}
private record Key(ExpressionPointcut expression, Method method) {}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -24,7 +24,7 @@ import java.util.Map;
import org.springframework.lang.Nullable;
/**
* Abstract superclass for counting advices etc.
* Abstract superclass for counting advice, etc.
*
* @author Rod Johnson
* @author Chris Beams
@@ -62,7 +62,7 @@ public class MethodCounter implements Serializable {
*/
@Override
public boolean equals(@Nullable Object other) {
return (other != null && other.getClass() == this.getClass());
return (other != null && getClass() == other.getClass());
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -33,6 +33,7 @@ import java.util.Map;
import java.util.Set;
import kotlin.jvm.JvmClassMappingKt;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.reflect.KClass;
import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter;
@@ -669,7 +670,9 @@ public abstract class BeanUtils {
ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor));
Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor);
Assert.state(paramNames.length == ctor.getParameterCount(),
int parameterCount = (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasDefaultConstructorMarker(ctor) ?
ctor.getParameterCount() - 1 : ctor.getParameterCount());
Assert.state(paramNames.length == parameterCount,
() -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
return paramNames;
}
@@ -939,6 +942,11 @@ public abstract class BeanUtils {
}
return kotlinConstructor.callBy(argParameters);
}
public static boolean hasDefaultConstructorMarker(Constructor<?> ctor) {
int parameterCount = ctor.getParameterCount();
return parameterCount > 0 && ctor.getParameters()[parameterCount -1].getType() == DefaultConstructorMarker.class;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -68,10 +68,10 @@ public class AutowiredArgumentsCodeGenerator {
for (int i = startIndex; i < parameterTypes.length; i++) {
code.add(i > startIndex ? ", " : "");
if (!ambiguous) {
code.add("$L.get($L)", variableName, i - startIndex);
code.add("$L.get($L)", variableName, i);
}
else {
code.add("$L.get($L, $T.class)", variableName, i - startIndex, parameterTypes[i]);
code.add("$L.get($L, $T.class)", variableName, i, parameterTypes[i]);
}
}
return code.build();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2025 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.
@@ -29,10 +29,6 @@ import org.springframework.lang.Nullable;
* load and register methods for bean definitions, specific to
* their bean definition format.
*
* <p>Note that a bean definition reader does not have to implement
* this interface. It only serves as a suggestion for bean definition
* readers that want to follow standard naming conventions.
*
* @author Juergen Hoeller
* @since 1.1
* @see org.springframework.core.io.Resource
@@ -1679,7 +1679,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
if (autowiredBeanNames != null) {
autowiredBeanNames.add(dependencyName);
}
boolean preExisting = containsSingleton(dependencyName);
Object dependencyBean = getBean(dependencyName);
if (preExisting && dependencyBean instanceof NullBean) {
// for backwards compatibility with addCandidateEntry in the regular code path
dependencyBean = null;
}
return resolveInstance(dependencyBean, descriptor, type, dependencyName);
}
}
@@ -1760,7 +1765,6 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
throw new BeanNotOfRequiredTypeException(name, type, candidate.getClass());
}
return result;
}
@Nullable
@@ -2245,7 +2249,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
* i.e. whether the candidate points back to the original bean or to a factory method
* on the original bean.
*/
@Contract("null, _ -> false;_, null -> false;")
@Contract("null, _ -> false; _, null -> false;")
private boolean isSelfReference(@Nullable String beanName, @Nullable String candidateName) {
return (beanName != null && candidateName != null &&
(beanName.equals(candidateName) || (containsBeanDefinition(candidateName) &&
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -33,13 +33,14 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Chris Beams
*/
class FactoryBeanLookupTests {
private BeanFactory beanFactory;
private final BeanFactory beanFactory = new DefaultListableBeanFactory();
@BeforeEach
void setUp() {
beanFactory = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader((BeanDefinitionRegistry) beanFactory).loadBeanDefinitions(
new ClassPathResource("FactoryBeanLookupTests-context.xml", this.getClass()));
new ClassPathResource("FactoryBeanLookupTests-context.xml", getClass()));
}
@Test
@@ -71,6 +72,7 @@ class FactoryBeanLookupTests {
Foo foo = beanFactory.getBean("fooFactory", Foo.class);
assertThat(foo).isNotNull();
}
}
class FooFactoryBean extends AbstractFactoryBean<Foo> {
@@ -960,11 +960,34 @@ class AutowiredAnnotationBeanPostProcessorTests {
@Test
void constructorResourceInjectionWithNoCandidatesAndNoFallback() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class));
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(() -> bf.getBean("annotatedBean"))
.satisfies(methodParameterDeclaredOn(ConstructorWithoutFallbackBean.class));
}
@Test
void constructorResourceInjectionWithCandidateAndNoFallback() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class));
RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class);
tb.setFactoryMethodName("createTestBean");
bf.registerBeanDefinition("testBean", tb);
bf.getBean("testBean");
assertThat(bf.getBean("annotatedBean", ConstructorWithoutFallbackBean.class).getTestBean3()).isNull();
}
@Test
void constructorResourceInjectionWithNameMatchingCandidateAndNoFallback() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ConstructorWithoutFallbackBean.class));
RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class);
tb.setFactoryMethodName("createTestBean");
bf.registerBeanDefinition("testBean3", tb);
bf.getBean("testBean3");
assertThat(bf.getBean("annotatedBean", ConstructorWithoutFallbackBean.class).getTestBean3()).isNull();
}
@Test
void constructorResourceInjectionWithSometimesNullBeanEarly() {
RootBeanDefinition bd = new RootBeanDefinition(ConstructorWithNullableArgument.class);
@@ -1193,6 +1216,7 @@ class AutowiredAnnotationBeanPostProcessorTests {
@Test
void singleConstructorInjectionWithMissingDependency() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class));
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(() -> bf.getBean("annotatedBean"));
}
@@ -1203,6 +1227,7 @@ class AutowiredAnnotationBeanPostProcessorTests {
RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class);
tb.setFactoryMethodName("createTestBean");
bf.registerBeanDefinition("testBean", tb);
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(() -> bf.getBean("annotatedBean"));
}
@@ -2428,7 +2453,7 @@ class AutowiredAnnotationBeanPostProcessorTests {
}
@Test
@SuppressWarnings("unchecked")
@SuppressWarnings({ "unchecked", "rawtypes" })
void genericsBasedConstructorInjectionWithNonTypedTarget() {
RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
@@ -3060,7 +3085,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
protected ITestBean testBean3;
@Autowired(required = false)
public ConstructorWithoutFallbackBean(ITestBean testBean3) {
this.testBean3 = testBean3;
}
@@ -3075,7 +3099,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
protected ITestBean testBean3;
@Autowired(required = false)
public ConstructorWithNullableArgument(@Nullable ITestBean testBean3) {
this.testBean3 = testBean3;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@@ -67,7 +67,7 @@ class AutowiredArgumentsCodeGeneratorTests {
AutowiredArgumentsCodeGenerator generator = new AutowiredArgumentsCodeGenerator(
Outer.Nested.class, constructor);
assertThat(generator.generateCode(constructor.getParameterTypes(), 1))
.hasToString("args.get(0), args.get(1)");
.hasToString("args.get(1), args.get(2)");
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -36,6 +36,7 @@ import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.support.AnnotationConsumer;
import org.junit.jupiter.params.support.ParameterDeclarations;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.beans.factory.ObjectProvider;
@@ -691,7 +692,7 @@ class BeanInstanceSupplierTests {
}
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
public Stream<? extends Arguments> provideArguments(ParameterDeclarations parameters, ExtensionContext context) {
return this.source.provideArguments(context);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -133,11 +133,13 @@ class InstanceSupplierCodeGeneratorTests {
@Test
void generateWhenHasConstructorWithInnerClassAndParameter() {
BeanDefinition beanDefinition = new RootBeanDefinition(EnvironmentAwareComponent.class);
StandardEnvironment environment = new StandardEnvironment();
this.beanFactory.registerSingleton("configuration", new InnerComponentConfiguration());
this.beanFactory.registerSingleton("environment", new StandardEnvironment());
this.beanFactory.registerSingleton("environment", environment);
compile(beanDefinition, (instanceSupplier, compiled) -> {
Object bean = getBean(beanDefinition, instanceSupplier);
assertThat(bean).isInstanceOf(EnvironmentAwareComponent.class);
assertThat(bean).hasFieldOrPropertyWithValue("environment", environment);
assertThat(compiled.getSourceFile()).contains(
"getBeanFactory().getBean(InnerComponentConfiguration.class).new EnvironmentAwareComponent(");
});
@@ -162,11 +164,13 @@ class InstanceSupplierCodeGeneratorTests {
@Test
void generateWhenHasNonPublicConstructorWithInnerClassAndParameter() {
BeanDefinition beanDefinition = new RootBeanDefinition(EnvironmentAwareComponentWithoutPublicConstructor.class);
StandardEnvironment environment = new StandardEnvironment();
this.beanFactory.registerSingleton("configuration", new InnerComponentConfiguration());
this.beanFactory.registerSingleton("environment", new StandardEnvironment());
this.beanFactory.registerSingleton("environment", environment);
compile(beanDefinition, (instanceSupplier, compiled) -> {
Object bean = getBean(beanDefinition, instanceSupplier);
assertThat(bean).isInstanceOf(EnvironmentAwareComponentWithoutPublicConstructor.class);
assertThat(bean).hasFieldOrPropertyWithValue("environment", environment);
assertThat(compiled.getSourceFile()).doesNotContain(
"getBeanFactory().getBean(InnerComponentConfiguration.class)");
});
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -84,7 +84,7 @@ class PropertyPlaceholderConfigurerTests {
.getBeanDefinition());
PropertyPlaceholderConfigurer pc = new PropertyPlaceholderConfigurer();
Resource resource = new ClassPathResource("PropertyPlaceholderConfigurerTests.properties", this.getClass());
Resource resource = new ClassPathResource("PropertyPlaceholderConfigurerTests.properties", getClass());
pc.setLocation(resource);
pc.postProcessBeanFactory(bf);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -46,7 +46,7 @@ class DuplicateBeanIdTests {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf);
assertThatException().as("duplicate ids in same nesting level").isThrownBy(() ->
reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-sameLevel-context.xml", this.getClass())));
reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-sameLevel-context.xml", getClass())));
}
@Test
@@ -54,7 +54,7 @@ class DuplicateBeanIdTests {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.setAllowBeanDefinitionOverriding(true);
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(bf);
reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-multiLevel-context.xml", this.getClass()));
reader.loadBeanDefinitions(new ClassPathResource("DuplicateBeanIdTests-multiLevel-context.xml", getClass()));
TestBean testBean = bf.getBean(TestBean.class); // there should be only one
assertThat(testBean.getName()).isEqualTo("nested");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -36,7 +36,7 @@ class NestedBeansElementAttributeRecursionTests {
void defaultLazyInit() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader(bf).loadBeanDefinitions(
new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", this.getClass()));
new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", getClass()));
assertLazyInits(bf);
}
@@ -47,7 +47,7 @@ class NestedBeansElementAttributeRecursionTests {
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf);
xmlBeanDefinitionReader.setValidating(false);
xmlBeanDefinitionReader.loadBeanDefinitions(
new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", this.getClass()));
new ClassPathResource("NestedBeansElementAttributeRecursionTests-lazy-context.xml", getClass()));
assertLazyInits(bf);
}
@@ -70,7 +70,7 @@ class NestedBeansElementAttributeRecursionTests {
void defaultMerge() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader(bf).loadBeanDefinitions(
new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", this.getClass()));
new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", getClass()));
assertMerge(bf);
}
@@ -81,7 +81,7 @@ class NestedBeansElementAttributeRecursionTests {
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf);
xmlBeanDefinitionReader.setValidating(false);
xmlBeanDefinitionReader.loadBeanDefinitions(
new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", this.getClass()));
new ClassPathResource("NestedBeansElementAttributeRecursionTests-merge-context.xml", getClass()));
assertMerge(bf);
}
@@ -109,7 +109,7 @@ class NestedBeansElementAttributeRecursionTests {
void defaultAutowireCandidates() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader(bf).loadBeanDefinitions(
new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", this.getClass()));
new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", getClass()));
assertAutowireCandidates(bf);
}
@@ -120,7 +120,7 @@ class NestedBeansElementAttributeRecursionTests {
XmlBeanDefinitionReader xmlBeanDefinitionReader = new XmlBeanDefinitionReader(bf);
xmlBeanDefinitionReader.setValidating(false);
xmlBeanDefinitionReader.loadBeanDefinitions(
new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", this.getClass()));
new ClassPathResource("NestedBeansElementAttributeRecursionTests-autowire-candidates-context.xml", getClass()));
assertAutowireCandidates(bf);
}
@@ -149,7 +149,7 @@ class NestedBeansElementAttributeRecursionTests {
void initMethod() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
new XmlBeanDefinitionReader(bf).loadBeanDefinitions(
new ClassPathResource("NestedBeansElementAttributeRecursionTests-init-destroy-context.xml", this.getClass()));
new ClassPathResource("NestedBeansElementAttributeRecursionTests-init-destroy-context.xml", getClass()));
InitDestroyBean beanA = bf.getBean("beanA", InitDestroyBean.class);
InitDestroyBean beanB = bf.getBean("beanB", InitDestroyBean.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -26,15 +26,14 @@ import org.springframework.core.io.Resource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for new nested beans element support in Spring XML
*
* @author Chris Beams
*/
class NestedBeansElementTests {
private final Resource XML =
new ClassPathResource("NestedBeansElementTests-context.xml", this.getClass());
private final Resource XML = new ClassPathResource("NestedBeansElementTests-context.xml", getClass());
@Test
void getBean_withoutActiveProfile() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -93,7 +93,6 @@ class BeanUtilsKotlinTests {
@Test
fun `Instantiate value class`() {
val constructor = BeanUtils.findPrimaryConstructor(ValueClass::class.java)!!
assertThat(constructor).isNotNull()
val value = "Hello value class!"
val instance = BeanUtils.instantiateClass(constructor, value)
assertThat(instance).isEqualTo(ValueClass(value))
@@ -102,7 +101,6 @@ class BeanUtilsKotlinTests {
@Test
fun `Instantiate value class with multiple constructors`() {
val constructor = BeanUtils.findPrimaryConstructor(ValueClassWithMultipleConstructors::class.java)!!
assertThat(constructor).isNotNull()
val value = "Hello value class!"
val instance = BeanUtils.instantiateClass(constructor, value)
assertThat(instance).isEqualTo(ValueClassWithMultipleConstructors(value))
@@ -111,7 +109,6 @@ class BeanUtilsKotlinTests {
@Test
fun `Instantiate class with value class parameter`() {
val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithValueClass::class.java)!!
assertThat(constructor).isNotNull()
val value = ValueClass("Hello value class!")
val instance = BeanUtils.instantiateClass(constructor, value)
assertThat(instance).isEqualTo(ConstructorWithValueClass(value))
@@ -120,7 +117,6 @@ class BeanUtilsKotlinTests {
@Test
fun `Instantiate class with nullable value class parameter`() {
val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithNullableValueClass::class.java)!!
assertThat(constructor).isNotNull()
val value = ValueClass("Hello value class!")
var instance = BeanUtils.instantiateClass(constructor, value)
assertThat(instance).isEqualTo(ConstructorWithNullableValueClass(value))
@@ -131,7 +127,6 @@ class BeanUtilsKotlinTests {
@Test
fun `Instantiate primitive value class`() {
val constructor = BeanUtils.findPrimaryConstructor(PrimitiveValueClass::class.java)!!
assertThat(constructor).isNotNull()
val value = 0
val instance = BeanUtils.instantiateClass(constructor, value)
assertThat(instance).isEqualTo(PrimitiveValueClass(value))
@@ -140,7 +135,6 @@ class BeanUtilsKotlinTests {
@Test
fun `Instantiate class with primitive value class parameter`() {
val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithPrimitiveValueClass::class.java)!!
assertThat(constructor).isNotNull()
val value = PrimitiveValueClass(0)
val instance = BeanUtils.instantiateClass(constructor, value)
assertThat(instance).isEqualTo(ConstructorWithPrimitiveValueClass(value))
@@ -149,7 +143,6 @@ class BeanUtilsKotlinTests {
@Test
fun `Instantiate class with nullable primitive value class parameter`() {
val constructor = BeanUtils.findPrimaryConstructor(ConstructorWithNullablePrimitiveValueClass::class.java)!!
assertThat(constructor).isNotNull()
val value = PrimitiveValueClass(0)
var instance = BeanUtils.instantiateClass(constructor, value)
assertThat(instance).isEqualTo(ConstructorWithNullablePrimitiveValueClass(value))
@@ -157,6 +150,48 @@ class BeanUtilsKotlinTests {
assertThat(instance).isEqualTo(ConstructorWithNullablePrimitiveValueClass(null))
}
@Test
fun `Get parameter names with Foo`() {
val ctor = BeanUtils.findPrimaryConstructor(Foo::class.java)!!
val names = BeanUtils.getParameterNames(ctor)
assertThat(names).containsExactly("param1", "param2")
}
@Test
fun `Get parameter names filters out DefaultConstructorMarker with ConstructorWithValueClass`() {
val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithValueClass::class.java)!!
val names = BeanUtils.getParameterNames(ctor)
assertThat(names).containsExactly("value")
}
@Test
fun `getParameterNames filters out DefaultConstructorMarker with ConstructorWithNullableValueClass`() {
val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithNullableValueClass::class.java)!!
val names = BeanUtils.getParameterNames(ctor)
assertThat(names).containsExactly("value")
}
@Test
fun `getParameterNames filters out DefaultConstructorMarker with ConstructorWithPrimitiveValueClass`() {
val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithPrimitiveValueClass::class.java)!!
val names = BeanUtils.getParameterNames(ctor)
assertThat(names).containsExactly("value")
}
@Test
fun `getParameterNames filters out DefaultConstructorMarker with ConstructorWithNullablePrimitiveValueClass`() {
val ctor = BeanUtils.findPrimaryConstructor(ConstructorWithNullablePrimitiveValueClass::class.java)!!
val names = BeanUtils.getParameterNames(ctor)
assertThat(names).containsExactly("value")
}
@Test
fun `getParameterNames with ClassWithZeroParameterCtor`() {
val ctor = BeanUtils.findPrimaryConstructor(ClassWithZeroParameterCtor::class.java)!!
val names = BeanUtils.getParameterNames(ctor)
assertThat(names).isEmpty()
}
class Foo(val param1: String, val param2: Int)
@@ -216,4 +251,6 @@ class BeanUtilsKotlinTests {
data class ConstructorWithNullablePrimitiveValueClass(val value: PrimitiveValueClass?)
class ClassWithZeroParameterCtor()
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -28,7 +28,10 @@ public class InnerComponentConfiguration {
public class EnvironmentAwareComponent {
final Environment environment;
public EnvironmentAwareComponent(Environment environment) {
this.environment = environment;
}
}
@@ -40,7 +43,10 @@ public class InnerComponentConfiguration {
public class EnvironmentAwareComponentWithoutPublicConstructor {
final Environment environment;
EnvironmentAwareComponentWithoutPublicConstructor(Environment environment) {
this.environment = environment;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -16,6 +16,7 @@
package org.springframework.context.annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
@@ -27,6 +28,7 @@ import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
@@ -286,8 +288,8 @@ class ConfigurationClassBeanDefinitionReader {
}
if (logger.isTraceEnabled()) {
logger.trace(String.format("Registering bean definition for @Bean method %s.%s()",
configClass.getMetadata().getClassName(), beanName));
logger.trace("Registering bean definition for @Bean method %s.%s()"
.formatted(configClass.getMetadata().getClassName(), beanName));
}
this.registry.registerBeanDefinition(beanName, beanDefToRegister);
}
@@ -344,9 +346,8 @@ class ConfigurationClassBeanDefinitionReader {
"@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
}
if (logger.isDebugEnabled()) {
logger.debug(String.format("Skipping bean definition for %s: a definition for bean '%s' " +
"already exists. This top-level bean definition is considered as an override.",
beanMethod, beanName));
logger.debug("Skipping bean definition for %s: a definition for bean '%s' already exists. " +
"This top-level bean definition is considered as an override.".formatted(beanMethod, beanName));
}
return true;
}
@@ -372,9 +373,11 @@ class ConfigurationClassBeanDefinitionReader {
BeanDefinitionReader reader = readerInstanceCache.get(readerClass);
if (reader == null) {
try {
Constructor<? extends BeanDefinitionReader> constructor =
readerClass.getDeclaredConstructor(BeanDefinitionRegistry.class);
// Instantiate the specified BeanDefinitionReader
reader = readerClass.getConstructor(BeanDefinitionRegistry.class).newInstance(this.registry);
// Delegate the current ResourceLoader to it if possible
reader = BeanUtils.instantiateClass(constructor, this.registry);
// Delegate the current ResourceLoader and Environment to it if possible
if (reader instanceof AbstractBeanDefinitionReader abdr) {
abdr.setResourceLoader(this.resourceLoader);
abdr.setEnvironment(this.environment);
@@ -383,11 +386,9 @@ class ConfigurationClassBeanDefinitionReader {
}
catch (Throwable ex) {
throw new IllegalStateException(
"Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]");
"Could not instantiate BeanDefinitionReader class [" + readerClass.getName() + "]", ex);
}
}
// TODO SPR-6310: qualify relative path locations as done in AbstractContextLoader.modifyLocations
reader.loadBeanDefinitions(resource);
});
}
@@ -16,6 +16,7 @@
package org.springframework.context.annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
@@ -139,14 +140,20 @@ class ConfigurationClassEnhancer {
}
/**
* Checks whether the given config class relies on package visibility,
* either for the class itself or for any of its {@code @Bean} methods.
* Checks whether the given config class relies on package visibility, either for
* the class and any of its constructors or for any of its {@code @Bean} methods.
*/
private boolean reliesOnPackageVisibility(Class<?> configSuperClass) {
int mod = configSuperClass.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
for (Constructor<?> ctor : configSuperClass.getDeclaredConstructors()) {
mod = ctor.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
}
for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) {
if (BeanAnnotationHelper.isBeanAnnotated(method)) {
mod = method.getModifiers();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2025 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.
@@ -29,14 +29,15 @@ import org.springframework.core.annotation.AliasFor;
* Indicates one or more resources containing bean definitions to import.
*
* <p>Like {@link Import @Import}, this annotation provides functionality similar to
* the {@code <import/>} element in Spring XML. It is typically used when designing
* {@link Configuration @Configuration} classes to be bootstrapped by an
* {@link AnnotationConfigApplicationContext}, but where some XML functionality such
* as namespaces is still necessary.
* the {@code <import/>} element in Spring XML configuration. It is typically used
* when designing {@link Configuration @Configuration} classes to be bootstrapped by
* an {@link AnnotationConfigApplicationContext}, but where some XML functionality
* such as namespaces is still necessary.
*
* <p>By default, arguments to the {@link #value} attribute will be processed using a
* <p>By default, arguments to the {@link #locations() locations} or {@link #value() value}
* attribute will be processed using a
* {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader GroovyBeanDefinitionReader}
* if ending in {@code ".groovy"}; otherwise, an
* for resource locations ending in {@code ".groovy"}; otherwise, an
* {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader XmlBeanDefinitionReader}
* will be used to parse Spring {@code <beans/>} XML files. Optionally, the {@link #reader}
* attribute may be declared, allowing the user to choose a custom {@link BeanDefinitionReader}
@@ -77,12 +78,19 @@ public @interface ImportResource {
/**
* {@link BeanDefinitionReader} implementation to use when processing
* resources specified via the {@link #value} attribute.
* resources specified via the {@link #locations() locations} or
* {@link #value() value} attribute.
* <p>The configured {@code BeanDefinitionReader} type must declare a
* constructor that accepts a single
* {@link org.springframework.beans.factory.support.BeanDefinitionRegistry
* BeanDefinitionRegistry} argument.
* <p>By default, the reader will be adapted to the resource path specified:
* {@code ".groovy"} files will be processed with a
* {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader GroovyBeanDefinitionReader};
* whereas, all other resources will be processed with an
* {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader XmlBeanDefinitionReader}.
* {@link org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader
* GroovyBeanDefinitionReader}; whereas, all other resources will be processed
* with an {@link org.springframework.beans.factory.xml.XmlBeanDefinitionReader
* XmlBeanDefinitionReader}.
* @see #locations
* @see #value
*/
Class<? extends BeanDefinitionReader> reader() default BeanDefinitionReader.class;
@@ -24,6 +24,7 @@ import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PlaceholderConfigurerSupport;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.ConfigurablePropertyResolver;
import org.springframework.core.env.Environment;
@@ -243,16 +244,34 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
@Override
@Nullable
public Object getProperty(String name) {
// Declare String as covariant return type, since a String is actually required.
public String getProperty(String name) {
for (PropertySource<?> propertySource : super.source.getPropertySources()) {
Object candidate = propertySource.getProperty(name);
if (candidate != null) {
return candidate;
return convertToString(candidate);
}
}
return null;
}
/**
* Convert the supplied value to a {@link String} using the {@link ConversionService}
* from the {@link Environment}.
* <p>This is a modified version of
* {@link org.springframework.core.env.AbstractPropertyResolver#convertValueIfNecessary(Object, Class)}.
* @param value the value to convert
* @return the converted value, or the original value if no conversion is necessary
* @since 6.2.8
*/
@Nullable
private String convertToString(Object value) {
if (value instanceof String string) {
return string;
}
return super.source.getConversionService().convert(value, String.class);
}
@Override
public String toString() {
return "ConfigurableEnvironmentPropertySource {propertySources=" + super.source.getPropertySources() + "}";
@@ -279,7 +298,8 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
@Override
@Nullable
public Object getProperty(String name) {
// Declare String as covariant return type, since a String is actually required.
public String getProperty(String name) {
return super.source.getProperty(name);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -250,7 +250,7 @@ public abstract class ValidationUtils {
Assert.notNull(errors, "Errors object must not be null");
Object value = errors.getFieldValue(field);
if (value == null ||!StringUtils.hasText(value.toString())) {
if (value == null || !StringUtils.hasText(value.toString())) {
errors.rejectValue(field, errorCode, errorArgs, defaultMessage);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -130,7 +130,7 @@ class SimpleKeyGeneratorTests {
private Object generateKey(Object[] arguments) {
Method method = ReflectionUtils.findMethod(this.getClass(), "generateKey", Object[].class);
Method method = ReflectionUtils.findMethod(getClass(), "generateKey", Object[].class);
return this.generator.generate(this, method, arguments);
}
@@ -104,6 +104,31 @@ class ConfigurationClassEnhancerTests {
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicConstructor() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader);
assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicMethod() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
@@ -160,6 +185,19 @@ class ConfigurationClassEnhancerTests {
}
@Configuration
public static class MyConfigWithNonPublicConstructor {
MyConfigWithNonPublicConstructor() {
}
@Bean
public String myBean() {
return "bean";
}
}
@Configuration
public static class MyConfigWithNonPublicMethod {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -16,8 +16,6 @@
package org.springframework.context.annotation.configuration;
import java.util.Collections;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.Test;
@@ -25,18 +23,18 @@ import org.junit.jupiter.api.Test;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.core.testfixture.env.MockPropertySource;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link ImportResource} support.
* Integration tests for {@link ImportResource @ImportResource} support.
*
* @author Chris Beams
* @author Juergen Hoeller
@@ -45,81 +43,88 @@ import static org.assertj.core.api.Assertions.assertThat;
class ImportResourceTests {
@Test
void importXml() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlConfig.class);
assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue();
TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class);
assertThat(tb.getName()).isEqualTo("myName");
ctx.close();
void importResource() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlConfig.class)) {
assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue();
TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class);
assertThat(tb.getName()).isEqualTo("myName");
}
}
@Test
void importXmlIsInheritedFromSuperclassDeclarations() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class);
assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue();
ctx.close();
void importResourceIsInheritedFromSuperclassDeclarations() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(FirstLevelSubConfig.class)) {
assertThat(ctx.containsBean("xmlDeclaredBean")).isTrue();
}
}
@Test
void importXmlIsMergedFromSuperclassDeclarations() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SecondLevelSubConfig.class);
assertThat(ctx.containsBean("secondLevelXmlDeclaredBean")).as("failed to pick up second-level-declared XML bean").isTrue();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("failed to pick up parent-declared XML bean").isTrue();
ctx.close();
void importResourceIsMergedFromSuperclassDeclarations() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(SecondLevelSubConfig.class)) {
assertThat(ctx.containsBean("secondLevelXmlDeclaredBean")).as("failed to pick up second-level-declared XML bean").isTrue();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("failed to pick up parent-declared XML bean").isTrue();
}
}
@Test
void importXmlWithNamespaceConfig() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class);
Object bean = ctx.getBean("proxiedXmlBean");
assertThat(AopUtils.isAopProxy(bean)).isTrue();
ctx.close();
void importResourceWithNamespaceConfig() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithAopNamespaceConfig.class)) {
Object bean = ctx.getBean("proxiedXmlBean");
assertThat(AopUtils.isAopProxy(bean)).isTrue();
}
}
@Test
void importXmlWithOtherConfigurationClass() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithConfigurationClass.class);
assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue();
TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class);
assertThat(tb.getName()).isEqualTo("myName");
ctx.close();
void importResourceWithOtherConfigurationClass() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlWithConfigurationClass.class)) {
assertThat(ctx.containsBean("javaDeclaredBean")).as("did not contain java-declared bean").isTrue();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue();
TestBean tb = ctx.getBean("javaDeclaredBean", TestBean.class);
assertThat(tb.getName()).isEqualTo("myName");
}
}
@Test
void importWithPlaceholder() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
PropertySource<?> propertySource = new MapPropertySource("test",
Collections.<String, Object> singletonMap("test", "springframework"));
ctx.getEnvironment().getPropertySources().addFirst(propertySource);
ctx.register(ImportXmlConfig.class);
ctx.refresh();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue();
ctx.close();
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext()) {
ctx.getEnvironment().getPropertySources().addFirst(new MockPropertySource("test").withProperty("test", "springframework"));
ctx.register(ImportXmlConfig.class);
ctx.refresh();
assertThat(ctx.containsBean("xmlDeclaredBean")).as("did not contain xml-declared bean").isTrue();
}
}
@Test
void importXmlWithAutowiredConfig() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class);
String name = ctx.getBean("xmlBeanName", String.class);
assertThat(name).isEqualTo("xml.declared");
ctx.close();
void importResourceWithAutowiredConfig() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportXmlAutowiredConfig.class)) {
String name = ctx.getBean("xmlBeanName", String.class);
assertThat(name).isEqualTo("xml.declared");
}
}
@Test
void importNonXmlResource() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class);
assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue();
ctx.close();
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportNonXmlResourceConfig.class)) {
assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue();
}
}
@Test
void importResourceWithPrivateReader() {
try (AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ImportWithPrivateReaderConfig.class)) {
assertThat(ctx.containsBean("propertiesDeclaredBean")).isTrue();
}
}
@Configuration
@ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml")
static class ImportXmlConfig {
@Value("${name}")
private String name;
@Bean public TestBean javaDeclaredBean() {
return new TestBean(this.name);
}
@@ -146,6 +151,7 @@ class ImportResourceTests {
@Aspect
static class AnAspect {
@Before("execution(* org.springframework.beans.testfixture.beans.TestBean.*(..))")
public void advice() { }
}
@@ -158,18 +164,37 @@ class ImportResourceTests {
@Configuration
@ImportResource("classpath:org/springframework/context/annotation/configuration/ImportXmlConfig-context.xml")
static class ImportXmlAutowiredConfig {
@Autowired TestBean xmlDeclaredBean;
@Bean public String xmlBeanName() {
@Autowired
TestBean xmlDeclaredBean;
@Bean
public String xmlBeanName() {
return xmlDeclaredBean.getName();
}
}
@SuppressWarnings("deprecation")
@Configuration
@ImportResource(locations = "classpath:org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig-context.properties",
@ImportResource(locations = "org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties",
reader = org.springframework.beans.factory.support.PropertiesBeanDefinitionReader.class)
static class ImportNonXmlResourceConfig {
}
@SuppressWarnings("deprecation")
@Configuration
@ImportResource(locations = "org/springframework/context/annotation/configuration/ImportNonXmlResourceConfig.properties",
reader = PrivatePropertiesBeanDefinitionReader.class)
static class ImportWithPrivateReaderConfig {
}
@SuppressWarnings("deprecation")
private static class PrivatePropertiesBeanDefinitionReader
extends org.springframework.beans.factory.support.PropertiesBeanDefinitionReader {
PrivatePropertiesBeanDefinitionReader(BeanDefinitionRegistry registry) {
super(registry);
}
}
}
@@ -41,6 +41,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.SpringProperties;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.AbstractPropertyResolver;
import org.springframework.core.env.EnumerablePropertySource;
@@ -90,6 +92,38 @@ class PropertySourcesPlaceholderConfigurerTests {
assertThat(ppc.getAppliedPropertySources()).isNotNull();
}
/**
* Ensure that a {@link Converter} registered in the {@link ConversionService}
* used by the {@code Environment} is applied during placeholder resolution
* against a {@link PropertySource} registered in the {@code Environment}.
*/
@Test // gh-34936
void replacementFromEnvironmentPropertiesWithConversion() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("testBean",
genericBeanDefinition(TestBean.class)
.addPropertyValue("name", "${my.name}")
.getBeanDefinition());
record Point(int x, int y) {
}
Converter<Point, String> pointToStringConverter =
point -> "(%d,%d)".formatted(point.x, point.y);
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(Point.class, String.class, pointToStringConverter);
MockEnvironment env = new MockEnvironment();
env.setConversionService(conversionService);
env.setProperty("my.name", new Point(4,5));
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
ppc.setEnvironment(env);
ppc.postProcessBeanFactory(bf);
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("(4,5)");
}
/**
* Ensure that a {@link PropertySource} added to the {@code Environment} after context
* refresh (i.e., after {@link PropertySourcesPlaceholderConfigurer#postProcessBeanFactory()}
@@ -136,7 +170,7 @@ class PropertySourcesPlaceholderConfigurerTests {
.getBeanDefinition());
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
Resource resource = new ClassPathResource("PropertySourcesPlaceholderConfigurerTests.properties", this.getClass());
Resource resource = new ClassPathResource("PropertySourcesPlaceholderConfigurerTests.properties", getClass());
ppc.setLocation(resource);
ppc.postProcessBeanFactory(bf);
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -32,6 +32,7 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.ArgumentsProvider;
import org.junit.jupiter.params.provider.ArgumentsSource;
import org.junit.jupiter.params.support.ParameterDeclarations;
import static java.time.Instant.MAX;
import static java.time.Instant.MIN;
@@ -91,7 +92,7 @@ class InstantFormatterTests {
private static final Random random = new Random();
@Override
public final Stream<Arguments> provideArguments(ExtensionContext context) {
public final Stream<Arguments> provideArguments(ParameterDeclarations parameters, ExtensionContext context) {
return provideArguments().map(Arguments::of).limit(DATA_SET_SIZE);
}
@@ -137,7 +138,7 @@ class InstantFormatterTests {
private static final Random random = new Random();
@Override
public Stream<Arguments> provideArguments(ExtensionContext context) {
public Stream<Arguments> provideArguments(ParameterDeclarations parameters, ExtensionContext context) {
return random.longs(DATA_SET_SIZE, Long.MIN_VALUE, Long.MAX_VALUE)
.mapToObj(Instant::ofEpochMilli)
.map(instant -> instant.truncatedTo(ChronoUnit.MILLIS))
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -27,7 +27,7 @@ import org.springframework.core.testfixture.env.MockPropertySource;
* @author Chris Beams
* @author Sam Brannen
* @since 3.2
* @see org.springframework.core.testfixture.env.MockPropertySource
* @see MockPropertySource
*/
public class MockEnvironment extends AbstractEnvironment {
@@ -44,19 +44,23 @@ public class MockEnvironment extends AbstractEnvironment {
/**
* Set a property on the underlying {@link MockPropertySource} for this environment.
* @since 6.2.8
* @see MockPropertySource#setProperty(String, Object)
*/
public void setProperty(String key, String value) {
this.propertySource.setProperty(key, value);
public void setProperty(String name, Object value) {
this.propertySource.setProperty(name, value);
}
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* Convenient synonym for {@link #setProperty(String, Object)} that returns
* the current instance.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockEnvironment} instance
* @see MockPropertySource#withProperty
* @since 6.2.8
* @see MockPropertySource#withProperty(String, Object)
*/
public MockEnvironment withProperty(String key, String value) {
setProperty(key, value);
public MockEnvironment withProperty(String name, Object value) {
setProperty(name, value);
return this;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -81,7 +81,7 @@ public abstract class AbstractTypeReference implements TypeReference {
@Override
public int compareTo(TypeReference other) {
return this.getCanonicalName().compareToIgnoreCase(other.getCanonicalName());
return getCanonicalName().compareToIgnoreCase(other.getCanonicalName());
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -150,7 +150,8 @@ public class BindingReflectionHintsRegistrar {
String companionClassName = clazz.getCanonicalName() + KOTLIN_COMPANION_SUFFIX;
if (ClassUtils.isPresent(companionClassName, null)) {
Class<?> companionClass = ClassUtils.resolveClassName(companionClassName, null);
Method serializerMethod = ClassUtils.getMethodIfAvailable(companionClass, "serializer");
Method serializerMethod = ClassUtils.getMethodIfAvailable(companionClass, "serializer",
(Class<?>[]) null);
if (serializerMethod != null) {
hints.registerMethod(serializerMethod, ExecutableMode.INVOKE);
}
@@ -198,7 +198,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
public void validateRequiredProperties() {
MissingRequiredPropertiesException ex = new MissingRequiredPropertiesException();
for (String key : this.requiredProperties) {
if (this.getProperty(key) == null) {
if (getProperty(key) == null) {
ex.addMissingRequiredProperty(key);
}
}
@@ -45,7 +45,6 @@ import org.springframework.util.ResourceUtils;
*/
public abstract class AbstractFileResolvingResource extends AbstractResource {
@SuppressWarnings("try")
@Override
public boolean exists() {
try {
@@ -90,9 +89,15 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
// existence of the entry (or the jar root in case of no entryName).
// getJarFile() called for enforced presence check of the jar file,
// throwing a NoSuchFileException otherwise (turned to false below).
try (JarFile jarFile = jarCon.getJarFile()) {
JarFile jarFile = jarCon.getJarFile();
try {
return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null);
}
finally {
if (!jarCon.getUseCaches()) {
jarFile.close();
}
}
}
else if (con.getContentLengthLong() > 0) {
return true;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@@ -18,6 +18,8 @@ package org.springframework.core.io.buffer;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Objects;
import org.springframework.util.Assert;
@@ -103,10 +105,44 @@ final class DataBufferInputStream extends InputStream {
this.closed = true;
}
@Override
public byte[] readNBytes(int len) throws IOException {
if (len < 0) {
throw new IllegalArgumentException("len < 0");
}
checkClosed();
int size = Math.min(available(), len);
byte[] out = new byte[size];
this.dataBuffer.read(out);
return out;
}
@Override
public long skip(long n) throws IOException {
checkClosed();
if (n <= 0) {
return 0L;
}
int skipped = Math.min(available(), n > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) n);
this.dataBuffer.readPosition(Math.min(this.end, this.dataBuffer.readPosition() + skipped));
return skipped;
}
@Override
public long transferTo(OutputStream out) throws IOException {
Objects.requireNonNull(out, "out");
checkClosed();
if (available() == 0) {
return 0L;
}
byte[] buf = readAllBytes();
out.write(buf);
return buf.length;
}
private void checkClosed() throws IOException {
if (this.closed) {
throw new IOException("DataBufferInputStream is closed");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -18,8 +18,6 @@ package org.springframework.lang;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
@@ -77,7 +75,6 @@ import java.lang.annotation.Target;
* NullAway custom contract annotations</a>
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface Contract {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -25,13 +25,14 @@ import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.Charset;
import org.springframework.lang.Contract;
import org.springframework.lang.Nullable;
/**
* Simple utility methods for dealing with streams. The copy methods of this class are
* similar to those defined in {@link FileCopyUtils} except that all affected streams are
* left open when done. All copy methods use a block size of 8192 bytes.
* Simple utility methods for dealing with streams.
*
* <p>The copy methods of this class are similar to those defined in
* {@link FileCopyUtils} except that all affected streams are left open when done.
* All copy methods use a block size of {@value #BUFFER_SIZE} bytes.
*
* <p>Mainly for use within the framework, but also useful for application code.
*
@@ -190,14 +191,14 @@ public abstract class StreamUtils {
}
/**
* Drain the remaining content of the given InputStream.
* <p>Leaves the InputStream open when done.
* @param in the InputStream to drain
* @return the number of bytes read
* Drain the remaining content of the given {@link InputStream}.
* <p>Leaves the {@code InputStream} open when done.
* @param in the {@code InputStream} to drain
* @return the number of bytes read, or {@code 0} if the supplied
* {@code InputStream} is {@code null} or empty
* @throws IOException in case of I/O errors
* @since 4.3
*/
@Contract("null -> fail")
public static int drain(@Nullable InputStream in) throws IOException {
if (in == null) {
return 0;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class AnnotationBackCompatibilityTests {
@Test
void multiplRoutesToMetaAnnotation() {
void multipleRoutesToMetaAnnotation() {
Class<?> source = WithMetaMetaTestAnnotation1AndMetaTestAnnotation2.class;
// Merged annotation chooses lowest depth
MergedAnnotation<TestAnnotation> mergedAnnotation = MergedAnnotations.from(source).get(TestAnnotation.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -983,7 +983,7 @@ class MergedAnnotationsTests {
}
@Test
void getDirectFromClassgetDirectFromClassMetaMetaAnnotatedClass() {
void getDirectFromClassWithMetaMetaAnnotatedClass() {
MergedAnnotation<?> annotation = MergedAnnotations.from(
MetaMetaAnnotatedClass.class, SearchStrategy.TYPE_HIERARCHY).get(Component.class);
assertThat(annotation.getString("value")).isEqualTo("meta2");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 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.
@@ -46,7 +46,7 @@ class ConverterTests {
}
@Test
void andThenCanConvertfromDifferentSourceType() {
void andThenCanConvertFromDifferentSourceType() {
Converter<String, Integer> length = String::length;
assertThat(length.andThen(this.moduloTwo).convert("example")).isEqualTo(1);
assertThat(length.andThen(this.addOne).convert("example")).isEqualTo(8);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -16,6 +16,7 @@
package org.springframework.core.io.buffer;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
@@ -342,6 +343,48 @@ class DataBufferTests extends AbstractDataBufferAllocatingTests {
assertThat(len).isEqualTo(3);
assertThat(bytes).containsExactly('c', 'd', 'e');
buffer.readPosition(0);
inputStream = buffer.asInputStream();
assertThat(inputStream.readAllBytes()).asString().isEqualTo("abcde");
assertThat(inputStream.available()).isEqualTo(0);
assertThat(inputStream.readAllBytes()).isEmpty();
buffer.readPosition(0);
inputStream = buffer.asInputStream();
inputStream.mark(5);
assertThat(inputStream.readNBytes(0)).isEmpty();
assertThat(inputStream.readNBytes(1000)).asString().isEqualTo("abcde");
inputStream.reset();
assertThat(inputStream.readNBytes(3)).asString().isEqualTo("abc");
assertThat(inputStream.readNBytes(2)).asString().isEqualTo("de");
assertThat(inputStream.readNBytes(10)).isEmpty();
buffer.readPosition(0);
inputStream = buffer.asInputStream();
inputStream.mark(5);
assertThat(inputStream.skip(1)).isEqualTo(1);
assertThat(inputStream.readAllBytes()).asString().isEqualTo("bcde");
assertThat(inputStream.skip(10)).isEqualTo(0);
assertThat(inputStream.available()).isEqualTo(0);
inputStream.reset();
assertThat(inputStream.skip(100)).isEqualTo(5);
assertThat(inputStream.available()).isEqualTo(0);
buffer.readPosition(0);
inputStream = buffer.asInputStream();
inputStream.mark(5);
ByteArrayOutputStream out = new ByteArrayOutputStream();
assertThat(inputStream.transferTo(out)).isEqualTo(5);
assertThat(out.toByteArray()).asString().isEqualTo("abcde");
assertThat(inputStream.available()).isEqualTo(0);
out.reset();
inputStream.reset();
assertThat(inputStream.read()).isEqualTo('a');
assertThat(inputStream.transferTo(out)).isEqualTo(4);
assertThat(out.toByteArray()).asString().isEqualTo("bcde");
assertThat(inputStream.available()).isEqualTo(0);
assertThat(inputStream.transferTo(OutputStream.nullOutputStream())).isEqualTo(0);
release(buffer);
}
@@ -44,6 +44,8 @@ import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
@@ -298,8 +300,8 @@ class PathMatchingResourcePatternResolverTests {
@Test
void rootPatternRetrievalInJarFiles() throws IOException {
assertThat(resolver.getResources("classpath*:aspectj*.dtd")).extracting(Resource::getFilename)
.as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar")
.containsExactly("aspectj_1_5_0.dtd");
.as("Could not find aspectj_1_5_0.dtd in the root of the aspectjweaver jar")
.containsExactly("aspectj_1_5_0.dtd");
}
}
@@ -310,6 +312,16 @@ class PathMatchingResourcePatternResolverTests {
@TempDir
Path temp;
@BeforeAll
static void suppressJarCaches() {
URLConnection.setDefaultUseCaches("jar", false);
}
@AfterAll
static void restoreJarCaches() {
URLConnection.setDefaultUseCaches("jar", true);
}
@Test
void javaDashJarFindsClassPathManifestEntries() throws Exception {
Path lib = this.temp.resolve("lib");
@@ -333,6 +345,7 @@ class PathMatchingResourcePatternResolverTests {
StreamUtils.copy("test", StandardCharsets.UTF_8, jar);
jar.closeEntry();
}
assertThat(new FileSystemResource(path).exists()).isTrue();
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue();
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isTrue();
@@ -340,6 +353,14 @@ class PathMatchingResourcePatternResolverTests {
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isFalse();
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isFalse();
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse();
Resource resource = new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt");
try (InputStream is = resource.getInputStream()) {
assertThat(resource.exists()).isTrue();
assertThat(resource.createRelative("file.txt").exists()).isTrue();
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue();
is.readAllBytes();
}
}
private void writeApplicationJar(Path path) throws Exception {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -491,8 +491,8 @@ class AntPathMatcherTests {
assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}", "/hotels/{hotel}/booking")).isEqualTo(1);
// SPR-10550
assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/**")).isEqualTo(-1);
assertThat(comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")).isEqualTo(1);
assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/customers/{customer}", "/**")).isEqualTo(-1);
assertThat(comparator.compare("/**", "/hotels/{hotel}/bookings/{booking}/customers/{customer}")).isEqualTo(1);
assertThat(comparator.compare("/**", "/**")).isEqualTo(0);
assertThat(comparator.compare("/hotels/{hotel}", "/hotels/*")).isEqualTo(-1);
@@ -505,8 +505,8 @@ class AntPathMatcherTests {
assertThat(comparator.compare("/hotels/{hotel}", "/hotels/{hotel}.*")).isEqualTo(2);
// SPR-6741
assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/cutomers/{customer}", "/hotels/**")).isEqualTo(-1);
assertThat(comparator.compare("/hotels/**", "/hotels/{hotel}/bookings/{booking}/cutomers/{customer}")).isEqualTo(1);
assertThat(comparator.compare("/hotels/{hotel}/bookings/{booking}/customers/{customer}", "/hotels/**")).isEqualTo(-1);
assertThat(comparator.compare("/hotels/**", "/hotels/{hotel}/bookings/{booking}/customers/{customer}")).isEqualTo(1);
assertThat(comparator.compare("/hotels/foo/bar/**", "/hotels/{hotel}")).isEqualTo(1);
assertThat(comparator.compare("/hotels/{hotel}", "/hotels/foo/bar/**")).isEqualTo(-1);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -228,7 +228,7 @@ class ClassUtilsTests {
@Test
void getShortNameAsProperty() {
String shortName = ClassUtils.getShortNameAsProperty(this.getClass());
String shortName = ClassUtils.getShortNameAsProperty(getClass());
assertThat(shortName).as("Class name did not match").isEqualTo("classUtilsTests");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -382,7 +382,7 @@ class CollectionUtilsTests {
if (this == rhs) {
return true;
}
if (rhs == null || this.getClass() != rhs.getClass()) {
if (rhs == null || getClass() != rhs.getClass()) {
return false;
}
Instance instance = (Instance) rhs;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -81,6 +81,12 @@ class BindingReflectionHintsRegistrarKotlinTests {
assertThat(RuntimeHintsPredicates.reflection().onType(SampleClass::class.java)
.withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS)).accepts(hints)
}
@Test
fun `Register reflection hints on serializer function with parameter`() {
bindingRegistrar.registerReflectionHints(hints.reflection(), SampleResult::class.java)
assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleResult.Companion::class.java, "serializer")).accepts(hints)
}
}
@kotlinx.serialization.Serializable
@@ -89,3 +95,17 @@ class SampleSerializableClass(val name: String)
data class SampleDataClass(val name: String, val isNonNullable: Boolean, val isNullable: Boolean?)
class SampleClass(val name: String)
@kotlinx.serialization.Serializable
data class SampleResult <T>(
val code: Int,
val message: String,
val data: T,
) {
companion object {
private const val SUCCESS: Int = 200
private const val FAILURE: Int = 500
fun <T> success(message: String, data: T) = SampleResult<T>(code = SUCCESS, message = message, data = data)
fun <T> failure(message: String, data: T) = SampleResult<T>(code = FAILURE, message = message, data = data)
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -22,7 +22,7 @@ import org.junit.jupiter.api.Test
/**
* Kotlin tests for [BridgeMethodResolver].
*
* @author Sebastien Deleuzes
* @author Sebastien Deleuze
*/
class BridgeMethodResolverKotlinTests {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -26,7 +26,7 @@ import org.springframework.core.env.PropertySource;
* a user-provided {@link Properties} object, or if omitted during construction,
* the implementation will initialize its own.
*
* The {@link #setProperty} and {@link #withProperty} methods are exposed for
* <p>The {@link #setProperty} and {@link #withProperty} methods are exposed for
* convenience, for example:
* <pre class="code">
* {@code
@@ -95,7 +95,7 @@ public class MockPropertySource extends PropertiesPropertySource {
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockPropertySource} instance
*/
public MockPropertySource withProperty(String name, Object value) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -691,9 +691,8 @@ public class CodeFlow implements Opcodes {
* Determine whether the given number is to be considered as an integer
* for the purposes of a numeric operation at the bytecode level.
* @param number the number to check
* @return {@code true} if it is an {@link Integer}, {@link Short} or {@link Byte}
* @return {@code true} if it is an {@link Integer}, {@link Short}, or {@link Byte}
*/
@Contract("null -> false")
public static boolean isIntegerForNumericOp(Number number) {
return (number instanceof Integer || number instanceof Short || number instanceof Byte);
}
@@ -276,7 +276,7 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
@Test
void functionMethodMustBeStatic() throws Exception {
context.registerFunction("nonStatic", this.getClass().getMethod("nonStatic"));
context.registerFunction("nonStatic", getClass().getMethod("nonStatic"));
SpelExpression expression = parser.parseRaw("#nonStatic()");
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(context))
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -174,7 +174,7 @@ class MappingJackson2MessageConverterTests {
@Test
void toTextMessageWithReturnType() throws JMSException, NoSuchMethodException {
Method method = this.getClass().getDeclaredMethod("summary");
Method method = getClass().getDeclaredMethod("summary");
MethodParameter returnType = new MethodParameter(method, -1);
testToTextMessageWithReturnType(returnType);
verify(sessionMock).createTextMessage("{\"name\":\"test\"}");
@@ -188,7 +188,7 @@ class MappingJackson2MessageConverterTests {
@Test
void toTextMessageWithReturnTypeAndNoJsonView() throws JMSException, NoSuchMethodException {
Method method = this.getClass().getDeclaredMethod("none");
Method method = getClass().getDeclaredMethod("none");
MethodParameter returnType = new MethodParameter(method, -1);
testToTextMessageWithReturnType(returnType);
@@ -197,7 +197,7 @@ class MappingJackson2MessageConverterTests {
@Test
void toTextMessageWithReturnTypeAndMultipleJsonViews() throws NoSuchMethodException {
Method method = this.getClass().getDeclaredMethod("invalid");
Method method = getClass().getDeclaredMethod("invalid");
MethodParameter returnType = new MethodParameter(method, -1);
assertThatIllegalArgumentException().isThrownBy(() ->
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -332,7 +332,7 @@ public class MessageHeaderAccessor {
protected void verifyType(@Nullable String headerName, @Nullable Object headerValue) {
if (headerName != null && headerValue != null) {
if (MessageHeaders.ERROR_CHANNEL.equals(headerName) ||
MessageHeaders.REPLY_CHANNEL.endsWith(headerName)) {
MessageHeaders.REPLY_CHANNEL.equals(headerName)) {
if (!(headerValue instanceof MessageChannel || headerValue instanceof String)) {
throw new IllegalArgumentException(
"'" + headerName + "' header value must be a MessageChannel or String");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -59,9 +59,9 @@ import static org.mockito.Mockito.verify;
* @author Sebastien Deleuze
*/
@ExtendWith(MockitoExtension.class)
public class SubscriptionMethodReturnValueHandlerTests {
class SubscriptionMethodReturnValueHandlerTests {
public static final MimeType MIME_TYPE = new MimeType("text", "plain", StandardCharsets.UTF_8);
private static final MimeType MIME_TYPE = new MimeType("text", "plain", StandardCharsets.UTF_8);
private static final String PAYLOAD = "payload";
@@ -95,16 +95,16 @@ public class SubscriptionMethodReturnValueHandlerTests {
jsonMessagingTemplate.setMessageConverter(new MappingJackson2MessageConverter());
this.jsonHandler = new SubscriptionMethodReturnValueHandler(jsonMessagingTemplate);
Method method = this.getClass().getDeclaredMethod("getData");
Method method = getClass().getDeclaredMethod("getData");
this.subscribeEventReturnType = new MethodParameter(method, -1);
method = this.getClass().getDeclaredMethod("getDataAndSendTo");
method = getClass().getDeclaredMethod("getDataAndSendTo");
this.subscribeEventSendToReturnType = new MethodParameter(method, -1);
method = this.getClass().getDeclaredMethod("handle");
method = getClass().getDeclaredMethod("handle");
this.messageMappingReturnType = new MethodParameter(method, -1);
method = this.getClass().getDeclaredMethod("getJsonView");
method = getClass().getDeclaredMethod("getJsonView");
this.subscribeEventJsonViewReturnType = new MethodParameter(method, -1);
}
@@ -144,7 +144,7 @@ public class SubscriptionMethodReturnValueHandlerTests {
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void testHeadersPassedToMessagingTemplate() throws Exception {
void testHeadersPassedToMessagingTemplate() throws Exception {
String sessionId = "sess1";
String subscriptionId = "subs1";
String destination = "/dest";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -34,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* @author Mark Fisher
* @author Rossen Stoyanchev
* @author Mengqi Xu
*/
class MessageBuilderTests {
@@ -238,4 +239,22 @@ class MessageBuilderTests {
assertThat(message3.getHeaders().get("foo")).isEqualTo("bar3");
}
@Test // gh-34949
void buildMessageWithReplyChannelHeader() {
MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor();
MessageBuilder<?> messageBuilder = MessageBuilder.withPayload("payload").setHeaders(headerAccessor);
headerAccessor.setHeader(MessageHeaders.REPLY_CHANNEL, "foo");
Message<?> message1 = messageBuilder.build();
assertThat(message1.getHeaders().get(MessageHeaders.REPLY_CHANNEL)).isEqualTo("foo");
headerAccessor.setHeader("hannel", 0);
Message<?> message2 = messageBuilder.build();
assertThat(message2.getHeaders().get("hannel")).isEqualTo(0);
assertThatIllegalArgumentException()
.isThrownBy(() -> headerAccessor.setHeader(MessageHeaders.REPLY_CHANNEL, 0))
.withMessage("'%s' header value must be a MessageChannel or String", MessageHeaders.REPLY_CHANNEL);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -26,7 +26,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
* @author Chris Beams
* @author Sam Brannen
* @since 3.2
* @see org.springframework.mock.env.MockPropertySource
* @see MockPropertySource
*/
public class MockEnvironment extends AbstractEnvironment {
@@ -43,19 +43,43 @@ public class MockEnvironment extends AbstractEnvironment {
/**
* Set a property on the underlying {@link MockPropertySource} for this environment.
* @since 6.2.8
* @see MockPropertySource#setProperty(String, Object)
*/
public void setProperty(String key, String value) {
this.propertySource.setProperty(key, value);
public void setProperty(String name, Object value) {
this.propertySource.setProperty(name, value);
}
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* @return this {@link MockEnvironment} instance
* @see MockPropertySource#withProperty
* Set a property on the underlying {@link MockPropertySource} for this environment.
* @see #setProperty(String, Object)
*/
public MockEnvironment withProperty(String key, String value) {
setProperty(key, value);
public void setProperty(String name, String value) {
this.propertySource.setProperty(name, value);
}
/**
* Convenient synonym for {@link #setProperty(String, Object)} that returns
* the current instance.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockEnvironment} instance
* @since 6.2.8
* @see MockPropertySource#withProperty(String, Object)
*/
public MockEnvironment withProperty(String name, Object value) {
setProperty(name, value);
return this;
}
/**
* Convenient synonym for {@link #setProperty(String, String)} that returns
* the current instance.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockEnvironment} instance
* @see #withProperty(String, Object)
*/
public MockEnvironment withProperty(String name, String value) {
setProperty(name, value);
return this;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -26,7 +26,7 @@ import org.springframework.core.env.PropertySource;
* a user-provided {@link Properties} object, or if omitted during construction,
* the implementation will initialize its own.
*
* The {@link #setProperty} and {@link #withProperty} methods are exposed for
* <p>The {@link #setProperty} and {@link #withProperty} methods are exposed for
* convenience, for example:
* <pre class="code">
* {@code
@@ -36,7 +36,7 @@ import org.springframework.core.env.PropertySource;
*
* @author Chris Beams
* @since 3.1
* @see org.springframework.mock.env.MockEnvironment
* @see MockEnvironment
*/
public class MockPropertySource extends PropertiesPropertySource {
@@ -95,7 +95,7 @@ public class MockPropertySource extends PropertiesPropertySource {
/**
* Convenient synonym for {@link #setProperty} that returns the current instance.
* Useful for method chaining and fluent-style use.
* <p>Useful for method chaining and fluent-style use.
* @return this {@link MockPropertySource} instance
*/
public MockPropertySource withProperty(String name, Object value) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@@ -105,7 +105,7 @@ public class MockClientHttpRequest extends MockHttpOutputMessage implements Clie
/**
* Set the {@link ClientHttpResponse} to be used as the result of executing
* the this request.
* this request.
* @see #execute()
*/
public void setResponse(ClientHttpResponse clientHttpResponse) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -1064,8 +1064,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
* @see #getDateHeader
*/
public void addHeader(String name, Object value) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name) &&
!this.headers.containsKey(HttpHeaders.CONTENT_TYPE)) {
if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
setContentType(value.toString());
}
else if (HttpHeaders.ACCEPT_LANGUAGE.equalsIgnoreCase(name) &&
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -226,6 +226,15 @@ class MockHttpServletRequestTests {
assertThat(request.getCharacterEncoding()).isEqualTo("UTF-8");
}
@Test
void contentTypeMultipleCalls() {
String contentType = "text/html";
request.addHeader(HttpHeaders.CONTENT_TYPE, "text/plain");
request.addHeader(HttpHeaders.CONTENT_TYPE, contentType);
assertThat(request.getContentType()).isEqualTo(contentType);
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(contentType);
}
@Test // SPR-12677
void setContentTypeHeaderWithMoreComplexCharsetSyntax() {
String contentType = "test/plain;charset=\"utf-8\";foo=\"charset=bar\";foocharset=bar;foo=bar";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -110,7 +110,7 @@ public class PersonEntity extends PersistentEntity implements Person {
public String toString() {
// @formatter:off
return new ToStringCreator(this)
.append("id", this.getId())
.append("id", getId())
.append("name", this.name)
.append("age", this.age)
.append("eyeColor", this.eyeColor)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -19,7 +19,6 @@ package org.springframework.test.web.client.samples;
import java.io.IOException;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
@@ -46,27 +45,22 @@ import static org.springframework.test.web.client.response.MockRestResponseCreat
/**
* Examples to demonstrate writing client-side REST tests with Spring MVC Test.
* While the tests in this class invoke the RestTemplate directly, in actual
*
* <p>While the tests in this class invoke the RestTemplate directly, in actual
* tests the RestTemplate may likely be invoked indirectly, i.e. through client
* code.
*
* @author Rossen Stoyanchev
*/
public class SampleTests {
class SampleTests {
private MockRestServiceServer mockServer;
private final RestTemplate restTemplate = new RestTemplate();
private RestTemplate restTemplate;
private final MockRestServiceServer mockServer = MockRestServiceServer.bindTo(this.restTemplate).ignoreExpectOrder(true).build();
@BeforeEach
public void setup() {
this.restTemplate = new RestTemplate();
this.mockServer = MockRestServiceServer.bindTo(this.restTemplate).ignoreExpectOrder(true).build();
}
@Test
public void performGet() {
void performGet() {
String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}";
this.mockServer.expect(requestTo("/composers/42")).andExpect(method(HttpMethod.GET))
@@ -83,8 +77,7 @@ public class SampleTests {
}
@Test
public void performGetManyTimes() {
void performGetManyTimes() {
String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}";
this.mockServer.expect(manyTimes(), requestTo("/composers/42")).andExpect(method(HttpMethod.GET))
@@ -105,8 +98,7 @@ public class SampleTests {
}
@Test
public void expectNever() {
void expectNever() {
String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}";
this.mockServer.expect(once(), requestTo("/composers/42")).andExpect(method(HttpMethod.GET))
@@ -120,8 +112,7 @@ public class SampleTests {
}
@Test
public void expectNeverViolated() {
void expectNeverViolated() {
String responseBody = "{\"name\" : \"Ludwig van Beethoven\", \"someDouble\" : \"1.6035\"}";
this.mockServer.expect(once(), requestTo("/composers/42")).andExpect(method(HttpMethod.GET))
@@ -135,9 +126,8 @@ public class SampleTests {
}
@Test
public void performGetWithResponseBodyFromFile() {
Resource responseBody = new ClassPathResource("ludwig.json", this.getClass());
void performGetWithResponseBodyFromFile() {
Resource responseBody = new ClassPathResource("ludwig.json", getClass());
this.mockServer.expect(requestTo("/composers/42")).andExpect(method(HttpMethod.GET))
.andRespond(withSuccess(responseBody, MediaType.APPLICATION_JSON));
@@ -152,8 +142,7 @@ public class SampleTests {
}
@Test
public void verify() {
void verify() {
this.mockServer.expect(requestTo("/number")).andExpect(method(HttpMethod.GET))
.andRespond(withSuccess("1", MediaType.TEXT_PLAIN));
@@ -183,9 +172,8 @@ public class SampleTests {
}
@Test // SPR-14694
public void repeatedAccessToResponseViaResource() {
Resource resource = new ClassPathResource("ludwig.json", this.getClass());
void repeatedAccessToResponseViaResource() {
Resource resource = new ClassPathResource("ludwig.json", getClass());
RestTemplate restTemplate = new RestTemplate();
restTemplate.setInterceptors(Collections.singletonList(new ContentInterceptor(resource)));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -471,12 +471,12 @@ class MockHttpServletRequestBuilderTests {
assertThat(request.getContentType()).isEqualTo("yaml");
}
@Test // SPR-11308
@Test
void contentTypeViaMultipleHeaderValues() {
this.builder.header("Content-Type", MediaType.TEXT_HTML_VALUE, MediaType.ALL_VALUE);
this.builder.header("Content-Type", MediaType.TEXT_HTML_VALUE, MediaType.TEXT_PLAIN_VALUE);
MockHttpServletRequest request = this.builder.buildRequest(this.servletContext);
assertThat(request.getContentType()).isEqualTo("text/html");
assertThat(request.getContentType()).isEqualTo("text/plain");
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -257,7 +257,7 @@ class PrintingResultHandlerTests {
this.mvcResult.setHandler(handlerMethod);
this.handler.handle(mvcResult);
assertValue("Handler", "Type", this.getClass().getName());
assertValue("Handler", "Type", getClass().getName());
assertValue("Handler", "Method", handlerMethod);
}
@@ -68,6 +68,7 @@ public final class ContentDisposition {
for (int i=33; i<= 126; i++) {
PRINTABLE.set(i);
}
PRINTABLE.set(34, false); // "
PRINTABLE.set(61, false); // =
PRINTABLE.set(63, false); // ?
PRINTABLE.set(95, false); // _
@@ -149,7 +149,16 @@ class JdkClientHttpRequest extends AbstractStreamingClientHttpRequest {
}
});
builder.method(this.method.name(), bodyPublisher(headers, body));
switch (this.method.name()) {
case "GET" :
builder.GET();
break;
case "DELETE" :
builder.DELETE();
break;
default :
builder.method(this.method.name(), bodyPublisher(headers, body));
}
return builder.build();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -19,6 +19,7 @@ package org.springframework.http.client;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.Objects;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatusCode;
@@ -103,6 +104,7 @@ final class SimpleClientHttpResponse implements ClientHttpResponse {
if (this.responseStream == null) {
getBody();
}
Objects.requireNonNull(this.responseStream);
StreamUtils.drain(this.responseStream);
this.responseStream.close();
}
@@ -20,6 +20,7 @@ import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -76,6 +77,11 @@ public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends
this.format = format;
}
@Override
public List<MediaType> getSupportedMediaTypes(Class<?> clazz) {
return getSupportedMediaTypes();
}
@Override
protected boolean supports(Class<?> clazz) {
return serializer(ResolvableType.forClass(clazz)) != null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 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.
@@ -16,6 +16,7 @@
package org.springframework.http.converter.xml;
import java.nio.charset.Charset;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@@ -24,7 +25,10 @@ import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConversionException;
import org.springframework.lang.Nullable;
/**
* Abstract base class for {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverters}
@@ -116,4 +120,21 @@ public abstract class AbstractJaxb2HttpMessageConverter<T> extends AbstractXmlHt
});
}
/**
* Detect the charset from the given {@link HttpHeaders#getContentType()}.
* @param httpHeaders the current HTTP headers
* @return the charset defined in the content type header, or {@code null} if not found
* @since 6.2.8
*/
@Nullable
protected Charset detectCharset(HttpHeaders httpHeaders) {
MediaType contentType = httpHeaders.getContentType();
if (contentType != null && contentType.getCharset() != null) {
return contentType.getCharset();
}
else {
return null;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@@ -19,6 +19,7 @@ package org.springframework.http.converter.xml;
import java.io.IOException;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
@@ -148,7 +149,10 @@ public class Jaxb2CollectionHttpMessageConverter<T extends Collection>
try {
Unmarshaller unmarshaller = createUnmarshaller(elementClass);
XMLStreamReader streamReader = this.inputFactory.createXMLStreamReader(inputMessage.getBody());
Charset detectedCharset = detectCharset(inputMessage.getHeaders());
XMLStreamReader streamReader = (detectedCharset != null) ?
this.inputFactory.createXMLStreamReader(inputMessage.getBody(), detectedCharset.name()) :
this.inputFactory.createXMLStreamReader(inputMessage.getBody());
int event = moveToFirstChildOfRootElement(streamReader);
while (event != XMLStreamReader.END_DOCUMENT) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -17,6 +17,7 @@
package org.springframework.http.converter.xml;
import java.io.StringReader;
import java.nio.charset.Charset;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
@@ -135,7 +136,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
@Override
protected Object readFromSource(Class<?> clazz, HttpHeaders headers, Source source) throws Exception {
try {
source = processSource(source);
source = processSource(source, detectCharset(headers));
Unmarshaller unmarshaller = createUnmarshaller(clazz);
if (clazz.isAnnotationPresent(XmlRootElement.class)) {
return unmarshaller.unmarshal(source);
@@ -160,9 +161,19 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
}
}
protected Source processSource(Source source) {
/**
* Process {@code source} with {@code charset}.
* @param source source to process
* @param charset charset to use
* @return source
* @since 6.2.8
*/
protected Source processSource(Source source, @Nullable Charset charset) {
if (source instanceof StreamSource streamSource) {
InputSource inputSource = new InputSource(streamSource.getInputStream());
if (charset != null) {
inputSource.setEncoding(charset.name());
}
try {
// By default, Spring will prevent the processing of external entities.
// This is a mitigation against XXE attacks.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@@ -69,7 +69,7 @@ public class HttpMediaTypeNotAcceptableException extends HttpMediaTypeException
return HttpHeaders.EMPTY;
}
HttpHeaders headers = new HttpHeaders();
headers.setAccept(this.getSupportedMediaTypes());
headers.setAccept(getSupportedMediaTypes());
return headers;
}
@@ -71,23 +71,25 @@ import org.springframework.core.annotation.AliasFor;
*
* <p>The following return types are supported for handler methods:
* <ul>
* <li>A {@code ModelAndView} object (from Servlet MVC).
* <li>A {@link org.springframework.ui.Model} object, with the view name implicitly
* <li>{@code ModelAndView} object (from Servlet MVC).
* <li>{@link org.springframework.ui.Model} object, with the view name implicitly
* determined through a {@link org.springframework.web.servlet.RequestToViewNameTranslator}.
* <li>A {@link java.util.Map} object for exposing a model,
* <li>{@link java.util.Map} object for exposing a model,
* with the view name implicitly determined through a
* {@link org.springframework.web.servlet.RequestToViewNameTranslator}.
* <li>A {@link org.springframework.web.servlet.View} object.
* <li>A {@link String} value which is interpreted as view name.
* <li>{@link org.springframework.web.servlet.View} object.
* <li>{@link String} value which is interpreted as view name.
* <li>{@link ResponseBody @ResponseBody} annotated methods (Servlet-only)
* to set the response content. The return value will be converted to the
* response stream using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}.
* <li>An {@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} or
* <li>{@link org.springframework.http.HttpEntity HttpEntity&lt;?&gt;} or
* {@link org.springframework.http.ResponseEntity ResponseEntity&lt;?&gt;} object
* (Servlet-only) to set response headers and content. The ResponseEntity body
* will be converted and written to the response stream using
* {@linkplain org.springframework.http.converter.HttpMessageConverter message converters}.
* <li>{@link org.springframework.http.ProblemDetail} or {@link org.springframework.web.ErrorResponse}
* object to render an RFC 9457 error response with details in the body.
* <li>{@code void} if the method handles the response itself (by
* writing the response content directly, declaring an argument of type
* {@link jakarta.servlet.ServletResponse} / {@link jakarta.servlet.http.HttpServletResponse}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -26,18 +26,16 @@ import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
/**
* A convenience annotation that is itself annotated with
* {@link ControllerAdvice @ControllerAdvice}
* and {@link ResponseBody @ResponseBody}.
* A shortcut annotation that combines {@link ControllerAdvice @ControllerAdvice}
* with {@link ResponseBody @ResponseBody}, in effect simply an
* {@code @ControllerAdvice} whose exception handler methods render to the
* response body.
*
* <p>Types that carry this annotation are treated as controller advice where
* {@link ExceptionHandler @ExceptionHandler} methods assume
* {@link ResponseBody @ResponseBody} semantics by default.
* <p>By default, {@code @RestControllerAdvice} applies to any controller,
* including {@code @Controller} and {@code @RestController}. Use attributes of
* the annotation to apply more specific filtering criteria.
*
* <p><b>NOTE:</b> {@code @RestControllerAdvice} is processed if an appropriate
* {@code HandlerMapping}-{@code HandlerAdapter} pair is configured such as the
* {@code RequestMappingHandlerMapping}-{@code RequestMappingHandlerAdapter} pair
* which are the default in the MVC Java config and the MVC namespace.
* <p>See {@link ControllerAdvice} for more details.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 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.
@@ -107,12 +107,12 @@ public interface ServerWebExchange {
}
/**
* Return the web session for the current request. Always guaranteed to
* return an instance either matching to the session id requested by the
* client, or with a new session id either because the client did not
* specify one or because the underlying session had expired. Use of this
* method does not automatically create a session. See {@link WebSession}
* for more details.
* Return the web session for the current request.
* <p>Always guaranteed to return either an instance matching the session id
* requested by the client, or a new session either because the client did not
* specify a session id or because the underlying session expired.
* <p>Use of this method does not automatically create a session. See
* {@link WebSession} for more details.
*/
Mono<WebSession> getSession();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -79,9 +79,9 @@ public class InMemoryWebSessionStore implements WebSessionStore {
}
/**
* Configure the {@link Clock} to use to set lastAccessTime on every created
* session and to calculate if it is expired.
* <p>This may be useful to align to different timezone or to set the clock
* Configure the {@link Clock} to use to set the {@code lastAccessTime} on
* every created session and to calculate if the session has expired.
* <p>This may be useful to align to different time zones or to set the clock
* back in a test, for example, {@code Clock.offset(clock, Duration.ofMinutes(-31))}
* in order to simulate session expiration.
* <p>By default this is {@code Clock.system(ZoneId.of("GMT"))}.
@@ -94,16 +94,17 @@ public class InMemoryWebSessionStore implements WebSessionStore {
}
/**
* Return the configured clock for session lastAccessTime calculations.
* Return the configured clock for session {@code lastAccessTime} calculations.
*/
public Clock getClock() {
return this.clock;
}
/**
* Return the map of sessions with an {@link Collections#unmodifiableMap
* unmodifiable} wrapper. This could be used for management purposes, to
* list active sessions, invalidate expired ones, etc.
* Return an {@linkplain Collections#unmodifiableMap unmodifiable} copy of the
* map of sessions.
* <p>This could be used for management purposes, to list active sessions,
* to invalidate expired sessions, etc.
* @since 5.0.8
*/
public Map<String, WebSession> getSessions() {
@@ -157,10 +158,11 @@ public class InMemoryWebSessionStore implements WebSessionStore {
}
/**
* Check for expired sessions and remove them. Typically such checks are
* kicked off lazily during calls to {@link #createWebSession() create} or
* {@link #retrieveSession retrieve}, no less than 60 seconds apart.
* This method can be called to force a check at a specific time.
* Check for expired sessions and remove them.
* <p>Typically such checks are kicked off lazily during calls to
* {@link #createWebSession()} or {@link #retrieveSession}, no less than 60
* seconds apart.
* <p>This method can be called to force a check at a specific time.
* @since 5.0.8
*/
public void removeExpiredSessions() {
@@ -237,7 +239,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
InMemoryWebSessionStore.this.sessions.remove(currentId);
String newId = String.valueOf(idGenerator.generateId());
this.id.set(newId);
InMemoryWebSessionStore.this.sessions.put(this.getId(), this);
InMemoryWebSessionStore.this.sessions.put(this.id.get(), this);
return Mono.empty();
})
.subscribeOn(Schedulers.boundedElastic())
@@ -266,11 +268,11 @@ public class InMemoryWebSessionStore implements WebSessionStore {
if (isStarted()) {
// Save
InMemoryWebSessionStore.this.sessions.put(this.getId(), this);
InMemoryWebSessionStore.this.sessions.put(this.id.get(), this);
// Unless it was invalidated
if (this.state.get().equals(State.EXPIRED)) {
InMemoryWebSessionStore.this.sessions.remove(this.getId());
InMemoryWebSessionStore.this.sessions.remove(this.id.get());
return Mono.error(new IllegalStateException("Session was invalidated"));
}
}
@@ -281,7 +283,7 @@ public class InMemoryWebSessionStore implements WebSessionStore {
private void checkMaxSessionsLimit() {
if (sessions.size() >= maxSessions) {
expiredSessionChecker.removeExpiredSessions(clock.instant());
if (sessions.size() >= maxSessions) {
if (sessions.size() >= maxSessions && !sessions.containsKey(this.id.get())) {
throw new IllegalStateException("Max sessions limit reached: " + sessions.size());
}
}
@@ -32,10 +32,10 @@ import org.springframework.web.server.WebSession;
public interface WebSessionManager {
/**
* Return the {@link WebSession} for the given exchange. Always guaranteed
* to return an instance either matching to the session id requested by the
* client, or a new session either because the client did not specify one
* or because the underlying session expired.
* Return the {@link WebSession} for the given exchange.
* <p>Always guaranteed to return either an instance matching the session id
* requested by the client, or a new session either because the client did not
* specify a session id or because the underlying session expired.
* @param exchange the current exchange
* @return promise for the WebSession
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 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.
@@ -43,7 +43,7 @@ public interface WebSessionStore {
* Return the WebSession for the given id.
* <p><strong>Note:</strong> This method should perform an expiration check,
* and if it has expired remove the session and return empty. This method
* should also update the lastAccessTime of retrieved sessions.
* should also update the {@code lastAccessTime} of retrieved sessions.
* @param sessionId the session to load
* @return the session, or an empty {@code Mono}
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -2040,7 +2040,7 @@ final class WhatWgUrlParser {
if (obj == this) {
return true;
}
if (obj == null || obj.getClass() != this.getClass()) {
if (obj == null || getClass() != obj.getClass()) {
return false;
}
UrlRecord that = (UrlRecord) obj;
@@ -2322,7 +2322,7 @@ final class WhatWgUrlParser {
@Override
public boolean equals(Object obj) {
return obj == this || obj != null && obj.getClass() == this.getClass();
return obj == this || obj != null && getClass() == obj.getClass();
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -374,19 +374,49 @@ public class PathPattern implements Comparable<PathPattern> {
}
/**
* Combine this pattern with another.
* Combine this pattern with the one given as parameter, returning a new
* {@code PathPattern} instance that concatenates or merges both.
* This operation is not commutative, meaning {@code pattern1.combine(pattern2)}
* is not equal to {@code pattern2.combine(pattern1)}.
*
* <p>Combining two "fixed" patterns effectively concatenates them:
* <ul>
* <li><code> "/projects" + "/spring-framework" => "/projects/spring-framework"</code>
* </ul>
* Combining a "fixed" pattern with a "matching" pattern concatenates them:
* <ul>
* <li><code> "/projects" + "/{project}" => "/projects/{project}"</code>
* </ul>
* Combining a "matching" pattern with a "fixed" pattern merges them:
* <ul>
* <li><code> "/projects/&#42;" + "/spring-framework" => "/projects/spring-framework"</code>
* <li><code> "/projects/&#42;.html" + "/spring-framework.html" => "/projects/spring-framework.html"</code>
* </ul>
* Combining two "matching" patterns merges them:
* <ul>
* <li><code> "/projects/&#42;&#42;" + "/&#42;.html" => "/projects/&#42;.html"</code>
* <li><code> "/projects/&#42;" + "/{project}" => "/projects/{project}"</code>
* <li><code> "/projects/&#42;.html" + "/spring-framework.&#42;" => "/projects/spring-framework.html"</code>
* </ul>
* Note, if a pattern does not end with a "matching" segment, it is considered as a "fixed" one:
* <ul>
* <li><code> "/projects/&#42;/releases" + "/{id}" => "/projects/&#42;/releases/{id}"</code>
* </ul>
* @param otherPattern the pattern to be combined with the current one
* @return the new {@code PathPattern} that combines both patterns
* @throws IllegalArgumentException if the combination is not allowed
*/
public PathPattern combine(PathPattern pattern2string) {
// If one of them is empty the result is the other. If both empty the result is ""
public PathPattern combine(PathPattern otherPattern) {
// If one of them is empty, the result is the other. If both are empty, the result is ""
if (!StringUtils.hasLength(this.patternString)) {
if (!StringUtils.hasLength(pattern2string.patternString)) {
if (!StringUtils.hasLength(otherPattern.patternString)) {
return this.parser.parse("");
}
else {
return pattern2string;
return otherPattern;
}
}
else if (!StringUtils.hasLength(pattern2string.patternString)) {
else if (!StringUtils.hasLength(otherPattern.patternString)) {
return this;
}
@@ -395,40 +425,55 @@ public class PathPattern implements Comparable<PathPattern> {
// However:
// /usr + /user => /usr/user
// /{foo} + /bar => /{foo}/bar
if (!this.patternString.equals(pattern2string.patternString) && this.capturedVariableCount == 0 &&
matches(PathContainer.parsePath(pattern2string.patternString))) {
return pattern2string;
if (!this.patternString.equals(otherPattern.patternString) && this.capturedVariableCount == 0 &&
matches(PathContainer.parsePath(otherPattern.patternString))) {
return otherPattern;
}
// /hotels/* + /booking => /hotels/booking
// /hotels/* + booking => /hotels/booking
if (this.endsWithSeparatorWildcard) {
return this.parser.parse(concat(
this.patternString.substring(0, this.patternString.length() - 2),
pattern2string.patternString));
String prefix = this.patternString.length() > 2 ?
this.patternString.substring(0, this.patternString.length() - 2) :
String.valueOf(getSeparator());
return this.parser.parse(concat(prefix, otherPattern.patternString));
}
// /hotels/** + "/booking/rooms => /hotels/booking/rooms
if (this.catchAll) {
return this.parser.parse(concat(this.patternString.substring(0, this.patternString.length() - 3),
otherPattern.patternString));
}
// /hotels + /booking => /hotels/booking
// /hotels + booking => /hotels/booking
int starDotPos1 = this.patternString.indexOf("*."); // Are there any file prefix/suffix things to consider?
if (this.capturedVariableCount != 0 || starDotPos1 == -1 || getSeparator() == '.') {
return this.parser.parse(concat(this.patternString, pattern2string.patternString));
int firstStarDotPos = this.patternString.indexOf("*."); // Are there any file prefix/suffix things to consider?
if (this.capturedVariableCount != 0 || firstStarDotPos == -1 || getSeparator() == '.') {
return this.parser.parse(concat(this.patternString, otherPattern.patternString));
}
// /*.html + /hotel => /hotel.html
// /*.html + /hotel.* => /hotel.html
String firstExtension = this.patternString.substring(starDotPos1 + 1); // looking for the first extension
String p2string = pattern2string.patternString;
int dotPos2 = p2string.indexOf('.');
String file2 = (dotPos2 == -1 ? p2string : p2string.substring(0, dotPos2));
String secondExtension = (dotPos2 == -1 ? "" : p2string.substring(dotPos2));
boolean firstExtensionWild = (firstExtension.equals(".*") || firstExtension.isEmpty());
boolean secondExtensionWild = (secondExtension.equals(".*") || secondExtension.isEmpty());
if (!firstExtensionWild && !secondExtensionWild) {
int secondDotPos = otherPattern.patternString.indexOf('.');
String firstExtension = this.patternString.substring(firstStarDotPos + 1); // looking for the first extension
String secondExtension = (secondDotPos == -1 ? "" : otherPattern.patternString.substring(secondDotPos));
boolean isFirstExtensionWildcard = (firstExtension.equals(".*") || firstExtension.isEmpty());
boolean isSecondExtensionWildcard = (secondExtension.equals(".*") || secondExtension.isEmpty());
if (!isFirstExtensionWildcard && !isSecondExtensionWildcard) {
throw new IllegalArgumentException(
"Cannot combine patterns: " + this.patternString + " and " + pattern2string);
"Cannot combine patterns: " + this.patternString + " and " + otherPattern);
}
return this.parser.parse(file2 + (firstExtensionWild ? secondExtension : firstExtension));
String firstPath = this.patternString.substring(0, this.patternString.lastIndexOf(getSeparator()));
String secondPath = otherPattern.patternString.substring(0, otherPattern.patternString.lastIndexOf(getSeparator()));
if (!this.parser.parse(firstPath).matches(PathContainer.parsePath(secondPath))) {
throw new IllegalArgumentException(
"Cannot combine patterns: " + this.patternString + " and " + otherPattern);
}
String secondFile = (secondDotPos == -1 ? otherPattern.patternString : otherPattern.patternString.substring(0, secondDotPos));
return this.parser.parse(secondFile + (isFirstExtensionWildcard ? secondExtension : firstExtension));
}
@Override
@@ -313,6 +313,13 @@ class ContentDispositionTests {
tester.accept("foo.txt\\\\\\", "foo.txt\\\\\\\\\\\\");
}
@Test
void formatWithUtf8FilenameWithQuotes() {
String filename = "\"中文.txt";
assertThat(ContentDisposition.formData().filename(filename, StandardCharsets.UTF_8).build().toString())
.isEqualTo("form-data; filename=\"=?UTF-8?Q?=22=E4=B8=AD=E6=96=87.txt?=\"; filename*=UTF-8''%22%E4%B8%AD%E6%96%87.txt");
}
@Test
void formatWithEncodedFilenameUsingInvalidCharset() {
assertThatIllegalArgumentException().isThrownBy(() ->
@@ -18,15 +18,19 @@ package org.springframework.http.client;
import java.io.IOException;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJreRange;
import org.junit.jupiter.api.condition.JRE;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.lang.Nullable;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -34,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link JdkClientHttpRequestFactory}.
*
* @author Marten Deinum
* @author Brian Clozel
*/
class JdkClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests {
@@ -88,4 +93,22 @@ class JdkClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests {
}
}
@Test // gh-34971
@EnabledForJreRange(min = JRE.JAVA_19) // behavior fixed in Java 19
void requestContentLengthHeader() throws Exception {
URI uri = URI.create(baseUrl + "/header/Content-Length");
assertNoContentLength(uri, HttpMethod.GET);
assertNoContentLength(uri, HttpMethod.DELETE);
}
protected void assertNoContentLength(URI uri, HttpMethod method) throws Exception {
ClientHttpRequest request = factory.createRequest(uri, method);
try (ClientHttpResponse response = request.execute()) {
assertThat(response.getStatusCode()).as("Invalid response status").isEqualTo(HttpStatus.OK);
assertThat(StreamUtils.copyToString(response.getBody(), StandardCharsets.ISO_8859_1))
.as("Invalid Content-Length request header").isEqualTo("Content-Length:null");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 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.
@@ -615,7 +615,7 @@ class Jackson2ObjectMapperBuilderTests {
@Override
public String getModuleName() {
return this.getClass().getSimpleName();
return getClass().getSimpleName();
}
@Override

Some files were not shown because too many files have changed in this diff Show More