Compare commits

...
520 Commits
Author SHA1 Message Date
Brian Clozel 2e6e620ee1 Merge branch '7.0.x' 2026-09-18 17:30:55 +02:00
Brian Clozel f6e53b76ae Polishing contribution
See gh-37272
2026-09-18 17:21:08 +02:00
Eymen f9f9186478 Expose matched PathPattern as MVC request attribute
Signed-off-by: Eymen <eymenonar123@gmail.com>
2026-09-18 17:21:08 +02:00
Brian Clozel 12cf9b7d54 Build against Hibernate 7.4 Javadoc 2026-09-18 17:15:57 +02:00
Sam Brannen 96cbca1db5 Merge branch '7.0.x' 2026-09-18 16:48:36 +02:00
Sam Brannen 6f4021ad87 Merge branch '7.0.x' 2026-09-18 16:37:49 +02:00
Sam Brannen bd1616b0aa Merge branch '7.0.x' 2026-09-18 15:54:08 +02:00
머랭 71965c1c85 Align DispatcherServlet bean detection logs
RequestToViewNameTranslator and FlashMapManager logged the simple class
name at TRACE and the object at DEBUG. Swap them to match the multipart
and locale resolver logs. This follows Spring's logging guidance to keep
DEBUG output compact.

See: https://github.com/spring-projects/spring-framework/wiki/Logging

Closes gh-37296

Signed-off-by: cookie-meringue <daehyeon3351@gmail.com>
2026-09-18 15:08:00 +02:00
cookie-meringue 3d023b3ef5 Remove unchecked cast in DispatcherServlet
Match the attribute snapshot parameter type to its sole caller.
This removes the unchecked cast and warning suppression.

Signed-off-by: cookie-meringue <daehyeon3351@gmail.com>
2026-09-18 14:43:51 +02:00
Yanming Zhou 00291a5172 Polish PathPattern to refine null-safety
Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-09-18 14:30:08 +02:00
Yanming Zhou c1ba31c40f Use "isEmpty" to check if a collection is empty
Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-09-18 14:14:36 +02:00
Brian Clozel 4cdd314850 Merge branch '7.0.x' 2026-09-18 14:09:23 +02:00
Sam Brannen 884975b7eb Polish AOT generated resources support
This commit fixes Javadoc typos, grammar, and incorrect/swapped
references; avoids redundant self-validation in GeneratedResource's
createOrValidate(); adds missing Javadoc; and adds/renames tests
accordingly.

See gh-35862
2026-09-18 13:25:21 +02:00
Brian Clozel e8eb2b6751 Add Java 27 to CI testing matrix 2026-09-17 18:28:22 +02:00
Stéphane Nicoll 496ed729a0 Add support for AOT generated resources
This commit updates the AOT infrastructure to handle generated resources
in a similar fashion than generated classes: naming conventions, feature
prefixes, and uniqueness are applied.

The new abstraction also provides a more explicit contract that guides
users to either create the resource or create it if it does not exist
and validate its content if it does.

As part of this change ClassNameGenerator has been renamed to
NameGenerator as it is responsible to generate names for both classes
and resources.

Closes gh-35862
2026-09-17 18:22:26 +02:00
Brian Clozel 7ca362a003 Merge branch '7.0.x' 2026-09-17 16:52:42 +02:00
Brian Clozel c3360ba7da Switch to Micrometer SNAPSHOTs
See gh-37288
2026-09-17 16:07:26 +02:00
Brian Clozel 3222c1b3a8 Deprecate PropertyAccessorUtils formally
Now that all `PropertyAccessorUtils` has been removed and replaced by
`PropertyPath` and local private methods, we can officially deprecate
this utility class and remove it in the future.

Closes gh-37275
2026-09-17 15:37:40 +02:00
Brian Clozel cd110ad14e Use PropertyPath instead of utility methods
Prior to this commit, `DataBinder` and the property accessor
hierarchy relied on `PropertyAccessorUtils` and several
independent scanners for property paths.
This commit migrates all of them to `PropertyPath`, so
there is exactly one parser deciding what a well-formed property
path is, used identically for policy checks and for actual
navigation.

This removes long standing protected methods like
A`getPropertyAccessorForPropertyPathi` and `getFinalPath` from
`bstractNestablePropertyAccessor`. The path is now parsed exactly
once per public entry point, via the new `resolvePropertyPath`, which
returns a `ResolvedProperty`. Then, property navigation walks
the parsed segment list rather than re-scanning partial strings.
Any subclass overriding the removed method will need to adapt.

This removal initially conflicted with gh-37252 (maxNestedPathDepth
support). The public configuration remains but the actual behavior
changed; it is replaced with `PropertyPath.Options` which enforces
limit right after parsing, before property navigation begins.
The exception thrown changes from `InvalidPropertyException` to
`InvalidPropertyPathException`.

This commmit also reverts the `map[']` / `map["]` quoting behavior
from gh-36765 as it is incompatible with the new grammar.

See gh-37275
2026-09-17 15:37:40 +02:00
Brian Clozel 0d079ea435 Extract bean property path support in PropertyPath
Prior to this commit, bean property path support would be duplicated in
`AbstractNestablePropertyAccessor` and `PropertyAccessorUtils`. This
means they both supported the parsing, validation and extraction of path
segments. Implementations were not always in sync and could cause
issues.

This commit introduces a new `PropertyPath` type that holds the
canonical form of the property path and the parsed path segments for
property access. This implements an efficient parser that rejects
invalid property paths early if they don't match the new grammar.

`PropertyPath.parse(String, Options)` additionally accepts a maximum
nesting depth, rejecting an excessively deep path immediately after
parsing and before any navigation of an object graph begins.
This moves the implementation introduced in gh-37252, but keeps the
public configuration in place.

`InvalidPropertyPathException` is introduced to report a syntactically
invalid path, as distinct from a syntactically valid path that
happens not to resolve against a particular target object (see
`NotReadablePropertyException` and `NotWritablePropertyException`).
It extends `PropertyAccessException`, but not `InvalidPropertyException`.
Malformed paths should be collected into
`PropertyBatchUpdateException` alongside other per-property failures.

See gh-37275
2026-09-17 15:37:40 +02:00
Sam Brannen e2cc271e0c Merge branch '7.0.x' 2026-09-17 12:52:59 +02:00
Sam Brannen 8ab525f6f0 Add a configurable limit for maximum nested property path depth
AbstractNestablePropertyAccessor resolves a nested property path
recursively, one recursive call per path segment, and there was
previously no limit on the nesting depth. Consequently, a sufficiently
deeply nested property path -- for example, against a self-referential
type -- could exhaust the current thread's call stack, resulting in a
StackOverflowError which lacks useful diagnostics for developers
attempting to assess what went wrong.

Note that the existing autoGrowCollectionLimit bounds array and
collection growth, not path depth.

The same is true for constructor binding via DataBinder.construct(),
which constructs a nested constructor argument recursively through a
nested property path. Path segments are constrained to declared
constructor parameters there, but a self-referential type nonetheless
permits an arbitrarily deep path.

With this commit, each property accessor tracks the number of nested
properties traversed to reach the object that it wraps, and an
InvalidPropertyException is thrown once the configured (or default)
maxNestedPathDepth limit is exceeded, with a message that reports the
configured limit. The limit applies regardless of autoGrowNestedPaths,
since resolving an existing deep object graph recurses in the same
manner as auto-growing one. Tracking the depth per property accessor
rather than threading it through the recursion allows the recursion to
dispatch through the protected
getPropertyAccessorForPropertyPath(String) method, which subclasses may
override, and avoids deriving the depth from the nested path, which
would require rescanning an ever longer path prefix at each level.

Constructor binding likewise tracks the nesting depth while
constructing nested objects as well as indexed and mapped elements, and
throws the same InvalidPropertyException once the limit is exceeded.

The maxNestedPathDepth (which defaults to 100) can be configured on a
per-use-case basis via ConfigurablePropertyAccessor or DataBinder,
which applies it to constructor binding directly and supplies it to the
property accessor via its binding result. In contrast to the auto-grow
collection limit, which is unlimited on a plain accessor, the nesting
depth is bounded by default even for programmatic property access,
since a large array or collection can be perfectly legitimate whereas a
deeply nested property path effectively never is.

Specifying zero for the maxNestedPathDepth disables support for nested
property paths altogether while continuing to allow simple, indexed,
and mapped property access, which is a reasonable way to constrain data
binding for a target object that is not intended to be traversed (such
as a flat DTO). However, negative values for maxNestedPathDepth are
always rejected.

Closes gh-37252
2026-09-16 17:52:04 +02:00
rstoyanchev 33988a4621 Refine lost connection checks in DefaultHandlerExceptionResolver
This commit adds additional "disconnected client" checks for
HttpMessageNotReadableException and HttpMessageNotWriteableException,
both of which wrap I/O errors and could be due to a lost connection.

Closes gh-37151
2026-09-15 16:26:37 +01:00
Brian Clozel 5b424c0431 Merge branch '7.0.x' 2026-09-15 10:06:25 +02:00
Sam Brannen 6504e75669 Merge branch '7.0.x' 2026-09-14 18:17:15 +02:00
Sam Brannen 566887d573 Merge branch '7.0.x' 2026-09-14 18:07:12 +02:00
Sam Brannen 0148c4ccde Merge branch '7.0.x' 2026-09-14 17:39:11 +02:00
Sam Brannen dabd63770f Add dedicated unit test for getBean(String, ParameterizedTypeReference)
See gh-34687
See gh-37047
2026-09-14 16:59:18 +02:00
Juergen Hoeller fe2617582a Upgrade to Groovy 5.1.2, Hibernate ORM 7.4.8, Jackson 3.1.6/2.21.6, Woodstox 7.2.2 2026-09-14 16:27:52 +02:00
Juergen Hoeller c4376c04b1 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-09-14 16:22:05 +02:00
Yanming Zhou 74a6c1c328 Fix BeanFactory.getBean(String, ParameterizedTypeReference) to respect AOP proxy
Before this commit, the implementation uses `ResolvableType::isInstance` which doesn't take JDK proxy into account, it fails if `proxyTargetClass = false`:

```
Bean named 'userDao' is expected to be of type 'org.springframework.cache.config.ExpressionCachingIntegrationTests$BaseDao<org.springframework.cache.config.ExpressionCachingIntegrationTests$User>' but was actually of type 'org.springframework.cache.config.$Proxy53'
org.springframework.beans.factory.BeanNotOfRequiredTypeException: Bean named 'userDao' is expected to be of type 'org.springframework.cache.config.ExpressionCachingIntegrationTests$BaseDao<org.springframework.cache.config.ExpressionCachingIntegrationTests$User>' but was actually of type 'org.springframework.cache.config.$Proxy53'
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:212)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1312)
	at org.springframework.cache.config.ExpressionCachingIntegrationTests.expressionIsCacheBasedOnActualMethod(ExpressionCachingIntegrationTests.java:42)
```

See gh-34687
Closes gh-37047

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-09-14 15:57:07 +02:00
Sam Brannen 05a1075b69 Merge branch '7.0.x' 2026-09-14 15:02:41 +02:00
Sam Brannen 9f8a476f94 Merge branch '7.0.x' 2026-09-14 14:51:44 +02:00
Sam Brannen afdbebee70 Merge branch '7.0.x' 2026-09-14 14:39:51 +02:00
Sébastien Deleuze a099debca9 Upgrade to Kotlin 2.4.20
Closes gh-37271
2026-09-11 18:03:52 +02:00
Brian Clozel c1d4a76692 Merge branch '7.0.x' 2026-09-10 16:59:22 +02:00
Sam Brannen 8d50a5b724 Merge branch '7.0.x' 2026-09-10 16:27:35 +02:00
Brian Clozel b40ed77e94 Follow up changes in Servlet multipart support
Apply similar changes to the Servlet multipart message converter.

See gh-37264
2026-09-10 14:41:13 +02:00
Brian Clozel d1402d3c7f Merge branch '7.0.x' 2026-09-10 14:34:01 +02:00
Artyom Tsvirko adbc8ceeab Ignore invalid SSE retry field
Per the SSE specification, a "retry" field whose value is not made up
solely of ASCII digits must be ignored. ServerSentEventHttpMessageReader
passed the value straight to Long.parseLong, so "retry:none", an empty
"retry:", or a value too large for a long raised NumberFormatException
and terminated the event stream. A client cannot control what a server
sends, so an unusable reconnection hint would kill an otherwise healthy
subscription.

Signed-off-by: Artyom Tsvirko <36863599+lArtiquel@users.noreply.github.com>
2026-09-09 15:08:23 +02:00
Brian Clozel 60b9f4cd3a Merge branch '7.0.x' 2026-09-09 14:56:35 +02:00
Brian Clozel aa655718a8 Switch to Reactor 2026.0.0 SNAPSHOTs
See gh-37263
2026-09-09 11:38:48 +02:00
Brian Clozel 06f2fba5ed Merge branch '7.0.x' 2026-09-09 10:40:54 +02:00
Tran Ngoc Nhan e8f5e31219 Remove redundant whitespace
Closes gh-37262

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-09-09 10:18:37 +02:00
Sam Brannen 572850bdcf Merge branch '7.0.x' 2026-09-08 13:30:50 +02:00
Sam Brannen 1bc5bb0f90 Make canonical SpelParserConfiguration constructor package-private
The 9-arg canonical constructor for SpelParserConfiguration was
recently introduced to support the new maximumNestingDepth property in
7.1. However, this feature has not yet been released, and in the
interim we introduced a builder API which supersedes the use of those
constructors.

Since no released version has ever exposed this constructor publicly,
this commit converts it to package-private in favor of exclusively
using the builder to construct instances which need to override the
default value for maximumNestingDepth.

See gh-36723
See gh-37187
See gh-37190
2026-09-08 12:59:07 +02:00
Brian Clozel 30a07ed551 Merge branch '7.0.x' 2026-09-08 11:59:09 +02:00
Brian Clozel 4c8c6409a2 Polishing contribution
See gh-37202
2026-09-07 11:57:05 +02:00
Sagar Chanchal 8f4fcb6cbc Emit multipart parts with empty bodies in PartGenerator
Parts were emitted only from PartListener.onBody(buffer, last=true), so a
part with an empty body (for example a blank form field, or a trailing
empty part) was silently dropped from the resulting MultiValueMap, and
was indistinguishable from an absent field.

This carries over the fix from the reactive DefaultPartHttpMessageReader
(spring-framework#30953): State gains an onComplete() callback that emits
the part, also when it has an empty body. It is invoked when a new part
begins, and when parsing completes for the final part.

Signed-off-by: Sagar Chanchal <Sagarr2112@gmail.com>
2026-09-07 11:42:44 +02:00
Sam Brannen 2028c54d01 Polish TableMetaDataContextTests
See gh-37014
2026-09-07 10:48:19 +02:00
김준형 e06482ad51 Reject overlapping declared and generated key columns in SimpleJdbcInsert
When a column was declared via usingColumns() and also listed in
usingGeneratedKeyColumns(), TableMetaDataContext.reconcileColumnsToUse
accepted the declared list as-is: the generated key column was rendered
into the INSERT statement and counted against the parameter values,
even though the database is expected to generate its value.

Such an overlap is a configuration error, so it is now rejected at
compile time with an InvalidDataAccessApiUsageException naming the
offending columns in their declared spelling, consistent with the
existing validation in AbstractJdbcInsert.compile(). Matching is
case-insensitive, mirroring the normalization used for auto-discovered
columns; the auto-discovery path itself is unchanged and continues to
exclude generated key columns silently.

The tests cover the rejection, its message, a case-insensitive variant,
and the untouched non-overlapping declared path.

Closes gh-37014

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-09-07 10:34:38 +02:00
Sam Brannen 99f2ccc2ec Merge branch '7.0.x' 2026-09-07 10:18:58 +02:00
Brian Clozel 1aedfa5d5a Merge branch '7.0.x' 2026-09-07 10:00:25 +02:00
Brian Clozel 93528f096a Merge branch '7.0.x' 2026-09-07 09:35:59 +02:00
Sam Brannen e74054be0a Merge branch '7.0.x' 2026-09-05 14:52:07 +02:00
Sam Brannen 42fe29218b Merge branch '7.0.x' 2026-09-05 13:42:41 +02:00
dxbjavid e92bf76055 validate samesite attribute in ResponseCookie
Signed-off-by: dxbjavid <dxbjavid@gmail.com>
2026-09-04 18:27:48 +02:00
Sunghyun Shin 19fdc0b77c Migrate responseBodyAdvice test to Jackson 3 converter
Migrate RequestMappingHandlerAdapterTests#responseBodyAdvice from the
deprecated MappingJackson2HttpMessageConverter to
JacksonJsonHttpMessageConverter.

The test advice now implements ResponseBodyAdvice directly and returns a
map body that is written by the selected converter.

This maintains the test coverage for gh-22638, verifying that a
ControllerAdvice implementing both ResponseBodyAdvice and
RequestBodyAdvice is not registered twice.

Signed-off-by: Sunghyun Shin <froggy0m0a@gmail.com>
2026-09-04 18:21:37 +02:00
Clayton Walker 82018e1510 Fix configuration-cache compatibility with ArchRule task
Signed-off-by: Clayton Walker <clayton.m.walker@gmail.com>
2026-09-04 17:42:06 +02:00
Brian Clozel 9bedb06b9b Merge branch '7.0.x' 2026-09-04 17:34:11 +02:00
Brian Clozel 54b3a8c868 Merge branch '7.0.x' 2026-09-04 17:17:23 +02:00
Brian Clozel 59df1a7031 Merge branch '7.0.x' 2026-09-04 17:09:36 +02:00
Tran Ngoc Nhan c21ea9e249 Add Validation section examples
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-09-04 16:57:58 +02:00
Brian Clozel acbae80205 Merge branch '7.0.x' 2026-09-04 16:43:22 +02:00
Brian Clozel 60e5abff7f Enforce "data: " prefix for outgoing SSE data payloads
Prior to this commit, SSE support in Spring would write payloads with
the "data:" prefix (without space). While this is OK with the standard,
this makes it harder for implementations to support reading and writing
payloads with Spring (the round trip use case).

This commit introduces a breaking change and now enforces "data: " in
all variants. This has the potential of breaking some low level test
suites with text/plain or custom media types, but this should overall
make the situation better for developers.

Closes gh-37242
2026-09-04 16:21:13 +02:00
Sam Brannen 21bb726934 Suppress removal warnings for Derby DB
See gh-36045
2026-09-04 15:47:03 +02:00
Patrick Strawderman 658c263cf6 Use immutable map for static cache in TypeDescriptor
Switch to Map.of for the static commonTypesCache field for immutability.

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
2026-09-04 15:38:40 +02:00
JunHwan 34b9815215 Polish DateTimeFormatterRegistrar to refine null-safety
Narrow the scope of the @⁠SuppressWarnings("NullAway") annotation in
DateTimeFormatterRegistrar from the class level to a single, new
getFactory(Type) accessor.

The `factories` map is a private, final EnumMap that is fully populated
for every `Type` in the constructor and never mutated afterward, so the
suppression only needs to cover that one lookup instead of masking
unrelated issues across the whole class.

Closes gh-37225

Signed-off-by: Junhwan Choi <devjunsday@gmail.com>C
2026-09-04 15:32:23 +02:00
Sam Brannen 21ee87448c Merge branch '7.0.x' 2026-09-04 15:28:48 +02:00
Sam Brannen c0519b9bbf Merge branch '7.0.x' 2026-09-04 15:14:36 +02:00
Brian Clozel c39d48a261 Merge branch '7.0.x' 2026-09-04 15:12:07 +02:00
Raphael Schweikert 130b0ec50c Parse RFC 9651-like date headers
RFC 9651 specifies @«timestamp» as a new format for date headers.
The Deprecation header as specified in RFC 9745, for example, makes use of it.
Make sure this can be parsed using `HttpHeaders#getFirstDate` and `HttpHeaders#getFirstZonedDateTime`

Signed-off-by: Raphael Schweikert <any@sabberworm.com>
2026-09-04 14:28:29 +02:00
Brian Clozel 815911b39c Upgrade XJC Gradle plugin 2026-09-04 14:17:11 +02:00
Hyunwoo Jung d130601a74 Avoid using Project objects as dependency notation
Gradle 9.6 deprecates passing a Project instance as dependency notation,
which currently causes the build to emit deprecation warnings and will
become an error in Gradle 10.

This commit updates KotlinConventions and RuntimeHintsAgentPlugin to use
DependencyFactory#createProjectDependency instead.

Signed-off-by: Hyunwoo Jung <hyunwoojung@kakao.com>
2026-09-04 14:17:11 +02:00
Sam Brannen 4f0b8e4205 Merge branch '7.0.x' 2026-09-04 14:15:54 +02:00
Sam Brannen 42cffc2a55 Merge branch '7.0.x' 2026-09-04 13:55:03 +02:00
Mateo Maza d798317a2b Add WebFlux OpenTelemetry observation convention
Closes gh-37131

Signed-off-by: Mateo Maza <mateomaza.github@gmail.com>
2026-09-04 10:21:44 +02:00
Brian Clozel d550ab1313 Count in memory buffered data against limit in PartGeenrator
The new PartGenerator for parsing multipart requests supports buffering
the content in memory and switching to a file after a configured size.
More specifically, when a multipart part exceeds maxInMemorySize and
InMemoryState switches it over to FileState, the bytes that were
already buffered in memory are flushed to the temp file.

Prior to this commit, this was done via FileState.writeBuffer(), meaning
that the in memory buffered data would not be counted against the
configured limit for writing to a file.
This commit fixes this by writing buffered data with FileState.onBody().

Fixes gh-37238
2026-09-04 09:24:15 +02:00
Brian Clozel 136dddb67d Merge branch '7.0.x' 2026-09-03 17:38:29 +02:00
froggy0m0 0de56c41df Remove stray TODO in exception handler tests
Closes gh-37003

Signed-off-by: Sunghyun Shin <79225728+froggy0m0@users.noreply.github.com>
2026-09-03 13:55:58 +02:00
Yanming Zhou 29602e95b6 Polish DefaultListableBeanFactoryTests
Fix that scope is not overridden and asserted.

Closes gh-36941

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-09-03 13:53:47 +02:00
Yanming Zhou 94458c05e0 Polish method parameter name
The parameter type is `TransactionManager` not `PlatformTransactionManager`.

Closes gh-36702

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-09-03 13:45:46 +02:00
Sam Brannen 351a6d8f61 Merge branch '7.0.x' 2026-09-03 13:23:27 +02:00
Sam Brannen ff9192fa8f Merge branch '7.0.x' 2026-09-03 11:49:10 +02:00
Sam Brannen 3be70836ce Merge branch '7.0.x' 2026-09-03 11:43:00 +02:00
Sam Brannen 1911647ab5 Eagerly reject property paths with unbalanced brackets
As a follow up to 1b56f58999, this commit introduces
hasUnbalancedBrackets() in PropertyAccessorUtils, which
AbstractNestablePropertyAccessor uses to reject property paths with
unbalanced '[' or ']' brackets by throwing a
NotReadablePropertyException with an informative message, thereby
improving diagnostics for users.

See gh-36999
2026-09-02 17:25:16 +02:00
Brian Clozel 2ce9563b3c Merge branch '7.0.x' 2026-09-02 14:45:26 +02:00
rstoyanchev e2235b702a Merge branch '7.0.x' 2026-09-02 10:00:49 +01:00
Brian Clozel 558e8955f0 Merge branch '7.0.x' 2026-09-01 11:20:33 +02:00
Brian Clozel 46913bca91 Revert "Remove unnessary args after Kotlin 2.4 upgrade"
This reverts commit ce00aac748.
2026-09-01 09:54:16 +02:00
Brian Clozel 5ab70bb036 Merge branch '7.0.x' 2026-08-31 20:24:39 +02:00
Brian Clozel ce00aac748 Remove unnessary args after Kotlin 2.4 upgrade
See gh-37074
2026-08-31 18:46:06 +02:00
Brian Clozel 990d1a8370 Fix Derby warnings
See gh-36045
2026-08-31 18:45:41 +02:00
Brian Clozel 5df0c2dc0c Merge branch '7.0.x' 2026-08-31 18:39:19 +02:00
Juergen Hoeller 5c4bf226bb Merge branch '7.0.x' 2026-08-31 17:31:45 +02:00
Artyom Tsvirko 2028eb3694 Handle MIME type parameter names case-insensitively
MIME type parameter names are case-insensitive, and MimeType already
stores them in a LinkedCaseInsensitiveMap. Several code paths, however,
still compared them with case-sensitive String.equals().

As a result, MimeType.hashCode() disagreed with MimeType.equals() for
parameter names that differ only in case, breaking the equals/hashCode
contract: text/plain;FOO=bar and text/plain;foo=bar are equal but hash
differently, so one is not found in a hash-based collection holding the
other. MimeType.compareTo() had the same blind spot for the charset
parameter.

MediaType was affected in two further ways: an out-of-range quality
value escaped validation when spelled Q=, and removeQualityValue() left
a Q= parameter in place.

Signed-off-by: Artyom Tsvirko <36863599+lArtiquel@users.noreply.github.com>
2026-08-31 17:30:39 +02:00
Sam Brannen ee19b96ee9 Merge branch '7.0.x' 2026-08-31 17:17:01 +02:00
junhyeong9812andYash bc66622342 Reject MIME type parameters differing only in case
MIME type parameter names are case-insensitive, but MimeTypeParser
accumulates parameters in a case-sensitive LinkedHashMap. As a result,
duplicate parameters differing only in case (such as "charset" and
"CHARSET") were not rejected and were silently collapsed to the last
value by the case-insensitive parameter map of MimeType.

Accumulate parameters in a LinkedCaseInsensitiveMap so that duplicates
differing only in case map to the same key and are rejected consistently
with exact duplicates.

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
Co-authored-by: Yash <190389954+yashsiwacha@users.noreply.github.com>
2026-08-31 16:23:20 +02:00
Sam Brannen dd110d4604 Revise contribution
See gh-37219
2026-08-31 15:17:13 +02:00
Yanming Zhou 513ca9d9e5 Polish ScheduledAnnotationBeanPostProcessor to refine null-safety
Closes gh-37219

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-08-31 15:10:57 +02:00
Sam Brannen 4b5afaff53 Revise "Close contexts when clearing test context cache"
This commit picks up where 97067f9c9c left off by removing the four
trailing collection-clearing calls in clear(), which are now redundant
since the removal loop already empties contextMap, hierarchyMap,
contextUsageMap, and unusedContexts as an invariant. This commit also
adds regression tests in LruContextCacheTests for closing a context
hierarchy via clear() and reset(), asserting bottom-up close order with
Mockito.inOrder(), and confirming that getParentContextCount() and
getContextUsageCount() return to zero.

See gh-36825
2026-08-31 13:21:18 +02:00
Will-thom 97067f9c9c Close contexts when clearing test context cache
This updates the TestContext Framework cache so that clearing the
cache also closes cached ConfigurableApplicationContext instances
instead of only dropping the internal references. The implementation
reuses the existing removal path, preserving the hierarchy-aware close
behavior already used by cache eviction/removal.

The ContextCache contract now documents the close behavior for
clear(), and LruContextCacheTests covers both clear() and reset(),
since reset() delegates to clear().

See gh-26196
Closes gh-36825

Signed-off-by: Will-thom <116388885+Will-thom@users.noreply.github.com>
2026-08-31 13:07:54 +02:00
Brian Clozel 5524fc6a55 Merge branch '7.0.x' 2026-08-31 12:58:00 +02:00
Juergen Hoeller 49d955650a Upgrade to Groovy 5.1.1 and Hibernate ORM 7.4.7 2026-08-31 11:08:44 +02:00
Juergen Hoeller b020aca2eb Merge branch '7.0.x' 2026-08-31 11:02:55 +02:00
Juergen Hoeller 354ade9f40 Merge branch '7.0.x' 2026-08-29 20:02:43 +02:00
Sam Brannen 1b56f58999 Use depth-aware bracket parsing in PropertyAccessorUtils
Previously, canonicalPropertyName() located the end of a [key]
expression via a naive indexOf("]") search, while
AbstractNestablePropertyAccessor.getPropertyNameKeyEnd() -- used during
actual property resolution -- tracked bracket nesting depth. This meant
the two methods could disagree on the canonical form of a property
path whose map key itself contains bracket characters (e.g.,
map['key[0]']).

Similarly, getNestedPropertySeparatorIndex() tracked whether a dot
separator occurs inside a [key] expression using a simple boolean
toggle that flips on both '[' and ']', which produces an incorrect
result when a key contains an odd net count of inner bracket
characters.

To address those inconsistencies, this commit extracts the private
getPropertyNameKeyEnd() method from AbstractNestablePropertyAccessor to
a package-private static utility method in PropertyAccessorUtils, so
that canonicalPropertyName() can reuse the same depth-aware bracket
matching, and getNestedPropertySeparatorIndex() has been reworked to
track bracket nesting depth instead of toggling a boolean flag.

Closes gh-36999
2026-08-28 19:11:20 +02:00
Brian Clozel 3eb93a4873 Merge branch '7.0.x' 2026-08-28 18:36:12 +02:00
jungh8n 09548c0874 Upgrade HtmlUnit and Selenium dependencies
HtmlUnit 5 moved its Cookie type to org.htmlunit.http, so
Spring Test's HtmlUnit integration now uses the new API.

Constraint: Keep HtmlUnit and htmlunit3-driver versions compatible.
Rejected: Upgrade HtmlUnit alone | driver requires HtmlUnit 5.4.0.
Confidence: high
Scope-risk: narrow
Directive: Keep HtmlUnit and the driver version aligned.
Tested: JAVA_HOME=/opt/homebrew/opt/openjdk@25 ./gradlew build
Not-tested: None
Signed-off-by: jungh8n <jh981113@naver.com>
2026-08-28 18:20:04 +02:00
Brian Clozel 491859b5ce Merge branch '7.0.x' 2026-08-28 18:15:06 +02:00
Brian Clozel 5d6a56fe4a Polishing CacheControl behavior
This commit builds on the previous commit and ensures that
"must-understand" is only used with "no-store". This check is performed
at runtime as a staged interface/builder would be a major breaking
change for a behavior that is highlighted as "SHOULD" in the
specification.

This commit also performs similar runtime checks for:
* cache-public + cache-private
* cache-public + no-store

See gh-36918
2026-08-28 15:49:57 +02:00
heka1024 d9e240b37d Add Cache-Control must-understand directive
Signed-off-by: heka1024 <heka1024@gmail.com>
2026-08-28 15:10:37 +02:00
Yanming Zhou 2588fb078e Polish ConcurrentLruCache to refine null-safety
Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-08-28 14:53:40 +02:00
Istvan Verhas 752193fca9 Refactor JettyDataBuffer with new JettyVirtualDataBuffer
This commits simplifies the Jetty buffer support by consolidating
the shared logic and delegating operations to the parent class
thanks to the new `JettyVirtualDataBuffer`.

Signed-off-by: Istvan Verhas <vi@mocker.guru>
2026-08-28 14:42:41 +02:00
junhyung8795 17e0daf8a1 Use computeIfAbsent in CommandLineArgs.addOptionArg
Signed-off-by: junhyung8795 <junhyung8795@naver.com>
2026-08-27 17:00:41 +02:00
Brian Clozel d883602919 Remove mentions of the Derby database support
See gh-36045
2026-08-27 16:57:58 +02:00
Philippe Marschall 1b354f2705 Deprecate Derby support
Deprecate Derby support since Apache Derby is retired since 2023.

Signed-off-by: Philippe Marschall <philippe.marschall@gmail.com>
2026-08-27 16:50:58 +02:00
Brian Clozel 7daf1013aa Merge branch '7.0.x' 2026-08-27 14:37:23 +02:00
Sam Brannen 6e9534df4d Limit bracket depth in PropertyEditorRegistrySupport
Previously, PropertyEditorRegistrySupport.addStrippedPropertyPaths()
recursively enumerated every combination of stripped/retained [key]
segments in a property path, producing 2^n - 1 variants for a path
with n bracket pairs.

To address that, this commit limits the recursion at a depth of 8,
preserving existing behavior for realistic property paths while
bounding the work done for paths with an unusually large number of
bracket segments.

Closes gh-37020
2026-08-26 16:36:40 +02:00
Sam Brannen a00fb1b5ae Avoid redundant object construction in DataBinder.createMap()
Previously, createMap() invoked createIndexedValue() – and therefore
createObject() for non-simple value types – once per matching parameter
name rather than once per distinct map key, causing redundant nested
object construction for map entries whose value type has multiple
constructor parameters.

To address that, this commit aligns createMap() with createList() and
createArray() by skipping construction for keys that have already been
resolved.

Closes gh-37019
2026-08-26 13:34:57 +02:00
rstoyanchev b28569119f Merge branch '7.0.x' 2026-08-24 17:21:41 +01:00
Sam Brannen 79a75a5762 Polish contribution
See gh-36935
2026-08-24 12:36:28 +02:00
seonwoojung ac95b96c21 Suppress CGLIB validation WARN for lifecycle callbacks
When CglibAopProxy validates the target class, it logs a WARN-level
message for each public final method that implements an interface,
suggesting to use interface-based JDK proxies instead. For final
methods inherited from Spring's configuration callback interfaces
(InitializingBean, DisposableBean, Aware sub-interfaces, Closeable,
AutoCloseable) that recommendation is misleading: those methods are
container-driven, are not advised by typical application pointcuts, and
the user usually cannot make them non-final.

The validation now only emits the WARN-level message when at least one
user-defined interface declares the method. Methods inherited
exclusively from configuration callback interfaces fall back to the
existing DEBUG diagnostic.

In addition, the isConfigurationCallbackInterface() method has been
extracted from ProxyProcessorSupport into a static package-private
method in AopProxyUtils with the same signature, and
ProxyProcessorSupport and CglibAopProxy now delegate to the new shared
static utility in AopProxyUtils.

See gh-35365
Closes gh-36935

Signed-off-by: seonwoo_jung <laborlawseon@kap.kr>
Signed-off-by: seonwooj0810 <seonwooj0810@gmail.com>
2026-08-24 12:18:48 +02:00
Sam Brannen 91eb42645e Deprecate SpelParserConfiguration constructors in favor of the builder API
Since we now have an official builder API for SpelParserConfiguration
(introduced in 7.0.10), this commit follows through on the plan stated
in that commit's Javadoc and formally deprecates all 9 overloaded
constructors in SpelParserConfiguration, thereby encouraging users to
benefit from the simplicity of the builder API -- or
SpelParserConfiguration.withDefaults() for the common case -- instead
of having to migrate to the latest-and-greatest full constructor every
time a new configuration property is introduced.

The no-arg constructor points users to withDefaults(), and all other
constructors -- including the canonical 9-parameter constructor --
point to the builder API. Builder.build() has been annotated with
@SuppressWarnings("deprecation"), since it is the sole legitimate
internal caller of the now-deprecated canonical constructor.

The SpelParserConfigurationTests.LegacyConstructorTests nested class
(and its sibling builderAppliesSameDefaultsAsNoArgConstructor() test
method) are annotated with @SuppressWarnings("deprecation"), since they
exist specifically to provide regression coverage for the deprecated
constructors. IndexingTests.MaxAutoGrowSizeTests and
SpelParserTests.MaxNestingDepthTests, on the other hand, were both
introduced before the builder API existed and had no such need for the
legacy constructors, so they have been converted to use the builder API
instead, avoiding the need for any deprecation suppression there.

See gh-37187
Closes gh-37190
2026-08-22 13:28:57 +02:00
Sam Brannen aee39843f8 Merge branch '7.0.x' 2026-08-22 12:09:56 +02:00
Sam Brannen 9dabfe98e8 Account for all array objects when checking array size in SpEL
Prior to this commit, ConstructorReference.createArray() enforced the
MAX_ARRAY_ELEMENTS threshold for multi-dimensional arrays by checking
only the product of all dimension sizes, which is equivalent to the
total number of leaf-level elements. However, Array.newInstance()
allocates a distinct array object at every nesting level, not just at
the leaf level. For dimensions [d0, d1, ..., dk-1], the total number
of array objects created is 1 + d0 + d0*d1 + ... + d0*d1*...*d(k-2).
As a result, an expression such as new int[262143][1][1]...[1], whose
trailing dimensions are all 1, kept the leaf-element product just
under the threshold while still causing tens of millions of array
objects to be allocated.

To address that, this commit introduces a second running total,
totalArrayObjects, alongside the existing leaf-element product in the
multi-dimensional array construction loop. Both totals are checked
against MAX_ARRAY_ELEMENTS on every iteration, so array constructions
that fan out into an excessive number of array objects are now
rejected even when the leaf-element count remains within bounds.

Note that SimpleEvaluationContext does not permit array construction
in SpEL expressions at all, so this fix effectively only changes
behavior for expressions evaluated via StandardEvaluationContext.

Tests have been added to ArrayConstructorTests to verify that the new
check rejects array constructions with an excessive number of array
objects and that array constructions just under the threshold remain
unaffected.

Closes gh-36998
2026-08-21 15:21:13 +02:00
Sam Brannen fb240829b3 Align SpEL's default max auto-grow size with Spring data binding
Prior to this commit, the SpelParserConfiguration constructors that
omit an explicit maximumAutoGrowSize left collection auto-growing
effectively unbounded, defaulting to Integer.MAX_VALUE. That default
was inconsistent with the auto-grow limit applied elsewhere in the
framework for data binding (see
DataBinder.DEFAULT_AUTO_GROW_COLLECTION_LIMIT).

To address that, this commit introduces a new
SpelParserConfiguration.DEFAULT_MAX_AUTO_GROW_SIZE constant (set to 256
to match DataBinder.DEFAULT_AUTO_GROW_COLLECTION_LIMIT) and switches
the constructors that previously hard-coded Integer.MAX_VALUE to use
this new default instead. Constructors that accept an explicit
maximumAutoGrowSize are unaffected.

In addition, SpelParserConfiguration now enforces that a user-supplied
maximumAutoGrowSize is not a negative value, consistent with the
preconditions already enforced for maximumExpressionLength,
maximumOperations, maximumBigPowerBits, and maximumNestingDepth. A
value of 0 remains supported (effectively disabling collection
auto-growing) and is now documented as such in the Javadoc.

The Spring Framework reference documentation has also been updated to
describe the new default, and tests have been added to IndexingTests to
verify the default, the ability to override it, and the new
precondition.

Closes gh-36995
2026-08-21 14:11:04 +02:00
Sam Brannen 68d438c9ff Polishing
See gh-36723
2026-08-21 13:23:54 +02:00
Sam Brannen 8473ec3e25 Add a configurable limit for maximum nesting depth in SpEL expressions
This commit introduces support for limiting the structural nesting
depth of a SpEL expression during parsing. Without such a limit, an
expression with deeply nested constructs (for example, inline lists or
maps, parenthesized expressions, ternary or Elvis expressions, or
chained unary operators) can cause SpEL's recursive-descent parser to
throw a StackOverflowError which lacks useful diagnostics for
developer's attempting to assess what went wrong.

With this commit, a nesting-depth counter is now tracked around the
parser's primary recursive entry point (eatExpression()) as well as
around chained unary operators (eatUnaryExpression()), ensuring that
independent, sibling uses of assignment, Elvis, and ternary expressions
do not inadvertently accumulate depth and trip the limit.

If the configured (or default) nesting-depth limit is exceeded during
parsing, a SpelParseException is thrown instead, with a message that
reports the configured limit.

The limit can be configured on a per-use-case basis via
SpelParserConfiguration and defaults to 1000.

Closes gh-36723
2026-08-21 13:15:14 +02:00
Brian Clozel 89047909ea Merge branch '7.0.x' 2026-08-20 18:26:02 +02:00
Brian Clozel df72838ce1 Merge commit 'v7.1.0-M1~1' 2026-08-20 18:18:13 +02:00
Sam Brannen 6ee3ef6af5 Avoid unnecessarily synthesizing meta-annotations with attributes
In commit 622fc3edf7, I introduced a check in
TypeMappedAnnotation#isSynthesizable() intended to force synthesis when
an attribute value needs to be resolved from a different level of a
multi-level annotation hierarchy whose root annotation does not
redeclare the target attribute itself.

That check tested if `resolvedMirrors.length > 0` for a
meta-annotation; however, resolvedMirrors is always sized according to
the number of attributes declared by the mapped annotation type,
regardless of whether any of those attributes actually participate in
mirroring or an @⁠AliasFor override. As a result, the check effectively
synthesized any meta-annotation that declares at least one attribute,
which reintroduced the unnecessary-synthesis behavior that commit
d6768ccc18 had fixed, merely narrowed to meta-annotations with
attributes.

This commit replaces that overly broad check with a precise one in
AnnotationTypeMapping#computeSynthesizableFlag(), which now also
considers whether any attribute's value must be resolved from a
different annotation in the meta-annotation hierarchy (tracked via
annotationValueSource). This correctly identifies the original
multi-level hierarchy scenario without over-matching on ordinary
meta-annotations that have nothing to merge or override.

See gh-28704
See gh-28716
Closes gh-37135
2026-08-20 16:32:05 +02:00
greg taube 59a784cb7e Avoid unnecessary allocations for cached annotation mappings
This commit defers creation of the visited annotation types set until a
cache miss occurs, which avoids allocating a HashSet for every cached
annotation mapping lookup while preserving recursive annotation
handling during mapping creation.

Closes gh-37141

Signed-off-by: GT <gregjotau@gmail.com>
2026-08-20 16:27:18 +02:00
김준형 af466ccf63 Fix OptionalToObjectConverter applicability check
OptionalToObjectConverter.matches() used
TypeDescriptor.getElementTypeDescriptor(), which returns null for an
Optional (element types are only resolved for arrays, streams and
collections). ConversionUtils.canConvertElements() then treats a null
source element type as "maybe" and returns true unconditionally, so
ConversionService.canConvert(Optional<X>, target) reported true even
when X is not convertible to the target -- a violation of the
canConvert contract, since the subsequent conversion fails.

To address that, this commit resolves the Optional's element type from
its generic and checks it against the target, mirroring
ObjectToOptionalConverter. A raw or otherwise unresolved element type
remains permissive.

Closes gh-36913

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-08-20 16:19:37 +02:00
Sam Brannen bcfa6c3c4f Merge branch '7.0.x' 2026-08-20 15:56:20 +02:00
Sam Brannen 74b6a5ba3e Merge branch '7.0.x' 2026-08-20 14:57:50 +02:00
Brian Clozel 2730d77f82 Polishing contribution
Closes gh-34993
2026-08-20 14:13:55 +02:00
Mario Daniel Ruiz Saavedra 4a64537ac6 Add QUERY HTTP method
Signed-off-by: Mario Daniel Ruiz Saavedra <desiderantes93@gmail.com>
2026-08-20 14:13:55 +02:00
Sam Brannen 555ac3768d Merge branch '7.0.x' 2026-08-20 12:45:02 +02:00
Sam Brannen e9e00e3257 Merge branch '7.0.x' 2026-08-20 10:59:06 +02:00
Sam Brannen 526c706d1c Merge branch '7.0.x' 2026-08-19 18:41:13 +02:00
junhyeong9812 e8e293a706 Reject write methods not starting with "set" in Property
Prior to this commit, Property.resolveName() located the "set" prefix
of a write method with String.indexOf(), which matches the token
anywhere in the method name. A write method that merely contains "set"
(for example "offsetX" or "upset") was silently accepted and resolved
to a meaningless property name derived from whatever follows the token,
while only names with no "set" token at all were rejected.

To address that, this commit matches the "set" prefix only at the start
of the method name via startsWith(), so that an
IllegalArgumentException is consistently thrown for any write method
candidate that is not a setter.

See gh-36911
Closes gh-37139

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-08-19 18:26:46 +02:00
김준형 f067d40f0a Fix Property name resolution for record-style accessors
Property.resolveName() located the get/is accessor prefix with
String.indexOf, which matches the prefix anywhere in the method name.
A plain accessor whose name embeds such a prefix (for example
budget()) had the wrong portion stripped and resolved to an empty or
wrong property name, which in turn caused the backing field's
annotations to be silently dropped.

Match the get/is prefix only at the start of the method name and do
not strip it when the method is a plain accessor for a data class,
that is, a non-static no-arg method referring to an instance field of
the same name. This supports Java records, Kotlin data classes, and
custom Java data classes alike, without relying on java.lang.Record.

As a consequence, a getter backed by a field of the exact same name
(for example isUrgent()) now resolves to the field name.

Closes gh-36911

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-08-19 18:19:04 +02:00
Sam Brannen fd95ab16ba Use verified property names when constructing Property instances in SpEL
ReflectivePropertyAccessor's canRead(), read(), and canWrite() methods
previously constructed org.springframework.core.convert.Property
instances without an explicit name, forcing Property#resolveName() to
re-derive the property name from the accessor method via prefix
matching. That heuristic incorrectly resolves record-style and other
prefix-less accessor methods whose names embed or start with "get"/"is"
(for example, budget(), issue(), or island()), and it also normalizes
acronym-style JavaBean properties inconsistently (for example, getURL()
resolves to "uRL" rather than "URL").

By the time these three methods construct a Property, they have already
located the accessor method by searching for exactly the requested
property name, so the resolved name is already known and verified. This
commit passes that name through explicitly via the 4-arg Property
constructor, bypassing Property#resolveName() entirely at these call
sites.

This commit also introduces tests in PropertyAccessTests to cover the
following scenarios:

- A genuine record accessor whose component name embeds or starts with
  a "get"/"is" prefix
- The same scenario on a hand-written, non-record "data class"
- The read() call site exercised directly, since it is otherwise
  unreachable once canRead() has warmed the cache
- A boolean isXxx() getter, as a plain regression check
- An acronym-style property with a decoy field to prove that the
  correct field (and its annotations) is now resolved for both reads
  and writes

See gh-36911
Closes gh-37123
2026-08-19 17:45:47 +02:00
Sam Brannen ba13cae6cc Make nullability contracts in JMS SimpleMessageConverter explicit
TextMessage.getText() and ObjectMessage.getObject() may both return
null per the JMS specification when the message body was never set, but
SimpleMessageConverter.fromMessage() and its parent MessageConverter
interface currently declare a non-null return type despite residing in
an @⁠NullMarked package.

To address that, this commit updates MessageConverter.fromMessage(),
SimpleMessageConverter, and the protected
extractStringFromMessage()/extractSerializableFromMessage() methods to
declare @⁠Nullable accordingly and propagate the resulting nullability
through MessagingMessageConverter and
AbstractAdaptableMessageListener/MessageListenerAdapter, raising a
clear MessageConversionException where a non-null payload is required
by the Message<T> contract.

Closes gh-37148
2026-08-19 17:26:45 +02:00
rstoyanchev ee3b666a5c Merge branch '7.0.x' 2026-08-19 14:19:30 +03:00
Hyunwoo Jung a165094470 Fix RestClient API usage in documentation
Closes gh-37137

Signed-off-by: Hyunwoo Jung <hyunwoojung@kakao.com>
2026-08-18 18:49:06 +02:00
Sam Brannen 04e5c92162 Polish MockCookieTests
See gh-37134
See gh-37136
2026-08-18 18:24:54 +02:00
Tran Ngoc Nhan b20d31a5cd Update MockCookie#parse(String) validation to align with Javadoc
See gh-37134
Closes gh-37136

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-08-18 18:24:38 +02:00
Sam Brannen daa8c10031 Polish contribution
See gh-36556
2026-08-18 18:17:57 +02:00
Vedran Pavic 7da47f8c47 Simplify programmatic scheduling of cron tasks with time zone
This commit adds an overloaded `addCronTask()` method to `ScheduledTaskRegistrar`
that allows simpler scheduling of cron tasks with non-default time zones.

Closes gh-36556

Signed-off-by: Vedran Pavic <vedran@vedranpavic.com>
2026-08-18 18:14:01 +02:00
Sam Brannen a947f9bf79 Merge branch '7.0.x' 2026-08-18 17:14:31 +02:00
Sam Brannen a13056f6af Merge branch '7.0.x' 2026-08-18 16:44:50 +02:00
Brian Clozel d823822dd6 Merge branch '7.0.x' 2026-08-18 09:25:15 +02:00
Brian Clozel 14927a959f Upgrade to Reactor 2026.0.0-M1
Closes gh-37107
2026-08-14 09:21:18 +02:00
Brian Clozel efbae85324 Upgrade to Micrometer 1.18.0-M1 and Tracing 1.8.0-M1
Closes gh-37108
2026-08-14 09:21:18 +02:00
rstoyanchev e12f0761f3 Refactor maxInMemory limit handling for async XML parsing
The limit was previously enforced in XmlEventDecoder, because it is
what parses incoming buffers. However, the actual caching is in
Jaxb2Decoder, which holds on to XML events, but has no good way to
estimate their size.

After this commit XmlEventDecoder no longer enforces memory limits
for async parsing. It releases each buffer immediately anyway.

Instead XmlEventDecoder is only responsible to update the number
of bytes received via a new ReceivedByteTracker type while
Jaxb2XmlDecoder uses the same to perform limit and reset the
count depending on when it is aggregating XML events.

Closes gh-37031
2026-08-14 09:19:58 +02:00
rstoyanchev 30e3a5719e Leading slash handling in UrlHandlerFilter
Closes gh-37030
2026-08-14 09:19:58 +02:00
rstoyanchev d31f7a5a80 Apply ResourceHandlerUtils checks in XsltView
Closes gh-37029
2026-08-14 09:19:58 +02:00
rstoyanchev 1c77e241e6 Consistent maxPartSize check in PartEventHttpMessageReader
Closes gh-37028
2026-08-14 09:19:58 +02:00
rstoyanchev 9312e25e24 Check viewName for special prefixes in UrlFilenameViewController
Closes gh-37027
2026-08-14 09:19:58 +02:00
rstoyanchev d5dbc9b310 Ensure Payload release on early error in createHeaders
Closes gh-37026
2026-08-14 09:19:58 +02:00
rstoyanchev a69fe71630 Return sameSite cookie value in Jetty response
Closes gh-37025
2026-08-14 09:19:58 +02:00
rstoyanchev d6f5356db1 Add preflight handling in RouterFunctionWebHandler
Request predicates support preflight request matching based on the "would be"
request (e.g. target HTTP method) so the actual handler is not meant to be
invoked. That's the case only with a DispatcherHandler setup.

Closes gh-37024
2026-08-14 09:19:58 +02:00
rstoyanchev b4d9b514f5 Update exception messages in HandshakeWebSocketService
Closes gh-37023
2026-08-14 09:19:58 +02:00
Sam Brannen ea3e61fd2f Disable SpEL expression compilation by default in SimpleEvaluationContext
Prior to this commit, SpEL expression compilation could be silently
activated in a SimpleEvaluationContext via the
`spring.expression.compiler.mode` Spring/system property or
SpelParserConfiguration. Once an expression is compiled, the evaluation
guards enforced during interpreted evaluation are no longer applied,
which is at odds with the restricted intent of SimpleEvaluationContext.

To address that, this commit introduces a mechanism analogous to
isAssignmentEnabled() which disables compilation by default in
SimpleEvaluationContext. Specifically:

- A new isCompilationSupported() default method has been introduced in
  the EvaluationContext API, which returns true by default.

- SimpleEvaluationContext overrides isCompilationSupported() to return
  false by default. However, compilation can be opted into explicitly
  via the new withCompilationSupported() method in the
  SimpleEvaluationContext.Builder.

- SpelExpression.checkCompile() now consults isCompilationSupported()
  before triggering new compilation, ensuring that evaluation within an
  EvaluationContext never produces a compiled form of the expression if
  the context's isCompilationSupported() method returns false.

- All eight getValue() variants in SpelExpression now consult
  isCompilationSupported() before executing a compiled expression,
  ensuring that a compiled expression produced via a different
  EvaluationContext is not silently reused if the caller inadvertently
  switches to an EvaluationContext that does not support compilation.

Closes gh-37035
2026-08-14 09:19:58 +02:00
Sam Brannen 8a92c19e4d Limit result size of BigDecimal/BigInteger power operations in SpEL
This commit introduces a configurable limit on the estimated result size
of BigDecimal and BigInteger power operations within SpEL expressions.
The estimated result size in bits is computed as the product of the base
value's bit length and the exponent. If this limit is exceeded, a
SpelEvaluationException is thrown.

The limit defaults to 1,000,000 bits, which is approximately equivalent
to a decimal number with 300,000 digits, and can be configured either
on a per-use-case basis via the new maximumBigPowerBits constructor
argument in SpelParserConfiguration or globally as a JVM system
property or Spring property named `spring.expression.maxBigPowerBits`.
Parsers intended for trusted internal expressions may supply
Integer.MAX_VALUE to remove the limit entirely.

Closes ch-37034
2026-08-14 09:19:58 +02:00
Sam Brannen ee1874ac52 Check list index after auto-grow in AbstractNestablePropertyAccessor
Prior to this commit, the List branch in
AbstractNestablePropertyAccessor's getPropertyValue() method called
list.get(index) unconditionally after invoking
growCollectionIfNecessary(), which implicitly relied on the list
throwing an IndexOutOfBoundsException for out-of-range access. Such an
exception is caught downstream and wrapped as an
InvalidPropertyException; however, any List implementation whose get()
method allocates elements on demand rather than throwing an
IndexOutOfBoundsException could bypass that check.

This behavior was also inconsistent with the Collection/Iterable branch
in the same method, which already performs an explicit `index >=
collection.size()` bounds check before attempting element access.

To address that, this commit aligns the List branch with the
Collection/Iterable branch by adding an explicit `index < 0 || index >=
list.size()` check immediately after the auto-grow attempt. If the
index remains out of bounds after growCollectionIfNecessary() runs –
for example, because growth was capped by autoGrowCollectionLimit or
auto-growing was disabled – an InvalidPropertyException is now thrown
rather than delegating to list.get() which may or may not throw an
exception.

Closes gh-37036
2026-08-14 09:19:58 +02:00
Sébastien Deleuze 8cb1151375 Ensure consistent EscapedErrors field error escaping
Closes gh-37055
2026-08-14 09:19:58 +02:00
Sébastien Deleuze bb65d819a4 Reject backslashes in SpringTemplateLoader template names
Closes gh-37054
2026-08-14 09:19:58 +02:00
Brian Clozel 5abe6d3e5f Centralize Server Sent Event utility methods
Prior to this commit, many classes would support writing Server Sent
Events in some way to the response output stream. This has lead to some
code duplication.

This commit refactors the duplicated code in a shared `SseUtils` class.

Closes gh-37065
2026-08-14 09:19:58 +02:00
Brian Clozel 999f428987 Ensure parsing/tostring symmetry in ContentDisposition
Prior to this commit, building a "Content-Disposition" header to a
String and then parsing it back would not always result in the original
header.

This commit ensures that ContentDisposition guarantees this and honors
the "equals" contract.

Fixes gh-37064
2026-08-14 09:19:58 +02:00
Brian Clozel fd50270b8d Escape SSE view fragments
Prior to this commit, the MVC and WebFlux view fragments rendering would
only partially escape rendered view fragments before sending then as SSE
events. This could in some cases break the SSE stream with invalid data.

This commit ensures that the rendered views are properly escaped before
they are sent as SSE events.

Fixes gh-37061
2026-08-14 09:19:58 +02:00
Brian Clozel b10179ffdf Switch to INTERNAL-SNAPSHOTs 2026-08-14 09:19:58 +02:00
Brian Clozel f8974ad620 Prepare main-internal branch 2026-08-14 09:19:58 +02:00
Sam Brannen c7712052ce Merge branch '7.0.x' 2026-08-13 19:01:47 +02:00
Sam Brannen e78d3df566 Revert "Update MockCookie#parse(String) validation to align with Javadoc"
This reverts commit 8b894933ae due to
code freeze on main.

See gh-37134
2026-08-13 17:15:34 +02:00
Sam Brannen 951c1f306a Revert "Polish MockCookieTests"
This reverts commit 27a85be4e0 due to
code freeze on main.

See gh-37134
2026-08-13 17:15:10 +02:00
Sam Brannen 27a85be4e0 Polish MockCookieTests
See gh-37134
2026-08-13 12:41:41 +02:00
Tran Ngoc Nhan 8b894933ae Update MockCookie#parse(String) validation to align with Javadoc
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-08-13 12:38:02 +02:00
Juergen Hoeller 68e6acd37e Merge branch '7.0.x' 2026-08-12 00:09:51 +02:00
Juergen Hoeller 88b383a153 Upgrade to Jackson 3.1.5 and 2.21.5 2026-08-11 23:55:26 +02:00
Juergen Hoeller 00b9063e7d Polishing 2026-08-11 23:48:56 +02:00
Juergen Hoeller cc751e61a2 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-08-11 23:23:40 +02:00
Sam Brannen 69bf83ad71 Merge branch '7.0.x' 2026-08-09 17:47:04 +03:00
Sam Brannen da4b31c82b Merge branch '7.0.x' 2026-08-07 10:54:27 +03:00
rstoyanchev b6cb9a5f87 Merge branch '7.0.x' 2026-08-07 10:32:13 +03:00
Juergen Hoeller 7f01cd0a5b Introduce enforceReadOnly flag for JTA 2.1 read-only mode
Closes gh-35915
2026-08-06 20:18:13 +02:00
Brian Clozel 51c4539bb6 Stop using deprecated HttpMessageConverterExtractor in StatusHandler
Prior to this commit, `HttpMessageConverterExtractor` was deprecated
with `RestTemplate` and related types. `StatusHandler` was still using
it and causing a deprecation warning.

This commit extracts the relevant implementation from
`DefaultRestClient` and promotes it as a shared static method in
`RestClientUtils`.

Fixes gh-37010
2026-08-05 17:54:02 +02:00
samlightfoot 595c246cce Skip logging operators in DefaultExchangeFunction when possible
Prior to this commit, `DefaultExchangeFunction.exchange` added
logging operations within `doOnRequest`/`doOnCancel` operators
unconditionally, which costs two subscriber wrappers per request
even though the log message construction itself is already
guarded lazily. This commit gates the operators on `isDebugEnabled()`,
checked per exchange so runtime log level changes are still honored.

Signed-off-by: samlightfoot <samueldlightfoot@gmail.com>
2026-08-05 16:56:13 +02:00
Brian Clozel 4b5c92703c Merge branch '7.0.x' 2026-08-05 10:44:06 +02:00
Brian Clozel c17b4ad787 Merge branch '7.0.x' 2026-08-05 10:09:11 +02:00
Brian Clozel e8729d0438 Switch to SNAPSHOT dependencies
See gh-37107
See gh-37108
2026-08-03 15:17:34 +02:00
Brian Clozel d2d7fd36da Merge branch '7.0.x' 2026-08-03 11:36:43 +02:00
Sam Brannen 0abf59feee Merge branch '7.0.x' 2026-08-03 12:35:10 +03:00
Sam Brannen 11da74d51b Merge branch '7.0.x' 2026-08-03 11:35:45 +03:00
Brian Clozel eceebb3077 Merge branch '7.0.x' 2026-07-31 18:52:03 +02:00
Brian Clozel f3e202e1b5 Merge branch '7.0.x' 2026-07-31 18:26:09 +02:00
Brian Clozel 17002a26cc Merge branch '7.0.x'
# Conflicts:
#	.github/workflows/build-and-deploy-snapshot.yml
#	.github/workflows/release-milestone.yml
#	.github/workflows/release.yml
2026-07-31 18:15:39 +02:00
Juergen Hoeller 0376dd9a78 Merge branch '7.0.x' 2026-07-31 16:28:17 +02:00
Sam Brannen 7c2fdcc1fb Merge branch '7.0.x' 2026-07-30 16:25:12 +03:00
Sam Brannen abe33703b4 Update Javadoc for PropertyDescriptorUtils.determineBasicProperties()
See gh-37081
2026-07-30 15:16:19 +03:00
Arnab Nandy badddeb0dc Ignore static get/is accessor methods in PropertyDescriptorUtils
Prior to this commit, PropertyDescriptorUtils.determineBasicProperties()
incorrectly recognized static `get` and `is` accessor methods as
JavaBean read methods, in contrast to the standard
java.beans.Introspector, which has always excluded static methods from
property discovery. This regression was introduced in Spring Framework
6.0 when determineBasicProperties() replaced the delegation to
java.beans.Introspector for the fast property-discovery path used by
SimpleBeanInfoFactory. As a result, an unrelated static method such as
a singleton accessor could be exposed as a bean property, and
reflective access to such a property (for example, via BeanWrapperImpl)
could lead to a StackOverflowError if the property's value recursively
exposed the same static accessor.

To address that, this commit adds Modifier.isStatic(...) checks to the
`get` and `is` branches in determineBasicProperties(), mirroring the
equivalent check already present in
CachedIntrospectionResults.isPlainAccessor(). Static `set` methods
continue to be supported as write methods, consistent with the existing
behavior in ExtendedBeanInfo.

See gh-37068
Closes gh-37081

Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
2026-07-30 14:12:08 +02:00
rstoyanchev 50f923ace4 Restore MatchableHandlerMapping
See gh-36481
2026-07-30 12:07:17 +03:00
Sam Brannen 317eae88d0 Merge branch '7.0.x' 2026-07-29 22:07:54 +03:00
Sam Brannen 2aaeef7190 Merge branch '7.0.x' 2026-07-29 21:38:13 +03:00
rstoyanchev 8bc5e11ec3 Remove HandlerMappingIntrospector
Closes gh-36481
2026-07-29 18:03:09 +03:00
rstoyanchev f53674d582 Replace HandlerMappingIntrospector with DefaultPreFlightRequestHandler
See gh-36481
2026-07-29 18:03:08 +03:00
rstoyanchev 67d54dccd2 Polishing contribution
See gh-36816
2026-07-29 16:17:54 +03:00
jhan0121 4bcae5305d Deprecate setDisallowedFields in DataBinder
Closes gh-36816
Signed-off-by: Juhwan Lee <jhan0121@gmail.com>
2026-07-29 16:17:09 +03:00
rstoyanchev 3f632382d6 Add WebClientResponseException.PreconditionFailed
See gh-36807
2026-07-29 15:55:04 +03:00
rstoyanchev cde75754bc Polishing in HttpClientErrorException
See gh-36807
2026-07-29 15:50:39 +03:00
Dominik Kovács dddd237449 Add HttpClientErrorException.PreconditionFailed
Closes gh-36807

Signed-off-by: Dominik Kovács <dominik.kovacs28@gmail.com>
2026-07-29 15:50:38 +03:00
rstoyanchev 56f7cc2dab Merge branch '7.0.x' 2026-07-29 15:18:45 +03:00
Juergen Hoeller 91c6851f29 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-07-29 12:17:09 +02:00
Juergen Hoeller 16d9965fe3 Consistently enforce non-null instance in AbstractFactoryBean
Closes gh-37091
2026-07-27 19:47:12 +02:00
rstoyanchev 1d1aac3674 Merge branch '7.0.x' 2026-07-27 12:58:34 +03:00
Sam Brannen d5acf5bceb Merge branch '7.0.x' 2026-07-26 10:59:55 +03:00
Sam Brannen 4c192bf58f Merge branch '7.0.x' 2026-07-26 10:21:32 +03:00
Sam Brannen cb6226c98a Merge branch '7.0.x' 2026-07-25 11:06:31 +03:00
rstoyanchev ffcf37468f Update documentation on forwarded headers
See gh-37072
2026-07-24 22:31:52 +03:00
Brian Clozel e24f5f2ca7 Merge branch '7.0.x' 2026-07-24 15:15:52 +02:00
Brian Clozel 079992021c Improve MimeType parser for RFC compliance
Prior to this commit, the `MimeType` class would compare raw parameter
values for the equals/hashcode contract. This went against the RFC which
states that quoted and unquoted parameter values are equivalent.

This commit rewrote the entire `MimeType` parser in `MimeTypeUtils`
as a state parser to improve robustness and performance.
The `MimeType` equals, compareTo and hascode contracts now unquote
parameter values before comparing them.

This change also optimizes the `tokenize` function that splits many
comma-separated MIME types into a list. Now that this method isn't used
anywhere else, it is also deprecated as of 7.1. This method was
initially made public to be reused within Spring Framework and has no
particular use in Spring applications in general.

Finally, this also makes `MediaType` and `MimeType` leverage the
`MimeType` LRU cache as much as possible, including when parsing
`Accept:` HTTP headers.

Closes gh-36729
2026-07-24 14:29:36 +02:00
rstoyanchev 9bdeadcfbd Deprecate historic forwarded header behavior
See gh-37072
2026-07-23 12:15:24 +03:00
rstoyanchev 31c37d4f2c Require choice between Forwarded and X-Forwarded headers
This commit introduces a constructor argument to select whether
to use the standard "Forwarded" header or the "X-Forwarded-*"
alternative headers. A separate property enables support for
X-Forwarded-Prefix.

Closes gh-37072
2026-07-23 12:15:24 +03:00
rstoyanchev 68862530fb Parse the standard "Forwarded" header directly
This commit introduces manual parsing of the standard "Forwarded"
header instead of using regular expressions.

Closes gh-36964
2026-07-23 12:15:24 +03:00
rstoyanchev 7b71df72e5 Add ForwardedInfo to ForwardedHeaderUtils
This commit adds two new methods in ForwardedHeaderUtils, one to parse
the standard "Forwarded" header only, and another to parse the
"X-Forwarded-*" alternative headers. As those are single parse methods,
a ForwardedInfo container type is necessary to return the results.

See gh-36964
2026-07-23 12:15:24 +03:00
Brian Clozel 224522244f Merge branch '7.0.x' 2026-07-22 10:54:58 +02:00
Tran Ngoc Nhan 1502ab0b20 Correct HandlerMethodValidationExceptionTests package
Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>
2026-07-21 15:54:53 +03:00
Sam Brannen 304f8eb27e Merge branch '7.0.x' 2026-07-21 12:00:08 +03:00
Sébastien Deleuze 3dfb4ef754 Upgrade Kotlin Coroutines to 1.11.0
Closes gh-37076
2026-07-20 15:30:59 +02:00
Sébastien Deleuze 08a4844288 Upgrade Kotlin to 2.4.10
Closes gh-37074
2026-07-20 15:30:58 +02:00
Sam Brannen 38e1bf5970 Merge branch '7.0.x' 2026-07-20 11:07:00 +03:00
Brian Clozel 5ac20a8104 Reinstate invalid resource location checks
This checks was removed previously because the location was considered
as invalid in #36695, but they were later reinstated in #36692.

This commit also reinstates the check that prevents static resource
resolution in those locations.

Closes gh-37063
2026-07-17 13:41:38 +02:00
Brian Clozel 12d71c9a9b Merge branch '7.0.x' 2026-07-16 19:21:35 +02:00
Brian Clozel 0791d9a6e3 Merge branch '7.0.x' 2026-07-16 19:10:36 +02:00
Sébastien Deleuze 734c7ed4a7 Merge branch '7.0.x' 2026-07-16 17:32:26 +02:00
Juergen Hoeller 40f7d56ed4 Register original bean name as alias if not taken already
Closes gh-37038
2026-07-16 10:45:16 +02:00
Juergen Hoeller c4c0a84f83 Merge branch '7.0.x' 2026-07-16 10:20:53 +02:00
Sam Brannen 99b991b6f3 Upgrade to JUnit 6.1.2
Closes gh-36815
2026-07-13 10:16:37 +02:00
Brian Clozel 9024d5ffbd Merge branch '7.0.x' 2026-07-12 11:55:38 +02:00
Sébastien Deleuze 6dd2aa3988 Merge branch '7.0.x' 2026-07-08 20:46:34 +02:00
Sébastien Deleuze 28bf619887 Merge branch '7.0.x' 2026-07-08 17:23:42 +02:00
Sam Brannen 1700fad16d Update due to deprecation warnings 2026-07-06 15:01:43 +02:00
Sam Brannen d71643c347 Remove unused code 2026-07-06 14:55:47 +02:00
Juergen Hoeller 257687d44b Upgrade to Aalto 1.4, Gson 2.14, Woodstox 7.2.1 2026-07-06 12:52:52 +02:00
Juergen Hoeller 9a82d107c0 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-07-06 12:32:43 +02:00
Sam Brannen 45b4d00d9f Merge branch '7.0.x' 2026-07-06 11:03:12 +02:00
Sam Brannen 4bcb6cc081 Merge branch '7.0.x' 2026-07-02 12:08:26 +02:00
Sébastien Deleuze df0ec74107 Merge branch '7.0.x' 2026-07-01 15:41:32 +02:00
Sam Brannen c5d7908a78 Find transitive interface annotations in findAllLocalMergedAnnotations()
This commit picks up where ce718cf699 left off by ensuring that
findAllLocalMergedAnnotations() also finds annotations declared on
transitive interfaces — that is, on interfaces of the directly
implemented interfaces of the root declaring class.

The previous implementation filtered annotations from a single
TYPE_HIERARCHY search by checking whether the annotation's source was
either the root declaring class or one of its directly declared
interfaces. However, this excluded annotations inherited through an
interface chain such as First -> Second -> Third, where the annotation
is declared on Third but not on Second.

This commit replaces that approach with two targeted searches whose
results are combined:

- DIRECT on the root declaring class, to capture annotations declared
  directly on the class (including via composed/meta-annotations)
- TYPE_HIERARCHY on each directly implemented interface, which
  naturally traverses the full super-interface chain of each interface

A corresponding test for this scenario has also been added.

Closes gh-36975
2026-06-30 16:44:53 +02:00
Sam Brannen ce718cf699 Always find interface annotations in findAllLocalMergedAnnotations()
Prior to this commit, AnnotationDescriptor's
findAllLocalMergedAnnotations() filtered results using
MergedAnnotationPredicates.firstRunOf(
MergedAnnotation::getAggregateIndex), which retains only annotations
that share the same aggregate index as the first annotation
encountered. Since annotations on the root declaring class have
aggregate index 0 and annotations on interfaces have higher indices,
interface annotations were silently excluded whenever the root
declaring class itself declared the annotation.

This commit fixes the issue by replacing the aggregate-index-based
filter with a source-based filter that explicitly includes annotations
from the root declaring class and from each directly implemented
interface. The Javadoc for findAllLocalMergedAnnotations() has also
been updated to document the ordering guarantee: annotations from the
root declaring class appear first, followed by annotations from
implemented interfaces in declaration order.

Closes gh-36975
2026-06-28 14:35:18 +02:00
Sam Brannen efaa53b488 Upgrade to JUnit 6.1.1
Closes gh-36815
2026-06-28 13:55:12 +02:00
Sam Brannen bb34bf6dc6 Merge branch '7.0.x' 2026-06-27 18:09:31 +02:00
Sam Brannen 8f539b6e23 Merge branch '7.0.x' 2026-06-27 17:14:07 +02:00
Sam Brannen 62eea96151 Merge branch '7.0.x' 2026-06-27 16:19:38 +02:00
Sam Brannen 3cf19aabcd Merge branch '7.0.x' 2026-06-27 15:20:15 +02:00
Juergen Hoeller fc0ad3d367 Merge branch '7.0.x' 2026-06-26 18:21:51 +02:00
Sam Brannen 69839889c4 Merge branch '7.0.x' 2026-06-26 17:41:35 +02:00
rstoyanchev 2b7ec43571 Merge branch '7.0.x' 2026-06-25 16:10:55 +01:00
Sam Brannen 787f9e1cbb Merge branch '7.0.x' 2026-06-25 13:57:56 +02:00
rstoyanchev ed0919b92e Merge branch '7.0.x' 2026-06-25 12:50:49 +01:00
Sam Brannen 7e9da44e18 Merge branch '7.0.x' 2026-06-25 13:33:10 +02:00
Sam Brannen a077324670 Polish contribution
See gh-36956
2026-06-23 12:19:28 +02:00
Yanming Zhou c35e17b038 Refactor NumberToDataSizeConverter to use DataSize.ofBytes(long) directly
Prior to this commit, a `Number` was converted to a `String` and then
the string was parsed/matched using regular expressions back into a
suitable `long` which was inefficient and also prevented valid data
size values such as 10.0.

To address those issues, this commit refactors
NumberToDataSizeConverter to use DataSize.ofBytes(long) directly, first
checking that the supplied Number does not have a fractional part.

Closes gh-36956

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-06-23 12:19:27 +02:00
Juergen Hoeller 0cdb0cfae4 Upgrade to Jackson 3.1.4 / 2.21.4, Hibernate ORM 7.4.2, Hibernate Validator 9.1.1 2026-06-23 12:07:35 +02:00
Juergen Hoeller bd405756ab Avoid "NullAway.Init" suppression in favor of explicit field handling
Closes gh-36961
2026-06-23 11:55:58 +02:00
Juergen Hoeller 9130ded96f Resolve against type variable from same declaration if possible
Closes gh-36890
2026-06-22 22:09:52 +02:00
Juergen Hoeller 0dc2d03093 Merge branch '7.0.x'
# Conflicts:
#	spring-context/src/main/java/org/springframework/validation/DataBinder.java
2026-06-22 21:55:43 +02:00
rstoyanchev 8a2e4a9e0a Merge branch '7.0.x' 2026-06-22 14:08:46 +01:00
rstoyanchev 0fbe714bd1 Merge branch '7.0.x' 2026-06-22 13:50:10 +01:00
Sam Brannen 9c64be98e4 Merge branch '7.0.x' 2026-06-19 16:32:04 +02:00
rstoyanchev 8e6b6c5ac3 Refine error handling in MultipartParser
Closes gh-36947
2026-06-17 15:54:48 +01:00
Sam Brannen 7b31e0c2dc Polish contribution
See gh-36938
2026-06-17 15:49:10 +02:00
junhyeong9812 233e7b91f9 Throw ClassNotFoundException for missing class resource in ThrowawayClassLoader
Prior to this commit, ThrowawayClassLoader.loadClass fell back to
loadClassFromResource(), which returns null when no class resource is
available. Returning null from loadClass violates the ClassLoader
contract and leads to a NullPointerException in callers such as
PreComputeFieldFeature.

To address that, this commit rethrows the original
ClassNotFoundException when the resource fallback yields no class.

Closes gh-36938

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-06-17 15:47:27 +02:00
Sam Brannen 077bfaf095 Polish contribution
See gh-36830
2026-06-17 14:15:04 +02:00
YeongJae Min 175f551d91 Add DataSize converters to DefaultConversionService
Spring Boot already provides equivalent converters, and DataSize itself
already exposes parsing support via DataSize.parse(...).

This commit makes that conversion available through Spring Framework's
default conversion service.

- new StringToDataSizeConverter
- new NumberToDataSizeConverter
- both converters are registered with the DefaultConversionService
- new tests for string, number, empty, and invalid inputs

This intentionally does not move Spring Boot's @⁠DataSizeUnit support
into Spring Framework.

See gh-28910
Closes gh-36830

Signed-off-by: YeongJae Min <whereismysejong@naver.com>
2026-06-17 14:15:04 +02:00
rstoyanchev 2bc0ee7ec1 Polishing in PartGenerator 2026-06-17 13:05:10 +01:00
rstoyanchev 65fe0f1d2f PartGenerator disposes of resources in current State
Closes gh-36942
2026-06-17 13:05:10 +01:00
rstoyanchev d5dee4ef1c Close OutputStream after part created in PartGenerator
Closes gh-36945
2026-06-17 13:05:10 +01:00
rstoyanchev 97db213d41 Minor refactoring in MultipartParser
Move the nested InternalParser class up, merging it with the top-level
MultipartParser, make the constructor private, and expose a static
parse method.
2026-06-17 13:05:10 +01:00
leestana01 7565c51dc5 Restore thread interrupt flag in DefaultMvcResult
awaitAsyncDispatch() catches InterruptedException, returns false, and
discards the interruption. Catching InterruptedException without
rethrowing should restore the interrupt status (as is done across the
framework's main sources), so re-assert it before returning false.

Closes gh-36876

Signed-off-by: leestana01 <leestana01@naver.com>
2026-06-17 13:32:36 +02:00
junhyeong9812 03d80feed0 Close class resource InputStream in ThrowawayClassLoader
Prior to this commit, ThrowawayClassLoader#loadClassFromResource opened
an InputStream via getResourceAsStream(...) but never closed it. The
stream leaked on both the success path (after defineClass) and the
IOException path, as the surrounding try-block had neither a finally
nor a try-with-resources clause.

This commit adapts the existing inputStream variable as a
try-with-resources resource so that it is closed on every path, leaving
the loading logic unchanged.

Closes gh-36933

Signed-off-by: junhyeong9812 <pickjog@gmail.com>
2026-06-17 13:12:23 +02:00
Sam Brannen b611fcf114 Use double division to calculate applied jitter in ExponentialBackOff
In order to avoid the staircase scaling effect that results from our
current use of integer division, this commit revises applyJitter(long)
in ExponentialBackOffExecution to use floating-point (double) division
to calculate the applied jitter.

Closes gh-36943
2026-06-17 13:02:49 +02:00
Sam Brannen 472e610c4a Merge branch '7.0.x' 2026-06-17 12:43:48 +02:00
Sam Brannen 2723847917 Merge branch '7.0.x' 2026-06-17 11:59:46 +02:00
Sam Brannen 94db4f7f7a Merge branch '7.0.x' 2026-06-17 11:56:51 +02:00
rstoyanchev 8cfe90c4c0 Polishing in MultipartHttpMessageConverter 2026-06-17 10:14:01 +01:00
Sam Brannen 30287d789c Merge branch '7.0.x' 2026-06-15 15:44:04 +02:00
Sam Brannen c26029c26b Use "instanceof pattern matching" in WebSocketExtension.equals() 2026-06-15 15:25:59 +02:00
Sam Brannen 0bfe82b315 Polishing 2026-06-15 15:25:06 +02:00
Yanming Zhou cdc3c52640 Replace isAssignableFrom() with isInstance() where feasible
Closes gh-36899

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-06-15 15:16:18 +02:00
Juergen Hoeller 0c60266986 Merge branch '7.0.x' 2026-06-09 22:42:03 +02:00
Brian Clozel 99f6f1a77a Merge branch '7.0.x' 2026-06-08 22:19:46 +02:00
Brian Clozel 1bd3ad2050 Merge branch '7.0.x' 2026-06-08 19:51:16 +02:00
Sam Brannen 2b08e6e1d3 Merge branch '7.0.x' 2026-06-08 18:29:20 +02:00
Brian Clozel 4e39c28b92 Merge branch '7.0.x' 2026-06-08 14:49:45 +02:00
Brian Clozel 381fa10477 Merge branch '7.0.x' 2026-06-08 10:41:12 +02:00
rstoyanchev 14f4deef3a Merge branch '7.0.x' 2026-06-05 14:36:23 +01:00
Sam Brannen e23971efb9 Merge branch '7.0.x' 2026-06-05 15:15:19 +02:00
Sam Brannen 2469aae672 Merge branch '7.0.x' 2026-06-05 14:57:29 +02:00
cookie-meringue 83e29382b3 Optimize ClassNameReader.getClassName via direct ASM API
getClassName now calls ClassReader.getClassName() directly instead
of routing through the visitor-based getClassInfo. Previously, it
allocated a List and a ClassVisitor and decoded super_class and
every interface name only to discard all but the first element.

The method is on the hot path of every CGLIB proxy class definition,
so this change significantly lowers its per-call processing cost.

Closes gh-36814

Signed-off-by: cookie-meringue <daehyeon3351@gmail.com>
2026-06-04 14:20:49 +02:00
Brian Clozel ee5c82a2f8 Merge branch '7.0.x' 2026-06-04 12:27:18 +02:00
rstoyanchev ae7891e797 Revise disconnected client error handling in WebFlux
A disconnected client error does not necessarily prevent us from setting
the status of the WebFlux ServerHttpResponse, which is only gated by a
committed flag and does not necessarily reflect the connection state.

This is why we need to check if we have a disconnected client error
first and handle it accordingly. We still set the response to 500
in case the disconnect client error is to a remote host in which
case it will propagate to the client.

Closes gh-36811
2026-06-04 11:10:58 +01:00
rstoyanchev 98ed1dfcf8 Revise disconnected client error handling in Spring MVC
DisconnectedClientHelper identifies lost connection issues, but it's
not always easy to know if it is the connection to the client or to
another remote host. DisconnectedClientHelper does recognize and
filter out common client exceptions, but there is a possibility for
other similar custom exceptions.

DefaultHandlerExceptionResolver now attempts to set the status to
500, which won't impact a client that has gone away, but it will
set the status correct on the off chance that the exception is
actually a server side issue.

Closes gh-34481
2026-06-04 11:10:58 +01:00
Brian Clozel 9b8a851969 Merge branch '7.0.x' 2026-06-04 10:49:36 +02:00
seungchan 4c6194a2ad Simplify BUFFER_COUNT in ConcurrentLruCache to a constant
The detectNumberOfBuffers() method attempted to scale the read
buffer count based on the number of available processors, but used
Math.min(4, nextPowerOfTwo) which effectively caps the result at 4
regardless of CPU count. For systems with fewer than 3 processors,
the buffer count would be reduced below 4, but this edge case adds
complexity without measurable benefit.

Simplify BUFFER_COUNT to a constant value of 4, removing the
unnecessary CPU-detection logic.

Closes gh-36872

Signed-off-by: seungchan <s24041@gsm.hs.kr>
2026-06-04 09:30:36 +02:00
Juergen Hoeller 2fc99eb12f Merge branch '7.0.x' 2026-06-03 23:04:51 +02:00
Brian Clozel 783388e9f8 Merge branch '7.0.x' 2026-06-03 11:23:03 +02:00
Juergen Hoeller de4de02a1d Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
#	spring-context/src/main/java/org/springframework/validation/DataBinder.java
2026-06-02 17:33:07 +02:00
Juergen Hoeller e6ce2a3c36 Expose autoGrowCollectionLimit in ConfigurablePropertyAccessor interface
See gh-36862
2026-06-02 17:20:52 +02:00
Matthias Kurz 481a5743b3 Apply auto-grow limit to direct field binding
DataBinder applies its auto-grow collection limit to bean
property access, but direct field access left DirectFieldAccessor
at its default limit.

Pass DataBinder's configured limit into DirectFieldBindingResult
and apply it to the DirectFieldAccessor.

Closes gh-36861

Signed-off-by: Matthias Kurz <m.kurz@irregular.at>
2026-06-02 16:58:47 +02:00
wushiyuanmaimob 85a8868bae Preserve generic type info in awaitEntity()
awaitEntity() used T::class.java which erases generic type
information (e.g. List<Foo> becomes just List). Use the
reified toEntity<T>() extension instead, which preserves
full generic type via ParameterizedTypeReference.

Closes gh-36834
Signed-off-by: wushiyuanmaimob <wushiyuanwork@outlook.com>
2026-06-02 12:26:41 +02:00
rstoyanchev dd72b9eb4b Merge branch '7.0.x' 2026-06-01 10:53:55 +01:00
Juergen Hoeller 2e65c1dfe6 Merge branch '7.0.x' 2026-05-29 13:31:01 +02:00
Sam Brannen ca72cd66e3 Merge branch '7.0.x' 2026-05-28 10:59:38 +02:00
Juergen Hoeller a3f3ba5685 Merge branch '7.0.x' 2026-05-27 18:59:32 +02:00
rstoyanchev 79f4f76bd2 Merge branch '7.0.x' 2026-05-27 17:19:22 +01:00
Juergen Hoeller 744d136cf7 Upgrade to Hibernate ORM 7.4
Closes gh-36519
2026-05-27 16:44:26 +02:00
Juergen Hoeller 00ca23859e Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-05-27 16:35:51 +02:00
Sam Brannen facc7c5371 Merge branch '7.0.x' 2026-05-27 12:16:46 +02:00
Sam Brannen bd1e7e16a9 Merge branch '7.0.x' 2026-05-27 12:05:04 +02:00
Brian Clozel 25e8395df8 Reject duplicate MIME type parameters
Prior to this commit, MIME type parsing in Spring would allow duplicate
parameters like "text/plain; dupe=1; dupe=2", effectively retaining the
latest value and ignoring the first.

RFC 6838 4.3 states that this should be treated as an error and this
commit ensures that this is the case.

Closes gh-36841
2026-05-26 17:53:28 +02:00
Sam Brannen 148b7fd8f3 Merge branch '7.0.x' 2026-05-26 16:40:06 +02:00
Brian Clozel 68338aa818 Use ASCII chars in Content-Disposition filename parameter
Prior to this commit, gh-36328 avoided using RFC 2047 encoding for the
"filename" parameter and use ISO-8859-1 only. This change unfortunately
caused issues because some implementations might try and detect the
encoding automatically.

This commit restricts the filename parameter to ASCII encoding only by:
* transliterating characters to the closes ASCII character
("é"->"e", "ä"->"ae"...)
* falling back to "_" for other chacacters with non latin alphabet or
  emojis

Fixes gh-36805
2026-05-22 21:33:40 +02:00
Sam Brannen fbbc0f487c Use Constants API introduced in JUnit 6.1
See gh-36815
2026-05-21 12:48:37 +02:00
Sam Brannen 611c390417 Use EngineTestKit in ParallelExecutionSpringExtensionTests 2026-05-21 12:27:33 +02:00
Sam Brannen cf3196e8de Merge branch '7.0.x' 2026-05-20 17:49:42 +02:00
Sam Brannen 80e9e4f6f2 Make ParallelExecutionSpringExtensionTests more robust
... due to changes in JUnit 6.1.0.

See gh-36815
2026-05-20 17:32:01 +02:00
Sam Brannen 91655be0db Merge branch '7.0.x' 2026-05-20 16:53:56 +02:00
Sam Brannen 3b030e0431 Upgrade to JUnit 6.1
Closes gh-36815
2026-05-20 16:34:26 +02:00
Sam Brannen 9870ce1844 Only update ObservationThreadLocalAccessor when test has an active ApplicationContext
Prior to this commit,
MicrometerObservationRegistryTestExecutionListener always attempted to
load the test's ApplicationContext in order to update the
ObservationThreadLocalAccessor in its beforeTestMethod() callback, even
if there was no active ApplicationContext.

To avoid unnecessarily loading an ApplicationContext or attempting to
load an ApplicationContext that cannot be loaded (for example, due to a
context-load failure), this commit applies a hasApplicationContext()
check in beforeTestMethod().

Since the MicrometerObservationRegistryTestExecutionListener is
registered after the DependencyInjectionTestExecutionListener (at least
by default), an active ApplicationContext should be present unless
dependency injection from the context failed or the context failed to
load.

Closes gh-36817
2026-05-20 14:52:20 +02:00
Sam Brannen d87c03a6be Reset mocks only when a test has an ApplicationContext
Prior to this commit and the previous commit,
MockitoResetTestExecutionListener always attempted to load the
ApplicationContext to reset mocks in its beforeTestMethod() and
afterTestMethod() callbacks, even if there was no active
ApplicationContext.

The reason this was noticed is that the @BeforeMethod(alwaysRun = true)
and @AfterMethod(alwaysRun = true) lifecycle methods in
AbstractTestNGSpringContextTests are always invoked, even if a previous
lifecycle configuration method failed (for example, due to a
context-load failure).

However, with JUnit Jupiter and the SpringExtension the
beforeTestMethod() and afterTestMethod() callbacks in the
TestExecutionListener API are not invoked if there was a previous
lifecycle failure.

Consequently, the reported drawbacks only exist when using Spring's
TestNG base support classes

This commit picks up where the previous commit left off by applying the
same hasApplicationContext() check in beforeTestMethod().

This commit also introduces unit and integration tests for both Jupiter
and TestNG support.

Closes gh-36782
2026-05-20 14:04:09 +02:00
seregamorph 1d91982f83 Reset mocks after test only when the test has an ApplicationContext
See gh-36782

Signed-off-by: seregamorph <serega.morph@gmail.com>
2026-05-20 14:03:55 +02:00
Sam Brannen 0c25d817bd Merge branch '7.0.x' 2026-05-17 15:33:24 +02:00
Sam Brannen 17ed3e8370 Merge branch '7.0.x' 2026-05-17 13:56:47 +02:00
rstoyanchev 2f458f9093 Merge branch '7.0.x' 2026-05-15 13:51:31 +01:00
rstoyanchev 16e0ed0463 Merge branch '7.0.x' 2026-05-14 16:56:21 +01:00
rstoyanchev e24448118c Merge branch '7.0.x' 2026-05-14 16:42:29 +01:00
Juergen Hoeller a0ec6656b4 Merge branch '7.0.x' 2026-05-13 19:46:39 +02:00
Juergen Hoeller 5ab4c5c5d9 Add support for JPA 4.0 @PersistenceAgent injection
Closes gh-36264
2026-05-13 19:41:21 +02:00
Juergen Hoeller c3baa01535 Merge branch '7.0.x' 2026-05-12 16:21:43 +02:00
Juergen Hoeller c0b749479a Merge branch '7.0.x' 2026-05-12 13:10:34 +02:00
Juergen Hoeller 5bf58cfe05 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-05-11 13:09:38 +02:00
Vinod Kumar 238a24cb6d Polish collection usage in HttpHeadersTests
Signed-off-by: Vinod Kumar <codingkiddo@gmail.com>
2026-05-11 09:34:28 +02:00
Sam Brannen 9a54f75d6d Merge branch '7.0.x' 2026-05-09 16:40:53 +02:00
Juergen Hoeller 9db16e4c15 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-05-08 16:02:00 +02:00
Sam Brannen ec8f0ca6b2 Merge branch '7.0.x' 2026-05-06 13:54:03 +02:00
Sam Brannen 1a0bd12c34 Merge branch '7.0.x' 2026-05-06 13:26:10 +02:00
Brian Clozel e6590aa1a8 Merge branch '7.0.x'
Closes gh-36753
2026-05-05 11:59:37 +02:00
Sam Brannen 051b09694e Merge branch '7.0.x' 2026-05-04 17:21:22 +02:00
Sam Brannen 39ff8e46ab Use String#replace instead of String#replaceAll in tests
See gh-36678
2026-05-03 14:36:23 +02:00
shenjianeng 27bdf24482 Use String#replace instead of String#replaceAll where appropriate
Avoid using String#replaceAll when the pattern is not a regular
expression.

Using java.lang.String#replace(CharSequence, CharSequence) will
improve performance.

Closes gh-36678

Signed-off-by: shenjianeng <ishenjianeng@qq.com>
2026-05-03 14:34:01 +02:00
Sam Brannen d9ecf945cc Merge branch '7.0.x' 2026-05-02 18:39:59 +02:00
Sam Brannen b35da5b140 Merge branch '7.0.x' 2026-05-02 18:35:24 +02:00
rstoyanchev 367a62018d Merge branch '7.0.x' 2026-05-01 21:48:29 +01:00
Juergen Hoeller 1b26f5d1e6 Adapt bean overriding test for deferred BeanRegistrar processing in 7.1
See gh-36648
See gh-21497
2026-04-30 14:33:55 +02:00
Juergen Hoeller 6ff2d187cf Merge branch '7.0.x'
# Conflicts:
#	spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java
2026-04-30 14:21:49 +02:00
Juergen Hoeller 72cf389754 Merge branch '7.0.x' 2026-04-29 21:53:07 +02:00
rstoyanchev 3184eb3acc Merge branch '7.0.x' 2026-04-29 12:12:31 +01:00
Brian Clozel 9e4c127eda Merge branch '7.0.x' 2026-04-28 23:36:56 +02:00
Sébastien Deleuze 1f642b973b Merge branch '7.0.x' 2026-04-28 11:16:42 +02:00
Brian Clozel 80c9efd638 Merge branch '7.0.x' 2026-04-28 09:29:26 +02:00
Sam Brannen 0e1a2b4f87 Merge branch '7.0.x' 2026-04-27 14:01:51 +03:00
Brian Clozel 5e6e223686 Merge branch '7.0.x' 2026-04-23 16:24:27 +02:00
Brian Clozel 7a5844f1ce Reject unsafe resource handling locations
As of gh-36692, Spring logs a WARN message when an unsafe resource
handling location is configured. This change now rejects entirely such
setups by failing before the application starts up.

Closes gh-36695
2026-04-23 11:29:47 +02:00
Brian Clozel 0d14aa5856 Merge branch '7.0.x' 2026-04-23 10:55:18 +02:00
Juergen Hoeller c6096ff6e5 Upgrade to Hibernate ORM 7.3.2 and Woodstox 7.1.1 2026-04-21 20:37:08 +02:00
Juergen Hoeller 6ff758391c Merge branch '7.0.x' 2026-04-21 20:36:27 +02:00
Brian Clozel ca9b26eb8f Merge branch '7.0.x' 2026-04-21 18:52:41 +02:00
Brian Clozel 279409acce Merge branch '7.0.x' 2026-04-21 17:47:25 +02:00
Sam Brannen 4f1b4b36bf Merge branch '7.0.x' 2026-04-17 17:25:53 +02:00
Juergen Hoeller 680f4ee482 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-04-16 23:30:21 +02:00
rstoyanchev 59cd577fd1 Merge branch '7.0.x' 2026-04-16 21:15:42 +01:00
Sébastien Deleuze 9f92183710 Upgrade to Kotlin Serialization 1.11.0
Closes gh-36657
2026-04-16 16:36:56 +02:00
Sam Brannen 680854d1f3 Merge branch '7.0.x' 2026-04-16 16:35:25 +02:00
Sam Brannen 1a7161c85e Merge branch '7.0.x' 2026-04-16 15:12:44 +02:00
Sam Brannen 8bce51267a Merge branch '7.0.x' 2026-04-16 14:42:00 +02:00
Sam Brannen d169eb547e Polishing 2026-04-15 14:30:03 +02:00
Sam Brannen 9c8535f5e4 Compile SpEL expressions that use Optional with null-safe & Elvis operators
In Spring Framework 7.0, we introduced support for using `Optional`
with the null-safe and Elvis operators in SpEL expressions; however,
such expressions were previously not compilable.

To address that, this commit introduces a new
insertOptionalUnwrapIfNecessary() method in CodeFlow which effectively
inserts byte code instructions for `myOptional.orElse(null)`, and the
Elvis, Indexer, MethodReference, and PropertyOrFieldReference
implementations have been modified to track the need to unwrap an
`Optional` in compiled mode and delegate to
insertOptionalUnwrapIfNecessary() accordingly.

See gh-20433
See gh-36331
Closes gh-36330
2026-04-15 14:09:57 +02:00
Sam Brannen 829add3aa9 Update Javadoc for HttpMethod.valueOf() on main
See gh-36642
See gh-36652
2026-04-14 16:30:56 +02:00
Sam Brannen 20e57608ba Merge branch '7.0.x' 2026-04-14 16:18:23 +02:00
Sam Brannen 6275f46a66 Merge branch '7.0.x' 2026-04-13 17:42:59 +02:00
Sam Brannen 9a17b5c453 Merge branch '7.0.x' 2026-04-13 12:59:04 +02:00
Sam Brannen 1787d3e885 Merge branch '7.0.x' 2026-04-12 17:17:27 +02:00
Sam Brannen 1f5af8f364 Merge branch '7.0.x' 2026-04-12 16:46:39 +02:00
Sam Brannen a4b33b98df Merge branch '7.0.x' 2026-04-12 14:29:42 +02:00
Sam Brannen 59f9cf8645 Polish SpEL internals 2026-04-12 14:20:50 +02:00
Sébastien Deleuze fb34264169 Merge branch '7.0.x' 2026-04-10 16:10:45 +02:00
Sam Brannen c2cf5e065d Perform case-insensitive lookup in HttpMethod.valueOf()
Prior to this commit, the implementation of HttpMethod.valueOf()
aligned with the semantics of Enum#valueOf() which requires an exact
match for the enum constant name.

However, since HttpMethod is no longer an enum, that restriction is no
longer necessary. Consequently, this commit revises the implementation
of valueOf() to perform a case-insensitive lookup for predefined
constants.

In other words, HttpMethod.valueOf("GET") and HttpMethod.valueOf("get")
now both resolve to HttpMethod.GET.

Closes gh-36518
2026-04-10 15:04:42 +02:00
Sam Brannen e0e78257d6 Merge branch '7.0.x' 2026-04-09 13:07:19 +02:00
Sam Brannen 227ddf817d Merge branch '7.0.x' 2026-04-09 12:57:01 +02:00
Sam Brannen 07aa952bed Merge branch '7.0.x' 2026-04-09 12:16:02 +02:00
Brian Clozel 598f0b64f0 Merge branch '7.0.x' 2026-04-09 11:02:07 +02:00
Sam Brannen d4e3d6be58 Merge branch '7.0.x' 2026-04-09 10:36:15 +02:00
Juergen Hoeller 8fe0eec5bf Merge branch '7.0.x' 2026-04-08 16:11:34 +02:00
Sam Brannen f78264d158 Polish contribution
See gh-36626
2026-04-08 16:08:09 +02:00
Junseo Bae 4b0101a9dc Defensively copy sentDate in SimpleMailMessage
Use defensive Date copies for sentDate to avoid shared mutable state.

Apply consistent handling in setSentDate, getSentDate, the copy constructor, and copyTo.

Add regression tests for mutation safety and copy isolation.

Closes gh-36626

Signed-off-by: Junseo Bae <ferrater1013@gmail.com>
2026-04-08 16:07:33 +02:00
Sam Brannen e940a38014 Merge branch '7.0.x' 2026-04-08 14:19:16 +02:00
Juergen Hoeller 623bdfb677 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-04-08 13:43:25 +02:00
Sam Brannen 8566e7bf55 Favor Class#getTypeName over ClassUtils#getQualifiedName where feasible 2026-04-08 13:27:52 +02:00
Sam Brannen c17f25f939 Fall back to type name in ClassUtils.getCanonicalName()
See gh-36607
2026-04-08 12:39:40 +02:00
Sam Brannen 9da22ecb46 Merge branch '7.0.x' 2026-04-08 12:35:54 +02:00
Sam Brannen f602967dbc Consistently supply List to MergedAnnotations.of() 2026-04-08 11:59:24 +02:00
Yanming Zhou f7d3556b8c Polish DisconnectedClientHelper
Use `CollectionUtils::newLinkedHashSet` instead of `LinkedHashSet::new` to avoid resizing.

Closes gh-36618

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-04-08 11:58:36 +02:00
Sam Brannen 813e113ea9 Merge branch '7.0.x' 2026-04-08 11:40:52 +02:00
Sam Brannen 306a1f6c99 Merge branch '7.0.x' 2026-04-08 11:14:48 +02:00
Sam Brannen 62c6d67615 Align StandardMethodMetadata with ASM/ClassFile support for getReturnTypeName()
We currently have three implementations of MethodMetadata:

- StandardMethodMetadata (Java reflection)
- SimpleMethodMetadata (ASM)
- ClassFileMethodMetadata (ClassFile API)

The ASM and ClassFile variants return a string equivalent to
Class#getTypeName(); whereas, StandardMethodMetadata currently returns a
binary name using Class#getName() (for example, `[I` instead of `int[]`).

In order to align with the ASM and ClassFile variants and provide
consistent results for all MethodMetadata implementations, this commit
revises StandardMethodMetadata.getReturnTypeName() to use
Class#getTypeName().

Closes gh-36619
2026-04-08 10:27:39 +02:00
Sam Brannen c4c0aca69b Polishing 2026-04-08 10:11:23 +02:00
Sam Brannen 6f08c0b473 Merge branch '7.0.x' 2026-04-07 18:17:00 +02:00
daguimu d37d7abb17 Reject unbalanced parentheses in profile expressions
ProfilesParser.parseTokens() silently accepts unbalanced parentheses
in profile expressions such as "dev)" or "(dev", treating them as
valid. This can lead to unexpected behavior where malformed @Profile
annotations are silently interpreted instead of being rejected.

This commit tightens the validation in parseTokens() to reject:
- Unmatched closing parenthesis at the top level
- Unmatched opening parenthesis when tokens are exhausted

Also fixes an existing test that inadvertently relied on this lenient
behavior by using "spring&framework)" instead of "(spring&framework)".

Closes gh-36550

Signed-off-by: daguimu <daguimu.geek@gmail.com>
2026-04-07 15:38:50 +02:00
Sébastien Deleuze 2a1678f246 Merge branch '7.0.x' 2026-04-07 15:11:54 +02:00
Brian Clozel 3806315e31 Merge branch '7.0.x' 2026-04-07 11:53:44 +02:00
Sam Brannen 6062363738 Use canonical names in error messages in annotation processing
Prior to this commit, we invoked `Class.getName()` when building error
messages during annotation processing, resulting in exceptions like the
following which use binary names for nested types and arrays.

  Attribute 'chars' in annotation
  org.springframework.core.annotation.AnnotationUtilsTests$CharsContainer
  should be compatible with [C but a [I value was returned

This commit switches to canonical names in error messages in annotation
processing, resulting in improved such errors messages such as the
following.

  Attribute 'chars' in annotation
  org.springframework.core.annotation.AnnotationUtilsTests.CharsContainer
  should be compatible with char[] but a int[] value was returned

In addition, this commit introduces a new getCanonicalName(Class) method
in ClassUtils, which has effectively been extracted from the following
classes where this functionality was previously duplicated.

- AttributeMethods
- SynthesizedMergedAnnotationInvocationHandler
- TypeDescriptor
- DefaultRetryPolicy
- ReflectiveIndexAccessor

Closes gh-36607
2026-04-06 17:34:54 +02:00
Sam Brannen 31d9fe5f41 Merge branch '7.0.x' 2026-04-06 16:56:26 +02:00
Sébastien Deleuze 2ee4c3a363 Provide bean conditional registration capabilities in BeanRegistrarDsl
Closes gh-36601
2026-04-05 18:49:07 +02:00
Sam Brannen 82b179f238 Merge branch '7.0.x' 2026-04-04 17:21:16 +02:00
Sam Brannen f993e9710d Align with JDK by throwing TypeNotPresentException in MergedAnnotations
Prior to this commit, we used ClassUtils.resolveClassName() in
TypeMappedAnnotation.adapt(...) which throws an IllegalStateException
or IllegalArgumentException if a type referenced by an annotation
attribute cannot be loaded. However, if such an error occurs while
using the JDK's reflection APIs, a TypeNotPresentException is thrown
instead.

In order to align with the standard behavior of the JDK, this commit
modifies TypeMappedAnnotation.adapt(...) to use ClassUtils.forName()
and throw a TypeNotPresentException in such scenarios.

This commit also makes similar changes in
MergedAnnotationReadingVisitor and ClassFileAnnotationDelegate.

Closes gh-36593
2026-04-03 16:44:28 +02:00
Sam Brannen 822001c6a4 Merge branch '7.0.x' 2026-04-03 15:42:28 +02:00
Sam Brannen f2d4d59f5a Merge branch '7.0.x' 2026-04-03 15:39:58 +02:00
Sam Brannen 4709f68446 Merge branch '7.0.x' 2026-04-02 18:40:08 +02:00
Sam Brannen afba74c516 Merge branch '7.0.x' 2026-04-02 17:21:10 +02:00
Sam Brannen fd50c0841c Merge branch '7.0.x' 2026-04-02 16:55:02 +02:00
Brian Clozel cbe8b148b1 Merge branch '7.0.x' 2026-04-02 14:48:56 +02:00
Stéphane Nicoll 2086508924 Polish
See gh-36581
2026-04-02 14:41:26 +02:00
Juergen Hoeller 759b173b1a Merge branch '7.0.x' 2026-04-02 14:31:56 +02:00
Sam Brannen 7590c4c92e Fix Javadoc link
See gh-36581
2026-04-02 13:32:32 +02:00
Sam Brannen 596c0df826 Merge branch '7.0.x' 2026-04-02 12:45:50 +02:00
Stéphane Nicoll d5b6f4a7ee Polish BeanRegistrar Javadoc and add tests for non-invocation semantics
Revise the BeanRegistrar Javadoc to document the two distinct usage
modes: @Configuration/@Import and programmatic GenericApplicationContext
setup.

Clarify that implementations are not Spring components (requiring a
no-arg constructor and no dependency injection), and detail the ordering
guarantees for each mode.

Add missing tests

Signed-off-by: Stéphane Nicoll <stephane.nicoll@broadcom.com>
2026-04-02 11:18:58 +02:00
Sam Brannen 5dc4a3a7a0 Merge branch '7.0.x' 2026-04-02 11:06:39 +02:00
Brian Clozel 21f8b6d2f3 Merge branch '7.0.x' 2026-04-02 10:19:55 +02:00
Juergen Hoeller 6ebaeba1c2 Merge branch '7.0.x' 2026-04-01 21:17:00 +02:00
Juergen Hoeller 3bf31e45dd Replace DeferredBeanRegistrar with implicit ordering semantics
GenericApplicationContext-registered BeanRegistrars are invoked after other programmatic bean definitions. Configuration-imported BeanRegistrars participate in configuration class order and in particular in Boot's auto-configuration ordering.

Closes gh-21497
2026-04-01 20:59:45 +02:00
Brian Clozel 5e16e25109 Merge branch '7.0.x' 2026-04-01 11:06:24 +02:00
Sam Brannen 6b5f0e92b1 Merge branch '7.0.x' 2026-04-01 10:15:08 +02:00
Brian Clozel 2c973d3034 Mention when RestTemplate will be removed
`RestTemplate` is deprecated, this commit amends the JavaDoc to mention
its scheduled removal for the next major version, Spring Framework 8.0.

See gh-36574
2026-03-31 22:03:36 +02:00
Brian Clozel 94e2f49e9f Read multipart requests from RestTestClient
Prior to this commit, the `RestTestClient` MockMvc integration would
support transforming client requests into MockMvc requests.
`RestTestClient` can serialize `MultiValueMap` request bodies as
multipart requests. In this case, the `MockMvcClientHttpRequestFactory`
would only read the body as byte stream and would not turn this into a
proper `MockMultipartHttpServletRequestBuilder`.

This commit uses the new `MultipartHttpMessageConverter` to parse the
request payload as `MockPart` instances to be added to the MockMvc
requests.

Closes gh-35569
2026-03-31 19:13:06 +02:00
Yanming ZhouandSam Brannen 8b62ea13a1 Remove deprecated methodIdentification() method in CacheAspectSupport
The Javadoc said it's used for logging, but it's not used anywhere.

Closes gh-36560

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
Signed-off-by: Sam Brannen <104798+sbrannen@users.noreply.github.com>
Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com>
2026-03-31 17:56:47 +02:00
Sam Brannen 76ad7c9cc0 Merge branch '7.0.x' 2026-03-31 17:50:15 +02:00
rstoyanchev 8586095370 Merge branch '7.0.x' 2026-03-31 16:22:16 +01:00
Sébastien Deleuze 32970fe10d Fix WebClient context propagation in Kotlin Coroutines
Prior to this commit, thread-local variables like Trace ID were not
automatically propagated into the Reactor Context when making requests
using Kotlin coroutine WebClient extensions like `awaitExchange`.

This commit updates `CoroutineContext.toReactorContext()` to capture
thread-local values via Micrometer Context Propagation when available,
ensuring observations and traces are properly reused.

Closes gh-36182
2026-03-31 14:45:55 +02:00
Brian Clozel 83c2afb643 Deprecate RestTemplate and related types
As announced in "the state of HTTP clients in Spring" blog post
(https://spring.io/blog/2025/09/30/the-state-of-http-clients-in-spring),
the deprecation timeline for `RestTemplate` was announced last November
and docs were updated accordingly.

This commit `@Deprecate` `RestTemplate` and related types for removal to
send a stronger signal to our community.
The actual removal is scheduled for Spring Framework 8.0 (not yet
scheduled).

Closes gh-36574
2026-03-31 12:28:44 +02:00
rstoyanchev 7473cd5fbc Merge branch '7.0.x' 2026-03-31 11:00:51 +01:00
Brian Clozel 8de3683ccb Merge branch '7.0.x' 2026-03-31 11:47:41 +02:00
Brian Clozel 208f62ba07 Merge branch '7.0.x' 2026-03-31 11:02:57 +02:00
Sébastien Deleuze a0d51c8f7d Upgrade to Dokka 2.2.0
Closes gh-36570
2026-03-31 09:40:46 +02:00
Sébastien Deleuze f4cefb6ec5 Upgrade to Jackson 3.1 and 2.21
This commit raises the Jackson baseline and upgrades related
dependencies to Jackson 3.1 and 2.21 which are the new LTS.

Closes gh-36130
2026-03-31 09:21:48 +02:00
Sam Brannen e7fdbb8339 Merge branch '7.0.x' 2026-03-30 17:01:56 +02:00
Sam Brannen a64b001f6a Use AtomicBoolean as a "plain" holder in BeanOverrideUtils
Since the AtomicBoolean cannot be accessed by another thread, there is
no need to use compareAndSet().
2026-03-30 13:44:12 +02:00
Sam Brannen c68470d0fd Simplify Bean Override support in the SpringExtension
See gh-36096
2026-03-30 13:22:12 +02:00
Sam Brannen 182e6b744a Polishing 2026-03-30 13:22:12 +02:00
rstoyanchev d46e96f1ca Correct ApiVersionConfigurer method signature
Closes gh-36551
2026-03-30 12:07:06 +01:00
Sam Brannen 51a5489438 Polishing 2026-03-29 17:34:32 +02:00
Sam Brannen f9523a785b Support @⁠MockitoBean and @⁠MockitoSpyBean on test constructor parameters
Prior to this commit, @⁠MockitoBean and @⁠MockitoSpyBean could be
declared on fields or at the type level (on test classes and test
interfaces), but not on constructor parameters. Consequently, a test
class could not use constructor injection for bean overrides.

To address that, this commit introduces support for @⁠MockitoBean and
@⁠MockitoSpyBean on constructor parameters in JUnit Jupiter test
classes. Specifically, the Bean Override infrastructure has been
overhauled to support constructor parameters as declaration sites and
injection points alongside fields, and the SpringExtension now
recognizes composed @⁠BeanOverride annotations on constructor
parameters in supportsParameter() and resolves them properly in
resolveParameter(). Note, however, that this support has not been
introduced for @⁠TestBean.

For example, the following which uses field injection:

   @⁠SpringJUnitConfig(TestConfig.class)
   class BeanOverrideTests {

      @⁠MockitoBean
      CustomService customService;

      // tests...
   }

Can now be rewritten to use constructor injection:

   @⁠SpringJUnitConfig(TestConfig.class)
   class BeanOverrideTests {

      private final CustomService customService;

      BeanOverrideTests(@⁠MockitoBean CustomService customService) {
         this.customService = customService;
      }

      // tests...
   }

With Kotlin this can be achieved even more succinctly via a compact
constructor declaration:

   @⁠SpringJUnitConfig(TestConfig::class)
   class BeanOverrideTests(@⁠MockitoBean val customService: CustomService) {

      // tests...
   }

Of course, if one is a fan of so-called "test records", that can also
be achieved succinctly with a Java record:

   @⁠SpringJUnitConfig(TestConfig.class)
   record BeanOverrideTests(@⁠MockitoBean CustomService customService) {

      // tests...
   }

Closes gh-36096
2026-03-29 17:17:16 +02:00
Juergen Hoeller 955f9d3ea9 Merge branch '7.0.x'
# Conflicts:
#	spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java
#	spring-context/src/main/java/org/springframework/context/annotation/Import.java
2026-03-28 20:41:57 +01:00
Juergen Hoeller 7502b92392 Introduce DeferredBeanRegistrar and BeanRegistry#containsBean methods
Closes gh-21497
2026-03-28 20:27:23 +01:00
Juergen Hoeller 99e7543a7f Merge branch '7.0.x' 2026-03-28 11:13:56 +01:00
Sam Brannen 82a9d66079 Merge branch '7.0.x' 2026-03-27 11:26:11 +01:00
Sam Brannen 5708b73ea9 Introduce ResolvableType.forParameter() factory method
Prior to this commit, one could invoke
ResolvableType.forMethodParameter(MethodParameter.forParameter(parameter))
to create a ResolvableType for a Parameter; however, that's slightly
cumbersome.

To address that, this commit introduces ResolvableType.forParameter(),
analogous to existing convenience factory methods in ResolvableType.

Closes gh-36545
2026-03-26 16:43:50 +01:00
Sam Brannen 5e975f51dd Remove redundant Assert.notNull() checks in ResolvableType
Since equivalent Assert.notNull() checks are already performed by
subsequent code (constructors and factory methods), there is no need
to perform the exact same assertion twice in such use cases.

Closes gh-36544
2026-03-26 16:40:17 +01:00
Sam Brannen ccf0cae18c Polishing 2026-03-26 15:22:04 +01:00
Brian Clozel de0dfe5d93 Fix Javadoc format error
See gh-33263
2026-03-26 15:19:20 +01:00
Brian Clozel aac521c116 Add integration tests and docs for multipart support
This commit adds integration tests and reference documentation
 for multipart support in `RestClient` and `RestTestClient`.

Closes gh-35569
Closes gh-33263
2026-03-26 15:10:01 +01:00
Brian Clozel ab8de8ec4b Support Map payloads in FormHttpMessageConverter
Prior to this commit, the `FormHttpMessageConverter` would only support
`MultiValueMap` payloads for reading and writing. While this can be
useful when web forms contain multiple values under the same key, this
prevent developers from using a common `Map` type and factory methods
like `Map.of(...)`.

This commit relaxes constraints in `FormHttpMessageConverter` and
ensures that `Map` types are supported for reading and writing URL
encoded forms.
Note that when reading forms to a `Map`, only the first value for each
key will be considered and other values will be dropped if they exist.

Closes gh-36408
2026-03-26 15:09:44 +01:00
Brian Clozel abc3cfc7be Move multipart support to dedicated converter
Prior to this commit, gh-36255 introduced the new
`MultipartHttpMessageConverter`, focusing on multipart message
conversion in a separate converter. The `FormHttpMessageConverter` did
conflate URL encoded forms and multipart messages in the same converter.

With the introduction of the new converter and related types in the same
package (with `Part`, `FormFieldPart` and `FilePart`), we can now
revisit this arrangement.

This commit restricts the `FormHttpMessageConverter` to URL encoded
forms only and as a result, changes its implementation to only consider
`MultiValueMap<String, String>` types for reading and writing HTTP
messages. Because type erasure, this converter is now a
`SmartHttpMessageConverter` to get better type information with
`ResolvableType`.

As a result, the `AllEncompassingFormHttpMessageConverter` is formally
deprecated and replaced by the `MultipartHttpMessageConverter`, by
setting part converters explicitly in its constructor.

Closes gh-36256
2026-03-26 15:08:58 +01:00
Brian Clozel 44302aca93 Add MultipartHttpMessageConverter
Prior to this commit, the `FormHttpMessageConverter` would write, but
not read, multipart HTTP messages. Reading multipart messages is
typicaly performed by Servlet containers with Spring's
`MultipartResolver` infrastructure.

This commit introduces a new `MultipartHttpMessageConverter` that copies
the existing feature for writing multipart messages, borrowed from
`FormHttpMessageConverter`. This also introduces a new `MultipartParser`
class that is an imperative port of the reactive variant, but keeping it
based on the `DataBuffer` abstraction. This will allow us to maintain
both side by side more easily.

This change also adds new `Part`, `FilePart` and `FormFieldPart` types
that will be used when converting multipart messages to
`MultiValueMap<String, Part>` maps.

Closes gh-36255
2026-03-26 15:08:22 +01:00
Brian Clozel ac86dc1264 Move multipart test files to a common location
This commit moves "*.multipart" test files to a common location
in order to share these resources with another test suite.

Closes gh-36253
2026-03-26 15:08:01 +01:00
Brian Clozel 051219c3c9 Introduce HttpMessageConverter#canWriteRepeatedly
The `AbstractHttpMessageConverter#supportsRepeatableWrites`
contract is a protected method that message converters can override.
This method tells whether the current converter can write several
times the payload given as a parameter. This is mainly useful on the
client side, where we need to know if we can send the same HTTP
message again, after receiving an HTTP redirect status.

Because this method is protected, this limits our ability to call
it from a different package; this is needed for gh-33263.

This commit promotes this method to the main `HttpMessageConverter`
interface and deprecates the former.

Closes gh-36252
2026-03-26 15:07:48 +01:00
Sam Brannen d8916a326a Merge branch '7.0.x' 2026-03-26 14:29:16 +01:00
Brian Clozel 3be51b1c77 Merge branch '7.0.x' 2026-03-25 21:53:09 +01:00
Sam Brannen a3118f276f Remove @⁠ContextConfiguration from MockitoBeanNestedTests
See gh-31456
2026-03-25 17:46:48 +01:00
Sam Brannen 3d70074089 Introduce support for custom parameter names in ParameterResolutionDelegate
The resolveDependency() utility method in ParameterResolutionDelegate
resolves a dependency using the name of the parameter as a fallback
qualifier. That suffices for most use cases; however, there are times
when a custom parameter name should be used instead.

For example, for our Bean Override support in the Spring TestContext
Framework, an annotation such as @⁠MockitoBean("myBean") specifies an
explicit name that should be used instead of name of the annotated
parameter.

Furthermore, introducing support for custom parameter names will
greatly simplify the logic in SpringExtension that will be required to
implement #36096.

To address those issues, this commit introduces an overloaded variant
of resolveDependency() which accepts a custom parameter name.
Internally, a custom DependencyDescriptor has been implemented to
transparently support this use case.

See gh-36096
Closes gh-36534
2026-03-25 13:15:15 +01:00
Sam Brannen bcc9e27dd0 Polishing 2026-03-25 12:41:05 +01:00
Sam Brannen 0f4ee906de Extract BeanOverrideUtils from BeanOverrideHandler
Prior to this commit, BeanOverrideHandler contained a large amount of
logic solely related to search algorithms for finding handlers.
Consequently, BeanOverrideHandler took on more responsibility than it
ideally should. In addition, we will soon increase the complexity of
those search algorithms, and we will need to make another utility
method public for use outside the bean.override package.

To address those issues, this commit extracts the search utilities from
BeanOverrideHandler into a new BeanOverrideUtils class.

Closes gh-36533
2026-03-25 11:58:49 +01:00
Juergen Hoeller 5f9dd9e135 Merge branch '7.0.x' 2026-03-24 23:50:32 +01:00
Juergen Hoeller 5335dbf802 Merge branch '7.0.x' 2026-03-24 18:08:10 +01:00
Brian Clozel 4c1b4f33a8 Skip Jaxb auto-detection in HttpMessageConverters for servers
Prior to this commit, `HttpMessageConverters` would consider the JAXB
message converters when building `HttpMessageConverters` instances.
We noticed that, on the server side, the Jakarta JAXB dependency is very
common on the classpath and often brought transitively. At runtime, this
converter can use significant CPU resources when checking the
`canRead`/`canWrite` methods. This can happen when content types aren't
strictly called out on controller endpoints.

This commit changes the auto-detection mechanism in
`HttpMessageConverters` to not consider the JAXB message converter for
server use cases.
For client use cases, we keep considering this converter as the runtime
cost there is lower.

Closes gh-36302
2026-03-24 17:23:49 +01:00
Brian Clozel f1a60f1664 Merge branch '7.0.x' 2026-03-24 13:19:49 +01:00
Sam Brannen d66e6571c1 Merge branch '7.0.x' 2026-03-24 11:07:55 +01:00
Brian Clozel dc8b9fff57 Merge branch '7.0.x' 2026-03-24 10:26:04 +01:00
Yanming Zhou 0eba6f0da3 Add typesafe method to get generic bean by name with type reference
Fix GH-34687

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-03-24 08:02:58 +01:00
Juergen Hoeller a5579614d9 Merge branch '7.0.x'
# Conflicts:
#	framework-platform/framework-platform.gradle
2026-03-23 20:14:26 +01:00
Juergen Hoeller eca6d91532 Refine nullable declaration of internal type array constant 2026-03-23 17:11:32 +01:00
Juergen Hoeller 354c315f55 Upgrade to Hibernate ORM 7.3
Closes gh-36519
2026-03-23 17:11:21 +01:00
Brian Clozel a302ad88f3 Merge branch '7.0.x' 2026-03-23 14:35:05 +01:00
Sam Brannen 447e1ee0cd Merge branch '7.0.x' 2026-03-23 11:50:36 +01:00
Sam Brannen bef2f4488f Merge branch '7.0.x' 2026-03-23 11:36:48 +01:00
Sam Brannen d5adfd7980 Merge branch '7.0.x' 2026-03-22 18:04:53 +01:00
Sam Brannen 3777a9fcbd Merge branch '7.0.x' 2026-03-22 17:33:01 +01:00
Sam Brannen 7c834224a2 Merge branch '7.0.x' 2026-03-22 16:56:20 +01:00
Paul Ngo (Do Not Bug) c1e90d51ca GenericTypeResolver.resolveType should resolve TypeVariable with nested ParameterizedType (#36480)
Signed-off-by: anaconda875 <hflbtmax@gmail.com>
2026-03-21 13:28:12 +01:00
Juergen Hoeller a0dba60fa6 Merge branch '7.0.x' 2026-03-21 12:45:25 +01:00
Brian Clozel 63e01cf6b3 Merge branch '7.0.x' 2026-03-20 15:56:10 +01:00
Sam Brannen 5d45036c09 Merge branch '7.0.x' 2026-03-20 11:13:01 +01:00
Sam Brannen b31f78de50 Merge branch '7.0.x' 2026-03-19 14:16:58 +01:00
Sam Brannen 967fd099f9 Merge branch '7.0.x' 2026-03-19 14:02:36 +01:00
rstoyanchev e655edafec Merge branch '7.0.x' 2026-03-19 09:12:49 +00:00
Sam Brannen 6ec2455e24 Merge branch '7.0.x' 2026-03-18 18:39:17 +01:00
Sam Brannen 003d8b2f80 Merge branch '7.0.x' 2026-03-18 18:19:31 +01:00
Sam Brannen b846a29b17 Polishing 2026-03-18 17:42:18 +01:00
박준형 8b994be381 Remove unnecessary space in contributing guide title
Closes gh-36491

Signed-off-by: junhyung8795 <junhyung8795@naver.com>
2026-03-18 15:43:45 +01:00
Brian Clozel b246fc881b Merge branch '7.0.x' 2026-03-18 12:19:22 +01:00
rstoyanchev 5785923c0e Lower log level of cache miss in HandlerMappingIntrospector
See gh-36309
2026-03-17 19:00:34 +00:00
rstoyanchev 66607cb145 Add PreFlightRequestFilter
Closes gh-36482
2026-03-17 18:54:29 +00:00
Brian Clozel 4637da1f36 Merge branch '7.0.x' 2026-03-17 18:12:40 +01:00
Brian Clozel 22e4d84993 Add support for "application/jsonl" JSON lines
Prior to this commit, Spring web frameworks were using the
"application/x-ndjson" media type for streaming JSON payloads delimited
with newlines.

The "application/jsonl" media type seems to gain popularity in the
broader ecosystem and could supersede NDJSON in the future. This commit
adds support for JSON Lines as an alternative.

Closes gh-36485
2026-03-17 12:18:23 +01:00
Sébastien Deleuze e2b9b19970 Upgrade to Kotlin 2.3.20
Closes gh-36484
2026-03-17 11:59:45 +01:00
Brian Clozel 75f964657f Merge branch '7.0.x' 2026-03-17 09:20:16 +01:00
Sam Brannen 23b73168a0 Merge branch '7.0.x' 2026-03-16 13:39:30 +01:00
Juergen Hoeller 391dd90e84 Support "classpath*:" prefix for resource bundle basename
Closes gh-36292
See gh-36415
2026-03-16 12:23:35 +01:00
Juergen Hoeller 1345760087 Support "classpath*:" prefix for ResourceLoader#getResource
Introduces a general-purpose consumeContent method on Resource and EncodedResource with special behavior for multi-content resources. Regular getInputStream/getReader calls will expose the merged content of all same-named resources in the classpath.

Closes gh-36415
2026-03-16 12:23:16 +01:00
Brian Clozel 51fccf226f Merge branch '7.0.x' 2026-03-16 11:16:48 +01:00
Sam Brannen a14a20b61b Merge branch '7.0.x' 2026-03-15 17:06:18 +01:00
Sam Brannen fadbd0fa31 Partially revert forward merge of "Branch for 7.0.x maintenance" 2026-03-15 14:45:09 +01:00
Sam Brannen 40ef084e7f Merge branch '7.0.x' 2026-03-15 14:40:47 +01:00
Sam Brannen a07033f3fb Resolve all default context configuration within test class hierarchies
Prior to this commit, if a superclass or enclosing test class (such as
one annotated with @⁠SpringBootTest or simply
@⁠ExtendWith(SpringExtension.class)) was not annotated with
@⁠ContextConfiguration (or @⁠Import with @⁠SpringBootTest), the
ApplicationContext loaded for a subclass or @⁠Nested test class would
not use any default context configuration for the superclass or
enclosing test class.

Effectively, a default XML configuration file or static nested
@⁠Configuration class for the superclass or enclosing test class was
not discovered by the AbstractTestContextBootstrapper when attempting
to build the MergedContextConfiguration (application context cache key).

To address that, this commit introduces a new
resolveDefaultContextConfigurationAttributes() method in
ContextLoaderUtils which is responsible for creating instances of
ContextConfigurationAttributes for all superclasses and enclosing
classes. This effectively enables AbstractTestContextBootstrapper to
delegate to the resolved SmartContextLoader to properly detect a
default XML configuration file or static nested @⁠Configuration class
even if such classes are not annotated with @⁠ContextConfiguration.

Closes gh-31456
2026-03-13 15:39:05 +01:00
Sam Brannen 293a9c0ee3 Allow local @⁠BootstrapWith annotation to override a meta-annotation
This commit revises the resolveExplicitTestContextBootstrapper()
algorithm in BootstrapUtils to allow a local @⁠BootstrapWith annotation
to override a meta-annotation within the same composed annotation.

Closes gh-35938
2026-03-13 15:24:38 +01:00
Sam Brannen 32d1b83f62 Override Servlet 6.1's doPatch() method in FrameworkServlet
See gh-12640
See gh-14975
See gh-36258
Closes gh-36247
2026-03-13 15:17:04 +01:00
Brian Clozel 227bc196b1 Consider 7.0.x branch for Antora UI upgrades 2026-03-13 15:02:25 +01:00
Brian Clozel 2fcef050e0 Build 7.1.0-SNAPSHOT 2026-03-13 14:50:12 +01:00
610 changed files with 19457 additions and 5693 deletions
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
on:
push:
branches:
- 7.0.x
- 'main'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
@@ -31,7 +31,7 @@ jobs:
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
/**/framework-api-*-docs.zip::zip.type=docs
/**/framework-api-*-schema.zip::zip.type=schema
build-name: ${{ vars.COMMERCIAL && format('spring-framework-commercial-{0}', '7.0.x') || format('spring-framework-{0}', '7.0.x') }}
build-name: ${{ vars.COMMERCIAL && format('spring-framework-commercial-{0}', '7.1.x') || format('spring-framework-{0}', '7.1.x') }}
folder: 'deployment-repository'
project: ${{ vars.COMMERCIAL && 'spring' }}
repository: ${{ vars.COMMERCIAL && 'spring-enterprise-maven-dev-local' || 'libs-snapshot-local' }}
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
toolchain: true
- version: 25
toolchain: false
- version: 26
- version: 27
toolchain: true
exclude:
- os:
+2 -2
View File
@@ -2,8 +2,8 @@ name: Release Milestone
on:
push:
tags:
- v7.0.0-M[1-9]
- v7.0.0-RC[1-9]
- v7.1.0-M[1-9]
- v7.1.0-RC[1-9]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
+1 -1
View File
@@ -2,7 +2,7 @@ name: Release
on:
push:
tags:
- v7.0.[0-9]+
- v7.1.[0-9]+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
+1 -1
View File
@@ -1,4 +1,4 @@
# Contributing to the Spring Framework
# Contributing to the Spring Framework
First off, thank you for taking the time to contribute! :+1: :tada:
+3 -3
View File
@@ -3,7 +3,7 @@ plugins {
// kotlinVersion is managed in gradle.properties
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
id 'org.jetbrains.dokka'
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
id 'com.github.bjornvester.xjc' version '1.9.1' apply false
id 'com.gradleup.shadow' version "9.2.2" apply false
id 'me.champeau.jmh' version '0.7.2' apply false
id 'io.spring.nullability' version '0.0.15' apply false
@@ -64,13 +64,13 @@ configure([rootProject] + javaProjects) { project ->
ext.javadocLinks = [
"https://docs.oracle.com/en/java/javase/17/docs/api/",
//"https://jakarta.ee/specifications/platform/11/apidocs/",
"https://docs.hibernate.org/orm/7.2/javadocs/",
"https://docs.hibernate.org/orm/7.4/javadocs/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://hc.apache.org/httpcomponents-client-5.6.x/5.6/httpclient5/apidocs/",
"https://projectreactor.io/docs/core/release/api/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
"https://docs.junit.org/6.0.3/api/",
"https://docs.junit.org/6.1.2/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
"https://jspecify.dev/docs/api/"
+1 -1
View File
@@ -20,7 +20,7 @@ ext {
dependencies {
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.1.0"
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.2.0"
implementation "com.tngtech.archunit:archunit:1.4.1"
implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
@@ -37,7 +37,8 @@ public class KotlinConventions {
if (project.getLayout().getProjectDirectory().dir("src/main/kotlin").getAsFile().exists()) {
project.getPlugins().apply(DokkaPlugin.class);
project.getExtensions().configure(DokkaExtension.class, dokka -> configure(project, dokka));
project.project(":framework-api").getDependencies().add("dokka", project);
project.project(":framework-api").getDependencies()
.add("dokka", project.getDependencyFactory().createProjectDependency());
}
});
}
@@ -54,7 +55,7 @@ public class KotlinConventions {
"-Xjsr305=strict", // For dependencies using JSR 305
"-opt-in=kotlin.RequiresOptIn",
"-Xjdk-release=17", // Needed due to https://youtrack.jetbrains.com/issue/KT-49746
"-Xannotation-default-target=param-property" // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255
"-Xannotation-default-target=param-property" // Preferred behavior, default with Kotlin language version set to 2.4+, see https://youtrack.jetbrains.com/issue/KT-73255
);
});
}
@@ -18,7 +18,6 @@ package org.springframework.build.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.EvaluationResult;
import java.io.File;
import java.io.IOException;
@@ -44,12 +43,6 @@ import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import static org.springframework.build.architecture.ArchitectureRules.allPackagesShouldBeFreeOfTangles;
import static org.springframework.build.architecture.ArchitectureRules.classesShouldNotImportForbiddenTypes;
import static org.springframework.build.architecture.ArchitectureRules.javaClassesShouldNotImportKotlinAnnotations;
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToLowerCaseWithoutLocale;
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToUpperCaseWithoutLocale;
/**
* {@link Task} that checks for architecture problems.
*
@@ -63,12 +56,11 @@ public abstract class ArchitectureCheck extends DefaultTask {
public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getProhibitObjectsRequireNonNull().convention(true);
getRules().addAll(classesShouldNotImportForbiddenTypes(),
javaClassesShouldNotImportKotlinAnnotations(),
allPackagesShouldBeFreeOfTangles(),
noClassesShouldCallStringToLowerCaseWithoutLocale(),
noClassesShouldCallStringToUpperCaseWithoutLocale());
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
getRules().addAll(ArchitectureRules.CLASSES_SHOULD_NOT_IMPORT_FORBIDDEN_TYPES,
ArchitectureRules.JAVA_CLASSES_SHOULD_NOT_IMPORT_KOTLIN_ANNOTATIONS,
ArchitectureRules.ALL_PACKAGES_SHOULD_BE_FREE_OF_TANGLES,
ArchitectureRules.NO_CLASSES_SHOULD_CALL_STRING_TO_LOWER_CASE_WITHOUT_LOCALE,
ArchitectureRules.NO_CLASSES_SHOULD_CALL_STRING_TO_UPPER_CASE_WITHOUT_LOCALE);
}
@TaskAction
@@ -77,6 +69,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
.importPaths(this.classes.getFiles().stream().map(File::toPath).toList());
List<EvaluationResult> violations = getRules().get()
.stream()
.map(ArchitectureRules::archRule)
.map((rule) -> rule.evaluate(javaClasses))
.filter(EvaluationResult::hasViolation)
.toList();
@@ -122,14 +115,9 @@ public abstract class ArchitectureCheck extends DefaultTask {
@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();
@Internal
public abstract ListProperty<ArchRule> getRules();
@Input
public abstract ListProperty<ArchitectureRules> getRules();
@Internal
public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
@Input
// The rules themselves can't be an input as they aren't serializable so we use
// their descriptions instead
abstract ListProperty<String> getRuleDescriptions();
}
@@ -25,7 +25,24 @@ import com.tngtech.archunit.library.dependencies.SliceIdentifier;
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
import java.util.List;
abstract class ArchitectureRules {
public enum ArchitectureRules {
ALL_PACKAGES_SHOULD_BE_FREE_OF_TANGLES(allPackagesShouldBeFreeOfTangles()),
NO_CLASSES_SHOULD_CALL_STRING_TO_LOWER_CASE_WITHOUT_LOCALE(noClassesShouldCallStringToLowerCaseWithoutLocale()),
NO_CLASSES_SHOULD_CALL_STRING_TO_UPPER_CASE_WITHOUT_LOCALE(noClassesShouldCallStringToUpperCaseWithoutLocale()),
CLASSES_SHOULD_NOT_IMPORT_FORBIDDEN_TYPES(classesShouldNotImportForbiddenTypes()),
JAVA_CLASSES_SHOULD_NOT_IMPORT_KOTLIN_ANNOTATIONS(javaClassesShouldNotImportKotlinAnnotations())
;
private final ArchRule archRule;
public ArchRule archRule() {
return this.archRule;
}
private ArchitectureRules(ArchRule archRule) {
this.archRule = archRule;
}
static ArchRule allPackagesShouldBeFreeOfTangles() {
return SlicesRuleDefinition.slices()
@@ -61,10 +78,10 @@ abstract class ArchitectureRules {
static ArchRule javaClassesShouldNotImportKotlinAnnotations() {
return ArchRuleDefinition.noClasses()
.that(new DescribedPredicate<JavaClass>("is not a Kotlin class") {
@Override
public boolean test(JavaClass javaClass) {
return javaClass.getSourceCodeLocation()
.getSourceFileName().endsWith(".java");
@Override
public boolean test(JavaClass javaClass) {
return javaClass.getSourceCodeLocation()
.getSourceFileName().endsWith(".java");
}
}
)
@@ -65,7 +65,8 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension));
});
project.getTasks().named("check", task -> task.dependsOn(agentTest));
project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test"));
project.getDependencies().add(CONFIGURATION_NAME,
project.getDependencyFactory().createProjectDependency(":spring-core-test"));
});
}
+1 -1
View File
@@ -31,7 +31,7 @@ asciidoc:
spring-org: 'spring-projects'
spring-github-org: "https://github.com/{spring-org}"
spring-framework-github: "https://github.com/{spring-org}/spring-framework"
spring-framework-code: '{spring-framework-github}/tree/7.0.x'
spring-framework-code: '{spring-framework-github}/tree/main'
spring-framework-issues: '{spring-framework-github}/issues'
spring-framework-wiki: '{spring-framework-github}/wiki'
# Docs
@@ -560,6 +560,14 @@ Kotlin::
----
======
When collection auto-growing is enabled, a collection cannot automatically grow beyond
256 elements by default; however, the `maximumAutoGrowSize` value is configurable. This
default is aligned with `DataBinder.DEFAULT_AUTO_GROW_COLLECTION_LIMIT`, for consistency
with the auto-grow limit used for data binding in Spring MVC and Spring WebFlux. If you
create a `SpelExpressionParser` programmatically, you can specify a custom
`maximumAutoGrowSize` when creating the `SpelParserConfiguration` that you provide to the
`SpelExpressionParser`.
By default, a SpEL expression cannot contain more than 10,000 characters; however, the
`maxExpressionLength` is configurable. If you create a `SpelExpressionParser`
programmatically, you can specify a custom `maxExpressionLength` via
@@ -598,6 +606,32 @@ not able to configure an explicit value for `maximumBigPowerBits` via
`spring.expression.maxBigPowerBits` to the maximum result size in bits (see
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
Likewise, the structural nesting depth of a SpEL expression -- for example, the depth of
nested inline lists or maps, parenthesized expressions, ternary or Elvis expressions, or
chained unary operators -- cannot exceed 1,000 by default; however, the
`maximumNestingDepth` value is configurable. If you create a `SpelExpressionParser`
programmatically, you can specify a custom `maximumNestingDepth` value via
`SpelParserConfiguration.builder().maximumNestingDepth(...)` when creating the
`SpelParserConfiguration` that you provide to the `SpelExpressionParser`. Unlike
`maxExpressionLength` and `maxOperations`, there is currently no JVM system property or
Spring property available for configuring `maximumNestingDepth` globally.
[NOTE]
====
Without such a limit, a sufficiently deeply nested expression can drive SpEL's
recursive-descent parser to exhaust the current thread's call stack, resulting in a
`StackOverflowError` instead of a descriptive exception.
The `maximumNestingDepth` limit improves diagnostics for that common case by converting
it into a clear `SpelParseException`; however, it is not a guaranteed defense against
`StackOverflowError` under every possible JVM thread stack size configuration, since the
amount of stack space consumed per level of nesting depends on the JVM, its JIT
compilation state, and the platform. Applications and frameworks that evaluate SpEL
expressions from an untrusted source should not rely on `maximumNestingDepth` alone and
should instead heed the <<expressions-evaluation-context-security,security
considerations>> discussed previously in this chapter.
====
[[expressions-spel-compilation]]
== SpEL Compilation
@@ -737,7 +771,6 @@ following kinds of expressions cannot be compiled.
* Expressions relying on the conversion service
* Expressions using custom resolvers
* Expressions using overloaded operators
* Expressions using `Optional` with the null-safe or Elvis operator
* Expressions using array construction syntax
* Expressions using selection or projection
* Expressions using bean references
@@ -61,7 +61,8 @@ corresponding implementation (`BeanWrapperImpl`). As quoted from the javadoc, th
`BeanWrapper` offers functionality to set and get property values (individually or in
bulk), get property descriptors, and query properties to determine if they are
readable or writable. Also, the `BeanWrapper` offers support for nested properties,
enabling the setting of properties on sub-properties to an unlimited depth. The
enabling the setting of properties on sub-properties up to a
<<data-binding-nested-path-depth,configurable maximum nesting depth>>. The
`BeanWrapper` also supports the ability to add standard JavaBeans `PropertyChangeListeners`
and `VetoableChangeListeners`, without the need for supporting code in the target class.
Last but not least, the `BeanWrapper` provides support for setting indexed properties.
@@ -236,6 +237,48 @@ Kotlin::
======
[[data-binding-nested-path-depth]]
=== Maximum Nesting Depth for Nested Property Paths
A nested property path such as `managingDirector.salary` is resolved recursively, one
level per nested property. The nesting depth of a property path therefore corresponds to
the number of intermediate properties that must be traversed in order to reach the final
property -- for example, `address.country.name` has a nesting depth of 2, since the
`address` and `country` properties must be traversed in order to reach the `name`
property.
The nesting depth of a property path cannot exceed 100 by default; however, the
`maxNestedPathDepth` value is configurable. You can specify a custom value via
`setMaxNestedPathDepth(...)` on a `ConfigurablePropertyAccessor` such as
`BeanWrapperImpl`, or on a `DataBinder` -- and therefore also on a `WebDataBinder`, for
example within an `@InitBinder` method. If a property path exceeds the configured limit,
an `InvalidPropertyException` is thrown. Specify `0` to disable support for nested
property paths altogether, while continuing to allow simple, indexed, and mapped property
access -- for example, `name`, `accounts[2]`, or `accounts[KEY]`.
Note that this limit applies to property binding as well as to
<<data-binding-constructor-binding,constructor binding>> via `DataBinder.construct`,
since a constructor parameter which is itself an object is constructed recursively
through a nested property path.
[NOTE]
====
Without such a limit, a sufficiently deeply nested property path can drive the recursive
resolution of nested property paths to exhaust the current thread's call stack, resulting
in a `StackOverflowError` instead of a descriptive exception.
The `maxNestedPathDepth` limit improves diagnostics for that common case by converting
it into a clear `InvalidPropertyException`; however, it is not a guaranteed defense against
`StackOverflowError` under every possible JVM thread stack size configuration, since the
amount of stack space consumed per level of nesting depends on the JVM, its JIT
compilation state, and the platform.
When binding untrusted input, you should additionally constrain binding to the expected
input as described in
xref:web/webmvc/mvc-data-binding.adoc#mvc-data-binding-design[Model Design].
====
[[data-binding-conversion]]
== ``PropertyEditor``s
@@ -3,9 +3,8 @@
The `org.springframework.jdbc.datasource.embedded` package provides support for embedded
Java database engines. Support for https://www.hsqldb.org[HSQL],
https://www.h2database.com[H2], and https://db.apache.org/derby[Derby] is provided
natively. You can also use an extensible API to plug in new embedded database types and
`DataSource` implementations.
and https://www.h2database.com[H2] is provided natively.
You can also use an extensible API to plug in new embedded database types and `DataSource` implementations.
[[jdbc-why-embedded-database]]
@@ -41,7 +40,6 @@ supports. It includes the following topics:
* <<jdbc-embedded-database-using-HSQL,Using HSQL>>
* <<jdbc-embedded-database-using-H2,Using H2>>
* <<jdbc-embedded-database-using-Derby,Using Derby>>
[[jdbc-embedded-database-using-HSQL]]
=== Using HSQL
@@ -58,13 +56,6 @@ Spring supports the H2 database. To enable H2, set the `type` attribute of the
`embedded-database` tag to `H2`. If you use the builder API, call the
`setType(EmbeddedDatabaseType)` method with `EmbeddedDatabaseType.H2`.
[[jdbc-embedded-database-using-Derby]]
=== Using Derby
Spring supports Apache Derby 10.5 and above. To enable Derby, set the `type`
attribute of the `embedded-database` tag to `DERBY`. If you use the builder API,
call the `setType(EmbeddedDatabaseType)` method with `EmbeddedDatabaseType.DERBY`.
[[jdbc-embedded-database-types-custom]]
== Customizing the Embedded Database Type
@@ -17,7 +17,7 @@ xref:data-access/jdbc/simple.adoc[Simplifying JDBC Operations with the `SimpleJd
for easy `DataSource` access and various simple `DataSource` implementations that you can
use for testing and running unmodified JDBC code outside of a Jakarta EE container. A subpackage
named `org.springframework.jdbc.datasource.embedded` provides support for creating
embedded databases by using Java database engines, such as HSQL, H2, and Derby. See
embedded databases by using Java database engines, such as HSQL and H2. See
xref:data-access/jdbc/connections.adoc[Controlling Database Connections] and
xref:data-access/jdbc/embedded-database-support.adoc[Embedded Database Support].
@@ -483,7 +483,7 @@ as input. See the <<jdbc-params,next section>> for details on how to define an `
NOTE: Explicit declarations are necessary if the database you use is not a Spring-supported
database. Currently, Spring supports metadata lookup of stored procedure calls for the
following databases: Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle, and Sybase.
following databases: DB2, MySQL, Microsoft SQL Server, Oracle, and Sybase.
We also support metadata lookup of stored functions for MySQL, Microsoft SQL Server,
and Oracle.
@@ -32,7 +32,7 @@ example writes notifications to the console:
}
public boolean isNotificationEnabled(Notification notification) {
return AttributeChangeNotification.class.isAssignableFrom(notification.getClass());
return (notification instanceof AttributeChangeNotification);
}
}
@@ -15,6 +15,7 @@ The Spring Framework provides the following choices for making calls to REST end
`RestClient` is a synchronous HTTP client that provides a fluent API to perform requests.
It serves as an abstraction over HTTP libraries, and handles conversion of HTTP request and response content to and from higher level Java objects.
[[rest-restclient.create]]
=== Create a `RestClient`
`RestClient` has static `create` shortcut methods.
@@ -32,48 +33,7 @@ Once created, a `RestClient` is safe to use in multiple threads.
The below shows how to create or build a `RestClient`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim"]
----
RestClient defaultClient = RestClient.create();
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.defaultVersion("1.2")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim"]
----
val defaultClient = RestClient.create()
val customClient = RestClient.builder()
.requestFactory(HttpComponentsClientHttpRequestFactory())
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
.baseUrl("https://example.com")
.defaultUriVariables(mapOf("variable" to "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.defaultVersion("1.2")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build()
----
======
include-code::./RestClientCreation[tag=snippet,indent=0]
=== Use the `RestClient`
@@ -390,47 +350,42 @@ xref:web/webmvc/message-converters.adoc#message-converters[See the supported HTT
To serialize only a subset of the object properties, you can specify a {baeldung-blog}/jackson-json-view-annotation[Jackson JSON View], as the following example shows:
[source,java,indent=0,subs="verbatim"]
----
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
.contentType(APPLICATION_JSON)
.body(value)
.retrieve()
.toBodilessEntity();
----
include-code::./../restmessageconversion/RestClientMessageConversion[tag=jsonview,indent=0]
==== URL encoded Forms
URL encoded forms, using the `"application/x-www-form-urlencoded"` media type, are useful for sending String key/values over the wire.
This is supported by the `FormHttpMessageConverter`, if the application uses a `MultiValueMap<String, String>` as source instance
or a target type.
For example:
include-code::./../restmessageconversion/RestClientMessageConversion[tag=urlencodedform,indent=0]
==== Multipart
To send multipart data, you need to provide a `MultiValueMap<String, Object>` whose values may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for part content with headers.
For example:
[source,java,indent=0,subs="verbatim"]
----
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
// send using RestClient.post or RestTemplate.postForEntity
----
include-code::./../restmessageconversion/RestClientMessageConversion[tag=multipartrequest,indent=0]
In most cases, you do not have to specify the `Content-Type` for each part.
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension.
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
The `Content-Type` is set to `multipart/form-data` by the `MultipartHttpMessageConverter`.
As seen in the previous section, `MultiValueMap` types can also be used for URL encoded forms.
It is preferable to explicitly set the media type in the `Content-Type` or `Accept` HTTP request headers to ensure that the expected
message converter is used.
`RestClient` can also receive multipart responses.
To decode a multipart response body, use a `ParameterizedTypeReference<MultiValueMap<String, Part>>`.
The decoded map contains `Part` instances where `FormFieldPart` represents form field values
and `FilePart` represents file parts with a `filename()` and a `transferTo()` method.
include-code::./../restmessageconversion/RestClientMessageConversion[tag=multipartresponse,indent=0]
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`.
If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`.
If necessary the `Content-Type` may also be set explicitly.
[[rest-request-factories]]
=== Client Request Factories
@@ -12,18 +12,20 @@ The annotations can be applied in the following ways.
* On a non-static field in a test class or any of its superclasses.
* On a non-static field in an enclosing class for a `@Nested` test class or in any class
in the type hierarchy or enclosing class hierarchy above the `@Nested` test class.
* On a parameter in the constructor for a test class.
* At the type level on a test class or any superclass or implemented interface in the
type hierarchy above the test class.
* At the type level on an enclosing class for a `@Nested` test class or on any class or
interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test
class.
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy
is inferred from the type of the annotated field. If multiple candidates exist in the
`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help
disambiguate. In the absence of a `@Qualifier` annotation, the name of the annotated
field will be used as a _fallback qualifier_. Alternatively, you can explicitly specify a
bean name to mock or spy by setting the `value` or `name` attribute in the annotation.
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field or constructor parameter,
the bean to mock or spy is inferred from the type of the annotated field or parameter. If
multiple candidates exist in the `ApplicationContext`, a `@Qualifier` annotation can be
declared on the field or parameter to help disambiguate. In the absence of a `@Qualifier`
annotation, the name of the annotated field or parameter will be used as a _fallback
qualifier_. Alternatively, you can explicitly specify a bean name to mock or spy by
setting the `value` or `name` attribute in the annotation.
When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean
(or beans) to mock or spy must be supplied via the `types` attribute in the annotation
@@ -211,6 +213,82 @@ Kotlin::
<1> Replace the bean named `service` with a Mockito mock.
======
The following example shows how to use `@MockitoBean` on a constructor parameter for a
by-type lookup.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
private final CustomService customService;
BeanOverrideTests(@MockitoBean CustomService customService) { // <1>
this.customService = customService;
}
// tests...
}
----
<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into
the constructor.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests(@MockitoBean val customService: CustomService) { // <1>
// tests...
}
----
<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into
the constructor.
======
The following example shows how to use `@MockitoBean` on a constructor parameter for a
by-name lookup.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
private final CustomService customService;
BeanOverrideTests(@MockitoBean("service") CustomService customService) { // <1>
this.customService = customService;
}
// tests...
}
----
<1> Replace the bean named `service` with a Mockito mock and inject it into the
constructor.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests(@MockitoBean("service") val customService: CustomService) { // <1>
// tests...
}
----
<1> Replace the bean named `service` with a Mockito mock and inject it into the
constructor.
======
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
[tabs]
@@ -385,6 +463,80 @@ Kotlin::
<1> Wrap the bean named `service` with a Mockito spy.
======
The following example shows how to use `@MockitoSpyBean` on a constructor parameter for
a by-type lookup.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
private final CustomService customService;
BeanOverrideTests(@MockitoSpyBean CustomService customService) { // <1>
this.customService = customService;
}
// tests...
}
----
<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the
constructor.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests(@MockitoSpyBean val customService: CustomService) { // <1>
// tests...
}
----
<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the
constructor.
======
The following example shows how to use `@MockitoSpyBean` on a constructor parameter for
a by-name lookup.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
private final CustomService customService;
BeanOverrideTests(@MockitoSpyBean("service") CustomService customService) { // <1>
this.customService = customService;
}
// tests...
}
----
<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig::class)
class BeanOverrideTests(@MockitoSpyBean("service") val customService: CustomService) { // <1>
// tests...
}
----
<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor.
======
The following `@SharedSpies` annotation registers two spies by-type and one spy by-name.
[tabs]
@@ -142,6 +142,9 @@ provides two alternative ways to verify the response:
1. <<resttestclient.workflow,Built-in Assertions>> extend the request workflow with a chain of expectations
2. <<resttestclient.assertj,AssertJ Integration>> to verify the response via `assertThat()` statements
TIP: See the xref:integration/rest-clients.adoc#rest-message-conversion[HTTP Message Conversion]
section for examples on how to prepare a request with any content, including form data and multipart data.
[[resttestclient.workflow]]
@@ -213,6 +216,16 @@ To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
include-code::./JsonTests[tag=jsonPath,indent=0]
[[resttestclient.multipart]]
==== Multipart Content
When testing endpoints that return multipart responses, you can decode the body to a
`MultiValueMap<String, Part>` and assert individual parts using the `FormFieldPart`
and `FilePart` subtypes.
include-code::./MultipartTests[tag=multipart,indent=0]
[[resttestclient.assertj]]
=== AssertJ Integration
@@ -2,8 +2,9 @@
= Bean Overriding in Tests
Bean overriding in tests refers to the ability to override specific beans in the
`ApplicationContext` for a test class, by annotating the test class or one or more
non-static fields in the test class.
`ApplicationContext` for a test class, by annotating the test class, one or more
non-static fields in the test class, or one or more parameters in the constructor for the
test class.
NOTE: This feature is intended as a less risky alternative to the practice of registering
a bean via `@Bean` with the `DefaultListableBeanFactory`
@@ -42,9 +43,9 @@ The `spring-test` module registers implementations of the latter two
{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`
properties file].
The bean overriding infrastructure searches for annotations on test classes as well as
annotations on non-static fields in test classes that are meta-annotated with
`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
The bean overriding infrastructure searches for annotations on test classes, non-static
fields in test classes, and parameters in test class constructors that are meta-annotated
with `@BeanOverride`, and instantiates the corresponding `BeanOverrideProcessor` which is
responsible for creating an appropriate `BeanOverrideHandler`.
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
@@ -179,6 +179,10 @@ If a specific parameter in a constructor for a JUnit Jupiter test class is of ty
`ApplicationContext` (or a sub-type thereof) or is annotated or meta-annotated with
`@Autowired`, `@Qualifier`, or `@Value`, Spring injects the value for that specific
parameter with the corresponding bean or value from the test's `ApplicationContext`.
Similarly, if a specific parameter is annotated with `@MockitoBean` or `@MockitoSpyBean`,
Spring will inject a Mockito mock or spy, respectively &mdash; see
xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[`@MockitoBean` and `@MockitoSpyBean`]
for details.
Spring can also be configured to autowire all arguments for a test class constructor if
the constructor is considered to be _autowirable_. A constructor is considered to be
@@ -580,8 +580,8 @@ Kotlin::
[[webtestclient-stream]]
==== Streaming Responses
To test potentially infinite streams such as `"text/event-stream"` or
`"application/x-ndjson"`, start by verifying the response status and headers, and then
To test potentially infinite streams such as `"text/event-stream"`,
`"application/jsonl"` or `"application/x-ndjson"`, start by verifying the response status and headers, and then
obtain a `FluxExchangeResult`:
[tabs]
@@ -65,64 +65,4 @@ The exception contains a list of ``ParameterValidationResult``s that group valid
by method parameter. You can either iterate over those, or provide a visitor with callback
methods by controller method parameter type:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
HandlerMethodValidationException ex = ... ;
ex.visitResults(new HandlerMethodValidationException.Visitor() {
@Override
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
// ...
}
@Override
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
// ...
}
@Override
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
// ...
@Override
public void other(ParameterValidationResult result) {
// ...
}
});
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// HandlerMethodValidationException
val ex
ex.visitResults(object : HandlerMethodValidationException.Visitor {
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
// ...
}
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
// ...
}
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
// ...
}
// ...
override fun other(result: ParameterValidationResult) {
// ...
}
})
----
======
include-code::./HandlerMethodValidationExceptionVisitor[tag=snippet,indent=0]
@@ -352,9 +352,9 @@ outside. A proxy at the edge of trust must remove forwarded headers including bo
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
to protect applications which may check both.
When creating `ForwardedHeaderTransformer` you can specify whether to use the
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the transformer
lets you turn use of `"X-Forwarded-Prefix"` on and off.
When creating `ForwardedHeaderTransformer` you need to specify whether it should use the
standard `"Forwarded"` or `"X-Forwarded"` headers. If needed `"X-Forwarded-Prefix"`
must be enabled separately through a property on the transformer.
`ForwardedHeaderTransformer` can be configured in `removeOnly` mode, in which case it removes
forwarded headers from the request without using them.
@@ -490,8 +490,8 @@ The `JacksonJsonEncoder` works as follows:
* For a multi-value publisher with `application/json`, by default collect the values with
`Flux#collectToList()` and then serialize the resulting collection.
* For a multi-value publisher with a streaming media type such as
`application/x-ndjson` or `application/stream+x-jackson-smile`, encode, write, and
flush each value individually using a
`application/jsonl`, `application/x-ndjson` or `application/stream+x-jackson-smile`,
encode, write, and flush each value individually using a
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format. Other
streaming media types may be registered with the encoder.
* For SSE the `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure
@@ -603,7 +603,7 @@ To configure all three in WebFlux, you'll need to supply a pre-configured instan
[.small]#xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[See equivalent in the Servlet stack]#
When streaming to the HTTP response (for example, `text/event-stream`,
`application/x-ndjson`), it is important to send data periodically, in order to
`application/jsonl`, `application/x-ndjson`), it is important to send data periodically, in order to
reliably detect a disconnected client sooner rather than later. Such a send could be a
comment-only, empty SSE event or any other "no-op" data that would effectively serve as
a heartbeat.
@@ -66,9 +66,9 @@ outside. A proxy at the edge of trust must remove forwarded headers including bo
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
to protect applications which may check both.
When creating `ForwardedHeaderFilter` you can specify whether to use the
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the filter
lets you turn use of `"X-Forwarded-Prefix"` on and off.
When creating `ForwardedHeaderFilter` you need to specify whether it should use the
standard `"Forwarded"` or `"X-Forwarded"` headers. If needed `"X-Forwarded-Prefix"`
must be enabled separately through a property on the filter.
`ForwardedHeaderFilter` can be configured in `removeOnly` mode, in which case it removes
forwarded headers from the request without using them.
@@ -23,13 +23,17 @@ For all converters, a default media type is used, but you can override it by set
By default, this converter supports all text media types(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`.
| `FormHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write form data from the HTTP request and response.
| An `HttpMessageConverter` implementation that can read and write URL encoded forms.
By default, this converter reads and writes the `application/x-www-form-urlencoded` media type.
Form data is read from and written into a `MultiValueMap<String, String>`.
The converter can also write (but not read) multipart data read from a `MultiValueMap<String, Object>`.
By default, `multipart/form-data` is supported.
Additional multipart subtypes can be supported for writing form data.
Consult the javadoc for `FormHttpMessageConverter` for further details.
`Map<String, String>` is also supported, but multiple values under the same key will be ignored.
| `MultipartHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write multipart messages.
`MultiValueMap<String, Object>` can be written to multipart messages, converting each part independently using
the configured message converters. Multipart messages can be read into `MultiValueMap<String, Part>`, each value
being a `Part` or one of its subtypes (`FormFieldPart` and `FilePart`).
By default, `multipart/form-data` is supported. Additional multipart subtypes can be supported for writing form data.
| `ByteArrayHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write byte arrays from the HTTP request and response.
@@ -423,8 +423,8 @@ Reactive return values are handled as follows:
* A single-value promise is adapted to, similar to using `DeferredResult`. Examples
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
* A multi-value stream with a streaming media type (such as `application/jsonl`,
`application/x-ndjson` or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
`SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava).
Applications can also return `Flux<ServerSentEvent>` or `Observable<ServerSentEvent>`.
* A multi-value stream with any other media type (such as `application/json`) is adapted
@@ -65,64 +65,4 @@ The exception contains a list of ``ParameterValidationResult``s that group valid
by method parameter. You can either iterate over those, or provide a visitor with callback
methods by controller method parameter type:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
HandlerMethodValidationException ex = ... ;
ex.visitResults(new HandlerMethodValidationException.Visitor() {
@Override
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
// ...
}
@Override
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
// ...
}
@Override
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
// ...
@Override
public void other(ParameterValidationResult result) {
// ...
}
});
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// HandlerMethodValidationException
val ex
ex.visitResults(object : HandlerMethodValidationException.Visitor {
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
// ...
}
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
// ...
}
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
// ...
}
// ...
override fun other(result: ParameterValidationResult) {
// ...
}
})
----
======
include-code::./HandlerMethodValidationExceptionVisitor[tag=snippet,indent=0]
@@ -28,9 +28,9 @@ For example:
}
----
NOTE: It is also possible to configure `disallowedFields`, but that's fragile, and
due to be https://github.com/spring-projects/spring-framework/issues/36802[deprecated] in Spring Framework 7.1.
It is easy to overlook fields or introduce additional fields over time that should also be excluded.
NOTE: The `disallowedFields` property has been
https://github.com/spring-projects/spring-framework/issues/36802[deprecated in Spring Framework 7.1]
because it is fragile and easy to get out of sync with the actual properties over time.
The patterns given to `allowedFields` and `disallowedFields` are not limited to top-level
field names. They are property paths, using the same syntax supported for reading and
@@ -0,0 +1,144 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.integration.restmessageconversion;
import java.io.IOException;
import java.nio.file.Path;
import com.fasterxml.jackson.annotation.JsonView;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.io.FileSystemResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.multipart.FilePart;
import org.springframework.http.converter.multipart.FormFieldPart;
import org.springframework.http.converter.multipart.Part;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;
import static org.springframework.http.MediaType.APPLICATION_JSON;
public class RestClientMessageConversion {
private final RestClient restClient = RestClient.create();
private final Object myBean = new Object();
void useJsonView() {
// tag::jsonview[]
User user = new User("eric", "7!jd#h23");
ResponseEntity<Void> response = this.restClient.post()
.contentType(APPLICATION_JSON)
.body(user)
.hint(JsonView.class.getName(), User.WithoutPasswordView.class)
.retrieve()
.toBodilessEntity();
// end::jsonview[]
}
void sendUrlEncodedForm() {
// tag::urlencodedform[]
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("project", "Spring Framework");
form.add("module", "spring-web");
ResponseEntity<Void> response = this.restClient.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(form)
.retrieve()
.toBodilessEntity();
// end::urlencodedform[]
}
void sendMultipartData() {
// tag::multipartrequest[]
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(this.myBean, headers));
ResponseEntity<Void> response = this.restClient.post()
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(parts)
.retrieve()
.toBodilessEntity();
// end::multipartrequest[]
}
void receiveMultipartData() throws IOException {
// tag::multipartresponse[]
MultiValueMap<String, Part> result = this.restClient.get()
.uri("https://example.com/upload")
.accept(MediaType.MULTIPART_FORM_DATA)
.retrieve()
.body(new ParameterizedTypeReference<>() {});
Part field = result.getFirst("fieldPart");
if (field instanceof FormFieldPart formField) {
String fieldValue = formField.value();
}
Part file = result.getFirst("filePart");
if (file instanceof FilePart filePart) {
filePart.transferTo(Path.of("/tmp/" + filePart.filename()));
}
// end::multipartresponse[]
}
public static class User {
private String username;
private String password;
public User() {
}
public User(String username, String password) {
this.username = username;
this.password = password;
}
@JsonView(WithoutPasswordView.class)
public String getUsername() {
return this.username;
}
@JsonView(WithPasswordView.class)
public String getPassword() {
return this.password;
}
public interface WithoutPasswordView {
}
public interface WithPasswordView extends WithoutPasswordView {
}
}
private record Person(String name) {
}
}
@@ -0,0 +1,75 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.integration.restrestclient.create;
import java.io.IOException;
import java.util.Map;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInitializer;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.ApiVersionInserter;
import org.springframework.web.client.RestClient;
public class RestClientCreation {
void createRestClient() {
// tag::snippet[]
RestClient defaultClient = RestClient.create();
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.configureMessageConverters(converters -> converters.addCustomConverter(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.defaultApiVersion("1.2")
.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
.requestInterceptor(new MyCustomInterceptor())
.requestInitializer(new MyCustomInitializer())
.build();
// end::snippet[]
}
private static class MyCustomMessageConverter extends StringHttpMessageConverter {
}
private static class MyCustomInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body,
ClientHttpRequestExecution execution) throws IOException {
return execution.execute(request, body);
}
}
private static class MyCustomInitializer implements ClientHttpRequestInitializer {
@Override
public void initialize(ClientHttpRequest request) {
request.getHeaders().add("My-Header", "My-Value");
}
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2025-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.testing.resttestclient.multipart;
import org.junit.jupiter.api.Test;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.converter.multipart.FilePart;
import org.springframework.http.converter.multipart.FormFieldPart;
import org.springframework.http.converter.multipart.Part;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
public class MultipartTests {
RestTestClient client;
@Test
void multipart() {
// tag::multipart[]
client.get().uri("/upload")
.accept(MediaType.MULTIPART_FORM_DATA)
.exchange()
.expectStatus().isOk()
.expectBody(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
.value(result -> {
Part field = result.getFirst("fieldPart");
assertThat(field).isInstanceOfSatisfying(FormFieldPart.class,
formField -> assertThat(formField.value()).isEqualTo("fieldValue"));
Part file = result.getFirst("filePart");
assertThat(file).isInstanceOfSatisfying(FilePart.class,
filePart -> assertThat(filePart.filename()).isEqualTo("logo.png"));
});
// end::multipart[]
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webflux.controller.mvcannvalidation;
import org.jspecify.annotations.Nullable;
import org.springframework.validation.method.ParameterErrors;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
public class HandlerMethodValidationExceptionVisitor {
static void main() {
// tag::snippet[]
HandlerMethodValidationException ex = /**/ new HandlerMethodValidationException(null);
ex.visitResults(new HandlerMethodValidationException.Visitor() {
@Override
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
// ...
}
@Override
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
// ...
}
@Override
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
// ...
}
// @fold:on // ...
@Override
public void requestPart(RequestPart requestPart, ParameterErrors errors) {
}
@Override
public void cookieValue(CookieValue cookieValue, ParameterValidationResult result) {
}
@Override
public void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result) {
}
@Override
public void pathVariable(PathVariable pathVariable, ParameterValidationResult result) {
}
@Override
public void requestBody(RequestBody requestBody, ParameterErrors errors) {
}
// @fold:off
@Override
public void other(ParameterValidationResult result) {
// ...
}
});
// end::snippet[]
}
}
@@ -0,0 +1,90 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webmvc.mvccontroller.mvcannvalidation;
import org.jspecify.annotations.Nullable;
import org.springframework.validation.method.ParameterErrors;
import org.springframework.validation.method.ParameterValidationResult;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.MatrixVariable;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.method.annotation.HandlerMethodValidationException;
public class HandlerMethodValidationExceptionVisitor {
static void main() {
// tag::snippet[]
HandlerMethodValidationException ex = /**/ new HandlerMethodValidationException(null);
ex.visitResults(new HandlerMethodValidationException.Visitor() {
@Override
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
// ...
}
@Override
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
// ...
}
@Override
public void modelAttribute(@Nullable ModelAttribute modelAttribute, ParameterErrors errors) {
// ...
}
// @fold:on // ...
@Override
public void requestPart(RequestPart requestPart, ParameterErrors errors) {
}
@Override
public void cookieValue(CookieValue cookieValue, ParameterValidationResult result) {
}
@Override
public void matrixVariable(MatrixVariable matrixVariable, ParameterValidationResult result) {
}
@Override
public void pathVariable(PathVariable pathVariable, ParameterValidationResult result) {
}
@Override
public void requestBody(RequestBody requestBody, ParameterErrors errors) {
}
// @fold:off
@Override
public void other(ParameterValidationResult result) {
// ...
}
});
// end::snippet[]
}
}
@@ -0,0 +1,116 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.integration.restmessageconversion
import com.fasterxml.jackson.annotation.JsonView
import org.springframework.core.ParameterizedTypeReference
import org.springframework.core.io.FileSystemResource
import org.springframework.http.HttpEntity
import org.springframework.http.HttpHeaders
import org.springframework.http.MediaType
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.http.ResponseEntity
import org.springframework.http.converter.multipart.FilePart
import org.springframework.http.converter.multipart.FormFieldPart
import org.springframework.http.converter.multipart.Part
import org.springframework.util.LinkedMultiValueMap
import org.springframework.util.MultiValueMap
import org.springframework.web.client.RestClient
import java.nio.file.Path
class RestClientMessageConversion {
private val restClient = RestClient.create()
private val myBean = Any()
fun useJsonView() {
// tag::jsonview[]
val user = User("eric", "7!jd#h23")
val response: ResponseEntity<Void> = restClient.post()
.contentType(APPLICATION_JSON)
.body(user)
.hint(JsonView::class.java.name, User.WithoutPasswordView::class.java)
.retrieve()
.toBodilessEntity()
// end::jsonview[]
}
fun sendUrlEncodedForm() {
// tag::urlencodedform[]
val form: MultiValueMap<String, String> = LinkedMultiValueMap()
form.add("project", "Spring Framework")
form.add("module", "spring-web")
val response: ResponseEntity<Void> = this.restClient.post()
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.body(form)
.retrieve()
.toBodilessEntity()
// end::urlencodedform[]
}
fun sendMultipartData() {
// tag::multipartrequest[]
val parts: MultiValueMap<String, Any> = LinkedMultiValueMap()
parts.add("fieldPart", "fieldValue")
parts.add("filePart", FileSystemResource("...logo.png"))
parts.add("jsonPart", Person("Jason"))
val headers = HttpHeaders()
headers.contentType = MediaType.APPLICATION_XML
parts.add("xmlPart", HttpEntity(myBean, headers))
val response: ResponseEntity<Void> = this.restClient.post()
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(parts)
.retrieve()
.toBodilessEntity()
// end::multipartrequest[]
}
fun receiveMultipartData() {
// tag::multipartresponse[]
val result = this.restClient.get()
.uri("https://example.com/upload")
.accept(MediaType.MULTIPART_FORM_DATA)
.retrieve()
.body(object : ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
val field = result?.getFirst("fieldPart")
if (field is FormFieldPart) {
val fieldValue = field.value()
}
val file = result?.getFirst("filePart")
if (file is FilePart) {
file.transferTo(Path.of("/tmp/" + file.filename()))
}
// end::multipartresponse[]
}
class User(
@JsonView(WithoutPasswordView::class) val username: String,
@JsonView(WithPasswordView::class) val password: String) {
interface WithoutPasswordView
interface WithPasswordView : WithoutPasswordView
}
data class Person(val name: String)
}
@@ -0,0 +1,71 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.integration.restrestclient.create
import org.springframework.http.HttpRequest
import org.springframework.http.client.ClientHttpRequest
import org.springframework.http.client.ClientHttpRequestExecution
import org.springframework.http.client.ClientHttpRequestInitializer
import org.springframework.http.client.ClientHttpRequestInterceptor
import org.springframework.http.client.ClientHttpResponse
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory
import org.springframework.http.converter.StringHttpMessageConverter
import org.springframework.web.client.ApiVersionInserter
import org.springframework.web.client.RestClient
class RestClientCreation {
fun createRestClient() {
// tag::snippet[]
val defaultClient = RestClient.create()
val customClient = RestClient.builder()
.requestFactory(HttpComponentsClientHttpRequestFactory())
.configureMessageConverters { converters -> converters.addCustomConverter(MyCustomMessageConverter()) }
.baseUrl("https://example.com")
.defaultUriVariables(mapOf("variable" to "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.defaultApiVersion("1.2")
.apiVersionInserter(ApiVersionInserter.useHeader("API-Version"))
.requestInterceptor(MyCustomInterceptor())
.requestInitializer(MyCustomInitializer())
.build()
// end::snippet[]
}
private class MyCustomMessageConverter : StringHttpMessageConverter()
private class MyCustomInterceptor : ClientHttpRequestInterceptor {
override fun intercept(
request: HttpRequest,
body: ByteArray,
execution: ClientHttpRequestExecution
): ClientHttpResponse {
return execution.execute(request, body)
}
}
private class MyCustomInitializer : ClientHttpRequestInitializer {
override fun initialize(request: ClientHttpRequest) {
request.headers.add("My-Header", "My-Value")
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webflux.controller.mvcannvalidation
import org.springframework.context.MessageSourceResolvable
import org.springframework.validation.method.MethodValidationResult
import org.springframework.validation.method.ParameterErrors
import org.springframework.validation.method.ParameterValidationResult
import org.springframework.web.bind.annotation.*
import org.springframework.web.method.annotation.HandlerMethodValidationException
import java.lang.reflect.Method
class HandlerMethodValidationExceptionVisitor {
fun main() {
// tag::snippet[]
val ex: HandlerMethodValidationException = /**/ HandlerMethodValidationException(EmptyMethodValidationResult())
ex.visitResults(object : HandlerMethodValidationException.Visitor {
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
// ...
}
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
// ...
}
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
// ...
}
// @fold:on // ...
override fun requestPart(requestPart: RequestPart, errors: ParameterErrors) {
}
override fun cookieValue(cookieValue: CookieValue, result: ParameterValidationResult) {
}
override fun matrixVariable(matrixVariable: MatrixVariable, result: ParameterValidationResult) {
}
override fun pathVariable(pathVariable: PathVariable, result: ParameterValidationResult) {
}
override fun requestBody(requestBody: RequestBody, errors: ParameterErrors) {
}
// @fold:off
override fun other(result: ParameterValidationResult) {
// ...
}
})
// end::snippet[]
}
internal class EmptyMethodValidationResult : MethodValidationResult {
override fun getTarget(): Any {
TODO()
}
override fun getMethod(): Method {
TODO()
}
override fun isForReturnValue(): Boolean {
TODO()
}
override fun getParameterValidationResults(): List<ParameterValidationResult> {
TODO()
}
override fun getCrossParameterValidationResults(): List<MessageSourceResolvable> {
TODO()
}
override fun toString(): String {
TODO()
}
}
}
@@ -0,0 +1,96 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webmvc.mvccontroller.mvcannvalidation
import org.springframework.context.MessageSourceResolvable
import org.springframework.validation.method.MethodValidationResult
import org.springframework.validation.method.ParameterErrors
import org.springframework.validation.method.ParameterValidationResult
import org.springframework.web.bind.annotation.*
import org.springframework.web.method.annotation.HandlerMethodValidationException
import java.lang.reflect.Method
class HandlerMethodValidationExceptionVisitor {
fun main() {
// tag::snippet[]
val ex: HandlerMethodValidationException = /**/ HandlerMethodValidationException(EmptyMethodValidationResult())
ex.visitResults(object : HandlerMethodValidationException.Visitor {
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
// ...
}
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
// ...
}
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
// ...
}
// @fold:on // ...
override fun requestPart(requestPart: RequestPart, errors: ParameterErrors) {
}
override fun cookieValue(cookieValue: CookieValue, result: ParameterValidationResult) {
}
override fun matrixVariable(matrixVariable: MatrixVariable, result: ParameterValidationResult) {
}
override fun pathVariable(pathVariable: PathVariable, result: ParameterValidationResult) {
}
override fun requestBody(requestBody: RequestBody, errors: ParameterErrors) {
}
// @fold:off
override fun other(result: ParameterValidationResult) {
// ...
}
})
// end::snippet[]
}
internal class EmptyMethodValidationResult : MethodValidationResult {
override fun getTarget(): Any {
TODO()
}
override fun getMethod(): Method {
TODO()
}
override fun isForReturnValue(): Boolean {
TODO()
}
override fun getParameterValidationResults(): List<ParameterValidationResult> {
TODO()
}
override fun getCrossParameterValidationResults(): List<MessageSourceResolvable> {
TODO()
}
override fun toString(): String {
TODO()
}
}
}
+15 -15
View File
@@ -7,30 +7,30 @@ javaPlatform {
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.20.2"))
api(platform("io.micrometer:micrometer-bom:1.16.7"))
api(platform("com.fasterxml.jackson:jackson-bom:2.21.6"))
api(platform("io.micrometer:micrometer-bom:1.18.0-SNAPSHOT"))
api(platform("io.netty:netty-bom:4.2.18.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.7"))
api(platform("io.projectreactor:reactor-bom:2026.0.0-SNAPSHOT"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:5.0.8"))
api(platform("org.apache.groovy:groovy-bom:5.1.2"))
api(platform("org.apache.logging.log4j:log4j-bom:2.26.1"))
api(platform("org.assertj:assertj-bom:3.27.7"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.13"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.13"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
api(platform("org.junit:junit-bom:6.0.3"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.11.0"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0"))
api(platform("org.junit:junit-bom:6.1.2"))
api(platform("org.mockito:mockito-bom:5.23.0"))
api(platform("tools.jackson:jackson-bom:3.0.4"))
api(platform("tools.jackson:jackson-bom:3.1.6"))
constraints {
api("com.fasterxml:aalto-xml:1.3.4")
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.fasterxml:aalto-xml:1.4.0")
api("com.fasterxml.woodstox:woodstox-core:7.2.2")
api("com.github.ben-manes.caffeine:caffeine:3.2.4")
api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.13.2")
api("com.google.code.gson:gson:2.14.0")
api("com.google.protobuf:protobuf-java-util:4.36.1")
api("com.h2database:h2:2.4.240")
api("com.jayway.jsonpath:json-path:2.10.0")
@@ -120,10 +120,10 @@ dependencies {
api("org.glassfish:jakarta.el:4.0.2")
api("org.graalvm.sdk:graal-sdk:22.3.1")
api("org.hamcrest:hamcrest:3.0")
api("org.hibernate.orm:hibernate-core:7.2.24.Final")
api("org.hibernate.orm:hibernate-core:7.4.8.Final")
api("org.hibernate.validator:hibernate-validator:9.1.3.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.21.0")
api("org.htmlunit:htmlunit:5.4.0")
api("org.javamoney:moneta:1.4.4")
api("org.jboss.logging:jboss-logging:3.6.1.Final")
api("org.jruby:jruby:10.0.2.0")
@@ -134,8 +134,8 @@ dependencies {
api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.reactivestreams:reactive-streams:1.0.4")
api("org.seleniumhq.selenium:htmlunit3-driver:4.41.0")
api("org.seleniumhq.selenium:selenium-java:4.41.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.47.0")
api("org.seleniumhq.selenium:selenium-java:4.47.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.testng:testng:7.12.0")
api("org.webjars:webjars-locator-lite:1.1.0")
+2 -2
View File
@@ -1,10 +1,10 @@
version=7.0.10-SNAPSHOT
version=7.1.0-SNAPSHOT
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
org.gradle.parallel=true
kotlinVersion=2.2.21
kotlinVersion=2.4.20
byteBuddyVersion=1.17.6
kotlin.jvm.target.validation.mode=ignore
@@ -35,7 +35,7 @@ public class ComponentFactoryBean implements FactoryBean<Component> {
@Override
public Component getObject() {
if (this.children != null && this.children.size() > 0) {
if (this.children != null && !this.children.isEmpty()) {
for (Component child : children) {
this.parent.addComponent(child);
}
@@ -75,7 +75,7 @@ public class AspectJAfterThrowingAdvice extends AbstractAspectJAdvice
* is only invoked if the thrown exception is a subtype of the given throwing type.
*/
private boolean shouldInvokeOnThrowing(Throwable ex) {
return getDiscoveredThrowingType().isAssignableFrom(ex.getClass());
return getDiscoveredThrowingType().isInstance(ex);
}
}
@@ -75,11 +75,9 @@ final class InstantiationModelAwarePointcutAdvisorImpl
private @Nullable Advice instantiatedAdvice;
@SuppressWarnings("NullAway.Init")
private Boolean isBeforeAdvice;
private @Nullable Boolean isBeforeAdvice;
@SuppressWarnings("NullAway.Init")
private Boolean isAfterAdvice;
private @Nullable Boolean isAfterAdvice;
public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
@@ -198,7 +196,7 @@ final class InstantiationModelAwarePointcutAdvisorImpl
if (this.isBeforeAdvice == null) {
determineAdviceType();
}
return this.isBeforeAdvice;
return (this.isBeforeAdvice == Boolean.TRUE);
}
@Override
@@ -206,7 +204,7 @@ final class InstantiationModelAwarePointcutAdvisorImpl
if (this.isAfterAdvice == null) {
determineAdviceType();
}
return this.isAfterAdvice;
return (this.isAfterAdvice == Boolean.TRUE);
}
/**
@@ -16,6 +16,7 @@
package org.springframework.aop.framework;
import java.io.Closeable;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
@@ -30,6 +31,9 @@ import org.springframework.aop.TargetClassAware;
import org.springframework.aop.TargetSource;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.target.SingletonTargetSource;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.DecoratingProxy;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -301,4 +305,16 @@ public abstract class AopProxyUtils {
return arguments;
}
/**
* Determine whether the given interface is a Spring configuration callback
* interface (i.e. {@link InitializingBean}, {@link DisposableBean},
* {@link Closeable}/{@link AutoCloseable}, or an {@link Aware} sub-interface).
* @since 7.1
*/
static boolean isConfigurationCallbackInterface(Class<?> ifc) {
return (InitializingBean.class == ifc || DisposableBean.class == ifc ||
Closeable.class == ifc || AutoCloseable.class == ifc ||
ObjectUtils.containsElement(ifc.getInterfaces(), Aware.class));
}
}
@@ -292,8 +292,13 @@ class CglibAopProxy implements AopProxy, Serializable {
if (Modifier.isFinal(mod)) {
if (logger.isWarnEnabled() && Modifier.isPublic(mod)) {
if (implementsInterface(method, ifcs)) {
logger.warn("Unable to proxy interface-implementing method [" + method + "] because " +
"it is marked as final, consider using interface-based JDK proxies instead.");
// Final methods inherited from configuration callback interfaces are
// typically driven by the container itself rather than by user code, so
// logging a warning about CGLIB being unable to advise them is misleading noise.
if (!implementsOnlyConfigurationCallbackInterfaces(method, ifcs)) {
logger.warn("Unable to proxy interface-implementing method [" + method + "] because " +
"it is marked as final, consider using interface-based JDK proxies instead.");
}
}
else {
logger.warn("Public final method [" + method + "] cannot get proxied via CGLIB, " +
@@ -415,6 +420,25 @@ class CglibAopProxy implements AopProxy, Serializable {
return false;
}
/**
* Check whether every interface that declares the given method is a
* configuration callback interface.
* @since 7.1
* @see AopProxyUtils#isConfigurationCallbackInterface(Class)
*/
static boolean implementsOnlyConfigurationCallbackInterfaces(Method method, Set<Class<?>> ifcs) {
boolean matched = false;
for (Class<?> ifc : ifcs) {
if (ClassUtils.hasMethod(ifc, method)) {
if (!AopProxyUtils.isConfigurationCallbackInterface(ifc)) {
return false;
}
matched = true;
}
}
return matched;
}
/**
* Process a return value. Wraps a return of {@code this} if necessary to be the
* {@code proxy} and also verifies that {@code null} is not returned as a primitive.
@@ -16,17 +16,11 @@
package org.springframework.aop.framework;
import java.io.Closeable;
import org.jspecify.annotations.Nullable;
import org.springframework.beans.factory.Aware;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.Ordered;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Base class with common functionality for proxy processors, in particular
@@ -130,8 +124,7 @@ public class ProxyProcessorSupport extends ProxyConfig implements Ordered, BeanC
* @return whether the given interface is just a container callback
*/
protected boolean isConfigurationCallbackInterface(Class<?> ifc) {
return (InitializingBean.class == ifc || DisposableBean.class == ifc || Closeable.class == ifc ||
AutoCloseable.class == ifc || ObjectUtils.containsElement(ifc.getInterfaces(), Aware.class));
return AopProxyUtils.isConfigurationCallbackInterface(ifc);
}
/**
@@ -42,8 +42,7 @@ public abstract class AbstractRefreshableTargetSource implements TargetSource, R
/** Logger available to subclasses. */
protected final Log logger = LogFactory.getLog(getClass());
@SuppressWarnings("NullAway.Init")
protected Object targetObject;
protected @Nullable Object targetObject;
private long refreshCheckDelay = -1;
@@ -66,11 +65,11 @@ public abstract class AbstractRefreshableTargetSource implements TargetSource, R
@Override
public synchronized Class<?> getTargetClass() {
public synchronized @Nullable Class<?> getTargetClass() {
if (this.targetObject == null) {
refresh();
}
return this.targetObject.getClass();
return (this.targetObject != null ? this.targetObject.getClass() : null);
}
@Override
@@ -0,0 +1,177 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aop.framework;
import java.io.Closeable;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.ClassUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CglibAopProxy#implementsOnlyConfigurationCallbackInterfaces}.
*
* <p>Verifies that final methods inherited from Spring's configuration callback
* interfaces (InitializingBean, DisposableBean, Aware sub-interfaces,
* Closeable/AutoCloseable) are recognized so that the CGLIB validation warning
* can be suppressed for those container-driven methods.
*
* @since 7.1
* @see <a href="https://github.com/spring-projects/spring-framework/pull/36935>gh-36935</a>
*/
class CglibAopProxyConfigurationCallbackTests {
@Test
void finalAfterPropertiesSetIsRecognisedAsCallback() {
var method = ClassUtils.getMethod(WithFinalAfterPropertiesSet.class, "afterPropertiesSet");
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalAfterPropertiesSet.class);
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
}
@Test
void finalDestroyIsRecognisedAsCallback() {
var method = ClassUtils.getMethod(WithFinalDestroy.class, "destroy");
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalDestroy.class);
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
}
@Test
void finalAwareCallbackIsRecognisedAsCallback() {
var method = ClassUtils.getMethod(WithFinalBeanFactoryAware.class, "setBeanFactory", BeanFactory.class);
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalBeanFactoryAware.class);
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
}
@Test
void finalCloseableCloseIsRecognisedAsCallback() {
var method = ClassUtils.getMethod(WithFinalCloseableClose.class, "close");
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalCloseableClose.class);
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
}
@Test
void finalAutoCloseableCloseIsRecognisedAsCallback() {
var method = ClassUtils.getMethod(WithFinalAutoCloseableClose.class, "close");
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalAutoCloseableClose.class);
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isTrue();
}
@Test
void finalUserInterfaceMethodIsNotSuppressed() {
var method = ClassUtils.getMethod(WithFinalUserApi.class, "execute");
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithFinalUserApi.class);
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isFalse();
}
@Test
void methodSharedBetweenCallbackAndUserInterfaceIsNotSuppressed() {
var method = ClassUtils.getMethod(WithSharedSignature.class, "afterPropertiesSet");
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithSharedSignature.class);
// Even though InitializingBean declares afterPropertiesSet(), a user
// interface (CustomLifecycle) declares the same signature, so the
// warning must still fire.
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isFalse();
}
@Test
void finalMethodWithoutInterfaceMatchIsNotSuppressed() {
var method = ClassUtils.getMethod(WithStandaloneFinal.class, "doSomething");
var interfaces = ClassUtils.getAllInterfacesForClassAsSet(WithStandaloneFinal.class);
assertThat(CglibAopProxy.implementsOnlyConfigurationCallbackInterfaces(method, interfaces)).isFalse();
}
static class WithFinalAfterPropertiesSet implements InitializingBean {
@Override
public final void afterPropertiesSet() {
}
}
static class WithFinalDestroy implements DisposableBean {
@Override
public final void destroy() {
}
}
static class WithFinalBeanFactoryAware implements BeanFactoryAware {
@Override
public final void setBeanFactory(BeanFactory beanFactory) {
}
}
static class WithFinalCloseableClose implements Closeable {
@Override
public final void close() {
}
}
static class WithFinalAutoCloseableClose implements AutoCloseable {
@Override
public final void close() {
}
}
interface UserApi {
void execute();
}
static class WithFinalUserApi implements UserApi {
@Override
public final void execute() {
}
}
interface CustomLifecycle {
void afterPropertiesSet();
}
static class WithSharedSignature implements InitializingBean, CustomLifecycle {
@Override
public final void afterPropertiesSet() {
}
}
static class WithStandaloneFinal {
public final void doSomething() {
}
}
}
@@ -23,7 +23,6 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.lang.reflect.UndeclaredThrowableException;
import java.security.PrivilegedActionException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
@@ -42,7 +41,6 @@ import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* A basic {@link ConfigurablePropertyAccessor} that provides the necessary
@@ -60,6 +58,7 @@ import org.springframework.util.StringUtils;
* @author Rod Johnson
* @author Rob Harrop
* @author Sam Brannen
* @author Brian Clozel
* @since 4.2
* @see #registerCustomEditor
* @see #setPropertyValues
@@ -76,16 +75,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
*/
private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class);
private int autoGrowCollectionLimit = Integer.MAX_VALUE;
@Nullable Object wrappedObject;
private String nestedPath = "";
@Nullable Object rootObject;
/** Map with cached nested Accessors: nested path -> Accessor instance. */
private @Nullable Map<String, AbstractNestablePropertyAccessor> nestedPropertyAccessors;
/** Map with cached nested Accessors: path segment -> Accessor instance. */
private @Nullable Map<PropertyPath.Segment, AbstractNestablePropertyAccessor> nestedPropertyAccessors;
/**
@@ -152,25 +149,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
setExtractOldValueForEditor(parent.isExtractOldValueForEditor());
setAutoGrowNestedPaths(parent.isAutoGrowNestedPaths());
setAutoGrowCollectionLimit(parent.getAutoGrowCollectionLimit());
setMaxNestedPathDepth(parent.getMaxNestedPathDepth());
setConversionService(parent.getConversionService());
}
/**
* Specify a limit for array and collection auto-growing.
* <p>Default is unlimited on a plain accessor.
*/
public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
}
/**
* Return the limit for array and collection auto-growing.
*/
public int getAutoGrowCollectionLimit() {
return this.autoGrowCollectionLimit;
}
/**
* Switch the target object, replacing the cached introspection results only
* if the class of the new object is different to that of the replaced object.
@@ -231,61 +214,70 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
@Override
public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
AbstractNestablePropertyAccessor nestedPa;
ResolvedProperty resolved;
try {
nestedPa = getPropertyAccessorForPropertyPath(propertyName);
resolved = resolvePropertyPath(propertyName);
}
catch (InvalidPropertyPathException ex) {
// A malformed path is a syntax error, distinct from a syntactically
// valid path whose intermediate segment genuinely does not exist
// (caught below and reported as "not writable" instead).
throw new InvalidPropertyPathException(getRootInstance(), this.nestedPath + propertyName, value, ex);
}
catch (NotReadablePropertyException ex) {
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
"Nested property in path '" + propertyName + "' does not exist", ex);
}
PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));
resolved.accessor().setPropertyValue(resolved.segment(), new PropertyValue(propertyName, value));
}
@Override
public void setPropertyValue(PropertyValue pv) throws BeansException {
PropertyTokenHolder tokens = (PropertyTokenHolder) pv.resolvedTokens;
if (tokens == null) {
PropertyPath.Segment segment = (PropertyPath.Segment) pv.resolvedTokens;
if (segment == null) {
String propertyName = pv.getName();
AbstractNestablePropertyAccessor nestedPa;
ResolvedProperty resolved;
try {
nestedPa = getPropertyAccessorForPropertyPath(propertyName);
resolved = resolvePropertyPath(propertyName);
}
catch (InvalidPropertyPathException ex) {
throw new InvalidPropertyPathException(getRootInstance(), this.nestedPath + propertyName, pv.getValue(), ex);
}
catch (NotReadablePropertyException ex) {
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
"Nested property in path '" + propertyName + "' does not exist", ex);
}
tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
if (nestedPa == this) {
pv.getOriginalPropertyValue().resolvedTokens = tokens;
segment = resolved.segment();
if (resolved.accessor() == this) {
pv.getOriginalPropertyValue().resolvedTokens = segment;
}
nestedPa.setPropertyValue(tokens, pv);
resolved.accessor().setPropertyValue(segment, pv);
}
else {
setPropertyValue(tokens, pv);
setPropertyValue(segment, pv);
}
}
protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
if (tokens.keys != null) {
processKeyedProperty(tokens, pv);
protected void setPropertyValue(PropertyPath.Segment segment, PropertyValue pv) throws BeansException {
if (!segment.keys().isEmpty()) {
processKeyedProperty(segment, pv);
}
else {
processLocalProperty(tokens, pv);
processLocalProperty(segment, pv);
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) {
Object propValue = getPropertyHoldingValue(tokens);
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
private void processKeyedProperty(PropertyPath.Segment segment, PropertyValue pv) {
Object propValue = getPropertyHoldingValue(segment);
PropertyHandler ph = getLocalPropertyHandler(segment.name());
if (ph == null) {
throw new InvalidPropertyException(
getRootClass(), this.nestedPath + tokens.actualName, "No property handler found");
getRootClass(), this.nestedPath + segment.name(), "No property handler found");
}
Assert.state(tokens.keys != null, "No token keys");
String lastKey = tokens.keys[tokens.keys.length - 1];
List<String> keys = segment.keys();
String lastKey = keys.get(keys.size() - 1);
String canonicalName = segment.toCanonicalName();
if (propValue.getClass().isArray()) {
Class<?> componentType = propValue.getClass().componentType();
@@ -295,44 +287,43 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
if (isExtractOldValueForEditor() && arrayIndex < Array.getLength(propValue)) {
oldValue = Array.get(propValue, arrayIndex);
}
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
componentType, ph.nested(tokens.keys.length));
Object convertedValue = convertIfNecessary(canonicalName, oldValue, pv.getValue(),
componentType, ph.nested(keys.size()));
int length = Array.getLength(propValue);
if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) {
if (arrayIndex >= length && arrayIndex < getAutoGrowCollectionLimit()) {
Object newArray = Array.newInstance(componentType, arrayIndex + 1);
System.arraycopy(propValue, 0, newArray, 0, length);
int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
String propName = tokens.canonicalName.substring(0, lastKeyIndex);
String propName = segment.withoutLastKey().toCanonicalName();
setPropertyValue(propName, newArray);
propValue = getPropertyValue(propName);
}
Array.set(propValue, arrayIndex, convertedValue);
}
catch (IndexOutOfBoundsException ex) {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
"Invalid array index in property path '" + tokens.canonicalName + "'", ex);
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
"Invalid array index in property path '" + canonicalName + "'", ex);
}
}
else if (propValue instanceof List list) {
TypeDescriptor requiredType = ph.getCollectionType(tokens.keys.length);
TypeDescriptor requiredType = ph.getCollectionType(keys.size());
int index = Integer.parseInt(lastKey);
Object oldValue = null;
if (isExtractOldValueForEditor() && index < list.size()) {
oldValue = list.get(index);
}
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
Object convertedValue = convertIfNecessary(canonicalName, oldValue, pv.getValue(),
requiredType.getResolvableType().resolve(), requiredType);
int size = list.size();
if (index >= size && index < this.autoGrowCollectionLimit) {
if (index >= size && index < getAutoGrowCollectionLimit()) {
for (int i = size; i < index; i++) {
try {
list.add(null);
}
catch (NullPointerException ex) {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
"Cannot set element with index " + index + " in List of size " +
size + ", accessed using property path '" + tokens.canonicalName +
size + ", accessed using property path '" + canonicalName +
"': List does not support filling up gaps with null elements");
}
}
@@ -343,15 +334,15 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
list.set(index, convertedValue);
}
catch (IndexOutOfBoundsException ex) {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
"Invalid list index in property path '" + tokens.canonicalName + "'", ex);
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
"Invalid list index in property path '" + canonicalName + "'", ex);
}
}
}
else if (propValue instanceof Map map) {
TypeDescriptor mapKeyType = ph.getMapKeyType(tokens.keys.length);
TypeDescriptor mapValueType = ph.getMapValueType(tokens.keys.length);
TypeDescriptor mapKeyType = ph.getMapKeyType(keys.size());
TypeDescriptor mapValueType = ph.getMapValueType(keys.size());
// IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values.
Object convertedMapKey = convertIfNecessary(null, null, lastKey,
@@ -362,58 +353,54 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
// Pass full property name and old value in here, since we want full
// conversion ability for map values.
Object convertedMapValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
Object convertedMapValue = convertIfNecessary(canonicalName, oldValue, pv.getValue(),
mapValueType.getResolvableType().resolve(), mapValueType);
map.put(convertedMapKey, convertedMapValue);
}
else {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
"Property referenced in indexed property path '" + tokens.canonicalName +
throw new InvalidPropertyException(getRootClass(), this.nestedPath + canonicalName,
"Property referenced in indexed property path '" + canonicalName +
"' is neither an array nor a List nor a Map; returned value was [" + propValue + "]");
}
}
private Object getPropertyHoldingValue(PropertyTokenHolder tokens) {
private Object getPropertyHoldingValue(PropertyPath.Segment segment) {
// Apply indexes and map keys: fetch value for all keys but the last one.
Assert.state(tokens.keys != null, "No token keys");
PropertyTokenHolder getterTokens = new PropertyTokenHolder(tokens.actualName);
getterTokens.canonicalName = tokens.canonicalName;
getterTokens.keys = new String[tokens.keys.length - 1];
System.arraycopy(tokens.keys, 0, getterTokens.keys, 0, tokens.keys.length - 1);
PropertyPath.Segment getterSegment = segment.withoutLastKey();
Object propValue;
try {
propValue = getPropertyValue(getterTokens);
propValue = getPropertyValue(getterSegment);
}
catch (NotReadablePropertyException ex) {
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + tokens.canonicalName,
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + segment.toCanonicalName(),
"Cannot access indexed value in property referenced " +
"in indexed property path '" + tokens.canonicalName + "'", ex);
"in indexed property path '" + segment.toCanonicalName() + "'", ex);
}
if (propValue == null) {
// null map value case
if (isAutoGrowNestedPaths()) {
int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
getterTokens.canonicalName = tokens.canonicalName.substring(0, lastKeyIndex);
propValue = setDefaultValue(getterTokens);
propValue = setDefaultValue(getterSegment);
}
else {
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName,
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + segment.toCanonicalName(),
"Cannot access indexed value in property referenced " +
"in indexed property path '" + tokens.canonicalName + "': returned null");
"in indexed property path '" + segment.toCanonicalName() + "': returned null");
}
}
return propValue;
}
private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
private void processLocalProperty(PropertyPath.Segment segment, PropertyValue pv) {
// segment.keys() is always empty here (see setPropertyValue(Segment, PropertyValue)
// above), so the segment's canonical name is always just its raw name.
String name = segment.name();
PropertyHandler ph = getLocalPropertyHandler(name);
if (ph == null || !ph.isWritable()) {
if (pv.isOptional()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring optional value for property '" + tokens.actualName +
logger.debug("Ignoring optional value for property '" + name +
"' - property not found on bean class [" + getRootClass().getName() + "]");
}
return;
@@ -423,7 +410,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
// exception would be caught and swallowed higher up anyway...
return;
}
throw createNotWritablePropertyException(tokens.canonicalName);
throw createNotWritablePropertyException(name);
}
Object oldValue = null;
@@ -445,12 +432,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
if (logger.isDebugEnabled()) {
logger.debug("Could not read previous value of property '" +
this.nestedPath + tokens.canonicalName + "'", ex);
this.nestedPath + name + "'", ex);
}
}
}
valueToApply = convertForProperty(
tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
valueToApply = convertForProperty(name, oldValue, originalValue, ph.toTypeDescriptor());
}
pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue);
}
@@ -463,7 +449,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
catch (InvocationTargetException ex) {
PropertyChangeEvent propertyChangeEvent = new PropertyChangeEvent(
getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
getRootInstance(), this.nestedPath + name, oldValue, pv.getValue());
if (ex.getTargetException() instanceof ClassCastException) {
throw new TypeMismatchException(propertyChangeEvent, ph.getPropertyType(), ex.getTargetException());
}
@@ -478,7 +464,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
catch (Exception ex) {
PropertyChangeEvent pce = new PropertyChangeEvent(
getRootInstance(), this.nestedPath + tokens.canonicalName, oldValue, pv.getValue());
getRootInstance(), this.nestedPath + name, oldValue, pv.getValue());
throw new MethodInvocationException(pce, ex);
}
}
@@ -507,7 +493,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
}
}
catch (InvalidPropertyException ex) {
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
// Consider as not determinable.
}
return null;
@@ -516,14 +502,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
@Override
public @Nullable TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException {
try {
AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
String finalPath = getFinalPath(nestedPa, propertyName);
PropertyTokenHolder tokens = getPropertyNameTokens(finalPath);
PropertyHandler ph = nestedPa.getLocalPropertyHandler(tokens.actualName);
ResolvedProperty resolved = resolvePropertyPath(propertyName);
PropertyPath.Segment segment = resolved.segment();
PropertyHandler ph = resolved.accessor().getLocalPropertyHandler(segment.name());
if (ph != null) {
if (tokens.keys != null) {
List<String> keys = segment.keys();
if (!keys.isEmpty()) {
if (ph.isReadable() || ph.isWritable()) {
return ph.nested(tokens.keys.length);
return ph.nested(keys.size());
}
}
else {
@@ -533,7 +519,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
}
}
catch (InvalidPropertyException ex) {
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
// Consider as not determinable.
}
return null;
@@ -552,7 +538,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
return true;
}
}
catch (InvalidPropertyException ex) {
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
// Cannot be evaluated, so can't be readable.
}
return false;
@@ -571,7 +557,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
return true;
}
}
catch (InvalidPropertyException ex) {
catch (InvalidPropertyException | InvalidPropertyPathException ex) {
// Cannot be evaluated, so can't be writable.
}
return false;
@@ -606,25 +592,25 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
@Override
public @Nullable Object getPropertyValue(String propertyName) throws BeansException {
AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
return nestedPa.getPropertyValue(tokens);
ResolvedProperty resolved = resolvePropertyPath(propertyName);
return resolved.accessor().getPropertyValue(resolved.segment());
}
@SuppressWarnings({"rawtypes", "unchecked"})
protected @Nullable Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
String propertyName = tokens.canonicalName;
String actualName = tokens.actualName;
protected @Nullable Object getPropertyValue(PropertyPath.Segment segment) throws BeansException {
String propertyName = segment.toCanonicalName();
String actualName = segment.name();
PropertyHandler ph = getLocalPropertyHandler(actualName);
if (ph == null || !ph.isReadable()) {
throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);
}
try {
Object value = ph.getValue();
if (tokens.keys != null) {
List<String> keys = segment.keys();
if (!keys.isEmpty()) {
if (value == null) {
if (isAutoGrowNestedPaths()) {
value = setDefaultValue(new PropertyTokenHolder(tokens.actualName));
value = setDefaultValue(new PropertyPath.Segment(actualName, List.of()));
}
else {
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
@@ -632,10 +618,10 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
"property path '" + propertyName + "': returned null");
}
}
StringBuilder indexedPropertyName = new StringBuilder(tokens.actualName);
StringBuilder indexedPropertyName = new StringBuilder(actualName);
// apply indexes and map keys
for (int i = 0; i < tokens.keys.length; i++) {
String key = tokens.keys[i];
for (int i = 0; i < keys.size(); i++) {
String key = keys.get(i);
if (value == null) {
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
"Cannot access indexed value of property referenced in indexed " +
@@ -734,8 +720,8 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
*/
protected @Nullable PropertyHandler getPropertyHandler(String propertyName) throws BeansException {
Assert.notNull(propertyName, "Property name must not be null");
AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName);
return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName));
ResolvedProperty resolved = resolvePropertyPath(propertyName);
return resolved.accessor().getLocalPropertyHandler(resolved.segment().toCanonicalName());
}
/**
@@ -766,7 +752,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
return array;
}
int length = Array.getLength(array);
if (index >= length && index < this.autoGrowCollectionLimit) {
if (index >= length && index < getAutoGrowCollectionLimit()) {
Class<?> componentType = array.getClass().componentType();
Object newArray = Array.newInstance(componentType, index + 1);
System.arraycopy(array, 0, newArray, 0, length);
@@ -790,7 +776,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
return;
}
int size = collection.size();
if (index >= size && index < this.autoGrowCollectionLimit) {
if (index >= size && index < getAutoGrowCollectionLimit()) {
Class<?> elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric();
if (elementType != null) {
for (int i = collection.size(); i < index + 1; i++) {
@@ -801,35 +787,55 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
/**
* Get the last component of the path. Also works if not nested.
* @param pa property accessor to work on
* @param nestedPath property path we know is nested
* @return last component of the path (the property on the target bean)
* Resolve a property path to the accessor owning its final segment and
* that segment itself, parsing the path exactly once.
* @param propertyPath the property path, which may be nested
* @return the accessor for the target bean, paired with the final segment
* @throws InvalidPropertyPathException if the given path is not a
* well-formed property path, or if its nesting depth exceeds
* {@link #getMaxNestedPathDepth()}
* @since 7.1
*/
protected String getFinalPath(AbstractNestablePropertyAccessor pa, String nestedPath) {
if (pa == this) {
return nestedPath;
}
return nestedPath.substring(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex(nestedPath) + 1);
protected ResolvedProperty resolvePropertyPath(String propertyPath) {
PropertyPath.Options options = PropertyPath.Options.withMaxNestedPathDepth(getMaxNestedPathDepth());
List<PropertyPath.Segment> segments = PropertyPath.parse(propertyPath, options).segments();
AbstractNestablePropertyAccessor accessor = getPropertyAccessorForSegments(segments, 0);
PropertyPath.Segment segment = finalSegment(propertyPath, segments);
return new ResolvedProperty(accessor, segment);
}
/**
* Recursively navigate to return a property accessor for the nested property path.
* @param propertyPath property path, which may be nested
* @return a property accessor for the target bean
* The final segment of an already-parsed property path: the property to
* actually get or set. Handles the one case {@link PropertyPath} itself
* does not produce a segment for: the empty path ({@code ""}), a valid
* property path with zero segments, treated the same as a single segment
* with an empty name and no keys — a shape {@link PropertyPath.Segment}'s
* own constructor allows even though {@link PropertyPath#parse}'s grammar
* validation never produces it.
*/
protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
// Handle nested properties recursively.
if (pos > -1) {
String nestedProperty = propertyPath.substring(0, pos);
String nestedPath = propertyPath.substring(pos + 1);
AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
}
else {
private static PropertyPath.Segment finalSegment(String propertyPath, List<PropertyPath.Segment> segments) {
return (segments.isEmpty() ? new PropertyPath.Segment(propertyPath, List.of()) :
segments.get(segments.size() - 1));
}
/**
* Recursively navigate to return a property accessor for the given,
* already-parsed property path segments, peeling one segment off at a
* time until only the final segment (the property to actually get or
* set on the returned accessor) is left.
* @param segments the segments of the full property path, parsed exactly
* once by the caller
* @param fromIndex the index of the first segment not yet consumed
* @return a property accessor for the bean holding the final segment
*/
private AbstractNestablePropertyAccessor getPropertyAccessorForSegments(
List<PropertyPath.Segment> segments, int fromIndex) {
if (segments.isEmpty() || fromIndex >= segments.size() - 1) {
return this;
}
AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(segments.get(fromIndex));
return nestedPa.getPropertyAccessorForSegments(segments, fromIndex + 1);
}
/**
@@ -837,31 +843,30 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
* Create a new one if not found in the cache.
* <p>Note: Caching nested PropertyAccessors is necessary now,
* to keep registered custom editors for nested properties.
* @param nestedProperty property to create the PropertyAccessor for
* @param segment the already-parsed segment to create the PropertyAccessor for
* @return the PropertyAccessor instance, either cached or newly created
*/
private AbstractNestablePropertyAccessor getNestedPropertyAccessor(String nestedProperty) {
Map<String, AbstractNestablePropertyAccessor> nestedAccessors = this.nestedPropertyAccessors;
private AbstractNestablePropertyAccessor getNestedPropertyAccessor(PropertyPath.Segment segment) {
Map<PropertyPath.Segment, AbstractNestablePropertyAccessor> nestedAccessors = this.nestedPropertyAccessors;
if (nestedAccessors == null) {
nestedAccessors = new HashMap<>();
this.nestedPropertyAccessors = nestedAccessors;
}
// Get value of bean property.
PropertyTokenHolder tokens = getPropertyNameTokens(nestedProperty);
String canonicalName = tokens.canonicalName;
Object value = getPropertyValue(tokens);
Object value = getPropertyValue(segment);
if (value == null || (value instanceof Optional<?> optional && optional.isEmpty())) {
if (isAutoGrowNestedPaths()) {
value = setDefaultValue(tokens);
value = setDefaultValue(segment);
}
else {
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName);
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + segment.toCanonicalName());
}
}
// Lookup cached sub-PropertyAccessor, create new one if not found.
AbstractNestablePropertyAccessor nestedPa = nestedAccessors.get(canonicalName);
AbstractNestablePropertyAccessor nestedPa = nestedAccessors.get(segment);
if (nestedPa == null || nestedPa.getWrappedInstance() != ObjectUtils.unwrapOptional(value)) {
String canonicalName = segment.toCanonicalName();
if (logger.isTraceEnabled()) {
logger.trace("Creating new nested " + getClass().getSimpleName() + " for property '" + canonicalName + "'");
}
@@ -869,32 +874,33 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
// Inherit all type-specific PropertyEditors.
copyDefaultEditorsTo(nestedPa);
copyCustomEditorsTo(nestedPa, canonicalName);
nestedAccessors.put(canonicalName, nestedPa);
nestedAccessors.put(segment, nestedPa);
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Using cached nested property accessor for property '" + canonicalName + "'");
logger.trace("Using cached nested property accessor for property '" + segment.toCanonicalName() + "'");
}
}
return nestedPa;
}
private Object setDefaultValue(PropertyTokenHolder tokens) {
PropertyValue pv = createDefaultPropertyValue(tokens);
setPropertyValue(tokens, pv);
Object defaultValue = getPropertyValue(tokens);
private Object setDefaultValue(PropertyPath.Segment segment) {
PropertyValue pv = createDefaultPropertyValue(segment);
setPropertyValue(segment, pv);
Object defaultValue = getPropertyValue(segment);
Assert.state(defaultValue != null, "Default value must not be null");
return defaultValue;
}
private PropertyValue createDefaultPropertyValue(PropertyTokenHolder tokens) {
TypeDescriptor desc = getPropertyTypeDescriptor(tokens.canonicalName);
private PropertyValue createDefaultPropertyValue(PropertyPath.Segment segment) {
String canonicalName = segment.toCanonicalName();
TypeDescriptor desc = getPropertyTypeDescriptor(canonicalName);
if (desc == null) {
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + tokens.canonicalName,
throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + canonicalName,
"Could not determine property type for auto-growing a default value");
}
Object defaultValue = newValue(desc.getType(), desc, tokens.canonicalName);
return new PropertyValue(tokens.canonicalName, defaultValue);
Object defaultValue = newValue(desc.getType(), desc, canonicalName);
return new PropertyValue(canonicalName, defaultValue);
}
private Object newValue(Class<?> type, @Nullable TypeDescriptor desc, String name) {
@@ -942,70 +948,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
}
/**
* Parse the given property name into the corresponding property name tokens.
* @param propertyName the property name to parse
* @return representation of the parsed property tokens
*/
private PropertyTokenHolder getPropertyNameTokens(String propertyName) {
String actualName = null;
List<String> keys = new ArrayList<>(2);
int searchIndex = 0;
while (searchIndex != -1) {
int keyStart = propertyName.indexOf(PROPERTY_KEY_PREFIX, searchIndex);
searchIndex = -1;
if (keyStart != -1) {
int keyEnd = getPropertyNameKeyEnd(propertyName, keyStart + PROPERTY_KEY_PREFIX.length());
if (keyEnd != -1) {
if (actualName == null) {
actualName = propertyName.substring(0, keyStart);
}
String key = propertyName.substring(keyStart + PROPERTY_KEY_PREFIX.length(), keyEnd);
if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) ||
(key.startsWith("\"") && key.endsWith("\"")))) {
key = key.substring(1, key.length() - 1);
}
keys.add(key);
searchIndex = keyEnd + PROPERTY_KEY_SUFFIX.length();
}
}
}
PropertyTokenHolder tokens = new PropertyTokenHolder(actualName != null ? actualName : propertyName);
if (!keys.isEmpty()) {
tokens.canonicalName += PROPERTY_KEY_PREFIX +
StringUtils.collectionToDelimitedString(keys, PROPERTY_KEY_SUFFIX + PROPERTY_KEY_PREFIX) +
PROPERTY_KEY_SUFFIX;
tokens.keys = StringUtils.toStringArray(keys);
}
return tokens;
}
private int getPropertyNameKeyEnd(String propertyName, int startIndex) {
int unclosedPrefixes = 0;
int length = propertyName.length();
for (int i = startIndex; i < length; i++) {
switch (propertyName.charAt(i)) {
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> {
// The property name contains opening prefix(es)...
unclosedPrefixes++;
}
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
if (unclosedPrefixes == 0) {
// No unclosed prefix(es) in the property name (left) ->
// this is the suffix we are looking for.
return i;
}
else {
// This suffix does not close the initial prefix but rather
// just one that occurred within the property name.
unclosedPrefixes--;
}
}
}
}
return -1;
}
@Override
public String toString() {
String className = getClass().getName();
@@ -1016,6 +958,17 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
/**
* The result of resolving a property path: the accessor owning its final
* segment, and that segment itself.
* @since 7.1
* @param accessor the accessor for the target bean
* @param segment the final segment of the resolved path (the property to
* actually get or set on {@code accessor})
*/
protected record ResolvedProperty(AbstractNestablePropertyAccessor accessor, PropertyPath.Segment segment) {}
/**
* A handler for a specific property.
*/
@@ -1072,22 +1025,4 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
}
/**
* Holder class used to store property tokens.
*/
protected static class PropertyTokenHolder {
public PropertyTokenHolder(String name) {
this.actualName = name;
this.canonicalName = name;
}
public String actualName;
public String canonicalName;
public String @Nullable [] keys;
}
}
@@ -23,13 +23,17 @@ import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* Abstract implementation of the {@link PropertyAccessor} interface.
* Provides base implementations of all convenience methods, with the
*
* <p>Provides base implementations of all convenience methods, with the
* implementation of actual property access left to subclasses.
*
* @author Juergen Hoeller
* @author Stephane Nicoll
* @author Sam Brannen
* @since 2.0
* @see #getPropertyValue
* @see #setPropertyValue
@@ -40,6 +44,10 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
private boolean autoGrowNestedPaths = false;
private int autoGrowCollectionLimit = Integer.MAX_VALUE;
private int maxNestedPathDepth = DEFAULT_MAX_NESTED_PATH_DEPTH;
boolean suppressNotWritablePropertyException = false;
@@ -63,6 +71,27 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
return this.autoGrowNestedPaths;
}
@Override
public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
}
@Override
public int getAutoGrowCollectionLimit() {
return this.autoGrowCollectionLimit;
}
@Override
public void setMaxNestedPathDepth(int maxNestedPathDepth) {
Assert.isTrue(maxNestedPathDepth >= 0, "'maxNestedPathDepth' must not be negative");
this.maxNestedPathDepth = maxNestedPathDepth;
}
@Override
public int getMaxNestedPathDepth() {
return this.maxNestedPathDepth;
}
@Override
public void setPropertyValue(PropertyValue pv) throws BeansException {
@@ -18,6 +18,7 @@ package org.springframework.beans;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.util.Objects;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
@@ -213,9 +214,17 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements
@Override
public PropertyDescriptor getPropertyDescriptor(String propertyName) throws InvalidPropertyException {
BeanWrapperImpl nestedBw = (BeanWrapperImpl) getPropertyAccessorForPropertyPath(propertyName);
String finalPath = getFinalPath(nestedBw, propertyName);
PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults().getPropertyDescriptor(finalPath);
ResolvedProperty resolved;
try {
resolved = resolvePropertyPath(propertyName);
}
catch (InvalidPropertyPathException ex) {
throw new InvalidPropertyException(
getRootClass(), getNestedPath() + propertyName, Objects.requireNonNull(ex.getMessage()), ex);
}
BeanWrapperImpl nestedBw = (BeanWrapperImpl) resolved.accessor();
PropertyDescriptor pd = nestedBw.getCachedIntrospectionResults()
.getPropertyDescriptor(resolved.segment().toCanonicalName());
if (pd == null) {
throw new InvalidPropertyException(getRootClass(), getNestedPath() + propertyName,
"No property '" + propertyName + "' found");
@@ -21,19 +21,39 @@ import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.ConversionService;
/**
* Interface that encapsulates configuration methods for a PropertyAccessor.
* Also extends the PropertyEditorRegistry interface, which defines methods
* for PropertyEditor management.
* Interface that encapsulates configuration methods for a {@link PropertyAccessor}.
*
* <p>Also extends the {@link PropertyEditorRegistry} interface, which defines methods
* for {@link java.beans.PropertyEditor} management.
*
* <p>Serves as base interface for {@link BeanWrapper}.
*
* @author Juergen Hoeller
* @author Stephane Nicoll
* @author Sam Brannen
* @since 2.0
* @see BeanWrapper
*/
public interface ConfigurablePropertyAccessor extends PropertyAccessor, PropertyEditorRegistry, TypeConverter {
/**
* Default maximum nesting depth permitted for a nested property path: {@value}.
* <p>This limit guards against deeply nested property paths that could otherwise
* drive the recursive resolution of a nested property path to exhaust the current
* thread's call stack.
* <p><strong>NOTE</strong>: This limit improves diagnostics for the common case
* by converting what would otherwise be an opaque {@link StackOverflowError}
* into a descriptive {@link InvalidPropertyException}, but it is <em>not</em>
* a guaranteed defense against {@code StackOverflowError} under every possible
* JVM thread stack size configuration. The amount of stack space consumed per
* level of nesting depends on the JVM, its current JIT compilation state, and
* the platform.
* @since 7.1
* @see #setMaxNestedPathDepth(int)
*/
int DEFAULT_MAX_NESTED_PATH_DEPTH = 100;
/**
* Specify a {@link ConversionService} to use for converting
* property values, as an alternative to JavaBeans PropertyEditors.
@@ -74,4 +94,36 @@ public interface ConfigurablePropertyAccessor extends PropertyAccessor, Property
*/
boolean isAutoGrowNestedPaths();
/**
* Specify a limit for array and collection auto-growing.
* <p>Default is unlimited on a plain accessor.
* @since 7.1
*/
void setAutoGrowCollectionLimit(int autoGrowCollectionLimit);
/**
* Return the limit for array and collection auto-growing.
* @since 7.1
*/
int getAutoGrowCollectionLimit();
/**
* Specify the maximum nesting depth permitted for a nested property path.
* <p>The nesting depth corresponds to the number of intermediate properties
* traversed to reach the final property &mdash; for example,
* {@code "address.country.name"} has a nesting depth of 2.
* <p>Specify {@code 0} to disable nested property paths altogether, while
* still allowing simple, indexed, and mapped property access.
* <p>Default is {@link #DEFAULT_MAX_NESTED_PATH_DEPTH}.
* @param maxNestedPathDepth the maximum nesting depth; must not be negative
* @since 7.1
*/
void setMaxNestedPathDepth(int maxNestedPathDepth);
/**
* Return the maximum nesting depth permitted for a nested property path.
* @since 7.1
*/
int getMaxNestedPathDepth();
}
@@ -0,0 +1,98 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.PropertyChangeEvent;
import java.util.Objects;
import org.jspecify.annotations.Nullable;
/**
* Exception thrown when a property path is not a well-formed property path
* according to the grammar implemented by {@link PropertyPath}.
*
* <p>This signals a syntactically invalid path, as opposed to a
* syntactically valid path that happens not to resolve against a particular
* target object. The latter cases are reported as {@link NotReadablePropertyException}
* or {@link NotWritablePropertyException} instead.
*
* <p>Extends {@link PropertyAccessException} so that a malformed path
* encountered while binding a single property value can be collected into a
* {@link PropertyBatchUpdateException} alongside other per-property failures,
* rather than aborting the whole binding operation.
*
* @author Brian Clozel
* @since 7.1
* @see PropertyPath#parse(String)
*/
@SuppressWarnings("serial")
public class InvalidPropertyPathException extends PropertyAccessException {
/**
* Error code that an {@code InvalidPropertyPathException} is registered with.
*/
public static final String ERROR_CODE = "invalidPropertyPath";
private final String propertyPath;
/**
* Create a new {@code InvalidPropertyPathException}.
* @param propertyPath the offending property path
* @param reason a description of the grammar rule that was violated
*/
public InvalidPropertyPathException(String propertyPath, String reason) {
super("Invalid property path '" + propertyPath + "': " + reason, null);
this.propertyPath = propertyPath;
}
/**
* Create a new {@code InvalidPropertyPathException}.
* @param propertyChangeEvent the event for the property
* @param cause the original parsing exception
*/
public InvalidPropertyPathException(PropertyChangeEvent propertyChangeEvent, InvalidPropertyPathException cause) {
super(propertyChangeEvent, Objects.requireNonNull(cause.getMessage()), cause);
this.propertyPath = cause.propertyPath;
}
/**
* Create a new {@code InvalidPropertyPathException}.
* @param source the bean that fired the event
* @param propertyName the programmatic name of the property that was changed
* @param newValue the new value of the property
* @param cause the original parsing exception
*/
public InvalidPropertyPathException(Object source, String propertyName, @Nullable Object newValue, InvalidPropertyPathException cause) {
this(new PropertyChangeEvent(source, propertyName, null, newValue), cause);
}
/**
* Return the offending property path.
*/
public String getPropertyPath() {
return this.propertyPath;
}
@Override
public String getErrorCode() {
return ERROR_CODE;
}
}
@@ -23,8 +23,19 @@ import org.jspecify.annotations.Nullable;
* according to the {@link PropertyAccessor} interface.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @since 1.2.6
* @deprecated as of 7.1, in favor of {@link PropertyPath}. Use
* {@link PropertyPath#parse(String)} and {@link PropertyPath#canonicalName()}
* in place of {@link #canonicalPropertyName} and {@link #canonicalPropertyNames},
* and match a registered path against a property by comparing canonical names
* in place of {@link #matchesProperty}. There is no direct replacement for
* {@link #getPropertyName}, {@link #isNestedOrIndexedProperty},
* {@link #getFirstNestedPropertySeparatorIndex}, or
* {@link #getLastNestedPropertySeparatorIndex}, which operate on raw,
* unparsed property path text.
*/
@Deprecated(since = "7.1", forRemoval = true)
public abstract class PropertyAccessorUtils {
/**
@@ -81,21 +92,23 @@ public abstract class PropertyAccessorUtils {
/**
* Determine the first (or last) nested property separator in the
* given property path, ignoring dots in keys (like "map[my.key]").
* <p>Also tracks bracket nesting depth so that keys containing an unbalanced
* number of {@code [} or {@code ]} characters do not interfere with the
* detection of separators that precede or follow the key.
* @param propertyPath the property path to check
* @param last whether to return the last separator rather than the first
* @return the index of the nested property separator, or -1 if none
*/
private static int getNestedPropertySeparatorIndex(String propertyPath, boolean last) {
boolean inKey = false;
int depth = 0;
int length = propertyPath.length();
int i = (last ? length - 1 : 0);
while (last ? i >= 0 : i < length) {
switch (propertyPath.charAt(i)) {
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR, PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
inKey = !inKey;
}
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> depth += (last ? -1 : 1);
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> depth += (last ? 1 : -1);
case PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR -> {
if (!inKey) {
if (depth <= 0) {
return i;
}
}
@@ -118,17 +131,21 @@ public abstract class PropertyAccessorUtils {
* @return whether the paths match
*/
public static boolean matchesProperty(String registeredPath, String propertyPath) {
if (!registeredPath.startsWith(propertyPath)) {
// canonicalPropertyName, not PropertyPath.parse directly: this method's
// long-standing contract is non-throwing, even for a malformed path.
String registered = canonicalPropertyName(registeredPath);
String property = canonicalPropertyName(propertyPath);
if (!registered.startsWith(property)) {
return false;
}
if (registeredPath.length() == propertyPath.length()) {
if (registered.length() == property.length()) {
return true;
}
if (registeredPath.charAt(propertyPath.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
if (registered.charAt(property.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
return false;
}
return (registeredPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, propertyPath.length() + 1) ==
registeredPath.length() - 1);
return (registered.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, property.length() + 1) ==
registered.length() - 1);
}
/**
@@ -140,31 +157,7 @@ public abstract class PropertyAccessorUtils {
* @return the canonical representation of the property path
*/
public static String canonicalPropertyName(@Nullable String propertyName) {
if (propertyName == null) {
return "";
}
StringBuilder sb = new StringBuilder(propertyName);
int searchIndex = 0;
while (searchIndex != -1) {
int keyStart = sb.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX, searchIndex);
searchIndex = -1;
if (keyStart != -1) {
int keyEnd = sb.indexOf(
PropertyAccessor.PROPERTY_KEY_SUFFIX, keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length());
if (keyEnd != -1) {
String key = sb.substring(keyStart + PropertyAccessor.PROPERTY_KEY_PREFIX.length(), keyEnd);
if (key.length() > 1 && ((key.startsWith("'") && key.endsWith("'")) ||
(key.startsWith("\"") && key.endsWith("\"")))) {
sb.delete(keyStart + 1, keyStart + 2);
sb.delete(keyEnd - 2, keyEnd - 1);
keyEnd = keyEnd - 2;
}
searchIndex = keyEnd + PropertyAccessor.PROPERTY_KEY_SUFFIX.length();
}
}
}
return sb.toString();
return PropertyPath.canonicalNameOrOriginal(propertyName);
}
/**
@@ -19,6 +19,7 @@ package org.springframework.beans;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Enumeration;
@@ -50,6 +51,12 @@ abstract class PropertyDescriptorUtils {
* <p>This just supports the basic JavaBeans conventions, without indexed
* properties or any customizers, and without other BeanInfo metadata.
* For standard JavaBeans introspection, use the JavaBeans Introspector.
* <p>Note that, in contrast to the standard {@link java.beans.Introspector},
* this method does support a static {@code set} method as the write method
* for a property, resulting in a write-only property if no corresponding
* instance {@code get}/{@code is} method is present. Static {@code get} and
* {@code is} methods, on the other hand, are never considered read methods
* for a property, aligning with standard JavaBeans introspection.
* @param beanClass the target class to introspect
* @return a collection of property descriptors
* @throws IntrospectionException from introspecting the given bean class
@@ -71,11 +78,13 @@ abstract class PropertyDescriptorUtils {
setter = true;
nameIndex = 3;
}
else if (methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) {
else if (methodName.startsWith("get") && method.getParameterCount() == 0 &&
method.getReturnType() != void.class && !Modifier.isStatic(method.getModifiers())) {
setter = false;
nameIndex = 3;
}
else if (methodName.startsWith("is") && method.getParameterCount() == 0 && method.getReturnType() == boolean.class) {
else if (methodName.startsWith("is") && method.getParameterCount() == 0 &&
method.getReturnType() == boolean.class && !Modifier.isStatic(method.getModifiers())) {
setter = false;
nameIndex = 2;
}
@@ -79,12 +79,15 @@ import org.springframework.util.ClassUtils;
/**
* Base implementation of the {@link PropertyEditorRegistry} interface.
* Provides management of default editors and custom editors.
* Mainly serves as base class for {@link BeanWrapperImpl}.
*
* <p>Provides management of default editors and custom editors.
*
* <p>Mainly serves as base class for {@link BeanWrapperImpl}.
*
* @author Juergen Hoeller
* @author Rob Harrop
* @author Sebastien Deleuze
* @author Sam Brannen
* @since 1.2.6
* @see java.beans.PropertyEditorManager
* @see java.beans.PropertyEditorSupport#setAsText
@@ -92,6 +95,15 @@ import org.springframework.util.ClassUtils;
*/
public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
/**
* The maximum number of {@code [key]} segments that {@link #addStrippedPropertyPaths}
* will process in a single property path, to avoid excessive recursion and the
* resulting exponential blowup in the number of generated stripped paths for
* property paths with a large number of {@code [key]} segments.
* @since 7.1
*/
private static final int MAX_STRIPPED_PROPERTY_PATH_DEPTH = 8;
private @Nullable ConversionService conversionService;
private boolean defaultEditorsActive = false;
@@ -100,8 +112,7 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
private @Nullable PropertyEditorRegistrar defaultEditorRegistrar;
@SuppressWarnings("NullAway.Init")
private Map<Class<?>, PropertyEditor> defaultEditors;
private @Nullable Map<Class<?>, PropertyEditor> defaultEditors;
private @Nullable Map<Class<?>, PropertyEditor> overriddenDefaultEditors;
@@ -201,7 +212,7 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
}
}
if (this.defaultEditors == null) {
createDefaultEditors();
this.defaultEditors = createDefaultEditors();
}
return this.defaultEditors.get(requiredType);
}
@@ -209,75 +220,77 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
/**
* Actually register the default editors for this registry instance.
*/
private void createDefaultEditors() {
this.defaultEditors = new HashMap<>(64);
private Map<Class<?>, PropertyEditor> createDefaultEditors() {
Map<Class<?>, PropertyEditor> defaultEditors = new HashMap<>(64);
// Simple editors, without parameterization capabilities.
// The JDK does not contain a default editor for any of these target types.
this.defaultEditors.put(Charset.class, new CharsetEditor());
this.defaultEditors.put(Class.class, new ClassEditor());
this.defaultEditors.put(Class[].class, new ClassArrayEditor());
this.defaultEditors.put(Currency.class, new CurrencyEditor());
this.defaultEditors.put(File.class, new FileEditor());
this.defaultEditors.put(InputStream.class, new InputStreamEditor());
this.defaultEditors.put(InputSource.class, new InputSourceEditor());
this.defaultEditors.put(Locale.class, new LocaleEditor());
this.defaultEditors.put(Path.class, new PathEditor());
this.defaultEditors.put(Pattern.class, new PatternEditor());
this.defaultEditors.put(Properties.class, new PropertiesEditor());
this.defaultEditors.put(Reader.class, new ReaderEditor());
this.defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
this.defaultEditors.put(TimeZone.class, new TimeZoneEditor());
this.defaultEditors.put(URI.class, new URIEditor());
this.defaultEditors.put(URL.class, new URLEditor());
this.defaultEditors.put(UUID.class, new UUIDEditor());
this.defaultEditors.put(ZoneId.class, new ZoneIdEditor());
defaultEditors.put(Charset.class, new CharsetEditor());
defaultEditors.put(Class.class, new ClassEditor());
defaultEditors.put(Class[].class, new ClassArrayEditor());
defaultEditors.put(Currency.class, new CurrencyEditor());
defaultEditors.put(File.class, new FileEditor());
defaultEditors.put(InputStream.class, new InputStreamEditor());
defaultEditors.put(InputSource.class, new InputSourceEditor());
defaultEditors.put(Locale.class, new LocaleEditor());
defaultEditors.put(Path.class, new PathEditor());
defaultEditors.put(Pattern.class, new PatternEditor());
defaultEditors.put(Properties.class, new PropertiesEditor());
defaultEditors.put(Reader.class, new ReaderEditor());
defaultEditors.put(Resource[].class, new ResourceArrayPropertyEditor());
defaultEditors.put(TimeZone.class, new TimeZoneEditor());
defaultEditors.put(URI.class, new URIEditor());
defaultEditors.put(URL.class, new URLEditor());
defaultEditors.put(UUID.class, new UUIDEditor());
defaultEditors.put(ZoneId.class, new ZoneIdEditor());
// Default instances of collection editors.
// Can be overridden by registering custom instances of those as custom editors.
this.defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
this.defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
this.defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
this.defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
this.defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));
defaultEditors.put(Collection.class, new CustomCollectionEditor(Collection.class));
defaultEditors.put(Set.class, new CustomCollectionEditor(Set.class));
defaultEditors.put(SortedSet.class, new CustomCollectionEditor(SortedSet.class));
defaultEditors.put(List.class, new CustomCollectionEditor(List.class));
defaultEditors.put(SortedMap.class, new CustomMapEditor(SortedMap.class));
// Default editors for primitive arrays.
this.defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
this.defaultEditors.put(char[].class, new CharArrayPropertyEditor());
defaultEditors.put(byte[].class, new ByteArrayPropertyEditor());
defaultEditors.put(char[].class, new CharArrayPropertyEditor());
// The JDK does not contain a default editor for char!
this.defaultEditors.put(char.class, new CharacterEditor(false));
this.defaultEditors.put(Character.class, new CharacterEditor(true));
defaultEditors.put(char.class, new CharacterEditor(false));
defaultEditors.put(Character.class, new CharacterEditor(true));
// Spring's CustomBooleanEditor accepts more flag values than the JDK's default editor.
this.defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
this.defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));
defaultEditors.put(boolean.class, new CustomBooleanEditor(false));
defaultEditors.put(Boolean.class, new CustomBooleanEditor(true));
// The JDK does not contain default editors for number wrapper types!
// Override JDK primitive number editors with our own CustomNumberEditor.
this.defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
this.defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
this.defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
this.defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
this.defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
this.defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
this.defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
this.defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
this.defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
this.defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
this.defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
this.defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
this.defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
this.defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));
defaultEditors.put(byte.class, new CustomNumberEditor(Byte.class, false));
defaultEditors.put(Byte.class, new CustomNumberEditor(Byte.class, true));
defaultEditors.put(short.class, new CustomNumberEditor(Short.class, false));
defaultEditors.put(Short.class, new CustomNumberEditor(Short.class, true));
defaultEditors.put(int.class, new CustomNumberEditor(Integer.class, false));
defaultEditors.put(Integer.class, new CustomNumberEditor(Integer.class, true));
defaultEditors.put(long.class, new CustomNumberEditor(Long.class, false));
defaultEditors.put(Long.class, new CustomNumberEditor(Long.class, true));
defaultEditors.put(float.class, new CustomNumberEditor(Float.class, false));
defaultEditors.put(Float.class, new CustomNumberEditor(Float.class, true));
defaultEditors.put(double.class, new CustomNumberEditor(Double.class, false));
defaultEditors.put(Double.class, new CustomNumberEditor(Double.class, true));
defaultEditors.put(BigDecimal.class, new CustomNumberEditor(BigDecimal.class, true));
defaultEditors.put(BigInteger.class, new CustomNumberEditor(BigInteger.class, true));
// Only register config value editors if explicitly requested.
if (this.configValueEditorsActive) {
StringArrayPropertyEditor sae = new StringArrayPropertyEditor();
this.defaultEditors.put(String[].class, sae);
this.defaultEditors.put(short[].class, sae);
this.defaultEditors.put(int[].class, sae);
this.defaultEditors.put(long[].class, sae);
defaultEditors.put(String[].class, sae);
defaultEditors.put(short[].class, sae);
defaultEditors.put(int[].class, sae);
defaultEditors.put(long[].class, sae);
}
return defaultEditors;
}
/**
@@ -360,7 +373,7 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
public boolean hasCustomEditorForElement(@Nullable Class<?> elementType, @Nullable String propertyPath) {
if (propertyPath != null && this.customEditorsForPath != null) {
for (Map.Entry<String, CustomEditorHolder> entry : this.customEditorsForPath.entrySet()) {
if (PropertyAccessorUtils.matchesProperty(entry.getKey(), propertyPath) &&
if (matchesProperty(entry.getKey(), propertyPath) &&
entry.getValue().getPropertyEditor(elementType) != null) {
return true;
}
@@ -370,6 +383,26 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
return (elementType != null && this.customEditors != null && this.customEditors.containsKey(elementType));
}
/**
* Whether {@code registeredPath} is {@code propertyPath} itself or one
* indexed element of it.
*/
private static boolean matchesProperty(String registeredPath, String propertyPath) {
String canonicalRegisteredPath = PropertyPath.canonicalNameOrOriginal(registeredPath);
String canonicalPropertyPath = PropertyPath.canonicalNameOrOriginal(propertyPath);
if (!canonicalRegisteredPath.startsWith(canonicalPropertyPath)) {
return false;
}
if (canonicalRegisteredPath.length() == canonicalPropertyPath.length()) {
return true;
}
if (canonicalRegisteredPath.charAt(canonicalPropertyPath.length()) != PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
return false;
}
return (canonicalRegisteredPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR, canonicalPropertyPath.length() + 1) ==
canonicalRegisteredPath.length() - 1);
}
/**
* Determine the property type for the given property path.
* <p>Called by {@link #findCustomEditor} if no required type has been specified,
@@ -470,18 +503,25 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
* will be copied. If this is null, all editors will be copied.
*/
protected void copyCustomEditorsTo(PropertyEditorRegistry target, @Nullable String nestedProperty) {
String actualPropertyName =
(nestedProperty != null ? PropertyAccessorUtils.getPropertyName(nestedProperty) : null);
String actualPropertyName = (nestedProperty != null ? actualPropertyNameOf(nestedProperty) : null);
if (this.customEditors != null) {
this.customEditors.forEach(target::registerCustomEditor);
}
if (this.customEditorsForPath != null) {
this.customEditorsForPath.forEach((editorPath, editorHolder) -> {
if (nestedProperty != null) {
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(editorPath);
if (pos != -1) {
String editorNestedProperty = editorPath.substring(0, pos);
String editorNestedPath = editorPath.substring(pos + 1);
PropertyPath editorPropertyPath;
try {
editorPropertyPath = PropertyPath.parse(editorPath);
}
catch (InvalidPropertyPathException ex) {
// Not a well-formed path; nothing to nest into.
return;
}
List<PropertyPath.Segment> editorSegments = editorPropertyPath.segments();
if (editorSegments.size() > 1) {
String editorNestedProperty = editorSegments.get(0).toCanonicalName();
String editorNestedPath = editorPropertyPath.subPath(1).canonicalName();
if (editorNestedProperty.equals(nestedProperty) || editorNestedProperty.equals(actualPropertyName)) {
target.registerCustomEditor(
editorHolder.getRegisteredType(), editorNestedPath, editorHolder.getPropertyEditor());
@@ -496,15 +536,40 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
}
}
/**
* The raw name of {@code nestedProperty}'s single segment with its keys stripped,
* or {@code null} if malformed.
*/
private static @Nullable String actualPropertyNameOf(String nestedProperty) {
try {
List<PropertyPath.Segment> segments = PropertyPath.parse(nestedProperty).segments();
return (segments.size() == 1 ? segments.get(0).name() : null);
}
catch (InvalidPropertyPathException ex) {
return null;
}
}
/**
* Add property paths with all variations of stripped keys and/or indexes.
* Invokes itself recursively with nested paths.
* <p>Invokes itself recursively with nested paths, bounded to a nesting depth
* of {@link #MAX_STRIPPED_PROPERTY_PATH_DEPTH}.
* @param strippedPaths the result list to add to
* @param nestedPath the current nested path
* @param propertyPath the property path to check for keys/indexes to strip
*/
private void addStrippedPropertyPaths(List<String> strippedPaths, String nestedPath, String propertyPath) {
addStrippedPropertyPaths(strippedPaths, nestedPath, propertyPath, 0);
}
private void addStrippedPropertyPaths(
List<String> strippedPaths, String nestedPath, String propertyPath, int depth) {
if (depth >= MAX_STRIPPED_PROPERTY_PATH_DEPTH) {
// Avoid excessive recursion.
return;
}
int startIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
if (startIndex != -1) {
int endIndex = propertyPath.indexOf(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR);
@@ -515,9 +580,9 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
// Strip the first key.
strippedPaths.add(nestedPath + prefix + suffix);
// Search for further keys to strip, with the first key stripped.
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix);
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix, suffix, depth + 1);
// Search for further keys to strip, with the first key not stripped.
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix);
addStrippedPropertyPaths(strippedPaths, nestedPath + prefix + key, suffix, depth + 1);
}
}
}
@@ -0,0 +1,539 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* A parsed bean property path, such as {@code "person.addresses[1].city"}.
*
* <p>Parses bean property paths for property access: it decides whether a given
* string is a well-formed property path (see grammar) and where each path
* segment begins and ends. The {@linkplain #canonicalName() canonical form}
* is used for policy matching and error reporting.
* The {@linkplain #segments() structured segment list} is used for property
* navigation within beans.
*
* <p>The grammar is:
* <pre>
* PropertyPath := Segment ('.' Segment)*
* Segment := Name Index* -- Name may be empty only if at least one Index follows
* Name := char* excluding '.', '[', ']'
* Index := '[' Key ']'
* Key := QuotedKey | RawKey
* QuotedKey := "'" [^']* "'" | '"' [^"]* '"'
* RawKey := char* excluding quote characters, with balanced '[' / ']' nesting
* </pre>
*
* <p>Invalid property paths are rejected with {@link InvalidPropertyPathException}.
*
* @author Brian Clozel
* @since 7.1
*/
public final class PropertyPath {
private final String canonicalName;
private final List<Segment> segments;
private PropertyPath(String canonicalName, List<Segment> segments) {
this.canonicalName = canonicalName;
this.segments = segments;
}
/**
* Parse the given property path.
* @param path the property path to parse; an empty string is a valid path
* with no segments
* @return the parsed property path
* @throws InvalidPropertyPathException if the given path is not a
* well-formed property path
*/
public static PropertyPath parse(String path) throws InvalidPropertyPathException {
return parse(path, Options.UNLIMITED);
}
/**
* Parse the given property path, rejecting it if it exceeds the given {@code options}.
* @param path the property path to parse; an empty string is a valid path
* with no segments
* @param options the parsing options to apply
* @return the parsed property path
* @throws InvalidPropertyPathException if the given path is not a
* well-formed property path, or if it exceeds the given options
*/
public static PropertyPath parse(String path, Options options) throws InvalidPropertyPathException {
Assert.notNull(path, "Property path must not be null");
Assert.notNull(options, "Options must not be null");
if (path.isEmpty()) {
return new PropertyPath("", Collections.emptyList());
}
PropertyPath parsed = new Parser(path).parse();
int nestingDepth = parsed.segments.size() - 1;
if (nestingDepth > options.maxNestedPathDepth) {
throw new InvalidPropertyPathException(path,
"nesting depth exceeds the maximum of " + options.maxNestedPathDepth);
}
return parsed;
}
/**
* {@code path}'s canonical form, or {@code path} itself if it is not a
* well-formed property path (including a {@code null} path, for which
* this returns an empty string).
* <p>A convenience for callers with nothing better to fall back to than
* the original string, such as canonicalizing a user-supplied field name
* for display, comparison, or configuration matching, as opposed to
* {@link #parse(String)} itself, whose non-throwing behavior would be the
* wrong default for a caller that is about to navigate an object graph.
* @param path the property path to canonicalize, possibly {@code null}
* @return the canonical form of {@code path}, or {@code path} unchanged
* (or an empty string, if {@code path} is {@code null}) if it is not a
* well-formed property path
* @since 7.1
*/
public static String canonicalNameOrOriginal(@Nullable String path) {
if (path == null) {
return "";
}
try {
return parse(path).canonicalName();
}
catch (InvalidPropertyPathException ex) {
return path;
}
}
/**
* Return the canonical string form of this path.
* <p>Unnecessary surrounding quotes are removed from keys:
* {@code map['key'].name} &rarr; {@code map[key].name}.
*/
public String canonicalName() {
return this.canonicalName;
}
/**
* Return the segments of this path, in order, as an unmodifiable list.
*/
public List<Segment> segments() {
return this.segments;
}
/**
* Return the sub-path made up of this path's segments from the given
* index to the end, such as the {@code "country.name"} sub-path of
* {@code "address.country.name"} from index 1.
* @param fromIndex the index of the first segment to include (inclusive)
* @return the sub-path starting at {@code fromIndex}
* @throws IndexOutOfBoundsException if {@code fromIndex} is negative
* @throws IllegalArgumentException if {@code fromIndex} is greater than
* {@link #segments()}{@code .size()}
*/
public PropertyPath subPath(int fromIndex) {
if (fromIndex == 0) {
return this;
}
List<Segment> subSegments = this.segments.subList(fromIndex, this.segments.size());
StringBuilder subCanonicalName = new StringBuilder();
for (int i = 0; i < subSegments.size(); i++) {
if (i > 0) {
subCanonicalName.append(PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR);
}
subCanonicalName.append(subSegments.get(i).toCanonicalName());
}
return new PropertyPath(subCanonicalName.toString(), subSegments);
}
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof PropertyPath that &&
this.canonicalName.equals(that.canonicalName)));
}
@Override
public int hashCode() {
return this.canonicalName.hashCode();
}
@Override
public String toString() {
return this.canonicalName;
}
/**
* A dot-separated segment of a property path. It consists of a
* property name and the keys of any indexes applied to it, if the
* target property is an indexed collection.
* <p>For example, the path {@code "map[key].name"} has two segments:
* {@code Segment["map", ["key"]]} and {@code Segment["name", []]}.
* @param name the property name, which is empty only for a root-level
* indexed access such as {@code "[user]"}
* @param keys the keys of the indexes applied to the property, with any
* surrounding quotes removed; never {@code null}, possibly empty
*/
public record Segment(String name, List<String> keys) {
public Segment(String name, List<String> keys) {
Assert.notNull(name, "Segment name must not be null");
Assert.notNull(keys, "Segment keys must not be null");
this.name = name;
this.keys = List.copyOf(keys);
}
/**
* The canonical format of this segment alone: its name, followed by each
* key wrapped in brackets, quoted only when necessary.
*/
public String toCanonicalName() {
StringBuilder canonicalNameBuilder = new StringBuilder();
canonicalNameBuilder.append(this.name);
for (String key : this.keys) {
appendCanonicalKey(canonicalNameBuilder, key);
}
return canonicalNameBuilder.toString();
}
/**
* Return a new segment instance with the same name, but dropping the last key.
*/
public Segment withoutLastKey() {
if (this.keys.isEmpty()) {
return this;
}
return new Segment(this.name, this.keys.subList(0, this.keys.size() - 1));
}
/**
* Append the canonical form of the given key, which is the key
* unquoted, or the key re-quoted if it contains illegal raw chars,
* such as {@code map['a]b']}.
*/
private void appendCanonicalKey(StringBuilder canonicalNameBuilder, String key) {
char quoteChar = canonicalQuoteChar(key);
canonicalNameBuilder.append(PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR);
if (quoteChar != 0) {
canonicalNameBuilder.append(quoteChar);
}
canonicalNameBuilder.append(key);
if (quoteChar != 0) {
canonicalNameBuilder.append(quoteChar);
}
canonicalNameBuilder.append(PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR);
}
/**
* Determine the quote character to surround the given key with in the
* canonical name, or {@code 0} if the key needs no quoting.
*/
private static char canonicalQuoteChar(String key) {
int depth = 0;
boolean balanced = true;
boolean containsSingleQuote = false;
boolean containsDoubleQuote = false;
for (int i = 0; i < key.length(); i++) {
switch (key.charAt(i)) {
case PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR -> depth++;
case PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR -> {
if (--depth < 0) {
balanced = false;
}
}
case '\'' -> containsSingleQuote = true;
case '"' -> containsDoubleQuote = true;
default -> {
// Ordinary key character.
}
}
}
if (balanced && depth == 0 && !containsSingleQuote && !containsDoubleQuote) {
return 0;
}
return (containsSingleQuote && !containsDoubleQuote ? '"' : '\'');
}
}
/**
* Options to customize the parsing for {@link PropertyPath}.
*/
public static final class Options {
/**
* Options with no limit on nesting depth.
*/
public static final Options UNLIMITED = new Options(Integer.MAX_VALUE);
private final int maxNestedPathDepth;
private Options(int maxNestedPathDepth) {
this.maxNestedPathDepth = maxNestedPathDepth;
}
/**
* Create an {@link Options} instance that rejects a path whose nesting
* depth exceeds the given maximum.
* @param maxNestedPathDepth the maximum nesting depth
*/
public static Options withMaxNestedPathDepth(int maxNestedPathDepth) {
Assert.isTrue(maxNestedPathDepth >= 0, "'maxNestedPathDepth' must not be negative");
return new Options(maxNestedPathDepth);
}
}
/**
* Parser for a property path string.
* <p>Each stage of the enforced grammar is modeled by a separate {@link State}.
*/
private static final class Parser {
private final String path;
private final List<Segment> segments = new ArrayList<>(2);
private final StringBuilder canonicalName;
// Offset of the character currently being processed.
private int pos;
// Offset at which the current segment's name starts.
private int segmentStart;
// Offset at which the current segment's name ends, or -1 if not yet known.
private int segmentNameEnd = -1;
// Keys collected for the current segment
private @Nullable List<String> keys;
// Offset at which the current key starts.
private int keyStart;
// Bracket nesting depth within the current raw key.
private int depth;
// The quote character that opened the current quoted key.
private char quoteChar;
Parser(String path) {
this.path = path;
this.canonicalName = new StringBuilder(path.length());
}
PropertyPath parse() {
State state = State.NAME;
for (; this.pos < this.path.length(); this.pos++) {
state = state.process(this.path.charAt(this.pos), this);
}
state.onEof(this);
return new PropertyPath(this.canonicalName.toString(), Collections.unmodifiableList(this.segments));
}
private void addKey(String key) {
List<String> keys = this.keys;
if (keys == null) {
keys = new ArrayList<>(2);
this.keys = keys;
}
keys.add(key);
}
private void endSegment() {
String name = this.path.substring(this.segmentStart, this.segmentNameEnd);
List<String> keys = (this.keys != null ? this.keys : Collections.emptyList());
if (name.isEmpty() && keys.isEmpty()) {
throw new InvalidPropertyPathException(this.path,
"empty path segment (at position " + this.segmentStart + ")");
}
if (!this.segments.isEmpty()) {
this.canonicalName.append(PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR);
}
Segment segment = new Segment(name, keys);
this.segments.add(segment);
this.canonicalName.append(segment.toCanonicalName());
this.keys = null;
this.segmentNameEnd = -1;
}
private InvalidPropertyPathException error(String reason) {
return new InvalidPropertyPathException(this.path, reason + " (at position " + this.pos + ")");
}
private InvalidPropertyPathException unclosedIndex() {
return error("unclosed '" + PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR + "'");
}
}
private enum State {
// Reading a segment name, before any index of that segment
NAME {
@Override
State process(char ch, Parser parser) {
if (ch == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR) {
parser.segmentNameEnd = parser.pos;
parser.endSegment();
parser.segmentStart = parser.pos + 1;
return this;
}
if (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
parser.segmentNameEnd = parser.pos;
return INDEX_OPEN;
}
if (ch == PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
throw parser.error("unexpected '" + PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR +
"' without a matching '" + PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR + "'");
}
return this;
}
@Override
void onEof(Parser parser) {
parser.segmentNameEnd = parser.path.length();
parser.endSegment();
}
},
// Immediately after the "[" that opens an index
INDEX_OPEN {
@Override
State process(char ch, Parser parser) {
if (ch == PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
// An empty key, as in "map[]".
parser.addKey("");
return AFTER_INDEX;
}
if (ch == '\'' || ch == '"') {
parser.quoteChar = ch;
parser.keyStart = parser.pos + 1;
return QUOTED_KEY;
}
parser.keyStart = parser.pos;
// A key that itself opens a bracket level, as in "map[[a]]"
parser.depth = (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR ? 1 : 0);
return RAW_KEY;
}
@Override
void onEof(Parser parser) {
throw parser.unclosedIndex();
}
},
// Inside an unquoted key, tracking the depth of bracket nesting.
RAW_KEY {
@Override
State process(char ch, Parser parser) {
if (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
parser.depth++;
return this;
}
if (ch == PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
if (parser.depth == 0) {
parser.addKey(parser.path.substring(parser.keyStart, parser.pos));
return AFTER_INDEX;
}
parser.depth--;
return this;
}
if (ch == '\'' || ch == '"') {
throw parser.error("unexpected quote '" + ch + "' in an unquoted key; " +
"quote the whole key to use quote characters within it");
}
return this;
}
@Override
void onEof(Parser parser) {
throw parser.unclosedIndex();
}
},
// Inside a quoted key, looking for the closing quote.
QUOTED_KEY {
@Override
State process(char ch, Parser parser) {
if (ch == parser.quoteChar) {
parser.addKey(parser.path.substring(parser.keyStart, parser.pos));
return QUOTE_CLOSED;
}
return this;
}
@Override
void onEof(Parser parser) {
throw parser.error("unterminated quote '" + parser.quoteChar + "'");
}
},
// Immediately after a closing quote, only "]" is legal.
QUOTE_CLOSED {
@Override
State process(char ch, Parser parser) {
if (ch != PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR) {
throw parser.error("unexpected '" + ch + "' after a closing quote; expected '" +
PropertyAccessor.PROPERTY_KEY_SUFFIX_CHAR + "'");
}
return AFTER_INDEX;
}
@Override
void onEof(Parser parser) {
throw parser.unclosedIndex();
}
},
// Immediately after an index's "]" , only "." or "[" are legal.
AFTER_INDEX {
@Override
State process(char ch, Parser parser) {
if (ch == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR) {
parser.endSegment();
parser.segmentStart = parser.pos + 1;
return NAME;
}
if (ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
return INDEX_OPEN;
}
throw parser.error("unexpected '" + ch + "' after an index; expected '" +
PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR + "', '" +
PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR + "', or the end of the path");
}
@Override
void onEof(Parser parser) {
parser.endSegment();
}
};
abstract State process(char ch, Parser parser);
abstract void onEof(Parser parser);
}
}
@@ -100,6 +100,7 @@ import org.springframework.core.ResolvableType;
* @author Rod Johnson
* @author Juergen Hoeller
* @author Chris Beams
* @author Yanming Zhou
* @since 13 April 2001
* @see BeanNameAware#setBeanName
* @see BeanClassLoaderAware#setBeanClassLoader
@@ -175,6 +176,29 @@ public interface BeanFactory {
*/
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
/**
* Return an instance, which may be shared or independent, of the specified bean.
* <p>Behaves the same as {@link #getBean(String)}, but provides a measure of type
* safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the
* required type. This means that ClassCastException can't be thrown on casting
* the result correctly, as can happen with {@link #getBean(String)}.
* <p>Translates aliases back to the corresponding canonical bean name.
* <p>Will ask the parent factory if the bean cannot be found in this factory instance.
* @param name the name of the bean to retrieve
* @param typeReference the reference to obtain type the bean must match
* @return an instance of the bean.
* Note that the return value will never be {@code null}. In case of a stub for
* {@code null} from a factory method having been resolved for the requested bean, a
* {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised.
* Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies.
* @throws NoSuchBeanDefinitionException if there is no such bean definition
* @throws BeanNotOfRequiredTypeException if the bean is not of the required type
* @throws BeansException if the bean could not be created
* @since 7.1
* @see #getBean(String, Class)
*/
<T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException;
/**
* Return an instance, which may be shared or independent, of the specified bean.
* <p>Allows for specifying explicit constructor arguments / factory method arguments,
@@ -16,14 +16,17 @@
package org.springframework.beans.factory;
import java.lang.reflect.Type;
import org.springframework.beans.BeansException;
import org.springframework.util.ClassUtils;
import org.springframework.core.ResolvableType;
/**
* Thrown when a bean doesn't match the expected type.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @author Yanming Zhou
*/
@SuppressWarnings("serial")
public class BeanNotOfRequiredTypeException extends BeansException {
@@ -32,7 +35,7 @@ public class BeanNotOfRequiredTypeException extends BeansException {
private final String beanName;
/** The required type. */
private final Class<?> requiredType;
private final Type genericRequiredType;
/** The offending type. */
private final Class<?> actualType;
@@ -46,10 +49,22 @@ public class BeanNotOfRequiredTypeException extends BeansException {
* the expected type
*/
public BeanNotOfRequiredTypeException(String beanName, Class<?> requiredType, Class<?> actualType) {
super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) +
"' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'");
this(beanName, (Type) requiredType, actualType);
}
/**
* Create a new BeanNotOfRequiredTypeException.
* @param beanName the name of the bean requested
* @param requiredType the required type
* @param actualType the actual type returned, which did not match
* the expected type
* @since 7.1
*/
public BeanNotOfRequiredTypeException(String beanName, Type requiredType, Class<?> actualType) {
super("Bean named '" + beanName + "' is expected to be of type '" + requiredType.getTypeName() +
"' but was actually of type '" + actualType.getTypeName() + "'");
this.beanName = beanName;
this.requiredType = requiredType;
this.genericRequiredType = requiredType;
this.actualType = actualType;
}
@@ -65,7 +80,15 @@ public class BeanNotOfRequiredTypeException extends BeansException {
* Return the expected type for the bean.
*/
public Class<?> getRequiredType() {
return this.requiredType;
return (this.genericRequiredType instanceof Class<?> clazz ? clazz : ResolvableType.forType(this.genericRequiredType).toClass());
}
/**
* Return the expected generic type for the bean.
* @since 7.1
*/
public Type getGenericRequiredType() {
return this.genericRequiredType;
}
/**
@@ -19,21 +19,9 @@ package org.springframework.beans.factory;
import org.springframework.core.env.Environment;
/**
* Contract for registering beans programmatically, typically imported with an
* {@link org.springframework.context.annotation.Import @Import} annotation on
* a {@link org.springframework.context.annotation.Configuration @Configuration}
* class.
* <pre class="code">
* &#064;Configuration
* &#064;Import(MyBeanRegistrar.class)
* class MyConfiguration {
* }</pre>
* Can also be applied to an application context via
* {@link org.springframework.context.support.GenericApplicationContext#register(BeanRegistrar...)}.
* Contract for registering beans programmatically. Implementations use the
* {@link BeanRegistry} and {@link Environment} to register beans:
*
*
* <p>Bean registrar implementations use {@link BeanRegistry} and {@link Environment}
* APIs to register beans programmatically in a concise and flexible way.
* <pre class="code">
* class MyBeanRegistrar implements BeanRegistrar {
*
@@ -52,9 +40,55 @@ import org.springframework.core.env.Environment;
* }
* }</pre>
*
* <p>{@code BeanRegistrar} implementations are not Spring components: they must have
* a no-arg constructor and cannot rely on dependency injection or any other
* component-model feature. They can be used in two distinct ways depending on the
* application context setup.
*
* <h3>With the {@code @Configuration} model</h3>
*
* <p>A {@code BeanRegistrar} must be imported via
* {@link org.springframework.context.annotation.Import @Import} on a
* {@link org.springframework.context.annotation.Configuration @Configuration} class:
*
* <pre class="code">
* &#064;Configuration
* &#064;Import(MyBeanRegistrar.class)
* class MyConfiguration {
* }</pre>
*
* <p>This is the only mechanism that triggers bean registration in the annotation-based
* configuration model. Annotating an implementation with {@code @Configuration} or
* {@code @Component}, or returning an instance from a {@code @Bean} method, registers
* it as a bean but does <strong>not</strong> invoke its
* {@link #register(BeanRegistry, Environment) register} method.
*
* <p>When imported, the registrar is invoked in the order it is encountered during
* configuration class processing. It can therefore check for and build on beans that
* have already been defined, but has no visibility into beans that will be registered
* by classes processed later.
*
* <h3>Programmatic usage</h3>
*
* <p>A {@code BeanRegistrar} can also be applied directly to a
* {@link org.springframework.context.support.GenericApplicationContext}:
*
* <pre class="code">
* GenericApplicationContext context = new GenericApplicationContext();
* context.register(new MyBeanRegistrar());
* context.registerBean("myBean", MyBean.class);
* context.refresh();</pre>
*
* <p>This mode is primarily intended for fully programmatic application context setups.
* Registrars applied this way are invoked before any {@code @Configuration} class is
* processed. They can therefore observe beans registered programmatically (e.g., via
* one of the {@code GenericApplicationContext#registerBean} methods), but will
* <strong>not</strong> see any beans defined in {@code @Configuration} classes also
* registered with the context.
*
* <p>A {@code BeanRegistrar} implementing {@link org.springframework.context.annotation.ImportAware}
* can optionally introspect import metadata when used in an import scenario, otherwise the
* {@code setImportMetadata} method is simply not being called.
* can optionally introspect import metadata when used in an import scenario; otherwise
* the {@code setImportMetadata} method is not called.
*
* <p>In Kotlin, it is recommended to use {@code BeanRegistrarDsl} instead of
* implementing {@code BeanRegistrar}.
@@ -33,6 +33,7 @@ import org.springframework.core.env.Environment;
* programmatic bean registration capabilities.
*
* @author Sebastien Deleuze
* @author Juergen Hoeller
* @since 7.0
*/
public interface BeanRegistry {
@@ -140,6 +141,28 @@ public interface BeanRegistry {
*/
<T> void registerBean(String name, ParameterizedTypeReference<T> beanType, Consumer<Spec<T>> customizer);
/**
* Determine whether a bean of the given name is already registered.
* @param name the name of the bean
* @since 7.1
*/
boolean containsBean(String name);
/**
* Determine whether a bean of the given type is already registered.
* @param beanType the type of the bean
* @since 7.1
*/
boolean containsBean(Class<?> beanType);
/**
* Determine whether a bean of the given generics-containing type is
* already registered.
* @param beanType the generics-containing type of the bean
* @since 7.1
*/
<T> boolean containsBean(ParameterizedTypeReference<T> beanType);
/**
* Specification for customizing a bean.
@@ -92,16 +92,8 @@ public final class ParameterResolutionDelegate {
/**
* Resolve the dependency for the supplied {@link Parameter} from the
* supplied {@link AutowireCapableBeanFactory}.
* <p>Provides comprehensive autowiring support for individual method parameters
* on par with Spring's dependency injection facilities for autowired fields and
* methods, including support for {@link Autowired @Autowired},
* {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
* placeholders and SpEL expressions in {@code @Value} declarations.
* <p>The dependency is required unless the parameter is annotated or meta-annotated
* with {@link Autowired @Autowired} with the {@link Autowired#required required}
* flag set to {@code false}.
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
* will be used as the qualifier for resolving ambiguities.
* <p>See {@link #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)}
* for details.
* @param parameter the parameter whose dependency should be resolved (must not be
* {@code null})
* @param parameterIndex the index of the parameter in the constructor or method
@@ -113,13 +105,49 @@ public final class ParameterResolutionDelegate {
* the dependency (must not be {@code null})
* @return the resolved object, or {@code null} if none found
* @throws BeansException if dependency resolution failed
* @see #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)
*/
public static @Nullable Object resolveDependency(
Parameter parameter, int parameterIndex, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
throws BeansException {
return resolveDependency(parameter, parameterIndex, null, containingClass, beanFactory);
}
/**
* Resolve the dependency for the supplied {@link Parameter} from the
* supplied {@link AutowireCapableBeanFactory}.
* <p>Provides comprehensive autowiring support for individual method parameters
* on par with Spring's dependency injection facilities for autowired fields and
* methods, including support for {@link Autowired @Autowired},
* {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
* placeholders and SpEL expressions in {@code @Value} declarations.
* <p>The dependency is required unless the parameter is annotated or meta-annotated
* with {@link Autowired @Autowired} with the {@link Autowired#required required}
* flag set to {@code false}.
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
* (or a supplied custom name) will be used as the qualifier for resolving ambiguities.
* @param parameter the parameter whose dependency should be resolved (must not be
* {@code null})
* @param parameterIndex the index of the parameter in the constructor or method
* that declares the parameter
* @param parameterName a custom name for the parameter; or {@code null} to use
* the default parameter name discovery logic
* @param containingClass the concrete class that contains the parameter; this may
* differ from the class that declares the parameter in that it may be a subclass
* thereof, potentially substituting type variables (must not be {@code null})
* @param beanFactory the {@code AutowireCapableBeanFactory} from which to resolve
* the dependency (must not be {@code null})
* @return the resolved object, or {@code null} if none found
* @throws BeansException if dependency resolution failed
* @since 7.1
* @see #isAutowirable
* @see Autowired#required
* @see SynthesizingMethodParameter#forExecutable(Executable, int)
* @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)
*/
public static @Nullable Object resolveDependency(
Parameter parameter, int parameterIndex, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
public static @Nullable Object resolveDependency(Parameter parameter, int parameterIndex,
@Nullable String parameterName, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
throws BeansException {
Assert.notNull(parameter, "Parameter must not be null");
@@ -132,7 +160,7 @@ public final class ParameterResolutionDelegate {
MethodParameter methodParameter = SynthesizingMethodParameter.forExecutable(
parameter.getDeclaringExecutable(), parameterIndex);
DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required);
DependencyDescriptor descriptor = new NamedParameterDependencyDescriptor(methodParameter, required, parameterName);
descriptor.setContainingClass(containingClass);
return beanFactory.resolveDependency(descriptor, null);
}
@@ -171,4 +199,26 @@ public final class ParameterResolutionDelegate {
return parameter;
}
@SuppressWarnings("serial")
private static class NamedParameterDependencyDescriptor extends DependencyDescriptor {
private final @Nullable String parameterName;
NamedParameterDependencyDescriptor(MethodParameter methodParameter, boolean required, @Nullable String parameterName) {
super(methodParameter, required);
this.parameterName = parameterName;
}
@Override
public @Nullable String getDependencyName() {
return (this.parameterName != null ? this.parameterName : super.getDependencyName());
}
@Override
public boolean usesStandardBeanLookup() {
return true;
}
}
}
@@ -45,7 +45,7 @@ public interface AutowiredArguments {
Object value = getObject(index);
if (!ClassUtils.isAssignableValue(requiredType, value)) {
throw new IllegalArgumentException("Argument type mismatch: expected '" +
ClassUtils.getQualifiedName(requiredType) + "' for value [" + value + "]");
requiredType.getTypeName() + "' for value [" + value + "]");
}
return (T) value;
}
@@ -72,10 +72,7 @@ public abstract class AbstractFactoryBean<T>
private @Nullable BeanFactory beanFactory;
private boolean initialized = false;
@SuppressWarnings("NullAway.Init")
private T singletonInstance;
private @Nullable T singletonInstance;
private @Nullable T earlySingletonInstance;
@@ -134,7 +131,6 @@ public abstract class AbstractFactoryBean<T>
@Override
public void afterPropertiesSet() throws Exception {
if (isSingleton()) {
this.initialized = true;
this.singletonInstance = createInstance();
this.earlySingletonInstance = null;
}
@@ -149,7 +145,8 @@ public abstract class AbstractFactoryBean<T>
@Override
public final T getObject() throws Exception {
if (isSingleton()) {
return (this.initialized ? this.singletonInstance : getEarlySingletonInstance());
T instance = this.singletonInstance;
return (instance != null ? instance : getEarlySingletonInstance());
}
else {
return createInstance();
@@ -161,7 +158,7 @@ public abstract class AbstractFactoryBean<T>
* circular reference. Not called in a non-circular scenario.
*/
@SuppressWarnings("unchecked")
private T getEarlySingletonInstance() throws Exception {
private T getEarlySingletonInstance() {
Class<?>[] ifcs = getEarlySingletonInterfaces();
if (ifcs == null) {
throw new FactoryBeanNotInitializedException(
@@ -179,9 +176,10 @@ public abstract class AbstractFactoryBean<T>
* @return the singleton instance that this FactoryBean holds
* @throws IllegalStateException if the singleton instance is not initialized
*/
private @Nullable T getSingletonInstance() throws IllegalStateException {
Assert.state(this.initialized, "Singleton instance not initialized yet");
return this.singletonInstance;
private T getSingletonInstance() throws IllegalStateException {
T instance = this.singletonInstance;
Assert.state(instance != null, "Singleton instance not initialized yet");
return instance;
}
/**
@@ -191,7 +189,10 @@ public abstract class AbstractFactoryBean<T>
@Override
public void destroy() throws Exception {
if (isSingleton()) {
destroyInstance(this.singletonInstance);
T instance = this.singletonInstance;
if (instance != null) {
destroyInstance(instance);
}
}
}
@@ -241,7 +242,7 @@ public abstract class AbstractFactoryBean<T>
* @throws Exception in case of shutdown errors
* @see #createInstance()
*/
protected void destroyInstance(@Nullable T instance) throws Exception {
protected void destroyInstance(T instance) throws Exception {
}
@@ -260,7 +261,7 @@ public abstract class AbstractFactoryBean<T>
// Use hashCode of reference proxy.
return System.identityHashCode(proxy);
}
else if (!initialized && ReflectionUtils.isToStringMethod(method)) {
else if (ReflectionUtils.isToStringMethod(method) && singletonInstance == null) {
return "Early singleton proxy for interfaces " +
ObjectUtils.nullSafeToString(getEarlySingletonInterfaces());
}
@@ -89,15 +89,13 @@ public class PropertyPathFactoryBean implements FactoryBean<Object>, BeanNameAwa
private @Nullable BeanWrapper targetBeanWrapper;
@SuppressWarnings("NullAway.Init")
private String targetBeanName;
private @Nullable String targetBeanName;
private @Nullable String propertyPath;
private @Nullable Class<?> resultType;
@SuppressWarnings("NullAway.Init")
private String beanName;
private @Nullable String beanName;
private @Nullable BeanFactory beanFactory;
@@ -160,25 +158,27 @@ public class PropertyPathFactoryBean implements FactoryBean<Object>, BeanNameAwa
@Override
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
String targetBeanName = this.targetBeanName;
if (this.targetBeanWrapper != null && this.targetBeanName != null) {
if (this.targetBeanWrapper != null && targetBeanName != null) {
throw new IllegalArgumentException("Specify either 'targetObject' or 'targetBeanName', not both");
}
if (this.targetBeanWrapper == null && this.targetBeanName == null) {
if (this.targetBeanWrapper == null && targetBeanName == null) {
if (this.propertyPath != null) {
throw new IllegalArgumentException(
"Specify 'targetObject' or 'targetBeanName' in combination with 'propertyPath'");
}
// No other properties specified: check bean name.
int dotIndex = (this.beanName != null ? this.beanName.indexOf('.') : -1);
if (dotIndex == -1) {
int dotIndex;
if (this.beanName == null || (dotIndex = this.beanName.indexOf('.')) <= 0) {
throw new IllegalArgumentException(
"Neither 'targetObject' nor 'targetBeanName' specified, and PropertyPathFactoryBean " +
"bean name '" + this.beanName + "' does not follow 'beanName.property' syntax");
}
this.targetBeanName = this.beanName.substring(0, dotIndex);
targetBeanName = this.beanName.substring(0, dotIndex);
this.targetBeanName = targetBeanName;
this.propertyPath = this.beanName.substring(dotIndex + 1);
}
@@ -187,9 +187,10 @@ public class PropertyPathFactoryBean implements FactoryBean<Object>, BeanNameAwa
throw new IllegalArgumentException("'propertyPath' is required");
}
if (this.targetBeanWrapper == null && this.beanFactory.isSingleton(this.targetBeanName)) {
if (this.targetBeanWrapper == null && StringUtils.hasLength(targetBeanName) &&
this.beanFactory.isSingleton(targetBeanName)) {
// Eagerly fetch singleton target bean, and determine result type.
Object bean = this.beanFactory.getBean(this.targetBeanName);
Object bean = this.beanFactory.getBean(targetBeanName);
this.targetBeanWrapper = PropertyAccessorFactory.forBeanPropertyAccess(bean);
this.resultType = this.targetBeanWrapper.getPropertyType(this.propertyPath);
}
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import org.apache.commons.logging.Log;
@@ -194,30 +195,31 @@ public abstract class YamlProcessor {
}
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
int count = 0;
AtomicInteger count = new AtomicInteger();
try {
if (logger.isDebugEnabled()) {
logger.debug("Loading from YAML: " + resource);
}
try (Reader reader = new UnicodeReader(resource.getInputStream())) {
resource.consumeContent(inputStream -> {
Reader reader = new UnicodeReader(inputStream);
for (Object object : yaml.loadAll(reader)) {
if (object != null && process(asMap(object), callback)) {
count++;
count.incrementAndGet();
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
break;
}
}
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
logger.debug("Loaded " + count + " document" + (count.get() > 1 ? "s" : "") +
" from YAML resource: " + resource);
}
}
});
}
catch (IOException ex) {
handleProcessError(resource, ex);
}
return (count > 0);
return (count.get() > 0);
}
private void handleProcessError(Resource resource, IOException ex) {
@@ -42,7 +42,7 @@ import org.springframework.beans.BeanWrapperImpl;
import org.springframework.beans.BeansException;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyAccessorUtils;
import org.springframework.beans.PropertyAccessor;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.TypeConverter;
@@ -1751,7 +1751,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
*/
private boolean isConvertibleProperty(String propertyName, BeanWrapper bw) {
try {
return !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName) &&
return !isNestedOrIndexedProperty(propertyName) &&
BeanUtils.hasUniqueWriteMethod(bw.getPropertyDescriptor(propertyName));
}
catch (InvalidPropertyException ex) {
@@ -1759,6 +1759,19 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
}
}
/**
* Check whether the given property path indicates an indexed or nested property.
*/
private static boolean isNestedOrIndexedProperty(String propertyName) {
for (int i = 0; i < propertyName.length(); i++) {
char ch = propertyName.charAt(i);
if (ch == PropertyAccessor.NESTED_PROPERTY_SEPARATOR_CHAR || ch == PropertyAccessor.PROPERTY_KEY_PREFIX_CHAR) {
return true;
}
}
return false;
}
/**
* Convert the given value for the specified target property.
*/
@@ -17,6 +17,7 @@
package org.springframework.beans.factory.support;
import java.beans.PropertyEditor;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@@ -66,6 +67,7 @@ import org.springframework.beans.factory.config.Scope;
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
import org.springframework.core.DecoratingClassLoader;
import org.springframework.core.NamedThreadLocal;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.log.LogMessage;
@@ -201,6 +203,17 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
return doGetBean(name, requiredType, null, false);
}
@Override
@SuppressWarnings("unchecked")
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
Object bean = getBean(name);
Type requiredType = typeReference.getType();
if (!isTypeMatch(name, ResolvableType.forType(requiredType), true)) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return (T) bean;
}
@Override
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
return doGetBean(name, null, args, false);
@@ -413,7 +426,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
catch (TypeMismatchException ex) {
if (logger.isTraceEnabled()) {
logger.trace("Failed to convert bean '" + name + "' to required type '" +
ClassUtils.getQualifiedName(requiredType) + "'", ex);
requiredType.getTypeName() + "'", ex);
}
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
@@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.BeanRegistry;
import org.springframework.beans.factory.ListableBeanFactory;
@@ -176,6 +177,22 @@ public class BeanRegistryAdapter implements BeanRegistry {
this.beanRegistry.registerBeanDefinition(name, beanDefinition);
}
@Override
public boolean containsBean(String name) {
return this.beanFactory.containsBean(name);
}
@Override
public boolean containsBean(Class<?> beanType) {
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanType).length > 0;
}
@Override
public <T> boolean containsBean(ParameterizedTypeReference<T> beanType) {
ResolvableType resolvableType = ResolvableType.forType(beanType);
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, resolvableType).length > 0;
}
/**
* {@link RootBeanDefinition} subclass for {@code #registerBean} based
@@ -17,7 +17,6 @@
package org.springframework.beans.factory.support;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Enumeration;
import java.util.HashMap;
@@ -256,14 +255,14 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader
Properties props = new Properties();
try {
try (InputStream is = encodedResource.getResource().getInputStream()) {
encodedResource.getResource().consumeContent(is -> {
if (encodedResource.getEncoding() != null) {
getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding()));
}
else {
getPropertiesPersister().load(props, is);
}
}
});
int count = registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription());
if (logger.isDebugEnabled()) {
@@ -159,7 +159,7 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy {
return result;
}
catch (IllegalArgumentException ex) {
if (factoryBean != null && !factoryMethod.getDeclaringClass().isAssignableFrom(factoryBean.getClass())) {
if (factoryBean != null && !factoryMethod.getDeclaringClass().isInstance(factoryBean)) {
throw new BeanInstantiationException(factoryMethod,
"Illegal factory instance for factory method '" + factoryMethod.getName() + "'; " +
"instance: " + factoryBean.getClass().getName(), ex);
@@ -17,6 +17,7 @@
package org.springframework.beans.factory.support;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -64,6 +65,7 @@ import org.springframework.util.StringUtils;
* @author Rod Johnson
* @author Juergen Hoeller
* @author Sam Brannen
* @author Yanming Zhou
* @since 06.01.2003
* @see DefaultListableBeanFactory
*/
@@ -149,6 +151,17 @@ public class StaticListableBeanFactory implements ListableBeanFactory {
return (T) bean;
}
@Override
@SuppressWarnings("unchecked")
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
Object bean = getBean(name);
Type requiredType = typeReference.getType();
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
}
return (T) bean;
}
@Override
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
if (!ObjectUtils.isEmpty(args)) {
@@ -21,6 +21,7 @@ import java.io.InputStream;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import javax.xml.parsers.ParserConfigurationException;
@@ -337,12 +338,16 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
}
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
try {
AtomicInteger count = new AtomicInteger();
encodedResource.getResource().consumeContent(inputStream -> {
InputSource inputSource = new InputSource(inputStream);
if (encodedResource.getEncoding() != null) {
inputSource.setEncoding(encodedResource.getEncoding());
}
count.addAndGet(doLoadBeanDefinitions(inputSource, encodedResource.getResource()));
});
return count.get();
}
catch (IOException ex) {
throw new BeanDefinitionStoreException(
@@ -84,8 +84,8 @@ public class ClassArrayEditor extends PropertyEditorSupport {
return "";
}
StringJoiner sj = new StringJoiner(",");
for (Class<?> klass : classes) {
sj.add(ClassUtils.getQualifiedName(klass));
for (Class<?> clazz : classes) {
sj.add(clazz.getTypeName());
}
return sj.toString();
}
@@ -72,12 +72,7 @@ public class ClassEditor extends PropertyEditorSupport {
@Override
public String getAsText() {
Class<?> clazz = (Class<?>) getValue();
if (clazz != null) {
return ClassUtils.getQualifiedName(clazz);
}
else {
return "";
}
return (clazz != null ? clazz.getTypeName() : "");
}
}
@@ -24,6 +24,7 @@ import org.springframework.core.ResolvableType
* This extension is not subject to type erasure and retains actual generic type arguments.
*
* @author Sebastien Deleuze
* @author Yanming Zhou
* @since 5.0
*/
inline fun <reified T : Any> BeanFactory.getBean(): T =
@@ -31,14 +32,14 @@ inline fun <reified T : Any> BeanFactory.getBean(): T =
/**
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>("foo")` variant.
* Like the original Java method, this extension is subject to type erasure.
* This extension is not subject to type erasure and retains actual generic type arguments.
*
* @see BeanFactory.getBean(String, Class<T>)
* @author Sebastien Deleuze
* @since 5.0
*/
inline fun <reified T : Any> BeanFactory.getBean(name: String): T =
getBean(name, T::class.java)
getBean(name, (object : ParameterizedTypeReference<T>() {}))
/**
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>(arg1, arg2)` variant.
@@ -18,8 +18,8 @@ package org.springframework.beans.factory
import org.springframework.beans.factory.BeanRegistry.SupplierContext
import org.springframework.core.ParameterizedTypeReference
import org.springframework.core.ResolvableType
import org.springframework.core.env.Environment
import kotlin.reflect.KClass
/**
* Contract for registering programmatically beans.
@@ -364,6 +364,28 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean
return registry.registerBean(object: ParameterizedTypeReference<T>() {}, customizer)
}
/**
* Determine whether a bean of the given name is already registered.
* @param name the name of the bean
* @since 7.1
*/
fun containsBean(name: String): Boolean = registry.containsBean(name)
/**
* Determine whether a bean of the given type is already registered.
* @param beanType the type of the bean
* @since 7.1
*/
fun containsBean(beanType: KClass<*>): Boolean = registry.containsBean(beanType.java)
/**
* Determine whether a bean of the given type is already registered.
* @param T the type of the bean
* @since 7.1
*/
inline fun <reified T : Any> containsBean(): Boolean =
registry.containsBean(object: ParameterizedTypeReference<T>() {})
/**
* Context available from the bean instance supplier designed to give access
@@ -34,8 +34,12 @@ import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.annotation.Autowire;
import org.springframework.beans.propertyeditors.StringArrayPropertyEditor;
@@ -56,6 +60,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.within;
import static org.springframework.beans.ConfigurablePropertyAccessor.DEFAULT_MAX_NESTED_PATH_DEPTH;
/**
* Shared tests for property accessors.
@@ -67,6 +72,7 @@ import static org.assertj.core.api.Assertions.within;
* @author Chris Beams
* @author Dave Syer
* @author Stephane Nicoll
* @author Sam Brannen
*/
abstract class AbstractPropertyAccessorTests {
@@ -223,10 +229,7 @@ abstract class AbstractPropertyAccessorTests {
@Test
void getAnotherNestedDeepProperty() {
ITestBean target = new TestBean("rod", 31);
ITestBean kerry = new TestBean("kerry", 35);
target.setSpouse(kerry);
kerry.setSpouse(target);
ITestBean target = createSpouseCycle();
AbstractPropertyAccessor accessor = createAccessor(target);
Integer KA = (Integer) accessor.getPropertyValue("spouse.age");
assertThat(KA).as("kerry is 35").isEqualTo(35);
@@ -291,6 +294,18 @@ abstract class AbstractPropertyAccessorTests {
accessor.getPropertyValue("address.bar"));
}
@ParameterizedTest // gh-36999
@ValueSource(strings = {"address.[.city", "address.].city", "address.[[.city",
"address.]].city", "address.][.city", "address.[X.city", "address.X[.city"})
void getNestedPropertyWithUnbalancedBracket(String propertyPath) {
Person target = createPerson("John", "London", "UK");
AbstractPropertyAccessor accessor = createAccessor(target);
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> accessor.getPropertyValue(propertyPath))
.withMessageContaining("Invalid property path '" + propertyPath + "'");
}
@Test
void setSimpleProperty() {
Simple target = new Simple("John", 2);
@@ -1362,6 +1377,19 @@ abstract class AbstractPropertyAccessorTests {
accessor.setPropertyValue("address.bar", "value"));
}
@ParameterizedTest // gh-36999
@ValueSource(strings = {"address.[.city", "address.].city", "address.[[.city",
"address.]].city", "address.][.city", "address.[X.city", "address.X[.city"})
void setNestedPropertyWithUnbalancedBracket(String propertyPath) {
Person target = createPerson("John", "Paris", "FR");
AbstractPropertyAccessor accessor = createAccessor(target);
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> accessor.setPropertyValue(propertyPath, "Zürich"))
.withMessageContaining("Invalid property path '" + propertyPath + "'");
assertThat(target.getAddress().getCity()).isEqualTo("Paris");
}
@Test
void setPropertyValuesIgnoresInvalidNestedOnRequest() {
ITestBean target = new TestBean();
@@ -1413,8 +1441,6 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map['].name")).isEqualTo("name9");
assertThat(accessor.getPropertyValue("map[\"].name")).isEqualTo("name9");
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("nameC");
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA");
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB");
@@ -1583,10 +1609,128 @@ abstract class AbstractPropertyAccessorTests {
}
@Nested // gh-37252
class MaxNestedPathDepthTests {
@ParameterizedTest
@ValueSource(ints = {-1, Integer.MIN_VALUE})
void setMaxNestedPathDepthPreconditions(int depth) {
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
assertThatIllegalArgumentException()
.isThrownBy(() -> accessor.setMaxNestedPathDepth(depth))
.withMessage("'maxNestedPathDepth' must not be negative");
}
@Test
void maxNestedPathDepthOfZeroOnlyDisablesNestedPropertyPaths() {
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
accessor.setMaxNestedPathDepth(0);
// Simple property access is still supported.
assertThat(accessor.getPropertyValue("name")).isEqualTo("rod");
accessor.setPropertyValue("name", "ROD");
assertThat(accessor.getPropertyValue("name")).isEqualTo("ROD");
// As is indexed property access.
accessor.setPropertyValue("stringArray", new String[] {"a", "b"});
assertThat(accessor.getPropertyValue("stringArray[1]")).isEqualTo("b");
accessor.setPropertyValue("stringArray[1]", "B");
assertThat(accessor.getPropertyValue("stringArray[1]")).isEqualTo("B");
// As is mapped property access.
accessor.setPropertyValue("someMap[key]", "value");
assertThat(accessor.getPropertyValue("someMap[key]")).isEqualTo("value");
// Nested property paths, however, are not.
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(1)), 0);
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(2)), 0);
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(1), "Joe"), 0);
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(2), "Joe"), 0);
}
@Test
void defaultMaxNestedPathDepth() {
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
assertThat(accessor.getMaxNestedPathDepth()).isEqualTo(DEFAULT_MAX_NESTED_PATH_DEPTH);
// depth == max
assertThat(accessor.getPropertyValue(nestedSpousePath(DEFAULT_MAX_NESTED_PATH_DEPTH))).isEqualTo("rod");
// depth > max
assertNestedPathDepthExceeded(
() -> accessor.getPropertyValue(nestedSpousePath(DEFAULT_MAX_NESTED_PATH_DEPTH + 1)));
}
@Test
void getNestedPropertyWithCustomMaxNestedPathDepth() {
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
accessor.setMaxNestedPathDepth(10);
// depth < max
assertThat(accessor.getPropertyValue(nestedSpousePath(9))).isEqualTo("kerry");
// depth == max
assertThat(accessor.getPropertyValue(nestedSpousePath(10))).isEqualTo("rod");
// depth > max
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(11)), 10);
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(100)), 10);
}
@Test
void setNestedPropertyExceedingMaxNestedPathDepth() {
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
accessor.setMaxNestedPathDepth(10);
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(11), "Jane"), 10);
}
@Test
void maxNestedPathDepthProtectsAgainstStackOverflow() {
AbstractPropertyAccessor accessor = createAccessor(createSpouseCycle());
accessor.setAutoGrowNestedPaths(true);
assertNestedPathDepthExceeded(() -> accessor.getPropertyValue(nestedSpousePath(100_000)));
assertNestedPathDepthExceeded(() -> accessor.setPropertyValue(nestedSpousePath(100_000), "Jane"));
}
private static String nestedSpousePath(int depth) {
return "spouse.".repeat(depth) + "name";
}
private static void assertNestedPathDepthExceeded(ThrowingCallable throwingCallable) {
assertNestedPathDepthExceeded(throwingCallable, DEFAULT_MAX_NESTED_PATH_DEPTH);
}
private static void assertNestedPathDepthExceeded(ThrowingCallable throwingCallable, int maxDepth) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(throwingCallable)
.withMessageEndingWith("nesting depth exceeds the maximum of " + maxDepth);
}
}
private Person createPerson(String name, String city, String country) {
return new Person(name, new Address(city, country));
}
/**
* Create two beans that are each other's spouse, so that a nested property
* path consisting of any number of {@code spouse} segments can be traversed.
* @return a {@code "rod"} bean, whose spouse is a {@code "kerry"} bean
*/
private static ITestBean createSpouseCycle() {
ITestBean rod = new TestBean("rod", 31);
ITestBean kerry = new TestBean("kerry", 35);
rod.setSpouse(kerry);
kerry.setSpouse(rod);
return rod;
}
@SuppressWarnings("unused")
private static class Simple {
@@ -294,9 +294,18 @@ class BeanWrapperTests extends AbstractPropertyAccessorTests {
void incompletelyQuotedKeyLeadsToPropertyException() {
TestBean target = new TestBean();
BeanWrapper accessor = createAccessor(target);
assertThatExceptionOfType(NotWritablePropertyException.class)
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> accessor.setPropertyValue("[']", "foobar"))
.satisfies(ex -> assertThat(ex.getPossibleMatches()).isNull());
.withMessageContaining("unterminated quote");
}
@Test // gh-37275
void getPropertyDescriptorForMalformedPathThrowsInvalidPropertyException() {
TestBean target = new TestBean();
BeanWrapper accessor = createAccessor(target);
assertThatExceptionOfType(InvalidPropertyException.class)
.isThrownBy(() -> accessor.getPropertyDescriptor("map[unterminated"))
.withCauseInstanceOf(InvalidPropertyPathException.class);
}
@@ -25,7 +25,9 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Juergen Hoeller
* @author Chris Beams
* @author Sam Brannen
*/
@SuppressWarnings("removal")
class PropertyAccessorUtilsTests {
@Test
@@ -49,12 +51,24 @@ class PropertyAccessorUtilsTests {
void getFirstNestedPropertySeparatorIndex() {
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("[user]")).isEqualTo(-1);
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("user.name")).isEqualTo(4);
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("map[key[0]].name")).isEqualTo(11);
// A dot nested two (unclosed) bracket levels deep must not be mistaken
// for a top-level separator, even though the net number of brackets
// seen so far is even.
assertThat(PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex("a[b[c.d]]")).isEqualTo(-1);
}
@Test
void getLastNestedPropertySeparatorIndex() {
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("[user]")).isEqualTo(-1);
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("user.address.street")).isEqualTo(12);
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("map[key[0]].name")).isEqualTo(11);
// Symmetric case to the one above, but scanning from the end: a dot
// preceded (from the right) by two unmatched closing brackets must
// not be mistaken for a top-level separator.
assertThat(PropertyAccessorUtils.getLastNestedPropertySeparatorIndex("d.c]b]a")).isEqualTo(-1);
}
@Test
@@ -67,6 +81,13 @@ class PropertyAccessorUtilsTests {
assertThat(PropertyAccessorUtils.matchesProperty("user[name]", "user")).isTrue();
}
@Test // gh-37275
void matchesPropertyNeverThrowsForMalformedInput() {
// Non-throwing, best-effort contract, same as canonicalPropertyName.
assertThat(PropertyAccessorUtils.matchesProperty("map[key1]other", "map")).isFalse();
assertThat(PropertyAccessorUtils.matchesProperty("map", "map[key1]other")).isFalse();
}
@Test
void canonicalPropertyName() {
assertThat(PropertyAccessorUtils.canonicalPropertyName(null)).isEmpty();
@@ -83,6 +104,13 @@ class PropertyAccessorUtilsTests {
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key1]")).isEqualTo("map[\"key1]");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[']")).isEqualTo("map[']");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"]")).isEqualTo("map[\"]");
// Keys that themselves contain bracket characters must be resolved
// using depth-aware parsing, consistent with how the property
// accessor resolves the same paths during actual data binding.
assertThat(PropertyAccessorUtils.canonicalPropertyName("map[\"key[0]\"]")).isEqualTo("map[key[0]]");
assertThat(PropertyAccessorUtils.canonicalPropertyName("map['key[0]'].name")).isEqualTo("map[key[0]].name");
assertThat(PropertyAccessorUtils.canonicalPropertyName("users['admin[0]']")).isEqualTo("users[admin[0]]");
}
@Test
@@ -69,6 +69,13 @@ class PropertyDescriptorUtilsPropertyResolutionTests {
assertReadAndWriteMethodsForClassAndId(pdMap, Number.class, null);
}
@Test
void classWithStaticGetters() {
var pdMap = resolver.resolve(ClassWithStaticGetters.class);
assertThat(pdMap).containsOnlyKeys("class");
}
@Test
void classWithOnlySetter() {
var pdMap = resolver.resolve(ClassWithOnlySetter.class);
@@ -134,6 +141,17 @@ class PropertyDescriptorUtilsPropertyResolutionTests {
}
}
static class ClassWithStaticGetters {
public static Long getId() {
return 42L;
}
public static boolean isActive() {
return true;
}
}
static class ClassWithOnlySetter {
public void setId(Long id) {
@@ -0,0 +1,93 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.PropertyEditor;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.propertyeditors.CustomNumberEditor;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PropertyEditorRegistrySupport}.
*
* @author Sam Brannen
* @since 7.1
*/
class PropertyEditorRegistrySupportTests {
/**
* Matches the private {@code MAX_STRIPPED_PROPERTY_PATH_DEPTH} constant in
* {@link PropertyEditorRegistrySupport}.
*/
private static final int MAX_DEPTH = 8;
private final PropertyEditorRegistrySupport registry = new PropertyEditorRegistrySupport();
private final PropertyEditor editor = new CustomNumberEditor(Integer.class, true);
@Test
void findCustomEditorMatchesStrippedPathAtMaxSupportedNestingDepth() {
registry.registerCustomEditor(null, "list", this.editor);
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH);
assertThat(registry.findCustomEditor(null, propertyPath)).isSameAs(this.editor);
}
@Test
void findCustomEditorDoesNotMatchStrippedPathBeyondMaxSupportedNestingDepth() {
registry.registerCustomEditor(null, "list", this.editor);
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH + 1);
assertThat(registry.findCustomEditor(null, propertyPath)).isNull();
}
@Test // gh-37020
@Timeout(5)
void findCustomEditorWithExcessivelyNestedPropertyPathDoesNotHang() {
registry.registerCustomEditor(null, "attrs", this.editor);
// A property path with a bracket-nesting depth (40) that would previously
// have caused addStrippedPropertyPaths() to recursively enumerate 2^40 - 1
// stripped path variants. With the depth limit in place, this should return
// promptly.
String propertyPath = "attrs" + "[k]".repeat(40);
assertThat(registry.findCustomEditor(null, propertyPath)).isNull();
}
@Test
void guessPropertyTypeFromEditorsMatchesStrippedPathAtMaxSupportedNestingDepth() {
registry.registerCustomEditor(Integer.class, "list", this.editor);
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH);
assertThat(registry.guessPropertyTypeFromEditors(propertyPath)).isEqualTo(Integer.class);
}
@Test
void guessPropertyTypeFromEditorsDoesNotMatchStrippedPathBeyondMaxSupportedNestingDepth() {
registry.registerCustomEditor(Integer.class, "list", this.editor);
String propertyPath = "list" + "[0]".repeat(MAX_DEPTH + 1);
assertThat(registry.guessPropertyTypeFromEditors(propertyPath)).isNull();
}
}
@@ -0,0 +1,302 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.PropertyPath.Segment;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link PropertyPath}.
*/
class PropertyPathTests {
@Test
void canonicalNameOfWellFormedPath() {
assertThat(canonicalName("name")).isEqualTo("name");
assertThat(canonicalName("person.name")).isEqualTo("person.name");
assertThat(canonicalName("person.addresses[1].city")).isEqualTo("person.addresses[1].city");
assertThat(canonicalName("map[key1]")).isEqualTo("map[key1]");
assertThat(canonicalName("map['key1']")).isEqualTo("map[key1]");
assertThat(canonicalName("map[\"key1\"]")).isEqualTo("map[key1]");
assertThat(canonicalName("map['key1'].name")).isEqualTo("map[key1].name");
assertThat(canonicalName("map[key1][key2]")).isEqualTo("map[key1][key2]");
assertThat(canonicalName("map['key1'][\"key2\"]")).isEqualTo("map[key1][key2]");
assertThat(canonicalName("map[key[0]]")).isEqualTo("map[key[0]]");
assertThat(canonicalName("map['key[0]']")).isEqualTo("map[key[0]]");
assertThat(canonicalName("map['key[0]'].name")).isEqualTo("map[key[0]].name");
assertThat(canonicalName("users['admin[0]']")).isEqualTo("users[admin[0]]");
assertThat(canonicalName("map[]")).isEqualTo("map[]");
assertThat(canonicalName("map['']")).isEqualTo("map[]");
assertThat(canonicalName("map[\"\"]")).isEqualTo("map[]");
assertThat(canonicalName("map[my.key]")).isEqualTo("map[my.key]");
assertThat(canonicalName("map[[a]]")).isEqualTo("map[[a]]");
assertThat(canonicalName("[user]")).isEqualTo("[user]");
assertThat(canonicalName("[user].name")).isEqualTo("[user].name");
}
@Test
void canonicalNameKeepsQuotesWhenRequired() {
assertThat(canonicalName("map['a]b']")).isEqualTo("map['a]b']");
assertThat(canonicalName("map['a[b']")).isEqualTo("map['a[b']");
assertThat(canonicalName("map[\"a'b\"]")).isEqualTo("map[\"a'b\"]");
assertThat(canonicalName("map['a\"b']")).isEqualTo("map['a\"b']");
}
@Test
void parseEmptyPath() {
PropertyPath path = PropertyPath.parse("");
assertThat(path.canonicalName()).isEmpty();
assertThat(path.segments()).isEmpty();
}
@Test
void parseSimplePath() {
assertThat(PropertyPath.parse("name").segments())
.containsExactly(new Segment("name", List.of()));
}
@Test
void parseNestedPath() {
assertThat(PropertyPath.parse("person.addresses[1].city").segments())
.containsExactly(
new Segment("person", List.of()),
new Segment("addresses", List.of("1")),
new Segment("city", List.of()));
}
@Test
void parseMultipleIndexesInOneSegment() {
assertThat(PropertyPath.parse("map[key1][key2]").segments())
.containsExactly(new Segment("map", List.of("key1", "key2")));
}
@Test
void parseRootLevelIndexedAccess() {
assertThat(PropertyPath.parse("[user]").segments())
.containsExactly(new Segment("", List.of("user")));
}
@Test
void parseStripsQuotesFromKeys() {
assertThat(PropertyPath.parse("map['key1'][\"key2\"]").segments())
.containsExactly(new Segment("map", List.of("key1", "key2")));
}
@Test
void parseKeepsDotsInsideKeysOpaque() {
assertThat(PropertyPath.parse("map[my.key].name").segments())
.containsExactly(
new Segment("map", List.of("my.key")),
new Segment("name", List.of()));
}
@Test
void parseKeepsNestedBracketsInRawKey() {
assertThat(PropertyPath.parse("map[key[0]]").segments())
.containsExactly(new Segment("map", List.of("key[0]")));
}
@Test
void parseKeepsUnbalancedBracketInQuotedKey() {
assertThat(PropertyPath.parse("map['a]b']").segments())
.containsExactly(new Segment("map", List.of("a]b")));
}
@Test
void parseEmptyKey() {
assertThat(PropertyPath.parse("map[]").segments())
.containsExactly(new Segment("map", List.of("")));
}
@ParameterizedTest
@ValueSource(strings = {"", "name", "person.name", "person.addresses[1].city", "map[key1]",
"map['key1']", "map[\"key1\"]", "map[key1][key2]", "map[key[0]]", "map['key[0]']",
"map[]", "map['']", "map[my.key]", "map[[a]]", "[user]", "[user].name",
"map['a]b']", "map['a[b']", "map[\"a'b\"]", "map['a\"b']", "map['']['a]b']"})
void canonicalNameIdempotent(String path) {
PropertyPath parsed = PropertyPath.parse(path);
PropertyPath reparsed = PropertyPath.parse(parsed.canonicalName());
assertThat(reparsed.segments()).isEqualTo(parsed.segments());
assertThat(reparsed.canonicalName()).isEqualTo(parsed.canonicalName());
assertThat(reparsed).isEqualTo(parsed);
}
@ParameterizedTest
@ValueSource(strings = {"name", "person.name", "person.addresses[1].city", "person.map[key1]",
"person.map['key1']", "person.map[\"key1\"]", "map[key1].map[key2]", "map['key[0]']",
"person.map['a]b']", "person.map['a[b']", "person.map[\"a'b\"]", "person.map['a\"b']", "person.map['']['a]b']"})
void canonicalPathAndSegmentsAreEquivalent(String path) {
PropertyPath parsed = PropertyPath.parse(path);
assertThat(parsed.segments().stream()
.map(Segment::toCanonicalName)
.reduce((a, b) -> a + "." + b))
.hasValue(parsed.canonicalName());
}
@Test
void segmentsCannotBeModified() {
PropertyPath path = PropertyPath.parse("map[key1]");
assertThat(path.segments()).hasSize(1);
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> path.segments().clear());
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> path.segments().get(0).keys().clear());
}
@Test
void equalsAndHashCodeUseCanonicalName() {
assertThat(PropertyPath.parse("map['key1'].name"))
.isEqualTo(PropertyPath.parse("map[key1].name"))
.hasSameHashCodeAs(PropertyPath.parse("map[key1].name"));
assertThat(PropertyPath.parse("map[key1]")).isNotEqualTo(PropertyPath.parse("map[key2]"));
}
@Test
void toStringReturnsCanonicalName() {
assertThat(PropertyPath.parse("map['key1'].name")).hasToString("map[key1].name");
}
@Test
void parseRejectsNull() {
assertThatIllegalArgumentException().isThrownBy(() -> PropertyPath.parse(null));
}
@ParameterizedTest
@ValueSource(strings = {"map[key1]other", "map[key1]other.name", "map[key1]IGNORED[key2]",
"options[priority]X", "options[security]IGNORED[role]", "map[key1]]", "[user]x"})
void parseRejectsTextAfterIndex(String path) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse(path))
.withMessageContaining("after an index")
.satisfies(ex -> assertThat(ex.getPropertyPath()).isEqualTo(path));
}
@ParameterizedTest
@ValueSource(strings = {"map[key1", "map[", "map[[a]", "map[a][", "map['a'"})
void parseRejectsUnclosedIndex(String path) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse(path))
.withMessageContaining("unclosed '['");
}
@ParameterizedTest
@ValueSource(strings = {"map]", "map]name", "publication.].published", "address.].city", "]"})
void parseRejectsUnmatchedIndexSuffix(String path) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse(path))
.withMessageContaining("without a matching '['");
}
@ParameterizedTest
@ValueSource(strings = {".name", "person.", "person..name", ".", "..", "map[key1]."})
void parseRejectsEmptySegment(String path) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse(path))
.withMessageContaining("empty path segment");
}
@ParameterizedTest
@ValueSource(strings = {"map[']", "map[\"]", "map['key1]", "map[\"key1]", "map['a"})
void parseRejectsUnterminatedQuote(String path) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse(path))
.withMessageContaining("unterminated quote");
}
@ParameterizedTest
@ValueSource(strings = {"map[don't]", "map[a'b]", "map[a\"b]"})
void parseRejectsQuoteInRawKey(String path) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse(path))
.withMessageContaining("in an unquoted key");
}
@ParameterizedTest
@ValueSource(strings = {"map['a'b']", "map['a'b]", "map[\"a\"b\"]"})
void parseRejectsTextAfterClosingQuote(String path) {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse(path))
.withMessageContaining("after a closing quote");
}
@Test
void invalidPropertyPathExceptionIsAPropertyAccessException() {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() -> PropertyPath.parse("map[key1]other"))
.isInstanceOf(PropertyAccessException.class)
.withMessageContaining("Invalid property path 'map[key1]other'")
.satisfies(ex -> {
assertThat(ex.getErrorCode()).isEqualTo(InvalidPropertyPathException.ERROR_CODE);
assertThat(ex.getPropertyPath()).isEqualTo("map[key1]other");
});
}
@Test
void failsWhenExceedsNestingDepth() {
assertThatExceptionOfType(InvalidPropertyPathException.class)
.isThrownBy(() ->
PropertyPath.parse("one.two.three[1].four", PropertyPath.Options.withMaxNestedPathDepth(2)))
.withMessageContaining("nesting depth exceeds the maximum of 2");
}
@Test
void subPathFromZeroReturnsSamePath() {
PropertyPath path = PropertyPath.parse("address.country.name");
assertThat(path.subPath(0)).isSameAs(path);
}
@Test
void subPathDropsLeadingSegments() {
PropertyPath subPath = PropertyPath.parse("address.country[0].name").subPath(1);
assertThat(subPath.canonicalName()).isEqualTo("country[0].name");
assertThat(subPath.segments()).containsExactly(
new Segment("country", List.of("0")),
new Segment("name", List.of()));
}
@Test
void subPathAtLastIndexIsEmpty() {
PropertyPath subPath = PropertyPath.parse("address.name").subPath(2);
assertThat(subPath.canonicalName()).isEmpty();
assertThat(subPath.segments()).isEmpty();
}
@Test
void subPathRejectsOutOfBoundsIndex() {
PropertyPath path = PropertyPath.parse("address.name");
assertThatIllegalArgumentException().isThrownBy(() -> path.subPath(3));
}
private static String canonicalName(String path) {
return PropertyPath.parse(path).canonicalName();
}
}
@@ -19,6 +19,7 @@ package org.springframework.beans.factory;
import java.io.Closeable;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.text.NumberFormat;
import java.text.ParseException;
@@ -79,6 +80,7 @@ import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.beans.testfixture.beans.factory.DummyFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.annotation.Order;
@@ -1236,11 +1238,13 @@ class DefaultListableBeanFactoryTests {
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
bd1.setScope(BeanDefinition.SCOPE_PROTOTYPE);
lbf.registerBeanDefinition("testBean", bd1);
assertThat(lbf.getBeanDefinition("testBean").getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE);
assertThat(lbf.getBean("testBean")).isInstanceOf(TestBean.class);
RootBeanDefinition bd2 = new RootBeanDefinition(NestedTestBean.class);
bd2.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bd2.setScope(BeanDefinition.SCOPE_SINGLETON);
lbf.registerBeanDefinition("testBean", bd2);
assertThat(lbf.getBeanDefinition("testBean").getScope()).isEqualTo(BeanDefinition.SCOPE_SINGLETON);
assertThat(lbf.getBean("testBean")).isInstanceOf(NestedTestBean.class);
}
@@ -1682,6 +1686,46 @@ class DefaultListableBeanFactoryTests {
lbf.getBean(TestBean.class));
}
@Test // gh-34687
void getBeanByNameWithTypeReference() {
RootBeanDefinition bd1 = new RootBeanDefinition(StringTemplate.class);
RootBeanDefinition bd2 = new RootBeanDefinition(NumberTemplate.class);
lbf.registerBeanDefinition("bd1", bd1);
lbf.registerBeanDefinition("bd2", bd2);
Template<String> stringTemplate = lbf.getBean("bd1", new ParameterizedTypeReference<>() {});
Template<Number> numberTemplate = lbf.getBean("bd2", new ParameterizedTypeReference<>() {});
assertThat(stringTemplate).isInstanceOf(StringTemplate.class);
assertThat(numberTemplate).isInstanceOf(NumberTemplate.class);
assertThatExceptionOfType(BeanNotOfRequiredTypeException.class)
.isThrownBy(() -> lbf.getBean("bd2", new ParameterizedTypeReference<Template<String>>() {}))
.satisfies(ex -> {
assertThat(ex.getBeanName()).isEqualTo("bd2");
assertThat(ex.getRequiredType()).isEqualTo(Template.class);
assertThat(ex.getActualType()).isEqualTo(NumberTemplate.class);
assertThat(ex.getGenericRequiredType().toString()).endsWith("Template<java.lang.String>");
});
}
@Test // gh-37047
void getBeanByNameWithTypeReferenceMatchingGenericTypeOnAopProxy() {
RootBeanDefinition bd = new RootBeanDefinition(ProxiedStringTemplate.class);
bd.setTargetType(ResolvableType.forClass(ProxiedStringTemplate.class));
lbf.registerBeanDefinition("proxiedTemplate", bd);
// Simulate a JDK dynamic AOP proxy (as created for proxyTargetClass=false)
// which only exposes the raw ProxiedTemplate interface, erasing the
// generic type information that is available on the target class.
Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {ProxiedTemplate.class}, (target, method, args) -> null);
lbf.registerSingleton("proxiedTemplate", proxy);
ProxiedTemplate<String> template = lbf.getBean("proxiedTemplate", new ParameterizedTypeReference<>() {});
assertThat(template).isSameAs(proxy);
}
@Test
void getBeanByTypeWithPrimary() {
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
@@ -3872,4 +3916,19 @@ class DefaultListableBeanFactoryTests {
}
}
private static class Template<T> {
}
private static class StringTemplate extends Template<String> {
}
private static class NumberTemplate extends Template<Number> {
}
private interface ProxiedTemplate<T> {
}
private static class ProxiedStringTemplate implements ProxiedTemplate<String> {
}
}
@@ -45,9 +45,9 @@ class ParameterResolutionTests {
@Test
void isAutowirablePreconditions() {
assertThatIllegalArgumentException().isThrownBy(() ->
ParameterResolutionDelegate.isAutowirable(null, 0))
.withMessageContaining("Parameter must not be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> ParameterResolutionDelegate.isAutowirable(null, 0))
.withMessageContaining("Parameter must not be null");
}
@Test
@@ -87,29 +87,30 @@ class ParameterResolutionTests {
Parameter[] parameters = notAutowirableConstructor.getParameters();
for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
Parameter parameter = parameters[parameterIndex];
assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex)).as("Parameter " + parameter + " must not be autowirable").isFalse();
assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex))
.as("Parameter " + parameter + " must not be autowirable").isFalse();
}
}
@Test
void resolveDependencyPreconditionsForParameter() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, null, mock()))
.withMessageContaining("Parameter must not be null");
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, null, mock()))
.withMessageContaining("Parameter must not be null");
}
@Test
void resolveDependencyPreconditionsForContainingClass() {
assertThatIllegalArgumentException().isThrownBy(() ->
ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null))
.withMessageContaining("Containing class must not be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null))
.withMessageContaining("Containing class must not be null");
}
@Test
void resolveDependencyPreconditionsForBeanFactory() {
assertThatIllegalArgumentException().isThrownBy(() ->
ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null))
.withMessageContaining("AutowireCapableBeanFactory must not be null");
assertThatIllegalArgumentException()
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null))
.withMessageContaining("AutowireCapableBeanFactory must not be null");
}
private Parameter getParameter() throws NoSuchMethodException {
@@ -133,9 +134,64 @@ class ParameterResolutionTests {
parameter, parameterIndex, AutowirableClass.class, beanFactory);
assertThat(intermediateDependencyDescriptor.getAnnotatedElement()).isEqualTo(constructor);
assertThat(intermediateDependencyDescriptor.getMethodParameter().getParameter()).isEqualTo(parameter);
assertThat(intermediateDependencyDescriptor.usesStandardBeanLookup()).isTrue();
}
}
@Test
void resolveDependencyWithCustomParameterNamePreconditionsForParameter() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, "customName", getClass(), mock()))
.withMessageContaining("Parameter must not be null");
}
@Test
void resolveDependencyWithCustomParameterNamePreconditionsForContainingClass() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", null, mock()))
.withMessageContaining("Containing class must not be null");
}
@Test
void resolveDependencyWithCustomParameterNamePreconditionsForBeanFactory() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", getClass(), null))
.withMessageContaining("AutowireCapableBeanFactory must not be null");
}
@Test
void resolveDependencyWithNullCustomParameterNameFallsBackToDefaultParameterNameDiscovery() throws Exception {
Constructor<?> constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class);
AutowireCapableBeanFactory beanFactory = mock();
given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0));
Parameter[] parameters = constructor.getParameters();
for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
Parameter parameter = parameters[parameterIndex];
DependencyDescriptor via4ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
parameter, parameterIndex, AutowirableClass.class, beanFactory);
DependencyDescriptor via5ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
parameter, parameterIndex, null, AutowirableClass.class, beanFactory);
assertThat(via5ArgMethod.getDependencyName()).isEqualTo(via4ArgMethod.getDependencyName());
}
}
@Test
void resolveDependencyWithCustomParameterName() throws Exception {
Constructor<?> constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class);
AutowireCapableBeanFactory beanFactory = mock();
given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0));
Parameter parameter = constructor.getParameters()[0];
DependencyDescriptor descriptor = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
parameter, 0, "customBeanName", AutowirableClass.class, beanFactory);
assertThat(descriptor.getAnnotatedElement()).isEqualTo(constructor);
assertThat(descriptor.getMethodParameter().getParameter()).isEqualTo(parameter);
assertThat(descriptor.getDependencyName()).isEqualTo("customBeanName");
assertThat(descriptor.usesStandardBeanLookup()).isTrue();
}
void autowirableMethod(
@Autowired String firstParameter,
@@ -27,10 +27,10 @@ import javax.lang.model.element.Modifier;
import org.junit.jupiter.api.Test;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.MethodReference;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.aot.generate.NameGenerator;
import org.springframework.aot.generate.ValueCodeGenerationException;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration;
@@ -109,7 +109,7 @@ class BeanRegistrationsAotContributionTests {
@Test
void applyToWhenHasNameGeneratesPrefixedFeatureName() {
this.generationContext = new TestGenerationContext(
new ClassNameGenerator(TestGenerationContext.TEST_TARGET, "Management"));
new NameGenerator(TestGenerationContext.TEST_TARGET, "Management"));
this.beanFactoryInitializationCode = new MockBeanFactoryInitializationCode(this.generationContext);
RegisteredBean registeredBean = registerBean(new RootBeanDefinition(TestBean.class));
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(this.methodGeneratorFactory,
@@ -21,6 +21,7 @@ import io.mockk.mockk
import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.core.ParameterizedTypeReference
import org.springframework.core.ResolvableType
/**
@@ -53,7 +54,16 @@ class BeanFactoryExtensionsTests {
fun `getBean with String and reified type parameters`() {
val name = "foo"
bf.getBean<Foo>(name)
verify { bf.getBean(name, Foo::class.java) }
verify { bf.getBean(name, ofType<ParameterizedTypeReference<Foo>>()) }
}
@Test
fun `getBean with String and reified generic type parameters`() {
val name = "foo"
val foo = listOf(Foo())
every { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) } returns foo
assertThat(bf.getBean<List<Foo>>("foo")).isSameAs(foo)
verify { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) }
}
@Test
@@ -76,7 +76,6 @@ public class IndexedTestBean {
TestBean tb6 = new TestBean("name6", 0);
TestBean tb7 = new TestBean("name7", 0);
TestBean tb8 = new TestBean("name8", 0);
TestBean tb9 = new TestBean("name9", 0);
TestBean tbA = new TestBean("nameA", 0);
TestBean tbB = new TestBean("nameB", 0);
TestBean tbC = new TestBean("nameC", 0);
@@ -105,8 +104,6 @@ public class IndexedTestBean {
list.add(tbY);
this.map.put("key4", list);
this.map.put("key5[foo]", tb8);
this.map.put("'", tb9);
this.map.put("\"", tb9);
this.myTestBeans = new MyTestBeans(tbZ);
}
@@ -41,8 +41,7 @@ import org.springframework.context.annotation.Role;
@Configuration(proxyBeanMethods = false)
public abstract class AbstractJCacheConfiguration extends AbstractCachingConfiguration {
@SuppressWarnings("NullAway.Init")
protected Supplier<@Nullable CacheResolver> exceptionCacheResolver;
protected @Nullable Supplier<@Nullable CacheResolver> exceptionCacheResolver;
@Override
@@ -79,7 +79,7 @@ public class SimpleMailMessage implements MailMessage, Serializable {
this.to = copyOrNull(original.getTo());
this.cc = copyOrNull(original.getCc());
this.bcc = copyOrNull(original.getBcc());
this.sentDate = original.getSentDate();
this.sentDate = copyOrNull(original.sentDate);
this.subject = original.getSubject();
this.text = original.getText();
}
@@ -147,11 +147,11 @@ public class SimpleMailMessage implements MailMessage, Serializable {
@Override
public void setSentDate(@Nullable Date sentDate) {
this.sentDate = sentDate;
this.sentDate = copyOrNull(sentDate);
}
public @Nullable Date getSentDate() {
return this.sentDate;
return copyOrNull(this.sentDate);
}
@Override
@@ -194,8 +194,8 @@ public class SimpleMailMessage implements MailMessage, Serializable {
if (getBcc() != null) {
target.setBcc(copy(getBcc()));
}
if (getSentDate() != null) {
target.setSentDate(getSentDate());
if (this.sentDate != null) {
target.setSentDate((Date) this.sentDate.clone());
}
if (getSubject() != null) {
target.setSubject(getSubject());
@@ -247,6 +247,10 @@ public class SimpleMailMessage implements MailMessage, Serializable {
return copy(state);
}
private static @Nullable Date copyOrNull(@Nullable Date date) {
return (date != null ? (Date) date.clone() : null);
}
private static String[] copy(String[] state) {
return state.clone();
}
@@ -21,11 +21,16 @@ import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link SimpleMailMessage}.
*
* @author Dmitriy Kopylenko
* @author Juergen Hoeller
* @author Rick Evans
@@ -98,6 +103,64 @@ class SimpleMailMessageTests {
assertThat(copy.getBcc()[0]).isEqualTo("us@mail.org");
}
@Test // gh-36626
void setSentDateStoresACopy() {
SimpleMailMessage message = new SimpleMailMessage();
Date sentDate = new Date(1234L);
message.setSentDate(sentDate);
sentDate.setTime(0L);
assertThat(message.getSentDate()).isEqualTo(new Date(1234L));
}
@Test // gh-36626
void getSentDateReturnsACopy() {
SimpleMailMessage message = new SimpleMailMessage();
Date sentDate = new Date(1234L);
message.setSentDate(sentDate);
Date exportedDate = message.getSentDate();
exportedDate.setTime(0L);
assertThat(message.getSentDate()).isEqualTo(new Date(1234L));
}
@Test // gh-36626
void copyConstructorCopiesSentDate() {
Date sentDate = new Date(1234L);
SimpleMailMessage original = new SimpleMailMessage();
original.setSentDate(sentDate);
SimpleMailMessage copy = new SimpleMailMessage(original);
sentDate.setTime(0L);
Date copiedDate = copy.getSentDate();
assertThat(copiedDate).isNotNull();
copiedDate.setTime(1L);
assertThat(original.getSentDate()).isEqualTo(new Date(1234L));
assertThat(copy.getSentDate()).isEqualTo(new Date(1234L));
}
@Test // gh-36626
void copyToCopiesSentDate() {
SimpleMailMessage source = new SimpleMailMessage();
source.setSentDate(new Date(1234L));
MailMessage target = mock();
source.copyTo(target);
ArgumentCaptor<Date> dateCaptor = ArgumentCaptor.forClass(Date.class);
verify(target).setSentDate(dateCaptor.capture());
Date copiedDate = dateCaptor.getValue();
assertThat(copiedDate).isNotNull();
copiedDate.setTime(0L);
assertThat(source.getSentDate()).isEqualTo(new Date(1234L));
}
/**
* Tests that two equal SimpleMailMessages have equal hash codes.
*/
@@ -50,17 +50,13 @@ public abstract class AbstractCachingConfiguration implements ImportAware {
protected @Nullable AnnotationAttributes enableCaching;
@SuppressWarnings("NullAway.Init")
protected Supplier<@Nullable CacheManager> cacheManager;
protected @Nullable Supplier<@Nullable CacheManager> cacheManager;
@SuppressWarnings("NullAway.Init")
protected Supplier<@Nullable CacheResolver> cacheResolver;
protected @Nullable Supplier<@Nullable CacheResolver> cacheResolver;
@SuppressWarnings("NullAway.Init")
protected Supplier<@Nullable KeyGenerator> keyGenerator;
protected @Nullable Supplier<@Nullable KeyGenerator> keyGenerator;
@SuppressWarnings("NullAway.Init")
protected Supplier<@Nullable CacheErrorHandler> errorHandler;
protected @Nullable Supplier<@Nullable CacheErrorHandler> errorHandler;
@Override
@@ -291,23 +291,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
this.initialized = true;
}
/**
* Convenience method to return a String representation of this Method
* for use in logging. Can be overridden in subclasses to provide a
* different identifier for the given method.
* @param method the method we're interested in
* @param targetClass class the method is on
* @return log message identifying this method
* @see org.springframework.util.ClassUtils#getQualifiedMethodName
* @deprecated since 6.2.18 with no replacement, for removal in 7.1
*/
@Deprecated(since = "6.2.18", forRemoval = true)
protected String methodIdentification(Method method, Class<?> targetClass) {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
return ClassUtils.getQualifiedMethodName(specificMethod);
}
protected Collection<? extends Cache> getCaches(
CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
@@ -152,12 +152,12 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
private static final Set<Class<? extends Annotation>> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(2);
static {
JAKARTA_RESOURCE_TYPE = loadAnnotationType("jakarta.annotation.Resource");
JAKARTA_RESOURCE_TYPE = AnnotationUtils.loadAnnotationType("jakarta.annotation.Resource");
if (JAKARTA_RESOURCE_TYPE != null) {
resourceAnnotationTypes.add(JAKARTA_RESOURCE_TYPE);
}
EJB_ANNOTATION_TYPE = loadAnnotationType("jakarta.ejb.EJB");
EJB_ANNOTATION_TYPE = AnnotationUtils.loadAnnotationType("jakarta.ejb.EJB");
if (EJB_ANNOTATION_TYPE != null) {
resourceAnnotationTypes.add(EJB_ANNOTATION_TYPE);
}
@@ -191,8 +191,8 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
setOrder(Ordered.LOWEST_PRECEDENCE - 3);
// Jakarta EE 9 set of annotations in jakarta.annotation package
addInitAnnotationType(loadAnnotationType("jakarta.annotation.PostConstruct"));
addDestroyAnnotationType(loadAnnotationType("jakarta.annotation.PreDestroy"));
addInitAnnotationType(AnnotationUtils.loadAnnotationType("jakarta.annotation.PostConstruct"));
addDestroyAnnotationType(AnnotationUtils.loadAnnotationType("jakarta.annotation.PreDestroy"));
// java.naming module present on JDK 9+?
if (JNDI_PRESENT) {
@@ -575,18 +575,6 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
}
@SuppressWarnings("unchecked")
private static @Nullable Class<? extends Annotation> loadAnnotationType(String name) {
try {
return (Class<? extends Annotation>)
ClassUtils.forName(name, CommonAnnotationBeanPostProcessor.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
return null;
}
}
/**
* Class representing generic injection information about an annotated field
* or setter method, supporting @Resource and related annotations.
@@ -276,7 +276,7 @@ public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser {
strategyType.getName() + "]: a zero-argument constructor is required", ex);
}
if (!strategyType.isAssignableFrom(result.getClass())) {
if (!strategyType.isInstance(result)) {
throw new IllegalArgumentException("Provided class name must be an implementation of " + strategyType);
}
return result;
@@ -38,9 +38,11 @@ public interface ConfigurationBeanNameGenerator extends BeanNameGenerator {
/**
* Derive a default bean name for the given {@link Bean @Bean} method,
* taking into account the specified {@link Bean#name() name} attribute.
* <p>As of 7.1, the original {@code @Bean} name (typically the method name)
* will be registered as an alias if that name has not been taken already.
* @param beanMethod the method metadata for the {@link Bean @Bean} method
* @param beanName the {@link Bean#name() name} attribute or {@code null} if
* none is specified
* @param beanName the {@link Bean#name() name} attribute or {@code null}
* if none is specified
* @return the default bean name to use
*/
String deriveBeanName(MethodMetadata beanMethod, @Nullable String beanName);
@@ -210,6 +210,11 @@ class ConfigurationClassBeanDefinitionReader {
String localBeanName = defaultBeanName(beanName, methodName);
beanName = (this.importBeanNameGenerator instanceof ConfigurationBeanNameGenerator cbng ?
cbng.deriveBeanName(metadata, beanName) : localBeanName);
if (!localBeanName.equals(beanName) && !this.registry.containsBeanDefinition(localBeanName) &&
!this.registry.isAlias(localBeanName)) {
// Register original name as alias unless registered already.
this.registry.registerAlias(beanName, localBeanName);
}
if (explicitNames.length > 0) {
// Register aliases even when overridden below.
for (int i = 1; i < explicitNames.length; i++) {
@@ -423,12 +428,14 @@ class ConfigurationClassBeanDefinitionReader {
}
private void loadBeanDefinitionsFromBeanRegistrars(MultiValueMap<String, BeanRegistrar> registrars) {
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
throw new IllegalStateException("Cannot support bean registrars since " +
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
}
registrars.values().forEach(registrarList -> registrarList.forEach(registrar -> registrar.register(new BeanRegistryAdapter(
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment)));
registrars.values().forEach(registrarList -> registrarList.forEach(registrar -> {
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
throw new IllegalStateException("Cannot support bean registrars since " +
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
}
registrar.register(new BeanRegistryAdapter(
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment);
}));
}
@@ -32,10 +32,13 @@ import org.springframework.core.type.MethodMetadata;
* {@code @Bean} methods (which uses the plain method name), primarily for use
* in large applications with potential bean name overlaps. Favor this bean
* naming strategy over {@code FullyQualifiedAnnotationBeanNameGenerator} if
* you expect such naming conflicts for {@code @Bean} methods, as long as the
* application does not depend on {@code @Bean} method names as bean names.
* Where the name does matter, make sure to declare {@code @Bean("myBeanName")}
* in such a scenario, even if it repeats the method name as the bean name.
* you expect such naming conflicts for {@code @Bean} methods.
*
* <p>As of 7.1, the original {@code @Bean} method name will be registered
* as an alias if that name has not been taken already: effectively on first
* occurrence, whereas any later {@code @Bean} methods of the same name will
* not have aliases applied. This preserves the availability of common beans
* under the original bean names for retrieval and injection purposes.
*
* @author Juergen Hoeller
* @since 7.0

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