Compare commits

..

96 Commits

Author SHA1 Message Date
Brian Clozel b4f10d4f1d Release v6.2.1 2024-12-12 10:18:03 +01:00
Juergen Hoeller 0aa721cad0 Polishing 2024-12-11 17:52:59 +01:00
Juergen Hoeller 63af572ce8 Upgrade to Jackson 2.18.2, RxJava 3.1.10, Checkstyle 10.20.2 2024-12-11 17:34:53 +01:00
Juergen Hoeller 72c2343f63 Avoid deprecated ListenableFuture name for internal class 2024-12-11 17:34:48 +01:00
rstoyanchev 8aeced9f80 Support header filtering in web data binding
Closes gh-34039
2024-12-11 16:11:22 +00:00
rstoyanchev 70c326ed30 Support headers in DataBinding via constructor args
Closes gh-34073
2024-12-11 16:10:08 +00:00
rstoyanchev 7b4e19c69b Make ExtendedServletRequestDataBinder public
Make it public and move it down to the annotations package alongside
InitBinderBindingContext. This is mirrors the hierarchy in Spring MVC
with the ExtendedServletRequestDataBinder. The change will allow
customization of the header names to include/exclude in data binding.

See gh-34039
2024-12-11 16:09:56 +00:00
rstoyanchev 3b95d2c449 Support Flux<ServerSentEvent<Fragment>> in WebFlux
Closes gh-33975
2024-12-11 16:09:38 +00:00
rstoyanchev 640e570583 Minor refactoring in ServerSentEvent
Extract re-usable method to serialize SSE fields.

See gh-33975
2024-12-11 16:09:22 +00:00
rstoyanchev 66f33a8265 MapMethodProcessor supportsParameter is more specific
Closes gh-33160
2024-12-11 16:09:01 +00:00
Juergen Hoeller 68997d8416 Avoid javadoc references to deprecated types/methods 2024-12-11 16:58:37 +01:00
Brian Clozel 52006b71bc Support Servlet error message in MockMvc assertions
Prior to this commit, `MockMvc` would support checking for the Servlet
error message as the "response status reason". While this error message
can be driven with the `@ResponseStatus` annotation, this message is not
technically the HTTP status reason listed on the response status line.

This message is provided by the Servlet container in the error page when
the `response.sendError(int, String)` method is used.

This commit adds the missing
`mvc.get().uri("/error/message")).hasErrorMessage("error message")`
assertion to check for this Servlet error message.

Closes gh-34016
2024-12-11 16:00:41 +01:00
Sam Brannen 41d9f21ab9 Log alias removal in DefaultListableBeanFactory
Prior to this commit, information was logged when a bean definition
overrode an existing bean definition, but nothing was logged when the
registration of a bean definition resulted in the removal of an alias.

With this commit, an INFO message is now logged whenever an alias is
removed in DefaultListableBeanFactory.

Closes gh-34070
2024-12-11 15:03:52 +01:00
Sam Brannen 7206b28272 Implement toString() in TestBeanOverrideHandler
Closes gh-34072
2024-12-11 12:12:35 +01:00
Brian Clozel 0c688742e1 Fix custom scheduler support for @Scheduled methods
This commit fixes a regression introduced by gh-24560, when adding
execution metadata support for scheduled tasks. The
`OutcomeTrackingRunnable` would delegate to the actual runnable but
could also hide whether it implements the `SchedulingAwareRunnable`
contract.

This commit ensures that `OutcomeTrackingRunnable` always implements
that contract and delegates to the runnable if possible, or return
default values otherwise.

Fixes gh-34058
2024-12-10 22:33:20 +01:00
Juergen Hoeller 7de1dc826a Consistently handle generics in TypeDescriptor.equals
Properly processes recursive types through always comparing generics via the top-level ResolvableType (rather than through nested TypeDescriptors with custom ResolvableType instances).

Closes gh-33932
2024-12-10 22:16:10 +01:00
Juergen Hoeller 3e3ca74020 Log provider setup failure at info level without stacktrace
Closes gh-33979
2024-12-10 16:25:57 +01:00
Juergen Hoeller 66da5d7ab9 Restore original override behavior when override allowed
Closes gh-33920
2024-12-10 16:25:49 +01:00
Stéphane Nicoll 68d6cb9d35 Upgrade to Reactor 2024.0.1
Closes gh-34051
2024-12-10 14:06:07 +01:00
Stéphane Nicoll 6649a6e0f7 Upgrade to Micrometer 1.14.2
Closes gh-34050
2024-12-10 14:05:43 +01:00
Sam Brannen 54948a4e88 Log warning when one Bean Override overrides another Bean Override
It is currently possible for one Bean Override to override another
logically equivalent Bean Override.

For example, a @⁠TestBean can override a @⁠MockitoBean, and vice versa.

In fact, it's also possible for a @⁠MockitoBean to override another
@⁠MockitoBean, for a @⁠TestBean to override a @⁠TestBean, etc.

However, there may be viable use cases for one override overriding
another override. For example, one may have a need to spy on a bean
created by a @⁠TestBean factory method.

In light of that, we do not prohibit one Bean Override from overriding
another Bean Override; however, with this commit we now log a warning
to help developers diagnose issues in case such an override is
unintentional.

For example, given the following test class, where TestConfig registers
a single bean of type MyService named "myService"...

@⁠SpringJUnitConfig(TestConfig.class)
class MyTests {

  @⁠TestBean(methodName = "example.TestUtils#createMyService")
  MyService testService;

  @⁠MockitoBean
  MyService mockService;

  @⁠Test
  void test() {
    // ...
  }
}

... running that test class results in a log message similar to the
following, which has been formatted for readability.

WARN - Bean with name 'myService' was overridden by multiple handlers:
[
 [TestBeanOverrideHandler@44b21f9f
  field = example.MyService example.MyTests.testService,
  beanType = example.MyService,
  beanName = [null],
  strategy = REPLACE_OR_CREATE
 ],
 [MockitoBeanOverrideHandler@7ee8130e
  field = example.MyService example.MyTests.mockService,
  beanType = example.MyService,
  beanName = [null],
  strategy = REPLACE_OR_CREATE,
  reset = AFTER,
  extraInterfaces = set[[empty]],
  answers = RETURNS_DEFAULTS, serializable = false
 ]
]

NOTE: The last registered BeanOverrideHandler wins. In the above
example, that means that @⁠MockitoBean overrides @⁠TestBean, resulting
in a Mockito mock for the MyService bean in the test's
ApplicationContext.

Closes gh-34056
2024-12-10 12:49:48 +01:00
Sébastien Deleuze a00ba8dbcf Revert "Reuse NoTransactionInContextException instances"
This reverts commit 8e3846991d.

Closes gh-34048
2024-12-10 11:47:56 +01:00
Stéphane Nicoll 4f815b0055 Merge pull request #33891 from youabledev
* pr/33891:
  Polish

Closes gh-33891
2024-12-10 07:47:52 +01:00
youable 5494d78018 Polish
See gh-33891
2024-12-10 07:47:42 +01:00
Sam Brannen cd60a0013b Reject identical Bean Overrides
Prior to this commit, the Bean Override feature in the Spring
TestContext Framework (for annotations such as @⁠MockitoBean and
@⁠TestBean) silently allowed one bean override to override another
"identical" bean override; however, Spring Boot's @⁠MockBean and
@⁠SpyBean support preemptively rejects identical overrides and throws
an IllegalStateException to signal the configuration error to the user.

To align with the behavior of @⁠MockBean and @⁠SpyBean in Spring Boot,
and to help developers avoid scenarios that are potentially confusing
or difficult to debug, this commit rejects identical bean overrides in
the Spring TestContext Framework.

Note, however, that it is still possible for a bean override to
override a logically equivalent bean override. For example, a
@⁠TestBean can override a @⁠MockitoBean, and vice versa.

Closes gh-34054
2024-12-09 16:00:24 +01:00
Sam Brannen 2137750591 Improve Bean Override by-name integration tests 2024-12-09 13:01:42 +01:00
Brian Clozel 13df9058a4 Introduce "unsafeAllocated" flag in TypeHint
This metadata information is required for supporting libraries using
`sun.misc.Unsafe#allocateInstance(Class<?>)`, even though Spring
Framework is not using this feature.

Closes gh-34055
2024-12-09 11:08:48 +01:00
Sam Brannen aee52b53a1 Introduce MockitoAssertions 2024-12-08 18:41:10 +01:00
Sam Brannen 92bbaa21e0 Improve test coverage for @⁠MockitoSpyBean use cases 2024-12-08 18:41:10 +01:00
Stéphane Nicoll 837579c2e5 Start building against Reactor 2024.0.1 snapshots
See gh-34051
2024-12-08 10:49:57 +01:00
Stéphane Nicoll 03a75cf03a Start building against Micrometer 1.14.2 snapshots
See gh-34050
2024-12-08 10:48:45 +01:00
Sam Brannen aa7b459803 Fix Phantom Read problem for Bean Overrides in the TestContext framework
To make an analogy to read phenomena for transactional databases, this
commit effectively fixes the "Phantom Read" problem for Bean Overrides.

A phantom read occurs when the BeanOverrideBeanFactoryPostProcessor
retrieves a set of bean names by-type twice and a new bean definition
for a compatible type has been created in the BeanFactory by a
BeanOverrideHandler between the first and second retrieval.

Continue reading for the details...

Prior to this commit, the injection of test Bean Overrides (for
example, when using @⁠MockitoBean) could fail in certain scenarios if
overrides were created for nonexistent beans "by type" without an
explicit name or qualifier. Specifically, if an override for a SubType
was created first, and subsequently an attempt was made to create an
override for a SuperType (where SubType extends SuperType), the
override for the SuperType would "override the override" for the
SubType, effectively removing the override for the SubType.
Consequently, injection of the override instance into the SubType field
would fail with an error message similar to the following.

BeanNotOfRequiredTypeException: Bean named 'Subtype#0' is expected to
be of type 'Subtype' but was actually of type 'Supertype$Mock$XHb7Aspo'

This commit addresses this issue by tracking all generated bean names
(in a generatedBeanNames set) and ensuring that a new bean override
instance is created for the current BeanOverrideHandler if a previous
BeanOverrideHandler already created a bean override instance that now
matches the type required by the current BeanOverrideHandler.

In other words, if the generatedBeanNames set already contains the
beanName that we just found by-type, we cannot "override the override",
because we would lose one of the overrides. Instead, we must create a
new override for the current handler. In the example given above, we
must end up with overrides for both SuperType and SubType.

Closes gh-34025
2024-12-07 16:51:16 +01:00
Sam Brannen 03fe1f0df3 Improve documentation for BeanOverrideBeanFactoryPostProcessor 2024-12-07 16:05:54 +01:00
Stéphane Nicoll 0d72477742 Restore user type in generated root bean definitions
This commit restores the user class in generated RootBeanDefinition
instances. Previously the CGLIB subclass was exposed. While this is
important in regular runtime as the configuration class parser operates
on the bean definition, this is not relevant for AOT as this information
is internal and captured in the instance supplier.

Closes gh-33960
2024-12-06 15:34:00 +01:00
Stéphane Nicoll d26ee8f852 Merge pull request #34031 from ngocnhan-tran1996
* pr/34031:
  Fix link to MockMvcBuilders in reference documentation

Closes gh-34031
2024-12-06 09:44:01 +01:00
Tran Ngoc Nhan 47c66f4352 Fix link to MockMvcBuilders in reference documentation
See gh-34031
2024-12-06 09:43:41 +01:00
Juergen Hoeller b5dd0a60f8 Restore lenient match against unresolvable wildcard
Closes gh-33982
2024-12-05 17:41:49 +01:00
Stéphane Nicoll e618f922c2 Resolve nested placeholders with a fallback having one
This commit fixes a regression in PlaceHolderParser where it would no
longer resolve nested placeholders for a case where the fallback has a
placeholder itself.

This is due to the Part implementations and how they are structure, and
this commit makes sure that nested resolution happens consistently.

Closes gh-34020
2024-12-05 16:59:30 +01:00
Stéphane Nicoll 81a9f3d50b Restore public type for generated instance supplier of CGLIB proxy
This commit restores the signature of instance suppliers that are
exposing a CGLIB proxy. While calling the CGLIB proxy itself, and
making it available in BeanInstanceSupplier, is needed internally, such
type should not be exposed as it is an internal concern.

This was breaking InstanceSupplier.andThen as it expects the public
type of the bean to be exposed, not it's eventual CGLIB subclass.

Closes gh-33998
2024-12-05 15:48:49 +01:00
Sébastien Deleuze c807fa597f Remove unnecessary HandshakeHandlerRuntimeHints
Those hints are not needed anymore as of Spring Framework 6.1.

Closes gh-34032
2024-12-05 15:44:09 +01:00
Juergen Hoeller 576f109987 Use JDK proxy for introduction interface without target
Closes gh-33985
2024-12-04 21:03:09 +01:00
Juergen Hoeller 384dc2a9b8 Consistently use singleton lock for FactoryBean processing
Closes gh-33972
2024-12-04 21:02:30 +01:00
Juergen Hoeller edf7f3cd43 Polishing 2024-12-04 16:41:07 +01:00
Juergen Hoeller 58c64cba2c Consistent fallback to NoUpgradeStrategyWebSocketService
Closes gh-33970
2024-12-04 16:39:41 +01:00
Juergen Hoeller 307411631d Ignore SQLFeatureNotSupportedException on releaseSavepoint
Closes gh-33987
2024-12-04 16:38:57 +01:00
Sam Brannen 8d69370b95 Consider logical equality in AdvisedSupport.MethodCacheKey#equals
Prior to this commit, the equals() implementation in AdvisedSupport's
MethodCacheKey only considered methods to be equal based on an identity
comparison (`==`), which led to duplicate entries in the method cache
for the same logical method.

This is caused by the fact that AdvisedSupport's
getInterceptorsAndDynamicInterceptionAdvice() method is invoked at
various stages with different Method instances for the same method:

1) when creating the proxy
2) when invoking the method via the proxy

The reason the Method instances are different is due to the following.

- Methods such as Class#getDeclaredMethods() and
  Class#getDeclaredMethod() always returns "child copies" of the
  underlying Method instances -- which means that `equals()` should be
  used instead of (or in addition to) `==` whenever the compared Method
  instances can come from different sources.

With this commit, the equals() implementation in MethodCacheKey now
considers methods equal based on identity or logical equality, giving
preference to the quicker identity check.

See gh-32586
Closes gh-33915
2024-12-04 12:04:02 +01:00
Brian Clozel d990449b0d Improve toString for reactive ScheduledTask
Prior to this commit, the reactive Scheduled tasks would be wrapped as a
`SubscribingRunnable` which does not implement a custom `toString`. This
would result in task metadata using the default Java `toString`
representation for those.

This commit ensures that the bean class name and method name are used
for this `toString`.

Closes gh-34010
2024-12-03 15:06:27 +01:00
rstoyanchev 15dcc449a2 Refine Reactor Netty handling of request without body
Closes gh-34003
2024-12-02 17:52:39 +00:00
Sam Brannen 320831b18a Test status quo for StaticMethodMatcherPointcut#matches invocations
This commit introduces a test which verifies how many times the
matches() method of a StaticMethodMatcherPointcut is invoked during
ApplicationContext startup as well as during actual method invocations
via the advice chain, which also indirectly tests the behavior of the
equals() implementation in AdvisedSupport.MethodCacheKey.

In addition, this commit revises BeanFactoryTransactionTests to assert
that a transaction is started for the setAge() method.

See gh-33915
2024-12-01 16:28:32 +01:00
Sam Brannen 172c8b2c35 Polish AOP tests 2024-12-01 15:32:03 +01:00
Sam Brannen 51956fad89 Test MockReset strategy for @⁠MockitoSpyBean
As a follow up to commit 0088b9c7f8, this commit introduces an
integration test which verifies that a spy created via @⁠MockitoSpyBean
using the MockReset.AFTER strategy is not reset between the refresh of
the ApplicationContext and the first use of the spy within a @⁠Test
method.

See gh-33941
See gh-33986
2024-11-29 11:27:02 +01:00
Sébastien Deleuze ddec8d2653 Add missing @Contract annotation to ObjectUtils#isEmpty
Closes gh-33984
2024-11-28 15:17:02 +01:00
Sébastien Deleuze 1aede291bb Move Kotlin value class unboxing to InvocableHandlerMethod
Before this commit, in Spring Framework 6.2, Kotlin value class
unboxing was done at CoroutinesUtils level, which is a good fit
for InvocableHandlerMethod use case, but not for other ones like
AopUtils.

This commit moves such unboxing to InvocableHandlerMethod in
order to keep the HTTP response body support while fixing other
regressions.

Closes gh-33943
2024-11-27 16:39:26 +01:00
Sam Brannen ea3bd7ae0c Polish BeanValidationBeanRegistrationAotProcessor[Tests]
The log message for a NoClassDefFoundError is now a DEBUG level message
handled like a TypeNotPresentException and similar to the following.

DEBUG: Skipping validation constraint hint inference for class
org.example.CustomConstraint due to a NoClassDefFoundError for
com.example.MissingType

See gh-33949
2024-11-27 12:53:51 +01:00
Stefano Cordio 9b0253e117 Skip runtime hint registration for constraint with missing dependencies
Prior to this commit, AOT processing for bean validation failed with a
NoClassDefFoundError for constraints with missing dependencies.

With this commit, the processing no longer fails, and a warning is
logged instead.

See gh-33940
Closes gh-33949

Co-authored-by: Sam Brannen <sam.brannen@broadcom.com>
2024-11-27 12:42:12 +01:00
Stéphane Nicoll f3753e6d64 Merge pull request #33956 from CHOICORE
* pr/33956:
  Fix log level in PathMatchingResourcePatternResolver

Closes gh-33956
2024-11-26 20:04:25 +01:00
CHOICORE 41421d106b Fix log level in PathMatchingResourcePatternResolver
See gh-33956
2024-11-26 19:51:17 +01:00
Tran Ngoc Nhan 8c0ac8e062 Fix a typo in the filters documentation
Closes gh-33959
2024-11-26 16:09:45 +01:00
Sam Brannen 0088b9c7f8 Honor MockReset strategy for @⁠MockitoBean and @⁠MockitoSpyBean
Commit 6c2cba5d8a introduced a regression by inadvertently removing the
MockReset strategy comparison when resetting @⁠MockitoBean and
@⁠MockitoSpyBean mocks.

This commit reinstates the MockReset strategy check and introduces
tests for this feature.

Closes gh-33941
2024-11-26 11:54:46 +01:00
Sam Brannen 2b840ee7ef Upgrade to Gradle 8.11.1
Closes gh-33951
2024-11-24 14:27:39 +01:00
Sam Brannen 051f1dac24 Polish contribution
See gh-33950
2024-11-24 14:13:03 +01:00
Stefano Cordio 5e7b3a3bed Avoid infinite recursion in BeanValidationBeanRegistrationAotProcessor
Prior to this commit, AOT processing for bean validation failed with a
StackOverflowError for constraints with fields having recursive generic
types.

With this commit, the algorithm tracks visited classes and aborts
preemptively when a cycle is detected.

Closes gh-33950

Co-authored-by: Sam Brannen <sam.brannen@broadcom.com>
2024-11-24 14:09:48 +01:00
Johnny Lim 1910d32405 Replace TestObservationRegistryAssert.assertThat() with Assertions.assertThat()
See https://github.com/micrometer-metrics/micrometer/pull/5551

Closes gh-33929
2024-11-21 14:35:32 +01:00
sonallux 986ffc2072 Use full URI for URI template keyvalue with RestClient
Prior to this commit, `RestClient` would not use the full URI created by
the uri handler as a template request attribute.
This means that HTTP client observations would not contain the base URI
in recorded observations as the uri template keyvalue.

Closes gh-33928
2024-11-21 14:23:01 +01:00
Sam Brannen b9cf03f8f0 Construct consistent error messages in BeanOverrideBeanFactoryPostProcessor 2024-11-21 11:28:31 +01:00
Sam Brannen 08a789cee9 Honor @⁠Fallback semantics for Test Bean Overrides
Closes gh-33924
2024-11-21 09:50:17 +01:00
Sam Brannen 7a6e401d17 Document visibility requirements for Bean Overrides
This commit makes it clear that there are no visibility requirements
for @⁠TestBean fields or factory methods as well as @⁠MockitoBean or
@⁠MockitoSpyBean fields.

