Compare commits

...

118 Commits

Author SHA1 Message Date
Stéphane Nicoll debd312d50 Release v6.2.3 2025-02-13 13:39:03 +01:00
Sam Brannen a8be11ebaf Upgrade to TestNG 7.11 2025-02-13 13:08:54 +01:00
Juergen Hoeller dfc10c1a8d Wait for lenient bean creation in non-locked threads as well
Closes gh-34349
2025-02-12 23:06:22 +01:00
Juergen Hoeller bbb593db48 Consistently ignore non-jar files in classpath
Closes gh-34417
2025-02-12 19:18:30 +01:00
Sam Brannen b2134ee71f Avoid Gradle build warnings about @⁠SuppressFBWarnings from FindBugs
In order to avoid Gradle build warnings about @⁠SuppressFBWarnings, this
commit introduces a testCompileOnly dependency on `findbugs` in the
spring-webmvc module so that the class file for @⁠SuppressFBWarnings is
available to the compileTestJava task.

Closes gh-34418
2025-02-12 17:49:23 +01:00
Sam Brannen 440a259b71 Clean up warnings in Gradle build 2025-02-12 16:46:41 +01:00
Sam Brannen 6174055910 Link to @⁠MockitoBean, @⁠MockitoSpyBean, & @⁠TestBean Javadoc from ref docs 2025-02-12 16:31:33 +01:00
Sam Brannen e31ce359a1 Support @⁠MockitoSpyBean at the type level on test classes
Prior to this commit, @⁠MockitoSpyBean could only be declared on fields
within test classes, which prevented developers from being able to
easily reuse spy configuration across a test suite.

With this commit, @⁠MockitoSpyBean is now supported at the type level
on test classes, their superclasses, and interfaces implemented by
those classes. @⁠MockitoSpyBean is also supported on enclosing classes
for @⁠Nested test classes, their superclasses, and interfaces
implemented by those classes, while honoring @⁠NestedTestConfiguration
semantics.

In addition, @⁠MockitoSpyBean:

- has a new `types` attribute that can be used to declare the type or
  types to spy when @⁠MockitoSpyBean is declared at the type level

- can be declared as a repeatable annotation at the type level

- can be declared as a meta-annotation on a custom composed annotation
  which can be reused across a test suite (see the @⁠SharedSpies
  example in the reference manual)

To support these new features, this commit also includes the following
changes.

- MockitoSpyBeanOverrideProcessor has been revised to support
  @⁠MockitoSpyBean at the type level.

- The "Bean Overriding in Tests" and "@⁠MockitoBean and
  @⁠MockitoSpyBean" sections of the reference manual have been fully
  revised.

See gh-34408
Closes gh-33925
2025-02-12 15:54:54 +01:00
Juergen Hoeller b336bbe539 Wait for lenient bean creation in locked thread when necessary
Closes gh-34349
2025-02-12 12:17:02 +01:00
Sébastien Deleuze 056757b493 Refine tests in MockMvcExtensionsTests
Closes gh-34412
2025-02-12 11:33:42 +01:00
Kevin Houtz 79c5fec1be Add form fields support to MockMvc Kotlin DSL
See gh-34412

Signed-off-by: Kevin Houtz <kevin@khoutz.com>
2025-02-12 11:33:29 +01:00
Brian Clozel 689782cbad Make ProblemDetail Serializable
This commit makes the `ProblemDetail` type implement `Serializable` in
order to be serialized and shared in distributed systems.

Closes gh-34409
2025-02-11 22:33:05 +01:00
Juergen Hoeller 2df90e32c0 Upgrade to Netty 4.1.118 and Checkstyle 10.21.2 2025-02-11 22:10:15 +01:00
Juergen Hoeller f53da04717 Align with SmartClassLoader handling for AOP proxy classes
Closes gh-34274
2025-02-11 22:10:02 +01:00
Sam Brannen 124b38450b Remove unused code and improve assertion
See gh-34363
2025-02-11 16:30:52 +01:00
Sam Brannen b07217ab67 Use ConversionService to convert POJO to array for SpEL varargs invocations
Prior to this commit, if an appropriate Converter was registered with
the ConversionService that converts from a POJO to an array and that
ConversionService was registered with the Spring Expression Language
(SpEL) TypeConverter, an attempt to invoke a varargs method in a SpEL
expression with such a POJO would fail because the ConversionService
was not used to convert the POJO to an array suitable for the varargs
method invocation.

This commit revises the implementations of convertArguments(...) and
convertAllMethodHandleArguments(...) in ReflectionHelper to support
such use cases.

Closes gh-34371
2025-02-11 16:11:13 +01:00
Sam Brannen aa7e84c89f Polishing 2025-02-11 16:11:13 +01:00
Stéphane Nicoll bd1c7b379f Merge pull request #34400 from canattofilipe
* pr/34400:
  Polish "Use correct method to retrieve DefaultListableBeanFactory"
  Use correct method to retrieve DefaultListableBeanFactory

Closes gh-34400
2025-02-11 16:01:04 +01:00
Stéphane Nicoll 6af19244a5 Polish "Use correct method to retrieve DefaultListableBeanFactory"
See gh-34400
2025-02-11 16:00:34 +01:00
canattofilipe 328dd71f6e Use correct method to retrieve DefaultListableBeanFactory
See gh-34400

Signed-off-by: canattofilipe <canattofilipe@gmail.com>
2025-02-11 15:57:01 +01:00
rstoyanchev d04883f839 HTTP Interface client handles query param with ":"
Closes gh-34364
2025-02-11 11:11:05 +00:00
rstoyanchev 9f55296049 Nested list/map/array with constructor binding
Closes gh-34305
2025-02-11 11:11:05 +00:00
rstoyanchev 4591a67641 Handle [] leniently in constructor binding
See gh-34305
2025-02-11 11:11:05 +00:00
Sam Brannen 9797bc0acd Expose order values of TestExecutionListener implementations as constants
Prior to this commit, the order values of TestExecutionListener
implementations were hard-coded in their getOrder() methods.

To benefit users and integrators, this commit exposes those order values
as an ORDER constant in each TestExecutionListener.

See gh-34225
Closes gh-34404
2025-02-11 11:53:33 +01:00
Sam Brannen 9d3374b28d Finish incomplete sentences 2025-02-11 11:53:33 +01:00
Sam Brannen 09086fc648 Revise TestExecutionListener order values documentation
See gh-34265
2025-02-11 11:53:33 +01:00
Mengqi Xu 128e90064e Document order values for TestExecutionListener implementations
Closes gh-34265

Signed-off-by: Mengqi Xu <2663479778@qq.com>
2025-02-11 11:53:33 +01:00
Stéphane Nicoll 16e3973118 Upgrade to Reactor 2024.0.3
Closes gh-34403
2025-02-11 11:36:47 +01:00
Stéphane Nicoll 5c37d07ad3 Upgrade to RSocket 1.1.5
Closes gh-34402
2025-02-11 10:39:21 +01:00
Stéphane Nicoll 12891d1975 Upgrade to Micrometer 1.14.4
Closes gh-34401
2025-02-11 10:34:33 +01:00
Juergen Hoeller 1ca941ba9a Consistently resolve renamed type variables
Closes gh-34386
2025-02-10 15:51:39 +01:00
Sam Brannen 17a94fb110 Improve warning for unexpected use of value attribute as @⁠Component name
Prior to this commit, if a String 'value' attribute of an annotation
was annotated with @⁠AliasFor and explicitly configured to alias an
attribute other than @⁠Component.value, the value was still used as the
@⁠Component name, but the warning message that was logged stated that
the 'value' attribute should be annotated with
@⁠AliasFor(annotation=Component.class). However, it is not possible to
annotate an annotation attribute twice with @⁠AliasFor.

To address that, this commit revises the logic in
AnnotationBeanNameGenerator so that it issues a log message similar to
the following in such scenarios.

WARN o.s.c.a.AnnotationBeanNameGenerator - Although the 'value'
attribute in @⁠example.MyStereotype declares @⁠AliasFor for an
attribute other than @⁠Component's 'value' attribute, the value is
still used as the @⁠Component name based on convention. As of Spring
Framework 7.0, such a 'value' attribute will no longer be used as the
@⁠Component name.

See gh-34346
Closes gh-34317
2025-02-10 13:29:40 +01:00
Sam Brannen 2fcae65853 Polishing 2025-02-10 13:29:33 +01:00
rstoyanchev 7a0fe7d14f WebAsyncManager wraps disconnected client errors
If the Servlet container delegates a disconnected client error via
AsyncListener#onError, wrap it as AsyncRequestNotUsableException
for more targeted and consistent handling of such errors.

Closes gh-34363
2025-02-10 11:14:21 +00:00
rstoyanchev ccdaed594e Polishing contribution
Closes gh-34333
2025-02-10 11:14:21 +00:00
Branden Clark c41b0140cd Check hasNext on sessionIds in UserDestinationResult
See gh-34333

Signed-off-by: Branden Clark <brandenrayclark@gmail.com>
2025-02-10 11:14:21 +00:00
rstoyanchev ceffda7874 Polishing contribution
Closes gh-34362
2025-02-10 11:14:21 +00:00
Jared Wiltshire 49f9b40fba Support RFC 8441 upgrades over HTTP/2 CONNECT
See gh-34362

Signed-off-by: Jared Wiltshire <jazdw@users.noreply.github.com>
2025-02-10 11:14:21 +00:00
Sam Brannen d59991fcc9 Revise contribution
See gh-34006
2025-02-10 11:42:24 +01:00
Yanming Zhou cf46f391d7 Improve diagnostics when a Bean Override cannot be selected by type
See gh-34004
Closes gh-34006

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2025-02-10 11:06:10 +01:00
Andras Dobrosi 2b4c7d09b0 Match ContentDisposition attributes case-insensitively
This commit ensures that `ContentDisposition` parses attributes like
"filename" and "filename*" in a case insensitive fashion, per RFC 6266.

Closes gh-34383

Signed-off-by: Andras Dobrosi <dobrosi@gmail.com>
[brian.clozel@broadcom.com: apply code conventions]
Signed-off-by: Brian Clozel <brian.clozel@broadcom.com>
2025-02-10 09:21:02 +01:00
Johnny Lim 61138698c6 Fix copyright end year in ServerResponseResultHandler
See https://github.com/spring-projects/spring-framework/commit/1cea1fe962cc95544fce7be8fd2f56419e89aaaf#diff-fa20069f0305b5aee07bf90a7f8d0502a6ee139892639b7dfe470b54c3a8574aL2-R2

See gh-34066
Closes gh-34391

Signed-off-by: Johnny Lim <izeye@naver.com>
2025-02-09 11:38:24 +01:00
Daeho Kwon 3548e872b9 Fix deprecation warnings in RuntimeHintsAgentPlugin
Closes gh-34390

Signed-off-by: Daeho Kwon <trewq231@naver.com>
2025-02-08 16:19:26 +01:00
Sam Brannen 305686dbf7 Ensure Bean Overrides are discovered once in @⁠Nested hierarchies
Changes made to the Bean Override search algorithms in commit
9181cce65f resulted in a regression that caused tests to start failing
due to duplicate BeanOverrideHandlers under the following circumstances.

- An enclosing class (typically a top-level test class) declares a
  @⁠BeanOverride such as @⁠MockitoBean.

- An inner class is declared in that enclosing class.

- A @⁠Nested test class which extends that inner class is declared in
  the same enclosing class.

The reason for the duplicate detection is that the current search
algorithm visits the common enclosing class twice.

To address that, this commit revises the search algorithm in
BeanOverrideHandler so that enclosing classes are only visited once.

See gh-33925
Closes gh-34324
2025-02-08 13:30:22 +01:00
Juergen Hoeller ace2f0a3e5 Expose resolveAutowireCandidates method with basic candidate filtering
DependencyObjectProvider is aligned to check basic autowire-candidate status even in case of custom filtering.

Closes gh-34203
2025-02-07 19:00:25 +01:00
Juergen Hoeller 1a573d6e3c Lazily retrieve TypeDescriptor annotations on demand
Closes gh-33948
2025-02-07 18:55:24 +01:00
Sam Brannen bb7a8006c5 Fix @⁠Nullable declaration in DataBinder on 6.2.x 2025-02-07 18:33:03 +01:00
Sam Brannen ba56c1a8f4 Fix copy-n-paste error 2025-02-07 18:26:26 +01:00
Sam Brannen 9107f7b592 Honor @⁠Primary before fallback qualifier for Bean Overrides
Prior to this commit, test bean overrides (for example, @⁠MockitoBean,
@⁠TestBean, etc.) eagerly honored the name of the annotated field as a
fallback qualifier, effectively ignoring @⁠Primary and @⁠Fallback
semantics for certain use cases.

This led to situations where a bean override for a test would select a
different bean than the core container would for the same autowiring
metadata.

To address that, this commit revises the implementation of
BeanOverrideBeanFactoryPostProcessor so that @⁠Primary and @⁠Fallback
semantics are consistently honored before attempting to use the
annotated field's name as a fallback qualifier.

Closes gh-34374
2025-02-07 18:06:52 +01:00
rstoyanchev 1d7cb4fc13 Defer initialization of HandlerMethod validation flags
Re-create the HandlerMethod only after the original is used as a key
in the CORS lookup map.

Closes gh-34379
2025-02-07 13:20:16 +00:00
rstoyanchev 84992536c5 Defer initialization of HandlerMethod validation flags
Re-create the HandlerMethod only after the original is used as a key
in the CORS lookup map.

Closes gh-34375
2025-02-07 13:10:55 +00:00
rstoyanchev ff49b0b683 Align AnnotatedMethod#equals and #hashcode
See gh-34375
2025-02-07 13:10:55 +00:00
rstoyanchev 56c4d2d4ca Improve HandlerMethod#resolvedFromHandlerMethod initialization
Ensure the original instance is always the one returned no matter how
many times the HandlerMethod is re-created.

Make the constructor protected to allow subclasses to re-create the
HandlerMethod as the concrete subclass.

See gh-34375
2025-02-07 13:10:55 +00:00
Brian Clozel 174d0e4576 Fix "Nth day of week" Quartz-style cron expressions
Prior to this commit, `CronExpression` would support Quartz-style
expressions with "Nth occurence of a  dayOfWeek" semantics by using the
`TemporalAdjusters.dayOfWeekInMonth` JDK support. This method will
return the Nth occurence starting with the month of the given temporal,
but in some cases will overflow to the next or previous month.
This behavior is not expected for our cron expression support.

This commit ensures that when an overflow happens (meaning, the
resulting date is not in the same month as the input temporal), we
should instead have another attempt at finding a valid month for this
expression.

Fixes gh-34360
2025-02-06 18:27:07 +01:00
Sébastien Deleuze dba4881318 Refine the CGLIB error message with native
See gh-34370
2025-02-06 09:18:51 +01:00
Sébastien Deleuze 16ec08aec8 Provide a more actionable CGLIB error message with native
Closes gh-34370
2025-02-05 17:47:25 +01:00
rstoyanchev 462c2bd538 Enhance constructor binding for List/Map/Array
Support List/Map/Array of simple values, or values supported
by type conversion.

Closes gh-34305
2025-02-05 14:26:12 +00:00
rstoyanchev 7f29f0e663 Revert commit 3505c4bcad
The fix did not address the issue. It only made the constructor not
fail with tests succeeding due to setter binding instead.

See gh-34043
2025-02-05 14:26:12 +00:00
rstoyanchev 3898482d3f Revert commit 0f38c28e91
The fix is not how the issue needs to be addressed.

See gh-34121
2025-02-05 14:26:12 +00:00
rstoyanchev bb7ce21076 Expose Locale to method validation in WebFlux
Closes gh-33810
2025-02-05 14:26:12 +00:00
Stéphane Nicoll fd4dee7ce3 Merge pull request #34183 from remeio
* pr/34183:
  Polish "Add support for multidimensional arrays"
  Add support for multidimensional arrays

Closes gh-34183
2025-02-05 14:42:35 +01:00
Stéphane Nicoll ec037b1ec0 Polish "Add support for multidimensional arrays"
See gh-34183
2025-02-05 14:35:33 +01:00
xumengqi 68c1e2ac92 Add support for multidimensional arrays
See gh-34183
2025-02-05 14:23:09 +01:00
Sam Brannen 819a7c86c1 Clarify component scanning of abstract classes with @⁠Lookup methods
Due to changes in gh-19118, classes that contain @⁠Lookup methods are
no longer required to be concrete classes for use with component
scanning; however, the reference documentation still states that such
classes must not be abstract.

This commit therefore removes the outdated reference documentation and
updates the corresponding Javadoc.

See gh-19118
Closes gh-34367
2025-02-05 13:39:29 +01:00
Stéphane Nicoll 9c1346d240 Merge pull request #34316 from JoshuaChen
* pr/34316:
  Polish "Handle arbitrary JoinPoint argument index"
  Handle arbitrary JoinPoint argument index

Closes gh-34316
2025-02-05 11:50:35 +01:00
Stéphane Nicoll fb6e86551a Polish "Handle arbitrary JoinPoint argument index"
See gh-34316
2025-02-05 11:49:56 +01:00
Joshua Chen 13ba770428 Handle arbitrary JoinPoint argument index
See gh-34316

Signed-off-by: Joshua Chen <27291761@qq.com>
2025-02-05 11:44:51 +01:00
Stéphane Nicoll a4d99d6201 Merge pull request #33957 from anaconda875
* pr/33957:
  Polish "Use proper return type in AsyncExecutionInterceptor"
  Use proper return type in AsyncExecutionInterceptor

Closes gh-33957
2025-02-04 16:01:38 +01:00
Stéphane Nicoll 3923150dad Polish "Use proper return type in AsyncExecutionInterceptor"
See gh-33957
2025-02-04 15:59:35 +01:00
Bao.Ngo 8eb24458f8 Use proper return type in AsyncExecutionInterceptor
See gh-33957
2025-02-04 15:58:53 +01:00
Juergen Hoeller cda7e98643 Lazily override default editors when actually needed
Closes gh-34361
2025-02-04 13:26:16 +01:00
Stéphane Nicoll e92809d470 Update CI actions 2025-02-04 09:29:51 +01:00
rstoyanchev f477c1653d Allow WebSocket over HTTP CONNECT
Closes gh-34044
2025-02-03 15:28:27 +00:00
rstoyanchev 1cea1fe962 Polishing contribution
Closes gh-34066
2025-02-03 15:15:21 +00:00
Tarek Mues 8b07f93620 Add getters ServerResponseResultHandler
See gh-34066
2025-02-03 15:11:47 +00:00
Pierre Rossato a9ecf90524 Minor update in WebSocket STOMP documentation
Closes gh-34353

Signed-off-by: Pierre Rossato <pierre.rossato@gmail.com>
2025-02-03 15:10:32 +00:00
Daeho Kwon 7536777a18 Replace hardcoded "Sec-WebSocket-Version" with constant
Closes gh-34319

Signed-off-by: Daeho Kwon <trewq231@naver.com>
2025-02-03 15:06:28 +00:00
Juergen Hoeller 1b18928bf0 Explicitly set custom ClassLoader on CGLIB Enhancer
Closes gh-34274
2025-02-03 15:23:51 +01:00
Juergen Hoeller 8c2b44bd6c Support filtered/unfiltered stream access on ObjectProvider
Closes gh-34318
Closes gh-34203
2025-02-03 15:04:27 +01:00
Juergen Hoeller 2df8ea94e0 Clean root entry path to match jar entries format in cache
Closes gh-34348
2025-01-31 22:26:14 +01:00
Juergen Hoeller 323e52b5a9 Continue with pre-instantiation when current bean is in creation already
Closes gh-34349
2025-01-31 15:16:52 +01:00
Juergen Hoeller ed994dcd97 Resolve bounds for type variable before emptiness check
Closes gh-34328
2025-01-30 15:35:14 +01:00
Juergen Hoeller 4c3b435d23 Enforce exact match for bounds of nested type variable
Closes gh-34300
2025-01-30 15:34:55 +01:00
Brian Clozel cfe2db0581 Improve reference docs on RestClient.retrieve()
As of Spring Framework 6.2, the `RestClient.retrieve()` method is a
no-op and developers must invoke a terminal operation on the returned
`ResponseSpec` to have any side effect.

This has been documented in the Javadoc and the wiki release notes, but
this commit highlights this as well in the reference documentation.

Closes gh-34334
2025-01-29 18:26:12 +01:00
Sam Brannen 53afe27109 Clean up warnings in Gradle build
See gh-34332
2025-01-29 17:52:27 +01:00
Sam Brannen b9e43d05bd Restore property binding support for a Map that implements Iterable
The changes in commit c20a2e4763 introduced a regression with regard to
binding to a Map property when the Map also happens to implement
Iterable.

Although that is perhaps not a very common scenario, this commit
reorders the if-blocks in AbstractNestablePropertyAccessor's
getPropertyValue(PropertyTokenHolder) method so that a Map is
considered before an Iterable, thereby allowing an Iterable-Map to be
accessed as a Map.

See gh-907
Closes gh-34332
2025-01-29 17:34:47 +01:00
Brian Clozel d80de043ce Fix filtered HTTP headers in data binding
Prior to this commit, several common HTTP headers were ignored from the
data binding process when collecting property values, in gh-34039 and
gh-34182.

This commit completes the initial enhancement by ensuring that the
default header predicate is also considering cases where constructor
binding is applied and the Java type has a lowercase variant of the HTTP
header name to filter.

Fixes gh-34292
2025-01-29 16:06:19 +01:00
Brian Clozel 7c5b6f1e1c Delete failing Freemarker test
This test was already ignored as of Java 21 because of a Java behavior
change, and now it started failing as of 17.0.14.
This commit removes the test entirely.
2025-01-29 15:55:58 +01:00
rstoyanchev a6b6d19545 Update localAddress Javadoc in WebSocketSession
Closes gh-34304
2025-01-28 15:36:26 +00:00
rstoyanchev 9b58df8857 Update HandlerMethod#createWithResolvedBean
Avoid re-creating the instance unless it is a bean name that
needs to be resolved through the BeanFactory.

Closes gh-34277
2025-01-28 15:36:26 +00:00
rstoyanchev 1cc767e90b Polishing in ExtendedServletRequestDataBinder 2025-01-28 15:36:19 +00:00
Sam Brannen 34d6dd9b62 Polishing 2025-01-27 15:19:00 +01:00
Sébastien Deleuze 5499878de0 Support properties in kotlinx.serialization converters
This commit adds support for Kotlin properties in Spring WebMVC
controllers, supported for reasons explained in gh-31856, with
kotlinx.serialization converters.

Closes gh-34284
2025-01-27 15:06:57 +01:00
Sébastien Deleuze a970fc16aa Support properties in kotlinx.serialization codecs
This commit adds support for Kotlin properties in Spring WebFlux
controllers, supported for reasons explained in gh-31856, with
kotlinx.serialization codecs.

See gh-34284
2025-01-27 15:06:50 +01:00
Sam Brannen 683733a682 Link to AspectJ "runtime-api" Javadoc site
The AspectJ team has confirmed that the following Javadoc site hosts
the latest version (currently 1.9.22.1) of the runtime API for AspectJ,
generated with a modern version of the javadoc tool which generates the
element-list file required by the Spring Framework build.

https://eclipse.dev/aspectj/doc/latest/runtime-api/

See https://github.com/eclipse-aspectj/aspectj/issues/321
See gh-34293
2025-01-24 15:33:24 +01:00
Sam Brannen 11fe2987d0 Use appropriate link for Testing chapter
This commit effectively removes a reference to a nonexistent "#testing"
fragment.
2025-01-23 10:58:48 +01:00
Juergen Hoeller 90423a98b0 Enforce match for resolved part of unresolvable target type
Closes gh-34298
2025-01-21 19:15:20 +01:00
Brian Clozel e9dc6be51c Fix Javadoc for field reflection hints
Closes gh-34297
2025-01-21 17:25:15 +01:00
Sam Brannen 6863b904b5 Polish contribution
See gh-34295
2025-01-21 17:22:49 +01:00
Mengqi Xu 016a4c4260 Improve Javadoc for SpringProperties.getFlag()
getFlag() returns true when the property is equal, ignoring case, to the
string "true", not just "true"; "TrUe" also means true.

Closes gh-34295

Signed-off-by: Mengqi Xu <2663479778@qq.com>
2025-01-21 17:19:17 +01:00
rstoyanchev 233f755416 Update Javadoc that links to RestTemplate
Ensure RestClient is mentioned where appropriate.

