Compare commits

...

225 Commits

Author SHA1 Message Date
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 Zhou 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
336 changed files with 9544 additions and 2420 deletions
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
on:
push:
branches:
- 7.0.x
- main
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
@@ -27,7 +27,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: 'spring-framework-7.0.x'
build-name: 'spring-framework-7.1.x'
folder: 'deployment-repository'
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository: 'libs-snapshot-local'
+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:
+1 -1
View File
@@ -78,7 +78,7 @@ configure([rootProject] + javaProjects) { project ->
"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.0/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}"
+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
@@ -541,7 +541,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
@@ -402,6 +402,27 @@ To serialize only a subset of the object properties, you can specify a {baeldung
.toBodilessEntity();
----
==== 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:
[source,java,indent=0,subs="verbatim"]
----
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();
----
==== 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.
@@ -419,18 +440,70 @@ For example:
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
// send using RestClient.post or RestTemplate.postForEntity
ResponseEntity<Void> response = this.restClient.post()
.contentType(MediaType.MULTIPART_FORM_DATA)
.body(parts)
.retrieve()
.toBodilessEntity();
----
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.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim"]
----
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()));
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim"]
----
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()))
}
----
======
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
@@ -201,6 +203,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]
@@ -375,6 +453,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. xref:resttestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations
2. xref: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]
@@ -485,8 +485,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
@@ -598,7 +598,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.
@@ -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
@@ -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[]
}
}
+6 -6
View File
@@ -7,7 +7,7 @@ javaPlatform {
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.20.2"))
api(platform("com.fasterxml.jackson:jackson-bom:2.21.2"))
api(platform("io.micrometer:micrometer-bom:1.16.6"))
api(platform("io.netty:netty-bom:4.2.15.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.6"))
@@ -18,14 +18,14 @@ dependencies {
api(platform("org.eclipse.jetty:jetty-bom:12.1.9"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.9"))
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-serialization-bom:1.11.0"))
api(platform("org.junit:junit-bom:6.1.0"))
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.1"))
constraints {
api("com.fasterxml:aalto-xml:1.3.4")
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.fasterxml.woodstox:woodstox-core:7.1.1")
api("com.github.ben-manes.caffeine:caffeine:3.2.3")
api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1")
@@ -120,7 +120,7 @@ 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.17.Final")
api("org.hibernate.orm:hibernate-core:7.4.0.Final")
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.21.0")
+2 -2
View File
@@ -1,10 +1,10 @@
version=7.0.8-SNAPSHOT
version=7.1.0-SNAPSHOT
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
org.gradle.parallel=true
kotlinVersion=2.2.21
kotlinVersion=2.3.20
byteBuddyVersion=1.17.6
kotlin.jvm.target.validation.mode=ignore
@@ -76,8 +76,6 @@ 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 = "";
@@ -156,21 +154,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
/**
* 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.
@@ -298,7 +281,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
componentType, ph.nested(tokens.keys.length));
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('[');
@@ -324,7 +307,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
Object convertedValue = convertIfNecessary(tokens.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);
@@ -761,7 +744,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);
@@ -785,7 +768,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++) {
@@ -40,6 +40,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
private boolean autoGrowNestedPaths = false;
private int autoGrowCollectionLimit = Integer.MAX_VALUE;
boolean suppressNotWritablePropertyException = false;
@@ -63,6 +65,16 @@ 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 setPropertyValue(PropertyValue pv) throws BeansException {
@@ -74,4 +74,17 @@ 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();
}
@@ -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;
}
@@ -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) {
@@ -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 (!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 {
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()) {
@@ -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
@@ -79,6 +79,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;
@@ -1682,6 +1683,29 @@ class DefaultListableBeanFactoryTests {
lbf.getBean(TestBean.class));
}
@Test
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
void getBeanByTypeWithPrimary() {
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
@@ -3872,4 +3896,16 @@ class DefaultListableBeanFactoryTests {
}
}
private static class Template<T> {
}
private static class StringTemplate extends Template<String> {
}
private static class NumberTemplate extends Template<Number> {
}
}
@@ -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,
@@ -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
@@ -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.
*/
@@ -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.
@@ -423,12 +423,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);
}));
}
@@ -132,6 +132,7 @@ import org.springframework.util.ReflectionUtils;
* @author Sam Brannen
* @author Sebastien Deleuze
* @author Brian Clozel
* @author Yanming Zhou
* @since January 21, 2001
* @see #refreshBeanFactory
* @see #getBeanFactory
@@ -1305,6 +1306,12 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
return getBeanFactory().getBean(name, requiredType);
}
@Override
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name, typeReference);
}
@Override
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
assertBeanFactoryActive();
@@ -44,6 +44,8 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.PriorityOrdered;
import org.springframework.core.io.ProtocolResolver;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
@@ -105,6 +107,10 @@ import org.springframework.util.Assert;
*/
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
private static final String DEFERRED_REGISTRY_POST_PROCESSOR_BEAN_NAME =
GenericApplicationContext.class.getName() + ".deferredRegistryPostProcessor";
private final DefaultListableBeanFactory beanFactory;
private @Nullable ResourceLoader resourceLoader;
@@ -604,7 +610,13 @@ public class GenericApplicationContext extends AbstractApplicationContext implem
*/
public void register(BeanRegistrar... registrars) {
for (BeanRegistrar registrar : registrars) {
new BeanRegistryAdapter(this.beanFactory, getEnvironment(), registrar.getClass()).register(registrar);
DeferredRegistryPostProcessor pp = (DeferredRegistryPostProcessor)
this.beanFactory.getSingleton(DEFERRED_REGISTRY_POST_PROCESSOR_BEAN_NAME);
if (pp == null) {
pp = new DeferredRegistryPostProcessor();
this.beanFactory.registerSingleton(DEFERRED_REGISTRY_POST_PROCESSOR_BEAN_NAME, pp);
}
pp.addRegistrar(registrar);
}
}
@@ -648,4 +660,31 @@ public class GenericApplicationContext extends AbstractApplicationContext implem
}
}
/**
* Internal post-processor for invoking DeferredBeanRegistrars at the end
* of the BeanDefinitionRegistryPostProcessor PriorityOrdered phase,
* right before a potential ConfigurationClassPostProcessor.
*/
private class DeferredRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {
private final List<BeanRegistrar> registrars = new ArrayList<>();
public void addRegistrar(BeanRegistrar registrar) {
this.registrars.add(registrar);
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1; // within PriorityOrdered, 1 before ConfigurationClassPostProcessor
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
for (BeanRegistrar registrar : this.registrars) {
new BeanRegistryAdapter(beanFactory, getEnvironment(), registrar.getClass()).register(registrar);
}
}
}
}
@@ -60,9 +60,13 @@ import org.springframework.util.StringUtils;
* are treated in a slightly different fashion than the "basenames" property of
* {@link ResourceBundleMessageSource}. It follows the basic ResourceBundle rule of not
* specifying file extension or language codes, but can refer to any Spring resource
* location (instead of being restricted to classpath resources). With a "classpath:"
* prefix, resources can still be loaded from the classpath, but "cacheSeconds" values
* other than "-1" (caching forever) might not work reliably in this case.
* location (instead of being restricted to classpath resources).
*
* <p>With a "classpath:" prefix, resources can still be loaded from the classpath,
* but "cacheSeconds" values other than "-1" (caching forever) are not expected to
* be effective in this case. As of 7.1, a "classpath*:" prefix is accepted as well,
* loading all classpath resources of the same fully-qualified name: for example,
* "classpath*:/messages.properties" or "classpath*:META-INF/messages.properties".
*
* <p>For a typical web application, message files could be placed in {@code WEB-INF}:
* for example, a "WEB-INF/messages" basename would find a "WEB-INF/messages.properties",
@@ -562,8 +566,8 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
*/
protected Properties loadProperties(Resource resource, String filename) throws IOException {
Properties props = newProperties();
try (InputStream inputStream = resource.getInputStream()) {
String resourceFilename = resource.getFilename();
String resourceFilename = resource.getFilename();
resource.consumeContent(inputStream -> {
if (resourceFilename != null && resourceFilename.endsWith(XML_EXTENSION)) {
if (logger.isDebugEnabled()) {
logger.debug("Loading properties [" + resource.getFilename() + "]");
@@ -594,8 +598,8 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
this.propertiesPersister.load(props, inputStream);
}
}
return props;
}
});
return props;
}
/**
@@ -16,6 +16,7 @@
package org.springframework.jndi.support;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -59,6 +60,7 @@ import org.springframework.jndi.TypeMismatchNamingException;
* in particular if BeanFactory-style type checking is required.
*
* @author Juergen Hoeller
* @author Yanming Zhou
* @since 2.5
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory
* @see org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
@@ -132,6 +134,17 @@ public class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFac
}
}
@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 (args != null) {
@@ -273,9 +273,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* Specify the limit for array and collection auto-growing.
* <p>Default is 256, preventing OutOfMemoryErrors in case of large indexes.
* Raise this limit if your auto-growing needs are unusually high.
* <p>Used for setter injection via {@link #bind(PropertyValues)};
* not applicable to field injection, and not to constructor binding
* via {@link #construct} either.
* <p>Used for setter injection - and as of 7.1 also for field injection -
* via {@link #bind(PropertyValues)}; not applicable to constructor binding
* via {@link #construct}.
* @see #initBeanPropertyAccess()
* @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit
*/
@@ -342,7 +342,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
*/
protected AbstractPropertyBindingResult createDirectFieldBindingResult() {
DirectFieldBindingResult result = new DirectFieldBindingResult(getTarget(),
getObjectName(), isAutoGrowNestedPaths());
getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit());
if (this.conversionService != null) {
result.initConversion(this.conversionService);
@@ -41,6 +41,8 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult {
private final boolean autoGrowNestedPaths;
private final int autoGrowCollectionLimit;
private transient @Nullable ConfigurablePropertyAccessor directFieldAccessor;
@@ -60,9 +62,24 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult {
* @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value
*/
public DirectFieldBindingResult(@Nullable Object target, String objectName, boolean autoGrowNestedPaths) {
this(target, objectName, autoGrowNestedPaths, Integer.MAX_VALUE);
}
/**
* Create a new {@code DirectFieldBindingResult} for the given target.
* @param target the target object to bind onto
* @param objectName the name of the target object
* @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value
* @param autoGrowCollectionLimit the limit for array and collection auto-growing
* @since 7.1
*/
public DirectFieldBindingResult(@Nullable Object target, String objectName,
boolean autoGrowNestedPaths, int autoGrowCollectionLimit) {
super(objectName);
this.target = target;
this.autoGrowNestedPaths = autoGrowNestedPaths;
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
}
@@ -82,6 +99,7 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult {
this.directFieldAccessor = createDirectFieldAccessor();
this.directFieldAccessor.setExtractOldValueForEditor(true);
this.directFieldAccessor.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
this.directFieldAccessor.setAutoGrowCollectionLimit(this.autoGrowCollectionLimit);
}
return this.directFieldAccessor;
}
@@ -22,8 +22,10 @@ import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.testfixture.beans.factory.BarRegistrar;
import org.springframework.context.testfixture.beans.factory.ConditionalBeanRegistrar;
import org.springframework.context.testfixture.beans.factory.FooRegistrar;
import org.springframework.context.testfixture.beans.factory.GenericBeanRegistrar;
import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar;
@@ -32,17 +34,28 @@ import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar
import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Foo;
import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Init;
import org.springframework.context.testfixture.context.annotation.registrar.BeanRegistrarConfiguration;
import org.springframework.context.testfixture.context.annotation.registrar.ComponentBeanRegistrar;
import org.springframework.context.testfixture.context.annotation.registrar.ComponentBeanRegistrar.IgnoredFromComponent;
import org.springframework.context.testfixture.context.annotation.registrar.ConditionalBeanRegistrarConfiguration;
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar;
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.BeanBeanRegistrar;
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromBean;
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromConfiguration;
import org.springframework.context.testfixture.context.annotation.registrar.GenericBeanRegistrarConfiguration;
import org.springframework.context.testfixture.context.annotation.registrar.ImportAwareBeanRegistrarConfiguration;
import org.springframework.context.testfixture.context.annotation.registrar.MultipleBeanRegistrarsConfiguration;
import org.springframework.context.testfixture.context.annotation.registrar.TestBeanConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
* Tests for {@link BeanRegistrar} imported by @{@link org.springframework.context.annotation.Configuration}.
*
* @author Sebastien Deleuze
* @author Stephane Nicoll
*/
class BeanRegistrarConfigurationTests {
@@ -59,6 +72,36 @@ class BeanRegistrarConfigurationTests {
assertThat(beanDefinition.getDescription()).isEqualTo("Custom description");
}
@Test
void beanRegistrarIgnoreBeans() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigurationBeanRegistrar.class);
assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class));
assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class));
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class));
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> context.getBean(IgnoredFromBean.class));
}
@Test
void beanRegistrarWithClasspathScanningIgnoreBeans() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.scan("org.springframework.context.testfixture.context.annotation.registrar");
context.refresh();
assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class));
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class));
assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class));
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> context.getBean(IgnoredFromBean.class));
assertThatNoException().isThrownBy(() -> context.getBean(ComponentBeanRegistrar.class));
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
.isThrownBy(() -> context.getBean(IgnoredFromComponent.class));
}
@Test
void beanRegistrarWithProfile() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@@ -105,4 +148,42 @@ class BeanRegistrarConfigurationTests {
assertThat(context.getBean(BarRegistrar.Bar.class)).isNotNull();
}
@Test
void programmaticBeanRegistrarIsInvokedBeforeConfigurationClassPostProcessor() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(TestBeanConfiguration.class);
context.register(new ConditionalBeanRegistrar());
context.refresh();
assertThat(context.containsBean("myTestBean")).isFalse();
}
@Test
void programmaticBeanRegistrarHandlesProgrammaticRegisteredBean() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(new ConditionalBeanRegistrar());
context.registerBean("testBean", TestBean.class);
context.refresh();
assertThat(context.containsBean("myTestBean")).isTrue();
assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class);
}
@Test
void importedBeanRegistrarWithConditionNotMet() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(ConditionalBeanRegistrarConfiguration.class);
context.register(TestBeanConfiguration.class);
context.refresh();
assertThat(context.containsBean("myTestBean")).isFalse();
}
@Test
void importedBeanRegistrarWithConditionMet() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(TestBeanConfiguration.class);
context.register(ConditionalBeanRegistrarConfiguration.class);
context.refresh();
assertThat(context.containsBean("myTestBean")).isTrue();
assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class);
}
}
@@ -46,9 +46,11 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.testfixture.beans.factory.CircularBeanRegistrar;
import org.springframework.context.testfixture.beans.factory.ConditionalBeanRegistrar;
import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar;
import org.springframework.context.testfixture.beans.factory.OverridingBeanRegistrar;
import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar;
@@ -653,8 +655,8 @@ class GenericApplicationContextTests {
void beanRegistrarWithDefinitionOverride() {
GenericApplicationContext context = new GenericApplicationContext();
context.setAllowBeanDefinitionOverriding(false);
assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(
() -> context.register(new OverridingBeanRegistrar()));
context.register(new OverridingBeanRegistrar());
assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(context::refresh);
}
@Test
@@ -665,6 +667,24 @@ class GenericApplicationContextTests {
assertThat(context.getBean(ImportAwareBeanRegistrar.ClassNameHolder.class).className()).isNull();
}
@Test
void beanRegistrarWithConditionNotMet() {
GenericApplicationContext context = new GenericApplicationContext();
context.register(new ConditionalBeanRegistrar());
context.refresh();
assertThat(context.containsBean("myTestBean")).isFalse();
}
@Test
void beanRegistrarWithConditionMet() {
GenericApplicationContext context = new GenericApplicationContext();
context.register(new ConditionalBeanRegistrar());
context.registerBean("testBean", TestBean.class);
context.refresh();
assertThat(context.containsBean("myTestBean")).isTrue();
assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class);
}
private MergedBeanDefinitionPostProcessor registerMockMergedBeanDefinitionPostProcessor(GenericApplicationContext context) {
MergedBeanDefinitionPostProcessor bpp = mock();
@@ -17,10 +17,12 @@
package org.springframework.validation;
import java.beans.PropertyEditorSupport;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.InvalidPropertyException;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.NotWritablePropertyException;
import org.springframework.beans.NullValueInNestedPathException;
@@ -134,6 +136,25 @@ class DataBinderFieldAccessTests {
binder.bind(pvs));
}
@Test
void directFieldAccessHonorsDefaultAutoGrowCollectionLimit() {
FieldAccessForm target = new FieldAccessForm();
DataBinder binder = new DataBinder(target);
binder.initDirectFieldAccess();
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("items[255].name", "value");
binder.bind(pvs);
assertThat(target.items).hasSize(256);
assertThat(target.items.get(255).name).isEqualTo("value");
MutablePropertyValues outOfBounds = new MutablePropertyValues();
outOfBounds.add("items[256].name", "too-far");
assertThatExceptionOfType(InvalidPropertyException.class).isThrownBy(() ->
binder.bind(outOfBounds));
}
@Test
void bindingWithErrorsAndCustomEditors() {
FieldAccessBean rod = new FieldAccessBean();
@@ -176,4 +197,16 @@ class DataBinderFieldAccessTests {
assertThat(tb.getSpouse()).isNotNull();
});
}
static class FieldAccessForm {
public List<FieldAccessItem> items;
}
static class FieldAccessItem {
public String name;
}
}
@@ -84,7 +84,13 @@ class BeanRegistrarDslConfigurationTests {
assertThat(context.getBeanProvider<Bar>().singleOrNull()).isNotNull
}
@Test
fun containsBean() {
AnnotationConfigApplicationContext(ContainsBeanRegistrarKotlinConfiguration::class.java)
}
class Foo
data class Bar(val foo: Foo)
data class Baz(val message: String = "")
class Init : InitializingBean {
@@ -145,4 +151,18 @@ class BeanRegistrarDslConfigurationTests {
private class ChainedBeanRegistrar : BeanRegistrarDsl({
register(SampleBeanRegistrar())
})
@Configuration
@Import(ContainsBeanRegistrar::class)
internal class ContainsBeanRegistrarKotlinConfiguration
private class ContainsBeanRegistrar : BeanRegistrarDsl({
assertThat(containsBean("foo")).isFalse()
assertThat(containsBean(Foo::class)).isFalse()
assertThat(containsBean<Foo>()).isFalse()
registerBean<Foo>("foo")
assertThat(containsBean("foo")).isTrue()
assertThat(containsBean(Foo::class)).isTrue()
assertThat(containsBean<Foo>()).isTrue()
})
}
@@ -0,0 +1,36 @@
/*
* 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.context.testfixture.beans.factory;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.BeanRegistry;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.env.Environment;
public class ConditionalBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
if (registry.containsBean("testBean") &&
registry.containsBean(TestBean.class) &&
registry.containsBean(new ParameterizedTypeReference<Comparable<Object>>() {
})) {
registry.registerBean("myTestBean", TestBean.class);
}
}
}
@@ -0,0 +1,35 @@
/*
* 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.context.testfixture.context.annotation.registrar;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.BeanRegistry;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
@Component
public class ComponentBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(IgnoredFromComponent.class);
}
public record IgnoredFromComponent() {}
}
@@ -0,0 +1,26 @@
/*
* 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.context.testfixture.context.annotation.registrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.testfixture.beans.factory.ConditionalBeanRegistrar;
@Configuration
@Import(ConditionalBeanRegistrar.class)
public class ConditionalBeanRegistrarConfiguration {
}
@@ -0,0 +1,49 @@
/*
* 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.context.testfixture.context.annotation.registrar;
import org.springframework.beans.factory.BeanRegistrar;
import org.springframework.beans.factory.BeanRegistry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class ConfigurationBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(IgnoredFromConfiguration.class);
}
@Bean
BeanBeanRegistrar beanBeanRegistrar() {
return new BeanBeanRegistrar();
}
public static class BeanBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean(IgnoredFromBean.class);
}
}
public record IgnoredFromConfiguration() {}
public record IgnoredFromBean() {}
}
@@ -0,0 +1,31 @@
/*
* 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.context.testfixture.context.annotation.registrar;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TestBeanConfiguration {
@Bean
public TestBean testBean() {
return new TestBean();
}
}
@@ -20,6 +20,8 @@ import java.util.Objects;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ClassUtils;
/**
* Reference to a Java method, identified by its owner class and the method name.
*
@@ -43,7 +45,7 @@ public final class MethodReference {
}
public static MethodReference of(Class<?> klass, String methodName) {
return new MethodReference(klass.getCanonicalName(), methodName);
return new MethodReference(ClassUtils.getCanonicalName(klass), methodName);
}
/**
@@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Record of an invocation of a method relevant to {@link org.springframework.aot.hint.RuntimeHints}.
@@ -181,7 +182,7 @@ public final class RecordedInvocation {
else {
Class<?> instanceType = (getInstance() instanceof Class<?> clazz) ? clazz : getInstance().getClass();
return "<%s> invocation of <%s> on type <%s> with arguments %s".formatted(
getHintType().hintClassName(), getMethodReference(), instanceType.getCanonicalName(), getArguments());
getHintType().hintClassName(), getMethodReference(), ClassUtils.getCanonicalName(instanceType), getArguments());
}
}
@@ -34,9 +34,11 @@ public class ClassNameReader {
private static class EarlyExitException extends RuntimeException {
}
// SPRING PATCH BEGIN
public static String getClassName(ClassReader r) {
return getClassInfo(r)[0];
return r.getClassName().replace('/', '.');
}
// SPRING PATCH END
public static String[] getClassInfo(ClassReader r) {
final List<String> array = new ArrayList<>();
@@ -160,6 +160,10 @@ public final class GenericTypeResolver {
resolvedTypeVariable = ResolvableType.forVariableBounds(typeVariable);
}
if (resolvedTypeVariable != ResolvableType.NONE) {
Type type = resolvedTypeVariable.getType();
if (type instanceof ParameterizedType) {
return resolveType(type, contextClass);
}
Class<?> resolved = resolvedTypeVariable.resolve();
if (resolved != null) {
return resolved;
@@ -22,6 +22,7 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
@@ -1152,7 +1153,6 @@ public class ResolvableType implements Serializable {
* @see #forClassWithGenerics(Class, ResolvableType...)
*/
public static ResolvableType forClassWithGenerics(Class<?> clazz, Class<?>... generics) {
Assert.notNull(clazz, "Class must not be null");
Assert.notNull(generics, "Generics array must not be null");
ResolvableType[] resolvableGenerics = new ResolvableType[generics.length];
for (int i = 0; i < generics.length; i++) {
@@ -1281,6 +1281,18 @@ public class ResolvableType implements Serializable {
return forType(null, new FieldTypeProvider(field), owner.asVariableResolver()).getNested(nestingLevel);
}
/**
* Return a {@code ResolvableType} for the specified {@link Parameter}.
* <p>This is a convenience factory method for scenarios where a {@code Parameter}
* descriptor is already available.
* @param parameter the source parameter
* @return a {@code ResolvableType} for the specified parameter
* @since 7.1
*/
public static ResolvableType forParameter(Parameter parameter) {
return forMethodParameter(MethodParameter.forParameter(parameter));
}
/**
* Return a {@code ResolvableType} for the specified {@link Constructor} parameter.
* @param constructor the source constructor (must not be {@code null})
@@ -1289,7 +1301,6 @@ public class ResolvableType implements Serializable {
* @see #forConstructorParameter(Constructor, int, Class)
*/
public static ResolvableType forConstructorParameter(Constructor<?> constructor, int parameterIndex) {
Assert.notNull(constructor, "Constructor must not be null");
return forMethodParameter(new MethodParameter(constructor, parameterIndex));
}
@@ -1307,7 +1318,6 @@ public class ResolvableType implements Serializable {
public static ResolvableType forConstructorParameter(Constructor<?> constructor, int parameterIndex,
Class<?> implementationClass) {
Assert.notNull(constructor, "Constructor must not be null");
MethodParameter methodParameter = new MethodParameter(constructor, parameterIndex, implementationClass);
return forMethodParameter(methodParameter);
}
@@ -1319,7 +1329,6 @@ public class ResolvableType implements Serializable {
* @see #forMethodReturnType(Method, Class)
*/
public static ResolvableType forMethodReturnType(Method method) {
Assert.notNull(method, "Method must not be null");
return forMethodParameter(new MethodParameter(method, -1));
}
@@ -1333,7 +1342,6 @@ public class ResolvableType implements Serializable {
* @see #forMethodReturnType(Method)
*/
public static ResolvableType forMethodReturnType(Method method, Class<?> implementationClass) {
Assert.notNull(method, "Method must not be null");
MethodParameter methodParameter = new MethodParameter(method, -1, implementationClass);
return forMethodParameter(methodParameter);
}
@@ -1347,7 +1355,6 @@ public class ResolvableType implements Serializable {
* @see #forMethodParameter(MethodParameter)
*/
public static ResolvableType forMethodParameter(Method method, int parameterIndex) {
Assert.notNull(method, "Method must not be null");
return forMethodParameter(new MethodParameter(method, parameterIndex));
}
@@ -1363,7 +1370,6 @@ public class ResolvableType implements Serializable {
* @see #forMethodParameter(MethodParameter)
*/
public static ResolvableType forMethodParameter(Method method, int parameterIndex, Class<?> implementationClass) {
Assert.notNull(method, "Method must not be null");
MethodParameter methodParameter = new MethodParameter(method, parameterIndex, implementationClass);
return forMethodParameter(methodParameter);
}
@@ -24,6 +24,7 @@ import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
* Abstract base class for {@link MergedAnnotation} implementations.
@@ -215,7 +216,7 @@ abstract class AbstractMergedAnnotation<A extends Annotation> implements MergedA
T value = getAttributeValue(attributeName, type);
if (value == null) {
throw new NoSuchElementException("No attribute named '" + attributeName +
"' present in merged annotation " + getType().getName());
"' present in merged annotation " + ClassUtils.getCanonicalName(getType()));
}
return value;
}
@@ -25,6 +25,7 @@ import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
@@ -126,7 +127,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, @Nullable Object
AnnotationAttributes(Class<? extends Annotation> annotationType, boolean validated) {
Assert.notNull(annotationType, "'annotationType' must not be null");
this.annotationType = annotationType;
this.displayName = annotationType.getName();
this.displayName = ClassUtils.getCanonicalName(annotationType);
this.validated = validated;
}
@@ -32,6 +32,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -649,7 +650,7 @@ final class AnnotationTypeMapping {
throw new AnnotationConfigurationException(String.format(
"Different @AliasFor mirror values for annotation [%s]%s; attribute '%s' " +
"and its alias '%s' are declared with values of [%s] and [%s].",
getAnnotationType().getName(), on,
ClassUtils.getCanonicalName(getAnnotationType()), on,
attributes.get(result).getName(),
attribute.getName(),
ObjectUtils.nullSafeToString(lastValue),
@@ -28,6 +28,7 @@ import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
/**
@@ -124,7 +125,8 @@ final class AnnotationTypeMappings {
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
if (failureLogger.isEnabled()) {
failureLogger.log("Failed to introspect " + (meta ? "meta-annotation @" : "annotation @") +
annotationType.getName(), (source != null ? source.getAnnotationType() : null), ex);
ClassUtils.getCanonicalName(annotationType),
(source != null ? ClassUtils.getCanonicalName(source.getAnnotationType()) : null), ex);
}
}
}
@@ -36,6 +36,7 @@ import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
@@ -177,6 +178,23 @@ public abstract class AnnotationUtils {
return true;
}
/**
* Load the specified annotation type, if available.
* @param annotationName the fully-qualified name of the annotation type
* @return the annotation type as a {@code Class}, or {@code null} if not found
* @since 7.1
*/
@SuppressWarnings("unchecked")
public static @Nullable Class<? extends Annotation> loadAnnotationType(String annotationName) {
try {
return (Class<? extends Annotation>)
ClassUtils.forName(annotationName, AnnotationUtils.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
return null;
}
}
/**
* Get a single {@link Annotation} of {@code annotationType} from the supplied
* annotation: either the given annotation itself or a direct meta-annotation
@@ -26,6 +26,7 @@ import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ReflectionUtils;
@@ -143,8 +144,9 @@ final class AttributeMethods {
throw ex;
}
catch (Throwable ex) {
throw new IllegalStateException("Could not obtain annotation attribute value for " +
get(i).getName() + " declared on @" + getName(annotation.annotationType()), ex);
throw new IllegalStateException(
"Could not obtain annotation attribute value for " + get(i).getName() +
" declared on @" + ClassUtils.getCanonicalName(annotation.annotationType()), ex);
}
}
}
@@ -305,13 +307,8 @@ final class AttributeMethods {
if (attributeName == null) {
return "(none)";
}
String in = (annotationType != null ? " in annotation [" + annotationType.getName() + "]" : "");
String in = (annotationType != null ? " in annotation [" + ClassUtils.getCanonicalName(annotationType) + "]" : "");
return "attribute '" + attributeName + "'" + in;
}
private static String getName(Class<?> clazz) {
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
}
@@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
import org.springframework.lang.Contract;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ObjectUtils;
@@ -312,7 +313,7 @@ public abstract class RepeatableContainers {
if (returnType.componentType() != repeatable) {
throw new AnnotationConfigurationException(
"Container type [%s] must declare a 'value' attribute for an array of type [%s]"
.formatted(container.getName(), repeatable.getName()));
.formatted(ClassUtils.getCanonicalName(container), ClassUtils.getCanonicalName(repeatable)));
}
}
catch (AnnotationConfigurationException ex) {
@@ -321,7 +322,7 @@ public abstract class RepeatableContainers {
catch (Throwable ex) {
throw new AnnotationConfigurationException(
"Invalid declaration of container type [%s] for repeatable annotation [%s]"
.formatted(container.getName(), repeatable.getName()), ex);
.formatted(ClassUtils.getCanonicalName(container), ClassUtils.getCanonicalName(repeatable)), ex);
}
this.repeatable = repeatable;
this.container = container;
@@ -331,7 +332,7 @@ public abstract class RepeatableContainers {
private Class<? extends Annotation> deduceContainer(Class<? extends Annotation> repeatable) {
Repeatable annotation = repeatable.getAnnotation(Repeatable.class);
Assert.notNull(annotation, () -> "Annotation type must be a repeatable annotation: " +
"failed to resolve container type for " + repeatable.getName());
"failed to resolve container type for " + ClassUtils.getCanonicalName(repeatable));
return annotation.value();
}
@@ -136,7 +136,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
private String annotationToString() {
String string = this.string;
if (string == null) {
StringBuilder builder = new StringBuilder("@").append(getName(this.type)).append('(');
StringBuilder builder = new StringBuilder("@").append(ClassUtils.getCanonicalName(this.type)).append('(');
if (this.attributes.size() == 1 && this.attributes.get(0).getName().equals(MergedAnnotation.VALUE)) {
// Don't prepend "value=" for an annotation that only declares a "value" attribute.
builder.append(toString(getAttributeValue(this.attributes.get(0))));
@@ -208,7 +208,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
return e.name();
}
if (type == Class.class) {
return getName((Class<?>) value) + ".class";
return ClassUtils.getCanonicalName((Class<?>) value) + ".class";
}
return String.valueOf(value);
}
@@ -218,7 +218,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
return this.annotation.getValue(attributeName, type).orElseThrow(
() -> new NoSuchElementException("No value found for attribute named '" + attributeName +
"' in merged annotation " + getName(this.annotation.getType())));
"' in merged annotation " + ClassUtils.getCanonicalName(this.annotation.getType())));
});
// Clone non-empty arrays so that users cannot alter the contents of values in our cache.
@@ -272,9 +272,4 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
}
private static String getName(Class<?> clazz) {
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
}
@@ -450,7 +450,12 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
value = clazz.getName();
}
else if (value instanceof String str && type == Class.class) {
value = ClassUtils.resolveClassName(str, getClassLoader());
try {
value = ClassUtils.forName(str, getClassLoader());
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(str, ex);
}
}
else if (value instanceof Class<?>[] classes && type == String[].class) {
String[] names = new String[classes.length];
@@ -461,8 +466,14 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
}
else if (value instanceof String[] names && type == Class[].class) {
Class<?>[] classes = new Class<?>[names.length];
ClassLoader classLoader = getClassLoader();
for (int i = 0; i < names.length; i++) {
classes[i] = ClassUtils.resolveClassName(names[i], getClassLoader());
try {
classes[i] = ClassUtils.forName(names[i], classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(names[i], ex);
}
}
value = classes;
}
@@ -479,7 +490,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
}
if (!type.isInstance(value)) {
throw new IllegalArgumentException("Unable to adapt value of type " +
value.getClass().getName() + " to " + type.getName());
ClassUtils.getCanonicalName(value.getClass()) + " to " + ClassUtils.getCanonicalName(type));
}
return (T) value;
}
@@ -514,8 +525,8 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
}
if (!attributeType.isInstance(value)) {
throw new IllegalStateException("Attribute '" + attribute.getName() +
"' in annotation " + getType().getName() + " should be compatible with " +
attributeType.getName() + " but a " + value.getClass().getName() +
"' in annotation " + ClassUtils.getCanonicalName(getType()) + " should be compatible with " +
ClassUtils.getCanonicalName(attributeType) + " but a " + ClassUtils.getCanonicalName(value.getClass()) +
" value was returned");
}
return value;
@@ -572,7 +583,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
int attributeIndex = (isFiltered(attributeName) ? -1 : this.mapping.getAttributes().indexOf(attributeName));
if (attributeIndex == -1 && required) {
throw new NoSuchElementException("No attribute named '" + attributeName +
"' present in merged annotation " + getType().getName());
"' present in merged annotation " + ClassUtils.getCanonicalName(getType()));
}
return attributeIndex;
}
@@ -649,9 +660,10 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
catch (Exception ex) {
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
if (logger.isEnabled()) {
String type = mapping.getAnnotationType().getName();
String type = ClassUtils.getCanonicalName(mapping.getAnnotationType());
String item = (mapping.getDistance() == 0 ? "annotation " + type :
"meta-annotation " + type + " from " + mapping.getRoot().getAnnotationType().getName());
"meta-annotation " + type + " from " +
ClassUtils.getCanonicalName(mapping.getRoot().getAnnotationType()));
logger.log("Failed to introspect " + item, source, ex);
}
return null;
@@ -541,7 +541,7 @@ public class TypeDescriptor implements Serializable {
public String toString() {
StringBuilder builder = new StringBuilder();
for (Annotation ann : getAnnotations()) {
builder.append('@').append(getName(ann.annotationType())).append(' ');
builder.append('@').append(ClassUtils.getCanonicalName(ann.annotationType())).append(' ');
}
builder.append(getResolvableType());
return builder.toString();
@@ -726,11 +726,6 @@ public class TypeDescriptor implements Serializable {
return new TypeDescriptor(property).nested(nestingLevel);
}
private static String getName(Class<?> clazz) {
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getName());
}
private interface AnnotatedElementSupplier extends Supplier<AnnotatedElementAdapter>, Serializable {
}
@@ -88,13 +88,10 @@ final class ProfilesParser {
}
case "!" -> elements.add(not(parseTokens(expression, tokens, Context.NEGATE)));
case ")" -> {
Profiles merged = merge(expression, elements, operator);
if (context == Context.PARENTHESIS) {
return merged;
return merge(expression, elements, operator);
}
elements.clear();
elements.add(merged);
operator = null;
assertWellFormed(expression, false);
}
default -> {
Profiles value = equals(token);
@@ -105,6 +102,7 @@ final class ProfilesParser {
}
}
}
assertWellFormed(expression, context != Context.PARENTHESIS);
return merge(expression, elements, operator);
}
@@ -16,10 +16,19 @@
package org.springframework.core.io;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.SequenceInputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@@ -30,6 +39,7 @@ import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ResourceUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.function.IOConsumer;
/**
* Default implementation of the {@link ResourceLoader} interface.
@@ -158,6 +168,9 @@ public class DefaultResourceLoader implements ResourceLoader {
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else if (location.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
return new ClassPathAllResource(location.substring(CLASSPATH_ALL_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// Try to parse the location as a URL...
@@ -187,6 +200,96 @@ public class DefaultResourceLoader implements ResourceLoader {
}
/**
* A multi-content ClassPathResource handle that can expose the content
* from all matching resources in the classpath.
* @since 7.1
*/
protected static class ClassPathAllResource extends ClassPathResource {
public ClassPathAllResource(String path, @Nullable ClassLoader classLoader) {
super(path, classLoader);
}
@Override
public boolean isFile() {
return false;
}
@Override
public URL getURL() throws IOException {
throw new FileNotFoundException(
getDescription() + " cannot be resolved to single URL or File - use 'classpath:' instead");
}
@Override
public long contentLength() throws IOException {
long combinedLength = 0;
ClassLoader cl = getClassLoader();
Enumeration<URL> urls = (cl != null ? cl.getResources(getPath()) : ClassLoader.getSystemResources(getPath()));
while (urls.hasMoreElements()) {
URLConnection con = urls.nextElement().openConnection();
long length = con.getContentLengthLong();
if (length < 0) {
return -1;
}
combinedLength += length;
}
return combinedLength;
}
@Override
public InputStream getInputStream() throws IOException {
List<InputStream> streams = new ArrayList<>();
ClassLoader cl = getClassLoader();
Enumeration<URL> urls = (cl != null ? cl.getResources(getPath()) : ClassLoader.getSystemResources(getPath()));
while (urls.hasMoreElements()) {
try {
streams.add(urls.nextElement().openStream());
}
catch (IOException ex) {
streams.forEach(stream -> {
try {
stream.close();
}
catch (IOException ex2) {
ex.addSuppressed(ex2);
}
});
throw ex;
}
}
return switch (streams.size()) {
case 0 -> InputStream.nullInputStream();
case 1 -> streams.get(0);
default -> new SequenceInputStream(Collections.enumeration(streams));
};
}
@Override
public void consumeContent(IOConsumer<InputStream> consumer) throws IOException {
ClassLoader cl = getClassLoader();
Enumeration<URL> urls = (cl != null ? cl.getResources(getPath()) : ClassLoader.getSystemResources(getPath()));
while (urls.hasMoreElements()) {
try (InputStream inputStream = urls.nextElement().openStream()) {
consumer.accept(inputStream);
}
}
}
@Override
public Resource createRelative(String relativePath) {
String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
return new ClassPathAllResource(pathToUse, getClassLoader());
}
@Override
public String getDescription() {
return "'classpath*:' resource [" + getPath() + "]";
}
}
/**
* ClassPathResource that explicitly expresses a context-relative path
* through implementing the ContextResource interface.
@@ -30,6 +30,7 @@ import java.nio.file.Path;
import org.jspecify.annotations.Nullable;
import org.springframework.util.FileCopyUtils;
import org.springframework.util.function.IOConsumer;
/**
* Interface for a resource descriptor that abstracts from the actual
@@ -156,6 +157,26 @@ public interface Resource extends InputStreamSource {
return Channels.newChannel(getInputStream());
}
/**
* Process the contents of this resource through the given consumer callback.
* <p>The given consumer will be invoked a single time by default - but may
* also be invoked multiple times in case of a multi-content resource handle,
* for example returned from a
* {@link ResourceLoader#getResource getResource("classpath*:...")} call.
* While {@link #getInputStream()} returns a merged sequence of content
* in such a case, this method performs one callback per file content.
* @param consumer a consumer for each InputStream
* @throws IOException in case of general resolution/reading failures
* @since 7.1
* @see #getInputStream()
* @see ResourceLoader#CLASSPATH_ALL_URL_PREFIX
*/
default void consumeContent(IOConsumer<InputStream> consumer) throws IOException {
try (InputStream inputStream = getInputStream()) {
consumer.accept(inputStream);
}
}
/**
* Return the contents of this resource as a byte array.
* @return the contents of this resource as byte array
@@ -42,18 +42,47 @@ import org.springframework.util.ResourceUtils;
*/
public interface ResourceLoader {
/** Pseudo URL prefix for loading from the class path: "classpath:". */
/**
* Pseudo URL prefix for loading from the class path: {@value}.
* <p>This retrieves the "nearest" matching resource in the classpath.
* @see ClassLoader#getResource
*/
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
/**
* Pseudo URL prefix for all matching resources from the class path: {@value}.
* <p>This differs from the common {@link #CLASSPATH_URL_PREFIX "classpath:"} prefix
* in that it retrieves all matching resources for a given path. For example, to
* locate all "messages.properties" files in the root of all deployed JAR files
* you can use the location pattern {@code "classpath*:/messages.properties"}.
* <p>As of Spring Framework 6.0, the semantics for the {@code "classpath*:"}
* prefix have been expanded to include the module path as well as the class path.
* <p>As of Spring Framework 7.1, this prefix is supported for {@link #getResource}
* calls as well (exposing a multi-content resource handle), rather than just for
* {@link org.springframework.core.io.support.ResourcePatternResolver#getResources}.
* @since 7.1 (previously only declared on the
* {@link org.springframework.core.io.support.ResourcePatternResolver} sub-interface)
* @see ClassLoader#getResources
* @see Resource#consumeContent
*/
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
/**
* Return a {@code Resource} handle for the specified resource location.
* <p>The handle should always be a reusable resource descriptor,
* allowing for multiple {@link Resource#getInputStream()} calls.
* <p><ul>
* <li>Must support fully qualified URLs, for example, "file:C:/test.dat".
* <li>Must support classpath pseudo-URLs, for example, "classpath:test.dat".
* <li>Should support relative file paths, for example, "WEB-INF/test.dat".
* <ul>
* <li>Must support fully qualified URLs, for example, "file:C:/test.properties".
* <li>Must support classpath pseudo-URLs, for example, "classpath:test.properties".
* (Exposing the "nearest" resource in the classpath; see {@link ClassLoader#getResource}.)
* <li>Should support classpath-all URLs, for example, "classpath*:test.properties".
* (If supported, the returned {@code Resource} needs to expose the entire content of
* all same-named resources in the classpath through {@link Resource#consumeContent};
* see {@link ClassLoader#getResources}.
* For individual access to each such matching resource in the classpath, use
* {@link org.springframework.core.io.support.ResourcePatternResolver#getResources}.)
* <li>Should support relative file paths, for example, "WEB-INF/test.properties".
* (This will be implementation-specific, typically provided by an
* ApplicationContext implementation.)
* </ul>
@@ -62,8 +91,9 @@ public interface ResourceLoader {
* @param location the resource location
* @return a corresponding {@code Resource} handle (never {@code null})
* @see #CLASSPATH_URL_PREFIX
* @see #CLASSPATH_ALL_URL_PREFIX
* @see Resource#exists()
* @see Resource#getInputStream()
* @see Resource#consumeContent
*/
Resource getResource(String location);
@@ -26,8 +26,10 @@ import org.jspecify.annotations.Nullable;
import org.springframework.core.io.InputStreamSource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
import org.springframework.util.function.IOConsumer;
/**
* Holder that combines a {@link Resource} descriptor with a specific encoding
@@ -125,26 +127,6 @@ public class EncodedResource implements InputStreamSource {
return (this.encoding != null || this.charset != null);
}
/**
* Open a {@code java.io.Reader} for the specified resource, using the specified
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
* (if any).
* @throws IOException if opening the Reader failed
* @see #requiresReader()
* @see #getInputStream()
*/
public Reader getReader() throws IOException {
if (this.charset != null) {
return new InputStreamReader(this.resource.getInputStream(), this.charset);
}
else if (this.encoding != null) {
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
}
else {
return new InputStreamReader(this.resource.getInputStream());
}
}
/**
* Open an {@code InputStream} for the specified resource, ignoring any specified
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}.
@@ -157,6 +139,47 @@ public class EncodedResource implements InputStreamSource {
return this.resource.getInputStream();
}
/**
* Open a {@code java.io.Reader} for the specified resource, using the specified
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
* (if any).
* @throws IOException if opening the Reader failed
* @see #requiresReader()
* @see #getInputStream()
*/
public Reader getReader() throws IOException {
return getReader(this.resource.getInputStream());
}
private Reader getReader(InputStream inputStream) throws IOException {
if (this.charset != null) {
return new InputStreamReader(inputStream, this.charset);
}
else if (this.encoding != null) {
return new InputStreamReader(inputStream, this.encoding);
}
else {
return new InputStreamReader(inputStream);
}
}
/**
* Process the contents of this resource through the given consumer callback.
* <p>The given consumer will be invoked a single time by default - but may
* also be invoked multiple times in case of a multi-content resource handle,
* for example returned from a
* {@link ResourceLoader#getResource getResource("classpath*:...")} call.
* While {@link #getReader()} returns a merged sequence of content
* in such a case, this method performs one callback per file content.
* @param consumer a consumer for each Reader
* @throws IOException in case of general resolution/reading failures
* @since 7.1
* @see Resource#consumeContent
*/
public void consumeContent(IOConsumer<Reader> consumer) throws IOException {
this.resource.consumeContent(inputStream -> consumer.accept(getReader(inputStream)));
}
/**
* Returns the contents of the specified resource as a string, using the specified
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding} (if any).
@@ -57,18 +57,6 @@ import org.springframework.core.io.ResourceLoader;
*/
public interface ResourcePatternResolver extends ResourceLoader {
/**
* Pseudo URL prefix for all matching resources from the class path: {@code "classpath*:"}.
* <p>This differs from ResourceLoader's {@code "classpath:"} URL prefix in
* that it retrieves all matching resources for a given path &mdash; for
* example, to locate all "beans.xml" files in the root of all deployed JAR
* files you can use the location pattern {@code "classpath*:/beans.xml"}.
* <p>As of Spring Framework 6.0, the semantics for the {@code "classpath*:"}
* prefix have been expanded to include the module path as well as the class path.
* @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX
*/
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
/**
* Resolve the given location pattern into {@code Resource} objects.
* <p>Overlapping resource entries that point to the same physical
@@ -23,6 +23,7 @@ import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ExceptionTypeFilter;
import org.springframework.util.backoff.BackOff;
@@ -96,8 +97,7 @@ class DefaultRetryPolicy implements RetryPolicy {
private static String names(Set<Class<? extends Throwable>> types) {
StringJoiner result = new StringJoiner(", ", "[", "]");
for (Class<? extends Throwable> type : types) {
String name = type.getCanonicalName();
result.add(name != null? name : type.getName());
result.add(ClassUtils.getCanonicalName(type));
}
return result.toString();
}
@@ -24,6 +24,8 @@ import java.util.StringJoiner;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.util.ClassUtils;
/**
* {@link ValueStyler} that converts objects to String form &mdash; generally for
* debugging purposes &mdash; using simple styling conventions that mimic the
@@ -45,7 +47,7 @@ public class SimpleValueStyler extends DefaultValueStyler {
/**
* Default {@link Class} styling function: {@link Class#getCanonicalName()}.
*/
public static final Function<Class<?>, String> DEFAULT_CLASS_STYLER = Class::getCanonicalName;
public static final Function<Class<?>, String> DEFAULT_CLASS_STYLER = ClassUtils::getCanonicalName;
/**
* Default {@link Method} styling function: converts the supplied {@link Method}
@@ -54,6 +54,7 @@ public interface MethodMetadata extends AnnotatedTypeMetadata {
/**
* Get the fully-qualified name of the underlying method's declared return type.
* @since 4.2
* @see Class#getTypeName()
*/
String getReturnTypeName();
@@ -103,7 +103,7 @@ public class StandardMethodMetadata implements MethodMetadata {
@Override
public String getReturnTypeName() {
return this.introspectedMethod.getReturnType().getName();
return this.introspectedMethod.getReturnType().getTypeName();
}
@Override
@@ -101,7 +101,13 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
@SuppressWarnings("unchecked")
public <E extends Enum<E>> void visitEnum(String descriptor, String value, Consumer<E> consumer) {
String className = Type.getType(descriptor).getClassName();
Class<E> type = (Class<E>) ClassUtils.resolveClassName(className, this.classLoader);
Class<E> type = null;
try {
type = (Class<E>) ClassUtils.forName(className, this.classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(className, ex);
}
consumer.accept(Enum.valueOf(type, value));
}
@@ -113,7 +119,13 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
if (AnnotationFilter.PLAIN.matches(className)) {
return null;
}
Class<T> type = (Class<T>) ClassUtils.resolveClassName(className, this.classLoader);
Class<T> type = null;
try {
type = (Class<T>) ClassUtils.forName(className, this.classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(className, ex);
}
return new MergedAnnotationReadingVisitor<>(this.classLoader, this.source, type, consumer);
}
@@ -16,7 +16,9 @@
package org.springframework.core.type.classreading;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
@@ -57,7 +59,7 @@ final class SimpleAnnotationMetadataReadingVisitor extends ClassVisitor {
private final Set<String> memberClassNames = new LinkedHashSet<>(4);
private final Set<MergedAnnotation<?>> annotations = new LinkedHashSet<>(4);
private final List<MergedAnnotation<?>> annotations = new ArrayList<>(4);
private final Set<MethodMetadata> declaredMethods = new LinkedHashSet<>(4);
@@ -1131,11 +1131,26 @@ public abstract class ClassUtils {
return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
}
/**
* Return the {@linkplain Class#getCanonicalName() canonical name} of the given
* class, or the {@linkplain Class#getTypeName() type name} of the class if the
* canonical name does not exist.
* @param clazz the class
* @return the canonical name of the class, or the type name as a fallback
* @since 7.1
*/
public static String getCanonicalName(Class<?> clazz) {
Assert.notNull(clazz, "Class must not be null");
String canonicalName = clazz.getCanonicalName();
return (canonicalName != null ? canonicalName : clazz.getTypeName());
}
/**
* Return the qualified name of the given class: usually simply
* the class name, but component type class name + "[]" for arrays.
* @param clazz the class
* @return the qualified name of the class
* @see Class#getTypeName()
*/
public static String getQualifiedName(Class<?> clazz) {
Assert.notNull(clazz, "Class must not be null");
@@ -367,7 +367,7 @@ public final class ConcurrentLruCache<K, V> {
private static final class ReadOperations<K, V> {
private static final int BUFFER_COUNT = detectNumberOfBuffers();
private static final int BUFFER_COUNT = 4;
private static final int BUFFERS_MASK = BUFFER_COUNT - 1;
@@ -450,11 +450,6 @@ public final class ConcurrentLruCache<K, V> {
this.processedCount.lazySet(bufferIndex, writeCount);
}
private static int detectNumberOfBuffers() {
int availableProcessors = Runtime.getRuntime().availableProcessors();
int nextPowerOfTwo = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(availableProcessors - 1));
return Math.min(4, nextPowerOfTwo);
}
}
@@ -252,7 +252,9 @@ public abstract class MimeTypeUtils {
if (eqIndex >= 0) {
String attribute = parameter.substring(0, eqIndex).trim();
String value = parameter.substring(eqIndex + 1).trim();
parameters.put(attribute, value);
if (parameters.put(attribute, value) != null) {
throw new InvalidMimeTypeException(mimeType, "duplicate parameter '" + parameter + "'");
}
}
}
index = nextIndex;
@@ -0,0 +1,61 @@
/*
* 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.util.function;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.function.Consumer;
import org.springframework.core.io.Resource;
/**
* Common functional interface for I/O content consumption, for example
* consuming an {@link java.io.InputStream} or a {@link java.io.Reader}.
*
* @author Juergen Hoeller
* @since 7.1
* @param <C> the type of stream/reader
* @see Resource#consumeContent
* @see org.springframework.core.io.support.EncodedResource#consumeContent
* @see ThrowingConsumer
*/
@FunctionalInterface
public interface IOConsumer<C> extends Consumer<C> {
/**
* Performs this operation on the given argument, possibly throwing
* an {@link IOException}.
* @param content the stream/reader
* @throws IOException on error
*/
void acceptWithException(C content) throws IOException;
/**
* Default {@link Consumer#accept(Object)} that wraps any thrown
* {@link IOException} in an {@link UncheckedIOException}.
* @see java.util.function.Consumer#accept(Object)
*/
default void accept(C input) {
try {
acceptWithException(input);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
}
}
@@ -139,7 +139,12 @@ abstract class ClassFileAnnotationDelegate {
private static Class<?> loadEnumClass(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) {
String className = ClassFileAnnotationMetadata.resolveTypeName(enumValue.classSymbol());
return ClassUtils.resolveClassName(className, classLoader);
try {
return ClassUtils.forName(className, classLoader);
}
catch (ClassNotFoundException | LinkageError ex) {
throw new TypeNotPresentException(className, ex);
}
}
private static Class<?> resolveArrayElementType(List<AnnotationValue> values, @Nullable ClassLoader classLoader) {
@@ -255,7 +255,7 @@ final class ClassFileAnnotationMetadata implements AnnotationMetadata {
private Set<MethodMetadata> declaredMethods = new LinkedHashSet<>(4);
private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet());
private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptyList());
public Builder(ClassLoader classLoader) {
this.classLoader = classLoader;
@@ -25,6 +25,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
@@ -250,6 +251,15 @@ class GenericTypeResolverTests {
assertThat(resolvedType).isEqualTo(InheritsDefaultMethod.ConcreteType.class);
}
@Test
void resolveTypeFromNestedParameterizedType() {
Type resolvedType = resolveType(method(MyInterfaceType.class, "get").getGenericReturnType(), MyCollectionInterfaceType.class);
assertThat(resolvedType).isEqualTo(method(MyCollectionInterfaceType.class, "get").getGenericReturnType());
resolvedType = resolveType(method(MyInterfaceType.class, "get").getGenericReturnType(), MyOptionalInterfaceType.class);
assertThat(resolvedType).isEqualTo(method(MyOptionalInterfaceType.class, "get").getGenericReturnType());
}
private static Method method(Class<?> target, String methodName, Class<?>... parameterTypes) {
Method method = findMethod(target, methodName, parameterTypes);
assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull();
@@ -258,12 +268,29 @@ class GenericTypeResolverTests {
public interface MyInterfaceType<T> {
default T get() {
return null;
}
}
public class MySimpleInterfaceType implements MyInterfaceType<String> {
}
public class MyParameterizedInterfaceType<P> implements MyInterfaceType<Collection<P>> {
}
public class MyOptionalInterfaceType extends MyParameterizedInterfaceType<Optional<String>> {
@Override
public Collection<Optional<String>> get() {
return super.get();
}
}
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {
@Override
public Collection<String> get() {
return MyInterfaceType.super.get();
}
}
public abstract class MyAbstractType<T> implements MyInterfaceType<T> {
@@ -25,6 +25,7 @@ import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
@@ -207,6 +208,29 @@ class ResolvableTypeTests {
.withMessage("Field must not be null");
}
@Test
void forParameterForMethod() throws Exception {
Method method = Methods.class.getMethod("charSequenceParameter", List.class);
Parameter parameter = method.getParameters()[0];
ResolvableType type = ResolvableType.forParameter(parameter);
assertThat(type.getType()).isEqualTo(method.getGenericParameterTypes()[0]);
}
@Test
void forParameterForConstructor() throws Exception {
Constructor<Constructors> constructor = Constructors.class.getConstructor(List.class);
Parameter parameter = constructor.getParameters()[0];
ResolvableType type = ResolvableType.forParameter(parameter);
assertThat(type.getType()).isEqualTo(constructor.getGenericParameterTypes()[0]);
}
@Test
void forParameterMustNotBeNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ResolvableType.forParameter(null))
.withMessage("Parameter must not be null");
}
@Test
void forConstructorParameter() throws Exception {
Constructor<Constructors> constructor = Constructors.class.getConstructor(List.class);
@@ -221,6 +245,13 @@ class ResolvableTypeTests {
.withMessage("Constructor must not be null");
}
@Test
void forConstructorParameterWithImplementationClassMustNotBeNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ResolvableType.forConstructorParameter(null, 0, TypedConstructors.class))
.withMessage("Executable must not be null");
}
@Test
void forMethodParameterByIndex() throws Exception {
Method method = Methods.class.getMethod("charSequenceParameter", List.class);
@@ -235,6 +266,13 @@ class ResolvableTypeTests {
.withMessage("Method must not be null");
}
@Test
void forMethodParameterByIndexWithImplementationClassMustNotBeNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ResolvableType.forMethodParameter(null, 0, TypedMethods.class))
.withMessage("Executable must not be null");
}
@Test
void forMethodParameter() throws Exception {
Method method = Methods.class.getMethod("charSequenceParameter", List.class);
@@ -302,6 +340,13 @@ class ResolvableTypeTests {
.withMessage("Method must not be null");
}
@Test
void forMethodReturnWithImplementationClassMustNotBeNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ResolvableType.forMethodReturnType(null, TypedMethods.class))
.withMessage("Executable must not be null");
}
@Test // gh-27748
void genericMatchesReturnType() throws Exception {
Method method = SomeRepository.class.getMethod("someMethod", Class.class, Class.class, Class.class);
@@ -1307,6 +1352,13 @@ class ResolvableTypeTests {
assertThat(type.asMap().toString()).isEqualTo("java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>");
}
@Test
void forClassWithGenericsClassMustNotBeNull() {
assertThatIllegalArgumentException()
.isThrownBy(() -> ResolvableType.forClassWithGenerics(null, String.class))
.withMessage("Class must not be null");
}
@Test
void forClassWithMismatchedGenerics() {
assertThatIllegalArgumentException()
@@ -102,7 +102,7 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithBothValueAndAttribute.class))
.withMessage("In @AliasFor declared on attribute 'test' in annotation [%s], attribute 'attribute' " +
"and its alias 'value' are present with values of 'foo' and 'bar', but only one is permitted.",
AliasForWithBothValueAndAttribute.class.getName());
AliasForWithBothValueAndAttribute.class.getCanonicalName());
}
@Test
@@ -111,7 +111,7 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfNonExistingAttribute.class))
.withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] " +
"declares an alias for 'missing' which is not present.",
AliasForToSelfNonExistingAttribute.class.getName());
AliasForToSelfNonExistingAttribute.class.getCanonicalName());
}
@Test
@@ -119,8 +119,8 @@ class AnnotationTypeMappingsTests {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToOtherNonExistingAttribute.class))
.withMessage("Attribute 'test' in annotation [%s] is declared as an @AliasFor nonexistent " +
"attribute 'missing' in annotation [%s].", AliasForToOtherNonExistingAttribute.class.getName(),
AliasForToOtherNonExistingAttributeTarget.class.getName());
"attribute 'missing' in annotation [%s].", AliasForToOtherNonExistingAttribute.class.getCanonicalName(),
AliasForToOtherNonExistingAttributeTarget.class.getCanonicalName());
}
@Test
@@ -129,7 +129,7 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelf.class))
.withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] points to itself. " +
"Specify 'annotation' to point to a same-named attribute on a meta-annotation.",
AliasForToSelf.class.getName());
AliasForToSelf.class.getCanonicalName());
}
@Test
@@ -147,13 +147,13 @@ class AnnotationTypeMappingsTests {
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithIncompatibleReturnTypes.class))
.withMessage("Misconfigured aliases: attribute 'test' in annotation [%s] and attribute 'test' " +
"in annotation [%s] must declare the same return type.",
AliasForWithIncompatibleReturnTypes.class.getName(),
AliasForWithIncompatibleReturnTypesTarget.class.getName());
AliasForWithIncompatibleReturnTypes.class.getCanonicalName(),
AliasForWithIncompatibleReturnTypesTarget.class.getCanonicalName());
}
@Test
void forAnnotationTypeWhenAliasForToSelfAnnotatedToOtherAttribute() {
String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getName();
String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getCanonicalName();
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfAnnotatedToOtherAttribute.class))
.withMessage("Attribute 'b' in annotation [%1$s] must be declared as an @AliasFor attribute 'a' in " +
@@ -167,8 +167,8 @@ class AnnotationTypeMappingsTests {
}
private void assertMixedImplicitAndExplicitAliases(Class<? extends Annotation> annotationType, String overriddenAttribute) {
String annotationName = annotationType.getName();
String metaAnnotationName = AliasPair.class.getName();
String annotationName = annotationType.getCanonicalName();
String metaAnnotationName = AliasPair.class.getCanonicalName();
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(annotationType))
.withMessage("Attribute 'b' in annotation [" + annotationName +
@@ -180,8 +180,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasForNonMetaAnnotated() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForNonMetaAnnotated.class))
.withMessage("@AliasFor declaration on attribute 'test' in annotation [" + AliasForNonMetaAnnotated.class.getName() +
"] declares an alias for attribute 'test' in annotation [" + AliasForNonMetaAnnotatedTarget.class.getName() +
.withMessage("@AliasFor declaration on attribute 'test' in annotation [" + AliasForNonMetaAnnotated.class.getCanonicalName() +
"] declares an alias for attribute 'test' in annotation [" + AliasForNonMetaAnnotatedTarget.class.getCanonicalName() +
"] which is not meta-present.");
}
@@ -189,8 +189,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasForSelfWithDifferentDefaults() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithDifferentDefaults.class))
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() +
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithDifferentDefaults.class.getCanonicalName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithDifferentDefaults.class.getCanonicalName() +
"] must declare the same default value.");
}
@@ -198,8 +198,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasForSelfWithMissingDefault() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithMissingDefault.class))
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithMissingDefault.class.getName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithMissingDefault.class.getName() +
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithMissingDefault.class.getCanonicalName() +
"] and attribute 'b' in annotation [" + AliasForSelfWithMissingDefault.class.getCanonicalName() +
"] must declare default values.");
}
@@ -207,8 +207,8 @@ class AnnotationTypeMappingsTests {
void forAnnotationTypeWhenAliasWithExplicitMirrorAndDifferentDefaults() {
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasWithExplicitMirrorAndDifferentDefaults.class))
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() +
"] and attribute 'c' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() +
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getCanonicalName() +
"] and attribute 'c' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getCanonicalName() +
"] must declare the same default value.");
}
@@ -352,8 +352,8 @@ class AnnotationTypeMappingsTests {
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0);
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> resolveMirrorSets(mapping, WithDifferentValueAliasPair.class, AliasPair.class))
.withMessage("Different @AliasFor mirror values for annotation [" + AliasPair.class.getName() + "] declared on " +
WithDifferentValueAliasPair.class.getName() +
.withMessage("Different @AliasFor mirror values for annotation [" + AliasPair.class.getCanonicalName() +
"] declared on " + WithDifferentValueAliasPair.class.getName() +
"; attribute 'a' and its alias 'b' are declared with values of [test1] and [test2].");
}
@@ -718,11 +718,11 @@ class AnnotationUtilsTests {
ImplicitAliasesWithMissingDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
assertThat(config).isNotNull();
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() ->
synthesizeAnnotation(config, clazz))
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> synthesizeAnnotation(config, clazz))
.withMessageStartingWith("Misconfigured aliases:")
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("default values");
}
@@ -733,11 +733,11 @@ class AnnotationUtilsTests {
ImplicitAliasesWithDifferentDefaultValuesContextConfig.class;
ImplicitAliasesWithDifferentDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
assertThat(config).isNotNull();
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() ->
synthesizeAnnotation(config, clazz))
assertThatExceptionOfType(AnnotationConfigurationException.class)
.isThrownBy(() -> synthesizeAnnotation(config, clazz))
.withMessageStartingWith("Misconfigured aliases:")
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
.withMessageContaining("same default value");
}
@@ -919,8 +919,18 @@ class AnnotationUtilsTests {
Map<String, Object> map = Collections.singletonMap(VALUE, 42L);
assertThatIllegalStateException().isThrownBy(() ->
synthesizeAnnotation(map, Component.class, null).value())
.withMessageContaining("Attribute 'value' in annotation org.springframework.core.testfixture.stereotype.Component " +
"should be compatible with java.lang.String but a java.lang.Long value was returned");
.withMessageContaining("Attribute 'value' in annotation %s should be compatible with " +
"java.lang.String but a java.lang.Long value was returned",
Component.class.getCanonicalName());
}
@Test
void synthesizeAnnotationFromMapWithAttributeOfIncorrectArrayType() {
Map<String, Object> map = Collections.singletonMap(VALUE, new int[] {42});
assertThatIllegalStateException().isThrownBy(() ->
synthesizeAnnotation(map, CharsContainer.class, null).chars())
.withMessageContaining("Attribute 'chars' in annotation %s should be compatible with " +
"char[] but a int[] value was returned", CharsContainer.class.getCanonicalName());
}
@Test
@@ -183,32 +183,32 @@ class ComposedRepeatableAnnotationsTests {
assertThatIllegalArgumentException().isThrownBy(throwingCallable)
.withMessageStartingWith("Annotation type must be a repeatable annotation")
.withMessageContaining("failed to resolve container type for")
.withMessageContaining(NonRepeatable.class.getName());
.withMessageContaining(NonRepeatable.class.getCanonicalName());
}
private void expectContainerMissingValueAttribute(ThrowingCallable throwingCallable) {
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
.withMessageStartingWith("Invalid declaration of container type")
.withMessageContaining(ContainerMissingValueAttribute.class.getName())
.withMessageContaining(ContainerMissingValueAttribute.class.getCanonicalName())
.withMessageContaining("for repeatable annotation")
.withMessageContaining(InvalidRepeatable.class.getName())
.withMessageContaining(InvalidRepeatable.class.getCanonicalName())
.withCauseExactlyInstanceOf(NoSuchMethodException.class);
}
private void expectContainerWithNonArrayValueAttribute(ThrowingCallable throwingCallable) {
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
.withMessageStartingWith("Container type")
.withMessageContaining(ContainerWithNonArrayValueAttribute.class.getName())
.withMessageContaining(ContainerWithNonArrayValueAttribute.class.getCanonicalName())
.withMessageContaining("must declare a 'value' attribute for an array of type")
.withMessageContaining(InvalidRepeatable.class.getName());
.withMessageContaining(InvalidRepeatable.class.getCanonicalName());
}
private void expectContainerWithArrayValueAttributeButWrongComponentType(ThrowingCallable throwingCallable) {
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
.withMessageStartingWith("Container type")
.withMessageContaining(ContainerWithArrayValueAttributeButWrongComponentType.class.getName())
.withMessageContaining(ContainerWithArrayValueAttributeButWrongComponentType.class.getCanonicalName())
.withMessageContaining("must declare a 'value' attribute for an array of type")
.withMessageContaining(InvalidRepeatable.class.getName());
.withMessageContaining(InvalidRepeatable.class.getCanonicalName());
}
private void assertGetRepeatableAnnotations(AnnotatedElement element) {

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