Closes gh-33923
2024-11-20 16:49:52 +01:00
Sam Brannen 3569cfe990 Reject static Bean Override fields for @⁠MockitoBean, @⁠TestBean, etc.
Closes gh-33922
2024-11-20 11:29:01 +01:00
Simon Baslé 8b66d3c79a Upgrade Undertow-servlet/websockets to 2.3.18.Final
Follow up to 35b452b4 which only upgraded undertow-core.
2024-11-19 16:59:04 +01:00
Sam Brannen a3c132c442 Polish XmlExpectationsHelper[Tests] 2024-11-19 13:33:20 +01:00
boiarshinov 91791c1756 Fail with full description for XML diff in XmlExpectationsHelper
Closes gh-33827
2024-11-19 13:23:58 +01:00
Sam Brannen dd92eac3ad Refer to message "receipt" instead of "reception" 2024-11-19 13:18:12 +01:00
Sam Brannen 874f056984 Polishing 2024-11-19 13:18:12 +01:00
Tran Ngoc Nhan b77db64459 Fix typos and link in Observability documentation
Closes gh-33910
2024-11-19 12:12:01 +01:00
Simon Baslé d597d2e159 Fix WebClientIntegrationTest#applyAttributesToNativeRequest flaky test
For Reactor Netty and Reactor Netty 2 (with Netty 5), the attributes are
stored as an `Attribute` on the netty channel. Reactor Netty replaces
the channel with a static placeholder one once the request has been
processed and as such, capturing the native request in the test will
lead to asserting an attribute-less channel.

This change switches to capturing the `Attribute` itself, which is a
value-holder that can be asserted later on.

Closes gh-33909
2024-11-18 16:30:16 +01:00
Sam Brannen f5c3f3522e Simplify @⁠EnumSource usage 2024-11-18 14:12:13 +01:00
Sam Brannen d421f61a4a Polish @⁠DurationFormat Javadoc and tests 2024-11-18 14:12:13 +01:00
Tran Ngoc Nhan 883254e1d0 Polish
- Update copyright header.
- Consistent use of "SIMPLE" link text for Style#SIMPLE in javadoc.