Closes gh-34270
2025-01-21 12:21:22 +00:00
rstoyanchev b026680e1c Enhance DisconnectedClientHelper exception type checks
We now look for the target exception types in cause chain as well,
but return false if we encounter a RestClient or WebClient
exception in the chain.

Closes gh-34264
2025-01-21 12:21:22 +00:00
rstoyanchev e47cc19964 Restore exception phrase in DisconnectedClientHelper
Effectively revert 203fa7, and add implementation comment and tests.

See gh-34264
2025-01-21 12:21:22 +00:00
Sam Brannen 15b1310470 Link to AspectJ runtime 1.9.22.1 Javadoc site
Closes gh-34293
2025-01-21 11:31:14 +01:00
Juergen Hoeller ee60eb7207 Fall back to HTTP GET in case of 405 from HTTP HEAD
Closes gh-34217
2025-01-20 18:17:49 +01:00
Sam Brannen cecebd0ef1 Polish HttpHeadersAssertTests 2025-01-20 16:48:10 +01:00
Johnny Lim aac4dbf420 Polish HttpHeadersAssert
See gh-34286

Signed-off-by: Johnny Lim <izeye@naver.com>
2025-01-20 16:46:16 +01:00
Stéphane Nicoll 82bc4ff71d Handle TextPart with escaped separator
This commit harmonizes how a candidate value is parsed to extract its
key and default, if any. Rather than returning {@code null} if no
default is available, `splitKeyAndValue` now consistently returns a
non-null array.

This prevents an escaped separator character to be mistakenly identified
as a placeholder in certain cases.

Closes gh-34289
2025-01-20 15:04:06 +01:00
Brian Clozel 356d5c2cf2 Polishing 2025-01-20 08:02:57 +01:00
Brian Clozel 46c13adfa8 Document custom HttpServiceArgumentResolver usage
This commit adds a reference documentation section explaining how
applications can use custom `HttpServiceArgumentResolver`
implementations to support complex application types as method
parameters in HTTP interfaces.

Closes gh-34227
2025-01-20 07:57:56 +01:00
Sam Brannen 886ca7f2db Polish contribution and SimpleCommandLineArgs-related code
Closes gh-34282
2025-01-18 17:03:04 +01:00
puppy4c c463b937b8 Update Javadoc for SimpleCommandLinePropertySource
For ComandLineArgs, supplying the same option multiple times with
different values is valid, and the values will be stored in a List.

This commit also updates the Javadoc for SimpleCommandLineArgsParser.

See gh-34282

Signed-off-by: puppy4c <puppy4c@foxmail.com>
2025-01-18 16:26:14 +01:00
Sam Brannen 4783c321d9 Polish formatting for consistency across implementations 2025-01-18 15:04:25 +01:00
Sam Brannen 0dfcb44821 Remove unnecessary @⁠Nullable declaration 2025-01-18 15:03:47 +01:00
Sam Brannen 8c3d5621db Upgrade to AssertJ 3.27.3 2025-01-18 14:02:07 +01:00
Sam Brannen 12a6a84829 Polishing 2025-01-18 14:02:07 +01:00
Sam Brannen 6662ecc8cc Upgrade to Gradle Develocity plugin version 3.19
In light of the fact that the Develocity service on ge.spring.io has
been upgraded to 2024.3.1.
2025-01-17 11:47:47 +01:00
Stéphane Nicoll 4b0446fbef Next development version (v6.2.3-SNAPSHOT) 2025-01-16 10:06:12 +01:00
192 changed files with 4483 additions and 1114 deletions
@@ -31,7 +31,7 @@ runs:
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
${{ inputs.java-toolchain == 'true' && '17' || '' }}
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
with:
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
@@ -20,7 +20,7 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
uses: jfrog/setup-jfrog-cli@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
distribution: 'liberica'
java-version: 17
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
with:
cache-read-only: false
- name: Configure Gradle Properties
+1 -1
View File
@@ -91,7 +91,7 @@ configure([rootProject] + javaProjects) { project ->
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jakarta.ee/specifications/platform/9/apidocs/",
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
"https://eclipse.dev/aspectj/doc/released/aspectj5rt-api",
"https://eclipse.dev/aspectj/doc/latest/runtime-api/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("10.21.1");
checkstyle.setToolVersion("10.21.2");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -27,6 +27,7 @@ import org.gradle.api.attributes.Usage;
import org.gradle.api.attributes.java.TargetJvmVersion;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.jvm.JvmTestSuite;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.testing.Test;
import org.gradle.testing.base.TestingExtension;
@@ -52,7 +53,7 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
TestingExtension testing = project.getExtensions().getByType(TestingExtension.class);
JvmTestSuite jvmTestSuite = (JvmTestSuite) testing.getSuites().getByName("test");
RuntimeHintsAgentExtension agentExtension = createRuntimeHintsAgentExtension(project);
Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
TaskProvider<Test> agentTest = project.getTasks().register(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
test.useJUnitPlatform(options -> {
options.includeTags("RuntimeHintsTests");
});
@@ -63,7 +64,7 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
test.setClasspath(jvmTestSuite.getSources().getRuntimeClasspath());
test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension));
});
project.getTasks().getByName("check", task -> task.dependsOn(agentTest));
project.getTasks().named("check", task -> task.dependsOn(agentTest));
project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test"));
});
}
@@ -57,9 +57,9 @@ The following table describes these properties:
In addition to bean definitions that contain information on how to create a specific
bean, the `ApplicationContext` implementations also permit the registration of existing
objects that are created outside the container (by users). This is done by accessing the
ApplicationContext's `BeanFactory` through the `getBeanFactory()` method, which returns
the `DefaultListableBeanFactory` implementation. `DefaultListableBeanFactory` supports
this registration through the `registerSingleton(..)` and `registerBeanDefinition(..)`
ApplicationContext's `BeanFactory` through the `getAutowireCapableBeanFactory()` method,
which returns the `DefaultListableBeanFactory` implementation. `DefaultListableBeanFactory`
supports this registration through the `registerSingleton(..)` and `registerBeanDefinition(..)`
methods. However, typical applications work solely with beans defined through regular
bean definition metadata.
@@ -120,8 +120,6 @@ dynamically generate a subclass that overrides the method.
subclasses cannot be `final`, and the method to be overridden cannot be `final`, either.
* Unit-testing a class that has an `abstract` method requires you to subclass the class
yourself and to supply a stub implementation of the `abstract` method.
* Concrete methods are also necessary for component scanning, which requires concrete
classes to pick up.
* A further key limitation is that lookup methods do not work with factory methods and
in particular not with `@Bean` methods in configuration classes, since, in that case,
the container is not in charge of creating the instance and therefore cannot create
@@ -293,11 +291,6 @@ Kotlin::
----
======
Note that you should typically declare such annotated lookup methods with a concrete
stub implementation, in order for them to be compatible with Spring's component
scanning rules where abstract classes get ignored by default. This limitation does not
apply to explicitly registered or explicitly imported bean classes.
[TIP]
====
Another way of accessing differently scoped target beans is an `ObjectFactory`/
+1 -1
View File
@@ -7,7 +7,7 @@ xref:overview.adoc[Overview] :: History, Design Philosophy, Feedback,
Getting Started.
xref:core.adoc[Core] :: IoC Container, Events, Resources, i18n,
Validation, Data Binding, Type Conversion, SpEL, AOP, AOT.
<<testing.adoc#testing, Testing>> :: Mock Objects, TestContext Framework,
xref:testing.adoc[Testing] :: Mock Objects, TestContext Framework,
Spring MVC Test, WebTestClient.
xref:data-access.adoc[Data Access] :: Transactions, DAO Support,
JDBC, R2DBC, O/R Mapping, XML Marshalling.
@@ -115,11 +115,15 @@ Finally, the body can be set to a callback function that writes to an `OutputStr
==== Retrieving the response
Once the request has been set up, the HTTP response is accessed by invoking `retrieve()`.
The response body can be accessed by using `body(Class)` or `body(ParameterizedTypeReference)` for parameterized types like lists.
Once the request has been set up, it can be sent by chaining method calls after `retrieve()`.
For example, the response body can be accessed by using `retrieve().body(Class)` or `retrieve().body(ParameterizedTypeReference)` for parameterized types like lists.
The `body` method converts the response contents into various types for instance, bytes can be converted into a `String`, JSON can be converted into objects using Jackson, and so on (see <<rest-message-conversion>>).
The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body.
The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body, with `retrieve().toEntity(Class)`
NOTE: Calling `retrieve()` by itself is a no-op and returns a `ResponseSpec`.
Applications must invoke a terminal operation on the `ResponseSpec` to have any side effect.
If consuming the response has no interest for your use case, you can use `retrieve().toBodilessEntity()`.
This sample shows how `RestClient` can be used to perform a simple `GET` request.
@@ -988,6 +992,27 @@ parameter annotation) is set to `false`, or the parameter is marked optional as
[[rest-http-interface.custom-resolver]]
=== Custom argument resolver
For more complex cases, HTTP interfaces do not support `RequestEntity` types as method parameters.
This would take over the entire HTTP request and not improve the semantics of the interface.
Instead of adding many method parameters, developers can combine them into a custom type
and configure a dedicated `HttpServiceArgumentResolver` implementation.
In the following HTTP interface, we are using a custom `Search` type as a parameter:
include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0]
We can implement our own `HttpServiceArgumentResolver` that supports our custom `Search` type
and writes its data in the outgoing HTTP request.
include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0]
Finally, we can use this argument resolver during the setup and use our HTTP interface.
include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0]
[[rest-http-interface-return-values]]
=== Return Values
@@ -1,34 +1,62 @@
[[spring-testing-annotation-beanoverriding-mockitobean]]
= `@MockitoBean` and `@MockitoSpyBean`
`@MockitoBean` and `@MockitoSpyBean` are used on non-static fields in test classes to
override beans in the test's `ApplicationContext` with a Mockito _mock_ or _spy_,
respectively. In the latter case, an early instance of the original bean is captured and
wrapped by the spy.
{spring-framework-api}/test/context/bean/override/mockito/MockitoBean.html[`@MockitoBean`] and
{spring-framework-api}/test/context/bean/override/mockito/MockitoSpyBean.html[`@MockitoSpyBean`]
can be used in test classes to override a bean in the test's `ApplicationContext` with a
Mockito _mock_ or _spy_, respectively. In the latter case, an early instance of the
original bean is captured and wrapped by the spy.
By default, the annotated field's type is used to search for candidate beans to override.
If multiple candidates match, `@Qualifier` can be provided to narrow the candidate to
override. Alternatively, a candidate whose bean name matches the name of the field will
match.
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.
* 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 at the type level, the type of bean
(or beans) to mock or spy must be supplied via the `types` attribute in the annotation
for example, `@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple
candidates exist in the `ApplicationContext`, you can explicitly specify a bean name to
mock or spy by setting the `name` attribute. Note, however, that the `types` attribute
must contain a single type if an explicit bean `name` is configured for example,
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
To support reuse of mock configuration, `@MockitoBean` and `@MockitoSpyBean` may be used
as meta-annotations to create custom _composed annotations_ for example, to define
common mock or spy configuration in a single annotation that can be reused across a test
suite. `@MockitoBean` and `@MockitoSpyBean` can also be used as repeatable annotations at
the type level — for example, to mock or spy several beans by name.
[WARNING]
====
Qualifiers, including the name of the field, are used to determine if a separate
Qualifiers, including the name of a field, are used to determine if a separate
`ApplicationContext` needs to be created. If you are using this feature to mock or spy
the same bean in several test classes, make sure to name the field consistently to avoid
the same bean in several test classes, make sure to name the fields consistently to avoid
creating unnecessary contexts.
====
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
If no existing bean matches, a new bean is created on the fly. However, you can switch to
the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the
following section for an example.
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides].
If a corresponding bean does not exist, a new bean will be created. However, you can
switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`
for example, `@MockitoBean(enforceOverride = true)`.
The `@MockitoSpyBean` annotation uses the `WRAP`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy],
and the original instance is wrapped in a Mockito spy. This strategy requires that
exactly one candidate bean exists.
@@ -56,15 +84,8 @@ or `private` depending on the needs or coding practices of the project.
[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
== `@MockitoBean` Examples
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
exist. However, if you would like for the test to fail when a corresponding bean does not
exist, you can set the `enforceOverride` attribute to `true` for example,
`@MockitoBean(enforceOverride = true)`.
To use a by-name override rather than a by-type override, specify the `name` (or `value`)
attribute of the annotation.
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
The following example shows how to use the default behavior of the `@MockitoBean`
annotation.
[tabs]
======
@@ -81,7 +102,7 @@ Java::
// tests...
}
----
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
<1> Replace the bean with type `CustomService` with a Mockito mock.
======
In the example above, we are creating a mock for `CustomService`. If more than one bean
@@ -90,7 +111,8 @@ will fail, and you will need to provide a qualifier of some sort to identify whi
`CustomService` beans you want to override. If no such bean exists, a bean will be
created with an auto-generated bean name.
The following example uses a by-name lookup, rather than a by-type lookup:
The following example uses a by-name lookup, rather than a by-type lookup. If no bean
named `service` exists, one is created.
[tabs]
======
@@ -108,32 +130,9 @@ Java::
}
----
<1> Replace the bean named `service` with a Mockito `mock`.
<1> Replace the bean named `service` with a Mockito mock.
======
If no bean named `service` exists, one is created.
`@MockitoBean` can also be used at the type level:
- on a test class or any superclass or implemented interface in the type hierarchy above
the test class
- 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` is declared at the type level, the type of bean (or beans) to mock
must be supplied via the `types` attribute for example,
`@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple candidates
exist in the application context, you can explicitly specify a bean name to mock by
setting the `name` attribute. Note, however, that the `types` attribute must contain a
single type if an explicit bean `name` is configured for example,
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
To support reuse of mock configuration, `@MockitoBean` may be used as a meta-annotation
to create custom _composed annotations_ — for example, to define common mock
configuration in a single annotation that can be reused across a test suite.
`@MockitoBean` can also be used as a repeatable annotation at the type level — for
example, to mock several beans by name.
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
[tabs]
@@ -191,7 +190,7 @@ APIs.
== `@MockitoSpyBean` Examples
The following example shows how to use the default behavior of the `@MockitoSpyBean`
annotation:
annotation.
[tabs]
======
@@ -208,7 +207,7 @@ Java::
// tests...
}
----
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
<1> Wrap the bean with type `CustomService` with a Mockito spy.
======
In the example above, we are wrapping the bean with type `CustomService`. If more than
@@ -216,7 +215,7 @@ one bean of that type exists, the bean named `customService` is considered. Othe
the test will fail, and you will need to provide a qualifier of some sort to identify
which of the `CustomService` beans you want to spy.
The following example uses a by-name lookup, rather than a by-type lookup:
The following example uses a by-name lookup, rather than a by-type lookup.
[tabs]
======
@@ -233,5 +232,58 @@ Java::
// tests...
}
----
<1> Wrap the bean named `service` with a Mockito `spy`.
<1> Wrap the bean named `service` with a Mockito spy.
======
The following `@SharedSpies` annotation registers two spies by-type and one spy by-name.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoSpyBean(types = {OrderService.class, UserService.class}) // <1>
@MockitoSpyBean(name = "ps1", types = PrintingService.class) // <2>
public @interface SharedSpies {
}
----
<1> Register `OrderService` and `UserService` spies by-type.
<2> Register `PrintingService` spy by-name.
======
The following demonstrates how `@SharedSpies` can be used on a test class.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
@SharedSpies // <1>
class BeanOverrideTests {
@Autowired OrderService orderService; // <2>
@Autowired UserService userService; // <2>
@Autowired PrintingService ps1; // <2>
// Inject other components that rely on the spies.
@Test
void testThatDependsOnMocks() {
// ...
}
}
----
<1> Register common spies via the custom `@SharedSpies` annotation.
<2> Optionally inject spies to _stub_ or _verify_ them.
======
TIP: The spies can also be injected into `@Configuration` classes or other test-related
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
APIs.
@@ -1,8 +1,9 @@
[[spring-testing-annotation-beanoverriding-testbean]]
= `@TestBean`
`@TestBean` is used on a non-static field in a test class to override a specific bean in
the test's `ApplicationContext` with an instance provided by a factory method.
{spring-framework-api}/test/context/bean/override/convention/TestBean.html[`@TestBean`]
is used on a non-static field in a test class to override a specific bean in the test's
`ApplicationContext` with an instance provided by a factory method.
The associated factory method name is derived from the annotated field's name, or the
bean name if specified. The factory method must be `static`, accept no arguments, and
@@ -2,8 +2,8 @@
= 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 one or more non-static fields in the
test class.
`ApplicationContext` for a test class, by annotating the test class or one or more
non-static fields in 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,15 +42,16 @@ 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 in test classes for any non-static field that
is meta-annotated with `@BeanOverride` and instantiates the corresponding
`BeanOverrideProcessor` which is responsible for creating an appropriate
`BeanOverrideHandler`.
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
responsible for creating an appropriate `BeanOverrideHandler`.
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
defined by the corresponding `BeanOverrideStrategy`:
[[testcontext-bean-overriding-strategy]]
`REPLACE`::
Replaces the bean. Throws an exception if a corresponding bean does not exist.
`REPLACE_OR_CREATE`::
@@ -19,6 +19,6 @@ from where they are handled according to their destination prefix. As the channe
a `ThreadPoolExecutor`, messages are processed in different threads, and the resulting sequence
of handling may not match the exact order in which they were received.
To enable ordered publishing, set the `setPreserveReceiveOrder` flag as follows:
To enable ordered receiving, set the `setPreserveReceiveOrder` flag as follows:
include-code::./ReceiveOrderWebSocketConfiguration[tag=snippet,indent=0]
@@ -0,0 +1,105 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.integration.resthttpinterface.customresolver;
import java.util.List;
import org.springframework.core.MethodParameter;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.support.RestClientAdapter;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.invoker.HttpRequestValues;
import org.springframework.web.service.invoker.HttpServiceArgumentResolver;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
public class CustomHttpServiceArgumentResolver {
// tag::httpinterface[]
interface RepositoryService {
@GetExchange("/repos/search")
List<Repository> searchRepository(Search search);
}
// end::httpinterface[]
class Sample {
void sample() {
// tag::usage[]
RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory
.builderFor(adapter)
.customArgumentResolver(new SearchQueryArgumentResolver())
.build();
RepositoryService repositoryService = factory.createClient(RepositoryService.class);
Search search = Search.create()
.owner("spring-projects")
.language("java")
.query("rest")
.build();
List<Repository> repositories = repositoryService.searchRepository(search);
// end::usage[]
}
}
// tag::argumentresolver[]
static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver {
@Override
public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
if (parameter.getParameterType().equals(Search.class)) {
Search search = (Search) argument;
requestValues.addRequestParameter("owner", search.owner());
requestValues.addRequestParameter("language", search.language());
requestValues.addRequestParameter("query", search.query());
return true;
}
return false;
}
}
// end::argumentresolver[]
record Search (String query, String owner, String language) {
static Builder create() {
return new Builder();
}
static class Builder {
Builder query(String query) { return this;}
Builder owner(String owner) { return this;}
Builder language(String language) { return this;}
Search build() {
return new Search(null, null, null);
}
}
}
record Repository(String name) {
}
}
@@ -0,0 +1,78 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.integration.resthttpinterface.customresolver
import org.springframework.core.MethodParameter
import org.springframework.web.client.RestClient
import org.springframework.web.client.support.RestClientAdapter
import org.springframework.web.service.annotation.GetExchange
import org.springframework.web.service.invoker.HttpRequestValues
import org.springframework.web.service.invoker.HttpServiceArgumentResolver
import org.springframework.web.service.invoker.HttpServiceProxyFactory
class CustomHttpServiceArgumentResolver {
// tag::httpinterface[]
interface RepositoryService {
@GetExchange("/repos/search")
fun searchRepository(search: Search): List<Repository>
}
// end::httpinterface[]
class Sample {
fun sample() {
// tag::usage[]
val restClient = RestClient.builder().baseUrl("https://api.github.com/").build()
val adapter = RestClientAdapter.create(restClient)
val factory = HttpServiceProxyFactory
.builderFor(adapter)
.customArgumentResolver(SearchQueryArgumentResolver())
.build()
val repositoryService = factory.createClient<RepositoryService>(RepositoryService::class.java)
val search = Search(owner = "spring-projects", language = "java", query = "rest")
val repositories = repositoryService.searchRepository(search)
// end::usage[]
repositories.size
}
}
// tag::argumentresolver[]
class SearchQueryArgumentResolver : HttpServiceArgumentResolver {
override fun resolve(
argument: Any?,
parameter: MethodParameter,
requestValues: HttpRequestValues.Builder
): Boolean {
if (parameter.getParameterType() == Search::class.java) {
val search = argument as Search
requestValues.addRequestParameter("owner", search.owner)
.addRequestParameter("language", search.language)
.addRequestParameter("query", search.query)
return true
}
return false
}
}
// end::argumentresolver[]
data class Search(val query: String, val owner: String, val language: String)
data class Repository(val name: String)
}
+6 -6
View File
@@ -8,14 +8,14 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.2"))
api(platform("io.micrometer:micrometer-bom:1.14.3"))
api(platform("io.netty:netty-bom:4.1.117.Final"))
api(platform("io.micrometer:micrometer-bom:1.14.4"))
api(platform("io.netty:netty-bom:4.1.118.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.2"))
api(platform("io.rsocket:rsocket-bom:1.1.4"))
api(platform("io.projectreactor:reactor-bom:2024.0.3"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.2"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.16"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
@@ -142,7 +142,7 @@ dependencies {
api("org.seleniumhq.selenium:selenium-java:4.26.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.16")
api("org.testng:testng:7.10.2")
api("org.testng:testng:7.11.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.55")
api("org.webjars:webjars-locator-lite:1.0.0")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.2-SNAPSHOT
version=6.2.3
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,5 +1,5 @@
plugins {
id "com.gradle.develocity" version "3.17.2"
id "com.gradle.develocity" version "3.19"
id "io.spring.ge.conventions" version "0.0.17"
id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0"
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -276,14 +276,18 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
}
if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) {
// May need to add implicit join point arg name...
Class<?> firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0];
if (firstArgType == JoinPoint.class ||
firstArgType == ProceedingJoinPoint.class ||
firstArgType == JoinPoint.StaticPart.class) {
String[] oldNames = this.argumentNames;
this.argumentNames = new String[oldNames.length + 1];
this.argumentNames[0] = "THIS_JOIN_POINT";
System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length);
for (int i = 0; i < this.aspectJAdviceMethod.getParameterCount(); i++) {
Class<?> argType = this.aspectJAdviceMethod.getParameterTypes()[i];
if (argType == JoinPoint.class ||
argType == ProceedingJoinPoint.class ||
argType == JoinPoint.StaticPart.class) {
String[] oldNames = this.argumentNames;
this.argumentNames = new String[oldNames.length + 1];
System.arraycopy(oldNames, 0, this.argumentNames, 0, i);
this.argumentNames[i] = "THIS_JOIN_POINT";
System.arraycopy(oldNames, i, this.argumentNames, i + 1, oldNames.length - i);
break;
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -125,7 +125,7 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport imple
return null;
};
return doSubmit(task, executor, invocation.getMethod().getReturnType());
return doSubmit(task, executor, userMethod.getReturnType());
}
/**
@@ -0,0 +1,135 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aop.aspectj;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Consumer;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link AbstractAspectJAdvice}.
*
* @author Joshua Chen
* @author Stephane Nicoll
*/
class AbstractAspectJAdviceTests {
@Test
void setArgumentNamesFromStringArray_withoutJoinPointParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithNoJoinPoint");
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withJoinPointAsFirstParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsFirstParameter");
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withJoinPointAsLastParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsLastParameter");
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2", "THIS_JOIN_POINT"));
}
@Test
void setArgumentNamesFromStringArray_withJoinPointAsMiddleParameter() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsMiddleParameter");
assertThat(advice).satisfies(hasArgumentNames("arg1", "THIS_JOIN_POINT", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withProceedingJoinPoint() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithProceedingJoinPoint");
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
}
@Test
void setArgumentNamesFromStringArray_withStaticPart() {
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithStaticPart");
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
}
private Consumer<AbstractAspectJAdvice> hasArgumentNames(String... argumentNames) {
return advice -> assertThat(advice).extracting("argumentNames")
.asInstanceOf(InstanceOfAssertFactories.array(String[].class))
.containsExactly(argumentNames);
}
private AbstractAspectJAdvice getAspectJAdvice(final String methodName) {
AbstractAspectJAdvice advice = new TestAspectJAdvice(getMethod(methodName),
mock(AspectJExpressionPointcut.class), mock(AspectInstanceFactory.class));
advice.setArgumentNamesFromStringArray("arg1", "arg2");
return advice;
}
private Method getMethod(final String methodName) {
return Arrays.stream(Sample.class.getDeclaredMethods())
.filter(method -> method.getName().equals(methodName)).findFirst()
.orElseThrow();
}
@SuppressWarnings("serial")
public static class TestAspectJAdvice extends AbstractAspectJAdvice {
public TestAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut,
AspectInstanceFactory aspectInstanceFactory) {
super(aspectJAdviceMethod, pointcut, aspectInstanceFactory);
}
@Override
public boolean isBeforeAdvice() {
return false;
}
@Override
public boolean isAfterAdvice() {
return false;
}
}
@SuppressWarnings("unused")
static class Sample {
void methodWithNoJoinPoint(String arg1, String arg2) {
}
void methodWithJoinPointAsFirstParameter(JoinPoint joinPoint, String arg1, String arg2) {
}
void methodWithJoinPointAsLastParameter(String arg1, String arg2, JoinPoint joinPoint) {
}
void methodWithJoinPointAsMiddleParameter(String arg1, JoinPoint joinPoint, String arg2) {
}
void methodWithProceedingJoinPoint(ProceedingJoinPoint joinPoint, String arg1, String arg2) {
}
void methodWithStaticPart(JoinPoint.StaticPart staticPart, String arg1, String arg2) {
}
}
}
@@ -0,0 +1,73 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aop.interceptor;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Future;
import org.aopalliance.intercept.MethodInvocation;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.core.task.AsyncTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
/**
* Tests for {@link AsyncExecutionInterceptor}.
*
* @author Bao Ngo
* @since 7.0
*/
class AsyncExecutionInterceptorTests {
@Test
@SuppressWarnings("unchecked")
void invokeOnInterfaceWithGeneric() throws Throwable {
AsyncExecutionInterceptor interceptor = spy(new AsyncExecutionInterceptor(null));
FutureRunner impl = new FutureRunner();
MethodInvocation mi = mock();
given(mi.getThis()).willReturn(impl);
given(mi.getMethod()).willReturn(GenericRunner.class.getMethod("run"));
interceptor.invoke(mi);
ArgumentCaptor<Class<?>> classArgumentCaptor = ArgumentCaptor.forClass(Class.class);
verify(interceptor).doSubmit(any(Callable.class), any(AsyncTaskExecutor.class), classArgumentCaptor.capture());
assertThat(classArgumentCaptor.getValue()).isEqualTo(Future.class);
}
interface GenericRunner<O> {
O run();
}
static class FutureRunner implements GenericRunner<Future<Void>> {
@Override
public Future<Void> run() {
return CompletableFuture.runAsync(() -> {
});
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -658,6 +658,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
value = list.get(index);
}
else if (value instanceof Map map) {
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
// IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values.
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
value = map.get(convertedMapKey);
}
else if (value instanceof Iterable iterable) {
// Apply index to Iterator in case of a Set/Collection/Iterable.
int index = Integer.parseInt(key);
@@ -685,14 +693,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
currIndex + ", accessed using property path '" + propertyName + "'");
}
}
else if (value instanceof Map map) {
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
// IMPORTANT: Do not pass full property name in here - property editors
// must not kick in for map keys but rather only for map values.
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
value = map.get(convertedMapKey);
}
else {
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
"Property referenced in indexed property path '" + propertyName +
@@ -904,16 +904,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
private Object newValue(Class<?> type, @Nullable TypeDescriptor desc, String name) {
try {
if (type.isArray()) {
Class<?> componentType = type.componentType();
// TODO - only handles 2-dimensional arrays
if (componentType.isArray()) {
Object array = Array.newInstance(componentType, 1);
Array.set(array, 0, Array.newInstance(componentType.componentType(), 0));
return array;
}
else {
return Array.newInstance(componentType, 0);
}
return createArray(type);
}
else if (Collection.class.isAssignableFrom(type)) {
TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null);
@@ -937,6 +928,24 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
}
/**
* Create the array for the given array type.
* @param arrayType the desired type of the target array
* @return a new array instance
*/
private static Object createArray(Class<?> arrayType) {
Assert.notNull(arrayType, "Array type must not be null");
Class<?> componentType = arrayType.componentType();
if (componentType.isArray()) {
Object array = Array.newInstance(componentType, 1);
Array.set(array, 0, createArray(componentType));
return array;
}
else {
return Array.newInstance(componentType, 0);
}
}
/**
* Parse the given property name into the corresponding property name tokens.
* @param propertyName the property name to parse
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,4 +45,18 @@ public interface PropertyEditorRegistrar {
*/
void registerCustomEditors(PropertyEditorRegistry registry);
/**
* Indicate whether this registrar exclusively overrides default editors
* rather than registering custom editors, intended to be applied lazily.
* <p>This has an impact on registrar handling in a bean factory: see
* {@link org.springframework.beans.factory.config.ConfigurableBeanFactory#addPropertyEditorRegistrar}.
* @since 6.2.3
* @see PropertyEditorRegistry#registerCustomEditor
* @see PropertyEditorRegistrySupport#overrideDefaultEditor
* @see PropertyEditorRegistrySupport#setDefaultEditorRegistrar
*/
default boolean overridesDefaultEditors() {
return false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -99,6 +99,9 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
private boolean configValueEditorsActive = false;
@Nullable
private PropertyEditorRegistrar defaultEditorRegistrar;
@Nullable
private Map<Class<?>, PropertyEditor> defaultEditors;
@@ -155,6 +158,19 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
this.configValueEditorsActive = true;
}
/**
* Set a registrar for default editors, as a lazy way of overriding default editors.
* <p>This is expected to be a collaborator with {@link PropertyEditorRegistrySupport},
* downcasting the given {@link PropertyEditorRegistry} accordingly and calling
* {@link #overrideDefaultEditor} for registering additional default editors on it.
* @param registrar the registrar to call when default editors are actually needed
* @since 6.2.3
* @see #overrideDefaultEditor
*/
public void setDefaultEditorRegistrar(PropertyEditorRegistrar registrar) {
this.defaultEditorRegistrar = registrar;
}
/**
* Override the default editor for the specified type with the given property editor.
* <p>Note that this is different from registering a custom editor in that the editor
@@ -184,6 +200,9 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
if (!this.defaultEditorsActive) {
return null;
}
if (this.overriddenDefaultEditors == null && this.defaultEditorRegistrar != null) {
this.defaultEditorRegistrar.registerCustomEditors(this);
}
if (this.overriddenDefaultEditors != null) {
PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType);
if (editor != null) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -39,10 +39,14 @@ import org.springframework.util.StringUtils;
* (which the methods defined on the ListableBeanFactory interface don't,
* in contrast to the methods defined on the BeanFactory interface).
*
* <p><b>NOTE:</b> It is generally preferable to use {@link ObjectProvider#stream()}
* via {@link BeanFactory#getBeanProvider} instead of this utility class.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @author Chris Beams
* @since 04.07.2003
* @see BeanFactory#getBeanProvider
*/
public abstract class BeanFactoryUtils {
@@ -308,7 +312,7 @@ public abstract class BeanFactoryUtils {
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
* @param lbf the bean factory
* @param type type of bean to match
* @param type the type of bean to match
* @return the Map of matching bean instances, or an empty Map if none
* @throws BeansException if a bean could not be created
* @see ListableBeanFactory#getBeansOfType(Class)
@@ -347,7 +351,7 @@ public abstract class BeanFactoryUtils {
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
* @param lbf the bean factory
* @param type type of bean to match
* @param type the type of bean to match
* @param includeNonSingletons whether to include prototype or scoped beans too
* or just singletons (also applies to FactoryBeans)
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
@@ -395,7 +399,7 @@ public abstract class BeanFactoryUtils {
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
* @param lbf the bean factory
* @param type type of bean to match
* @param type the type of bean to match
* @return the matching bean instance
* @throws NoSuchBeanDefinitionException if no bean of the given type was found
* @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
@@ -425,7 +429,7 @@ public abstract class BeanFactoryUtils {
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
* @param lbf the bean factory
* @param type type of bean to match
* @param type the type of bean to match
* @param includeNonSingletons whether to include prototype or scoped beans too
* or just singletons (also applies to FactoryBeans)
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
@@ -457,7 +461,7 @@ public abstract class BeanFactoryUtils {
* <p>This version of {@code beanOfType} automatically includes
* prototypes and FactoryBeans.
* @param lbf the bean factory
* @param type type of bean to match
* @param type the type of bean to match
* @return the matching bean instance
* @throws NoSuchBeanDefinitionException if no bean of the given type was found
* @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
@@ -481,7 +485,7 @@ public abstract class BeanFactoryUtils {
* only raw FactoryBeans will be checked (which doesn't require initialization
* of each FactoryBean).
* @param lbf the bean factory
* @param type type of bean to match
* @param type the type of bean to match
* @param includeNonSingletons whether to include prototype or scoped beans too
* or just singletons (also applies to FactoryBeans)
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
@@ -529,7 +533,7 @@ public abstract class BeanFactoryUtils {
/**
* Extract a unique bean for the given type from the given Map of matching beans.
* @param type type of bean to match
* @param type the type of bean to match
* @param matchingBeans all matching beans found
* @return the unique bean instance
* @throws NoSuchBeanDefinitionException if no bean of the given type was found
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.beans.factory;
import java.util.Iterator;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -53,6 +54,18 @@ import org.springframework.lang.Nullable;
*/
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
/**
* A predicate for unfiltered type matches, including non-default candidates
* but still excluding non-autowire candidates when used on injection points.
* @since 6.2.3
* @see #stream(Predicate)
* @see #orderedStream(Predicate)
* @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate()
* @see org.springframework.beans.factory.support.AbstractBeanDefinition#isDefaultCandidate()
*/
Predicate<Class<?>> UNFILTERED = (clazz -> true);
@Override
default T getObject() throws BeansException {
Iterator<T> it = iterator();
@@ -198,6 +211,10 @@ public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
/**
* Return a sequential {@link Stream} over all matching object instances,
* without specific ordering guarantees (but typically in registration order).
* <p>Note: The result may be filtered by default according to qualifiers on the
* injection point versus target beans and the general autowire candidate status
* of matching beans. For custom filtering against type-matching candidates, use
* {@link #stream(Predicate)} instead (potentially with {@link #UNFILTERED}).
* @since 5.1
* @see #iterator()
* @see #orderedStream()
@@ -219,6 +236,10 @@ public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
* {@link #stream()} method. You may override this to apply an
* {@link org.springframework.core.annotation.AnnotationAwareOrderComparator}
* if necessary.
* <p>Note: The result may be filtered by default according to qualifiers on the
* injection point versus target beans and the general autowire candidate status
* of matching beans. For custom filtering against type-matching candidates, use
* {@link #stream(Predicate)} instead (potentially with {@link #UNFILTERED}).
* @since 5.1
* @see #stream()
* @see org.springframework.core.OrderComparator
@@ -227,4 +248,32 @@ public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
return stream().sorted(OrderComparator.INSTANCE);
}
/**
* Return a custom-filtered {@link Stream} over all matching object instances,
* without specific ordering guarantees (but typically in registration order).
* @param customFilter a custom type filter for selecting beans among the raw
* bean type matches (or {@link #UNFILTERED} for all raw type matches without
* any default filtering)
* @since 6.2.3
* @see #stream()
* @see #orderedStream(Predicate)
*/
default Stream<T> stream(Predicate<Class<?>> customFilter) {
return stream().filter(obj -> customFilter.test(obj.getClass()));
}
/**
* Return a custom-filtered {@link Stream} over all matching object instances,
* pre-ordered according to the factory's common order comparator.
* @param customFilter a custom type filter for selecting beans among the raw
* bean type matches (or {@link #UNFILTERED} for all raw type matches without
* any default filtering)
* @since 6.2.3
* @see #orderedStream()
* @see #stream(Predicate)
*/
default Stream<T> orderedStream(Predicate<Class<?>> customFilter) {
return orderedStream().filter(obj -> customFilter.test(obj.getClass()));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -183,7 +183,11 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single
* on the given registry, fresh for each bean creation attempt. This avoids
* the need for synchronization on custom editors; hence, it is generally
* preferable to use this method instead of {@link #registerCustomEditor}.
* <p>If the given registrar implements
* {@link PropertyEditorRegistrar#overridesDefaultEditors()} to return {@code true},
* it will be applied lazily (only when default editors are actually needed).
* @param registrar the PropertyEditorRegistrar to register
* @see PropertyEditorRegistrar#overridesDefaultEditors()
*/
void addPropertyEditorRegistrar(PropertyEditorRegistrar registrar);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -137,6 +137,9 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
@Nullable
private ConversionService conversionService;
/** Default PropertyEditorRegistrars to apply to the beans of this factory. */
private final Set<PropertyEditorRegistrar> defaultEditorRegistrars = new LinkedHashSet<>(4);
/** Custom PropertyEditorRegistrars to apply to the beans of this factory. */
private final Set<PropertyEditorRegistrar> propertyEditorRegistrars = new LinkedHashSet<>(4);
@@ -883,7 +886,12 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
@Override
public void addPropertyEditorRegistrar(PropertyEditorRegistrar registrar) {
Assert.notNull(registrar, "PropertyEditorRegistrar must not be null");
this.propertyEditorRegistrars.add(registrar);
if (registrar.overridesDefaultEditors()) {
this.defaultEditorRegistrars.add(registrar);
}
else {
this.propertyEditorRegistrars.add(registrar);
}
}
/**
@@ -1114,6 +1122,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
setBeanExpressionResolver(otherFactory.getBeanExpressionResolver());
setConversionService(otherFactory.getConversionService());
if (otherFactory instanceof AbstractBeanFactory otherAbstractFactory) {
this.defaultEditorRegistrars.addAll(otherAbstractFactory.defaultEditorRegistrars);
this.propertyEditorRegistrars.addAll(otherAbstractFactory.propertyEditorRegistrars);
this.customEditors.putAll(otherAbstractFactory.customEditors);
this.typeConverter = otherAbstractFactory.typeConverter;
@@ -1313,36 +1322,48 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
protected void registerCustomEditors(PropertyEditorRegistry registry) {
if (registry instanceof PropertyEditorRegistrySupport registrySupport) {
registrySupport.useConfigValueEditors();
}
if (!this.propertyEditorRegistrars.isEmpty()) {
for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
try {
registrar.registerCustomEditors(registry);
}
catch (BeanCreationException ex) {
Throwable rootCause = ex.getMostSpecificCause();
if (rootCause instanceof BeanCurrentlyInCreationException bce) {
String bceBeanName = bce.getBeanName();
if (bceBeanName != null && isCurrentlyInCreation(bceBeanName)) {
if (logger.isDebugEnabled()) {
logger.debug("PropertyEditorRegistrar [" + registrar.getClass().getName() +
"] failed because it tried to obtain currently created bean '" +
ex.getBeanName() + "': " + ex.getMessage());
}
onSuppressedException(ex);
continue;
}
}
throw ex;
}
if (!this.defaultEditorRegistrars.isEmpty()) {
// Optimization: lazy overriding of default editors only when needed
registrySupport.setDefaultEditorRegistrar(new BeanFactoryDefaultEditorRegistrar());
}
}
else if (!this.defaultEditorRegistrars.isEmpty()) {
// Fallback: proactive overriding of default editors
applyEditorRegistrars(registry, this.defaultEditorRegistrars);
}
if (!this.propertyEditorRegistrars.isEmpty()) {
applyEditorRegistrars(registry, this.propertyEditorRegistrars);
}
if (!this.customEditors.isEmpty()) {
this.customEditors.forEach((requiredType, editorClass) ->
registry.registerCustomEditor(requiredType, BeanUtils.instantiateClass(editorClass)));
}
}
private void applyEditorRegistrars(PropertyEditorRegistry registry, Set<PropertyEditorRegistrar> registrars) {
for (PropertyEditorRegistrar registrar : registrars) {
try {
registrar.registerCustomEditors(registry);
}
catch (BeanCreationException ex) {
Throwable rootCause = ex.getMostSpecificCause();
if (rootCause instanceof BeanCurrentlyInCreationException bce) {
String bceBeanName = bce.getBeanName();
if (bceBeanName != null && isCurrentlyInCreation(bceBeanName)) {
if (logger.isDebugEnabled()) {
logger.debug("PropertyEditorRegistrar [" + registrar.getClass().getName() +
"] failed because it tried to obtain currently created bean '" +
ex.getBeanName() + "': " + ex.getMessage());
}
onSuppressedException(ex);
return;
}
}
throw ex;
}
}
}
/**
* Return a merged RootBeanDefinition, traversing the parent bean definition
@@ -2095,4 +2116,20 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
final List<MergedBeanDefinitionPostProcessor> mergedDefinition = new ArrayList<>();
}
/**
* {@link PropertyEditorRegistrar} that delegates to the bean factory's
* default registrars, adding exception handling for circular reference
* scenarios where an editor tries to refer back to the currently created bean.
*
* @since 6.2.3
*/
class BeanFactoryDefaultEditorRegistrar implements PropertyEditorRegistrar {
@Override
public void registerCustomEditors(PropertyEditorRegistry registry) {
applyEditorRegistrars(registry, defaultEditorRegistrars);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,7 +33,9 @@ import java.util.Comparator;
import java.util.Set;
import org.springframework.beans.BeanMetadataElement;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -259,6 +261,24 @@ abstract class AutowireUtils {
return method.getReturnType();
}
/**
* Check the autowire-candidate status for the specified bean.
* @param beanFactory the bean factory
* @param beanName the name of the bean to check
* @return whether the specified bean qualifies as an autowire candidate
* @since 6.2.3
* @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate()
*/
public static boolean isAutowireCandidate(ConfigurableBeanFactory beanFactory, String beanName) {
try {
return beanFactory.getMergedBeanDefinition(beanName).isAutowireCandidate();
}
catch (NoSuchBeanDefinitionException ex) {
// A manually registered singleton instance not backed by a BeanDefinition.
return true;
}
}
/**
* Reflective {@link InvocationHandler} for lazy access to the current target object.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -508,6 +508,32 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
Stream<T> stream = matchingBeans.values().stream();
return stream.sorted(adaptOrderComparator(matchingBeans));
}
@SuppressWarnings("unchecked")
@Override
public Stream<T> stream(Predicate<Class<?>> customFilter) {
return Arrays.stream(getBeanNamesForTypedStream(requiredType, allowEagerInit))
.filter(name -> customFilter.test(getType(name)))
.map(name -> (T) getBean(name))
.filter(bean -> !(bean instanceof NullBean));
}
@SuppressWarnings("unchecked")
@Override
public Stream<T> orderedStream(Predicate<Class<?>> customFilter) {
String[] beanNames = getBeanNamesForTypedStream(requiredType, allowEagerInit);
if (beanNames.length == 0) {
return Stream.empty();
}
Map<String, T> matchingBeans = CollectionUtils.newLinkedHashMap(beanNames.length);
for (String beanName : beanNames) {
if (customFilter.test(getType(beanName))) {
Object beanInstance = getBean(beanName);
if (!(beanInstance instanceof NullBean)) {
matchingBeans.put(beanName, (T) beanInstance);
}
}
}
return matchingBeans.values().stream().sorted(adaptOrderComparator(matchingBeans));
}
};
}
@@ -1089,8 +1115,15 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
"without bootstrap executor configured - falling back to mainline initialization");
}
}
if (!mbd.isLazyInit()) {
instantiateSingleton(beanName);
try {
instantiateSingleton(beanName);
}
catch (BeanCurrentlyInCreationException ex) {
logger.info("Bean '" + beanName + "' marked for pre-instantiation (not lazy-init) " +
"but currently initialized by other thread - skipping it in mainline thread");
}
}
return null;
}
@@ -1885,8 +1918,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
candidates.put(candidateName, beanInstance);
}
}
else if (containsSingleton(candidateName) || (descriptor instanceof StreamDependencyDescriptor streamDescriptor &&
streamDescriptor.isOrdered())) {
else if (containsSingleton(candidateName) ||
(descriptor instanceof StreamDependencyDescriptor streamDescriptor && streamDescriptor.isOrdered())) {
Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this);
candidates.put(candidateName, (beanInstance instanceof NullBean ? null : beanInstance));
}
@@ -2479,6 +2512,34 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
Object result = doResolveDependency(descriptorToUse, this.beanName, null, null);
return (result instanceof Stream stream ? stream : Stream.of(result));
}
@Override
public Stream<Object> stream(Predicate<Class<?>> customFilter) {
return Arrays.stream(getBeanNamesForTypedStream(this.descriptor.getResolvableType(), true))
.filter(name -> AutowireUtils.isAutowireCandidate(DefaultListableBeanFactory.this, name))
.filter(name -> customFilter.test(getType(name)))
.map(name -> getBean(name))
.filter(bean -> !(bean instanceof NullBean));
}
@Override
public Stream<Object> orderedStream(Predicate<Class<?>> customFilter) {
String[] beanNames = getBeanNamesForTypedStream(this.descriptor.getResolvableType(), true);
if (beanNames.length == 0) {
return Stream.empty();
}
Map<String, Object> matchingBeans = CollectionUtils.newLinkedHashMap(beanNames.length);
for (String beanName : beanNames) {
if (AutowireUtils.isAutowireCandidate(DefaultListableBeanFactory.this, beanName) &&
customFilter.test(getType(beanName))) {
Object beanInstance = getBean(beanName);
if (!(beanInstance instanceof NullBean)) {
matchingBeans.put(beanName, beanInstance);
}
}
}
return matchingBeans.values().stream().sorted(adaptOrderComparator(matchingBeans));
}
}
@@ -24,6 +24,7 @@ import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Consumer;
@@ -100,6 +101,15 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
/** Names of beans currently excluded from in creation checks. */
private final Set<String> inCreationCheckExclusions = ConcurrentHashMap.newKeySet(16);
/** Specific lock for lenient creation tracking. */
private final Lock lenientCreationLock = new ReentrantLock();
/** Specific lock condition for lenient creation tracking. */
private final Condition lenientCreationFinished = this.lenientCreationLock.newCondition();
/** Names of beans that are currently in lenient creation. */
private final Set<String> singletonsInLenientCreation = new HashSet<>();
/** Flag that indicates whether we're currently within destroySingletons. */
private volatile boolean singletonsCurrentlyInDestruction = false;
@@ -243,6 +253,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
boolean acquireLock = !Boolean.FALSE.equals(lockFlag);
boolean locked = (acquireLock && this.singletonLock.tryLock());
boolean lenient = false;
try {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
@@ -257,6 +268,14 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
Thread.currentThread().getName() + "\" while other thread holds " +
"singleton lock for other beans " + this.singletonsCurrentlyInCreation);
}
lenient = true;
this.lenientCreationLock.lock();
try {
this.singletonsInLenientCreation.add(beanName);
}
finally {
this.lenientCreationLock.unlock();
}
}
else {
// No specific locking indication (outside a coordinated bootstrap) and
@@ -284,6 +303,26 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
beforeSingletonCreation(beanName);
}
catch (BeanCurrentlyInCreationException ex) {
this.lenientCreationLock.lock();
try {
while ((singletonObject = this.singletonObjects.get(beanName)) == null) {
if (!this.singletonsInLenientCreation.contains(beanName)) {
break;
}
try {
this.lenientCreationFinished.await();
}
catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
}
}
finally {
this.lenientCreationLock.unlock();
}
if (singletonObject != null) {
return singletonObject;
}
if (locked) {
throw ex;
}
@@ -339,6 +378,16 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
if (locked) {
this.singletonLock.unlock();
}
if (lenient) {
this.lenientCreationLock.lock();
try {
this.singletonsInLenientCreation.remove(beanName);
this.lenientCreationFinished.signalAll();
}
finally {
this.lenientCreationLock.unlock();
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,13 @@
package org.springframework.beans.factory.support;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.lang.Nullable;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
/**
* {@link AutowireCandidateResolver} implementation to use when no annotation
@@ -36,46 +40,6 @@ public class SimpleAutowireCandidateResolver implements AutowireCandidateResolve
*/
public static final SimpleAutowireCandidateResolver INSTANCE = new SimpleAutowireCandidateResolver();
@Override
public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
return bdHolder.getBeanDefinition().isAutowireCandidate();
}
@Override
public boolean isRequired(DependencyDescriptor descriptor) {
return descriptor.isRequired();
}
@Override
public boolean hasQualifier(DependencyDescriptor descriptor) {
return false;
}
@Override
@Nullable
public String getSuggestedName(DependencyDescriptor descriptor) {
return null;
}
@Override
@Nullable
public Object getSuggestedValue(DependencyDescriptor descriptor) {
return null;
}
@Override
@Nullable
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
return null;
}
@Override
@Nullable
public Class<?> getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) {
return null;
}
/**
* This implementation returns {@code this} as-is.
* @see #INSTANCE
@@ -85,4 +49,31 @@ public class SimpleAutowireCandidateResolver implements AutowireCandidateResolve
return this;
}
/**
* Resolve a map of all beans of the given type, also picking up beans defined in
* ancestor bean factories, with the specific condition that each bean actually
* has autowire candidate status. This matches simple injection point resolution
* as implemented by this {@link AutowireCandidateResolver} strategy, including
* beans which are not marked as default candidates but excluding beans which
* are not even marked as autowire candidates.
* @param lbf the bean factory
* @param type the type of bean to match
* @return the Map of matching bean instances, or an empty Map if none
* @throws BeansException if a bean could not be created
* @since 6.2.3
* @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class)
* @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate()
* @see AbstractBeanDefinition#isDefaultCandidate()
*/
public static <T> Map<String, T> resolveAutowireCandidates(ConfigurableListableBeanFactory lbf, Class<T> type) {
Map<String, T> candidates = new LinkedHashMap<>();
for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(lbf, type)) {
if (AutowireUtils.isAutowireCandidate(lbf, beanName)) {
candidates.put(beanName, lbf.getBean(beanName, type));
}
}
return candidates;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -135,4 +135,12 @@ public class ResourceEditorRegistrar implements PropertyEditorRegistrar {
}
}
/**
* Indicate the use of {@link PropertyEditorRegistrySupport#overrideDefaultEditor} above.
*/
@Override
public boolean overridesDefaultEditors() {
return true;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -139,6 +139,7 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isReadableProperty("list")).isTrue();
assertThat(accessor.isReadableProperty("set")).isTrue();
assertThat(accessor.isReadableProperty("map")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans")).isTrue();
assertThat(accessor.isReadableProperty("xxx")).isFalse();
@@ -146,6 +147,7 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isWritableProperty("list")).isTrue();
assertThat(accessor.isWritableProperty("set")).isTrue();
assertThat(accessor.isWritableProperty("map")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap")).isTrue();
assertThat(accessor.isWritableProperty("myTestBeans")).isTrue();
assertThat(accessor.isWritableProperty("xxx")).isFalse();
@@ -161,6 +163,14 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isReadableProperty("map[key4][0].name")).isTrue();
assertThat(accessor.isReadableProperty("map[key4][1]")).isTrue();
assertThat(accessor.isReadableProperty("map[key4][1].name")).isTrue();
assertThat(accessor.isReadableProperty("map[key999]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key1]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key1].name")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][0]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][0].name")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][1]")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key2][1].name")).isTrue();
assertThat(accessor.isReadableProperty("iterableMap[key999]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
assertThat(accessor.isReadableProperty("array[key1]")).isFalse();
@@ -177,6 +187,14 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.isWritableProperty("map[key4][0].name")).isTrue();
assertThat(accessor.isWritableProperty("map[key4][1]")).isTrue();
assertThat(accessor.isWritableProperty("map[key4][1].name")).isTrue();
assertThat(accessor.isWritableProperty("map[key999]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key1]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key1].name")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][0]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][0].name")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][1]")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key2][1].name")).isTrue();
assertThat(accessor.isWritableProperty("iterableMap[key999]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[0]")).isTrue();
assertThat(accessor.isReadableProperty("myTestBeans[1]")).isFalse();
assertThat(accessor.isWritableProperty("array[key1]")).isFalse();
@@ -1395,6 +1413,9 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map['key5[foo]'].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("map[\"key5[foo]\"].name")).isEqualTo("name8");
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("nameC");
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("nameA");
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("nameB");
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZ");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -1409,6 +1430,9 @@ abstract class AbstractPropertyAccessorTests {
pvs.add("map[key4][0].name", "nameA");
pvs.add("map[key4][1].name", "nameB");
pvs.add("map[key5[foo]].name", "name10");
pvs.add("iterableMap[key1].name", "newName1");
pvs.add("iterableMap[key2][0].name", "newName2A");
pvs.add("iterableMap[key2][1].name", "newName2B");
pvs.add("myTestBeans[0].name", "nameZZ");
accessor.setPropertyValues(pvs);
assertThat(tb0.getName()).isEqualTo("name5");
@@ -1428,6 +1452,9 @@ abstract class AbstractPropertyAccessorTests {
assertThat(accessor.getPropertyValue("map[key4][0].name")).isEqualTo("nameA");
assertThat(accessor.getPropertyValue("map[key4][1].name")).isEqualTo("nameB");
assertThat(accessor.getPropertyValue("map[key5[foo]].name")).isEqualTo("name10");
assertThat(accessor.getPropertyValue("iterableMap[key1].name")).isEqualTo("newName1");
assertThat(accessor.getPropertyValue("iterableMap[key2][0].name")).isEqualTo("newName2A");
assertThat(accessor.getPropertyValue("iterableMap[key2][1].name")).isEqualTo("newName2B");
assertThat(accessor.getPropertyValue("myTestBeans[0].name")).isEqualTo("nameZZ");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -103,6 +103,27 @@ class BeanWrapperAutoGrowingTests {
assertThat(bean.getThreeDimensionalArray()[1][2][3]).isInstanceOf(Bean.class);
}
@Test
void getPropertyValueAutoGrow3dArrayList() {
assertThat(wrapper.getPropertyValue("threeDimensionalArrayList[1][2][3][4]")).isNotNull();
assertThat(bean.getThreeDimensionalArrayList()).hasSize(2);
assertThat(bean.getThreeDimensionalArrayList().get(1)).hasNumberOfRows(3);
assertThat(bean.getThreeDimensionalArrayList().get(1)[2]).hasNumberOfRows(4);
assertThat(bean.getThreeDimensionalArrayList().get(1)[2][3]).hasSize(5);
assertThat(bean.getThreeDimensionalArrayList().get(1)[2][3][4]).isInstanceOf(Bean.class);
}
@Test
void getPropertyValueAutoGrow3dArrayListForDefault3dArray() {
assertThat(wrapper.getPropertyValue("threeDimensionalArrayList[0]")).isNotNull();
assertThat(bean.getThreeDimensionalArrayList()).hasSize(1);
// Default 3-dimensional array should be [[[]]]
assertThat(bean.getThreeDimensionalArrayList().get(0)).hasNumberOfRows(1);
assertThat(bean.getThreeDimensionalArrayList().get(0)[0]).hasNumberOfRows(1);
assertThat(bean.getThreeDimensionalArrayList().get(0)[0][0]).isEmpty();
}
@Test
void setPropertyValueAutoGrow2dArray() {
Bean newBean = new Bean();
@@ -123,6 +144,16 @@ class BeanWrapperAutoGrowingTests {
.extracting(Bean::getProp).isEqualTo("enigma");
}
@Test
void setPropertyValueAutoGrow3dArrayList() {
Bean newBean = new Bean();
newBean.setProp("enigma");
wrapper.setPropertyValue("threeDimensionalArrayList[0][1][2][3]", newBean);
assertThat(bean.getThreeDimensionalArrayList().get(0)[1][2][3])
.isInstanceOf(Bean.class)
.extracting(Bean::getProp).isEqualTo("enigma");
}
@Test
void getPropertyValueAutoGrowList() {
assertThat(wrapper.getPropertyValue("list[0]")).isNotNull();
@@ -215,6 +246,8 @@ class BeanWrapperAutoGrowingTests {
private Bean[][][] threeDimensionalArray;
private List<Bean[][][]> threeDimensionalArrayList;
private List<Bean> list;
private List<List<Bean>> nestedList;
@@ -269,6 +302,14 @@ class BeanWrapperAutoGrowingTests {
this.threeDimensionalArray = threeDimensionalArray;
}
public List<Bean[][][]> getThreeDimensionalArrayList() {
return threeDimensionalArrayList;
}
public void setThreeDimensionalArrayList(List<Bean[][][]> threeDimensionalArrayList) {
this.threeDimensionalArrayList = threeDimensionalArrayList;
}
public List<Bean> getList() {
return list;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1515,12 +1515,16 @@ class DefaultListableBeanFactoryTests {
bd1.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.LOWEST_PRECEDENCE);
lbf.registerBeanDefinition("bean1", bd1);
GenericBeanDefinition bd2 = new GenericBeanDefinition();
bd2.setBeanClass(TestBean.class);
bd2.setBeanClass(DerivedTestBean.class);
bd2.setPropertyValues(new MutablePropertyValues(List.of(new PropertyValue("name", "highest"))));
bd2.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.HIGHEST_PRECEDENCE);
lbf.registerBeanDefinition("bean2", bd2);
assertThat(lbf.getBeanProvider(TestBean.class).orderedStream().map(TestBean::getName))
.containsExactly("highest", "lowest");
assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(ObjectProvider.UNFILTERED).map(TestBean::getName))
.containsExactly("highest", "lowest");
assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(clazz -> !DerivedTestBean.class.isAssignableFrom(clazz))
.map(TestBean::getName)).containsExactly("lowest");
}
@Test
@@ -1540,6 +1544,8 @@ class DefaultListableBeanFactoryTests {
lbf.registerBeanDefinition("bean2", bd2);
assertThat(lbf.getBeanProvider(TestBean.class).orderedStream().map(TestBean::getName))
.containsExactly("fromLowestPrecedenceTestBeanFactoryBean", "fromHighestPrecedenceTestBeanFactoryBean");
assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(ObjectProvider.UNFILTERED).map(TestBean::getName))
.containsExactly("fromLowestPrecedenceTestBeanFactoryBean", "fromHighestPrecedenceTestBeanFactoryBean");
}
@Test
@@ -1934,6 +1940,11 @@ class DefaultListableBeanFactoryTests {
assertThat(resolved).hasSize(2);
assertThat(resolved).contains(lbf.getBean("bd1"));
assertThat(resolved).contains(lbf.getBean("bd2"));
resolved = provider.stream(ObjectProvider.UNFILTERED).collect(Collectors.toSet());
assertThat(resolved).hasSize(2);
assertThat(resolved).contains(lbf.getBean("bd1"));
assertThat(resolved).contains(lbf.getBean("bd2"));
}
@Test
@@ -1983,6 +1994,11 @@ class DefaultListableBeanFactoryTests {
assertThat(resolved).hasSize(2);
assertThat(resolved).contains(lbf.getBean("bd1"));
assertThat(resolved).contains(lbf.getBean("bd2"));
resolved = provider.stream(ObjectProvider.UNFILTERED).collect(Collectors.toSet());
assertThat(resolved).hasSize(2);
assertThat(resolved).contains(lbf.getBean("bd1"));
assertThat(resolved).contains(lbf.getBean("bd2"));
}
@Test
@@ -2378,11 +2394,20 @@ class DefaultListableBeanFactoryTests {
parentBf.registerBeanDefinition("highPriorityTestBean", bd2);
ObjectProvider<TestBean> testBeanProvider = lbf.getBeanProvider(ResolvableType.forClass(TestBean.class));
List<TestBean> resolved = testBeanProvider.orderedStream().toList();
assertThat(resolved).containsExactly(
assertThat(testBeanProvider.orderedStream()).containsExactly(
lbf.getBean("highPriorityTestBean", TestBean.class),
lbf.getBean("lowPriorityTestBean", TestBean.class),
lbf.getBean("plainTestBean", TestBean.class));
assertThat(testBeanProvider.orderedStream(clazz -> clazz != TestBean.class).toList()).containsExactly(
lbf.getBean("highPriorityTestBean", TestBean.class),
lbf.getBean("lowPriorityTestBean", TestBean.class));
assertThat(testBeanProvider.stream()).containsExactly(
lbf.getBean("plainTestBean", TestBean.class),
lbf.getBean("lowPriorityTestBean", TestBean.class),
lbf.getBean("highPriorityTestBean", TestBean.class));
assertThat(testBeanProvider.orderedStream(clazz -> clazz != TestBean.class).toList()).containsExactly(
lbf.getBean("lowPriorityTestBean", TestBean.class),
lbf.getBean("highPriorityTestBean", TestBean.class));
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,6 +48,7 @@ import org.mockito.Mockito;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.FactoryBean;
@@ -64,6 +65,8 @@ import org.springframework.beans.factory.support.AutowireCandidateQualifier;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.support.SimpleAutowireCandidateResolver;
import org.springframework.beans.testfixture.beans.DerivedTestBean;
import org.springframework.beans.testfixture.beans.ITestBean;
import org.springframework.beans.testfixture.beans.IndexedTestBean;
import org.springframework.beans.testfixture.beans.NestedTestBean;
@@ -1605,7 +1608,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class));
testBeans = bean.streamTestBeans();
assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class));
testBeans = bean.sortedTestBeans();
testBeans = bean.streamTestBeansInOrder();
assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class));
testBeans = bean.allTestBeans();
assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class));
testBeans = bean.allTestBeansInOrder();
assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class));
}
@@ -1632,7 +1639,13 @@ class AutowiredAnnotationBeanPostProcessorTests {
testBeans = bean.streamTestBeans();
assertThat(testBeans).hasSize(1);
assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class));
testBeans = bean.sortedTestBeans();
testBeans = bean.streamTestBeansInOrder();
assertThat(testBeans).hasSize(1);
assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class));
testBeans = bean.allTestBeans();
assertThat(testBeans).hasSize(1);
assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class));
testBeans = bean.allTestBeansInOrder();
assertThat(testBeans).hasSize(1);
assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class));
}
@@ -1656,7 +1669,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(testBeans).isEmpty();
testBeans = bean.streamTestBeans();
assertThat(testBeans).isEmpty();
testBeans = bean.sortedTestBeans();
testBeans = bean.streamTestBeansInOrder();
assertThat(testBeans).isEmpty();
testBeans = bean.allTestBeans();
assertThat(testBeans).isEmpty();
testBeans = bean.allTestBeansInOrder();
assertThat(testBeans).isEmpty();
}
@@ -1678,7 +1695,9 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(bean.iterateTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.forEachTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.streamTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.sortedTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.streamTestBeansInOrder()).containsExactly(testBean1, testBean2);
assertThat(bean.allTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.allTestBeansInOrder()).containsExactly(testBean1, testBean2);
}
@Test
@@ -1706,7 +1725,9 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(bean.iterateTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.forEachTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.streamTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.sortedTestBeans()).containsExactly(testBean2, testBean1);
assertThat(bean.streamTestBeansInOrder()).containsExactly(testBean2, testBean1);
assertThat(bean.allTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.allTestBeansInOrder()).containsExactly(testBean2, testBean1);
}
@Test
@@ -1722,8 +1743,53 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerBeanDefinition("testBean2", tb2);
ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class);
assertThat(bean.sortedTestBeans()).containsExactly(bf.getBean("testBean2", TestBean.class),
assertThat(bean.streamTestBeansInOrder()).containsExactly(bf.getBean("testBean2", TestBean.class),
bf.getBean("testBean1", TestBean.class));
assertThat(bean.allTestBeansInOrder()).containsExactly(bf.getBean("testBean2", TestBean.class),
bf.getBean("testBean1", TestBean.class));
}
@Test
void objectProviderInjectionWithNonCandidatesInStream() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class));
RootBeanDefinition tb1 = new RootBeanDefinition(TestBeanFactory.class);
tb1.setFactoryMethodName("newTestBean1");
bf.registerBeanDefinition("testBean1", tb1);
RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class);
tb2.setFactoryMethodName("newTestBean2");
bf.registerBeanDefinition("testBean2", tb2);
DefaultListableBeanFactory parent = new DefaultListableBeanFactory();
RootBeanDefinition tb3 = new RootBeanDefinition(TestBean.class);
tb3.setAutowireCandidate(false);
tb3.setLazyInit(true);
parent.registerBeanDefinition("testBean3", tb3);
RootBeanDefinition tb4 = new RootBeanDefinition(DerivedTestBean.class);
tb4.setDefaultCandidate(false);
tb4.setLazyInit(true);
parent.registerBeanDefinition("testBean4", tb4);
bf.setParentBeanFactory(parent);
ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class);
assertThat(bean.streamTestBeans()).containsExactly(bf.getBean("testBean1", TestBean.class),
bf.getBean("testBean2", TestBean.class));
assertThat(bean.streamTestBeansInOrder()).containsExactly(bf.getBean("testBean2", TestBean.class),
bf.getBean("testBean1", TestBean.class));
assertThat(bf.containsSingleton("testBean3")).isFalse();
assertThat(bean.plainTestBeans()).containsExactly(bf.getBean("testBean1", TestBean.class),
bf.getBean("testBean2", TestBean.class));
assertThat(bean.plainTestBeansInOrder()).containsExactly(bf.getBean("testBean2", TestBean.class),
bf.getBean("testBean1", TestBean.class));
assertThat(bf.containsSingleton("testBean4")).isFalse();
assertThat(bean.allTestBeans()).containsExactly(bf.getBean("testBean1", TestBean.class),
bf.getBean("testBean2", TestBean.class), bf.getBean("testBean4", TestBean.class));
assertThat(bean.allTestBeansInOrder()).containsExactly(bf.getBean("testBean2", TestBean.class),
bf.getBean("testBean1", TestBean.class), bf.getBean("testBean4", TestBean.class));
Map<String, TestBean> typeMatches = BeanFactoryUtils.beansOfTypeIncludingAncestors(bf, TestBean.class);
assertThat(typeMatches.remove("testBean3")).isNotNull();
Map<String, TestBean> candidates = SimpleAutowireCandidateResolver.resolveAutowireCandidates(bf, TestBean.class);
assertThat(candidates).containsExactlyEntriesOf(candidates);
}
@Test
@@ -3304,9 +3370,25 @@ class AutowiredAnnotationBeanPostProcessorTests {
return this.testBean.stream().toList();
}
public List<TestBean> sortedTestBeans() {
public List<TestBean> streamTestBeansInOrder() {
return this.testBean.orderedStream().toList();
}
public List<TestBean> plainTestBeans() {
return this.testBean.stream(clazz -> !DerivedTestBean.class.isAssignableFrom(clazz)).toList();
}
public List<TestBean> plainTestBeansInOrder() {
return this.testBean.orderedStream(clazz -> !DerivedTestBean.class.isAssignableFrom(clazz)).toList();
}
public List<TestBean> allTestBeans() {
return this.testBean.stream(ObjectProvider.UNFILTERED).toList();
}
public List<TestBean> allTestBeansInOrder() {
return this.testBean.orderedStream(ObjectProvider.UNFILTERED).toList();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -49,6 +50,8 @@ public class IndexedTestBean {
private SortedMap sortedMap;
private IterableMap iterableMap;
private MyTestBeans myTestBeans;
@@ -73,6 +76,9 @@ public class IndexedTestBean {
TestBean tb6 = new TestBean("name6", 0);
TestBean tb7 = new TestBean("name7", 0);
TestBean tb8 = new TestBean("name8", 0);
TestBean tbA = new TestBean("nameA", 0);
TestBean tbB = new TestBean("nameB", 0);
TestBean tbC = new TestBean("nameC", 0);
TestBean tbX = new TestBean("nameX", 0);
TestBean tbY = new TestBean("nameY", 0);
TestBean tbZ = new TestBean("nameZ", 0);
@@ -88,6 +94,12 @@ public class IndexedTestBean {
this.map.put("key2", tb5);
this.map.put("key.3", tb5);
List list = new ArrayList();
list.add(tbA);
list.add(tbB);
this.iterableMap = new IterableMap<>();
this.iterableMap.put("key1", tbC);
this.iterableMap.put("key2", list);
list = new ArrayList();
list.add(tbX);
list.add(tbY);
this.map.put("key4", list);
@@ -152,6 +164,14 @@ public class IndexedTestBean {
this.sortedMap = sortedMap;
}
public IterableMap getIterableMap() {
return this.iterableMap;
}
public void setIterableMap(IterableMap iterableMap) {
this.iterableMap = iterableMap;
}
public MyTestBeans getMyTestBeans() {
return myTestBeans;
}
@@ -161,6 +181,15 @@ public class IndexedTestBean {
}
@SuppressWarnings("serial")
public static class IterableMap<K,V> extends LinkedHashMap<K,V> implements Iterable<V> {
@Override
public Iterator<V> iterator() {
return values().iterator();
}
}
public static class MyTestBeans implements Iterable<TestBean> {
private final Collection<TestBean> testBeans;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.context.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashSet;
@@ -33,6 +34,7 @@ import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotation.Adapt;
@@ -41,6 +43,7 @@ import org.springframework.core.type.AnnotationMetadata;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
/**
@@ -147,16 +150,26 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
Set<String> metaAnnotationTypes = this.metaAnnotationTypesCache.computeIfAbsent(annotationType,
key -> getMetaAnnotationTypes(mergedAnnotation));
if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) {
Object value = attributes.get("value");
Object value = attributes.get(MergedAnnotation.VALUE);
if (value instanceof String currentName && !currentName.isBlank()) {
if (conventionBasedStereotypeCheckCache.add(annotationType) &&
metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) {
logger.warn("""
Support for convention-based stereotype names is deprecated and will \
be removed in a future version of the framework. Please annotate the \
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
to declare an explicit alias for @Component's 'value' attribute."""
.formatted(annotationType));
if (hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) {
logger.warn("""
Although the 'value' attribute in @%s declares @AliasFor for an attribute \
other than @Component's 'value' attribute, the value is still used as the \
@Component name based on convention. As of Spring Framework 7.0, such a \
'value' attribute will no longer be used as the @Component name."""
.formatted(annotationType));
}
else {
logger.warn("""
Support for convention-based @Component names is deprecated and will \
be removed in a future version of the framework. Please annotate the \
'value' attribute in @%s with @AliasFor(annotation=Component.class) \
to declare an explicit alias for @Component's 'value' attribute."""
.formatted(annotationType));
}
}
if (beanName != null && !currentName.equals(beanName)) {
throw new IllegalStateException("Stereotype annotations suggest inconsistent " +
@@ -224,7 +237,7 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
annotationType.equals("jakarta.inject.Named") ||
annotationType.equals("javax.inject.Named");
return (isStereotype && attributes.containsKey("value"));
return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE));
}
/**
@@ -255,4 +268,14 @@ public class AnnotationBeanNameGenerator implements BeanNameGenerator {
return StringUtils.uncapitalizeAsProperty(shortClassName);
}
/**
* Determine if the supplied annotation type declares a {@code value()} attribute
* with an explicit alias configured via {@link AliasFor @AliasFor}.
* @since 6.2.3
*/
private static boolean hasExplicitlyAliasedValueAttribute(Class<? extends Annotation> annotationType) {
Method valueAttribute = ReflectionUtils.findMethod(annotationType, MergedAnnotation.VALUE);
return (valueAttribute != null && valueAttribute.isAnnotationPresent(AliasFor.class));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -565,9 +565,10 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC
}
/**
* Determine whether the given bean definition qualifies as candidate.
* <p>The default implementation checks whether the class is not an interface
* and not dependent on an enclosing class.
* Determine whether the given bean definition qualifies as a candidate component.
* <p>The default implementation checks whether the class is not dependent on an
* enclosing class as well as whether the class is either concrete (and therefore
* not an interface) or has {@link Lookup @Lookup} methods.
* <p>Can be overridden in subclasses.
* @param beanDefinition the bean definition to check
* @return whether the bean definition qualifies as a candidate component
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -109,8 +109,16 @@ class ConfigurationClassEnhancer {
}
return configClass;
}
try {
Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
// Use original ClassLoader if config class not locally loaded in overriding class loader
if (classLoader instanceof SmartClassLoader smartClassLoader &&
classLoader != configClass.getClassLoader()) {
classLoader = smartClassLoader.getOriginalClassLoader();
}
Enhancer enhancer = newEnhancer(configClass, classLoader);
boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader());
Class<?> enhancedClass = createClass(enhancer, classLoaderMismatch);
if (logger.isTraceEnabled()) {
logger.trace(String.format("Successfully enhanced %s; enhanced class name is: %s",
configClass.getName(), enhancedClass.getName()));
@@ -129,6 +137,9 @@ class ConfigurationClassEnhancer {
*/
private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
Enhancer enhancer = new Enhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
}
enhancer.setSuperclass(configSuperClass);
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
enhancer.setUseFactory(false);
@@ -152,8 +163,21 @@ class ConfigurationClassEnhancer {
* Uses enhancer to generate a subclass of superclass,
* ensuring that callbacks are registered for the new subclass.
*/
private Class<?> createClass(Enhancer enhancer) {
Class<?> subclass = enhancer.createClass();
private Class<?> createClass(Enhancer enhancer, boolean fallback) {
Class<?> subclass;
try {
subclass = enhancer.createClass();
}
catch (CodeGenerationException ex) {
if (!fallback) {
throw ex;
}
// Possibly a package-visible @Bean method declaration not accessible
// in the given ClassLoader -> retry with original ClassLoader
enhancer.setClassLoader(null);
subclass = enhancer.createClass();
}
// Registering callbacks statically (as opposed to thread-local)
// is critical for usage in an OSGi environment (SPR-5932)...
Enhancer.registerStaticCallbacks(subclass, CALLBACKS);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -317,8 +317,16 @@ final class QuartzCronField extends CronField {
private static TemporalAdjuster dayOfWeekInMonth(int ordinal, DayOfWeek dayOfWeek) {
TemporalAdjuster adjuster = TemporalAdjusters.dayOfWeekInMonth(ordinal, dayOfWeek);
return temporal -> {
Temporal result = adjuster.adjustInto(temporal);
return rollbackToMidnight(temporal, result);
// TemporalAdjusters can overflow to a different month
// in this case, attempt the same adjustment with the next/previous month
for (int i = 0; i < 12; i++) {
Temporal result = adjuster.adjustInto(temporal);
if (result.get(ChronoField.MONTH_OF_YEAR) == temporal.get(ChronoField.MONTH_OF_YEAR)) {
return rollbackToMidnight(temporal, result);
}
temporal = result;
}
return null;
};
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -142,6 +142,10 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
*/
protected static final Log logger = LogFactory.getLog(DataBinder.class);
/** Internal constant for constructor binding via "[]". */
private static final int NO_INDEX = -1;
@Nullable
private Object target;
@@ -964,7 +968,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
value = createMap(paramPath, paramType, resolvableType, valueResolver);
}
else if (paramType.isArray()) {
value = createArray(paramPath, resolvableType, valueResolver);
value = createArray(paramPath, paramType, resolvableType, valueResolver);
}
}
@@ -981,11 +985,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
}
catch (TypeMismatchException ex) {
ex.initPropertyName(paramPath);
args[i] = null;
failedParamNames.add(paramPath);
getBindingResult().recordFieldValue(paramPath, paramType, value);
getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult());
handleTypeMismatchException(ex, paramPath, paramType, value);
}
}
}
@@ -1048,9 +1050,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
return false;
}
@SuppressWarnings("unchecked")
@Nullable
private <V> List<V> createList(
private List<?> createList(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
ResolvableType elementType = type.getNested(2);
@@ -1058,18 +1059,23 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
if (indexes == null) {
return null;
}
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
int lastIndex = Math.max(indexes.last(), 0);
int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1 : 0);
List<?> list = (List<?>) CollectionFactory.createCollection(paramType, size);
for (int i = 0; i < size; i++) {
list.add(null);
}
for (int index : indexes) {
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]";
list.set(Math.max(index, 0),
createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
}
return list;
}
@SuppressWarnings("unchecked")
@Nullable
private <V> Map<String, V> createMap(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
@@ -1080,34 +1086,44 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
if (!name.startsWith(paramPath + "[")) {
continue;
}
int startIdx = paramPath.length() + 1;
int endIdx = name.indexOf(']', startIdx);
String nestedPath = ((name.length() > endIdx + 1) ? name.substring(0, endIdx + 2) : "");
boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\'');
String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx));
if (map == null) {
map = CollectionFactory.createMap(paramType, 16);
}
if (!map.containsKey(key)) {
map.put(key, (V) createObject(elementType, nestedPath, valueResolver));
}
String indexedPath = name.substring(0, endIdx + 1);
map.put(key, createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver));
}
return map;
}
@SuppressWarnings("unchecked")
@Nullable
private <V> V[] createArray(String paramPath, ResolvableType type, ValueResolver valueResolver) {
private <V> V[] createArray(
String paramPath, Class<?> paramType, ResolvableType type, ValueResolver valueResolver) {
ResolvableType elementType = type.getNested(2);
SortedSet<Integer> indexes = getIndexes(paramPath, valueResolver);
if (indexes == null) {
return null;
}
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1: 0);
int lastIndex = Math.max(indexes.last(), 0);
int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1: 0);
V[] array = (V[]) Array.newInstance(elementType.resolve(), size);
for (int index : indexes) {
array[index] = (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver);
String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]";
array[Math.max(index, 0)] =
createIndexedValue(paramPath, paramType, elementType, indexedPath, valueResolver);
}
return array;
}
@@ -1116,9 +1132,18 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
SortedSet<Integer> indexes = null;
for (String name : valueResolver.getNames()) {
if (name.startsWith(paramPath + "[")) {
int endIndex = name.indexOf(']', paramPath.length() + 1);
String rawIndex = name.substring(paramPath.length() + 1, endIndex);
int index = Integer.parseInt(rawIndex);
int index;
if (paramPath.length() + 2 == name.length()) {
if (!name.endsWith("[]")) {
continue;
}
index = NO_INDEX;
}
else {
int endIndex = name.indexOf(']', paramPath.length() + 2);
String indexValue = name.substring(paramPath.length() + 1, endIndex);
index = Integer.parseInt(indexValue);
}
indexes = (indexes != null ? indexes : new TreeSet<>());
indexes.add(index);
}
@@ -1126,6 +1151,50 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
return indexes;
}
@SuppressWarnings("unchecked")
@Nullable
private <V> V createIndexedValue(
String paramPath, Class<?> containerType, ResolvableType elementType,
String indexedPath, ValueResolver valueResolver) {
Object value = null;
Class<?> elementClass = elementType.resolve(Object.class);
if (List.class.isAssignableFrom(elementClass)) {
value = createList(indexedPath, elementClass, elementType, valueResolver);
}
else if (Map.class.isAssignableFrom(elementClass)) {
value = createMap(indexedPath, elementClass, elementType, valueResolver);
}
else if (elementClass.isArray()) {
value = createArray(indexedPath, elementClass, elementType, valueResolver);
}
else {
Object rawValue = valueResolver.resolveValue(indexedPath, elementClass);
if (rawValue != null) {
try {
value = convertIfNecessary(rawValue, elementClass);
}
catch (TypeMismatchException ex) {
handleTypeMismatchException(ex, paramPath, containerType, rawValue);
}
}
else {
value = createObject(elementType, indexedPath + ".", valueResolver);
}
}
return (V) value;
}
private void handleTypeMismatchException(
TypeMismatchException ex, String paramPath, Class<?> paramType, @Nullable Object value) {
ex.initPropertyName(paramPath);
getBindingResult().recordFieldValue(paramPath, paramType, value);
getBindingErrorProcessor().processPropertyAccessException(ex, getBindingResult());
}
private void validateConstructorArgument(
Class<?> constructorClass, String nestedPath, String name, @Nullable Object value) {
@@ -1412,6 +1481,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
/**
* Return the names of all property values.
* <p>Useful for proactive checks whether there are property values nested
* further below the path for a constructor arg. If not then the
* constructor arg can be considered missing and not to be instantiated.
* @since 6.1.2
*/
Set<String> getNames();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -422,6 +422,7 @@ class CglibProxyTests extends AbstractAopProxyTests {
}
@Test // SPR-13328
@SuppressWarnings("unchecked")
void varargsWithEnumArray() {
ProxyFactory proxyFactory = new ProxyFactory(new MyBean());
MyBean proxy = (MyBean) proxyFactory.getProxy();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -141,6 +141,7 @@ class JdkDynamicProxyTests extends AbstractAopProxyTests {
}
@Test // SPR-13328
@SuppressWarnings("unchecked")
void varargsWithEnumArray() {
ProxyFactory proxyFactory = new ProxyFactory(new VarargTestBean());
VarargTestInterface proxy = (VarargTestInterface) proxyFactory.getProxy();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -168,6 +168,25 @@ class AnnotationBeanNameGeneratorTests {
assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice");
}
@Test // gh-34317
void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() {
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName".
assertGeneratedName(StereotypeWithoutExplicitName.class, "enigma");
}
@Test // gh-34317
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() {
// As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name.
// As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName".
assertGeneratedName(StereotypeWithGeneratedName.class, "enigma");
}
@Test // gh-34317
void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentName() {
assertGeneratedName(StereotypeWithExplicitName.class, "explicitName");
}
private void assertGeneratedName(Class<?> clazz, String expectedName) {
BeanDefinition bd = annotatedBeanDef(clazz);
@@ -210,7 +229,7 @@ class AnnotationBeanNameGeneratorTests {
@Retention(RetentionPolicy.RUNTIME)
@Component
@interface ConventionBasedComponent1 {
// This intentionally convention-based. Please do not add @AliasFor.
// This is intentionally convention-based. Please do not add @AliasFor.
// See gh-31093.
String value() default "";
}
@@ -218,7 +237,7 @@ class AnnotationBeanNameGeneratorTests {
@Retention(RetentionPolicy.RUNTIME)
@Component
@interface ConventionBasedComponent2 {
// This intentionally convention-based. Please do not add @AliasFor.
// This is intentionally convention-based. Please do not add @AliasFor.
// See gh-31093.
String value() default "";
}
@@ -260,7 +279,7 @@ class AnnotationBeanNameGeneratorTests {
@Target(ElementType.TYPE)
@Controller
@interface TestRestController {
// This intentionally convention-based. Please do not add @AliasFor.
// This is intentionally convention-based. Please do not add @AliasFor.
// See gh-31093.
String value() default "";
}
@@ -319,7 +338,6 @@ class AnnotationBeanNameGeneratorTests {
String[] basePackages() default {};
}
@TestControllerAdvice(basePackages = "com.example", name = "myControllerAdvice")
static class ControllerAdviceClass {
}
@@ -328,4 +346,56 @@ class AnnotationBeanNameGeneratorTests {
static class RestControllerAdviceClass {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
@interface MetaAnnotationWithStringAttribute {
String attribute() default "";
}
/**
* Custom stereotype annotation which has a {@code String value} attribute that
* is explicitly declared as an alias for an attribute in a meta-annotation
* other than {@link Component @Component}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
@MetaAnnotationWithStringAttribute
@interface MyStereotype {
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
String value() default "";
}
@MyStereotype("enigma")
static class StereotypeWithoutExplicitName {
}
/**
* Custom stereotype annotation which is identical to {@link MyStereotype @MyStereotype}
* except that it has a {@link #name} attribute that is an explicit alias for
* {@link Component#value}.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Component
@MetaAnnotationWithStringAttribute
@interface MyNamedStereotype {
@AliasFor(annotation = MetaAnnotationWithStringAttribute.class, attribute = "attribute")
String value() default "";
@AliasFor(annotation = Component.class, attribute = "value")
String name() default "";
}
@MyNamedStereotype(value = "enigma", name ="explicitName")
static class StereotypeWithExplicitName {
}
@MyNamedStereotype(value = "enigma")
static class StereotypeWithGeneratedName {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.context.annotation;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.testfixture.EnabledForTestGroups;
@@ -33,6 +34,28 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
*/
class BackgroundBootstrapTests {
@Test
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithUnmanagedThread() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class);
ctx.getBean("testBean1", TestBean.class);
ctx.getBean("testBean2", TestBean.class);
ctx.close();
}
@Test
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithUnmanagedThreads() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class);
ctx.getBean("testBean1", TestBean.class);
ctx.getBean("testBean2", TestBean.class);
ctx.getBean("testBean3", TestBean.class);
ctx.getBean("testBean4", TestBean.class);
ctx.close();
}
@Test
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
@@ -41,11 +64,81 @@ class BackgroundBootstrapTests {
ctx.getBean("testBean1", TestBean.class);
ctx.getBean("testBean2", TestBean.class);
ctx.getBean("testBean3", TestBean.class);
ctx.getBean("testBean4", TestBean.class);
ctx.close();
}
@Configuration
@Configuration(proxyBeanMethods = false)
static class UnmanagedThreadBeanConfig {
@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
new Thread(testBean2::getObject).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
return new TestBean();
}
@Bean
public TestBean testBean2() {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
return new TestBean();
}
}
@Configuration(proxyBeanMethods = false)
static class UnmanagedThreadsBeanConfig {
@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean3, ObjectProvider<TestBean> testBean4) {
new Thread(testBean3::getObject).start();
new Thread(testBean4::getObject).start();
new Thread(testBean3::getObject).start();
new Thread(testBean4::getObject).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
return new TestBean();
}
@Bean
public TestBean testBean2(TestBean testBean4) {
return new TestBean(testBean4);
}
@Bean
public TestBean testBean3(TestBean testBean4) {
return new TestBean(testBean4);
}
@Bean
public TestBean testBean4() {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
throw new RuntimeException(ex);
}
return new TestBean();
}
}
@Configuration(proxyBeanMethods = false)
static class CustomExecutorBeanConfig {
@Bean
@@ -58,7 +151,7 @@ class BackgroundBootstrapTests {
}
@Bean(bootstrap = BACKGROUND) @DependsOn("testBean3")
public TestBean testBean1(TestBean testBean3) throws InterruptedException{
public TestBean testBean1(TestBean testBean3) throws InterruptedException {
Thread.sleep(3000);
return new TestBean();
}
@@ -75,8 +168,8 @@ class BackgroundBootstrapTests {
}
@Bean
public String dependent(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) {
return "";
public TestBean testBean4(@Lazy TestBean testBean1, @Lazy TestBean testBean2, @Lazy TestBean testBean3) {
return new TestBean();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,11 +18,16 @@ package org.springframework.context.annotation;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.ProtectionDomain;
import java.security.SecureClassLoader;
import org.junit.jupiter.api.Test;
import org.springframework.core.OverridingClassLoader;
import org.springframework.core.SmartClassLoader;
import org.springframework.lang.Nullable;
import org.springframework.util.StreamUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -36,19 +41,108 @@ class ConfigurationClassEnhancerTests {
@Test
void enhanceReloadedClass() throws Exception {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader parentClassLoader = getClass().getClassLoader();
CustomClassLoader classLoader = new CustomClassLoader(parentClassLoader);
ClassLoader classLoader = new CustomSmartClassLoader(parentClassLoader);
Class<?> myClass = parentClassLoader.loadClass(MyConfig.class.getName());
configurationClassEnhancer.enhance(myClass, parentClassLoader);
Class<?> myReloadedClass = classLoader.loadClass(MyConfig.class.getName());
Class<?> enhancedReloadedClass = configurationClassEnhancer.enhance(myReloadedClass, classLoader);
assertThat(enhancedReloadedClass.getClassLoader()).isEqualTo(classLoader);
Class<?> enhancedClass = configurationClassEnhancer.enhance(myClass, parentClassLoader);
assertThat(myClass).isAssignableFrom(enhancedClass);
myClass = classLoader.loadClass(MyConfig.class.getName());
enhancedClass = configurationClassEnhancer.enhance(myClass, classLoader);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
assertThat(myClass).isAssignableFrom(enhancedClass);
}
@Test
void withPublicClass() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicClass() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicClass.class, classLoader);
assertThat(MyConfigWithNonPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
void withNonPublicMethod() {
ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer();
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new CustomSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Configuration
static class MyConfig {
@Bean
String myBean() {
return "bean";
}
}
@Configuration
public static class MyConfigWithPublicClass {
@Bean
public String myBean() {
return "bean";
@@ -56,9 +150,29 @@ class ConfigurationClassEnhancerTests {
}
static class CustomClassLoader extends SecureClassLoader implements SmartClassLoader {
@Configuration
static class MyConfigWithNonPublicClass {
CustomClassLoader(ClassLoader parent) {
@Bean
public String myBean() {
return "bean";
}
}
@Configuration
public static class MyConfigWithNonPublicMethod {
@Bean
String myBean() {
return "bean";
}
}
static class CustomSmartClassLoader extends SecureClassLoader implements SmartClassLoader {
CustomSmartClassLoader(ClassLoader parent) {
super(parent);
}
@@ -82,6 +196,29 @@ class ConfigurationClassEnhancerTests {
public boolean isClassReloadable(Class<?> clazz) {
return clazz.getName().contains("MyConfig");
}
@Override
public ClassLoader getOriginalClassLoader() {
return getParent();
}
@Override
public Class<?> publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) {
return defineClass(name, b, 0, b.length, protectionDomain);
}
}
static class BasicSmartClassLoader extends SecureClassLoader implements SmartClassLoader {
BasicSmartClassLoader(ClassLoader parent) {
super(parent);
}
@Override
public Class<?> publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) {
return defineClass(name, b, 0, b.length, protectionDomain);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -125,7 +125,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings("removal")
@SuppressWarnings({ "removal", "deprecation" })
void submitListenableRunnable() {
TestTask task = new TestTask(this.testName, 1);
// Act
@@ -156,7 +156,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings("removal")
@SuppressWarnings({ "removal", "deprecation" })
void submitFailingListenableRunnable() {
TestTask task = new TestTask(this.testName, 0);
org.springframework.util.concurrent.ListenableFuture<?> future = executor.submitListenable(task);
@@ -185,7 +185,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings("removal")
@SuppressWarnings({ "removal", "deprecation" })
void submitListenableRunnableWithGetAfterShutdown() throws Exception {
org.springframework.util.concurrent.ListenableFuture<?> future1 = executor.submitListenable(new TestTask(this.testName, -1));
org.springframework.util.concurrent.ListenableFuture<?> future2 = executor.submitListenable(new TestTask(this.testName, -1));
@@ -252,7 +252,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings("removal")
@SuppressWarnings({ "removal", "deprecation" })
void submitListenableCallable() {
TestCallable task = new TestCallable(this.testName, 1);
// Act
@@ -267,7 +267,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings("removal")
@SuppressWarnings({ "removal", "deprecation" })
void submitFailingListenableCallable() {
TestCallable task = new TestCallable(this.testName, 0);
// Act
@@ -283,7 +283,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings("removal")
@SuppressWarnings({ "removal", "deprecation" })
void submitListenableCallableWithGetAfterShutdown() throws Exception {
org.springframework.util.concurrent.ListenableFuture<?> future1 = executor.submitListenable(new TestCallable(this.testName, -1));
org.springframework.util.concurrent.ListenableFuture<?> future2 = executor.submitListenable(new TestCallable(this.testName, -1));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -39,6 +39,7 @@ import static java.time.temporal.TemporalAdjusters.next;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CronExpression}.
* @author Arjen Poutsma
*/
class CronExpressionTests {
@@ -1092,6 +1093,20 @@ class CronExpressionTests {
assertThat(actual.getDayOfWeek()).isEqualTo(FRIDAY);
}
@Test
void quartz5thMondayOfTheMonthDayName() {
CronExpression expression = CronExpression.parse("0 0 0 ? * MON#5");
LocalDateTime last = LocalDateTime.of(2025, 1, 1, 0, 0, 0);
// first occurrence of 5 mondays in a month from last
LocalDateTime expected = LocalDateTime.of(2025, 3, 31, 0, 0, 0);
LocalDateTime actual = expression.next(last);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(expected);
assertThat(actual.getDayOfWeek()).isEqualTo(MONDAY);
}
@Test
void quartzFifthWednesdayOfTheMonth() {
CronExpression expression = CronExpression.parse("0 0 0 ? * 3#5");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -103,17 +103,17 @@ class DataBinderConstructTests {
}
@Test
void listBinding() {
void dataClassWithListBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
"dataClassList[2].param1", "value3", "dataClassList[2].param2", "true"));
DataBinder binder = initDataBinder(ListDataClass.class);
DataBinder binder = initDataBinder(DataClassListRecord.class);
binder.construct(valueResolver);
ListDataClass dataClass = getTarget(binder);
List<DataClass> list = dataClass.dataClassList();
DataClassListRecord target = getTarget(binder);
List<DataClass> list = target.dataClassList();
assertThat(list).hasSize(3);
assertThat(list.get(0).param1()).isEqualTo("value1");
@@ -122,17 +122,17 @@ class DataBinderConstructTests {
}
@Test // gh-34145
void listBindingWithNonconsecutiveIndices() {
void dataClassWithListBindingWithNonconsecutiveIndices() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
"dataClassList[3].param1", "value3", "dataClassList[3].param2", "true"));
DataBinder binder = initDataBinder(ListDataClass.class);
DataBinder binder = initDataBinder(DataClassListRecord.class);
binder.construct(valueResolver);
ListDataClass dataClass = getTarget(binder);
List<DataClass> list = dataClass.dataClassList();
DataClassListRecord target = getTarget(binder);
List<DataClass> list = target.dataClassList();
assertThat(list.get(0).param1()).isEqualTo("value1");
assertThat(list.get(1).param1()).isEqualTo("value2");
@@ -140,17 +140,17 @@ class DataBinderConstructTests {
}
@Test
void mapBinding() {
void dataClassWithMapBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassMap[a].param1", "value1", "dataClassMap[a].param2", "true",
"dataClassMap[b].param1", "value2", "dataClassMap[b].param2", "true",
"dataClassMap['c'].param1", "value3", "dataClassMap['c'].param2", "true"));
DataBinder binder = initDataBinder(MapDataClass.class);
DataBinder binder = initDataBinder(DataClassMapRecord.class);
binder.construct(valueResolver);
MapDataClass dataClass = getTarget(binder);
Map<String, DataClass> map = dataClass.dataClassMap();
DataClassMapRecord target = getTarget(binder);
Map<String, DataClass> map = target.dataClassMap();
assertThat(map).hasSize(3);
assertThat(map.get("a").param1()).isEqualTo("value1");
@@ -159,17 +159,17 @@ class DataBinderConstructTests {
}
@Test
void arrayBinding() {
void dataClassWithArrayBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassArray[0].param1", "value1", "dataClassArray[0].param2", "true",
"dataClassArray[1].param1", "value2", "dataClassArray[1].param2", "true",
"dataClassArray[2].param1", "value3", "dataClassArray[2].param2", "true"));
DataBinder binder = initDataBinder(ArrayDataClass.class);
DataBinder binder = initDataBinder(DataClassArrayRecord.class);
binder.construct(valueResolver);
ArrayDataClass dataClass = getTarget(binder);
DataClass[] array = dataClass.dataClassArray();
DataClassArrayRecord target = getTarget(binder);
DataClass[] array = target.dataClassArray();
assertThat(array).hasSize(3);
assertThat(array[0].param1()).isEqualTo("value1");
@@ -177,6 +177,79 @@ class DataBinderConstructTests {
assertThat(array[2].param1()).isEqualTo("value3");
}
@Test
void simpleListBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerList[0]", "1", "integerList[1]", "2"));
DataBinder binder = initDataBinder(IntegerListRecord.class);
binder.construct(valueResolver);
IntegerListRecord target = getTarget(binder);
assertThat(target.integerList()).containsExactly(1, 2);
}
@Test
void simpleListBindingEmptyBrackets() {
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerList[]", "1"));
DataBinder binder = initDataBinder(IntegerListRecord.class);
binder.construct(valueResolver);
IntegerListRecord target = getTarget(binder);
assertThat(target.integerList()).containsExactly(1);
}
@Test
void simpleMapBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerMap[a]", "1", "integerMap[b]", "2"));
DataBinder binder = initDataBinder(IntegerMapRecord.class);
binder.construct(valueResolver);
IntegerMapRecord target = getTarget(binder);
assertThat(target.integerMap()).hasSize(2).containsEntry("a", 1).containsEntry("b", 2);
}
@Test
void simpleArrayBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of("integerArray[0]", "1", "integerArray[1]", "2"));
DataBinder binder = initDataBinder(IntegerArrayRecord.class);
binder.construct(valueResolver);
IntegerArrayRecord target = getTarget(binder);
assertThat(target.integerArray()).containsExactly(1, 2);
}
@Test
void nestedListWithinMap() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"integerListMap[a][0]", "1", "integerListMap[a][1]", "2",
"integerListMap[b][0]", "3", "integerListMap[b][1]", "4"));
DataBinder binder = initDataBinder(IntegerListMapRecord.class);
binder.construct(valueResolver);
IntegerListMapRecord target = getTarget(binder);
assertThat(target.integerListMap().get("a")).containsExactly(1, 2);
assertThat(target.integerListMap().get("b")).containsExactly(3, 4);
}
@Test
void nestedMapWithinList() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"integerMapList[0][a]", "1", "integerMapList[0][b]", "2",
"integerMapList[1][a]", "3", "integerMapList[1][b]", "4"));
DataBinder binder = initDataBinder(IntegerMapListRecord.class);
binder.construct(valueResolver);
IntegerMapListRecord target = getTarget(binder);
assertThat(target.integerMapList().get(0)).containsOnly(Map.entry("a", 1), Map.entry("b", 2));
assertThat(target.integerMapList().get(1)).containsOnly(Map.entry("a", 3), Map.entry("b", 4));
}
@SuppressWarnings("SameParameterValue")
private static DataBinder initDataBinder(Class<?> targetType) {
DataBinder binder = new DataBinder(null);
@@ -225,7 +298,7 @@ class DataBinderConstructTests {
}
private static class NestedDataClass {
static class NestedDataClass {
private final String param1;
@@ -248,15 +321,35 @@ class DataBinderConstructTests {
}
private record ListDataClass(List<DataClass> dataClassList) {
private record DataClassListRecord(List<DataClass> dataClassList) {
}
private record MapDataClass(Map<String, DataClass> dataClassMap) {
private record DataClassMapRecord(Map<String, DataClass> dataClassMap) {
}
private record ArrayDataClass(DataClass[] dataClassArray) {
private record DataClassArrayRecord(DataClass[] dataClassArray) {
}
private record IntegerListRecord(List<Integer> integerList) {
}
private record IntegerMapRecord(Map<String, Integer> integerMap) {
}
private record IntegerArrayRecord(Integer[] integerArray) {
}
private record IntegerMapListRecord(List<Map<String, Integer>> integerMapList) {
}
private record IntegerListMapRecord(Map<String, List<Integer>> integerListMap) {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,15 +32,19 @@ import java.lang.reflect.Method;
public enum MemberCategory {
/**
* A category that represents public {@linkplain Field fields}.
* @see Class#getFields()
* A category that represents reflective field access on public {@linkplain Field fields}.
* @see Field#get(Object)
* @see Field#set(Object, Object)
*/
PUBLIC_FIELDS,
/**
* A category that represents {@linkplain Class#getDeclaredFields() declared
* fields}: all fields defined by the class but not inherited fields.
* A category that represents reflective field access on
* {@linkplain Class#getDeclaredFields() declared fields}: all fields defined by the
* class but not inherited fields.
* @see Class#getDeclaredFields()
* @see Field#get(Object)
* @see Field#set(Object, Object)
*/
DECLARED_FIELDS,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -199,8 +199,8 @@ public class ReflectionHints {
}
/**
* Register the need for reflection on the specified {@link Field}.
* @param field the field that requires reflection
* Register the need for reflective field access on the specified {@link Field}.
* @param field the field that requires reflective access
* @return {@code this}, to facilitate method chaining
*/
public ReflectionHints registerField(Field field) {
@@ -182,9 +182,11 @@ public final class TypeHint implements ConditionalHint {
}
/**
* Register the need for reflection on the field with the specified name.
* Register the need for reflective access on the field with the specified name.
* @param name the name of the field
* @return {@code this}, to facilitate method chaining
* @see java.lang.reflect.Field#get(Object)
* @see java.lang.reflect.Field#set(Object, Object)
*/
public Builder withField(String name) {
this.fields.add(name);
@@ -183,9 +183,7 @@ public class ReflectionHintsPredicates {
}
/**
* Return a predicate that checks whether a reflection hint is registered for the given field.
* By default, unsafe or write access is not considered.
* <p>The returned type exposes additional methods that refine the predicate behavior.
* Return a predicate that checks whether a reflective field access hint is registered for the given field.
* @param field the field
* @return the {@link RuntimeHints} predicate
*/
@@ -360,7 +360,7 @@ abstract public class AbstractClassGenerator<T> implements ClassGenerator {
// SPRING PATCH BEGIN
if (inNativeImage) {
throw new UnsupportedOperationException("CGLIB runtime enhancement not supported on native image. " +
"Make sure to include a pre-generated class on the classpath instead: " + getClassName());
"Make sure to enable Spring AOT processing to pre-generate '" + getClassName() + "' at build time.");
}
// SPRING PATCH END
byte[] b = strategy.generate(this);
@@ -169,8 +169,8 @@ public final class GenericTypeResolver {
else if (genericType instanceof ParameterizedType parameterizedType) {
ResolvableType resolvedType = ResolvableType.forType(genericType);
if (resolvedType.hasUnresolvableGenerics()) {
ResolvableType[] generics = new ResolvableType[parameterizedType.getActualTypeArguments().length];
Type[] typeArguments = parameterizedType.getActualTypeArguments();
ResolvableType[] generics = new ResolvableType[typeArguments.length];
ResolvableType contextType = ResolvableType.forClass(contextClass);
for (int i = 0; i < typeArguments.length; i++) {
Type typeArgument = typeArguments[i];
@@ -180,7 +180,7 @@ public final class GenericTypeResolver {
generics[i] = resolvedTypeArgument;
}
else {
generics[i] = ResolvableType.forType(typeArgument).resolveType();
generics[i] = ResolvableType.forType(typeArgument);
}
}
else if (typeArgument instanceof ParameterizedType) {
@@ -209,6 +209,9 @@ public final class GenericTypeResolver {
}
resolvedType = variableResolver.resolveVariable(typeVariable);
if (resolvedType != null) {
while (resolvedType.getType() instanceof TypeVariable<?>) {
resolvedType = resolvedType.resolveType();
}
return resolvedType;
}
}
@@ -226,7 +229,7 @@ public final class GenericTypeResolver {
return resolvedType;
}
}
return ResolvableType.NONE;
return ResolvableType.forVariableBounds(typeVariable);
}
/**
@@ -329,9 +329,6 @@ public class ResolvableType implements Serializable {
other.getComponentType(), true, matchedBefore, upUntilUnresolvable));
}
// We're checking nested generic variables now...
boolean exactMatch = (strict && matchedBefore != null);
// Deal with wildcard bounds
WildcardBounds ourBounds = WildcardBounds.get(this);
WildcardBounds otherBounds = WildcardBounds.get(other);
@@ -345,8 +342,9 @@ public class ResolvableType implements Serializable {
else if (upUntilUnresolvable) {
return otherBounds.isAssignableFrom(this, matchedBefore);
}
else if (!exactMatch) {
return otherBounds.isAssignableTo(this, matchedBefore);
else if (!strict) {
return (matchedBefore != null ? otherBounds.equalsType(this) :
otherBounds.isAssignableTo(this, matchedBefore));
}
else {
return false;
@@ -359,6 +357,7 @@ public class ResolvableType implements Serializable {
}
// Main assignability check about to follow
boolean exactMatch = (strict && matchedBefore != null);
boolean checkGenerics = true;
Class<?> ourResolved = null;
if (this.type instanceof TypeVariable<?> variable) {
@@ -954,14 +953,6 @@ public class ResolvableType implements Serializable {
return NONE;
}
@Nullable
private Type resolveBounds(Type[] bounds) {
if (bounds.length == 0 || bounds[0] == Object.class) {
return null;
}
return bounds[0];
}
@Nullable
private ResolvableType resolveVariable(TypeVariable<?> variable) {
if (this.type instanceof TypeVariable) {
@@ -973,10 +964,10 @@ public class ResolvableType implements Serializable {
return null;
}
TypeVariable<?>[] variables = resolved.getTypeParameters();
Type[] typeArguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < variables.length; i++) {
if (ObjectUtils.nullSafeEquals(variables[i].getName(), variable.getName())) {
Type actualType = parameterizedType.getActualTypeArguments()[i];
return forType(actualType, this.variableResolver);
return forType(typeArguments[i], this.variableResolver);
}
}
Type ownerType = parameterizedType.getOwnerType();
@@ -1464,6 +1455,24 @@ public class ResolvableType implements Serializable {
return new ResolvableType(arrayType, componentType, null, null);
}
/**
* Return a {@code ResolvableType} for the bounds of the specified {@link TypeVariable}.
* @param typeVariable the type variable
* @return a {@code ResolvableType} for the specified bounds
* @since 6.2.3
*/
static ResolvableType forVariableBounds(TypeVariable<?> typeVariable) {
return forType(resolveBounds(typeVariable.getBounds()));
}
@Nullable
private static Type resolveBounds(Type[] bounds) {
if (bounds.length == 0 || bounds[0] == Object.class) {
return null;
}
return bounds[0];
}
/**
* Return a {@code ResolvableType} for the specified {@link Type}.
* <p>Note: The resulting {@code ResolvableType} instance may not be {@link Serializable}.
@@ -1492,7 +1501,6 @@ public class ResolvableType implements Serializable {
return forType(type, variableResolver);
}
/**
* Return a {@code ResolvableType} for the specified {@link ParameterizedTypeReference}.
* <p>Note: The resulting {@code ResolvableType} instance may not be {@link Serializable}.
@@ -1782,6 +1790,21 @@ public class ResolvableType implements Serializable {
}
}
/**
* Return {@code true} if these bounds are equal to the specified type.
* @param type the type to test against
* @return {@code true} if these bounds are equal to the type
* @since 6.2.3
*/
public boolean equalsType(ResolvableType type) {
for (ResolvableType bound : this.bounds) {
if (!type.equalsType(bound)) {
return false;
}
}
return true;
}
/**
* Return the underlying bounds.
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -123,8 +123,8 @@ public final class SpringProperties {
/**
* Retrieve the flag for the given property key.
* @param key the property key
* @return {@code true} if the property is set to "true",
* {@code} false otherwise
* @return {@code true} if the property is set to the string "true"
* (ignoring case), {@code} false otherwise
*/
public static boolean getFlag(String key) {
return Boolean.parseBoolean(getProperty(key));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -214,8 +214,8 @@ public class AnnotatedMethod {
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other != null && getClass() == other.getClass() &&
this.method.equals(((AnnotatedMethod) other).method)));
return (this == other || (other instanceof AnnotatedMethod otherHandlerMethod &&
this.method.equals(otherHandlerMethod.method)));
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Stream;
import org.springframework.core.MethodParameter;
@@ -70,7 +71,10 @@ public class TypeDescriptor implements Serializable {
private final ResolvableType resolvableType;
private final AnnotatedElementAdapter annotatedElement;
private final AnnotatedElementSupplier annotatedElementSupplier;
@Nullable
private volatile AnnotatedElementAdapter annotatedElement;
/**
@@ -82,7 +86,7 @@ public class TypeDescriptor implements Serializable {
public TypeDescriptor(MethodParameter methodParameter) {
this.resolvableType = ResolvableType.forMethodParameter(methodParameter);
this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType());
this.annotatedElement = AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ?
this.annotatedElementSupplier = () -> AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ?
methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations());
}
@@ -94,7 +98,7 @@ public class TypeDescriptor implements Serializable {
public TypeDescriptor(Field field) {
this.resolvableType = ResolvableType.forField(field);
this.type = this.resolvableType.resolve(field.getType());
this.annotatedElement = AnnotatedElementAdapter.from(field.getAnnotations());
this.annotatedElementSupplier = () -> AnnotatedElementAdapter.from(field.getAnnotations());
}
/**
@@ -107,7 +111,7 @@ public class TypeDescriptor implements Serializable {
Assert.notNull(property, "Property must not be null");
this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter());
this.type = this.resolvableType.resolve(property.getType());
this.annotatedElement = AnnotatedElementAdapter.from(property.getAnnotations());
this.annotatedElementSupplier = () -> AnnotatedElementAdapter.from(property.getAnnotations());
}
/**
@@ -123,7 +127,7 @@ public class TypeDescriptor implements Serializable {
public TypeDescriptor(ResolvableType resolvableType, @Nullable Class<?> type, @Nullable Annotation[] annotations) {
this.resolvableType = resolvableType;
this.type = (type != null ? type : resolvableType.toClass());
this.annotatedElement = AnnotatedElementAdapter.from(annotations);
this.annotatedElementSupplier = () -> AnnotatedElementAdapter.from(annotations);
}
@@ -250,12 +254,21 @@ public class TypeDescriptor implements Serializable {
return getType().isPrimitive();
}
private AnnotatedElementAdapter getAnnotatedElement() {
AnnotatedElementAdapter annotatedElement = this.annotatedElement;
if (annotatedElement == null) {
annotatedElement = this.annotatedElementSupplier.get();
this.annotatedElement = annotatedElement;
}
return annotatedElement;
}
/**
* Return the annotations associated with this type descriptor, if any.
* @return the annotations, or an empty array if none
*/
public Annotation[] getAnnotations() {
return this.annotatedElement.getAnnotations();
return getAnnotatedElement().getAnnotations();
}
/**
@@ -266,12 +279,13 @@ public class TypeDescriptor implements Serializable {
* @return {@code true} if the annotation is present
*/
public boolean hasAnnotation(Class<? extends Annotation> annotationType) {
if (this.annotatedElement.isEmpty()) {
AnnotatedElementAdapter annotatedElement = getAnnotatedElement();
if (annotatedElement.isEmpty()) {
// Shortcut: AnnotatedElementUtils would have to expect AnnotatedElement.getAnnotations()
// to return a copy of the array, whereas we can do it more efficiently here.
return false;
}
return AnnotatedElementUtils.isAnnotated(this.annotatedElement, annotationType);
return AnnotatedElementUtils.isAnnotated(annotatedElement, annotationType);
}
/**
@@ -282,12 +296,13 @@ public class TypeDescriptor implements Serializable {
*/
@Nullable
public <T extends Annotation> T getAnnotation(Class<T> annotationType) {
if (this.annotatedElement.isEmpty()) {
AnnotatedElementAdapter annotatedElement = getAnnotatedElement();
if (annotatedElement.isEmpty()) {
// Shortcut: AnnotatedElementUtils would have to expect AnnotatedElement.getAnnotations()
// to return a copy of the array, whereas we can do it more efficiently here.
return null;
}
return AnnotatedElementUtils.getMergedAnnotation(this.annotatedElement, annotationType);
return AnnotatedElementUtils.getMergedAnnotation(annotatedElement, annotationType);
}
/**
@@ -808,4 +823,8 @@ public class TypeDescriptor implements Serializable {
}
}
private interface AnnotatedElementSupplier extends Supplier<AnnotatedElementAdapter>, Serializable {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -341,8 +341,7 @@ public class GenericConversionService implements ConfigurableConversionService {
}
// Full check for complex generic type match required?
ResolvableType rt = targetType.getResolvableType();
if (!(rt.getType() instanceof Class) && !rt.isAssignableFrom(this.targetType) &&
!this.targetType.hasUnresolvableGenerics()) {
if (!(rt.getType() instanceof Class) && !rt.isAssignableFromResolvedPart(this.targetType)) {
return false;
}
return !(this.converter instanceof ConditionalConverter conditionalConverter) ||
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,8 +25,7 @@ import org.springframework.util.StringUtils;
/**
* Abstract base class for {@link PropertySource} implementations backed by command line
* arguments. The parameterized type {@code T} represents the underlying source of command
* line options. For instance, {@link SimpleCommandLinePropertySource} uses a String
* array.
* line options.
*
* <h3>Purpose and General Usage</h3>
*
@@ -259,10 +258,11 @@ public abstract class CommandLinePropertySource<T> extends EnumerablePropertySou
* This implementation first checks to see if the name specified is the special
* {@linkplain #setNonOptionArgsPropertyName(String) "non-option arguments" property},
* and if so delegates to the abstract {@link #getNonOptionArgs()} method. If so
* and the collection of non-option arguments is empty, this method returns {@code
* null}. If not empty, it returns a comma-separated String of all non-option
* arguments. Otherwise, delegates to and returns the result of the abstract {@link
* #getOptionValues(String)} method.
* and the collection of non-option arguments is empty, this method returns
* {@code null}. If not empty, it returns a comma-separated String of all non-option
* arguments. Otherwise, this method delegates to and returns a comma-separated String
* of the results of the abstract {@link #getOptionValues(String)} method or
* {@code null} if there are no such option values.
*/
@Override
@Nullable
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,7 +28,9 @@ package org.springframework.core.env;
* <p>That is, options must be prefixed with "{@code --}" and may or may not
* specify a value. If a value is specified, the name and value must be separated
* <em>without spaces</em> by an equals sign ("="). The value may optionally be
* an empty string.
* an empty string. If an option is present multiple times with different values
* &mdash; for example, {@code --foo=bar --foo=baz} &mdash; all supplied values
* will be stored for the option.
*
* <h4>Valid examples of option arguments</h4>
* <pre class="code">
@@ -37,14 +39,14 @@ package org.springframework.core.env;
* --foo=""
* --foo=bar
* --foo="bar then baz"
* --foo=bar,baz,biz</pre>
* --foo=bar,baz,biz
* --foo=bar --foo=baz --foo=biz</pre>
*
* <h4>Invalid examples of option arguments</h4>
* <pre class="code">
* -foo
* --foo bar
* --foo = bar
* --foo=bar --foo=baz --foo=biz</pre>
* --foo = bar</pre>
*
* <h3>End of option arguments</h3>
* <p>This parser supports the POSIX "end of options" delimiter, meaning that any
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,8 @@ import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
/**
* {@link CommandLinePropertySource} implementation backed by a simple String array.
* {@link CommandLinePropertySource} implementation backed by an instance of
* {@link CommandLineArgs}.
*
* <h3>Purpose</h3>
* <p>This {@code CommandLinePropertySource} implementation aims to provide the simplest
@@ -40,7 +41,9 @@ import org.springframework.util.StringUtils;
* <p>That is, options must be prefixed with "{@code --}" and may or may not
* specify a value. If a value is specified, the name and value must be separated
* <em>without spaces</em> by an equals sign ("="). The value may optionally be
* an empty string.
* an empty string. If an option is present multiple times with different values
* &mdash; for example, {@code --foo=bar --foo=baz} &mdash; all supplied values
* will be stored for the option.
*
* <h4>Valid examples of option arguments</h4>
* <pre class="code">
@@ -49,14 +52,14 @@ import org.springframework.util.StringUtils;
* --foo=""
* --foo=bar
* --foo="bar then baz"
* --foo=bar,baz,biz</pre>
* --foo=bar,baz,biz
* --foo=bar --foo=baz --foo=biz</pre>
*
* <h4>Invalid examples of option arguments</h4>
* <pre class="code">
* -foo
* --foo bar
* --foo = bar
* --foo=bar --foo=baz --foo=biz</pre>
* --foo = bar</pre>
*
* <h3>End of option arguments</h3>
* <p>The underlying parser supports the POSIX "end of options" delimiter, meaning
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,6 +66,20 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
else if (code == HttpURLConnection.HTTP_BAD_METHOD) {
con = url.openConnection();
customizeConnection(con);
if (con instanceof HttpURLConnection newHttpCon) {
code = newHttpCon.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
return true;
}
else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
httpCon = newHttpCon;
}
}
}
if (con.getContentLengthLong() > 0) {
return true;
@@ -111,6 +125,15 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
if (con instanceof HttpURLConnection httpCon) {
httpCon.setRequestMethod("HEAD");
int code = httpCon.getResponseCode();
if (code == HttpURLConnection.HTTP_BAD_METHOD) {
con = url.openConnection();
customizeConnection(con);
if (!(con instanceof HttpURLConnection newHttpCon)) {
return false;
}
code = newHttpCon.getResponseCode();
httpCon = newHttpCon;
}
if (code != HttpURLConnection.HTTP_OK) {
httpCon.disconnect();
return false;
@@ -259,7 +282,14 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
if (con instanceof HttpURLConnection httpCon) {
httpCon.setRequestMethod("HEAD");
}
return con.getContentLengthLong();
long length = con.getContentLengthLong();
if (length <= 0 && con instanceof HttpURLConnection httpCon &&
httpCon.getResponseCode() == HttpURLConnection.HTTP_BAD_METHOD) {
con = url.openConnection();
customizeConnection(con);
length = con.getContentLengthLong();
}
return length;
}
}
@@ -288,9 +318,17 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
httpCon.setRequestMethod("HEAD");
}
long lastModified = con.getLastModified();
if (fileCheck && lastModified == 0 && con.getContentLengthLong() <= 0) {
throw new FileNotFoundException(getDescription() +
" cannot be resolved in the file system for checking its last-modified timestamp");
if (lastModified == 0) {
if (con instanceof HttpURLConnection httpCon &&
httpCon.getResponseCode() == HttpURLConnection.HTTP_BAD_METHOD) {
con = url.openConnection();
customizeConnection(con);
lastModified = con.getLastModified();
}
if (fileCheck && con.getContentLengthLong() <= 0) {
throw new FileNotFoundException(getDescription() +
" cannot be resolved in the file system for checking its last-modified timestamp");
}
}
return lastModified;
}
@@ -813,6 +813,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
NavigableSet<String> entriesCache = this.jarEntriesCache.get(jarFileUrl);
if (entriesCache != null) {
Set<Resource> result = new LinkedHashSet<>(64);
// Clean root entry path to match jar entries format without "!" separators
rootEntryPath = rootEntryPath.replace(ResourceUtils.JAR_URL_SEPARATOR, "/");
// Search sorted entries from first entry with rootEntryPath prefix
for (String entryPath : entriesCache.tailSet(rootEntryPath, false)) {
if (!entryPath.startsWith(rootEntryPath)) {
@@ -841,8 +843,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
closeJarFile = !jarCon.getUseCaches();
}
catch (FileNotFoundException ex) {
// Happens in case of cached root directory without specific subdirectory present.
catch (ZipException | FileNotFoundException ex) {
// Happens in case of a non-jar file or in case of a cached root directory
// without specific subdirectory present, respectively.
return Collections.emptySet();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -175,9 +175,8 @@ final class PlaceholderParser {
}
private SimplePlaceholderPart createSimplePlaceholderPart(String text) {
String[] keyAndDefault = splitKeyAndDefault(text);
return ((keyAndDefault != null) ? new SimplePlaceholderPart(text, keyAndDefault[0], keyAndDefault[1]) :
new SimplePlaceholderPart(text, text, null));
ParsedSection section = parseSection(text);
return new SimplePlaceholderPart(text, section.key(), section.fallback());
}
private NestedPlaceholderPart createNestedPlaceholderPart(String text, List<Part> parts) {
@@ -193,28 +192,32 @@ final class PlaceholderParser {
}
else {
String candidate = part.text();
String[] keyAndDefault = splitKeyAndDefault(candidate);
if (keyAndDefault != null) {
keyParts.add(new TextPart(keyAndDefault[0]));
if (keyAndDefault[1] != null) {
defaultParts.add(new TextPart(keyAndDefault[1]));
}
ParsedSection section = parseSection(candidate);
keyParts.add(new TextPart(section.key()));
if (section.fallback() != null) {
defaultParts.add(new TextPart(section.fallback()));
defaultParts.addAll(parts.subList(i + 1, parts.size()));
return new NestedPlaceholderPart(text, keyParts, defaultParts);
}
else {
keyParts.add(part);
}
}
}
// No separator found
return new NestedPlaceholderPart(text, parts, null);
return new NestedPlaceholderPart(text, keyParts, null);
}
@Nullable
private String[] splitKeyAndDefault(String value) {
/**
* Parse an input value that may contain a separator character and return a
* {@link ParsedValue}. If a valid separator character has been identified, the
* given {@code value} is split between a {@code key} and a {@code fallback}. If not,
* only the {@code key} is set.
* <p>
* The returned key may be different from the original value as escaped
* separators, if any, are resolved.
* @param value the value to parse
* @return the parsed section
*/
private ParsedSection parseSection(String value) {
if (this.separator == null || !value.contains(this.separator)) {
return null;
return new ParsedSection(value, null);
}
int position = 0;
int index = value.indexOf(this.separator, position);
@@ -231,11 +234,11 @@ final class PlaceholderParser {
buffer.append(value, position, index);
String key = buffer.toString();
String fallback = value.substring(index + this.separator.length());
return new String[] { key, fallback };
return new ParsedSection(key, fallback);
}
}
buffer.append(value, position, value.length());
return new String[] { buffer.toString(), null };
return new ParsedSection(buffer.toString(), null);
}
private static void addText(String value, int start, int end, LinkedList<Part> parts) {
@@ -293,6 +296,10 @@ final class PlaceholderParser {
return (this.escape != null && index > 0 && value.charAt(index - 1) == this.escape);
}
record ParsedSection(String key, @Nullable String fallback) {
}
/**
* Provide the necessary context to handle and resolve underlying placeholders.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -180,10 +180,7 @@ class RegisterReflectionReflectiveProcessorTests {
@RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)
static class AnnotatedSimplePojo {
private String test;
AnnotatedSimplePojo(String test) {
this.test = test;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -405,7 +405,10 @@ class BridgeMethodResolverTests {
}
public abstract static class SubBar<T extends StringBuffer> extends InterBar<T> {
public abstract static class SubBar<T extends StringProducer> extends InterBar<T> {
}
public interface StringProducer extends CharSequence {
}
@@ -18,20 +18,25 @@ package org.springframework.core;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.core.GenericTypeResolver.getTypeVariableMap;
import static org.springframework.core.GenericTypeResolver.resolveParameterType;
import static org.springframework.core.GenericTypeResolver.resolveReturnType;
import static org.springframework.core.GenericTypeResolver.resolveReturnTypeArgument;
import static org.springframework.core.GenericTypeResolver.resolveType;
import static org.springframework.core.GenericTypeResolver.resolveTypeArgument;
import static org.springframework.core.GenericTypeResolver.resolveTypeArguments;
import static org.springframework.util.ReflectionUtils.findMethod;
/**
@@ -106,25 +111,25 @@ class GenericTypeResolverTests {
void testGetTypeVariableMap() {
Map<TypeVariable, Type> map;
map = GenericTypeResolver.getTypeVariableMap(MySimpleInterfaceType.class);
map = getTypeVariableMap(MySimpleInterfaceType.class);
assertThat(map.toString()).isEqualTo("{T=class java.lang.String}");
map = GenericTypeResolver.getTypeVariableMap(MyCollectionInterfaceType.class);
map = getTypeVariableMap(MyCollectionInterfaceType.class);
assertThat(map.toString()).isEqualTo("{T=java.util.Collection<java.lang.String>}");
map = GenericTypeResolver.getTypeVariableMap(MyCollectionSuperclassType.class);
map = getTypeVariableMap(MyCollectionSuperclassType.class);
assertThat(map.toString()).isEqualTo("{T=java.util.Collection<java.lang.String>}");
map = GenericTypeResolver.getTypeVariableMap(MySimpleTypeWithMethods.class);
map = getTypeVariableMap(MySimpleTypeWithMethods.class);
assertThat(map.toString()).isEqualTo("{T=class java.lang.Integer}");
map = GenericTypeResolver.getTypeVariableMap(TopLevelClass.class);
map = getTypeVariableMap(TopLevelClass.class);
assertThat(map.toString()).isEqualTo("{}");
map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.class);
map = getTypeVariableMap(TypedTopLevelClass.class);
assertThat(map.toString()).isEqualTo("{T=class java.lang.Integer}");
map = GenericTypeResolver.getTypeVariableMap(TypedTopLevelClass.TypedNested.class);
map = getTypeVariableMap(TypedTopLevelClass.TypedNested.class);
assertThat(map).hasSize(2);
Type t = null;
Type x = null;
@@ -142,19 +147,19 @@ class GenericTypeResolverTests {
@Test
void resolveTypeArgumentsOfAbstractType() {
Class<?>[] resolved = GenericTypeResolver.resolveTypeArguments(MyConcreteType.class, MyAbstractType.class);
Class<?>[] resolved = resolveTypeArguments(MyConcreteType.class, MyAbstractType.class);
assertThat(resolved).containsExactly(Character.class);
}
@Test // SPR-11030
void getGenericsCannotBeResolved() {
Class<?>[] resolved = GenericTypeResolver.resolveTypeArguments(List.class, Iterable.class);
Class<?>[] resolved = resolveTypeArguments(List.class, Iterable.class);
assertThat(resolved).isNull();
}
@Test // SPR-11052
void getRawMapTypeCannotBeResolved() {
Class<?>[] resolved = GenericTypeResolver.resolveTypeArguments(Map.class, Map.class);
Class<?>[] resolved = resolveTypeArguments(Map.class, Map.class);
assertThat(resolved).isNull();
}
@@ -163,26 +168,38 @@ class GenericTypeResolverTests {
void getGenericsOnArrayFromParamCannotBeResolved() throws Exception {
MethodParameter methodParameter = MethodParameter.forExecutable(
WithArrayBase.class.getDeclaredMethod("array", Object[].class), 0);
Class<?> resolved = GenericTypeResolver.resolveParameterType(methodParameter, WithArray.class);
Class<?> resolved = resolveParameterType(methodParameter, WithArray.class);
assertThat(resolved).isEqualTo(Object[].class);
}
@Test // SPR-11044
void getGenericsOnArrayFromReturnCannotBeResolved() throws Exception {
Class<?> resolved = GenericTypeResolver.resolveReturnType(
Class<?> resolved = resolveReturnType(
WithArrayBase.class.getDeclaredMethod("array", Object[].class), WithArray.class);
assertThat(resolved).isEqualTo(Object[].class);
}
@Test // SPR-11763
void resolveIncompleteTypeVariables() {
Class<?>[] resolved = GenericTypeResolver.resolveTypeArguments(IdFixingRepository.class, Repository.class);
Class<?>[] resolved = resolveTypeArguments(IdFixingRepository.class, Repository.class);
assertThat(resolved).isNotNull();
assertThat(resolved).hasSize(2);
assertThat(resolved[0]).isEqualTo(Object.class);
assertThat(resolved[1]).isEqualTo(Long.class);
}
@Test // gh-34386
void resolveVariableNameChange() {
Type resolved = resolveType(Repository.class.getTypeParameters()[0], ConcreteRepository.class);
assertThat(resolved).isEqualTo(String.class);
Method method = method(Repository.class,"store", Supplier.class);
resolved = resolveType(method.getGenericParameterTypes()[0], ConcreteRepository.class);
assertThat(resolved).isInstanceOf(ParameterizedType.class);
ParameterizedType pt = (ParameterizedType) resolved;
assertThat(pt.getRawType()).isEqualTo(Supplier.class);
assertThat(pt.getActualTypeArguments()[0]).isEqualTo(String.class);
}
@Test
void resolvePartiallySpecializedTypeVariables() {
Type resolved = resolveType(BiGenericClass.class.getTypeParameters()[0], TypeFixedBiGenericClass.class);
@@ -212,19 +229,27 @@ class GenericTypeResolverTests {
}
@Test
void resolvedTypeWithBase() {
Type type = method(WithBaseTypes.class, "get").getGenericReturnType();
Type resolvedType = resolveType(type, WithBaseTypes.class);
void resolveTypeWithElementBounds() {
Type type = method(WithElementBounds.class, "get").getGenericReturnType();
Type resolvedType = resolveType(type, WithElementBounds.class);
ParameterizedTypeReference<List<A>> reference = new ParameterizedTypeReference<>() {};
assertThat(resolvedType).isEqualTo(reference.getType());
}
@Test
void resolveTypeWithUnresolvableElement() {
Type type = method(WithUnresolvableElement.class, "get").getGenericReturnType();
Type resolvedType = resolveType(type, WithUnresolvableElement.class);
assertThat(resolvedType.toString()).isEqualTo("java.util.List<E>");
}
private static Method method(Class<?> target, String methodName, Class<?>... parameterTypes) {
Method method = findMethod(target, methodName, parameterTypes);
assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull();
return method;
}
public interface MyInterfaceType<T> {
}
@@ -348,9 +373,9 @@ class GenericTypeResolverTests {
static class GenericClass<T> {
}
class A{}
class A {}
class B<T>{}
class B<T> {}
class C extends A {}
@@ -358,23 +383,25 @@ class GenericTypeResolverTests {
class E extends C {}
class TestIfc<T>{}
class TestIfc<T> {}
class TestImpl<I extends A, T extends B<I>> extends TestIfc<T>{
class TestImpl<I extends A, T extends B<I>> extends TestIfc<T> {
}
abstract static class BiGenericClass<T extends B<?>, V extends A> {}
abstract static class SpecializedBiGenericClass<U extends C> extends BiGenericClass<D, U>{}
abstract static class SpecializedBiGenericClass<U extends C> extends BiGenericClass<D, U> {}
static class TypeFixedBiGenericClass extends SpecializedBiGenericClass<E> {}
static class TopLevelClass<T> {
class Nested<X> {
}
}
static class TypedTopLevelClass extends TopLevelClass<Integer> {
class TypedNested extends Nested<Long> {
}
}
@@ -388,30 +415,43 @@ class GenericTypeResolverTests {
}
interface Repository<T, ID extends Serializable> {
default void store(Supplier<T> t) {
}
}
interface IdFixingRepository<T> extends Repository<T, Long> {
interface IdFixingRepository<V> extends Repository<V, Long> {
}
static class ConcreteRepository implements IdFixingRepository<String> {
}
static class WithMethodParameter {
public void nestedGenerics(List<Map<String, Integer>> input) {
}
}
public interface ListOfListSupplier<T> {
interface ListOfListSupplier<T> {
List<List<T>> get();
}
public interface StringListOfListSupplier extends ListOfListSupplier<String> {
interface StringListOfListSupplier extends ListOfListSupplier<String> {
}
static class WithBaseTypes {
static class WithElementBounds {
<T extends A> List<T> get() {
return List.of();
}
}
static class WithUnresolvableElement<T> {
List<T> get() {
return List.of();
}
}
}
@@ -1230,6 +1230,8 @@ class ResolvableTypeTests {
ResolvableType consumerUnresolved = ResolvableType.forClass(Consumer.class);
ResolvableType consumerObject = ResolvableType.forClassWithGenerics(Consumer.class, Object.class);
ResolvableType consumerNestedUnresolved = ResolvableType.forClassWithGenerics(Consumer.class, ResolvableType.forClass(Consumer.class));
ResolvableType consumerNumber = ResolvableType.forClassWithGenerics(Consumer.class, Number.class);
ResolvableType consumerExtendsNumber = ResolvableType.forClass(SubConsumer.class);
assertThat(consumerUnresolved.isAssignableFrom(consumerObject)).isTrue();
assertThat(consumerUnresolved.isAssignableFromResolvedPart(consumerObject)).isTrue();
@@ -1239,6 +1241,10 @@ class ResolvableTypeTests {
assertThat(consumerUnresolved.isAssignableFromResolvedPart(consumerNestedUnresolved)).isTrue();
assertThat(consumerObject.isAssignableFrom(consumerNestedUnresolved)).isFalse();
assertThat(consumerObject.isAssignableFromResolvedPart(consumerNestedUnresolved)).isFalse();
assertThat(consumerObject.isAssignableFrom(consumerNumber)).isFalse();
assertThat(consumerObject.isAssignableFromResolvedPart(consumerNumber)).isFalse();
assertThat(consumerObject.isAssignableFrom(consumerExtendsNumber)).isFalse();
assertThat(consumerObject.isAssignableFromResolvedPart(consumerExtendsNumber)).isTrue();
}
@Test
@@ -1788,6 +1794,9 @@ class ResolvableTypeTests {
public interface Consumer<T> {
}
private static class SubConsumer<N extends Number> implements Consumer<N> {
}
public class Wildcard<T extends CharSequence> {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -565,6 +565,22 @@ class GenericConversionServiceTests {
assertThat(conversionService.convert("test", TypeDescriptor.valueOf(String.class), new TypeDescriptor(getClass().getField("integerCollection")))).isEqualTo(Collections.singleton("testX"));
}
@Test
void stringListToListOfSubclassOfUnboundGenericClass() {
conversionService.addConverter(new StringListToAListConverter());
conversionService.addConverter(new StringListToBListConverter());
List<?> aList = (List<?>) conversionService.convert(List.of("foo"),
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)),
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(ARaw.class)));
assertThat(aList).allMatch(e -> e instanceof ARaw);
List<?> bList = (List<?>) conversionService.convert(List.of("foo"),
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)),
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(BRaw.class)));
assertThat(bList).allMatch(e -> e instanceof BRaw);
}
@ExampleAnnotation(active = true)
public String annotatedString;
@@ -742,6 +758,7 @@ class GenericConversionServiceTests {
}
}
private interface MyEnumBaseInterface {
String getBaseCode();
}
@@ -923,4 +940,35 @@ class GenericConversionServiceTests {
return Color.decode(source.substring(0, 6));
}
}
private static class GenericBaseClass<T> {
}
@SuppressWarnings("rawtypes")
private static class ARaw extends GenericBaseClass {
}
@SuppressWarnings("rawtypes")
private static class BRaw extends GenericBaseClass {
}
private static class StringListToAListConverter implements Converter<List<String>, List<ARaw>> {
@Override
public List<ARaw> convert(List<String> source) {
return List.of(new ARaw());
}
}
private static class StringListToBListConverter implements Converter<List<String>, List<BRaw>> {
@Override
public List<BRaw> convert(List<String> source) {
return List.of(new BRaw());
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -46,7 +46,7 @@ class SimpleCommandLineArgsParserTests {
void withSingleOptionAndNoValue() {
CommandLineArgs args = parser.parse("--o1");
assertThat(args.containsOption("o1")).isTrue();
assertThat(args.getOptionValues("o1")).isEqualTo(Collections.EMPTY_LIST);
assertThat(args.getOptionValues("o1")).isEmpty();
}
@Test
@@ -56,6 +56,20 @@ class SimpleCommandLineArgsParserTests {
assertThat(args.getOptionValues("o1")).containsExactly("v1");
}
@Test
void withRepeatedOptionAndSameValues() {
CommandLineArgs args = parser.parse("--o1=v1", "--o1=v1", "--o1=v1");
assertThat(args.containsOption("o1")).isTrue();
assertThat(args.getOptionValues("o1")).containsExactly("v1", "v1", "v1");
}
@Test
void withRepeatedOptionAndDifferentValues() {
CommandLineArgs args = parser.parse("--o1=v1", "--o1=v2", "--o1=v3");
assertThat(args.containsOption("o1")).isTrue();
assertThat(args.getOptionValues("o1")).containsExactly("v1", "v2", "v3");
}
@Test
void withMixOfOptionsHavingValueAndOptionsHavingNoValue() {
CommandLineArgs args = parser.parse("--o1=v1", "--o2");
@@ -95,17 +109,17 @@ class SimpleCommandLineArgsParserTests {
}
@Test
void assertOptionNamesIsUnmodifiable() {
void optionNamesSetIsUnmodifiable() {
CommandLineArgs args = new SimpleCommandLineArgsParser().parse();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
args.getOptionNames().add("bogus"));
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> args.getOptionNames().add("bogus"));
}
@Test
void assertNonOptionArgsIsUnmodifiable() {
void nonOptionArgsListIsUnmodifiable() {
CommandLineArgs args = new SimpleCommandLineArgsParser().parse();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
args.getNonOptionArgs().add("foo"));
assertThatExceptionOfType(UnsupportedOperationException.class)
.isThrownBy(() -> args.getNonOptionArgs().add("foo"));
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -61,6 +61,15 @@ class SimpleCommandLinePropertySourceTests {
assertThat(ps.getProperty("o3")).isNull();
}
@Test // gh-34282
void withRepeatedOptionArgs() {
CommandLinePropertySource<?> ps = new SimpleCommandLinePropertySource("--o1=v1", "--o1=v2", "--o1=v3");
assertThat(ps.containsProperty("o1")).isTrue();
assertThat(ps.containsProperty("o2")).isFalse();
assertThat(ps.getProperty("o1")).isEqualTo("v1,v2,v3");
assertThat(ps.getProperty("o2")).isNull();
}
@Test // gh-24464
void withOptionalArg_andArgIsEmpty() {
EnumerablePropertySource<?> ps = new SimpleCommandLinePropertySource("--foo=");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -306,7 +306,9 @@ class ResourceTests {
@Nested
class UrlResourceTests {
private MockWebServer server = new MockWebServer();
private static final String LAST_MODIFIED = "Wed, 09 Apr 2014 09:57:42 GMT";
private final MockWebServer server = new MockWebServer();
@Test
void sameResourceWithRelativePathIsEqual() throws Exception {
@@ -385,22 +387,44 @@ class ResourceTests {
@Test
void missingRemoteResourceDoesNotExist() throws Exception {
String baseUrl = startServer();
String baseUrl = startServer(true);
UrlResource resource = new UrlResource(baseUrl + "/missing");
assertThat(resource.exists()).isFalse();
}
@Test
void remoteResourceExists() throws Exception {
String baseUrl = startServer();
String baseUrl = startServer(true);
UrlResource resource = new UrlResource(baseUrl + "/resource");
assertThat(resource.exists()).isTrue();
assertThat(resource.isReadable()).isTrue();
assertThat(resource.contentLength()).isEqualTo(6);
assertThat(resource.lastModified()).isGreaterThan(0);
}
@Test
void remoteResourceExistsFallback() throws Exception {
String baseUrl = startServer(false);
UrlResource resource = new UrlResource(baseUrl + "/resource");
assertThat(resource.exists()).isTrue();
assertThat(resource.isReadable()).isTrue();
assertThat(resource.contentLength()).isEqualTo(6);
assertThat(resource.lastModified()).isGreaterThan(0);
}
@Test
void canCustomizeHttpUrlConnectionForExists() throws Exception {
String baseUrl = startServer();
String baseUrl = startServer(true);
CustomResource resource = new CustomResource(baseUrl + "/resource");
assertThat(resource.exists()).isTrue();
RecordedRequest request = this.server.takeRequest();
assertThat(request.getMethod()).isEqualTo("HEAD");
assertThat(request.getHeader("Framework-Name")).isEqualTo("Spring");
}
@Test
void canCustomizeHttpUrlConnectionForExistsFallback() throws Exception {
String baseUrl = startServer(false);
CustomResource resource = new CustomResource(baseUrl + "/resource");
assertThat(resource.exists()).isTrue();
RecordedRequest request = this.server.takeRequest();
@@ -410,7 +434,7 @@ class ResourceTests {
@Test
void canCustomizeHttpUrlConnectionForRead() throws Exception {
String baseUrl = startServer();
String baseUrl = startServer(true);
CustomResource resource = new CustomResource(baseUrl + "/resource");
assertThat(resource.getInputStream()).hasContent("Spring");
RecordedRequest request = this.server.takeRequest();
@@ -420,7 +444,7 @@ class ResourceTests {
@Test
void useUserInfoToSetBasicAuth() throws Exception {
startServer();
startServer(true);
UrlResource resource = new UrlResource(
"http://alice:secret@localhost:" + this.server.getPort() + "/resource");
assertThat(resource.getInputStream()).hasContent("Spring");
@@ -436,8 +460,8 @@ class ResourceTests {
this.server.shutdown();
}
private String startServer() throws Exception {
this.server.setDispatcher(new ResourceDispatcher());
private String startServer(boolean withHeadSupport) throws Exception {
this.server.setDispatcher(new ResourceDispatcher(withHeadSupport));
this.server.start();
return "http://localhost:" + this.server.getPort();
}
@@ -456,15 +480,26 @@ class ResourceTests {
class ResourceDispatcher extends Dispatcher {
boolean withHeadSupport;
public ResourceDispatcher(boolean withHeadSupport) {
this.withHeadSupport = withHeadSupport;
}
@Override
public MockResponse dispatch(RecordedRequest request) {
if (request.getPath().equals("/resource")) {
return switch (request.getMethod()) {
case "HEAD" -> new MockResponse()
.addHeader("Content-Length", "6");
case "HEAD" -> (this.withHeadSupport ?
new MockResponse()
.addHeader("Content-Type", "text/plain")
.addHeader("Content-Length", "6")
.addHeader("Last-Modified", LAST_MODIFIED) :
new MockResponse().setResponseCode(405));
case "GET" -> new MockResponse()
.addHeader("Content-Length", "6")
.addHeader("Content-Type", "text/plain")
.addHeader("Content-Length", "6")
.addHeader("Last-Modified", LAST_MODIFIED)
.setBody("Spring");
default -> new MockResponse().setResponseCode(404);
};
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -278,6 +278,26 @@ class PlaceholderParserTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true);
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedInNestedPlaceholders")
void escapedSeparatorInNestedPlaceholder(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("app.environment", "qa");
properties.setProperty("app.service", "protocol");
properties.setProperty("protocol://host/qa/name", "protocol://example.com/qa/name");
properties.setProperty("service/host/qa/name", "https://example.com/qa/name");
properties.setProperty("service/host/qa/name:value", "https://example.com/qa/name-value");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
}
static Stream<Arguments> escapedInNestedPlaceholders() {
return Stream.of(
Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedPlaceholders")
void escapedPlaceholderIsNotReplaced(String text, String expected) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -319,8 +319,8 @@ public abstract class ReflectionHelper {
(sourceType.isArray() && !sourceType.isAssignableTo(targetType)) ||
(argument instanceof List)) {
TypeDescriptor targetTypeToUse =
(sourceType.isArray() || argument instanceof List ? targetType : componentTypeDesc);
TypeDescriptor targetTypeToUse = (sourceType.isArray() || argument instanceof List ||
converter.canConvert(sourceType, targetType) ? targetType : componentTypeDesc);
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse);
}
// Possible outcomes of the above if-else block:
@@ -420,8 +420,8 @@ public abstract class ReflectionHelper {
(sourceType.isArray() && !sourceType.isAssignableTo(varargsArrayType)) ||
(argument instanceof List)) {
TypeDescriptor targetTypeToUse =
(sourceType.isArray() || argument instanceof List ? varargsArrayType : varargsComponentType);
TypeDescriptor targetTypeToUse = (sourceType.isArray() || argument instanceof List ||
converter.canConvert(sourceType, varargsArrayType) ? varargsArrayType : varargsComponentType);
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetTypeToUse);
}
// Possible outcomes of the above if-else block:
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -118,6 +118,11 @@ class TestScenarioCreator {
"varargsFunction", MethodType.methodType(String.class, String[].class));
testContext.registerFunction("varargsFunctionHandle", varargsFunctionHandle);
// #varargsObjectFunctionHandle(args...)
MethodHandle varargsObjectFunctionHandle = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"varargsObjectFunction", MethodType.methodType(String.class, Object[].class));
testContext.registerFunction("varargsObjectFunctionHandle", varargsObjectFunctionHandle);
// #add(int, int)
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"add", MethodType.methodType(int.class, int.class, int.class));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,21 @@
package org.springframework.expression.spel;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeConverter;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -266,11 +276,10 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
@Test
void functionMethodMustBeStatic() throws Exception {
SpelExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("notStatic", this.getClass().getMethod("nonStatic"));
context.registerFunction("nonStatic", this.getClass().getMethod("nonStatic"));
SpelExpression expression = parser.parseRaw("#nonStatic()");
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> parser.parseRaw("#notStatic()").getValue(ctx))
.isThrownBy(() -> expression.getValue(context))
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(FUNCTION_MUST_BE_STATIC));
}
@@ -279,4 +288,75 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
public void nonStatic() {
}
@Nested // gh-34371
class VarargsAndPojoToArrayConversionTests {
private final StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext();
private final ArrayHolder arrayHolder = new ArrayHolder("a", "b", "c");
@BeforeEach
void setUp() {
DefaultConversionService conversionService = new DefaultConversionService();
conversionService.addConverter(new ArrayHolderConverter());
context.setTypeConverter(new StandardTypeConverter(conversionService));
context.setVariable("arrayHolder", arrayHolder);
}
@Test
void functionWithVarargsAndPojoToArrayConversion() {
// #varargsFunction: static String varargsFunction(String... strings) -> Arrays.toString(strings)
evaluate("#varargsFunction(#arrayHolder)", "[a, b, c]");
// #varargsObjectFunction: static String varargsObjectFunction(Object... args) -> Arrays.toString(args)
//
// Since ArrayHolder is an "instanceof Object" and Object is the varargs component type,
// we expect the ArrayHolder not to be converted to an array but rather to be passed
// "as is" as a single argument to the varargs method.
evaluate("#varargsObjectFunction(#arrayHolder)", "[" + arrayHolder.toString() + "]");
}
@Test
void functionWithVarargsAndPojoToArrayConversionViaMethodHandle() {
// #varargsFunctionHandle: static String varargsFunction(String... strings) -> Arrays.toString(strings)
evaluate("#varargsFunctionHandle(#arrayHolder)", "[a, b, c]");
// #varargsObjectFunctionHandle: static String varargsObjectFunction(Object... args) -> Arrays.toString(args)
//
// Since ArrayHolder is an "instanceof Object" and Object is the varargs component type,
// we expect the ArrayHolder not to be converted to an array but rather to be passed
// "as is" as a single argument to the varargs method.
evaluate("#varargsObjectFunctionHandle(#arrayHolder)", "[" + arrayHolder.toString() + "]");
}
private void evaluate(String expression, Object expectedValue) {
Expression expr = parser.parseExpression(expression);
assertThat(expr).as("expression").isNotNull();
Object value = expr.getValue(context);
assertThat(value).as("expression '" + expression + "'").isEqualTo(expectedValue);
}
record ArrayHolder(String... array) {
}
static class ArrayHolderConverter implements GenericConverter {
@Nullable
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Set.of(new ConvertiblePair(ArrayHolder.class, Object[].class));
}
@Nullable
@Override
public String[] convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return ((ArrayHolder) source).array();
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
@@ -281,12 +280,10 @@ public class UserDestinationMessageHandler implements MessageHandler, SmartLifec
return this.messagingTemplate;
}
public void send(UserDestinationResult destinationResult, Message<?> message) throws MessagingException {
Set<String> sessionIds = destinationResult.getSessionIds();
Iterator<String> itr = (sessionIds != null ? sessionIds.iterator() : null);
for (String target : destinationResult.getTargetDestinations()) {
String sessionId = (itr != null ? itr.next() : null);
public void send(UserDestinationResult result, Message<?> message) throws MessagingException {
Iterator<String> itr = result.getSessionIds().iterator();
for (String target : result.getTargetDestinations()) {
String sessionId = (itr.hasNext() ? itr.next() : null);
getTemplateToUse(sessionId).send(target, message);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,7 +44,11 @@ public class UserDestinationResult {
private final Set<String> sessionIds;
public UserDestinationResult(String sourceDestination, Set<String> targetDestinations,
/**
* Main constructor.
*/
public UserDestinationResult(
String sourceDestination, Set<String> targetDestinations,
String subscribeDestination, @Nullable String user) {
this(sourceDestination, targetDestinations, subscribeDestination, user, null);
@@ -114,7 +118,6 @@ public class UserDestinationResult {
/**
* Return the session id for the targetDestination.
*/
@Nullable
public Set<String> getSessionIds() {
return this.sessionIds;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.messaging.simp.user;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
@@ -98,6 +99,26 @@ class UserDestinationMessageHandlerTests {
assertThat(accessor.getFirstNativeHeader(ORIGINAL_DESTINATION)).isEqualTo("/user/queue/foo");
}
@Test
@SuppressWarnings("rawtypes")
void handleMessageWithoutSessionIds() {
UserDestinationResolver resolver = mock();
Message message = createWith(SimpMessageType.MESSAGE, "joe", null, "/user/joe/queue/foo");
UserDestinationResult result = new UserDestinationResult("/queue/foo-user123", Set.of("/queue/foo-user123"), "/user/queue/foo", "joe");
given(resolver.resolveDestination(message)).willReturn(result);
given(this.brokerChannel.send(Mockito.any(Message.class))).willReturn(true);
UserDestinationMessageHandler handler = new UserDestinationMessageHandler(new StubMessageChannel(), this.brokerChannel, resolver);
handler.handleMessage(message);
ArgumentCaptor<Message> captor = ArgumentCaptor.forClass(Message.class);
Mockito.verify(this.brokerChannel).send(captor.capture());
SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.wrap(captor.getValue());
assertThat(accessor.getDestination()).isEqualTo("/queue/foo-user123");
assertThat(accessor.getFirstNativeHeader(ORIGINAL_DESTINATION)).isEqualTo("/user/queue/foo");
}
@Test
@SuppressWarnings("rawtypes")
void handleMessageWithoutActiveSession() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -389,8 +389,8 @@ public class TestContextManager {
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* order in which they were registered.
* <p>Note that listeners will be executed in the opposite order in which they
* were registered.
* @param testInstance the current test instance
* @param testMethod the test method which has just been executed on the
* test instance
@@ -459,7 +459,8 @@ public class TestContextManager {
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* <p>Note that listeners will be executed in the opposite order in which they
* were registered.
* @param testInstance the current test instance
* @param testMethod the test method which has just been executed on the
* test instance
@@ -517,7 +518,8 @@ public class TestContextManager {
* have executed, the first caught exception will be rethrown with any
* subsequent exceptions {@linkplain Throwable#addSuppressed suppressed} in
* the first exception.
* <p>Note that registered listeners will be executed in the opposite
* <p>Note that listeners will be executed in the opposite order in which they
* were registered.
* @throws Exception if a registered TestExecutionListener throws an exception
* @since 3.0
* @see #getTestExecutionListeners()
@@ -59,6 +59,7 @@ import org.springframework.util.Assert;
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
* @author Yanming Zhou
* @since 6.2
*/
class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {
@@ -168,8 +169,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
else if (requireExistingBean) {
Field field = handler.getField();
throw new IllegalStateException(
"Unable to replace bean: there is no bean with name '%s' and type %s%s."
throw new IllegalStateException("""
Unable to replace bean: there is no bean with name '%s' and type %s%s. \
If the bean is defined in a @Bean method, make sure the return type is the \
most specific type possible (for example, the concrete implementation type)."""
.formatted(beanName, handler.getBeanType(), requiredByField(field)));
}
// 4) We are creating a bean by-name with the provided beanName.
@@ -249,26 +252,25 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
if (beanName == null) {
// We are wrapping an existing bean by-type.
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, handler, true);
int candidateCount = candidateNames.size();
if (candidateCount == 1) {
beanName = candidateNames.iterator().next();
String uniqueCandidate = determineUniqueCandidate(beanFactory, candidateNames, beanType, field);
if (uniqueCandidate != null) {
beanName = uniqueCandidate;
}
else {
String primaryCandidate = determinePrimaryCandidate(beanFactory, candidateNames, beanType.toClass());
if (primaryCandidate != null) {
beanName = primaryCandidate;
String message = "Unable to select a bean to wrap: ";
int candidateCount = candidateNames.size();
if (candidateCount == 0) {
message += """
there are no beans of type %s%s. \
If the bean is defined in a @Bean method, make sure the return type is the \
most specific type possible (for example, the concrete implementation type)."""
.formatted(beanType, requiredByField(field));
}
else {
String message = "Unable to select a bean to wrap: ";
if (candidateCount == 0) {
message += "there are no beans of type %s%s.".formatted(beanType, requiredByField(field));
}
else {
message += "found %d beans of type %s%s: %s"
.formatted(candidateCount, beanType, requiredByField(field), candidateNames);
}
throw new IllegalStateException(message);
message += "found %d beans of type %s%s: %s"
.formatted(candidateCount, beanType, requiredByField(field), candidateNames);
}
throw new IllegalStateException(message);
}
beanName = BeanFactoryUtils.transformedBeanName(beanName);
}
@@ -276,8 +278,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
// We are wrapping an existing bean by-name.
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
if (!candidates.contains(beanName)) {
throw new IllegalStateException(
"Unable to wrap bean: there is no bean with name '%s' and type %s%s."
throw new IllegalStateException("""
Unable to wrap bean: there is no bean with name '%s' and type %s%s. \
If the bean is defined in a @Bean method, make sure the return type is the \
most specific type possible (for example, the concrete implementation type)."""
.formatted(beanName, beanType, requiredByField(field)));
}
}
@@ -287,38 +291,37 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
@Nullable
private String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
private static String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean requireExistingBean) {
Field field = handler.getField();
ResolvableType beanType = handler.getBeanType();
Set<String> candidateNames = getExistingBeanNamesByType(beanFactory, handler, true);
int candidateCount = candidateNames.size();
if (candidateCount == 1) {
return candidateNames.iterator().next();
String uniqueCandidate = determineUniqueCandidate(beanFactory, candidateNames, beanType, field);
if (uniqueCandidate != null) {
return uniqueCandidate;
}
else if (candidateCount == 0) {
int candidateCount = candidateNames.size();
if (candidateCount == 0) {
if (requireExistingBean) {
throw new IllegalStateException(
"Unable to override bean: there are no beans of type %s%s."
throw new IllegalStateException("""
Unable to override bean: there are no beans of type %s%s. \
If the bean is defined in a @Bean method, make sure the return type is the \
most specific type possible (for example, the concrete implementation type)."""
.formatted(beanType, requiredByField(field)));
}
return null;
}
String primaryCandidate = determinePrimaryCandidate(beanFactory, candidateNames, beanType.toClass());
if (primaryCandidate != null) {
return primaryCandidate;
}
throw new IllegalStateException(
"Unable to select a bean to override: found %d beans of type %s%s: %s"
.formatted(candidateCount, beanType, requiredByField(field), candidateNames));
}
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean checkAutowiredCandidate) {
private static Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory,
BeanOverrideHandler handler, boolean checkAutowiredCandidate) {
Field field = handler.getField();
ResolvableType resolvableType = handler.getBeanType();
@@ -345,25 +348,56 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
// Filter out scoped proxy targets.
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);
// In case of multiple matches, fall back on the field's name as a last resort.
if (field != null && beanNames.size() > 1) {
return beanNames;
}
/**
* Determine the unique candidate in the given set of bean names.
* <p>Honors both <em>primary</em> and <em>fallback</em> semantics, and
* otherwise matches against the field name as a <em>fallback qualifier</em>.
* @return the name of the unique candidate, or {@code null} if none found
* @since 6.2.3
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determineAutowireCandidate
*/
@Nullable
private static String determineUniqueCandidate(ConfigurableListableBeanFactory beanFactory,
Set<String> candidateNames, ResolvableType beanType, @Nullable Field field) {
// Step 0: none or only one
int candidateCount = candidateNames.size();
if (candidateCount == 0) {
return null;
}
if (candidateCount == 1) {
return candidateNames.iterator().next();
}
// Step 1: check primary candidate
String primaryCandidate = determinePrimaryCandidate(beanFactory, candidateNames, beanType.toClass());
if (primaryCandidate != null) {
return primaryCandidate;
}
// Step 2: use the field name as a fallback qualifier
if (field != null) {
String fieldName = field.getName();
if (beanNames.contains(fieldName)) {
return Set.of(fieldName);
if (candidateNames.contains(fieldName)) {
return fieldName;
}
}
return beanNames;
return null;
}
/**
* Determine the primary candidate in the given set of bean names.
* <p>Honors both <em>primary</em> and <em>fallback</em> semantics.
* @return the name of the primary candidate, or {@code null} if none found
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determinePrimaryCandidate(Map, Class)
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determinePrimaryCandidate
*/
@Nullable
private static String determinePrimaryCandidate(
ConfigurableListableBeanFactory beanFactory, Set<String> candidateBeanNames, Class<?> beanType) {
private static String determinePrimaryCandidate(ConfigurableListableBeanFactory beanFactory,
Set<String> candidateBeanNames, Class<?> beanType) {
if (candidateBeanNames.isEmpty()) {
return null;
@@ -132,7 +132,7 @@ public abstract class BeanOverrideHandler {
private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolean localFieldsOnly) {
List<BeanOverrideHandler> handlers = new ArrayList<>();
findHandlers(testClass, testClass, handlers, localFieldsOnly);
findHandlers(testClass, testClass, handlers, localFieldsOnly, new HashSet<>());
return handlers;
}
@@ -146,26 +146,30 @@ public abstract class BeanOverrideHandler {
* @param testClass the original test class
* @param handlers the list of handlers found
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
* @param visitedEnclosingClasses the set of enclosing classes already visited
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly) {
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly);
Class<?> enclosingClass = clazz.getEnclosingClass();
if (visitedEnclosingClasses.add(enclosingClass)) {
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly);
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly);
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
// 4) Process current class.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,15 +37,21 @@ import org.springframework.test.context.support.DependencyInjectionTestExecution
public class BeanOverrideTestExecutionListener extends AbstractTestExecutionListener {
/**
* Returns {@code 1950}, which ensures that the {@code BeanOverrideTestExecutionListener}
* The {@link #getOrder() order} value for this listener: {@value}.
* @since 6.2.3
*/
public static final int ORDER = 1950;
/**
* Returns {@value #ORDER}, which ensures that the {@code BeanOverrideTestExecutionListener}
* is ordered after the
* {@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener
* DirtiesContextBeforeModesTestExecutionListener} and just before the
* DirtiesContextBeforeModesTestExecutionListener} and before the
* {@link DependencyInjectionTestExecutionListener}.
*/
@Override
public int getOrder() {
return 1950;
return ORDER;
}
/**
@@ -39,7 +39,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType,
@Nullable String beanName, BeanOverrideStrategy strategy, @Nullable MockReset reset) {
@Nullable String beanName, BeanOverrideStrategy strategy, MockReset reset) {
super(field, beanType, beanName, strategy);
this.reset = (reset != null ? reset : MockReset.AFTER);
@@ -31,9 +31,9 @@ import org.springframework.test.context.bean.override.BeanOverride;
/**
* {@code @MockitoBean} is an annotation that can be used in test classes to
* override beans in a test's
* override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* using Mockito mocks.
* with a Mockito mock.
*
* <p>{@code @MockitoBean} can be applied in the following ways.
* <ul>
@@ -49,18 +49,19 @@ import org.springframework.test.context.bean.override.BeanOverride;
* </ul>
*
* <p>When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
* from the type of the annotated field. If multiple candidates exist, a
* {@code @Qualifier} annotation can be declared on the field to help disambiguate.
* In the absence of a {@code @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 by setting the {@link #value() value} or
* {@link #name() name} attribute.
* from the type of the annotated field. If multiple candidates exist in the
* {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
* on the field to help disambiguate. In the absence of a {@code @Qualifier}
* annotation, the name of the annotated field will be used as a <em>fallback
* qualifier</em>. Alternatively, you can explicitly specify a bean name to mock
* by setting the {@link #value() value} or {@link #name() name} attribute.
*
* <p>When {@code @MockitoBean} is declared at the type level, the type of bean
* to mock must be supplied via the {@link #types() types} attribute. If multiple
* candidates exist, you can explicitly specify a bean name to mock by setting the
* {@link #name() name} attribute. Note, however, that the {@code types} attribute
* must contain a single type if an explicit bean {@code name} is configured.
* (or beans) to mock must be supplied via the {@link #types() types} attribute.
* If multiple candidates exist in the {@code ApplicationContext}, you can
* explicitly specify a bean name to mock by setting the {@link #name() name}
* attribute. Note, however, that the {@code types} attribute must contain a
* single type if an explicit bean {@code name} is configured.
*
* <p>A bean will be created if a corresponding bean does not exist. However, if
* you would like for the test to fail when a corresponding bean does not exist,
@@ -111,7 +112,7 @@ import org.springframework.test.context.bean.override.BeanOverride;
public @interface MockitoBean {
/**
* Alias for {@link #name()}.
* Alias for {@link #name() name}.
* <p>Intended to be used when no other attributes are needed &mdash; for
* example, {@code @MockitoBean("customBeanName")}.
* @see #name()
@@ -136,7 +137,7 @@ public @interface MockitoBean {
* <p>Each type specified will result in a mock being created and registered
* with the {@code ApplicationContext}.
* <p>Types must be omitted when the annotation is used on a field.
* <p>When {@code @MockitoBean} also defines a {@link #name}, this attribute
* <p>When {@code @MockitoBean} also defines a {@link #name name}, this attribute
* can only contain a single value.
* @return the types to mock
* @since 6.2.2
@@ -113,7 +113,9 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
}
@Override
protected Object createOverrideInstance(String beanName, @Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
protected Object createOverrideInstance(String beanName,
@Nullable BeanDefinition existingBeanDefinition, @Nullable Object existingBeanInstance) {
return createMock(beanName);
}
@@ -45,8 +45,10 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
"The @MockitoBean 'types' attribute must be omitted when declared on a field");
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
}
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean);
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
Assert.state(mockitoSpyBean.types().length == 0,
"The @MockitoSpyBean 'types' attribute must be omitted when declared on a field");
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoSpyBean);
}
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
@@ -56,21 +58,34 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
@Override
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
expected @MockitoBean on test class """ + testClass.getName());
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
Class<?>[] types = mockitoBean.types();
Assert.state(types.length > 0,
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
List<BeanOverrideHandler> handlers = new ArrayList<>();
for (Class<?> type : types) {
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
}
return handlers;
}
Class<?>[] types = mockitoBean.types();
Assert.state(types.length > 0,
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
List<BeanOverrideHandler> handlers = new ArrayList<>();
for (Class<?> type : types) {
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
else if (overrideAnnotation instanceof MockitoSpyBean mockitoSpyBean) {
Class<?>[] types = mockitoSpyBean.types();
Assert.state(types.length > 0,
"The @MockitoSpyBean 'types' attribute must not be empty when declared on a class");
Assert.state(mockitoSpyBean.name().isEmpty() || types.length == 1,
"The @MockitoSpyBean 'name' attribute cannot be used when mocking multiple types");
List<BeanOverrideHandler> handlers = new ArrayList<>();
for (Class<?> type : types) {
handlers.add(new MockitoSpyBeanOverrideHandler(ResolvableType.forClass(type), mockitoSpyBean));
}
return handlers;
}
return handlers;
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
expected either @MockitoBean or @MockitoSpyBean on test class %s"""
.formatted(testClass.getName()));
}
}
@@ -49,6 +49,13 @@ import org.springframework.util.ClassUtils;
*/
public class MockitoResetTestExecutionListener extends AbstractTestExecutionListener {
/**
* The {@link #getOrder() order} value for this listener
* ({@code Ordered.LOWEST_PRECEDENCE - 100}): {@value}.
* @since 6.2.3
*/
public static final int ORDER = Ordered.LOWEST_PRECEDENCE - 100;
private static final Log logger = LogFactory.getLog(MockitoResetTestExecutionListener.class);
/**
@@ -74,7 +81,10 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionList
/**
* Returns {@code Ordered.LOWEST_PRECEDENCE - 100}.
* Returns {@value #ORDER}, which ensures that the
* {@code MockitoResetTestExecutionListener} is ordered after all standard
* {@code TestExecutionListener} implementations.
* @see #ORDER
*/
@Override
public int getOrder() {
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -26,19 +27,40 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride;
/**
* {@code @MockitoSpyBean} is an annotation that can be applied to a non-static
* field in a test class to override a bean in the test's
* {@code @MockitoSpyBean} is an annotation that can be used in test classes to
* override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* with a Mockito spy that wraps the original bean instance.
*
* <p>By default, the bean to spy is inferred from the type of the annotated
* field. If multiple candidates exist, a {@code @Qualifier} annotation can be
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
* the name of the annotated field will be used as a fallback qualifier.
* Alternatively, you can explicitly specify a bean name to spy by setting the
* {@link #value() value} or {@link #name() name} attribute. If a bean name is
* specified, it is required that a target bean with that name has been previously
* registered in the application context.
* <p>{@code @MockitoSpyBean} can be applied in the following ways.
* <ul>
* <li>On a non-static field in a test class or any of its superclasses.</li>
* <li>On a non-static field in an enclosing class for a {@code @Nested} test class
* or in any class in the type hierarchy or enclosing class hierarchy above the
* {@code @Nested} test class.</li>
* <li>At the type level on a test class or any superclass or implemented interface
* in the type hierarchy above the test class.</li>
* <li>At the type level on an enclosing class for a {@code @Nested} test class
* or on any class or interface in the type hierarchy or enclosing class hierarchy
* above the {@code @Nested} test class.</li>
* </ul>
*
* <p>When {@code @MockitoSpyBean} is declared on a field, the bean to spy is
* inferred from the type of the annotated field. If multiple candidates exist in
* the {@code ApplicationContext}, a {@code @Qualifier} annotation can be declared
* on the field to help disambiguate. In the absence of a {@code @Qualifier}
* annotation, the name of the annotated field will be used as a <em>fallback
* qualifier</em>. Alternatively, you can explicitly specify a bean name to spy
* by setting the {@link #value() value} or {@link #name() name} attribute. If a
* bean name is specified, it is required that a target bean with that name has
* been previously registered in the application context.
*
* <p>When {@code @MockitoSpyBean} is declared at the type level, the type of bean
* (or beans) to spy must be supplied via the {@link #types() types} attribute.
* If multiple candidates exist in the {@code ApplicationContext}, you can
* explicitly specify a bean name to spy by setting the {@link #name() name}
* attribute. Note, however, that the {@code types} attribute must contain a
* single type if an explicit bean {@code name} is configured.
*
* <p>A spy cannot be created for components which are known to the application
* context but are not beans &mdash; for example, components
@@ -56,24 +78,33 @@ import org.springframework.test.context.bean.override.BeanOverride;
* (default visibility), or {@code private} depending on the needs or coding
* practices of the project.
*
* <p>{@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default.
* See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* <p>{@code @MockitoSpyBean} fields and type-level {@code @MockitoSpyBean} declarations
* will be inherited from an enclosing test class by default. See
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* for details.
*
* <p>{@code @MockitoSpyBean} may be used as a <em>meta-annotation</em> to create
* custom <em>composed annotations</em> &mdash; for example, to define common spy
* configuration in a single annotation that can be reused across a test suite.
* {@code @MockitoSpyBean} can also be used as a <em>{@linkplain Repeatable repeatable}</em>
* annotation at the type level &mdash; for example, to spy on several beans by
* {@link #name() name}.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
* @see org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
*/
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(MockitoSpyBeans.class)
@BeanOverride(MockitoBeanOverrideProcessor.class)
public @interface MockitoSpyBean {
/**
* Alias for {@link #name()}.
* Alias for {@link #name() name}.
* <p>Intended to be used when no other attributes are needed &mdash; for
* example, {@code @MockitoSpyBean("customBeanName")}.
* @see #name()
@@ -84,13 +115,27 @@ public @interface MockitoSpyBean {
/**
* Name of the bean to spy.
* <p>If left unspecified, the bean to spy is selected according to the
* annotated field's type, taking qualifiers into account if necessary. See
* the {@linkplain MockitoSpyBean class-level documentation} for details.
* configured {@link #types() types} or the annotated field's type, taking
* qualifiers into account if necessary. See the {@linkplain MockitoSpyBean
* class-level documentation} for details.
* @see #value()
*/
@AliasFor("value")
String name() default "";
/**
* One or more types to spy.
* <p>Defaults to none.
* <p>Each type specified will result in a spy being created and registered
* with the {@code ApplicationContext}.
* <p>Types must be omitted when the annotation is used on a field.
* <p>When {@code @MockitoSpyBean} also defines a {@link #name name}, this
* attribute can only contain a single value.
* @return the types to spy
* @since 6.2.3
*/
Class<?>[] types() default {};
/**
* The reset mode to apply to the spied bean.
* <p>The default is {@link MockReset#AFTER} meaning that spies are automatically
@@ -48,7 +48,11 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
new SpringAopBypassingVerificationStartedListener();
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
MockitoSpyBeanOverrideHandler(ResolvableType typeToSpy, MockitoSpyBean spyBean) {
this(null, typeToSpy, spyBean);
}
MockitoSpyBeanOverrideHandler(@Nullable Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
BeanOverrideStrategy.WRAP, spyBean.reset());
Assert.notNull(typeToSpy, "typeToSpy must not be null");
@@ -0,0 +1,41 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.test.context.bean.override.mockito;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Container for {@link MockitoSpyBean @MockitoSpyBean} annotations which allows
* {@code @MockitoSpyBean} to be used as a {@linkplain java.lang.annotation.Repeatable
* repeatable annotation} at the type level &mdash; for example, on test classes
* or interfaces implemented by test classes.
*
* @author Sam Brannen
* @since 6.2.3
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MockitoSpyBeans {
MockitoSpyBean[] value();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,6 +48,12 @@ import org.springframework.util.Assert;
*/
public class ApplicationEventsTestExecutionListener extends AbstractTestExecutionListener {
/**
* The {@link #getOrder() order} value for this listener: {@value}.
* @since 6.2.3
*/
public static final int ORDER = 1800;
/**
* Attribute name for a {@link TestContext} attribute which indicates
* whether the test class for the given test context is annotated with
@@ -61,11 +67,18 @@ public class ApplicationEventsTestExecutionListener extends AbstractTestExecutio
/**
* Returns {@code 1800}.
* Returns {@value #ORDER}, which ensures that the {@code ApplicationEventsTestExecutionListener}
* is ordered after the
* {@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener
* DirtiesContextBeforeModesTestExecutionListener} and before the
* {@link org.springframework.test.context.bean.override.BeanOverrideTestExecutionListener
* BeanOverrideTestExecutionListener} and the
* {@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener
* DependencyInjectionTestExecutionListener}.
*/
@Override
public final int getOrder() {
return 1800;
return ORDER;
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -98,11 +98,22 @@ import org.springframework.test.context.support.AbstractTestExecutionListener;
public class EventPublishingTestExecutionListener extends AbstractTestExecutionListener {
/**
* Returns {@code 10000}.
* The {@link #getOrder() order} value for this listener: {@value}.
* @since 6.2.3
*/
public static final int ORDER = 10_000;
/**
* Returns {@value #ORDER}, which ensures that the {@code EventPublishingTestExecutionListener}
* is ordered after the
* {@link org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener
* SqlScriptsTestExecutionListener} and before the
* {@link org.springframework.test.context.bean.override.mockito.MockitoResetTestExecutionListener
* MockitoResetTestExecutionListener}.
*/
@Override
public final int getOrder() {
return 10_000;
return ORDER;
}
/**

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