Closes gh-33883
2024-11-18 13:40:23 +01:00
Brian Clozel e919d0adcc Reflect well-known MediaTypes intent in Javadoc
Closes gh-33754
2024-11-18 11:57:22 +01:00
Brian Clozel 4aafae1c33 Reflect well-known HttpHeaders intent in Javadoc
Closes gh-33886
2024-11-18 11:57:15 +01:00
Brian Clozel afef439c1f Temporarily disable flaky integration test
See gh-33909
2024-11-18 11:57:09 +01:00
Sam Brannen 807e1e6126 Document support for varargs invocations in SpEL
Closes gh-33332
2024-11-18 11:54:22 +01:00
Stéphane Nicoll 5ac56bda87 Remove unnecessary workflow in maintenance branch 2024-11-18 11:52:08 +01:00
Sam Brannen 7196f3f554 Polish SpEL documentation 2024-11-18 11:38:20 +01:00
Sam Brannen a12d40e10b Fix SpEL examples in reference guide
Closes gh-33907
2024-11-18 11:37:30 +01:00
Sam Brannen 7f7819329c Update copyright headers
See gh-33903
2024-11-17 15:14:16 +01:00
KNU-K 2494ecb47b Simplify utility implementations in spring-core
Closes gh-33903
2024-11-17 15:08:30 +01:00
Sam Brannen 241b8b48f2 Clarify requirements for AOP around advice regarding MethodInterceptor
Closes gh-33901
2024-11-17 12:23:01 +01:00
Sam Brannen 173084f81a Polish Spring AOP documentation 2024-11-17 12:21:16 +01:00
Sam Brannen 6544698078 Polish contribution
See gh-33902
2024-11-17 11:45:37 +01:00
taehyun e0e96c487f Simplify implementation of FilteredIterator
Closes gh-33902
2024-11-17 11:41:22 +01:00
Sam Brannen bb32df0a06 Upgrade to Gradle 8.11
Closes gh-33895
2024-11-15 16:06:14 +01:00
Brian Clozel 66c3aeee84 Polishing 2024-11-15 15:37:00 +01:00
Brian Clozel 828980bd44 Fix workflow configuration for 6.2.x branch 2024-11-15 15:35:32 +01:00
Sam Brannen fe8573c0ab Upgrade to antora-ui-spring version 0.4.18
Closes gh-33892
2024-11-15 14:32:01 +01:00
Brian Clozel 130627f630 Next development version (v6.2.1-SNAPSHOT) 2024-11-14 17:48:54 +01:00
201 changed files with 3889 additions and 1479 deletions
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
on:
push:
branches:
- main
- 6.2.x
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
+3 -2
View File
@@ -1,7 +1,8 @@
name: CI
on:
schedule:
- cron: '30 9 * * *'
push:
branches:
- 6.2.x
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
-79
View File
@@ -1,79 +0,0 @@
name: Release Milestone
on:
push:
tags:
- v6.2.0-M[1-9]
- v6.2.0-RC[1-9]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build-and-stage-release:
if: ${{ github.repository == 'spring-projects/spring-framework' }}
name: Build and Stage Release
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
with:
develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
/**/framework-api-*-docs.zip::zip.type=docs
/**/framework-api-*-schema.zip::zip.type=schema
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
folder: 'deployment-repository'
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository: 'libs-staging-local'
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
uri: 'https://repo.spring.io'
username: ${{ secrets.ARTIFACTORY_USERNAME }}
outputs:
version: ${{ steps.build-and-publish.outputs.version }}
verify:
name: Verify
needs: build-and-stage-release
uses: ./.github/workflows/verify.yml
with:
staging: true
version: ${{ needs.build-and-stage-release.outputs.version }}
secrets:
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
promote-release:
name: Promote Release
needs:
- build-and-stage-release
- verify
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local
create-github-release:
name: Create GitHub Release
needs:
- build-and-stage-release
- promote-release
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
milestone: ${{ needs.build-and-stage-release.outputs.version }}
pre-release: true
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
@@ -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.20.1");
checkstyle.setToolVersion("10.20.2");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+1 -1
View File
@@ -36,4 +36,4 @@ runtime:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
+1
View File
@@ -60,6 +60,7 @@
**** xref:core/expressions/language-ref/constructors.adoc[]
**** xref:core/expressions/language-ref/variables.adoc[]
**** xref:core/expressions/language-ref/functions.adoc[]
**** xref:core/expressions/language-ref/varargs.adoc[]
**** xref:core/expressions/language-ref/bean-references.adoc[]
**** xref:core/expressions/language-ref/operator-ternary.adoc[]
**** xref:core/expressions/language-ref/operator-elvis.adoc[]
@@ -33,11 +33,11 @@ arbitrary advice types. This section describes the basic concepts and standard a
[[aop-api-advice-around]]
=== Interception Around Advice
The most fundamental advice type in Spring is interception around advice.
The most fundamental advice type in Spring is _interception around advice_.
Spring is compliant with the AOP `Alliance` interface for around advice that uses method
interception. Classes that implement `MethodInterceptor` and that implement around advice should also implement the
following interface:
Spring is compliant with the AOP Alliance interface for around advice that uses method
interception. Classes that implement around advice should therefore implement the
following `MethodInterceptor` interface from the `org.aopalliance.intercept` package:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -49,8 +49,8 @@ following interface:
The `MethodInvocation` argument to the `invoke()` method exposes the method being
invoked, the target join point, the AOP proxy, and the arguments to the method. The
`invoke()` method should return the invocation's result: the return value of the join
point.
`invoke()` method should return the invocation's result: typically the return value of
the join point.
The following example shows a simple `MethodInterceptor` implementation:
@@ -64,9 +64,9 @@ Java::
public Object invoke(MethodInvocation invocation) throws Throwable {
System.out.println("Before: invocation=[" + invocation + "]");
Object rval = invocation.proceed();
Object result = invocation.proceed();
System.out.println("Invocation returned");
return rval;
return result;
}
}
----
@@ -79,9 +79,9 @@ Kotlin::
override fun invoke(invocation: MethodInvocation): Any {
println("Before: invocation=[$invocation]")
val rval = invocation.proceed()
val result = invocation.proceed()
println("Invocation returned")
return rval
return result
}
}
----
@@ -105,7 +105,7 @@ currently define pointcut interfaces.
[[aop-api-advice-before]]
=== Before Advice
A simpler advice type is a before advice. This does not need a `MethodInvocation`
A simpler advice type is a _before advice_. This does not need a `MethodInvocation`
object, since it is called only before entering the method.
The main advantage of a before advice is that there is no need to invoke the `proceed()`
@@ -122,10 +122,6 @@ The following listing shows the `MethodBeforeAdvice` interface:
}
----
(Spring's API design would allow for
field before advice, although the usual objects apply to field interception and it is
unlikely for Spring to ever implement it.)
Note that the return type is `void`. Before advice can insert custom behavior before the join
point runs but cannot change the return value. If a before advice throws an
exception, it stops further execution of the interceptor chain. The exception
@@ -176,10 +172,10 @@ TIP: Before advice can be used with any pointcut.
[[aop-api-advice-throws]]
=== Throws Advice
Throws advice is invoked after the return of the join point if the join point threw
_Throws advice_ is invoked after the return of the join point if the join point threw
an exception. Spring offers typed throws advice. Note that this means that the
`org.springframework.aop.ThrowsAdvice` interface does not contain any methods. It is a
tag interface identifying that the given object implements one or more typed throws
marker interface identifying that the given object implements one or more typed throws
advice methods. These should be in the following form:
[source,java,indent=0,subs="verbatim,quotes"]
@@ -189,9 +185,10 @@ advice methods. These should be in the following form:
Only the last argument is required. The method signatures may have either one or four
arguments, depending on whether the advice method is interested in the method and
arguments. The next two listing show classes that are examples of throws advice.
arguments. The next two listings show classes that are examples of throws advice.
The following advice is invoked if a `RemoteException` is thrown (including from subclasses):
The following advice is invoked if a `RemoteException` is thrown (including subclasses of
`RemoteException`):
[tabs]
======
@@ -220,9 +217,9 @@ Kotlin::
----
======
Unlike the preceding
advice, the next example declares four arguments, so that it has access to the invoked method, method
arguments, and target object. The following advice is invoked if a `ServletException` is thrown:
Unlike the preceding advice, the next example declares four arguments, so that it has
access to the invoked method, method arguments, and target object. The following advice
is invoked if a `ServletException` is thrown:
[tabs]
======
@@ -304,7 +301,7 @@ TIP: Throws advice can be used with any pointcut.
[[aop-api-advice-after-returning]]
=== After Returning Advice
An after returning advice in Spring must implement the
An _after returning advice_ in Spring must implement the
`org.springframework.aop.AfterReturningAdvice` interface, which the following listing shows:
[source,java,indent=0,subs="verbatim,quotes"]
@@ -368,7 +365,7 @@ TIP: After returning advice can be used with any pointcut.
[[aop-api-advice-introduction]]
=== Introduction Advice
Spring treats introduction advice as a special kind of interception advice.
Spring treats _introduction advice_ as a special kind of interception advice.
Introduction requires an `IntroductionAdvisor` and an `IntroductionInterceptor` that
implement the following interface:
@@ -3,8 +3,10 @@
You can invoke constructors by using the `new` operator. You should use the fully
qualified class name for all types except those located in the `java.lang` package
(`Integer`, `Float`, `String`, and so on). The following example shows how to use the
`new` operator to invoke constructors:
(`Integer`, `Float`, `String`, and so on).
xref:core/expressions/language-ref/varargs.adoc[Varargs] are also supported.
The following example shows how to use the `new` operator to invoke constructors.
[tabs]
======
@@ -12,30 +14,29 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
Inventor einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
Inventor einstein = parser.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor.class);
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor(
'Albert Einstein', 'German'))").getValue(societyContext);
parser.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
.getValue(societyContext);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val einstein = p.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
val einstein = parser.parseExpression(
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
.getValue(Inventor::class.java)
// create new Inventor instance within the add() method of List
p.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
parser.parseExpression(
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
.getValue(societyContext)
----
======
@@ -2,8 +2,12 @@
= Functions
You can extend SpEL by registering user-defined functions that can be called within
expressions by using the `#functionName(...)` syntax. Functions can be registered as
variables in `EvaluationContext` implementations via the `setVariable()` method.
expressions by using the `#functionName(...)` syntax, and like with standard method
invocations, xref:core/expressions/language-ref/varargs.adoc[varargs] are also supported
for function invocations.
Functions can be registered as _variables_ in `EvaluationContext` implementations via the
`setVariable()` method.
[TIP]
====
@@ -110,8 +114,9 @@ potentially more efficient use cases if the `MethodHandle` target and parameters
been fully bound prior to registration; however, partially bound handles are also
supported.
Consider the `String#formatted(String, Object...)` instance method, which produces a
message according to a template and a variable number of arguments.
Consider the `String#formatted(Object...)` instance method, which produces a message
according to a template and a variable number of arguments
(xref:core/expressions/language-ref/varargs.adoc[varargs]).
You can register and use the `formatted` method as a `MethodHandle`, as the following
example shows:
@@ -151,10 +156,10 @@ Kotlin::
----
======
As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also
supported. This is likely to be more performant if both the target and all the arguments
are bound. In that case no arguments are necessary in the SpEL expression, as the
following example shows:
As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is
also supported. This is likely to be more performant if both the target and all the
arguments are bound. In that case no arguments are necessary in the SpEL expression, as
the following example shows:
[tabs]
======
@@ -168,9 +173,10 @@ Java::
String template = "This is a %s message with %s words: <%s>";
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class))
MethodType.methodType(String.class, Object[].class))
.bindTo(template)
.bindTo(varargs); //here we have to provide arguments in a single array binding
// Here we have to provide the arguments in a single array binding:
.bindTo(varargs);
context.setVariable("message", mh);
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
@@ -189,9 +195,10 @@ Kotlin::
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
MethodType.methodType(String::class.java, Array<Any>::class.java))
MethodType.methodType(String::class.java, Array<Any>::class.java))
.bindTo(template)
.bindTo(varargs) //here we have to provide arguments in a single array binding
// Here we have to provide the arguments in a single array binding:
.bindTo(varargs)
context.setVariable("message", mh)
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
@@ -201,4 +208,3 @@ Kotlin::
======
@@ -1,9 +1,11 @@
[[expressions-methods]]
= Methods
You can invoke methods by using typical Java programming syntax. You can also invoke methods
on literals. Variable arguments are also supported. The following examples show how to
invoke methods:
You can invoke methods by using the typical Java programming syntax. You can also invoke
methods directly on literals such as strings or numbers.
xref:core/expressions/language-ref/varargs.adoc[Varargs] are supported as well.
The following examples show how to invoke methods.
[tabs]
======
@@ -0,0 +1,151 @@
[[expressions-varargs]]
= Varargs Invocations
The Spring Expression Language supports
https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html[varargs]
invocations for xref:core/expressions/language-ref/constructors.adoc[constructors],
xref:core/expressions/language-ref/methods.adoc[methods], and user-defined
xref:core/expressions/language-ref/functions.adoc[functions].
The following example shows how to invoke the `java.lang.String#formatted(Object...)`
_varargs_ method within an expression by supplying the variable argument list as separate
arguments (`'blue', 1`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted('blue', 1)";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%d'.formatted('blue', 1)"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======
A variable argument list can also be supplied as an array, as demonstrated in the
following example (`new Object[] {'blue', 1}`).
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======
As an alternative, a variable argument list can be supplied as a `java.util.List` for
example, as an xref:core/expressions/language-ref/inline-lists.adoc[inline list]
(`{'blue', 1}`). The following example shows how to do that.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted({'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%d'.formatted({'blue', 1})"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======
[[expressions-varargs-type-conversion]]
== Varargs Type Conversion
In contrast to the standard support for varargs invocations in Java,
xref:core/expressions/evaluation.adoc#expressions-type-conversion[type conversion] may be
applied to the individual arguments when invoking varargs constructors, methods, or
functions in SpEL.
For example, if we have registered a custom
xref:core/expressions/language-ref/functions.adoc[function] in the `EvaluationContext`
under the name `#reverseStrings` for a method with the signature
`String reverseStrings(String... strings)`, we can invoke that function within a SpEL
expression with any argument that can be converted to a `String`, as demonstrated in the
following example.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "3.0, 2.0, 1, SpEL"
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
String message = parser.parseExpression(expression)
.getValue(evaluationContext, String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "3.0, 2.0, 1, SpEL"
val expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)"
val message = parser.parseExpression(expression)
.getValue(evaluationContext, String::class.java)
----
======
Similarly, any array whose component type is a subtype of the required varargs type can
be supplied as the variable argument list for a varargs invocation. For example, a
`String[]` array can be supplied to a varargs invocation that accepts an `Object...`
argument list.
The following listing demonstrates that we can supply a `String[]` array to the
`java.lang.String#formatted(Object...)` _varargs_ method. It also highlights that `1`
will be automatically converted to `"1"`.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// evaluates to "blue is color #1"
val expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})"
val message = parser.parseExpression(expression).getValue(String::class.java)
----
======
@@ -6,7 +6,7 @@ the same way as Spring's integration does for the JDBC API.
JMS can be roughly divided into two areas of functionality, namely the production and
consumption of messages. The `JmsTemplate` class is used for message production and
synchronous message reception. For asynchronous reception similar to Jakarta EE's
synchronous message receipt. For asynchronous receipt similar to Jakarta EE's
message-driven bean style, Spring provides a number of message-listener containers that
you can use to create Message-Driven POJOs (MDPs). Spring also provides a declarative way
to create message listeners.
@@ -5,7 +5,7 @@ This describes how to receive messages with JMS in Spring.
[[jms-receiving-sync]]
== Synchronous Reception
== Synchronous Receipt
While JMS is typically associated with asynchronous processing, you can
consume messages synchronously. The overloaded `receive(..)` methods provide this
@@ -16,11 +16,11 @@ the receiver should wait before giving up waiting for a message.
[[jms-receiving-async]]
== Asynchronous reception: Message-Driven POJOs
== Asynchronous Receipt: Message-Driven POJOs
NOTE: Spring also supports annotated-listener endpoints through the use of the `@JmsListener`
annotation and provides an open infrastructure to register endpoints programmatically.
This is, by far, the most convenient way to setup an asynchronous receiver.
annotation and provides open infrastructure to register endpoints programmatically.
This is, by far, the most convenient way to set up an asynchronous receiver.
See xref:integration/jms/annotated.adoc#jms-annotated-support[Enable Listener Endpoint Annotations] for more details.
In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Message-Driven
@@ -154,7 +154,7 @@ listener container.
You can activate local resource transactions through the `sessionTransacted` flag
on the listener container definition. Each message listener invocation then operates
within an active JMS transaction, with message reception rolled back in case of listener
within an active JMS transaction, with message receipt rolled back in case of listener
execution failure. Sending a response message (through `SessionAwareMessageListener`) is
part of the same local transaction, but any other resource operations (such as
database access) operate independently. This usually requires duplicate message
@@ -173,7 +173,7 @@ To configure a message listener container for XA transaction participation, you
to configure a `JtaTransactionManager` (which, by default, delegates to the Jakarta EE
server's transaction subsystem). Note that the underlying JMS `ConnectionFactory` needs to
be XA-capable and properly registered with your JTA transaction coordinator. (Check your
Jakarta EE server's configuration of JNDI resources.) This lets message reception as well
Jakarta EE server's configuration of JNDI resources.) This lets message receipt as well
as (for example) database access be part of the same transaction (with unified commit
semantics, at the expense of XA transaction log overhead).
@@ -167,13 +167,15 @@ operations that do not refer to a specific destination.
One of the most common uses of JMS messages in the EJB world is to drive message-driven
beans (MDBs). Spring offers a solution to create message-driven POJOs (MDPs) in a way
that does not tie a user to an EJB container. (See xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous reception: Message-Driven POJOs] for detailed
coverage of Spring's MDP support.) Since Spring Framework 4.1, endpoint methods can be
annotated with `@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints] for more details.
that does not tie a user to an EJB container. (See
xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous Receipt: Message-Driven POJOs]
for detailed coverage of Spring's MDP support.) Endpoint methods can be annotated with
`@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints]
for more details.
A message listener container is used to receive messages from a JMS message queue and
drive the `MessageListener` that is injected into it. The listener container is
responsible for all threading of message reception and dispatches into the listener for
responsible for all threading of message receipt and dispatches into the listener for
processing. A message listener container is the intermediary between an MDP and a
messaging provider and takes care of registering to receive messages, participating in
transactions, resource acquisition and release, exception conversion, and so on. This
@@ -227,7 +229,7 @@ the JMS provider, advanced functionality (such as participation in externally ma
transactions), and compatibility with Jakarta EE environments.
You can customize the cache level of the container. Note that, when no caching is enabled,
a new connection and a new session is created for each message reception. Combining this
a new connection and a new session is created for each message receipt. Combining this
with a non-durable subscription with high loads may lead to message loss. Make sure to
use a proper cache level in such a case.
@@ -246,7 +248,7 @@ in the form of a business entity existence check or a protocol table check.
Any such arrangements are significantly more efficient than the alternative:
wrapping your entire processing with an XA transaction (through configuring your
`DefaultMessageListenerContainer` with an `JtaTransactionManager`) to cover the
reception of the JMS message as well as the execution of the business logic in your
receipt of the JMS message as well as the execution of the business logic in your
message listener (including database operations, etc.).
IMPORTANT: The default `AUTO_ACKNOWLEDGE` mode does not provide proper reliability guarantees.
@@ -37,7 +37,7 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
|Processing time for an execution of a `@Scheduled` task
|===
NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
NOTE: Observations use Micrometer's official naming convention, but Metrics names will be automatically converted
{micrometer-docs}/concepts/naming.html[to the format preferred by the monitoring system backend]
(Prometheus, Atlas, Graphite, InfluxDB...).
@@ -97,7 +97,7 @@ This can be done by declaring a `SchedulingConfigurer` bean that sets the observ
include-code::./ObservationSchedulingConfigurer[]
It is using the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
It uses the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
You can configure a custom implementation on the `ObservationRegistry` directly.
During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type).
@@ -107,7 +107,7 @@ By default, the following `KeyValues` are created:
[cols="a,a"]
|===
|Name | Description
|`code.function` _(required)_|Name of Java `Method` that is scheduled for execution.
|`code.function` _(required)_|Name of the Java `Method` that is scheduled for execution.
|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method, or `"ANONYMOUS"` for anonymous classes.
|`error` _(required)_|Class name of the exception thrown during the execution, or `"none"` if no exception happened.
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
@@ -126,7 +126,7 @@ This instrumentation will create 2 types of observations:
* `"jms.message.publish"` when a JMS message is sent to the broker, typically with `JmsTemplate`.
* `"jms.message.process"` when a JMS message is processed by the application, typically with a `MessageListener` or a `@JmsListener` annotated method.
NOTE: currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the reception of a message.
NOTE: Currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the receipt of a message.
Such an integration would typically instrument `MessageConsumer#receive` method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application.
By default, both observations share the same set of possible `KeyValues`:
@@ -138,7 +138,7 @@ By default, both observations share the same set of possible `KeyValues`:
|`error` |Class name of the exception thrown during the messaging operation (or "none").
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|`messaging.destination.temporary` _(required)_|Whether the destination is a `TemporaryQueue` or `TemporaryTopic` (values: `"true"` or `"false"`).
|`messaging.operation` _(required)_|Name of JMS operation being performed (values: `"publish"` or `"process"`).
|`messaging.operation` _(required)_|Name of the JMS operation being performed (values: `"publish"` or `"process"`).
|===
.High cardinality Keys
@@ -146,7 +146,7 @@ By default, both observations share the same set of possible `KeyValues`:
|===
|Name | Description
|`messaging.message.conversation_id` |The correlation ID of the JMS message.
|`messaging.destination.name` |The name of destination the current message was sent to.
|`messaging.destination.name` |The name of the destination the current message was sent to.
|`messaging.message.id` |Value used by the messaging system as an identifier for the message.
|===
@@ -213,7 +213,7 @@ By default, the following `KeyValues` are created:
|Name | Description
|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened.
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
@@ -235,10 +235,10 @@ This can be done on the `WebHttpHandlerBuilder`, as follows:
include-code::./HttpHandlerConfiguration[]
It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
This will only record an observation as an error if the `Exception` has not been handled by an application Controller.
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and <<web.adoc#webflux-ann-rest-exceptions,`ProblemDetail` support>> will not be recorded with the observation.
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and xref:web/webflux/ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
include-code::./UserController[]
@@ -251,7 +251,7 @@ By default, the following `KeyValues` are created:
|Name | Description
|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened.
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
@@ -284,7 +284,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
[cols="a,a"]
|===
|Name | Description
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|`client.name` _(required)_|Client name derived from the request URI host.
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
@@ -312,7 +312,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
[cols="a,a"]
|===
|Name | Description
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
|`method` _(required)_|Name of the HTTP request method or `"none"` if the request could not be created.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|`client.name` _(required)_|Client name derived from the request URI host.
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
@@ -332,7 +332,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
[[observability.http-client.webclient]]
=== WebClient
Applications must configure an `ObservationRegistry` on the `WebClient` builder to enable the instrumentation; without that, observations are "no-ops".
Applications must configure an `ObservationRegistry` on the `WebClient.Builder` to enable the instrumentation; without that, observations are "no-ops".
Spring Boot will auto-configure `WebClient.Builder` beans with the observation registry already set.
Instrumentation uses the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
@@ -341,7 +341,7 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien
[cols="a,a"]
|===
|Name | Description
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|`client.name` _(required)_|Client name derived from the request URI host.
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
@@ -1,9 +1,10 @@
[[spring-testing-annotation-beanoverriding-mockitobean]]
= `@MockitoBean` and `@MockitoSpyBean`
`@MockitoBean` and `@MockitoSpyBean` are used on 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.
`@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.
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
@@ -51,6 +52,15 @@ When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
====
[NOTE]
====
There are no restrictions on the visibility of `@MockitoBean` and `@MockitoSpyBean`
fields.
Such fields can therefore be `public`, `protected`, package-private (default visibility),
or `private` depending on the needs or coding practices of the project.
====
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
[tabs]
@@ -61,7 +71,7 @@ Java::
----
class OverrideBeanTests {
@MockitoBean // <1>
private CustomService customService;
CustomService customService;
// test case body...
}
@@ -85,7 +95,7 @@ Java::
----
class OverrideBeanTests {
@MockitoBean("service") // <1>
private CustomService customService;
CustomService customService;
// test case body...
@@ -106,7 +116,7 @@ Java::
----
class OverrideBeanTests {
@MockitoSpyBean // <1>
private CustomService customService;
CustomService customService;
// test case body...
}
@@ -129,7 +139,7 @@ Java::
----
class OverrideBeanTests {
@MockitoSpyBean("service") // <1>
private CustomService customService;
CustomService customService;
// test case body...
@@ -1,8 +1,8 @@
[[spring-testing-annotation-beanoverriding-testbean]]
= `@TestBean`
`@TestBean` is used on a field in a test class to override a specific bean in the test's
`ApplicationContext` with an instance provided by a factory method.
`@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
@@ -30,6 +30,14 @@ same bean in several tests, make sure to name the field consistently to avoid cr
unnecessary contexts.
====
[NOTE]
====
There are no restrictions on the visibility of `@TestBean` fields or factory methods.
Such fields and methods can therefore be `public`, `protected`, package-private (default
visibility), or `private` depending on the needs or coding practices of the project.
====
The following example shows how to use the default behavior of the `@TestBean` annotation:
[tabs]
@@ -40,11 +48,11 @@ Java::
----
class OverrideBeanTests {
@TestBean // <1>
private CustomService customService;
CustomService customService;
// test case body...
private static CustomService customService() { // <2>
static CustomService customService() { // <2>
return new MyFakeCustomService();
}
}
@@ -68,11 +76,11 @@ Java::
----
class OverrideBeanTests {
@TestBean(name = "service", methodName = "createCustomService") // <1>
private CustomService customService;
CustomService customService;
// test case body...
private static CustomService createCustomService() { // <2>
static CustomService createCustomService() { // <2>
return new MyFakeCustomService();
}
}
@@ -2,7 +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 fields in the test class.
`ApplicationContext` for a test class, by annotating 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`
@@ -41,9 +42,10 @@ 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 field meta-annotated
with `@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
responsible for creating an appropriate `BeanOverrideHandler`.
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 internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
@@ -127,7 +127,7 @@ Kotlin::
======
For Spring MVC, use the following where the Spring `ApplicationContext` is passed to
{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup]
{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup(org.springframework.web.context.WebApplicationContext)[MockMvcBuilders.webAppContextSetup]
to create a xref:testing/mockmvc.adoc[MockMvc] instance to handle
requests:
@@ -30,7 +30,7 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method
[[forwarded-headers]]
[[filters-forwarded-headers]]
== Forwarded Headers
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]#
+6 -6
View File
@@ -7,11 +7,11 @@ javaPlatform {
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.1"))
api(platform("io.micrometer:micrometer-bom:1.14.0"))
api(platform("com.fasterxml.jackson:jackson-bom:2.18.2"))
api(platform("io.micrometer:micrometer-bom:1.14.2"))
api(platform("io.netty:netty-bom:4.1.115.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.0"))
api(platform("io.projectreactor:reactor-bom:2024.0.1"))
api(platform("io.rsocket:rsocket-bom:1.1.4"))
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
@@ -54,11 +54,11 @@ dependencies {
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
api("io.reactivex.rxjava3:rxjava:3.1.9")
api("io.reactivex.rxjava3:rxjava:3.1.10")
api("io.smallrye.reactive:mutiny:1.10.0")
api("io.undertow:undertow-core:2.3.18.Final")
api("io.undertow:undertow-servlet:2.3.17.Final")
api("io.undertow:undertow-websockets-jsr:2.3.17.Final")
api("io.undertow:undertow-servlet:2.3.18.Final")
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
api("io.vavr:vavr:0.10.4")
api("jakarta.activation:jakarta.activation-api:2.0.1")
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.0
version=6.2.1
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2008 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,11 +27,11 @@ package org.springframework.aop;
* <p>Some examples of valid methods would be:
*
* <pre class="code">public void afterThrowing(Exception ex)</pre>
* <pre class="code">public void afterThrowing(RemoteException)</pre>
* <pre class="code">public void afterThrowing(RemoteException ex)</pre>
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, Exception ex)</pre>
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)</pre>
*
* The first three arguments are optional, and only useful if we want further
* <p>The first three arguments are optional, and only useful if we want further
* information about the joinpoint, as in AspectJ <b>after-throwing</b> advice.
*
* <p><b>Note:</b> If a throws-advice method throws an exception itself, it will
@@ -668,7 +668,8 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
@Override
public boolean equals(@Nullable Object other) {
return (this == other || (other instanceof MethodCacheKey that && this.method == that.method));
return (this == other || (other instanceof MethodCacheKey that &&
(this.method == that.method || this.method.equals(that.method))));
}
@Override
@@ -60,11 +60,12 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || !config.hasUserSuppliedInterfaces()) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
if (targetClass == null && config.getProxiedInterfaces().length == 0) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
if (targetClass == null || targetClass.isInterface() ||
Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
@@ -340,6 +340,18 @@ class ProxyFactoryTests {
assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(MyDate.class);
}
@Test
void proxyInterfaceInCaseOfIntroducedInterfaceOnly() {
ProxyFactory pf = new ProxyFactory();
pf.addInterface(TimeStamped.class);
TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(0L);
pf.addAdvisor(new DefaultIntroductionAdvisor(ti, TimeStamped.class));
Object proxy = pf.getProxy();
assertThat(AopUtils.isJdkDynamicProxy(proxy)).as("Proxy is a JDK proxy").isTrue();
assertThat(proxy).isInstanceOf(TimeStamped.class);
assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(proxy.getClass());
}
@Test
void proxyInterfaceInCaseOfNonTargetInterface() {
ProxyFactory pf = new ProxyFactory();
@@ -123,7 +123,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
CodeBlock.Builder code = CodeBlock.builder();
RootBeanDefinition mbd = this.registeredBean.getMergedBeanDefinition();
Class<?> beanClass = (mbd.hasBeanClass() ? mbd.getBeanClass() : null);
Class<?> beanClass = (mbd.hasBeanClass() ? ClassUtils.getUserClass(mbd.getBeanClass()) : null);
CodeBlock beanClassCode = generateBeanClassCode(
beanRegistrationCode.getClassName().packageName(),
(beanClass != null ? beanClass : beanType.toClass()));
@@ -156,91 +156,96 @@ public class InstanceSupplierCodeGenerator {
}
private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Constructor<?> constructor) {
String beanName = registeredBean.getBeanName();
Class<?> beanClass = registeredBean.getBeanClass();
ConstructorDescriptor descriptor = new ConstructorDescriptor(
registeredBean.getBeanName(), constructor, registeredBean.getBeanClass());
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) {
return generateCodeForInaccessibleConstructor(beanName, constructor,
hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
Class<?> publicType = descriptor.publicType();
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) {
return generateCodeForInaccessibleConstructor(descriptor,
hints -> hints.registerType(publicType, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
}
if (!isVisible(constructor, constructor.getDeclaringClass())) {
return generateCodeForInaccessibleConstructor(beanName, constructor,
return generateCodeForInaccessibleConstructor(descriptor,
hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE));
}
return generateCodeForAccessibleConstructor(beanName, constructor);
return generateCodeForAccessibleConstructor(descriptor);
}
private CodeBlock generateCodeForAccessibleConstructor(String beanName, Constructor<?> constructor) {
private CodeBlock generateCodeForAccessibleConstructor(ConstructorDescriptor descriptor) {
Constructor<?> constructor = descriptor.constructor();
this.generationContext.getRuntimeHints().reflection().registerConstructor(
constructor, ExecutableMode.INTROSPECT);
if (constructor.getParameterCount() == 0) {
if (!this.allowDirectSupplierShortcut) {
return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, constructor.getDeclaringClass());
return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, descriptor.actualType());
}
if (!isThrowingCheckedException(constructor)) {
return CodeBlock.of("$T::new", constructor.getDeclaringClass());
return CodeBlock.of("$T::new", descriptor.actualType());
}
return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, constructor.getDeclaringClass());
return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, descriptor.actualType());
}
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method ->
buildGetInstanceMethodForConstructor(method, beanName, constructor, PRIVATE_STATIC));
buildGetInstanceMethodForConstructor(method, descriptor, PRIVATE_STATIC));
return generateReturnStatement(generatedMethod);
}
private CodeBlock generateCodeForInaccessibleConstructor(String beanName,
Constructor<?> constructor, Consumer<ReflectionHints> hints) {
private CodeBlock generateCodeForInaccessibleConstructor(ConstructorDescriptor descriptor,
Consumer<ReflectionHints> hints) {
Constructor<?> constructor = descriptor.constructor();
CodeWarnings codeWarnings = new CodeWarnings();
codeWarnings.detectDeprecation(constructor.getDeclaringClass(), constructor)
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
hints.accept(this.generationContext.getRuntimeHints().reflection());
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName());
method.addModifiers(PRIVATE_STATIC);
codeWarnings.suppress(method);
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, constructor.getDeclaringClass()));
method.addStatement(generateResolverForConstructor(constructor));
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, descriptor.publicType()));
method.addStatement(generateResolverForConstructor(descriptor));
});
return generateReturnStatement(generatedMethod);
}
private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, String beanName,
Constructor<?> constructor, javax.lang.model.element.Modifier... modifiers) {
private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, ConstructorDescriptor descriptor,
javax.lang.model.element.Modifier... modifiers) {
Class<?> declaringClass = constructor.getDeclaringClass();
Constructor<?> constructor = descriptor.constructor();
Class<?> publicType = descriptor.publicType();
Class<?> actualType = descriptor.actualType();
CodeWarnings codeWarnings = new CodeWarnings();
codeWarnings.detectDeprecation(declaringClass, constructor)
codeWarnings.detectDeprecation(actualType, constructor)
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName());
method.addModifiers(modifiers);
codeWarnings.suppress(method);
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, declaringClass));
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, publicType));
CodeBlock.Builder code = CodeBlock.builder();
code.add(generateResolverForConstructor(constructor));
code.add(generateResolverForConstructor(descriptor));
boolean hasArguments = constructor.getParameterCount() > 0;
boolean onInnerClass = ClassUtils.isInnerClass(declaringClass);
boolean onInnerClass = ClassUtils.isInnerClass(actualType);
CodeBlock arguments = hasArguments ?
new AutowiredArgumentsCodeGenerator(declaringClass, constructor)
new AutowiredArgumentsCodeGenerator(actualType, constructor)
.generateCode(constructor.getParameterTypes(), (onInnerClass ? 1 : 0))
: NO_ARGS;
CodeBlock newInstance = generateNewInstanceCodeForConstructor(declaringClass, arguments);
CodeBlock newInstance = generateNewInstanceCodeForConstructor(actualType, arguments);
code.add(generateWithGeneratorCode(hasArguments, newInstance));
method.addStatement(code.build());
}
private CodeBlock generateResolverForConstructor(Constructor<?> constructor) {
CodeBlock parameterTypes = generateParameterTypesCode(constructor.getParameterTypes());
private CodeBlock generateResolverForConstructor(ConstructorDescriptor descriptor) {
CodeBlock parameterTypes = generateParameterTypesCode(descriptor.constructor().getParameterTypes());
return CodeBlock.of("return $T.<$T>forConstructor($L)", BeanInstanceSupplier.class,
constructor.getDeclaringClass(), parameterTypes);
descriptor.publicType(), parameterTypes);
}
private CodeBlock generateNewInstanceCodeForConstructor(Class<?> declaringClass, CodeBlock args) {
@@ -438,4 +443,11 @@ public class InstanceSupplierCodeGenerator {
}
}
record ConstructorDescriptor(String beanName, Constructor<?> constructor, Class<?> publicType) {
Class<?> actualType() {
return this.constructor.getDeclaringClass();
}
}
}
@@ -991,54 +991,60 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
*/
@Nullable
private FactoryBean<?> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName);
if (bw != null) {
return (FactoryBean<?>) bw.getWrappedInstance();
}
Object beanInstance = getSingleton(beanName, false);
if (beanInstance instanceof FactoryBean<?> factoryBean) {
return factoryBean;
}
if (isSingletonCurrentlyInCreation(beanName) ||
(mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) {
return null;
}
Object instance;
this.singletonLock.lock();
try {
// Mark this bean as currently in creation, even if just partially.
beforeSingletonCreation(beanName);
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
instance = resolveBeforeInstantiation(beanName, mbd);
if (instance == null) {
bw = createBeanInstance(beanName, mbd, null);
instance = bw.getWrappedInstance();
this.factoryBeanInstanceCache.put(beanName, bw);
BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName);
if (bw != null) {
return (FactoryBean<?>) bw.getWrappedInstance();
}
}
catch (UnsatisfiedDependencyException ex) {
// Don't swallow, probably misconfiguration...
throw ex;
}
catch (BeanCreationException ex) {
// Don't swallow a linkage error since it contains a full stacktrace on
// first occurrence... and just a plain NoClassDefFoundError afterwards.
if (ex.contains(LinkageError.class)) {
Object beanInstance = getSingleton(beanName, false);
if (beanInstance instanceof FactoryBean<?> factoryBean) {
return factoryBean;
}
if (isSingletonCurrentlyInCreation(beanName) ||
(mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) {
return null;
}
Object instance;
try {
// Mark this bean as currently in creation, even if just partially.
beforeSingletonCreation(beanName);
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
instance = resolveBeforeInstantiation(beanName, mbd);
if (instance == null) {
bw = createBeanInstance(beanName, mbd, null);
instance = bw.getWrappedInstance();
this.factoryBeanInstanceCache.put(beanName, bw);
}
}
catch (UnsatisfiedDependencyException ex) {
// Don't swallow, probably misconfiguration...
throw ex;
}
// Instantiation failure, maybe too early...
if (logger.isDebugEnabled()) {
logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex);
catch (BeanCreationException ex) {
// Don't swallow a linkage error since it contains a full stacktrace on
// first occurrence... and just a plain NoClassDefFoundError afterwards.
if (ex.contains(LinkageError.class)) {
throw ex;
}
// Instantiation failure, maybe too early...
if (logger.isDebugEnabled()) {
logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex);
}
onSuppressedException(ex);
return null;
}
onSuppressedException(ex);
return null;
finally {
// Finished partial creation of this bean.
afterSingletonCreation(beanName);
}
return getFactoryBean(beanName, instance);
}
finally {
// Finished partial creation of this bean.
afterSingletonCreation(beanName);
this.singletonLock.unlock();
}
return getFactoryBean(beanName, instance);
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -54,6 +54,22 @@ public class BeanDefinitionOverrideException extends BeanDefinitionStoreExceptio
this.existingDefinition = existingDefinition;
}
/**
* Create a new BeanDefinitionOverrideException for the given new and existing definition.
* @param beanName the name of the bean
* @param beanDefinition the newly registered bean definition
* @param existingDefinition the existing bean definition for the same name
* @param msg the detail message to include
* @since 6.2.1
*/
public BeanDefinitionOverrideException(
String beanName, BeanDefinition beanDefinition, BeanDefinition existingDefinition, String msg) {
super(beanDefinition.getResourceDescription(), beanName, msg);
this.beanDefinition = beanDefinition;
this.existingDefinition = existingDefinition;
}
/**
* Return the description of the resource that the bean definition came from.
@@ -1170,6 +1170,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
else {
if (logger.isInfoEnabled()) {
logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName +
"' due to registration of bean definition for bean '" + beanName + "': [" +
beanDefinition + "]");
}
removeAlias(beanName);
}
}
@@ -76,6 +76,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
/** Common lock for singleton creation. */
final Lock singletonLock = new ReentrantLock();
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
@@ -91,8 +94,6 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
/** Set of registered singletons, containing the bean names in registration order. */
private final Set<String> registeredSingletons = Collections.synchronizedSet(new LinkedHashSet<>(256));
private final Lock singletonLock = new ReentrantLock();
/** Names of beans that are currently in creation. */
private final Set<String> singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet(16);
@@ -118,39 +118,45 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
*/
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
if (factory.isSingleton() && containsSingleton(beanName)) {
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
object = doGetObjectFromFactoryBean(factory, beanName);
// Only post-process and store if not put there already during getObject() call above
// (for example, because of circular reference processing triggered by custom getBean calls)
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
object = alreadyThere;
}
else {
if (shouldPostProcess) {
if (isSingletonCurrentlyInCreation(beanName)) {
// Temporarily return non-post-processed object, not storing it yet
return object;
this.singletonLock.lock();
try {
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
object = doGetObjectFromFactoryBean(factory, beanName);
// Only post-process and store if not put there already during getObject() call above
// (for example, because of circular reference processing triggered by custom getBean calls)
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
if (alreadyThere != null) {
object = alreadyThere;
}
else {
if (shouldPostProcess) {
if (isSingletonCurrentlyInCreation(beanName)) {
// Temporarily return non-post-processed object, not storing it yet
return object;
}
beforeSingletonCreation(beanName);
try {
object = postProcessObjectFromFactoryBean(object, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName,
"Post-processing of FactoryBean's singleton object failed", ex);
}
finally {
afterSingletonCreation(beanName);
}
}
beforeSingletonCreation(beanName);
try {
object = postProcessObjectFromFactoryBean(object, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(beanName,
"Post-processing of FactoryBean's singleton object failed", ex);
}
finally {
afterSingletonCreation(beanName);
if (containsSingleton(beanName)) {
this.factoryBeanObjectCache.put(beanName, object);
}
}
if (containsSingleton(beanName)) {
this.factoryBeanObjectCache.put(beanName, object);
}
}
return object;
}
finally {
this.singletonLock.unlock();
}
return object;
}
else {
Object object = doGetObjectFromFactoryBean(factory, beanName);
@@ -870,10 +870,15 @@ class DefaultListableBeanFactoryTests {
void beanDefinitionOverriding() {
lbf.setAllowBeanDefinitionOverriding(true);
lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class));
// Override "test" bean definition.
lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class));
// Temporary "test2" alias for nonexistent bean.
lbf.registerAlias("otherTest", "test2");
// Reassign "test2" alias to "test".
lbf.registerAlias("test", "test2");
// Assign "testX" alias to "test" as well.
lbf.registerAlias("test", "testX");
// Register new "testX" bean definition which also removes the "testX" alias for "test".
lbf.registerBeanDefinition("testX", new RootBeanDefinition(TestBean.class));
assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class);
@@ -36,10 +36,10 @@ import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader;
import org.springframework.beans.factory.parsing.SourceExtractor;
import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
import org.springframework.beans.factory.support.BeanDefinitionReader;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
@@ -297,13 +297,21 @@ class ConfigurationClassBeanDefinitionReader {
return false;
}
BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName);
ConfigurationClass configClass = beanMethod.getConfigurationClass();
// If the bean method is an overloaded case on the same configuration class,
// preserve the existing bean definition and mark it as overloaded.
if (existingBeanDef instanceof ConfigurationClassBeanDefinition ccbd) {
if (ccbd.getMetadata().getClassName().equals(beanMethod.getConfigurationClass().getMetadata().getClassName()) &&
ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) {
ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName());
if (ccbd.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) {
if (ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) {
ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName());
}
else if (!this.registry.isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionOverrideException(beanName,
new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName),
existingBeanDef,
"@Bean method override with same bean name but different method name: " + existingBeanDef);
}
return true;
}
else {
@@ -329,9 +337,11 @@ class ConfigurationClassBeanDefinitionReader {
// At this point, it's a top-level override (probably XML), just having been parsed
// before configuration class processing kicks in...
if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
if (!this.registry.isBeanDefinitionOverridable(beanName)) {
throw new BeanDefinitionOverrideException(beanName,
new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName),
existingBeanDef,
"@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
}
if (logger.isDebugEnabled()) {
logger.debug(String.format("Skipping bean definition for %s: a definition for bean '%s' " +
@@ -29,7 +29,8 @@ import org.springframework.lang.Nullable;
/**
* Declares that a field or method parameter should be formatted as a
* {@link java.time.Duration}, according to the specified {@link #style style}.
* {@link java.time.Duration}, according to the specified {@link #style Style}
* and {@link #defaultUnit Unit}.
*
* @author Simon Baslé
* @since 6.2
@@ -40,20 +41,20 @@ import org.springframework.lang.Nullable;
public @interface DurationFormat {
/**
* The {@code Style} to use for parsing and printing a {@code Duration}.
* The {@link Style} to use for parsing and printing a {@link Duration}.
* <p>Defaults to the JDK style ({@link Style#ISO8601}).
*/
Style style() default Style.ISO8601;
/**
* The {@link Unit} to fall back to in case the {@code style()} needs a unit
* The {@link Unit} to fall back to in case the {@link #style Style} needs a unit
* for either parsing or printing, and none is explicitly provided in the input.
* <p>Defaults to {@link Unit#MILLIS} if unspecified.
*/
Unit defaultUnit() default Unit.MILLIS;
/**
* Duration format styles.
* {@link Duration} format styles.
*/
enum Style {
@@ -62,7 +63,7 @@ public @interface DurationFormat {
* <p>Supported unit suffixes include: {@code ns, us, ms, s, m, h, d}.
* Those correspond to nanoseconds, microseconds, milliseconds, seconds,
* minutes, hours, and days, respectively.
* <p>Note that when printing a {@code Duration}, this style can be
* <p>Note that when printing a {@link Duration}, this style can be
* lossy if the selected unit is bigger than the resolution of the
* duration. For example, {@code Duration.ofMillis(5).plusNanos(1234)}
* would get truncated to {@code "5ms"} when printing using
@@ -73,7 +74,7 @@ public @interface DurationFormat {
/**
* ISO-8601 formatting.
* <p>This is what the JDK uses in {@link java.time.Duration#parse(CharSequence)}
* <p>This is what the JDK uses in {@link Duration#parse(CharSequence)}
* and {@link Duration#toString()}.
*/
ISO8601,
@@ -90,11 +91,11 @@ public @interface DurationFormat {
}
/**
* Duration format unit, which mirrors a subset of {@link ChronoUnit} and
* {@link Duration} format unit, which mirrors a subset of {@link ChronoUnit} and
* allows conversion to and from a supported {@code ChronoUnit} as well as
* conversion from durations to longs.
*
* <p>The enum includes its corresponding suffix in the {@link Style#SIMPLE simple}
* <p>The enum includes its corresponding suffix in the {@link Style#SIMPLE SIMPLE}
* {@code Duration} format style.
*/
enum Unit {
@@ -147,25 +148,24 @@ public @interface DurationFormat {
}
/**
* Convert this {@code DurationFormat.Unit} to its {@link ChronoUnit}
* equivalent.
* Convert this {@code Unit} to its {@link ChronoUnit} equivalent.
*/
public ChronoUnit asChronoUnit() {
return this.chronoUnit;
}
/**
* Convert this {@code DurationFormat.Unit} to a simple {@code String}
* suffix, suitable for the {@link Style#SIMPLE SIMPLE} style.
* Convert this {@code Unit} to a simple {@code String} suffix, suitable
* for the {@link Style#SIMPLE SIMPLE} style.
*/
public String asSuffix() {
return this.suffix;
}
/**
* Parse a {@code long} from a {@code String} and interpret it to be a
* {@code Duration} in the current unit.
* @param value the String representation of the long
* Parse a {@code long} from the given {@link String} and interpret it to be a
* {@link Duration} in the current unit.
* @param value the {@code String} representation of the long
* @return the corresponding {@code Duration}
*/
public Duration parse(String value) {
@@ -173,11 +173,11 @@ public @interface DurationFormat {
}
/**
* Print a {@code Duration} as a {@code String}, converting it to a long
* Print the given {@link Duration} as a {@link String}, converting it to a long
* value using this unit's precision via {@link #longValue(Duration)}
* and appending this unit's simple {@link #asSuffix() suffix}.
* @param value the {@code Duration} to convert to a String
* @return the String representation of the {@code Duration} in the
* @param value the {@code Duration} to convert to a {@code String}
* @return the {@code String} representation of the {@code Duration} in the
* {@link Style#SIMPLE SIMPLE} style
*/
public String print(Duration value) {
@@ -185,11 +185,12 @@ public @interface DurationFormat {
}
/**
* Convert the given {@code Duration} to a long value in the resolution
* of this unit. Note that this can be lossy if the current unit is
* bigger than the actual resolution of the duration.
* <p>For example, {@code Duration.ofMillis(5).plusNanos(1234)} would
* get truncated to {@code 5} for unit {@code MILLIS}.
* Convert the given {@link Duration} to a long value in the resolution
* of this unit.
* <p>Note that this can be lossy if the current unit is bigger than the
* actual resolution of the duration. For example,
* {@code Duration.ofMillis(5).plusNanos(1234)} would get truncated to
* {@code 5} for unit {@code MILLIS}.
* @param value the {@code Duration} to convert to a long
* @return the long value for the {@code Duration} in this {@code Unit}
*/
@@ -198,7 +199,7 @@ public @interface DurationFormat {
}
/**
* Get the {@code Unit} corresponding to the given {@code ChronoUnit}.
* Get the {@link Unit} corresponding to the given {@link ChronoUnit}.
* @throws IllegalArgumentException if the given {@code ChronoUnit} is
* not supported
*/
@@ -215,7 +216,7 @@ public @interface DurationFormat {
}
/**
* Get the {@code Unit} corresponding to the given {@code String} suffix.
* Get the {@link Unit} corresponding to the given {@link String} suffix.
* @throws IllegalArgumentException if the given suffix is not supported
*/
public static Unit fromSuffix(String suffix) {
@@ -123,8 +123,9 @@ abstract class ScheduledAnnotationReactiveSupport {
Publisher<?> publisher = getPublisherFor(method, targetBean);
Supplier<ScheduledTaskObservationContext> contextSupplier =
() -> new ScheduledTaskObservationContext(targetBean, method);
String displayName = targetBean.getClass().getName() + "." + method.getName();
return new SubscribingRunnable(publisher, shouldBlock, scheduled.scheduler(),
subscriptionTrackerRegistry, observationRegistrySupplier, contextSupplier);
subscriptionTrackerRegistry, displayName, observationRegistrySupplier, contextSupplier);
}
/**
@@ -192,6 +193,8 @@ abstract class ScheduledAnnotationReactiveSupport {
final boolean shouldBlock;
final String displayName;
@Nullable
private final String qualifier;
@@ -202,12 +205,13 @@ abstract class ScheduledAnnotationReactiveSupport {
final Supplier<ScheduledTaskObservationContext> contextSupplier;
SubscribingRunnable(Publisher<?> publisher, boolean shouldBlock,
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
Supplier<ObservationRegistry> observationRegistrySupplier,
Supplier<ScheduledTaskObservationContext> contextSupplier) {
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
String displayName, Supplier<ObservationRegistry> observationRegistrySupplier,
Supplier<ScheduledTaskObservationContext> contextSupplier) {
this.publisher = publisher;
this.shouldBlock = shouldBlock;
this.displayName = displayName;
this.qualifier = qualifier;
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
this.observationRegistrySupplier = observationRegistrySupplier;
@@ -253,6 +257,11 @@ abstract class ScheduledAnnotationReactiveSupport {
this.publisher.subscribe(subscriber);
}
}
@Override
public String toString() {
return this.displayName;
}
}
@@ -18,6 +18,8 @@ package org.springframework.scheduling.config;
import java.time.Instant;
import org.springframework.lang.Nullable;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.util.Assert;
/**
@@ -68,7 +70,7 @@ public class Task {
}
private class OutcomeTrackingRunnable implements Runnable {
private class OutcomeTrackingRunnable implements SchedulingAwareRunnable {
private final Runnable runnable;
@@ -89,6 +91,23 @@ public class Task {
}
}
@Override
public boolean isLongLived() {
if (this.runnable instanceof SchedulingAwareRunnable sar) {
return sar.isLongLived();
}
return SchedulingAwareRunnable.super.isLongLived();
}
@Nullable
@Override
public String getQualifier() {
if (this.runnable instanceof SchedulingAwareRunnable sar) {
return sar.getQualifier();
}
return SchedulingAwareRunnable.super.getQualifier();
}
@Override
public String toString() {
return this.runnable.toString();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,7 +18,6 @@ package org.springframework.validation.beanvalidation;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -107,7 +106,7 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
Set<Class<?>> validatedClasses = new HashSet<>();
Set<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses = new HashSet<>();
processAheadOfTime(beanClass, validatedClasses, constraintValidatorClasses);
processAheadOfTime(beanClass, new HashSet<>(), validatedClasses, constraintValidatorClasses);
if (!validatedClasses.isEmpty() || !constraintValidatorClasses.isEmpty()) {
return new AotContribution(validatedClasses, constraintValidatorClasses);
@@ -115,27 +114,38 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
return null;
}
private static void processAheadOfTime(Class<?> clazz, Collection<Class<?>> validatedClasses,
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
private static void processAheadOfTime(Class<?> clazz, Set<Class<?>> visitedClasses, Set<Class<?>> validatedClasses,
Set<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
Assert.notNull(validator, "Validator can't be null");
Assert.notNull(validator, "Validator cannot be null");
if (!visitedClasses.add(clazz)) {
return;
}
BeanDescriptor descriptor;
try {
descriptor = validator.getConstraintsForClass(clazz);
}
catch (RuntimeException ex) {
catch (RuntimeException | LinkageError ex) {
String className = clazz.getName();
if (KotlinDetector.isKotlinType(clazz) && ex instanceof ArrayIndexOutOfBoundsException) {
// See https://hibernate.atlassian.net/browse/HV-1796 and https://youtrack.jetbrains.com/issue/KT-40857
logger.warn("Skipping validation constraint hint inference for class " + clazz +
" due to an ArrayIndexOutOfBoundsException at validator level");
if (logger.isWarnEnabled()) {
logger.warn("Skipping validation constraint hint inference for class " + className +
" due to an ArrayIndexOutOfBoundsException at validator level");
}
}
else if (ex instanceof TypeNotPresentException) {
logger.debug("Skipping validation constraint hint inference for class " +
clazz + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
else if (ex instanceof TypeNotPresentException || ex instanceof NoClassDefFoundError) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping validation constraint hint inference for class %s due to a %s for %s"
.formatted(className, ex.getClass().getSimpleName(), ex.getMessage()));
}
}
else {
logger.warn("Skipping validation constraint hint inference for class " + clazz, ex);
if (logger.isWarnEnabled()) {
logger.warn("Skipping validation constraint hint inference for class " + className, ex);
}
}
return;
}
@@ -149,12 +159,12 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
ReflectionUtils.doWithFields(clazz, field -> {
Class<?> type = field.getType();
if (Iterable.class.isAssignableFrom(type) || List.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) {
if (Iterable.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) {
ResolvableType resolvableType = ResolvableType.forField(field);
Class<?> genericType = resolvableType.getGeneric(0).toClass();
if (shouldProcess(genericType)) {
validatedClasses.add(clazz);
processAheadOfTime(genericType, validatedClasses, constraintValidatorClasses);
processAheadOfTime(genericType, visitedClasses, validatedClasses, constraintValidatorClasses);
}
}
if (Map.class.isAssignableFrom(type)) {
@@ -163,11 +173,11 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
Class<?> valueGenericType = resolvableType.getGeneric(1).toClass();
if (shouldProcess(keyGenericType)) {
validatedClasses.add(clazz);
processAheadOfTime(keyGenericType, validatedClasses, constraintValidatorClasses);
processAheadOfTime(keyGenericType, visitedClasses, validatedClasses, constraintValidatorClasses);
}
if (shouldProcess(valueGenericType)) {
validatedClasses.add(clazz);
processAheadOfTime(valueGenericType, validatedClasses, constraintValidatorClasses);
processAheadOfTime(valueGenericType, visitedClasses, validatedClasses, constraintValidatorClasses);
}
}
});
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2024 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.validation.beanvalidation;
import jakarta.validation.ValidationException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
/**
@@ -39,7 +40,13 @@ public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {
super.afterPropertiesSet();
}
catch (ValidationException ex) {
LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
Log logger = LogFactory.getLog(getClass());
if (logger.isDebugEnabled()) {
logger.debug("Failed to set up a Bean Validation provider", ex);
}
else if (logger.isInfoEnabled()) {
logger.info("Failed to set up a Bean Validation provider: " + ex);
}
}
}
@@ -85,7 +85,7 @@ class AspectJAutoProxyCreatorTests {
void aspectsAreApplied() {
ClassPathXmlApplicationContext bf = newContext("aspects.xml");
ITestBean tb = (ITestBean) bf.getBean("adrian");
ITestBean tb = bf.getBean("adrian", ITestBean.class);
assertThat(tb.getAge()).isEqualTo(68);
MethodInvokingFactoryBean factoryBean = (MethodInvokingFactoryBean) bf.getBean("&factoryBean");
assertThat(AopUtils.isAopProxy(factoryBean.getTargetObject())).isTrue();
@@ -96,7 +96,7 @@ class AspectJAutoProxyCreatorTests {
void multipleAspectsWithParameterApplied() {
ClassPathXmlApplicationContext bf = newContext("aspects.xml");
ITestBean tb = (ITestBean) bf.getBean("adrian");
ITestBean tb = bf.getBean("adrian", ITestBean.class);
tb.setAge(10);
assertThat(tb.getAge()).isEqualTo(20);
}
@@ -105,7 +105,7 @@ class AspectJAutoProxyCreatorTests {
void aspectsAreAppliedInDefinedOrder() {
ClassPathXmlApplicationContext bf = newContext("aspectsWithOrdering.xml");
ITestBean tb = (ITestBean) bf.getBean("adrian");
ITestBean tb = bf.getBean("adrian", ITestBean.class);
assertThat(tb.getAge()).isEqualTo(71);
}
@@ -113,8 +113,8 @@ class AspectJAutoProxyCreatorTests {
void aspectsAndAdvisorAreApplied() {
ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml");
ITestBean shouldBeWeaved = (ITestBean) ac.getBean("adrian");
doTestAspectsAndAdvisorAreApplied(ac, shouldBeWeaved);
ITestBean shouldBeWoven = ac.getBean("adrian", ITestBean.class);
assertAspectsAndAdvisorAreApplied(ac, shouldBeWoven);
}
@Test
@@ -124,20 +124,22 @@ class AspectJAutoProxyCreatorTests {
GenericApplicationContext childAc = new GenericApplicationContext(ac);
// Create a child factory with a bean that should be woven
RootBeanDefinition bd = new RootBeanDefinition(TestBean.class);
bd.getPropertyValues().addPropertyValue(new PropertyValue("name", "Adrian"))
bd.getPropertyValues()
.addPropertyValue(new PropertyValue("name", "Adrian"))
.addPropertyValue(new PropertyValue("age", 34));
childAc.registerBeanDefinition("adrian2", bd);
// Register the advisor auto proxy creator with subclass
childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(), new RootBeanDefinition(
AnnotationAwareAspectJAutoProxyCreator.class));
childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(),
new RootBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class));
childAc.refresh();
ITestBean beanFromChildContextThatShouldBeWeaved = (ITestBean) childAc.getBean("adrian2");
//testAspectsAndAdvisorAreApplied(childAc, (ITestBean) ac.getBean("adrian"));
doTestAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWeaved);
ITestBean beanFromParentContextThatShouldBeWoven = ac.getBean("adrian", ITestBean.class);
ITestBean beanFromChildContextThatShouldBeWoven = childAc.getBean("adrian2", ITestBean.class);
assertAspectsAndAdvisorAreApplied(childAc, beanFromParentContextThatShouldBeWoven);
assertAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWoven);
}
protected void doTestAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWeaved) {
protected void assertAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWoven) {
TestBeanAdvisor tba = (TestBeanAdvisor) ac.getBean("advisor");
MultiplyReturnValue mrv = (MultiplyReturnValue) ac.getBean("aspect");
@@ -146,10 +148,10 @@ class AspectJAutoProxyCreatorTests {
tba.count = 0;
mrv.invocations = 0;
assertThat(AopUtils.isAopProxy(shouldBeWeaved)).as("Autoproxying must apply from @AspectJ aspect").isTrue();
assertThat(shouldBeWeaved.getName()).isEqualTo("Adrian");
assertThat(AopUtils.isAopProxy(shouldBeWoven)).as("Autoproxying must apply from @AspectJ aspect").isTrue();
assertThat(shouldBeWoven.getName()).isEqualTo("Adrian");
assertThat(mrv.invocations).isEqualTo(0);
assertThat(shouldBeWeaved.getAge()).isEqualTo((34 * mrv.getMultiple()));
assertThat(shouldBeWoven.getAge()).isEqualTo((34 * mrv.getMultiple()));
assertThat(tba.count).as("Spring advisor must be invoked").isEqualTo(2);
assertThat(mrv.invocations).as("Must be able to hold state in aspect").isEqualTo(1);
}
@@ -158,13 +160,13 @@ class AspectJAutoProxyCreatorTests {
void perThisAspect() {
ClassPathXmlApplicationContext bf = newContext("perthis.xml");
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
assertThat(adrian1.getAge()).isEqualTo(0);
assertThat(adrian1.getAge()).isEqualTo(1);
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
assertThat(adrian2).isNotSameAs(adrian1);
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
assertThat(adrian2.getAge()).isEqualTo(0);
@@ -178,7 +180,7 @@ class AspectJAutoProxyCreatorTests {
void perTargetAspect() throws SecurityException, NoSuchMethodException {
ClassPathXmlApplicationContext bf = newContext("pertarget.xml");
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
// Does not trigger advice or count
@@ -199,7 +201,7 @@ class AspectJAutoProxyCreatorTests {
adrian1.setName("Adrian");
//assertEquals("Any other setter does not increment", 2, adrian1.getAge());
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
assertThat(adrian2).isNotSameAs(adrian1);
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
assertThat(adrian2.getAge()).isEqualTo(34);
@@ -239,7 +241,7 @@ class AspectJAutoProxyCreatorTests {
void twoAdviceAspect() {
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspect.xml");
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
testAgeAspect(adrian1, 0, 2);
}
@@ -247,9 +249,9 @@ class AspectJAutoProxyCreatorTests {
void twoAdviceAspectSingleton() {
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectSingleton.xml");
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
testAgeAspect(adrian1, 0, 1);
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
assertThat(adrian2).isNotSameAs(adrian1);
testAgeAspect(adrian2, 2, 1);
}
@@ -258,9 +260,9 @@ class AspectJAutoProxyCreatorTests {
void twoAdviceAspectPrototype() {
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectPrototype.xml");
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
testAgeAspect(adrian1, 0, 1);
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
assertThat(adrian2).isNotSameAs(adrian1);
testAgeAspect(adrian2, 0, 1);
}
@@ -280,7 +282,7 @@ class AspectJAutoProxyCreatorTests {
void adviceUsingJoinPoint() {
ClassPathXmlApplicationContext bf = newContext("usesJoinPointAspect.xml");
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
adrian1.getAge();
AdviceUsingThisJoinPoint aspectInstance = (AdviceUsingThisJoinPoint) bf.getBean("aspect");
//(AdviceUsingThisJoinPoint) Aspects.aspectOf(AdviceUsingThisJoinPoint.class);
@@ -292,7 +294,7 @@ class AspectJAutoProxyCreatorTests {
void includeMechanism() {
ClassPathXmlApplicationContext bf = newContext("usesInclude.xml");
ITestBean adrian = (ITestBean) bf.getBean("adrian");
ITestBean adrian = bf.getBean("adrian", ITestBean.class);
assertThat(AopUtils.isAopProxy(adrian)).isTrue();
assertThat(adrian.getAge()).isEqualTo(68);
}
@@ -310,7 +312,7 @@ class AspectJAutoProxyCreatorTests {
void withAbstractFactoryBeanAreApplied() {
ClassPathXmlApplicationContext bf = newContext("aspectsWithAbstractBean.xml");
ITestBean adrian = (ITestBean) bf.getBean("adrian");
ITestBean adrian = bf.getBean("adrian", ITestBean.class);
assertThat(AopUtils.isAopProxy(adrian)).isTrue();
assertThat(adrian.getAge()).isEqualTo(68);
}
@@ -321,8 +323,7 @@ class AspectJAutoProxyCreatorTests {
UnreliableBean bean = (UnreliableBean) bf.getBean("unreliableBean");
RetryAspect aspect = (RetryAspect) bf.getBean("retryAspect");
int attempts = bean.unreliable();
assertThat(attempts).isEqualTo(2);
assertThat(bean.unreliable()).isEqualTo(2);
assertThat(aspect.getBeginCalls()).isEqualTo(2);
assertThat(aspect.getRollbackCalls()).isEqualTo(1);
assertThat(aspect.getCommitCalls()).isEqualTo(1);
@@ -332,7 +333,7 @@ class AspectJAutoProxyCreatorTests {
void withBeanNameAutoProxyCreator() {
ClassPathXmlApplicationContext bf = newContext("withBeanNameAutoProxyCreator.xml");
ITestBean tb = (ITestBean) bf.getBean("adrian");
ITestBean tb = bf.getBean("adrian", ITestBean.class);
assertThat(tb.getAge()).isEqualTo(68);
}
@@ -0,0 +1,98 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aop.framework.autoproxy;
import java.lang.reflect.Method;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.MethodInterceptor;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.AbstractPointcutAdvisor;
import org.springframework.aop.support.RootClassFilter;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link DefaultAdvisorAutoProxyCreator}.
*
* @author Sam Brannen
* @since 6.2.1
*/
class DefaultAdvisorAutoProxyCreatorTests {
/**
* Indirectly tests behavior of {@link org.springframework.aop.framework.AdvisedSupport.MethodCacheKey}.
* @see StaticMethodMatcherPointcut#matches(Method, Class)
*/
@Test // gh-33915
void staticMethodMatcherPointcutMatchesMethodIsNotInvokedAgainForActualMethodInvocation() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
DemoBean.class, DemoPointcutAdvisor.class, DefaultAdvisorAutoProxyCreator.class);
DemoPointcutAdvisor demoPointcutAdvisor = context.getBean(DemoPointcutAdvisor.class);
DemoBean demoBean = context.getBean(DemoBean.class);
assertThat(demoPointcutAdvisor.matchesInvocationCount).as("matches() invocations before").isEqualTo(2);
// Invoke multiple times to ensure additional invocations don't affect the outcome.
assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!");
assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!");
assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!");
assertThat(demoPointcutAdvisor.matchesInvocationCount).as("matches() invocations after").isEqualTo(2);
context.close();
}
static class DemoBean {
public String sayHello() {
return "Hello!";
}
}
@SuppressWarnings("serial")
static class DemoPointcutAdvisor extends AbstractPointcutAdvisor {
int matchesInvocationCount = 0;
@Override
public Pointcut getPointcut() {
StaticMethodMatcherPointcut pointcut = new StaticMethodMatcherPointcut() {
@Override
public boolean matches(Method method, Class<?> targetClass) {
if (method.getName().equals("sayHello")) {
matchesInvocationCount++;
return true;
}
return false;
}
};
pointcut.setClassFilter(new RootClassFilter(DemoBean.class));
return pointcut;
}
@Override
public Advice getAdvice() {
return (MethodInterceptor) invocation -> "Advised: " + invocation.proceed();
}
}
}
@@ -110,7 +110,7 @@ class ConfigurationClassProcessingTests {
private void aliasesAreRespected(Class<?> testClass, Supplier<TestBean> testBeanSupplier, String beanName) {
TestBean testBean = testBeanSupplier.get();
BeanFactory factory = initBeanFactory(testClass);
BeanFactory factory = initBeanFactory(false, testClass);
assertThat(factory.getBean(beanName)).isSameAs(testBean);
Arrays.stream(factory.getAliases(beanName)).map(factory::getBean).forEach(alias -> assertThat(alias).isSameAs(testBean));
@@ -141,30 +141,30 @@ class ConfigurationClassProcessingTests {
@Test
void finalBeanMethod() {
assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() ->
initBeanFactory(ConfigWithFinalBean.class));
initBeanFactory(false, ConfigWithFinalBean.class));
}
@Test
void finalBeanMethodWithoutProxy() {
initBeanFactory(ConfigWithFinalBeanWithoutProxy.class);
initBeanFactory(false, ConfigWithFinalBeanWithoutProxy.class);
}
@Test // gh-31007
void voidBeanMethod() {
assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() ->
initBeanFactory(ConfigWithVoidBean.class));
initBeanFactory(false, ConfigWithVoidBean.class));
}
@Test
void simplestPossibleConfig() {
BeanFactory factory = initBeanFactory(SimplestPossibleConfig.class);
BeanFactory factory = initBeanFactory(false, SimplestPossibleConfig.class);
String stringBean = factory.getBean("stringBean", String.class);
assertThat(stringBean).isEqualTo("foo");
}
@Test
void configWithObjectReturnType() {
BeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class);
BeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class);
assertThat(factory.getType("stringBean")).isEqualTo(Object.class);
assertThat(factory.isTypeMatch("stringBean", String.class)).isFalse();
String stringBean = factory.getBean("stringBean", String.class);
@@ -173,7 +173,7 @@ class ConfigurationClassProcessingTests {
@Test
void configWithFactoryBeanReturnType() {
ListableBeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class);
ListableBeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class);
assertThat(factory.getType("factoryBean")).isEqualTo(List.class);
assertThat(factory.isTypeMatch("factoryBean", List.class)).isTrue();
assertThat(factory.getType("&factoryBean")).isEqualTo(FactoryBean.class);
@@ -201,7 +201,7 @@ class ConfigurationClassProcessingTests {
@Test
void configurationWithPrototypeScopedBeans() {
BeanFactory factory = initBeanFactory(ConfigWithPrototypeBean.class);
BeanFactory factory = initBeanFactory(false, ConfigWithPrototypeBean.class);
TestBean foo = factory.getBean("foo", TestBean.class);
ITestBean bar = factory.getBean("bar", ITestBean.class);
@@ -213,7 +213,7 @@ class ConfigurationClassProcessingTests {
@Test
void configurationWithNullReference() {
BeanFactory factory = initBeanFactory(ConfigWithNullReference.class);
BeanFactory factory = initBeanFactory(false, ConfigWithNullReference.class);
TestBean foo = factory.getBean("foo", TestBean.class);
assertThat(factory.getBean("bar")).isEqualTo(null);
@@ -223,7 +223,15 @@ class ConfigurationClassProcessingTests {
@Test // gh-33330
void configurationWithMethodNameMismatch() {
assertThatExceptionOfType(BeanDefinitionOverrideException.class)
.isThrownBy(() -> initBeanFactory(ConfigWithMethodNameMismatch.class));
.isThrownBy(() -> initBeanFactory(false, ConfigWithMethodNameMismatch.class));
}
@Test // gh-33920
void configurationWithMethodNameMismatchAndOverridingAllowed() {
BeanFactory factory = initBeanFactory(true, ConfigWithMethodNameMismatch.class);
SpousyTestBean foo = factory.getBean("foo", SpousyTestBean.class);
assertThat(foo.getName()).isEqualTo("foo1");
}
@Test
@@ -353,13 +361,13 @@ class ConfigurationClassProcessingTests {
* When complete, the factory is ready to service requests for any {@link Bean} methods
* declared by {@code configClasses}.
*/
private DefaultListableBeanFactory initBeanFactory(Class<?>... configClasses) {
private DefaultListableBeanFactory initBeanFactory(boolean allowOverriding, Class<?>... configClasses) {
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
for (Class<?> configClass : configClasses) {
String configBeanName = configClass.getName();
factory.registerBeanDefinition(configBeanName, new RootBeanDefinition(configClass));
}
factory.setAllowBeanDefinitionOverriding(false);
factory.setAllowBeanDefinitionOverriding(allowOverriding);
ConfigurationClassPostProcessor ccpp = new ConfigurationClassPostProcessor();
ccpp.postProcessBeanDefinitionRegistry(factory);
ccpp.postProcessBeanFactory(factory);
@@ -537,12 +545,12 @@ class ConfigurationClassProcessingTests {
@Configuration
static class ConfigWithMethodNameMismatch {
@Bean(name = "foo") public TestBean foo() {
return new SpousyTestBean("foo");
@Bean(name = "foo") public TestBean foo1() {
return new SpousyTestBean("foo1");
}
@Bean(name = "foo") public TestBean fooX() {
return new SpousyTestBean("fooX");
@Bean(name = "foo") public TestBean foo2() {
return new SpousyTestBean("foo2");
}
}
@@ -66,8 +66,10 @@ import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;
import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.context.testfixture.context.annotation.AutowiredCglibConfiguration;
import org.springframework.context.testfixture.context.annotation.AutowiredComponent;
import org.springframework.context.testfixture.context.annotation.AutowiredGenericTemplate;
import org.springframework.context.testfixture.context.annotation.AutowiredMixedCglibConfiguration;
import org.springframework.context.testfixture.context.annotation.CglibConfiguration;
import org.springframework.context.testfixture.context.annotation.ConfigurableCglibConfiguration;
import org.springframework.context.testfixture.context.annotation.GenericTemplateConfiguration;
@@ -82,6 +84,7 @@ import org.springframework.context.testfixture.context.annotation.LazyResourceMe
import org.springframework.context.testfixture.context.annotation.PropertySourceConfiguration;
import org.springframework.context.testfixture.context.annotation.QualifierConfiguration;
import org.springframework.context.testfixture.context.annotation.ResourceComponent;
import org.springframework.context.testfixture.context.annotation.ValueCglibConfiguration;
import org.springframework.context.testfixture.context.generator.SimpleComponent;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
@@ -436,12 +439,14 @@ class ApplicationContextAotGeneratorTests {
@CompileWithForkedClassLoader
class ConfigurationClassCglibProxy {
private static final String CGLIB_CONFIGURATION_CLASS_SUFFIX = "$$SpringCGLIB$$0";
@Test
void processAheadOfTimeWhenHasCglibProxyWriteProxyAndGenerateReflectionHints() throws IOException {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean(CglibConfiguration.class);
TestGenerationContext context = processAheadOfTime(applicationContext);
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$0");
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + CGLIB_CONFIGURATION_CLASS_SUFFIX);
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$0");
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$1");
}
@@ -453,6 +458,43 @@ class ApplicationContextAotGeneratorTests {
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(context.getRuntimeHints());
}
@Test
void processAheadOfTimeExposeUserClassForCglibProxy() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean("config", ValueCglibConfiguration.class);
testCompiledResult(applicationContext, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", ValueCglibConfiguration.class));
assertThat(compiled.getSourceFile(".*ValueCglibConfiguration__BeanDefinitions"))
.contains("new RootBeanDefinition(ValueCglibConfiguration.class)")
.contains("new %s(".formatted(toCglibClassSimpleName(ValueCglibConfiguration.class)));
});
}
@Test
void processAheadOfTimeUsesCglibClassForFactoryMethod() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean("config", CglibConfiguration.class);
testCompiledResult(applicationContext, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", CglibConfiguration.class));
assertThat(compiled.getSourceFile(".*CglibConfiguration__BeanDefinitions"))
.contains("new RootBeanDefinition(CglibConfiguration.class)")
.contains(">forFactoryMethod(%s.class,".formatted(toCglibClassSimpleName(CglibConfiguration.class)))
.doesNotContain(">forFactoryMethod(%s.class,".formatted(CglibConfiguration.class));
});
}
private Consumer<GenericApplicationContext> hasBeanDefinitionOfBeanClass(String name, Class<?> beanClass) {
return context -> {
assertThat(context.containsBean(name)).isTrue();
assertThat(context.getBeanDefinition(name)).isInstanceOfSatisfying(RootBeanDefinition.class,
rbd -> assertThat(rbd.getBeanClass()).isEqualTo(beanClass));
};
}
@Test
void processAheadOfTimeWhenHasCglibProxyUseProxy() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
@@ -464,6 +506,47 @@ class ApplicationContextAotGeneratorTests {
});
}
@Test
void processAheadOfTimeWhenHasCglibProxyAndAutowiring() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean(AutowiredCglibConfiguration.class);
testCompiledResult(applicationContext, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> {
context.setEnvironment(new MockEnvironment().withProperty("hello", "Hi"));
initializer.initialize(context);
});
assertThat(freshApplicationContext.getBean("text", String.class)).isEqualTo("Hi World");
});
}
@Test
void processAheadOfTimeWhenHasCglibProxyAndMixedAutowiring() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean(AutowiredMixedCglibConfiguration.class);
testCompiledResult(applicationContext, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> {
context.setEnvironment(new MockEnvironment().withProperty("hello", "Hi")
.withProperty("world", "AOT World"));
initializer.initialize(context);
});
assertThat(freshApplicationContext.getBean("text", String.class)).isEqualTo("Hi AOT World");
});
}
@Test
void processAheadOfTimeWhenHasCglibProxyWithAnnotationsOnTheUserClasConstructor() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.registerBean("config", ValueCglibConfiguration.class);
testCompiledResult(applicationContext, (initializer, compiled) -> {
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> {
context.setEnvironment(new MockEnvironment().withProperty("name", "AOT World"));
initializer.initialize(context);
});
assertThat(freshApplicationContext.getBean(ValueCglibConfiguration.class)
.getName()).isEqualTo("AOT World");
});
}
@Test
void processAheadOfTimeWhenHasCglibProxyWithArgumentsUseProxy() {
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
@@ -487,6 +570,10 @@ class ApplicationContextAotGeneratorTests {
.accepts(generationContext.getRuntimeHints());
}
private String toCglibClassSimpleName(Class<?> configClass) {
return configClass.getSimpleName() + CGLIB_CONFIGURATION_CLASS_SUFFIX;
}
}
@Nested
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -47,7 +47,7 @@ public class EventCollector {
/**
* Return the events that the specified listener has received. The list of events
* is ordered according to their reception order.
* is ordered according to the order in which they were received.
*/
public List<Object> getEvents(Identifiable listener) {
return this.content.get(listener.getId());
@@ -42,7 +42,7 @@ import static org.springframework.format.annotation.DurationFormat.Style.SIMPLE;
class DurationFormatterUtilsTests {
@ParameterizedTest
@EnumSource(DurationFormat.Style.class)
@EnumSource
void parseEmptyStringFailsWithDedicatedException(DurationFormat.Style style) {
assertThatIllegalArgumentException()
.isThrownBy(() -> DurationFormatterUtils.parse("", style))
@@ -50,7 +50,7 @@ class DurationFormatterUtilsTests {
}
@ParameterizedTest
@EnumSource(DurationFormat.Style.class)
@EnumSource
void parseNullStringFailsWithDedicatedException(DurationFormat.Style style) {
assertThatIllegalArgumentException()
.isThrownBy(() -> DurationFormatterUtils.parse(null, style))
@@ -113,29 +113,30 @@ class DurationFormatterUtilsTests {
@Test
void parseIsoNoChronoUnit() {
//these are based on the examples given in Duration.parse
// "PT20.345S" -- parses as "20.345 seconds"
// These are based on the examples given in Duration.parse.
// "PT20.345S" -- parses as "20.345 seconds"
assertThat(DurationFormatterUtils.parse("PT20.345S", ISO8601))
.hasMillis(20345);
// "PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
// "PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
assertThat(DurationFormatterUtils.parse("PT15M", ISO8601))
.hasSeconds(15*60);
// "PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
// "PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
assertThat(DurationFormatterUtils.parse("PT10H", ISO8601))
.hasHours(10);
// "P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
// "P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
assertThat(DurationFormatterUtils.parse("P2D", ISO8601))
.hasDays(2);
// "P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes"
// "P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes"
assertThat(DurationFormatterUtils.parse("P2DT3H4M", ISO8601))
.isEqualTo(Duration.ofDays(2).plusHours(3).plusMinutes(4));
// "PT-6H3M" -- parses as "-6 hours and +3 minutes"
// "PT-6H3M" -- parses as "-6 hours and +3 minutes"
assertThat(DurationFormatterUtils.parse("PT-6H3M", ISO8601))
.isEqualTo(Duration.ofHours(-6).plusMinutes(3));
// "-PT6H3M" -- parses as "-6 hours and -3 minutes"
// "-PT6H3M" -- parses as "-6 hours and -3 minutes"
assertThat(DurationFormatterUtils.parse("-PT6H3M", ISO8601))
.isEqualTo(Duration.ofHours(-6).plusMinutes(-3));
// "-PT-6H+3M" -- parses as "+6 hours and -3 minutes"
// "-PT-6H+3M" -- parses as "+6 hours and -3 minutes"
assertThat(DurationFormatterUtils.parse("-PT-6H+3M", ISO8601))
.isEqualTo(Duration.ofHours(6).plusMinutes(-3));
}
@@ -189,7 +190,7 @@ class DurationFormatterUtilsTests {
.isEqualTo(Duration.ofMinutes(34).plusSeconds(57));
}
@Test //Kotlin style compatibility
@Test // Kotlin style compatibility
void parseCompositeNegativeWithSpacesAndParenthesis() {
assertThat(DurationFormatterUtils.parse("-(34m 57s)", COMPOSITE))
.isEqualTo(Duration.ofMinutes(-34).plusSeconds(-57));
@@ -315,7 +316,7 @@ class DurationFormatterUtilsTests {
assertThat(DurationFormatterUtils.detect("-(1d 2h 34m 2ns)"))
.as("COMPOSITE")
.isEqualTo(COMPOSITE);
.isEqualTo(COMPOSITE);
assertThatIllegalArgumentException().isThrownBy(() -> DurationFormatterUtils.detect("WPT2H-4M"))
.withMessage("'WPT2H-4M' is not a valid duration, cannot detect any known style")
@@ -175,8 +175,7 @@ class ScheduledAnnotationBeanPostProcessorObservabilityTests {
}
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatTaskObservation() {
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
.hasObservationWithNameEqualTo("tasks.scheduled.execution").that();
return assertThat(this.observationRegistry).hasObservationWithNameEqualTo("tasks.scheduled.execution").that();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -173,6 +173,17 @@ class ScheduledAnnotationReactiveSupportTests {
assertThat(p).hasToString("checkpoint(\"@Scheduled 'mono()' in 'org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupportTests$ReactiveMethods'\")");
}
@Test
void shouldProvideToString() {
ReactiveMethods target = new ReactiveMethods();
Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "mono");
Scheduled cron = AnnotationUtils.synthesizeAnnotation(Map.of("cron", "-"), Scheduled.class, null);
List<Runnable> tracker = new ArrayList<>();
assertThat(createSubscriptionRunnable(m, target, cron, () -> ObservationRegistry.NOOP, tracker))
.hasToString("org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupportTests$ReactiveMethods.mono");
}
static class ReactiveMethods {
@@ -125,7 +125,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings({ "deprecation", "removal" })
@SuppressWarnings("removal")
void submitListenableRunnable() {
TestTask task = new TestTask(this.testName, 1);
// Act
@@ -156,7 +156,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings({ "deprecation", "removal" })
@SuppressWarnings("removal")
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({ "deprecation", "removal" })
@SuppressWarnings("removal")
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));
@@ -209,18 +209,10 @@ abstract class AbstractSchedulingTaskExecutorTests {
CompletableFuture<?> future1 = executor.submitCompletable(new TestTask(this.testName, -1));
CompletableFuture<?> future2 = executor.submitCompletable(new TestTask(this.testName, -1));
shutdownExecutor();
try {
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> {
future1.get(1000, TimeUnit.MILLISECONDS);
}
catch (Exception ex) {
// ignore
}
Awaitility.await()
.atMost(5, TimeUnit.SECONDS)
.pollInterval(10, TimeUnit.MILLISECONDS)
.untilAsserted(() -> assertThatExceptionOfType(TimeoutException.class)
.isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS)));
future2.get(1000, TimeUnit.MILLISECONDS);
});
}
@Test
@@ -260,7 +252,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings({ "deprecation", "removal" })
@SuppressWarnings("removal")
void submitListenableCallable() {
TestCallable task = new TestCallable(this.testName, 1);
// Act
@@ -275,7 +267,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings({ "deprecation", "removal" })
@SuppressWarnings("removal")
void submitFailingListenableCallable() {
TestCallable task = new TestCallable(this.testName, 0);
// Act
@@ -291,7 +283,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
}
@Test
@SuppressWarnings({ "deprecation", "removal" })
@SuppressWarnings("removal")
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));
@@ -16,8 +16,12 @@
package org.springframework.scheduling.config;
import io.micrometer.observation.tck.TestObservationRegistry;
import org.junit.jupiter.api.Test;
import org.springframework.scheduling.SchedulingAwareRunnable;
import org.springframework.scheduling.support.ScheduledMethodRunnable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
@@ -72,6 +76,18 @@ class TaskTests {
assertThat(executionOutcome.throwable()).isInstanceOf(IllegalStateException.class);
}
@Test
void shouldDelegateToSchedulingAwareRunnable() throws Exception {
ScheduledMethodRunnable methodRunnable = new ScheduledMethodRunnable(new TestRunnable(),
TestRunnable.class.getMethod("run"), "myScheduler", TestObservationRegistry::create);
Task task = new Task(methodRunnable);
assertThat(task.getRunnable()).isInstanceOf(SchedulingAwareRunnable.class);
SchedulingAwareRunnable actual = (SchedulingAwareRunnable) task.getRunnable();
assertThat(actual.getQualifier()).isEqualTo(methodRunnable.getQualifier());
assertThat(actual.isLongLived()).isEqualTo(methodRunnable.isLongLived());
}
static class TestRunnable implements Runnable {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,6 +22,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import jakarta.validation.Constraint;
import jakarta.validation.ConstraintValidator;
@@ -31,6 +33,8 @@ import jakarta.validation.Valid;
import jakarta.validation.constraints.Pattern;
import org.hibernate.validator.internal.constraintvalidators.bv.PatternValidator;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.hint.MemberCategory;
@@ -40,6 +44,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RegisteredBean;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.core.OverridingClassLoader;
import org.springframework.lang.Nullable;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
@@ -121,6 +126,23 @@ class BeanValidationBeanRegistrationAotProcessorTests {
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
}
@ParameterizedTest // gh-33936
@ValueSource(classes = {BeanWithRecursiveIterable.class, BeanWithRecursiveMap.class, BeanWithRecursiveOptional.class})
void shouldProcessRecursiveGenericsWithoutInfiniteRecursion(Class<?> beanClass) {
process(beanClass);
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(1);
assertThat(RuntimeHintsPredicates.reflection().onType(beanClass)
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
}
@Test // gh-33940
void shouldSkipConstraintWithMissingDependency() throws Exception {
MissingDependencyClassLoader classLoader = new MissingDependencyClassLoader(getClass().getClassLoader());
Class<?> beanClass = classLoader.loadClass(ConstraintWithMissingDependency.class.getName());
process(beanClass);
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).isEmpty();
}
private void process(Class<?> beanClass) {
BeanRegistrationAotContribution contribution = createContribution(beanClass);
if (contribution != null) {
@@ -244,4 +266,43 @@ class BeanValidationBeanRegistrationAotProcessorTests {
}
}
static class BeanWithRecursiveIterable {
Iterable<BeanWithRecursiveIterable> iterable;
}
static class BeanWithRecursiveMap {
Map<BeanWithRecursiveMap, BeanWithRecursiveMap> map;
}
static class BeanWithRecursiveOptional {
Optional<BeanWithRecursiveOptional> optional;
}
static class ConstraintWithMissingDependency {
MissingType missingType;
}
static class MissingType {}
static class MissingDependencyClassLoader extends OverridingClassLoader {
MissingDependencyClassLoader(ClassLoader parent) {
super(parent);
}
@Override
protected boolean isEligibleForOverriding(String className) {
return className.startsWith(BeanValidationBeanRegistrationAotProcessorTests.class.getName());
}
@Override
protected Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
if (name.contains("MissingType")) {
throw new NoClassDefFoundError(name);
}
return super.loadClassForOverriding(name);
}
}
}
@@ -0,0 +1,35 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.testfixture.context.annotation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class AutowiredCglibConfiguration {
@Autowired
private Environment environment;
@Bean
public String text() {
return this.environment.getProperty("hello") + " World";
}
}
@@ -0,0 +1,41 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.testfixture.context.annotation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
public class AutowiredMixedCglibConfiguration {
@Value("${world:World}")
private String world;
private final Environment environment;
public AutowiredMixedCglibConfiguration(Environment environment) {
this.environment = environment;
}
@Bean
public String text() {
return this.environment.getProperty("hello") + " " + this.world;
}
}
@@ -0,0 +1,34 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.testfixture.context.annotation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ValueCglibConfiguration {
private final String name;
public ValueCglibConfiguration(@Value("${name:World}") String name) {
this.name = name;
}
public String getName() {
return this.name;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -124,6 +124,14 @@ public enum MemberCategory {
* reflection for inner classes but rather makes sure they are available
* via a call to {@link Class#getDeclaredClasses}.
*/
DECLARED_CLASSES
DECLARED_CLASSES,
/**
* A category that represents the need for
* {@link sun.misc.Unsafe#allocateInstance(Class) unsafe allocation}
* for this type.
* @since 6.2.1
*/
UNSAFE_ALLOCATED
}
@@ -124,6 +124,7 @@ class ReflectionHintsWriter {
attributes.put("allDeclaredMethods", true);
case PUBLIC_CLASSES -> attributes.put("allPublicClasses", true);
case DECLARED_CLASSES -> attributes.put("allDeclaredClasses", true);
case UNSAFE_ALLOCATED -> attributes.put("unsafeAllocated", true);
}
}
);
@@ -44,7 +44,6 @@ import kotlinx.coroutines.reactor.ReactorFlowKt;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.publisher.SynchronousSink;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@@ -109,7 +108,7 @@ public abstract class CoroutinesUtils {
* @throws IllegalArgumentException if {@code method} is not a suspending function
* @since 6.0
*/
@SuppressWarnings({"deprecation", "DataFlowIssue", "NullAway"})
@SuppressWarnings({"DataFlowIssue", "NullAway"})
public static Publisher<?> invokeSuspendingFunction(
CoroutineContext context, Method method, @Nullable Object target, @Nullable Object... args) {
@@ -146,7 +145,7 @@ public abstract class CoroutinesUtils {
}
return KCallables.callSuspendBy(function, argMap, continuation);
})
.handle(CoroutinesUtils::handleResult)
.filter(result -> result != Unit.INSTANCE)
.onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException);
KType returnType = function.getReturnType();
@@ -166,22 +165,4 @@ public abstract class CoroutinesUtils {
return ReactorFlowKt.asFlux(((Flow<?>) flow));
}
private static void handleResult(Object result, SynchronousSink<Object> sink) {
if (result == Unit.INSTANCE) {
sink.complete();
}
else if (KotlinDetector.isInlineClass(result.getClass())) {
try {
sink.next(result.getClass().getDeclaredMethod("unbox-impl").invoke(result));
sink.complete();
}
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
sink.error(ex);
}
}
else {
sink.next(result);
sink.complete();
}
}
}
@@ -334,19 +334,19 @@ public class ResolvableType implements Serializable {
// Deal with wildcard bounds
WildcardBounds ourBounds = WildcardBounds.get(this);
WildcardBounds typeBounds = WildcardBounds.get(other);
WildcardBounds otherBounds = WildcardBounds.get(other);
// In the form X is assignable to <? extends Number>
if (typeBounds != null) {
if (otherBounds != null) {
if (ourBounds != null) {
return (ourBounds.isSameKind(typeBounds) &&
ourBounds.isAssignableFrom(typeBounds.getBounds(), matchedBefore));
return (ourBounds.isSameKind(otherBounds) &&
ourBounds.isAssignableFrom(otherBounds.getBounds(), matchedBefore));
}
else if (upUntilUnresolvable) {
return typeBounds.isAssignableFrom(this, matchedBefore);
return otherBounds.isAssignableFrom(this, matchedBefore);
}
else if (!exactMatch) {
return typeBounds.isAssignableTo(this, matchedBefore);
return otherBounds.isAssignableTo(this, matchedBefore);
}
else {
return false;
@@ -400,8 +400,8 @@ public class ResolvableType implements Serializable {
if (checkGenerics) {
// Recursively check each generic
ResolvableType[] ourGenerics = getGenerics();
ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics();
if (ourGenerics.length != typeGenerics.length) {
ResolvableType[] otherGenerics = other.as(ourResolved).getGenerics();
if (ourGenerics.length != otherGenerics.length) {
return false;
}
if (ourGenerics.length > 0) {
@@ -410,7 +410,8 @@ public class ResolvableType implements Serializable {
}
matchedBefore.put(this.type, other.type);
for (int i = 0; i < ourGenerics.length; i++) {
if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore, upUntilUnresolvable)) {
if (!ourGenerics[i].isAssignableFrom(otherGenerics[i],
!other.hasUnresolvableGenerics(), matchedBefore, upUntilUnresolvable)) {
return false;
}
}
@@ -34,7 +34,6 @@ import org.springframework.lang.Contract;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* Contextual descriptor about a type to convert from or to.
@@ -501,16 +500,7 @@ public class TypeDescriptor implements Serializable {
if (!annotationsMatch(otherDesc)) {
return false;
}
if (isCollection() || isArray()) {
return ObjectUtils.nullSafeEquals(getElementTypeDescriptor(), otherDesc.getElementTypeDescriptor());
}
else if (isMap()) {
return (ObjectUtils.nullSafeEquals(getMapKeyTypeDescriptor(), otherDesc.getMapKeyTypeDescriptor()) &&
ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor()));
}
else {
return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics());
}
return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics());
}
private boolean annotationsMatch(TypeDescriptor otherDesc) {
@@ -978,8 +978,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
if (!Files.exists(rootPath)) {
if (logger.isInfoEnabled()) {
logger.info("Skipping search for files matching pattern [%s]: directory [%s] does not exist"
if (logger.isDebugEnabled()) {
logger.debug("Skipping search for files matching pattern [%s]: directory [%s] does not exist"
.formatted(subPattern, rootPath.toAbsolutePath()));
}
return result;
@@ -26,7 +26,9 @@ import java.util.function.Consumer;
import org.springframework.lang.Nullable;
/**
* Composite map that combines two other maps. This type is created via
* Composite map that combines two other maps.
*
* <p>This type is created via
* {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}.
*
* @author Arjen Poutsma
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,13 +98,11 @@ public class FastByteArrayOutputStream extends OutputStream {
if (this.closed) {
throw new IOException("Stream closed");
}
else {
if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
addBuffer(1);
}
// store the byte
this.buffers.getLast()[this.index++] = (byte) datum;
if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
addBuffer(1);
}
// store the byte
this.buffers.getLast()[this.index++] = (byte) datum;
}
@Override
@@ -384,22 +382,20 @@ public class FastByteArrayOutputStream extends OutputStream {
// This stream doesn't have any data in it...
return -1;
}
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
this.totalBytesRead++;
return this.currentBuffer[this.nextIndexInCurrentBuffer++] & 0xFF;
}
else {
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
this.totalBytesRead++;
return this.currentBuffer[this.nextIndexInCurrentBuffer++] & 0xFF;
if (this.buffersIterator.hasNext()) {
this.currentBuffer = this.buffersIterator.next();
updateCurrentBufferLength();
this.nextIndexInCurrentBuffer = 0;
}
else {
if (this.buffersIterator.hasNext()) {
this.currentBuffer = this.buffersIterator.next();
updateCurrentBufferLength();
this.nextIndexInCurrentBuffer = 0;
}
else {
this.currentBuffer = null;
}
return read();
this.currentBuffer = null;
}
return read();
}
}
@@ -23,8 +23,9 @@ import java.util.function.Predicate;
import org.springframework.lang.Nullable;
/**
* Iterator that filters out values that do not match a predicate.
* This type is used by {@link CompositeMap}.
* {@link Iterator} that filters out values that do not match a predicate.
*
* <p>This type is used by {@link CompositeMap}.
*
* @author Arjen Poutsma
* @since 6.2
@@ -39,7 +40,7 @@ final class FilteredIterator<E> implements Iterator<E> {
@Nullable
private E next;
private boolean nextSet;
private boolean hasNext;
public FilteredIterator(Iterator<E> delegate, Predicate<E> filter) {
@@ -52,22 +53,15 @@ final class FilteredIterator<E> implements Iterator<E> {
@Override
public boolean hasNext() {
if (this.nextSet) {
return true;
}
else {
return setNext();
}
return (this.hasNext || setNext());
}
@Override
public E next() {
if (!this.nextSet) {
if (!setNext()) {
throw new NoSuchElementException();
}
if (!this.hasNext && !setNext()) {
throw new NoSuchElementException();
}
this.nextSet = false;
this.hasNext = false;
Assert.state(this.next != null, "Next should not be null");
return this.next;
}
@@ -77,7 +71,7 @@ final class FilteredIterator<E> implements Iterator<E> {
E next = this.delegate.next();
if (this.filter.test(next)) {
this.next = next;
this.nextSet = true;
this.hasNext = true;
return true;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -226,14 +226,12 @@ public class MethodInvoker {
Method matchingMethod = null;
for (Method candidate : candidates) {
if (candidate.getName().equals(targetMethod)) {
if (candidate.getParameterCount() == argCount) {
Class<?>[] paramTypes = candidate.getParameterTypes();
int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments);
if (typeDiffWeight < minTypeDiffWeight) {
minTypeDiffWeight = typeDiffWeight;
matchingMethod = candidate;
}
if (candidate.getName().equals(targetMethod) && candidate.getParameterCount() == argCount) {
Class<?>[] paramTypes = candidate.getParameterTypes();
int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments);
if (typeDiffWeight < minTypeDiffWeight) {
minTypeDiffWeight = typeDiffWeight;
matchingMethod = candidate;
}
}
}
@@ -243,9 +243,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
if (s.length() < 2) {
return false;
}
else {
return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));
}
return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));
}
protected String unquote(String s) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -110,10 +110,8 @@ public abstract class NumberUtils {
// do not lose precision - use BigDecimal's own conversion
return (T) bigDecimal.toBigInteger();
}
else {
// original value is not a Big* number - use standard long conversion
return (T) BigInteger.valueOf(number.longValue());
}
// original value is not a Big* number - use standard long conversion
return (T) BigInteger.valueOf(number.longValue());
}
else if (Float.class == targetClass) {
return (T) Float.valueOf(number.floatValue());
@@ -138,6 +138,7 @@ public abstract class ObjectUtils {
* @see CollectionUtils#isEmpty(java.util.Collection)
* @see CollectionUtils#isEmpty(java.util.Map)
*/
@Contract("null -> true")
public static boolean isEmpty(@Nullable Object obj) {
if (obj == null) {
return true;
@@ -702,8 +703,7 @@ public abstract class ObjectUtils {
if (array == null) {
return NULL_STRING;
}
int length = array.length;
if (length == 0) {
if (array.length == 0) {
return EMPTY_ARRAY;
}
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
@@ -726,8 +726,7 @@ public abstract class ObjectUtils {
if (array == null) {
return NULL_STRING;
}
int length = array.length;
if (length == 0) {
if (array.length == 0) {
return EMPTY_ARRAY;
}
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
@@ -750,8 +749,7 @@ public abstract class ObjectUtils {
if (array == null) {
return NULL_STRING;
}
int length = array.length;
if (length == 0) {
if (array.length == 0) {
return EMPTY_ARRAY;
}
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
@@ -774,8 +772,7 @@ public abstract class ObjectUtils {
if (array == null) {
return NULL_STRING;
}
int length = array.length;
if (length == 0) {
if (array.length == 0) {
return EMPTY_ARRAY;
}
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
@@ -798,8 +795,7 @@ public abstract class ObjectUtils {
if (array == null) {
return NULL_STRING;
}
int length = array.length;
if (length == 0) {
if (array.length == 0) {
return EMPTY_ARRAY;
}
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
@@ -846,8 +842,7 @@ public abstract class ObjectUtils {
if (array == null) {
return NULL_STRING;
}
int length = array.length;
if (length == 0) {
if (array.length == 0) {
return EMPTY_ARRAY;
}
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
@@ -247,7 +247,7 @@ final class PlaceholderParser {
if (!parts.isEmpty()) {
Part current = parts.removeLast();
if (current instanceof TextPart textPart) {
parts.add(new TextPart(textPart.text + text));
parts.add(new TextPart(textPart.text() + text));
}
else {
parts.add(current);
@@ -420,51 +420,42 @@ final class PlaceholderParser {
/**
* A {@link Part} implementation that does not contain a valid placeholder.
* @param text the raw (and resolved) text
* A base {@link Part} implementation.
*/
record TextPart(String text) implements Part {
abstract static class AbstractPart implements Part {
private final String text;
protected AbstractPart(String text) {
this.text = text;
}
@Override
public String resolve(PartResolutionContext resolutionContext) {
public String text() {
return this.text;
}
}
/**
* A {@link Part} implementation that represents a single placeholder with
* a hard-coded fallback.
* @param text the raw text
* @param key the key of the placeholder
* @param fallback the fallback to use, if any
*/
record SimplePlaceholderPart(String text, String key, @Nullable String fallback) implements Part {
@Override
public String resolve(PartResolutionContext resolutionContext) {
String resolvedValue = resolveToText(resolutionContext, this.key);
if (resolvedValue != null) {
return resolvedValue;
}
else if (this.fallback != null) {
return this.fallback;
}
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
}
/**
* Resolve the placeholder with the given {@code key}. If the result of such
* resolution return other placeholders, those are resolved as well until the
* resolution no longer contains any placeholders.
* @param resolutionContext the resolution context to use
* @param key the initial placeholder
* @return the full resolution of the given {@code key} or {@code null} if
* the placeholder has no value to begin with
*/
@Nullable
private String resolveToText(PartResolutionContext resolutionContext, String text) {
String resolvedValue = resolutionContext.resolvePlaceholder(text);
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
String resolvedValue = resolutionContext.resolvePlaceholder(key);
if (resolvedValue != null) {
resolutionContext.flagPlaceholderAsVisited(text);
resolutionContext.flagPlaceholderAsVisited(key);
// Let's check if we need to recursively resolve that value
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
String value = toText(nestedParts);
if (!isTextOnly(nestedParts)) {
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
}
resolutionContext.removePlaceholder(text);
resolutionContext.removePlaceholder(key);
return value;
}
// Not found
@@ -483,26 +474,97 @@ final class PlaceholderParser {
}
/**
* A {@link Part} implementation that does not contain a valid placeholder.
*/
static class TextPart extends AbstractPart {
/**
* Create a new instance.
* @param text the raw (and resolved) text
*/
public TextPart(String text) {
super(text);
}
@Override
public String resolve(PartResolutionContext resolutionContext) {
return text();
}
}
/**
* A {@link Part} implementation that represents a single placeholder with
* a hard-coded fallback.
*/
static class SimplePlaceholderPart extends AbstractPart {
private final String key;
@Nullable
private final String fallback;
/**
* Create a new instance.
* @param text the raw text
* @param key the key of the placeholder
* @param fallback the fallback to use, if any
*/
public SimplePlaceholderPart(String text,String key, @Nullable String fallback) {
super(text);
this.key = key;
this.fallback = fallback;
}
@Override
public String resolve(PartResolutionContext resolutionContext) {
String value = resolveRecursively(resolutionContext, this.key);
if (value != null) {
return value;
}
else if (this.fallback != null) {
return this.fallback;
}
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
}
}
/**
* A {@link Part} implementation that represents a single placeholder
* containing nested placeholders.
* @param text the raw text of the root placeholder
* @param keyParts the parts of the key
* @param defaultParts the parts of the fallback, if any
*/
record NestedPlaceholderPart(String text, List<Part> keyParts, @Nullable List<Part> defaultParts) implements Part {
static class NestedPlaceholderPart extends AbstractPart {
private final List<Part> keyParts;
@Nullable
private final List<Part> defaultParts;
/**
* Create a new instance.
* @param text the raw text of the root placeholder
* @param keyParts the parts of the key
* @param defaultParts the parts of the fallback, if any
*/
NestedPlaceholderPart(String text, List<Part> keyParts, @Nullable List<Part> defaultParts) {
super(text);
this.keyParts = keyParts;
this.defaultParts = defaultParts;
}
@Override
public String resolve(PartResolutionContext resolutionContext) {
String resolvedKey = Part.resolveAll(this.keyParts, resolutionContext);
String value = resolutionContext.resolvePlaceholder(resolvedKey);
String value = resolveRecursively(resolutionContext, resolvedKey);
if (value != null) {
return value;
}
else if (this.defaultParts != null) {
return Part.resolveAll(this.defaultParts, resolutionContext);
}
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -59,7 +59,7 @@ class ReflectionHintsWriterTests {
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS,
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES)
MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES, MemberCategory.UNSAFE_ALLOCATED)
.withField("DEFAULT_CHARSET")
.withField("defaultCharset")
.withField("aScore")
@@ -83,6 +83,7 @@ class ReflectionHintsWriterTests {
"allDeclaredMethods": true,
"allPublicClasses": true,
"allDeclaredClasses": true,
"unsafeAllocated": true,
"fields": [
{ "name": "aScore" },
{ "name": "DEFAULT_CHARSET" },
@@ -1188,6 +1188,26 @@ class ResolvableTypeTests {
assertThatResolvableType(complex4).isNotAssignableFrom(complex3);
}
@Test
void isAssignableFromForUnresolvedWildcards() {
ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>());
ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed());
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class);
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
assertThat(wildcard.isAssignableFromResolvedPart(wildcardFixed)).isTrue();
assertThat(wildcard.isAssignableFrom(wildcardConcrete)).isTrue();
assertThat(wildcard.isAssignableFromResolvedPart(wildcardConcrete)).isTrue();
assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse();
assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcard)).isFalse();
assertThat(wildcardFixed.isAssignableFrom(wildcardConcrete)).isFalse();
assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcardConcrete)).isFalse();
assertThat(wildcardConcrete.isAssignableFrom(wildcard)).isTrue();
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcard)).isTrue();
assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse();
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcardFixed)).isFalse();
}
@Test
void identifyTypeVariable() throws Exception {
Method method = ClassArguments.class.getMethod("typedArgumentFirst", Class.class, Class.class, Class.class);
@@ -1367,6 +1387,30 @@ class ResolvableTypeTests {
assertThat(type.hasUnresolvableGenerics()).isFalse();
}
@Test // gh-33932
void recursiveType() {
assertThat(ResolvableType.forClass(RecursiveMap.class)).isEqualTo(
ResolvableType.forClass(RecursiveMap.class));
ResolvableType resolvableType1 = ResolvableType.forClassWithGenerics(Map.class,
String.class, RecursiveMap.class);
ResolvableType resolvableType2 = ResolvableType.forClassWithGenerics(Map.class,
String.class, RecursiveMap.class);
assertThat(resolvableType1).isEqualTo(resolvableType2);
}
@Test // gh-33932
void recursiveTypeWithInterface() {
assertThat(ResolvableType.forClass(RecursiveMapWithInterface.class)).isEqualTo(
ResolvableType.forClass(RecursiveMapWithInterface.class));
ResolvableType resolvableType1 = ResolvableType.forClassWithGenerics(Map.class,
String.class, RecursiveMapWithInterface.class);
ResolvableType resolvableType2 = ResolvableType.forClassWithGenerics(Map.class,
String.class, RecursiveMapWithInterface.class);
assertThat(resolvableType1).isEqualTo(resolvableType2);
}
@Test
void spr11219() throws Exception {
ResolvableType type = ResolvableType.forField(BaseProvider.class.getField("stuff"), BaseProvider.class);
@@ -1685,7 +1729,6 @@ class ResolvableTypeTests {
}
}
public class MySimpleInterfaceType implements MyInterfaceType<String> {
}
@@ -1695,7 +1738,6 @@ class ResolvableTypeTests {
public abstract class ExtendsMySimpleInterfaceTypeWithImplementsRaw extends MySimpleInterfaceTypeWithImplementsRaw {
}
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {
}
@@ -1703,20 +1745,17 @@ class ResolvableTypeTests {
public abstract class MySuperclassType<T> {
}
public class MySimpleSuperclassType extends MySuperclassType<String> {
}
public class MyCollectionSuperclassType extends MySuperclassType<Collection<String>> {
}
interface Wildcard<T extends Number> extends List<T> {
public class Wildcard<T extends Number> {
}
interface RawExtendsWildcard extends Wildcard {
public class WildcardFixed extends Wildcard<Integer> {
}
@@ -1821,6 +1860,16 @@ class ResolvableTypeTests {
}
@SuppressWarnings("serial")
static class RecursiveMap extends HashMap<String, RecursiveMap> {
}
@SuppressWarnings("serial")
static class RecursiveMapWithInterface extends HashMap<String, RecursiveMapWithInterface>
implements Map<String, RecursiveMapWithInterface> {
}
private static class ResolvableTypeAssert extends AbstractAssert<ResolvableTypeAssert, ResolvableType>{
public ResolvableTypeAssert(ResolvableType actual) {
@@ -770,6 +770,30 @@ class TypeDescriptorTests {
assertThat(td1).isNotEqualTo(td2);
}
@Test // gh-33932
void recursiveType() {
assertThat(TypeDescriptor.valueOf(RecursiveMap.class)).isEqualTo(
TypeDescriptor.valueOf(RecursiveMap.class));
TypeDescriptor typeDescriptor1 = TypeDescriptor.map(Map.class,
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMap.class));
TypeDescriptor typeDescriptor2 = TypeDescriptor.map(Map.class,
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMap.class));
assertThat(typeDescriptor1).isEqualTo(typeDescriptor2);
}
@Test // gh-33932
void recursiveTypeWithInterface() {
assertThat(TypeDescriptor.valueOf(RecursiveMapWithInterface.class)).isEqualTo(
TypeDescriptor.valueOf(RecursiveMapWithInterface.class));
TypeDescriptor typeDescriptor1 = TypeDescriptor.map(Map.class,
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMapWithInterface.class));
TypeDescriptor typeDescriptor2 = TypeDescriptor.map(Map.class,
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMapWithInterface.class));
assertThat(typeDescriptor1).isEqualTo(typeDescriptor2);
}
// Methods designed for test introspection
@@ -987,6 +1011,16 @@ class TypeDescriptorTests {
}
@SuppressWarnings("serial")
static class RecursiveMap extends HashMap<String, RecursiveMap> {
}
@SuppressWarnings("serial")
static class RecursiveMapWithInterface extends HashMap<String, RecursiveMapWithInterface>
implements Map<String, RecursiveMapWithInterface> {
}
// Annotations used on tested elements
@Target({ElementType.PARAMETER})
@@ -210,6 +210,8 @@ class PlaceholderParserTests {
static Stream<Arguments> nestedPlaceholders() {
return Stream.of(
Arguments.of("${p6}", "v1:v2:def"),
Arguments.of("${p6:not-used}", "v1:v2:def"),
Arguments.of("${p6:${invalid}}", "v1:v2:def"),
Arguments.of("${invalid:${p1}:${p2}}", "v1:v2"),
Arguments.of("${invalid:${p3}}", "v1:v2"),
Arguments.of("${invalid:${p4}}", "v1:v2"),
@@ -192,7 +192,7 @@ class CoroutinesUtilsTests {
@Test
fun invokeSuspendingFunctionWithValueClassParameter() {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClass") }
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassParameter") }
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono
runBlocking {
Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo")
@@ -204,7 +204,16 @@ class CoroutinesUtilsTests {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassReturnValue") }
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono
runBlocking {
Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo")
Assertions.assertThat(mono.awaitSingle()).isEqualTo(ValueClass("foo"))
}
}
@Test
fun invokeSuspendingFunctionWithResultOfUnitReturnValue() {
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithResultOfUnitReturnValue") }
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono
runBlocking {
Assertions.assertThat(mono.awaitSingle()).isEqualTo(Result.success(Unit))
}
}
@@ -314,7 +323,7 @@ class CoroutinesUtilsTests {
return null
}
suspend fun suspendingFunctionWithValueClass(value: ValueClass): String {
suspend fun suspendingFunctionWithValueClassParameter(value: ValueClass): String {
delay(1)
return value.value
}
@@ -324,6 +333,11 @@ class CoroutinesUtilsTests {
return ValueClass("foo")
}
suspend fun suspendingFunctionWithResultOfUnitReturnValue(): Result<Unit> {
delay(1)
return Result.success(Unit)
}
suspend fun suspendingFunctionWithValueClassWithInit(value: ValueClassWithInit): String {
delay(1)
return value.value
@@ -36,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThatException;
* Tests invocation of constructors.
*
* @author Andy Clement
* @see MethodInvocationTests
* @see VariableAndFunctionTests
*/
class ConstructorInvocationTests extends AbstractExpressionTests {
@@ -48,6 +48,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Andy Clement
* @author Phillip Webb
* @author Sam Brannen
* @see ConstructorInvocationTests
* @see VariableAndFunctionTests
*/
class MethodInvocationTests extends AbstractExpressionTests {
@@ -21,10 +21,13 @@ import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import example.Color;
import example.FruitMap;
@@ -658,7 +661,8 @@ class SpelDocumentationTests extends AbstractExpressionTests {
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(String.class, "formatted",
MethodType.methodType(String.class, Object[].class))
.bindTo(template)
.bindTo(varargs); // here we have to provide arguments in a single array binding
// Here we have to provide the arguments in a single array binding:
.bindTo(varargs);
context.registerFunction("message", methodHandle);
String message = parser.parseExpression("#message()").getValue(context, String.class);
@@ -666,6 +670,59 @@ class SpelDocumentationTests extends AbstractExpressionTests {
}
}
@Nested
class Varargs {
@Test
void varargsMethodInvocationWithIndividualArguments() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted('blue', 1)";
String message = parser.parseExpression(expression)
.getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
@Test
void varargsMethodInvocationWithArgumentsAsObjectArray() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
String message = parser.parseExpression(expression)
.getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
@Test
void varargsMethodInvocationWithArgumentsAsInlineList() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%d'.formatted({'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
@Test
void varargsMethodInvocationWithTypeConversion() {
Method reverseStringsMethod = ReflectionUtils.findMethod(StringUtils.class, "reverseStrings", String[].class);
SimpleEvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
evaluationContext.setVariable("reverseStrings", reverseStringsMethod);
// String reverseStrings(String... strings)
// evaluates to "3.0, 2.0, 1.0, SpEL"
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
String message = parser.parseExpression(expression)
.getValue(evaluationContext, String.class);
assertThat(message).isEqualTo("3.0, 2.0, 1, SpEL");
}
@Test
void varargsMethodInvocationWithArgumentsAsStringArray() {
// evaluates to "blue is color #1"
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
String message = parser.parseExpression(expression).getValue(String.class);
assertThat(message).isEqualTo("blue is color #1");
}
}
@Nested
class TernaryOperator {
@@ -905,6 +962,12 @@ class SpelDocumentationTests extends AbstractExpressionTests {
public static String reverseString(String input) {
return new StringBuilder(input).reverse().toString();
}
public static String reverseStrings(String... strings) {
List<String> list = Arrays.asList(strings);
Collections.reverse(list);
return list.stream().collect(Collectors.joining(", "));
}
}
private static class ListConcatenation implements OperatorOverloader {
@@ -32,6 +32,8 @@ import static org.springframework.expression.spel.SpelMessage.INCORRECT_NUMBER_O
*
* @author Andy Clement
* @author Sam Brannen
* @see ConstructorInvocationTests
* @see MethodInvocationTests
*/
class VariableAndFunctionTests extends AbstractExpressionTests {
@@ -17,6 +17,7 @@
package org.springframework.jdbc.datasource;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Savepoint;
import org.springframework.lang.Nullable;
@@ -179,6 +180,9 @@ public abstract class JdbcTransactionObjectSupport implements SavepointManager,
try {
conHolder.getConnection().releaseSavepoint((Savepoint) savepoint);
}
catch (SQLFeatureNotSupportedException ex) {
// typically on Oracle - ignore
}
catch (Throwable ex) {
throw new TransactionSystemException("Could not explicitly release JDBC savepoint", ex);
}
@@ -55,7 +55,7 @@ import static org.mockito.Mockito.verify;
*/
class SqlQueryTests {
//FIXME inline?
// FIXME inline?
private static final String SELECT_ID =
"select id from custmr";
private static final String SELECT_ID_WHERE =
@@ -67,7 +67,7 @@ class H2SequenceMaxValueIncrementerTests {
* Tests that the incrementer works when using all supported H2 <em>compatibility modes</em>.
*/
@ParameterizedTest
@EnumSource(ModeEnum.class)
@EnumSource
void incrementsSequenceWithExplicitH2CompatibilityMode(ModeEnum mode) {
String connectionUrl = String.format("jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=%s", UUID.randomUUID(), mode);
DataSource dataSource = new SimpleDriverDataSource(new org.h2.Driver(), connectionUrl, "sa", "");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -102,7 +102,7 @@ import org.springframework.util.ErrorHandler;
* (i.e. after your business logic executed but before the JMS part got committed),
* so duplicate message detection is just there to cover a corner case.
* <li>Or wrap your <i>entire processing with an XA transaction</i>, covering the
* reception of the JMS message as well as the execution of the business logic in
* receipt of the JMS message as well as the execution of the business logic in
* your message listener (including database operations etc). This is only
* supported by {@link DefaultMessageListenerContainer}, through specifying
* an external "transactionManager" (typically a
@@ -45,7 +45,7 @@ import org.springframework.util.Assert;
*
* <p>This listener container variant is built for repeated polling attempts,
* each invoking the {@link #receiveAndExecute} method. The MessageConsumer used
* may be reobtained fo reach attempt or cached in between attempts; this is up
* may be reobtained for each attempt or cached in between attempts; this is up
* to the concrete implementation. The receive timeout for each attempt can be
* configured through the {@link #setReceiveTimeout "receiveTimeout"} property.
*
@@ -56,7 +56,7 @@ import org.springframework.util.Assert;
* full control over the listening process, allowing for custom scaling and throttling
* and of concurrent message processing (which is up to concrete subclasses).
*
* <p>Message reception and listener execution can automatically be wrapped
* <p>Message receipt and listener execution can automatically be wrapped
* in transactions through passing a Spring
* {@link org.springframework.transaction.PlatformTransactionManager} into the
* {@link #setTransactionManager "transactionManager"} property. This will usually
@@ -105,7 +105,7 @@ public abstract class AbstractPollingMessageListenerContainer extends AbstractMe
/**
* Specify the Spring {@link org.springframework.transaction.PlatformTransactionManager}
* to use for transactional wrapping of message reception plus listener execution.
* to use for transactional wrapping of message receipt plus listener execution.
* <p>Default is none, not performing any transactional wrapping.
* If specified, this will usually be a Spring
* {@link org.springframework.transaction.jta.JtaTransactionManager} or one
@@ -131,7 +131,7 @@ public abstract class AbstractPollingMessageListenerContainer extends AbstractMe
/**
* Return the Spring PlatformTransactionManager to use for transactional
* wrapping of message reception plus listener execution.
* wrapping of message receipt plus listener execution.
*/
@Nullable
protected final PlatformTransactionManager getTransactionManager() {
@@ -47,7 +47,7 @@ import org.springframework.util.backoff.FixedBackOff;
/**
* Message listener container variant that uses plain JMS client APIs, specifically
* a loop of {@code MessageConsumer.receive()} calls that also allow for
* transactional reception of messages (registering them with XA transactions).
* transactional receipt of messages (registering them with XA transactions).
* Designed to work in a native JMS environment as well as in a Jakarta EE environment,
* with only minimal differences in configuration.
*
@@ -70,7 +70,7 @@ import org.springframework.util.backoff.FixedBackOff;
* {@code MessageConsumer} (only refreshed in case of failure), using the JMS provider's
* resources as efficiently as possible.
*
* <p>Message reception and listener execution can automatically be wrapped
* <p>Message receipt and listener execution can automatically be wrapped
* in transactions by passing a Spring
* {@link org.springframework.transaction.PlatformTransactionManager} into the
* {@link #setTransactionManager "transactionManager"} property. This will usually
@@ -474,7 +474,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
/**
* Specify the maximum number of messages to process in one task.
* More concretely, this limits the number of message reception attempts
* More concretely, this limits the number of message receipt attempts
* per task, which includes receive iterations that did not actually
* pick up a message until they hit their timeout (see the
* {@link #setReceiveTimeout "receiveTimeout"} property).
@@ -562,7 +562,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
* The minimum number of consumers
* (see {@link #setConcurrentConsumers "concurrentConsumers"})
* will be kept around until shutdown in any case.
* <p>Within each task execution, a number of message reception attempts
* <p>Within each task execution, a number of message receipt attempts
* (according to the "maxMessagesPerTask" setting) will each wait for an incoming
* message (according to the "receiveTimeout" setting). If all of those receive
* attempts in a given task return without a message, the task is considered
@@ -56,7 +56,7 @@ import org.springframework.util.Assert;
*
* <p>For a different style of MessageListener handling, through looped
* {@code MessageConsumer.receive()} calls that also allow for
* transactional reception of messages (registering them with XA transactions),
* transactional receipt of messages (registering them with XA transactions),
* see {@link DefaultMessageListenerContainer}.
*
* @author Juergen Hoeller
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2024 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.
@@ -89,7 +89,7 @@ public interface JmsHeaders {
/**
* Specify if the message was resent. This occurs when a message
* consumer fails to acknowledge the message reception.
* consumer fails to acknowledge receipt of the message.
* <p>Read-only value.
* @see jakarta.jms.Message#getJMSRedelivered()
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,9 +36,9 @@ public interface PollableChannel extends MessageChannel {
/**
* Receive a message from this channel, blocking until either a message is available
* or the specified timeout period elapses.
* @param timeout the timeout in milliseconds or {@link MessageChannel#INDEFINITE_TIMEOUT}.
* @param timeout the timeout in milliseconds or {@link MessageChannel#INDEFINITE_TIMEOUT}
* @return the next available {@link Message} or {@code null} if the specified timeout
* period elapses or the message reception is interrupted
* period elapses or the message receipt is interrupted
*/
@Nullable
Message<?> receive(long timeout);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2024 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,9 +22,7 @@ import org.springframework.messaging.Message;
/**
* Convenient base class for {@link AsyncHandlerMethodReturnValueHandler}
* implementations that support only asynchronous (Future-like) return values
* and merely serve as adapters of such types to Spring's
* {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}.
* implementations that support only asynchronous (Future-like) return values.
*
* @author Sebastien Deleuze
* @since 4.2
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -573,7 +573,7 @@ public abstract class AbstractMethodMessageHandler<T>
if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) {
CompletableFuture<?> future = this.returnValueHandlers.toCompletableFuture(returnValue, returnType);
if (future != null) {
future.whenComplete(new ReturnValueListenableFutureCallback(invocable, message));
future.whenComplete(new ReturnValueCallback(invocable, message));
}
}
else {
@@ -704,13 +704,13 @@ public abstract class AbstractMethodMessageHandler<T>
}
private class ReturnValueListenableFutureCallback implements BiConsumer<Object, Throwable> {
private class ReturnValueCallback implements BiConsumer<Object, Throwable> {
private final InvocableHandlerMethod handlerMethod;
private final Message<?> message;
public ReturnValueListenableFutureCallback(InvocableHandlerMethod handlerMethod, Message<?> message) {
public ReturnValueCallback(InvocableHandlerMethod handlerMethod, Message<?> message) {
this.handlerMethod = handlerMethod;
this.message = message;
}
@@ -24,8 +24,6 @@ import org.springframework.lang.Nullable;
/**
* An extension of {@link HandlerMethodReturnValueHandler} for handling async,
* Future-like return value types that support success and error callbacks.
* Essentially anything that can be adapted to a
* {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}.
*
* <p>Implementations should consider extending the convenient base class
* {@link AbstractAsyncReturnValueHandler}.
@@ -181,7 +181,7 @@ class ChannelSendOperator<T> extends Mono<Void> implements Scannable {
requiredWriteSubscriber().onNext(item);
return;
}
//FIXME revisit in case of reentrant sync deadlock
// FIXME revisit in case of reentrant sync deadlock
synchronized (this) {
if (this.state == State.READY_TO_WRITE) {
requiredWriteSubscriber().onNext(item);
@@ -30,7 +30,7 @@ import org.springframework.aot.hint.annotation.Reflective;
* <p>Specifying this annotation registers the configured {@link BeanOverrideProcessor}
* which must be capable of handling the composed annotation and its attributes.
*
* <p>Since the composed annotation should only be applied to fields, it is
* <p>Since the composed annotation should only be applied to non-static fields, it is
* expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}.
*
* <p>For concrete examples, see
@@ -18,12 +18,12 @@ package org.springframework.test.context.bean.override;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
@@ -44,7 +44,7 @@ import org.springframework.util.Assert;
/**
* A {@link BeanFactoryPostProcessor} implementation that processes identified
* use of {@link BeanOverride @BeanOverride} and adapts the {@link BeanFactory}
* use of {@link BeanOverride @BeanOverride} and adapts the {@code BeanFactory}
* accordingly.
*
* <p>For each override, the bean factory is prepared according to the chosen
@@ -94,12 +94,15 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
Set<String> generatedBeanNames = new HashSet<>();
for (BeanOverrideHandler handler : this.beanOverrideHandlers) {
registerBeanOverride(beanFactory, handler);
registerBeanOverride(beanFactory, handler, generatedBeanNames);
}
}
private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) {
private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
Set<String> generatedBeanNames) {
String beanName = handler.getBeanName();
Field field = handler.getField();
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
@@ -108,27 +111,43 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
beanName, field.getDeclaringClass().getSimpleName(), field.getName()));
switch (handler.getStrategy()) {
case REPLACE -> replaceOrCreateBean(beanFactory, handler, true);
case REPLACE_OR_CREATE -> replaceOrCreateBean(beanFactory, handler, false);
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
case REPLACE_OR_CREATE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, false);
case WRAP -> wrapBean(beanFactory, handler);
}
}
private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean requireExistingBean) {
Set<String> generatedBeanNames, boolean requireExistingBean) {
// NOTE: This method supports 3 distinct scenarios which must be accounted for.
//
// 1) JVM runtime
// 2) AOT processing
// 3) AOT runtime
// - JVM runtime
// - AOT processing
// - AOT runtime
//
// In addition, this method supports 4 distinct use cases.
//
// 1) Override existing bean by-type
// 2) Create bean by-type, with a generated name
// 3) Override existing bean by-name
// 4) Create bean by-name, with a provided name
String beanName = handler.getBeanName();
Field field = handler.getField();
BeanDefinition existingBeanDefinition = null;
if (beanName == null) {
beanName = getBeanNameForType(beanFactory, handler, requireExistingBean);
if (beanName != null) {
// We are overriding an existing bean by-type.
// If the generatedBeanNames set already contains the beanName that we
// just found by-type, that means we are experiencing a "phantom read"
// (i.e., we found a bean that was not previously there). Consequently,
// we cannot "override the override", because we would lose one of the
// overrides. Instead, we must create a new override for the current
// handler. For example, if one handler creates an override for a SubType
// and a subsequent handler creates an override for a SuperType of that
// SubType, we must end up with overrides for both SuperType and SubType.
if (beanName != null && !generatedBeanNames.contains(beanName)) {
// 1) We are overriding an existing bean by-type.
beanName = BeanFactoryUtils.transformedBeanName(beanName);
// If we are overriding a manually registered singleton, we won't find
// an existing bean definition.
@@ -137,23 +156,26 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
}
else {
// We will later generate a name for the nonexistent bean, but since NullAway
// will reject leaving the beanName set to null, we set it to a placeholder.
// 2) We are creating a bean by-type, with a generated name.
// Since NullAway will reject leaving the beanName set to null,
// we set it to a placeholder that will be replaced later.
beanName = PSEUDO_BEAN_NAME_PLACEHOLDER;
}
}
else {
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
if (candidates.contains(beanName)) {
// We are overriding an existing bean by-name.
// 3) We are overriding an existing bean by-name.
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
}
else if (requireExistingBean) {
throw new IllegalStateException("""
Unable to override bean: there is no bean to replace \
with name [%s] and type [%s]."""
.formatted(beanName, handler.getBeanType()));
Unable to replace bean: there is no bean with name '%s' and type %s \
(as required by field '%s.%s')."""
.formatted(beanName, handler.getBeanType(),
field.getDeclaringClass().getSimpleName(), field.getName()));
}
// 4) We are creating a bean by-name with the provided beanName.
}
if (existingBeanDefinition != null) {
@@ -179,7 +201,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
if (!(beanFactory instanceof BeanDefinitionRegistry registry)) {
throw new IllegalStateException("Cannot process bean override with a BeanFactory " +
"that doesn't implement BeanDefinitionRegistry: " + beanFactory.getClass().getName());
"that does not implement BeanDefinitionRegistry: " + beanFactory.getClass().getName());
}
RootBeanDefinition pseudoBeanDefinition = createPseudoBeanDefinition(handler);
@@ -187,6 +209,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
// Generate a name for the nonexistent bean.
if (PSEUDO_BEAN_NAME_PLACEHOLDER.equals(beanName)) {
beanName = beanNameGenerator.generateBeanName(pseudoBeanDefinition, registry);
generatedBeanNames.add(beanName);
}
registry.registerBeanDefinition(beanName, pseudoBeanDefinition);
@@ -212,14 +235,18 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
/**
* Check that a bean with the specified {@link BeanOverrideHandler#getBeanName() name}
* and {@link BeanOverrideHandler#getBeanType() type} is registered.
* <p>If so, put the {@link BeanOverrideHandler} in the early tracking map.
* <p>The map will later be checked to see if a given bean should be wrapped
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference}
* phase.
* or {@link BeanOverrideHandler#getBeanType() type} has already been registered
* in the {@code BeanFactory}.
* <p>If so, register the {@link BeanOverrideHandler} and the corresponding bean
* name in the {@link BeanOverrideRegistry}.
* <p>The registry will later be checked to see if a given bean should be wrapped
* upon creation, during the early bean post-processing phase.
* @see BeanOverrideRegistry#registerBeanOverrideHandler(BeanOverrideHandler, String)
* @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)
*/
private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) {
String beanName = handler.getBeanName();
Field field = handler.getField();
ResolvableType beanType = handler.getBeanType();
if (beanName == null) {
@@ -235,13 +262,17 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
beanName = primaryCandidate;
}
else {
Field field = handler.getField();
throw new IllegalStateException("""
Unable to select a bean to override by wrapping: found %d bean instances of type %s \
(as required by annotated field '%s.%s')%s"""
String message = "Unable to select a bean to wrap: ";
if (candidateCount == 0) {
message += "there are no beans of type %s (as required by field '%s.%s')."
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName());
}
else {
message += "found %d beans of type %s (as required by field '%s.%s'): %s"
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
field.getName(), (candidateCount > 0 ? ": " + candidateNames : "")));
field.getName(), candidateNames);
}
throw new IllegalStateException(message);
}
}
beanName = BeanFactoryUtils.transformedBeanName(beanName);
@@ -251,9 +282,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
if (!candidates.contains(beanName)) {
throw new IllegalStateException("""
Unable to override bean by wrapping: there is no existing bean \
with name [%s] and type [%s]."""
.formatted(beanName, beanType));
Unable to wrap bean: there is no bean with name '%s' and type %s \
(as required by field '%s.%s')."""
.formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(),
field.getName()));
}
}
@@ -276,7 +308,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
else if (candidateCount == 0) {
if (requireExistingBean) {
throw new IllegalStateException(
"Unable to override bean: no beans of type %s (as required by annotated field '%s.%s')"
"Unable to override bean: there are no beans of type %s (as required by field '%s.%s')."
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()));
}
return null;
@@ -287,9 +319,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
return primaryCandidate;
}
throw new IllegalStateException("""
Unable to select a bean to override: found %s beans of type %s \
(as required by annotated field '%s.%s'): %s"""
throw new IllegalStateException(
"Unable to select a bean to override: found %d beans of type %s (as required by field '%s.%s'): %s"
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
field.getName(), candidateNames));
}
@@ -331,6 +362,12 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
return beanNames;
}
/**
* 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)
*/
@Nullable
private static String determinePrimaryCandidate(
ConfigurableListableBeanFactory beanFactory, Set<String> candidateBeanNames, Class<?> beanType) {
@@ -340,6 +377,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
String primaryBeanName = null;
// First pass: identify unique primary candidate
for (String candidateBeanName : candidateBeanNames) {
if (beanFactory.containsBeanDefinition(candidateBeanName)) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName);
@@ -352,6 +390,21 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
}
}
// Second pass: identify unique non-fallback candidate
if (primaryBeanName == null) {
for (String candidateBeanName : candidateBeanNames) {
if (beanFactory.containsBeanDefinition(candidateBeanName)) {
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName);
if (!beanDefinition.isFallback()) {
if (primaryBeanName != null) {
// More than one non-fallback bean found among candidates.
return null;
}
primaryBeanName = candidateBeanName;
}
}
}
}
return primaryBeanName;
}
@@ -364,7 +417,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
* respectively.
* <p>The returned bean definition should <strong>not</strong> be used to create
* a bean instance but rather only for the purpose of having suitable bean
* definition metadata available in the {@link BeanFactory} &mdash; for example,
* definition metadata available in the {@code BeanFactory} &mdash; for example,
* for autowiring candidate resolution.
*/
private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) {
@@ -394,7 +447,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
private static void destroySingleton(ConfigurableListableBeanFactory beanFactory, String beanName) {
if (!(beanFactory instanceof DefaultListableBeanFactory dlbf)) {
throw new IllegalStateException("Cannot process bean override with a BeanFactory " +
"that doesn't implement DefaultListableBeanFactory: " + beanFactory.getClass().getName());
"that does not implement DefaultListableBeanFactory: " + beanFactory.getClass().getName());
}
dlbf.destroySingleton(beanName);
}
@@ -16,7 +16,7 @@
package org.springframework.test.context.bean.override;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@@ -24,6 +24,7 @@ import org.springframework.lang.Nullable;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
/**
* {@link ContextCustomizerFactory} implementation that provides support for
@@ -42,7 +43,7 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
public BeanOverrideContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
Set<BeanOverrideHandler> handlers = new HashSet<>();
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>();
findBeanOverrideHandler(testClass, handlers);
if (handlers.isEmpty()) {
return null;
@@ -51,10 +52,13 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
}
private void findBeanOverrideHandler(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
handlers.addAll(BeanOverrideHandler.forTestClass(testClass));
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
findBeanOverrideHandler(testClass.getEnclosingClass(), handlers);
}
BeanOverrideHandler.forTestClass(testClass).forEach(handler ->
Assert.state(handlers.add(handler), () ->
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
.formatted(testClass.getName(), handler)));
}
}
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@@ -105,6 +106,8 @@ public abstract class BeanOverrideHandler {
private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
Assert.state(!Modifier.isStatic(field.getModifiers()),
() -> "@BeanOverride field must not be static: " + field);
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
@@ -17,8 +17,13 @@
package org.springframework.test.context.bean.override;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@@ -37,9 +42,12 @@ import org.springframework.util.StringUtils;
*/
class BeanOverrideRegistry {
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new HashMap<>();
private static final Log logger = LogFactory.getLog(BeanOverrideRegistry.class);
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new HashMap<>();
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new LinkedHashMap<>();
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new LinkedHashMap<>();
private final ConfigurableBeanFactory beanFactory;
@@ -57,7 +65,25 @@ class BeanOverrideRegistry {
* bean via {@link #wrapBeanIfNecessary(Object, String)}.
*/
void registerBeanOverrideHandler(BeanOverrideHandler handler, String beanName) {
Assert.state(!this.handlerToBeanNameMap.containsKey(handler), () ->
"Cannot register BeanOverrideHandler for bean with name '%s'; detected multiple registrations for %s"
.formatted(beanName, handler));
// Check if beanName was already registered, before adding the new mapping.
boolean beanNameAlreadyRegistered = this.handlerToBeanNameMap.containsValue(beanName);
// Add new mapping before potentially logging a warning, to ensure that
// the current handler is logged as well.
this.handlerToBeanNameMap.put(handler, beanName);
if (beanNameAlreadyRegistered && logger.isWarnEnabled()) {
List<BeanOverrideHandler> competingHandlers = this.handlerToBeanNameMap.entrySet().stream()
.filter(entry -> entry.getValue().equals(beanName))
.map(Entry::getKey)
.toList();
logger.warn("Bean with name '%s' was overridden by multiple handlers: %s"
.formatted(beanName, competingHandlers));
}
if (handler.getStrategy() == BeanOverrideStrategy.WRAP) {
this.wrappingBeanOverrideHandlers.put(beanName, handler);
}
@@ -27,8 +27,8 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride;
/**
* {@code @TestBean} is an annotation that can be applied to a field in a test
* class to override a bean in the test's
* {@code @TestBean} is an annotation that can be applied to a non-static field
* in a test class to override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* using a static factory method.
*
@@ -105,6 +105,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton bean
* corresponding to the value returned from the {@code @TestBean} factory method.
*
* <p>There are no restrictions on the visibility of {@code @TestBean} fields or
* factory methods. Such fields and methods can therefore be {@code public},
* {@code protected}, package-private (default visibility), or {@code private}
* depending on the needs or coding practices of the project.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
@@ -23,6 +23,7 @@ import java.util.Objects;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.core.ResolvableType;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
@@ -83,4 +84,15 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler {
return this.factoryMethod.hashCode() * 29 + super.hashCode();
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("field", getField())
.append("beanType", getBeanType())
.append("beanName", getBeanName())
.append("strategy", getStrategy())
.append("factoryMethod", this.factoryMethod)
.toString();
}
}
@@ -99,9 +99,10 @@ public enum MockReset {
}
/**
* Get the {@link MockReset} associated with the given mock.
* @param mock the source mock
* @return the reset type (never {@code null})
* Get the {@link MockReset} strategy associated with the given mock.
* @param mock the mock
* @return the reset strategy for the given mock, or {@link MockReset#NONE}
* if no strategy is associated with the given mock
*/
static MockReset get(Object mock) {
MockingDetails mockingDetails = Mockito.mockingDetails(mock);
@@ -29,8 +29,8 @@ import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.bean.override.BeanOverride;
/**
* {@code @MockitoBean} is an annotation that can be applied to a field in a test
* class to override a bean in the test's
* {@code @MockitoBean} is an annotation that can be applied to a non-static field
* in a test class to override a bean in the test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* using a Mockito mock.
*
@@ -58,6 +58,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton mock
* of the type of object created by the {@code FactoryBean}.
*
* <p>There are no restrictions on the visibility of a {@code @MockitoBean} field.
* Such fields can therefore be {@code public}, {@code protected}, package-private
* (default visibility), or {@code private} depending on the needs or coding
* practices of the project.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2

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