Compare commits

..

98 Commits

Author SHA1 Message Date
Stéphane Nicoll d5da602bc2 Release v6.2.2 2025-01-16 09:19:54 +01:00
rstoyanchev 384d2749c6 Polishing in HttpServiceMethod 2025-01-15 19:03:00 +00:00
rstoyanchev 5150a9a6ad Polishing contribution
Closes gh-34230
2025-01-15 18:31:50 +00:00
Yanming Zhou a8c5885aff Fix wrong document about RequestHeaderArgumentResolver
Also test is added to verify that.

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>

See gh-34230
2025-01-15 18:31:49 +00:00
Juergen Hoeller 7d844274dc Upgrade to Jetty 12.0.16, Netty 4.1.117, Protobuf 4.29.3, Checkstyle 10.21.1 2025-01-15 18:27:49 +01:00
Juergen Hoeller 70ceb3abdf Refer to local Consumer type instead of Kotlin test type 2025-01-15 17:41:22 +01:00
Sam Brannen 9181cce65f Support @⁠MockitoBean at the type level on test classes
Prior to this commit, @⁠MockitoBean could only be declared on fields
within test classes, which prevented developers from being able to
easily reuse mock configuration across a test suite.

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

In addition, @⁠MockitoBean:

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

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

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

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

- The `field` property in BeanOverrideHandler is now @⁠Nullable.

- BeanOverrideProcessor has a new `default` createHandlers() method
  which is invoked when a @⁠BeanOverride annotation is found at the
  type level.

- MockitoBeanOverrideProcessor implements the new createHandlers()
  method.

- The internal findHandlers() method in BeanOverrideHandler has been
  completely overhauled.

- The @⁠MockitoBean and @⁠MockitoSpyBean section of the reference
  manual has been completely overhauled.

Closes gh-33925
2025-01-15 17:13:35 +01:00
Sam Brannen 8b6523a35b Cross reference @⁠NestedTestConfiguration for Bean Overrides 2025-01-15 17:13:35 +01:00
Stéphane Nicoll e7f349328d Merge pull request #34086 from rPraml
* pr/34086:
  Polish "Resolve base type in parameterized type if necessary"
  Resolve base type in parameterized type if necessary

Closes gh-34086
2025-01-15 17:07:58 +01:00
Stéphane Nicoll fe5f5d53b2 Polish "Resolve base type in parameterized type if necessary"
See gh-34086
2025-01-15 17:06:05 +01:00
Roland Praml 033d277bcf Resolve base type in parameterized type if necessary
See gh-34086
2025-01-15 17:03:20 +01:00
Juergen Hoeller 227385083d Restrict lenient nested matching to immediate type variable
Includes fix for matching multiple wildcard bounds properly.

Closes gh-34119
Closes gh-34234
2025-01-15 15:02:46 +01:00
Stéphane Nicoll d280358e98 Fix typo in HttpHeadersAssert#doesNotContainHeaders
This commit deprecates the existing doesNotContainsHeaders in favor of a
newly introduced method that does not have the typo.

Closes gh-34263
2025-01-15 11:19:00 +01:00
Stéphane Nicoll e08a7ece61 Merge pull request #34223 from remeio
* pr/34223:
  Polish contribution
  Polish HttpEntityMethodProcessor

Closes gh-34223
2025-01-14 17:36:45 +01:00
Stéphane Nicoll 24fa9ea394 Polish contribution
See gh-34223
2025-01-14 17:30:39 +01:00
Mengqi Xu 4acd9ee69b Polish HttpEntityMethodProcessor
Signed-off-by: Mengqi Xu <2663479778@qq.com>

See gh-34223
2025-01-14 17:28:00 +01:00
Stéphane Nicoll cd1aefa5e0 Upgrade to Reactor 2024.0.2
Closes gh-34252
2025-01-14 16:36:43 +01:00
Sam Brannen 3c3b8c7aac Track only qualifier annotations in BeanOverrideHandler
Closes gh-34260
2025-01-14 11:35:00 +01:00
Sam Brannen 5a68d1f029 Remove @⁠FunctionalInterface declaration from BeanOverrideProcessor
Closes gh-34259
2025-01-14 11:29:45 +01:00
Sam Brannen ff9b7678a7 Polish Bean Override internals and tests 2025-01-14 11:25:37 +01:00
Brian Clozel 799566573a Fix Wrong parentId tracking in JFR application startup
This commit fixes the tracking of the main event parentId for the Java
Flight Recorder implementation variant.

Fixes gh-34128
2025-01-13 19:03:43 +01:00
Juergen Hoeller 467d5f3ca3 Try late locking for waiting on specific bean to be finished
Closes gh-34186
2025-01-13 17:48:13 +01:00
Stéphane Nicoll 172c25fb0e Upgrade to Micrometer 1.14.3
Closes gh-34251
2025-01-13 13:54:47 +01:00
Juergen Hoeller 8771b9ea21 Defensively acquire singleton lock for FactoryBean type check
Closes gh-34247
2025-01-13 13:04:42 +01:00
Juergen Hoeller ff72652890 Defensively check expected type for qualified bean
Closes gh-34187
2025-01-13 13:03:25 +01:00
Stéphane Nicoll a1503a59ee Start building against Reactor 2024.0.2 snapshots
See gh-34252
2025-01-13 09:09:19 +01:00
Stéphane Nicoll 5f29df3ac3 Start building against Micrometer 1.14.3 snapshots
See gh-34251
2025-01-13 09:08:06 +01:00
Juergen Hoeller 25287205ba Use ReentrantLock for compilation without synchronization
Closes gh-34133
2025-01-12 18:11:47 +01:00
Juergen Hoeller 2bb4df79c3 Ignore SQLServerException with "not supported" message
Closes gh-34233
2025-01-12 18:08:54 +01:00
Juergen Hoeller 36fd82f32f Defensively resolve JarFile from JarURLConnection
Closes gh-34216
2025-01-12 18:07:09 +01:00
Juergen Hoeller 0f26f42da7 Defensively check for jar separator in jar entry names
Closes gh-34126
2025-01-12 18:05:49 +01:00
Juergen Hoeller c48fec885c Avoid caching invalid root directories
Closes gh-34111
2025-01-12 18:00:10 +01:00
Stéphane Nicoll 7c4351ab0a Polish
See gh-34231
2025-01-11 12:35:42 +01:00
rstoyanchev 07455b10f3 Use response decorator to check if error handled
Closes gh-34231
2025-01-10 10:51:39 +01:00
rstoyanchev a72855b2b3 DefaultResponseErrorHandler updates
Deprecate handleError(response), and ensure it continues to be invoked
if overridden.

See gh-34231
2025-01-10 10:51:11 +01:00
rstoyanchev cdddf09c20 Polishing in DefaultResponseErrorHandler
See gh-34231
2025-01-10 10:50:41 +01:00
xumengqi 53b3b934fc BindException declared but not handled by ExceptionHandler
Closes gh-34214

Signed-off-by: Mengqi Xu <2663479778@qq.com>
2025-01-08 16:29:12 +01:00
Brian Clozel 9173e13f74 Document that http.client.requests measure the entire HTTP exchange
Closes gh-34201
2025-01-08 14:39:50 +01:00
Brian Clozel 84be0d85b4 Refine location checks for PathResource
This commit ensures that checks for PathResource locations are skipped
because this resource implementation will always resolve under the
current location.

Closes gh-34167
2025-01-08 10:55:22 +01:00
Brian Clozel c971276f34 Refine default filtered headers for web data binding
Prior to this commit, HTTP request data binding had been improved to
filter out by default the "Priority" header in #34039.

This commit extends the set of filtered header names with:
"Accept", "Authorization", "Connection", "Cookie", "From", "Host",
"Origin", "Priority", "Range", "Referer", "Upgrade".

If an application wishes to let those header be bound, it will need to
configure the binder and replace the default header predicate by calling
`setHeaderPredicate`.

Closes gh-34182
2025-01-07 22:11:41 +01:00
Sam Brannen cd2fbb1ec5 Properly resolve @⁠TestBean factory method within class hierarchy
Prior to this commit, the search algorithm used to locate a @⁠TestBean
factory method within a test class hierarchy incorrectly found factory
methods declared in subclasses or nested test classes "below" the class
in which the @⁠TestBean field was declared. This resulted in "duplicate
bean override" failures for @⁠TestBean overrides which are clearly not
duplicates but rather "overrides of an override".

This commit ensures that @⁠TestBean factory method resolution is
consistent in type hierarchies as well as in enclosing class
hierarchies (for @⁠Nested test classes) by beginning the search for a
factory method in the class which declares the @⁠TestBean field.

Closes gh-34204
2025-01-07 17:20:52 +02:00
Sam Brannen e8745522fc Polishing 2025-01-07 11:52:33 +02:00
Brian Clozel 2bfb283c91 Update CONTRIBUTING guidelines for DCO
The CLA process has been replaced with a DCO, this commit updates the
CONTRIBUTING guidelines accordingly.
See https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring
2025-01-07 09:55:36 +01:00
Mattias-Sehlstedt 50b1fb0b15 Change the description for the uri client request observation
This commit describes what parts that are removed from the URI template
keyvalue.

Closes: gh-34116
Signed-off-by: Mattias-Sehlstedt <60173714+Mattias-Sehlstedt@users.noreply.github.com>
2025-01-07 09:02:25 +01:00
rstoyanchev a985b73939 Improve logging in ReactiveTypeHandler
See gh-34188
2025-01-06 12:13:29 +00:00
rstoyanchev 6ec7dcf2c1 Synchronize in WebAsyncManager onError/onTimeout
On connection loss, in a race between application thread and onError
callback trying to set the DeferredResult and dispatch, the onError
callback must not exit until dispatch completes. Currently, it may do
so because the DeferredResult has checks to bypasses locking or even
trying to dispatch if result is already set.

Closes gh-34192
2025-01-06 12:06:43 +00:00
rstoyanchev d94e04d97a Minor refactoring in WebAsyncManager
There is no need to set the DeferredResult from WebAsyncManager in an
onError notification because it is already done from the Lifecycle
interceptor in DeferredResult.

See gh-34192
2025-01-06 12:06:43 +00:00
rstoyanchev 5a44897c55 Polishing in WebAsyncManager
See gh-34192
2025-01-06 12:06:43 +00:00
Sam Brannen 181db1db75 Update copyright headers to 2025 2025-01-05 17:00:47 +02:00
Sam Brannen 138bdc597d Upgrade to AssertJ 3.27.2 2025-01-05 12:28:23 +02:00
Sam Brannen ef4f1f0a71 Ensure @⁠BeanOverride in subclass takes precedence over superclass
Prior to this commit, a @⁠BeanOverride (such as @⁠TestBean) for a
specific target bean which was declared in a superclass always took
precedence over a bean override for the same target bean in a subclass,
thereby rendering the bean override configuration in the subclass
useless. In other words, there was no way for a test class to override
a bean override declared in a superclass.

To address that, this commit switches from direct use of
ReflectionUtils.doWithFields() to a custom search algorithm that
traverses the class hierarchy using tail recursion for processing
@⁠BeanOverride fields (delegating now to
ReflectionUtils.doWithLocalFields() in order to continue to benefit
from the caching of declared fields in ReflectionUtils).

Closes gh-34194
2025-01-04 18:27:05 +02:00
Sam Brannen 51b89743e1 Polishing 2025-01-04 18:12:13 +02:00
Sam Brannen 019f76468b Revise TestBeanForInheritanceIntegrationTests
Amongst other refinements, this commit ensures that the "anotherBean"
use case is actually tested.
2025-01-04 14:16:59 +02:00
Stéphane Nicoll f802c0cf24 Restore use of MethodSource 2025-01-03 16:53:54 +01:00
Sam Brannen 0da4ae96b4 Rename internal MockitoBeans class to MockBeans
This is a prerequisite for gh-33925 which will introduce a public
@MockitoBeans container annotation in the same package.
2025-01-03 17:26:45 +02:00
Sam Brannen 2ba0022704 Polishing 2025-01-03 17:26:45 +02:00
Stéphane Nicoll c59ca087b4 Backport tests for exact match resolution
See gh-34124

(cherry picked from commit 898d3ec86a)
2025-01-03 17:10:17 +02:00
Sam Brannen 9de2b947cb Upgrade to AssertJ 3.27.1 2025-01-03 16:42:07 +02:00
Sam Brannen e73e7564d0 Upgrade to Mockito 5.15.2 2025-01-03 16:36:39 +02:00
Brian Clozel d927d64c40 Improve query params in uri KeyValue with HTTP interface client
Prior to this commit, the HTTP interface client would create URI
templates and name query params like so:
"?{queryParam0}={queryParam0[0]}".
While technically correct, the URI template is further used in
observations as a KeyValue. This means that several service methods
could result in having the exact same URI template even if they produce
a different set of query params.

This commit improves the naming of query params in the generated URI
templates for better observability integration.

Closes gh-34176
2025-01-02 20:00:38 +01:00
Stéphane Nicoll 8544435833 Merge pull request #34180 from v-perfilev
* pr/34180:
  Polish "Fix broken link to Code of Conduct in CONTRIBUTING.md"
  Fix broken link to Code of Conduct in CONTRIBUTING.md

Closes gh-34180
2025-01-01 10:08:39 +01:00
Stéphane Nicoll 80ea1f62b6 Polish "Fix broken link to Code of Conduct in CONTRIBUTING.md"
See gh-34180
2025-01-01 10:08:18 +01:00
Vladimir Perfilev bc287918f2 Fix broken link to Code of Conduct in CONTRIBUTING.md
See gh-34180
2025-01-01 10:07:29 +01:00
Stéphane Nicoll c9436992c9 Update actions in CI build 2024-12-31 09:49:43 +01:00
rstoyanchev 4350fc21b3 List constructor arg initialized correctly
DataBinder now uses the calculated List size rather than
the number of indexes to initialize the list.

Closes gh-34145
2024-12-30 14:47:26 +00:00
rstoyanchev 59ed4686c5 Create ParameterErrors for type level constraint
Closes gh-34105
2024-12-30 14:47:26 +00:00
Sébastien Deleuze d5bebd5ced Upgrade to Java 17.0.13 2024-12-30 12:15:24 +01:00
Johnny Lim 6d86b23fbe Apply Checkstyle MethodParamPadCheck module
This commit also fixes its violations.

Closes gh-34173
2024-12-30 09:02:06 +01:00
Simon Baslé 0f38c28e91 Fix ServletRequestDataBinder ctor binding with []-indexed query params
This change ensures that a request containing query parameters in the
array format `someArray[]=value` can be bound into a simple array in
constructors, even for cases where the array values don't have nested
properties.

The value resolver is directly called in the constructor case, before
any mutable properties are considered or even cleared (see
`WebDataBinder#adaptEmptyArrayIndices` method). As a result, we need to
accommodate the possibility that the request stores array elements under
the `name[]` key rather than `name`. This change attempts a secondary
lookup with the `[]` suffix if the type is a list or array, and the key
doesn't include an index.

Closes gh-34121
2024-12-27 10:59:15 +01:00
Simon Baslé 3505c4bcad Ensure DataBinder can bind constructor with a Map with simple values
This change ensures that DataBinder can bind constructor with a Map
parameter that has no nested properties, but just simple values like
a String: `someMap[0]=exampleString`.

Integration tests have been added to cover similar cases that use the
ServletRequestDataBinder.

Closes gh-34043
2024-12-27 10:57:30 +01:00
Stéphane Nicoll 58670db46e Merge pull request #34165 from brendenehlers
* pr/34165:
  Polish contribution
  Fix grammatical structure in "Basic Concepts"

Closes gh-34165
2024-12-27 09:31:47 +01:00
Stéphane Nicoll 0cdb8a1657 Polish contribution
Using commas render better than em dashes.

See gh-34165
2024-12-27 09:31:34 +01:00
Brenden Ehlers fe8c70dddb Fix grammatical structure in "Basic Concepts"
See gh-34165
2024-12-27 09:30:43 +01:00
Sébastien Deleuze 1ed74efa6b Polishing
Closes gh-34148
2024-12-26 13:16:09 +01:00
Brenden Ehlers eabd574038 Fix XML code snippet
See gh-34148
2024-12-26 13:15:51 +01:00
Stéphane Nicoll ea386c2398 Merge pull request #34149 from brendenehlers
* pr/34149:
  Fix typo in link

Closes gh-34149
2024-12-25 09:29:52 +01:00
Brenden Ehlers 6c6e8ad831 Fix typo in link
See gh-34149
2024-12-25 09:29:29 +01:00
Sébastien Deleuze 69b74d7645 Fix NullPointerException in HttpComponentsClientHttpResponse
Closes gh-34132
2024-12-23 15:34:04 +01:00
Stéphane Nicoll 5ce5647d09 Restore support of exact match property
This commit fixes a regression in property placeholder resolution where
the original key was no longer considered for an exact match before
processing the placeholder itself.

By default, property resolution uses ':' as the separator between the
key and the fallback value.

Consider a request to resolve ${prefix://service}. Previously,
placeholder resolution would first attempt to resolve the raw text, that
is 'prefix://service', before attempting to resolve the 'prefix' key and
then use '//service' if the key did not resolve.

This commit restores that behaviour purely for backward compatible
reason.

Closes gh-34124
2024-12-23 12:19:45 +01:00
Tran Ngoc Nhan ebae02a92b Fix broken links in the web reference documentation
Closes gh-34115
2024-12-23 11:14:40 +01:00
Sam Brannen 99cb6ba0a7 Upgrade to Gradle 8.12 2024-12-21 16:02:15 +01:00
Sam Brannen 2baf70768d Upgrade to AssertJ 3.27 2024-12-21 15:49:31 +01:00
Sam Brannen 724700d708 Polishing 2024-12-21 15:31:01 +01:00
Andreas Kruck 83ba7e69ec Fix minor typo in reference documentation for Kotlin annotations
Closes gh-34134
2024-12-21 15:28:33 +01:00
Sam Brannen c1236a3340 Support varargs-only MethodHandle as SpEL function
Prior to this commit, if a MethodHandle was registered as a custom
function in the Spring Expression Language (SpEL) for a static method
that accepted only a variable argument list (for example,
`static String func(String... args)`), attempting to invoke the
registered function within a SpEL expression resulted in a
ClassCastException because the varargs array was unnecessarily wrapped
in an Object[].

This commit modifies the logic in FunctionReference's internal
executeFunctionViaMethodHandle() method to address that.

Closes gh-34109
2024-12-18 17:59:14 +01:00
luozongle01 a942362221 Fix a typo
Closes gh-34101
2024-12-17 11:40:47 +01:00
Brian Clozel 1a34b0dd87 Improve PathMatcher/PatternParser XML configuration
Prior to this commit, the MVC namespace for the XML Spring configuration
model would use the `PathMatcher` bean instance when provided like this:

```
<bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"/>
<mvc:annotation-driven>
  <mvc:path-matching path-matcher="pathMatcher"/>
</mvc:annotation-driven>
<mvc:resources mapping="/resources/**" location="classpath:/static/"/>
```

With this configuration, the handler mapping for annotated controller
would use the given `AntPathMatcher` instance but the handler mapping
for resources would still use the default, which is `PathPatternParser`
since 6.0.

This commit ensures that when a custom `path-matcher` is defined, it's
consistently used for all MVC handler mappings as an alias to the
well-known bean name. This allows to use `AntPathMatcher` consistently
while working on a migration path to `PathPatternParser`

This commit also adds a new XML attribute to the path matching
configuration that makes it possible to use a custom `PathPatternParser`
instance:

```
<bean id="patternParser" class="org.springframework.web.util.pattern.PathPatternParser"/>
<mvc:annotation-driven>
  <mvc:path-matching pattern-parser="patternParser"/>
</mvc:annotation-driven>
```

Closes gh-34064
2024-12-17 10:14:18 +01:00
Sam Brannen b0fcde9aa3 Upgrade to JUnit 5.11.4
Closes gh-34095
2024-12-16 14:42:30 +01:00
Sam Brannen a89db89fc0 Polishing 2024-12-14 17:10:41 +01:00
Sam Brannen 3d0fffa8e4 Polish contribution
See gh-33945
2024-12-14 16:40:49 +01:00
Mico Piira 71f872e8bb Fix implicit variable resolution in JSP EvalTag
Prior to this commit, the order of parameters passed to
ELResolver#getValue was incorrect.

The `name` should correspond to the `property` parameter of the
`getValue` method instead the `base` parameter.

See gh-32383
See gh-33942
Closes gh-33945
2024-12-14 16:40:31 +01:00
Juergen Hoeller 54a90b20ed Avoid logger serialization behind shared EntityManager proxy
See gh-34084
2024-12-12 19:48:21 +01:00
Stéphane Nicoll d7a3971403 Merge pull request #34077 from scordio
* pr/34077:
  Polish "Prevent execution of Antora jobs on forks"
  Prevent execution of Antora jobs on forks

Closes gh-34077
2024-12-12 17:35:51 +01:00
Stéphane Nicoll cfa3463cec Polish "Prevent execution of Antora jobs on forks"
See gh-34077
2024-12-12 17:35:38 +01:00
Stefano Cordio d2264b221a Prevent execution of Antora jobs on forks
See gh-34077
2024-12-12 17:34:15 +01:00
Stéphane Nicoll 2c32601553 Add note about using @EventListener on lazy beans
Closes gh-34057
2024-12-12 17:32:18 +01:00
Sam Brannen ea8b18fbc7 Polish Javadoc for BeanOverrideHandler 2024-12-12 14:41:23 +01:00
Brian Clozel 3ab4ee2bba Next development version (v6.2.2-SNAPSHOT) 2024-12-12 10:53:14 +01:00
169 changed files with 3371 additions and 1085 deletions
@@ -31,10 +31,11 @@ runs:
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
${{ inputs.java-toolchain == 'true' && '17' || '' }}
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
with:
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
develocity-token-expiry: 4
- name: Configure Gradle Properties
shell: bash
run: |
@@ -20,7 +20,7 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts
@@ -21,7 +21,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Deploy
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
+3 -14
View File
@@ -9,22 +9,11 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Set Up JDK 17
uses: actions/setup-java@v4
with:
distribution: 'liberica'
java-version: '17'
- name: Check Out
- name: Check Out Code
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
- name: Build
env:
CI: 'true'
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build
id: build
uses: ./.github/actions/build
- name: Print JVM Thread Dumps When Cancelled
if: cancelled()
uses: ./.github/actions/print-jvm-thread-dumps
+1 -1
View File
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
@@ -12,8 +12,9 @@ permissions:
jobs:
update-antora-ui-spring:
runs-on: ubuntu-latest
name: Update on Supported Branches
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
strategy:
matrix:
branch: [ '6.1.x' ]
@@ -25,8 +26,9 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
antora-file-path: 'framework-docs/antora-playbook.yml'
update-antora-ui-spring-docs-build:
runs-on: ubuntu-latest
name: Update on docs-build
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
steps:
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc
name: Update
+1 -1
View File
@@ -46,7 +46,7 @@ jobs:
distribution: 'liberica'
java-version: 17
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
uses: gradle/actions/setup-gradle@cc4fc85e6b35bafd578d5ffbc76a5518407e1af0 # v4.2.1
with:
cache-read-only: false
- name: Configure Gradle Properties
+1 -1
View File
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=17.0.12-librca
java=17.0.13-librca
+6 -5
View File
@@ -16,7 +16,7 @@ First off, thank you for taking the time to contribute! :+1: :tada:
### Code of Conduct
This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc).
This project is governed by the [Spring Code of Conduct](https://github.com/spring-projects/spring-framework#coc-ov-file).
By participating you are expected to uphold this code.
Please report unacceptable behavior to spring-code-of-conduct@spring.io.
@@ -65,10 +65,6 @@ follow-up reports will need to be created as new issues with a fresh description
#### Submit a Pull Request
1. If you have not previously done so, please sign the
[Contributor License Agreement](https://cla.spring.io/sign/spring). You will be reminded
automatically when you submit the PR.
1. Should you create an issue first? No, just create the pull request and use the
description to provide context and motivation, as you would for an issue. If you want
to start a discussion first or have already created an issue, once a pull request is
@@ -85,6 +81,11 @@ multiple edits or corrections of the same logical change. See
[Rewriting History section of Pro Git](https://git-scm.com/book/en/Git-Tools-Rewriting-History)
for an overview of streamlining the commit history.
1. All commits must include a _Signed-off-by_ trailer at the end of each commit message
to indicate that the contributor agrees to the Developer Certificate of Origin.
For additional details, please refer to the blog post
[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring).
1. Format commit messages using 55 characters for the subject line, 72 characters per line
for the description, followed by the issue fixed, for example, `Closes gh-22276`. See the
[Commit Guidelines section of Pro Git](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines)
+4 -4
View File
@@ -25,17 +25,17 @@ configure(allprojects) { project ->
repositories {
mavenCentral()
maven {
url "https://repo.spring.io/milestone"
url = "https://repo.spring.io/milestone"
content {
// Netty 5 optional support
includeGroup 'io.projectreactor.netty'
}
}
if (version.contains('-')) {
maven { url "https://repo.spring.io/milestone" }
maven { url = "https://repo.spring.io/milestone" }
}
if (version.endsWith('-SNAPSHOT')) {
maven { url "https://repo.spring.io/snapshot" }
maven { url = "https://repo.spring.io/snapshot" }
}
}
configurations.all {
@@ -102,7 +102,7 @@ configure([rootProject] + javaProjects) { project ->
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.11.3/api/",
// "https://junit.org/junit5/docs/5.11.4/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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.2");
checkstyle.setToolVersion("10.21.1");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+2 -2
View File
@@ -9,7 +9,7 @@ apply from: "${rootDir}/gradle/publications.gradle"
repositories {
maven {
url "https://repo.spring.io/release"
url = "https://repo.spring.io/release"
}
}
@@ -87,7 +87,7 @@ tasks.register('schemaZip', Zip) {
archiveClassifier.set("schema")
description = "Builds -${archiveClassifier} archive containing all " +
"XSDs for deployment at https://springframework.org/schema."
duplicatesStrategy DuplicatesStrategy.EXCLUDE
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
moduleProjects.each { module ->
def Properties schemas = new Properties();
+1 -1
View File
@@ -37,7 +37,7 @@ javadoc {
repositories {
maven {
url "https://repo.spring.io/release"
url = "https://repo.spring.io/release"
}
}
@@ -578,6 +578,8 @@ Kotlin::
----
======
NOTE: Do not define such beans to be lazy as the `ApplicationContext` will honour that and will not register the method to listen to events.
The method signature once again declares the event type to which it listens,
but, this time, with a flexible name and without implementing a specific listener interface.
The event type can also be narrowed through generics as long as the actual event type
@@ -3,7 +3,7 @@
If a bean is a dependency of another bean, that usually means that one bean is set as a
property of another. Typically you accomplish this with the
xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-ref-element[`<ref/>` element>]
xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-ref-element[`<ref/>` element]
in XML-based metadata or through xref:core/beans/dependencies/factory-autowire.adoc[autowiring].
However, sometimes dependencies between beans are less direct. An example is when a static
@@ -168,8 +168,8 @@ listings shows how to use the `parent` attribute:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
<!-- in the child (descendant) context, bean name is the same as the parent bean -->
<bean id="accountService"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
@@ -64,7 +64,7 @@ This prevents the same `@Bean` method from accidentally being invoked through a
Java method call, which helps to reduce subtle bugs that can be hard to track down.
When `@Bean` methods are declared within classes that are not annotated with
`@Configuration` - or when `@Configuration(proxyBeanMethods=false)` is declared -,
`@Configuration`, or when `@Configuration(proxyBeanMethods=false)` is declared,
they are referred to as being processed in a "lite" mode. In such scenarios,
`@Bean` methods are effectively a general-purpose factory method mechanism without
special runtime processing (that is, without generating a CGLIB subclass for it).
@@ -270,6 +270,7 @@ By default, the following `KeyValues` are created:
== HTTP Client Instrumentation
HTTP client exchange observations are created with the name `"http.client.requests"` for blocking and reactive clients.
This observation measures the entire HTTP request/response exchange, from connection establishment up to body deserialization.
Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an `ObservationRegistry` on the client.
[[observability.http-client.resttemplate]]
@@ -285,7 +286,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|===
|Name | Description
|`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.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not 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.
|`outcome` _(required)_|Outcome of the HTTP client exchange.
@@ -313,7 +314,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|===
|Name | Description
|`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.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not 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.
|`outcome` _(required)_|Outcome of the HTTP client exchange.
@@ -342,7 +343,7 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien
|===
|Name | Description
|`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.
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not 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.
|`outcome` _(required)_|Outcome of the HTTP client exchange.
@@ -938,10 +938,10 @@ method parameters:
| Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute
| `@RequestHeader`
| Add a request header or multiple headers. The argument may be a `Map<String, ?>` or
`MultiValueMap<String, ?>` with multiple headers, a `Collection<?>` of values, or an
individual value. Type conversion is supported for non-String values. This overrides
the annotation's `headers` attribute.
| Add a request header or multiple headers. The argument may be a single value,
a `Collection<?>` of values, `Map<String, ?>`,`MultiValueMap<String, ?>`.
Type conversion is supported for non-String values. Header values are added and
do not override already added header values.
| `@PathVariable`
| Add a variable for expand a placeholder in the request URL. The argument may be a
@@ -14,16 +14,13 @@ For example, `@Autowired lateinit var thing: Thing` implies that a bean
of type `Thing` must be registered in the application context, while `@Autowired lateinit var thing: Thing?`
does not raise an error if such a bean does not exist.
Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)` implies
Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, car)` implies
that a bean of type `Toy` must be registered in the application context, while a bean of
type `Car` may or may not exist. The same behavior applies to autowired constructor parameters.
NOTE: If you use bean validation on classes with properties or a primary constructor
parameters, you may need to use
with parameters, you may need to use
{kotlin-docs}/annotations.html#annotation-use-site-targets[annotation use-site targets],
such as `@field:NotNull` or `@get:Size(min=5, max=15)`, as described in
{stackoverflow-site}/a/35853200/1092077[this Stack Overflow response].
@@ -11,29 +11,21 @@ If multiple candidates match, `@Qualifier` can be provided to narrow the candida
override. Alternatively, a candidate whose bean name matches the name of the field will
match.
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
exist. However, if you would like for the test to fail when a corresponding bean does not
exist, you can set the `enforceOverride` attribute to `true` for example,
`@MockitoBean(enforceOverride = true)`.
To use a by-name override rather than a by-type override, specify the `name` attribute
of the annotation.
[WARNING]
====
Qualifiers, including the name of the field, are used to determine if a separate
`ApplicationContext` needs to be created. If you are using this feature to mock or spy
the same bean in several tests, make sure to name the field consistently to avoid
the same bean in several test classes, make sure to name the field consistently to avoid
creating unnecessary contexts.
====
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
By default, the `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
If no existing bean matches, a new bean is created on the fly. As mentioned previously,
you can switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to
`true`.
If no existing bean matches, a new bean is created on the fly. However, you can switch to
the `REPLACE` strategy by setting the `enforceOverride` attribute to `true`. See the
following section for an example.
The `@MockitoSpyBean` annotation uses the `WRAP`
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
@@ -61,6 +53,17 @@ Such fields can therefore be `public`, `protected`, package-private (default vis
or `private` depending on the needs or coding practices of the project.
====
[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
== `@MockitoBean` Examples
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
exist. However, if you would like for the test to fail when a corresponding bean does not
exist, you can set the `enforceOverride` attribute to `true` for example,
`@MockitoBean(enforceOverride = true)`.
To use a by-name override rather than a by-type override, specify the `name` (or `value`)
attribute of the annotation.
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
[tabs]
@@ -69,11 +72,13 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
class OverrideBeanTests {
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoBean // <1>
CustomService customService;
// test case body...
// tests...
}
----
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
@@ -82,8 +87,8 @@ Java::
In the example above, we are creating a mock for `CustomService`. If more than one bean
of that type exists, the bean named `customService` is considered. Otherwise, the test
will fail, and you will need to provide a qualifier of some sort to identify which of the
`CustomService` beans you want to override. If no such bean exists, a bean definition
will be created with an auto-generated bean name.
`CustomService` beans you want to override. If no such bean exists, a bean will be
created with an auto-generated bean name.
The following example uses a by-name lookup, rather than a by-type lookup:
@@ -93,20 +98,43 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
class OverrideBeanTests {
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoBean("service") // <1>
CustomService customService;
// test case body...
// tests...
}
----
<1> Replace the bean named `service` with a Mockito `mock`.
======
If no bean definition named `service` exists, one is created.
If no bean named `service` exists, one is created.
The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation:
`@MockitoBean` can also be used at the type level:
- on a test class or any superclass or implemented interface in the type hierarchy above
the test class
- on an enclosing class for a `@Nested` test class or on any class or interface in the
type hierarchy or enclosing class hierarchy above the `@Nested` test class
When `@MockitoBean` is declared at the type level, the type of bean (or beans) to mock
must be supplied via the `types` attribute for example,
`@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple candidates
exist in the application context, you can explicitly specify a bean name to mock by
setting the `name` attribute. Note, however, that the `types` attribute must contain a
single type if an explicit bean `name` is configured for example,
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
To support reuse of mock configuration, `@MockitoBean` may be used as a meta-annotation
to create custom _composed annotations_ — for example, to define common mock
configuration in a single annotation that can be reused across a test suite.
`@MockitoBean` can also be used as a repeatable annotation at the type level — for
example, to mock several beans by name.
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
[tabs]
======
@@ -114,11 +142,70 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
class OverrideBeanTests {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@MockitoBean(types = {OrderService.class, UserService.class}) // <1>
@MockitoBean(name = "ps1", types = PrintingService.class) // <2>
public @interface SharedMocks {
}
----
<1> Register `OrderService` and `UserService` mocks by-type.
<2> Register `PrintingService` mock by-name.
======
The following demonstrates how `@SharedMocks` can be used on a test class.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
@SharedMocks // <1>
class BeanOverrideTests {
@Autowired OrderService orderService; // <2>
@Autowired UserService userService; // <2>
@Autowired PrintingService ps1; // <2>
// Inject other components that rely on the mocks.
@Test
void testThatDependsOnMocks() {
// ...
}
}
----
<1> Register common mocks via the custom `@SharedMocks` annotation.
<2> Optionally inject mocks to _stub_ or _verify_ them.
======
TIP: The mocks can also be injected into `@Configuration` classes or other test-related
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
APIs.
[[spring-testing-annotation-beanoverriding-mockitospybean-examples]]
== `@MockitoSpyBean` Examples
The following example shows how to use the default behavior of the `@MockitoSpyBean`
annotation:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoSpyBean // <1>
CustomService customService;
// test case body...
// tests...
}
----
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
@@ -137,12 +224,13 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
class OverrideBeanTests {
@SpringJUnitConfig(TestConfig.class)
class BeanOverrideTests {
@MockitoSpyBean("service") // <1>
CustomService customService;
// test case body...
// tests...
}
----
<1> Wrap the bean named `service` with a Mockito `spy`.
@@ -92,8 +92,10 @@ Java::
[TIP]
====
Spring searches for the factory method to invoke in the test class, in the test class
hierarchy, and in the enclosing class hierarchy for a `@Nested` test class.
To locate the factory method to invoke, Spring searches in the class in which the
`@TestBean` field is declared, in one of its superclasses, or in any implemented
interfaces. If the `@TestBean` field is declared in a `@Nested` test class, the enclosing
class hierarchy will also be searched.
Alternatively, a factory method in an external class can be referenced via its
fully-qualified method name following the syntax `<fully-qualified class name>#<method name>`
@@ -193,7 +193,7 @@ Kotlin::
[[webtestclient-fn-config]]
=== Bind to Router Function
This setup allows you to test <<web-reactive.adoc#webflux-fn, functional endpoints>> via
This setup allows you to test xref:web/webflux-functional.adoc[functional endpoints] via
mock request and response objects, without a running server.
For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to
@@ -1,5 +1,6 @@
[[webflux-cors]]
= CORS
[.small]#xref:web/webmvc-cors.adoc[See equivalent in the Servlet stack]#
Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section
@@ -364,7 +365,7 @@ Kotlin::
You can apply CORS support through the built-in
{spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a
good fit with <<webflux-fn, functional endpoints>>.
good fit with xref:web/webflux-functional.adoc[functional endpoints].
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
@@ -1,5 +1,6 @@
[[webflux-fn]]
= Functional Endpoints
[.small]#xref:web/webmvc-functional.adoc[See equivalent in the Servlet stack]#
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions
@@ -1,5 +1,6 @@
[[webflux-websocket]]
= WebSockets
[.small]#xref:web/websocket.adoc[See equivalent in the Servlet stack]#
This part of the reference documentation covers support for reactive-stack WebSocket
@@ -605,9 +605,4 @@ For method parameters and returns values, generally, `@HttpExchange` supports a
subset of the method parameters that `@RequestMapping` does. Notably, it excludes any
server-side specific parameter types. For details, see the list for
xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
`@HttpExchange` also supports a `headers()` parameter which accepts `"name=value"`-like
pairs like in `@RequestMapping(headers={})` on the client side. On the server side,
this extends to the full syntax that
xref:#webflux-ann-requestmapping-params-and-headers[`@RequestMapping`] supports.
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
@@ -101,7 +101,7 @@ On that foundation, Spring WebFlux provides a choice of two programming models:
from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive
(Reactor and RxJava) return types, and, as a result, it is not easy to tell them apart. One notable
difference is that WebFlux also supports reactive `@RequestBody` arguments.
* <<webflux-fn>>: Lambda-based, lightweight, and functional programming model. You can think of
* xref:web/webflux-functional.adoc[Functional Endpoints]: Lambda-based, lightweight, and functional programming model. You can think of
this as a small library or a set of utilities that an application can use to route and
handle requests. The big difference with annotated controllers is that the application
is in charge of request handling from start to finish versus declaring intent through
@@ -1,5 +1,6 @@
[[mvc-cors]]
= CORS
[.small]#xref:web/webflux-cors.adoc[See equivalent in the Reactive stack]#
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section
@@ -1,6 +1,7 @@
[[webmvc-fn]]
= Functional Endpoints
[.small]#<<web-reactive.adoc#webflux-fn, See equivalent in the Reactive stack>>#
[.small]#xref:web/webflux-functional.adoc[See equivalent in the Reactive stack]#
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions
are used to route and handle requests and contracts are designed for immutability.
@@ -1,6 +1,7 @@
[[websocket]]
= WebSockets
:page-section-summary-toc: 1
[.small]#xref:web/webflux-websocket.adoc[See equivalent in the Reactive stack]#
This part of the reference documentation covers support for Servlet stack, WebSocket
+9 -9
View File
@@ -8,20 +8,20 @@ javaPlatform {
dependencies {
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.micrometer:micrometer-bom:1.14.3"))
api(platform("io.netty:netty-bom:4.1.117.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.1"))
api(platform("io.projectreactor:reactor-bom:2024.0.2"))
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"))
api(platform("org.assertj:assertj-bom:3.26.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.15"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15"))
api(platform("org.assertj:assertj-bom:3.27.2"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.16"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.11.3"))
api(platform("org.mockito:mockito-bom:5.14.2"))
api(platform("org.junit:junit-bom:5.11.4"))
api(platform("org.mockito:mockito-bom:5.15.2"))
constraints {
api("com.fasterxml:aalto-xml:1.3.2")
@@ -31,7 +31,7 @@ dependencies {
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.11.0")
api("com.google.protobuf:protobuf-java-util:4.28.3")
api("com.google.protobuf:protobuf-java-util:4.29.3")
api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.1
version=6.2.2
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.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+1 -2
View File
@@ -86,8 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -99,7 +99,7 @@ abstract class AbstractProxyExceptionHandlingTests {
assertThat(throwableSeenByInterceptor).isSameAs(undeclaredCheckedException);
assertThat(throwableSeenByCaller)
.isInstanceOf(UndeclaredThrowableException.class)
.hasCauseReference(undeclaredCheckedException);
.cause().isSameAs(undeclaredCheckedException);
}
@Test
@@ -147,7 +147,7 @@ abstract class AbstractProxyExceptionHandlingTests {
invokeProxy();
assertThat(throwableSeenByCaller)
.isInstanceOf(UndeclaredThrowableException.class)
.hasCauseReference(undeclaredCheckedException);
.cause().isSameAs(undeclaredCheckedException);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -95,7 +95,7 @@ public abstract class BeanFactoryAnnotationUtils {
// Full qualifier matching supported.
return qualifiedBeanOfType(lbf, beanType, qualifier);
}
else if (beanFactory.containsBean(qualifier)) {
else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) {
// Fallback: target bean at least found by bean name.
return beanFactory.getBean(qualifier, beanType);
}
@@ -110,16 +110,16 @@ public abstract class BeanFactoryAnnotationUtils {
/**
* Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a qualifier
* (for example, {@code <qualifier>} or {@code @Qualifier}) matching the given qualifier).
* @param bf the factory to get the target bean from
* @param beanFactory the factory to get the target bean from
* @param beanType the type of bean to retrieve
* @param qualifier the qualifier for selecting between multiple bean matches
* @return the matching bean of type {@code T} (never {@code null})
*/
private static <T> T qualifiedBeanOfType(ListableBeanFactory bf, Class<T> beanType, String qualifier) {
String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(bf, beanType);
private static <T> T qualifiedBeanOfType(ListableBeanFactory beanFactory, Class<T> beanType, String qualifier) {
String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, beanType);
String matchingBean = null;
for (String beanName : candidateBeans) {
if (isQualifierMatch(qualifier::equals, beanName, bf)) {
if (isQualifierMatch(qualifier::equals, beanName, beanFactory)) {
if (matchingBean != null) {
throw new NoUniqueBeanDefinitionException(beanType, matchingBean, beanName);
}
@@ -127,11 +127,11 @@ public abstract class BeanFactoryAnnotationUtils {
}
}
if (matchingBean != null) {
return bf.getBean(matchingBean, beanType);
return beanFactory.getBean(matchingBean, beanType);
}
else if (bf.containsBean(qualifier)) {
else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) {
// Fallback: target bean at least found by bean name - probably a manually registered singleton.
return bf.getBean(qualifier, beanType);
return beanFactory.getBean(qualifier, beanType);
}
else {
throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() +
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -991,7 +991,11 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
*/
@Nullable
private FactoryBean<?> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
this.singletonLock.lock();
boolean locked = this.singletonLock.tryLock();
if (!locked) {
return null;
}
try {
BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName);
if (bw != null) {
@@ -177,7 +177,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
/** Map from bean name to merged BeanDefinitionHolder. */
private final Map<String, BeanDefinitionHolder> mergedBeanDefinitionHolders = new ConcurrentHashMap<>(256);
// Set of bean definition names with a primary marker. */
/** Set of bean definition names with a primary marker. */
private final Set<String> primaryBeanNames = ConcurrentHashMap.newKeySet(16);
/** Map of singleton and non-singleton bean names, keyed by dependency type. */
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -279,7 +279,25 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
if (logger.isDebugEnabled()) {
logger.debug("Creating shared instance of singleton bean '" + beanName + "'");
}
beforeSingletonCreation(beanName);
try {
beforeSingletonCreation(beanName);
}
catch (BeanCurrentlyInCreationException ex) {
if (locked) {
throw ex;
}
// Try late locking for waiting on specific bean to be finished.
this.singletonLock.lock();
locked = true;
// Singleton object should have appeared in the meantime.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject != null) {
return singletonObject;
}
beforeSingletonCreation(beanName);
}
boolean newSingleton = false;
boolean recordSuppressedExceptions = (locked && this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.testfixture.beans.TestBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* @author Juergen Hoeller
@@ -56,9 +55,6 @@ class BeanFactoryLockingTests {
@Override
public void afterPropertiesSet() throws Exception {
Thread thread = new Thread(() -> {
// Fail for circular reference from other thread
assertThatExceptionOfType(BeanCurrentlyInCreationException.class).isThrownBy(() ->
beanFactory.getBean(ThreadDuringInitialization.class));
// Leniently create unrelated other bean outside of singleton lock
assertThat(beanFactory.getBean(TestBean.class).getName()).isEqualTo("tb");
// Creation attempt in other thread was successful
@@ -1060,7 +1060,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
int size = (indexes.last() < this.autoGrowCollectionLimit ? indexes.last() + 1 : 0);
List<V> list = (List<V>) CollectionFactory.createCollection(paramType, size);
indexes.forEach(i -> list.add(null));
for (int i = 0; i < size; i++) {
list.add(null);
}
for (int index : indexes) {
list.set(index, (V) createObject(elementType, paramPath + "[" + index + "].", valueResolver));
}
@@ -1080,7 +1082,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
int startIdx = paramPath.length() + 1;
int endIdx = name.indexOf(']', startIdx);
String nestedPath = name.substring(0, endIdx + 2);
String nestedPath = ((name.length() > endIdx + 1) ? name.substring(0, endIdx + 2) : "");
boolean quoted = (endIdx - startIdx > 2 && name.charAt(startIdx) == '\'' && name.charAt(endIdx - 1) == '\'');
String key = (quoted ? name.substring(startIdx + 1, endIdx - 1) : name.substring(startIdx, endIdx));
if (map == null) {
@@ -1114,7 +1116,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
SortedSet<Integer> indexes = null;
for (String name : valueResolver.getNames()) {
if (name.startsWith(paramPath + "[")) {
int endIndex = name.indexOf(']', paramPath.length() + 2);
int endIndex = name.indexOf(']', paramPath.length() + 1);
String rawIndex = name.substring(paramPath.length() + 1, endIndex);
int index = Integer.parseInt(rawIndex);
indexes = (indexes != null ? indexes : new TreeSet<>());
@@ -367,7 +367,7 @@ public class MethodValidationAdapter implements MethodValidator {
container = null;
}
if (node.getKind().equals(ElementKind.PROPERTY)) {
if (node.getKind().equals(ElementKind.PROPERTY) || node.getKind().equals(ElementKind.BEAN)) {
nestedViolations
.computeIfAbsent(parameterNode, k ->
new ParamErrorsBuilder(parameter, value, container, index, key))
@@ -213,8 +213,8 @@ class ApplicationListenerMethodAdapterTests extends AbstractApplicationEventList
@Test
void invokeListenerWithWrongGenericPayload() {
Method method = ReflectionUtils.findMethod
(SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class);
Method method = ReflectionUtils.findMethod(
SampleEvents.class, "handleGenericStringPayload", EntityWrapper.class);
EntityWrapper<Integer> payload = new EntityWrapper<>(123);
invokeListener(method, new PayloadApplicationEvent<>(this, payload));
verify(this.sampleEvents, times(0)).handleGenericStringPayload(any());
@@ -67,7 +67,7 @@ class ConversionServiceFactoryBeanTests {
converters.add(new ConverterFactory<String, Bar>() {
@Override
public <T extends Bar> Converter<String, T> getConverter(Class<T> targetType) {
return new Converter<> () {
return new Converter<>() {
@SuppressWarnings("unchecked")
@Override
public T convert(String source) {
@@ -121,6 +121,24 @@ class DataBinderConstructTests {
assertThat(list.get(2).param1()).isEqualTo("value3");
}
@Test // gh-34145
void listBindingWithNonconsecutiveIndices() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
"dataClassList[0].param1", "value1", "dataClassList[0].param2", "true",
"dataClassList[1].param1", "value2", "dataClassList[1].param2", "true",
"dataClassList[3].param1", "value3", "dataClassList[3].param2", "true"));
DataBinder binder = initDataBinder(ListDataClass.class);
binder.construct(valueResolver);
ListDataClass dataClass = getTarget(binder);
List<DataClass> list = dataClass.dataClassList();
assertThat(list.get(0).param1()).isEqualTo("value1");
assertThat(list.get(1).param1()).isEqualTo("value2");
assertThat(list.get(3).param1()).isEqualTo("value3");
}
@Test
void mapBinding() {
MapValueResolver valueResolver = new MapValueResolver(Map.of(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -180,7 +180,7 @@ public final class GenericTypeResolver {
generics[i] = resolvedTypeArgument;
}
else {
generics[i] = ResolvableType.forType(typeArgument);
generics[i] = ResolvableType.forType(typeArgument).resolveType();
}
}
else if (typeArgument instanceof ParameterizedType) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -410,8 +410,9 @@ public class ResolvableType implements Serializable {
}
matchedBefore.put(this.type, other.type);
for (int i = 0; i < ourGenerics.length; i++) {
if (!ourGenerics[i].isAssignableFrom(otherGenerics[i],
!other.hasUnresolvableGenerics(), matchedBefore, upUntilUnresolvable)) {
ResolvableType otherGeneric = otherGenerics[i];
if (!ourGenerics[i].isAssignableFrom(otherGeneric,
!otherGeneric.isUnresolvableTypeVariable(), matchedBefore, upUntilUnresolvable)) {
return false;
}
}
@@ -1729,8 +1730,16 @@ public class ResolvableType implements Serializable {
* @return {@code true} if these bounds are assignable from all types
*/
public boolean isAssignableFrom(ResolvableType[] types, @Nullable Map<Type, Type> matchedBefore) {
for (ResolvableType type : types) {
if (!isAssignableFrom(type, matchedBefore)) {
for (ResolvableType bound : this.bounds) {
boolean matched = false;
for (ResolvableType type : types) {
if (this.kind == Kind.UPPER ? bound.isAssignableFrom(type, false, matchedBefore, false) :
type.isAssignableFrom(bound, false, matchedBefore, false)) {
matched = true;
break;
}
}
if (!matched) {
return false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -40,6 +40,7 @@ import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.NavigableSet;
@@ -645,13 +646,15 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
if (currentPrefix != null) {
// A prefix match found, potentially to be turned into a common parent cache entry.
if (commonPrefix == null || !commonUnique || currentPrefix.length() > commonPrefix.length()) {
commonPrefix = currentPrefix;
existingPath = path;
}
else if (currentPrefix.equals(commonPrefix)) {
commonUnique = false;
if (checkPathWithinPackage(path.substring(currentPrefix.length()))) {
// A prefix match found, potentially to be turned into a common parent cache entry.
if (commonPrefix == null || !commonUnique || currentPrefix.length() > commonPrefix.length()) {
commonPrefix = currentPrefix;
existingPath = path;
}
else if (currentPrefix.equals(commonPrefix)) {
commonUnique = false;
}
}
}
else if (actualRootPath == null || path.length() > actualRootPath.length()) {
@@ -804,7 +807,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (separatorIndex == -1) {
separatorIndex = urlFile.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
}
if (separatorIndex != -1) {
if (separatorIndex >= 0) {
jarFileUrl = urlFile.substring(0, separatorIndex);
rootEntryPath = urlFile.substring(separatorIndex + 2); // both separators are 2 chars
NavigableSet<String> entriesCache = this.jarEntriesCache.get(jarFileUrl);
@@ -831,11 +834,17 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (con instanceof JarURLConnection jarCon) {
// Should usually be the case for traditional JAR files.
jarFile = jarCon.getJarFile();
jarFileUrl = jarCon.getJarFileURL().toExternalForm();
JarEntry jarEntry = jarCon.getJarEntry();
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
closeJarFile = !jarCon.getUseCaches();
try {
jarFile = jarCon.getJarFile();
jarFileUrl = jarCon.getJarFileURL().toExternalForm();
JarEntry jarEntry = jarCon.getJarEntry();
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
closeJarFile = !jarCon.getUseCaches();
}
catch (FileNotFoundException ex) {
// Happens in case of cached root directory without specific subdirectory present.
return Collections.emptySet();
}
}
else {
// No JarURLConnection -> need to resort to URL file parsing.
@@ -872,7 +881,13 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
Set<Resource> result = new LinkedHashSet<>(64);
NavigableSet<String> entriesCache = new TreeSet<>();
for (String entryPath : jarFile.stream().map(JarEntry::getName).sorted().toList()) {
Iterator<String> entryIterator = jarFile.stream().map(JarEntry::getName).sorted().iterator();
while (entryIterator.hasNext()) {
String entryPath = entryIterator.next();
int entrySeparatorIndex = entryPath.indexOf(ResourceUtils.JAR_URL_SEPARATOR);
if (entrySeparatorIndex >= 0) {
entryPath = entryPath.substring(entrySeparatorIndex + ResourceUtils.JAR_URL_SEPARATOR.length());
}
entriesCache.add(entryPath);
if (entryPath.startsWith(rootEntryPath)) {
String relativePath = entryPath.substring(rootEntryPath.length());
@@ -1103,6 +1118,10 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
return (path.startsWith("/") ? path.substring(1) : path);
}
private static boolean checkPathWithinPackage(String path) {
return (path.contains("/") && !path.contains(ResourceUtils.JAR_URL_SEPARATOR));
}
/**
* Inner delegate class, avoiding a hard JBoss VFS API dependency at runtime.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,10 +51,11 @@ public class FlightRecorderApplicationStartup implements ApplicationStartup {
@Override
public StartupStep start(String name) {
Long parentId = this.currentSteps.getFirst();
long sequenceId = this.currentSequenceId.incrementAndGet();
this.currentSteps.offerFirst(sequenceId);
return new FlightRecorderStartupStep(sequenceId, name,
this.currentSteps.getFirst(), committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
parentId, committedStep -> this.currentSteps.removeFirstOccurrence(sequenceId));
}
}
@@ -519,7 +519,7 @@ final class PlaceholderParser {
@Override
public String resolve(PartResolutionContext resolutionContext) {
String value = resolveRecursively(resolutionContext, this.key);
String value = resolveRecursively(resolutionContext);
if (value != null) {
return value;
}
@@ -528,6 +528,17 @@ final class PlaceholderParser {
}
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
}
@Nullable
private String resolveRecursively(PartResolutionContext resolutionContext) {
if (!this.text().equals(this.key)) {
String value = resolveRecursively(resolutionContext, this.text());
if (value != null) {
return value;
}
}
return resolveRecursively(resolutionContext, this.key);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -719,8 +719,7 @@ public abstract class ReflectionUtils {
// Keep backing up the inheritance hierarchy.
Class<?> targetClass = clazz;
do {
Field[] fields = getDeclaredFields(targetClass);
for (Field field : fields) {
for (Field field : getDeclaredFields(targetClass)) {
if (ff != null && !ff.matches(field)) {
continue;
}
@@ -148,8 +148,8 @@ class GeneratedClassesTests {
"one", TestComponent.class, emptyTypeCustomizer);
GeneratedClass generatedClass2 = this.generatedClasses.getOrAddForFeatureComponent(
"one", TestComponent.class, emptyTypeCustomizer);
GeneratedClass generatedClass3 = prefixed.getOrAddForFeatureComponent
("one", TestComponent.class, emptyTypeCustomizer);
GeneratedClass generatedClass3 = prefixed.getOrAddForFeatureComponent(
"one", TestComponent.class, emptyTypeCustomizer);
GeneratedClass generatedClass4 = prefixed.getOrAddForFeatureComponent(
"one", TestComponent.class, emptyTypeCustomizer);
assertThat(generatedClass1).isSameAs(generatedClass2).isNotSameAs(generatedClass3);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -211,6 +211,14 @@ class GenericTypeResolverTests {
assertThat(ResolvableType.forType(resolved).getGeneric(0).getGeneric(0).resolve()).isEqualTo(String.class);
}
@Test
void resolvedTypeWithBase() {
Type type = method(WithBaseTypes.class, "get").getGenericReturnType();
Type resolvedType = resolveType(type, WithBaseTypes.class);
ParameterizedTypeReference<List<A>> reference = new ParameterizedTypeReference<>() {};
assertThat(resolvedType).isEqualTo(reference.getType());
}
private static Method method(Class<?> target, String methodName, Class<?>... parameterTypes) {
Method method = findMethod(target, methodName, parameterTypes);
assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull();
@@ -398,4 +406,12 @@ class GenericTypeResolverTests {
public interface StringListOfListSupplier extends ListOfListSupplier<String> {
}
static class WithBaseTypes {
<T extends A> List<T> get() {
return List.of();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1189,10 +1189,11 @@ class ResolvableTypeTests {
}
@Test
void isAssignableFromForUnresolvedWildcards() {
void isAssignableFromForUnresolvedWildcard() {
ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>());
ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed());
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class);
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, CharSequence.class);
ResolvableType wildcardConsumer = ResolvableType.forInstance(new WildcardConsumer<>());
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
assertThat(wildcard.isAssignableFromResolvedPart(wildcardFixed)).isTrue();
@@ -1206,6 +1207,38 @@ class ResolvableTypeTests {
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcard)).isTrue();
assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse();
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcardFixed)).isFalse();
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFrom(wildcard)).isFalse();
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFromResolvedPart(wildcard)).isTrue();
}
@Test
void isAssignableFromForUnresolvedDoubleWildcard() {
ResolvableType wildcard = ResolvableType.forInstance(new DoubleWildcard<>());
ResolvableType wildcardFixed = ResolvableType.forInstance(new DoubleWildcardFixed());
ResolvableType wildcardConsumer = ResolvableType.forInstance(new DoubleWildcardConsumer<>());
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
assertThat(wildcard.isAssignableFromResolvedPart(wildcardFixed)).isTrue();
assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse();
assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcard)).isFalse();
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFrom(wildcard)).isTrue();
assertThat(wildcardConsumer.as(Consumer.class).getGeneric().isAssignableFromResolvedPart(wildcard)).isTrue();
}
@Test
void strictGenericsMatching() {
ResolvableType consumerUnresolved = ResolvableType.forClass(Consumer.class);
ResolvableType consumerObject = ResolvableType.forClassWithGenerics(Consumer.class, Object.class);
ResolvableType consumerNestedUnresolved = ResolvableType.forClassWithGenerics(Consumer.class, ResolvableType.forClass(Consumer.class));
assertThat(consumerUnresolved.isAssignableFrom(consumerObject)).isTrue();
assertThat(consumerUnresolved.isAssignableFromResolvedPart(consumerObject)).isTrue();
assertThat(consumerObject.isAssignableFrom(consumerUnresolved)).isTrue();
assertThat(consumerObject.isAssignableFromResolvedPart(consumerUnresolved)).isTrue();
assertThat(consumerUnresolved.isAssignableFrom(consumerNestedUnresolved)).isTrue();
assertThat(consumerUnresolved.isAssignableFromResolvedPart(consumerNestedUnresolved)).isTrue();
assertThat(consumerObject.isAssignableFrom(consumerNestedUnresolved)).isFalse();
assertThat(consumerObject.isAssignableFromResolvedPart(consumerNestedUnresolved)).isFalse();
}
@Test
@@ -1752,12 +1785,28 @@ class ResolvableTypeTests {
}
public class Wildcard<T extends Number> {
public interface Consumer<T> {
}
public class WildcardFixed extends Wildcard<Integer> {
public class Wildcard<T extends CharSequence> {
}
public class WildcardFixed extends Wildcard<String> {
}
public class WildcardConsumer<T extends CharSequence & Serializable> implements Consumer<Wildcard<T>> {
}
public class DoubleWildcard<T extends CharSequence & Serializable> {
}
public class DoubleWildcardFixed extends DoubleWildcard<String> {
}
public class DoubleWildcardConsumer<T extends CharSequence & Serializable> implements Consumer<DoubleWildcard<T>> {
}
interface VariableNameSwitch<V, K> extends MultiValueMap<K, V> {
}
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InOrder;
import org.springframework.util.PlaceholderParser.ParsedValue;
import org.springframework.util.PlaceholderParser.TextPart;
@@ -33,6 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -220,12 +222,37 @@ class PlaceholderParserTests {
);
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("exactMatchPlaceholders")
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("prefix://my-service", "example-service");
properties.setProperty("px", "prefix");
properties.setProperty("p1", "${prefix://my-service}");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
}
static Stream<Arguments> exactMatchPlaceholders() {
return Stream.of(
Arguments.of("${prefix://my-service}", "example-service"),
Arguments.of("${p1}", "example-service")
);
}
@Test
void parseWithKeyEqualsToText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "Steve");
assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
.isEqualTo("Steve");
verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
void parseWithHardcodedFallback() {
PlaceholderResolver resolver = mockPlaceholderResolver();
assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver))
.isEqualTo("Steve");
verifyPlaceholderResolutions(resolver, "firstName");
verifyPlaceholderResolutions(resolver, "firstName:Steve", "firstName");
}
@Test
@@ -233,7 +260,7 @@ class PlaceholderParserTests {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver))
.isEqualTo("John");
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
verifyPlaceholderResolutions(resolver, "invalid:Name", "invalid", "firstName");
}
@Test
@@ -348,8 +375,9 @@ class PlaceholderParserTests {
}
void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
InOrder ordered = inOrder(mock);
for (String placeholder : placeholders) {
verify(mock).resolvePlaceholder(placeholder);
ordered.verify(mock).resolvePlaceholder(placeholder);
}
verifyNoMoreInteractions(mock);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,6 +44,7 @@ class PropertyPlaceholderHelperTests {
private final PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}");
@Test
void withProperties() {
String text = "foo=${foo}";
@@ -116,8 +117,8 @@ class PropertyPlaceholderHelperTests {
props.setProperty("foo", "bar");
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper("${", "}", null, null, false);
assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
helper.replacePlaceholders(text, props));
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> helper.replacePlaceholders(text, props));
}
@Nested
@@ -153,13 +154,30 @@ class PropertyPlaceholderHelperTests {
);
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("exactMatchPlaceholders")
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("prefix://my-service", "example-service");
properties.setProperty("px", "prefix");
properties.setProperty("p1", "${prefix://my-service}");
assertThat(this.helper.replacePlaceholders(text, properties)).isEqualTo(expected);
}
static Stream<Arguments> exactMatchPlaceholders() {
return Stream.of(
Arguments.of("${prefix://my-service}", "example-service"),
Arguments.of("${p1}", "example-service")
);
}
}
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
private static PlaceholderResolver mockPlaceholderResolver(String... pairs) {
if (pairs.length % 2 == 1) {
throw new IllegalArgumentException("size must be even, it is a set of key=value pairs");
}
PlaceholderResolver resolver = mock(PlaceholderResolver.class);
PlaceholderResolver resolver = mock();
for (int i = 0; i < pairs.length; i += 2) {
String key = pairs[i];
String value = pairs[i + 1];
@@ -168,5 +186,4 @@ class PropertyPlaceholderHelperTests {
return resolver;
}
}
@@ -229,8 +229,9 @@ public class FunctionReference extends SpelNodeImpl {
ReflectionHelper.convertAllMethodHandleArguments(converter, functionArgs, methodHandle, varArgPosition);
if (isSuspectedVarargs) {
if (declaredParamCount == 1) {
// We only repackage the varargs if it is the ONLY argument -- for example,
if (declaredParamCount == 1 && !methodHandle.isVarargsCollector()) {
// We only repackage the arguments if the MethodHandle accepts a single
// argument AND the MethodHandle is not a "varargs collector" -- for example,
// when we are dealing with a bound MethodHandle.
functionArgs = ReflectionHelper.setupArgumentsForVarargsInvocation(
methodHandle.type().parameterArray(), functionArgs);
@@ -108,11 +108,16 @@ class TestScenarioCreator {
"formatObjectVarargs", MethodType.methodType(String.class, String.class, Object[].class));
testContext.registerFunction("formatObjectVarargs", formatObjectVarargs);
// #formatObjectVarargs(format, args...)
// #formatPrimitiveVarargs(format, args...)
MethodHandle formatPrimitiveVarargs = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"formatPrimitiveVarargs", MethodType.methodType(String.class, String.class, int[].class));
testContext.registerFunction("formatPrimitiveVarargs", formatPrimitiveVarargs);
// #varargsFunctionHandle(args...)
MethodHandle varargsFunctionHandle = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"varargsFunction", MethodType.methodType(String.class, String[].class));
testContext.registerFunction("varargsFunctionHandle", varargsFunctionHandle);
// #add(int, int)
MethodHandle add = MethodHandles.lookup().findStatic(TestScenarioCreator.class,
"add", MethodType.methodType(int.class, int.class, int.class));
@@ -79,6 +79,8 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
@Test
void functionWithVarargs() {
// static String varargsFunction(String... strings) -> Arrays.toString(strings)
evaluate("#varargsFunction()", "[]", String.class);
evaluate("#varargsFunction(new String[0])", "[]", String.class);
evaluate("#varargsFunction('a')", "[a]", String.class);
@@ -241,6 +243,27 @@ class VariableAndFunctionTests extends AbstractExpressionTests {
evaluate("#formatObjectVarargs('x -> %s %s %s', {'a', 'b', 'c'})", expected, String.class);
}
@Test // gh-34109
void functionViaMethodHandleForStaticMethodThatAcceptsOnlyVarargs() {
// #varargsFunctionHandle: static String varargsFunction(String... strings) -> Arrays.toString(strings)
evaluate("#varargsFunctionHandle()", "[]", String.class);
evaluate("#varargsFunctionHandle(new String[0])", "[]", String.class);
evaluate("#varargsFunctionHandle('a')", "[a]", String.class);
evaluate("#varargsFunctionHandle('a','b','c')", "[a, b, c]", String.class);
evaluate("#varargsFunctionHandle(new String[]{'a','b','c'})", "[a, b, c]", String.class);
// Conversion from int to String
evaluate("#varargsFunctionHandle(25)", "[25]", String.class);
evaluate("#varargsFunctionHandle('b',25)", "[b, 25]", String.class);
evaluate("#varargsFunctionHandle(new int[]{1, 2, 3})", "[1, 2, 3]", String.class);
// Strings that contain a comma
evaluate("#varargsFunctionHandle('a,b')", "[a,b]", String.class);
evaluate("#varargsFunctionHandle('a', 'x,y', 'd')", "[a, x,y, d]", String.class);
// null values
evaluate("#varargsFunctionHandle(null)", "[null]", String.class);
evaluate("#varargsFunctionHandle('a',null,'b')", "[a, null, b]", String.class);
}
@Test
void functionMethodMustBeStatic() throws Exception {
SpelExpressionParser parser = new SpelExpressionParser();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.sql.DataSource;
@@ -66,6 +68,9 @@ public abstract class AbstractJdbcCall {
/** List of RefCursor/ResultSet RowMapper objects. */
private final Map<String, RowMapper<?>> declaredRowMappers = new LinkedHashMap<>();
/** Lock for the compilation step. */
private final Lock compilationLock = new ReentrantLock();
/**
* Has this operation been compiled? Compilation means at least checking
* that a DataSource or JdbcTemplate has been provided.
@@ -284,24 +289,30 @@ public abstract class AbstractJdbcCall {
* @throws org.springframework.dao.InvalidDataAccessApiUsageException if the object hasn't
* been correctly initialized, for example if no DataSource has been provided
*/
public final synchronized void compile() throws InvalidDataAccessApiUsageException {
if (!isCompiled()) {
if (getProcedureName() == null) {
throw new InvalidDataAccessApiUsageException("Procedure or Function name is required");
}
try {
this.jdbcTemplate.afterPropertiesSet();
}
catch (IllegalArgumentException ex) {
throw new InvalidDataAccessApiUsageException(ex.getMessage());
}
compileInternal();
this.compiled = true;
if (logger.isDebugEnabled()) {
logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") +
" [" + getProcedureName() + "] compiled");
public final void compile() throws InvalidDataAccessApiUsageException {
this.compilationLock.lock();
try {
if (!isCompiled()) {
if (getProcedureName() == null) {
throw new InvalidDataAccessApiUsageException("Procedure or Function name is required");
}
try {
this.jdbcTemplate.afterPropertiesSet();
}
catch (IllegalArgumentException ex) {
throw new InvalidDataAccessApiUsageException(ex.getMessage());
}
compileInternal();
this.compiled = true;
if (logger.isDebugEnabled()) {
logger.debug("SqlCall for " + (isFunction() ? "function" : "procedure") +
" [" + getProcedureName() + "] compiled");
}
}
}
finally {
this.compilationLock.unlock();
}
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -183,6 +183,13 @@ public abstract class JdbcTransactionObjectSupport implements SavepointManager,
catch (SQLFeatureNotSupportedException ex) {
// typically on Oracle - ignore
}
catch (SQLException ex) {
// ignore Microsoft SQLServerException: This operation is not supported.
String msg = ex.getMessage();
if (msg == null || !msg.contains("not supported")) {
throw new TransactionSystemException("Could not explicitly release JDBC savepoint", ex);
}
}
catch (Throwable ex) {
throw new TransactionSystemException("Could not explicitly release JDBC savepoint", ex);
}
@@ -98,7 +98,7 @@ public class CompositeMessageCondition implements MessageCondition<CompositeMess
checkCompatible(other);
List<MessageCondition<?>> otherConditions = other.getMessageConditions();
for (int i = 0; i < this.messageConditions.size(); i++) {
int result = compare (this.messageConditions.get(i), otherConditions.get(i), message);
int result = compare(this.messageConditions.get(i), otherConditions.get(i), message);
if (result != 0) {
return result;
}
@@ -186,7 +186,7 @@ public abstract class SharedEntityManagerCreator {
@SuppressWarnings("serial")
private static class SharedEntityManagerInvocationHandler implements InvocationHandler, Serializable {
private final Log logger = LogFactory.getLog(getClass());
private static final Log logger = LogFactory.getLog(SharedEntityManagerInvocationHandler.class);
private final EntityManagerFactory targetFactory;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -82,6 +82,9 @@ import org.springframework.lang.Nullable;
* <li>{@link ActiveProfiles @ActiveProfiles}</li>
* <li>{@link TestPropertySource @TestPropertySource}</li>
* <li>{@link DynamicPropertySource @DynamicPropertySource}</li>
* <li>{@link org.springframework.test.context.bean.override.convention.TestBean @TestBean}</li>
* <li>{@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}</li>
* <li>{@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}</li>
* <li>{@link org.springframework.test.annotation.DirtiesContext @DirtiesContext}</li>
* <li>{@link org.springframework.transaction.annotation.Transactional @Transactional}</li>
* <li>{@link org.springframework.test.annotation.Rollback @Rollback}</li>
@@ -25,15 +25,19 @@ import java.lang.annotation.Target;
import org.springframework.aot.hint.annotation.Reflective;
/**
* Mark a composed annotation as eligible for Bean Override processing.
* Mark a <em>composed annotation</em> as eligible for Bean Override processing.
*
* <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 non-static fields, it is
* expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}.
* <p>Since the composed annotation will typically only be applied to non-static
* fields, it is expected that the composed annotation is meta-annotated with
* {@link Target @Target(ElementType.FIELD)}. However, certain bean override
* annotations may be declared with an additional {@code ElementType.TYPE} target
* for use at the type level, as is the case for {@code @MockitoBean} which can
* be declared on a field, test class, or test interface.
*
* <p>For concrete examples, see
* <p>For concrete examples of such composed annotations, see
* {@link org.springframework.test.context.bean.override.convention.TestBean @TestBean},
* {@link org.springframework.test.context.bean.override.mockito.MockitoBean @MockitoBean}, and
* {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -104,11 +104,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
Set<String> generatedBeanNames) {
String beanName = handler.getBeanName();
Field field = handler.getField();
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
Unable to override bean '%s' for field '%s.%s': a FactoryBean cannot be overridden. \
To override the bean created by the FactoryBean, remove the '&' prefix.""".formatted(
beanName, field.getDeclaringClass().getSimpleName(), field.getName()));
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName), () -> """
Unable to override bean '%s'%s: a FactoryBean cannot be overridden. \
To override the bean created by the FactoryBean, remove the '&' prefix."""
.formatted(beanName, forField(handler.getField())));
switch (handler.getStrategy()) {
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
@@ -134,7 +133,6 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
// 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);
@@ -169,11 +167,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
}
else if (requireExistingBean) {
throw new IllegalStateException("""
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()));
Field field = handler.getField();
throw new IllegalStateException(
"Unable to replace bean: there is no bean with name '%s' and type %s%s."
.formatted(beanName, handler.getBeanType(), requiredByField(field)));
}
// 4) We are creating a bean by-name with the provided beanName.
}
@@ -264,13 +261,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
else {
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());
message += "there are no beans of type %s%s.".formatted(beanType, requiredByField(field));
}
else {
message += "found %d beans of type %s (as required by field '%s.%s'): %s"
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
field.getName(), candidateNames);
message += "found %d beans of type %s%s: %s"
.formatted(candidateCount, beanType, requiredByField(field), candidateNames);
}
throw new IllegalStateException(message);
}
@@ -281,11 +276,9 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
// We are wrapping an existing bean by-name.
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
if (!candidates.contains(beanName)) {
throw new IllegalStateException("""
Unable to wrap bean: there is no bean with name '%s' and type %s \
(as required by field '%s.%s')."""
.formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(),
field.getName()));
throw new IllegalStateException(
"Unable to wrap bean: there is no bean with name '%s' and type %s%s."
.formatted(beanName, beanType, requiredByField(field)));
}
}
@@ -308,8 +301,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
else if (candidateCount == 0) {
if (requireExistingBean) {
throw new IllegalStateException(
"Unable to override bean: there are no beans of type %s (as required by field '%s.%s')."
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()));
"Unable to override bean: there are no beans of type %s%s."
.formatted(beanType, requiredByField(field)));
}
return null;
}
@@ -320,14 +313,14 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
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));
"Unable to select a bean to override: found %d beans of type %s%s: %s"
.formatted(candidateCount, beanType, requiredByField(field), candidateNames));
}
private Set<String> getExistingBeanNamesByType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
boolean checkAutowiredCandidate) {
Field field = handler.getField();
ResolvableType resolvableType = handler.getBeanType();
Class<?> type = resolvableType.toClass();
@@ -345,16 +338,16 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
}
// Filter out non-matching autowire candidates.
if (checkAutowiredCandidate) {
DependencyDescriptor descriptor = new DependencyDescriptor(handler.getField(), true);
if (field != null && checkAutowiredCandidate) {
DependencyDescriptor descriptor = new DependencyDescriptor(field, true);
beanNames.removeIf(beanName -> !beanFactory.isAutowireCandidate(beanName, descriptor));
}
// Filter out scoped proxy targets.
beanNames.removeIf(ScopedProxyUtils::isScopedTarget);
// In case of multiple matches, fall back on the field's name as a last resort.
if (beanNames.size() > 1) {
String fieldName = handler.getField().getName();
if (field != null && beanNames.size() > 1) {
String fieldName = field.getName();
if (beanNames.contains(fieldName)) {
return Set.of(fieldName);
}
@@ -452,4 +445,19 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
dlbf.destroySingleton(beanName);
}
private static String forField(@Nullable Field field) {
if (field == null) {
return "";
}
return " for field '%s.%s'".formatted(field.getDeclaringClass().getSimpleName(), field.getName());
}
private static String requiredByField(@Nullable Field field) {
if (field == null) {
return "";
}
return " (as required by field '%s.%s')".formatted(
field.getDeclaringClass().getSimpleName(), field.getName());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,7 @@ import org.springframework.test.context.MergedContextConfiguration;
/**
* {@link ContextCustomizer} implementation that registers the necessary
* infrastructure to support {@linkplain BeanOverride bean overriding}.
* infrastructure to support {@linkplain BeanOverride Bean Overrides}.
*
* @author Simon Baslé
* @author Stephane Nicoll
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,12 +23,11 @@ import java.util.Set;
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
* Bean Overriding.
* {@linkplain BeanOverride Bean Overrides}.
*
* @author Simon Baslé
* @author Stephane Nicoll
@@ -44,18 +43,15 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
List<ContextConfigurationAttributes> configAttributes) {
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>();
findBeanOverrideHandler(testClass, handlers);
findBeanOverrideHandlers(testClass, handlers);
if (handlers.isEmpty()) {
return null;
}
return new BeanOverrideContextCustomizer(handlers);
}
private void findBeanOverrideHandler(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
findBeanOverrideHandler(testClass.getEnclosingClass(), handlers);
}
BeanOverrideHandler.forTestClass(testClass).forEach(handler ->
private void findBeanOverrideHandlers(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
BeanOverrideHandler.findAllHandlers(testClass).forEach(handler ->
Assert.state(handlers.add(handler), () ->
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
.formatted(testClass.getName(), handler)));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,16 +17,18 @@
package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BiConsumer;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.BeanDefinition;
@@ -36,6 +38,7 @@ import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.test.context.TestContextAnnotationUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
@@ -51,12 +54,14 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
* unique set of metadata used to identify the bean to override. Overridden
* {@code equals()} and {@code hashCode()} methods should also delegate to the
* {@code super} implementations in this class in order to support the basic
* metadata used by all bean overrides.
* metadata used by all bean overrides. In addition, it is recommended that
* implementations override {@code toString()} to include all relevant metadata
* in order to enhance diagnostics.
*
* <p>Concrete implementations of {@code BeanOverrideHandler} can store additional
* metadata to use during override {@linkplain #createOverrideInstance instance
* creation} &mdash; for example, based on further processing of the annotation
* or the annotated field.
* creation} &mdash; for example, based on further processing of the annotation,
* the annotated field, or the annotated class.
*
* <p><strong>NOTE</strong>: Only <em>singleton</em> beans can be overridden.
* Any attempt to override a non-singleton bean will result in an exception.
@@ -68,9 +73,14 @@ import static org.springframework.core.annotation.MergedAnnotations.SearchStrate
*/
public abstract class BeanOverrideHandler {
private static final Comparator<MergedAnnotation<? extends Annotation>> reversedMetaDistance =
Comparator.<MergedAnnotation<? extends Annotation>> comparingInt(MergedAnnotation::getDistance).reversed();
@Nullable
private final Field field;
private final Set<Annotation> fieldAnnotations;
private final Set<Annotation> qualifierAnnotations;
private final ResolvableType beanType;
@@ -80,11 +90,11 @@ public abstract class BeanOverrideHandler {
private final BeanOverrideStrategy strategy;
protected BeanOverrideHandler(Field field, ResolvableType beanType, @Nullable String beanName,
protected BeanOverrideHandler(@Nullable Field field, ResolvableType beanType, @Nullable String beanName,
BeanOverrideStrategy strategy) {
this.field = field;
this.fieldAnnotations = annotationSet(field);
this.qualifierAnnotations = getQualifierAnnotations(field);
this.beanType = beanType;
this.beanName = beanName;
this.strategy = strategy;
@@ -93,39 +103,117 @@ public abstract class BeanOverrideHandler {
/**
* Process the given {@code testClass} and build the corresponding
* {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
* fields in the test class, its type hierarchy, and its enclosing class hierarchy.
* fields in the test class and its type hierarchy.
* <p>This method does not search the enclosing class hierarchy and does not
* search for {@code @BeanOverride} declarations on classes or interfaces.
* @param testClass the test class to process
* @return a list of bean override handlers
* @see #findAllHandlers(Class)
*/
public static List<BeanOverrideHandler> forTestClass(Class<?> testClass) {
List<BeanOverrideHandler> handlers = new LinkedList<>();
ReflectionUtils.doWithFields(testClass, field -> processField(field, testClass, handlers));
return findHandlers(testClass, true);
}
/**
* Process the given {@code testClass} and build the corresponding
* {@code BeanOverrideHandler} list derived from {@link BeanOverride @BeanOverride}
* fields in the test class and in its type hierarchy as well as from
* {@code @BeanOverride} declarations on classes and interfaces.
* <p>This method additionally searches for {@code @BeanOverride} declarations
* in the enclosing class hierarchy based on
* {@link TestContextAnnotationUtils#searchEnclosingClass(Class)} semantics.
* @param testClass the test class to process
* @return a list of bean override handlers
* @since 6.2.2
*/
static List<BeanOverrideHandler> findAllHandlers(Class<?> testClass) {
return findHandlers(testClass, false);
}
private static List<BeanOverrideHandler> findHandlers(Class<?> testClass, boolean localFieldsOnly) {
List<BeanOverrideHandler> handlers = new ArrayList<>();
findHandlers(testClass, testClass, handlers, localFieldsOnly);
return handlers;
}
/**
* Find handlers using tail recursion to ensure that "locally declared" bean overrides
* take precedence over inherited bean overrides.
* <p>Note: the search algorithm is effectively the inverse of the algorithm used in
* {@link org.springframework.test.context.TestContextAnnotationUtils#findAnnotationDescriptor(Class, Class)},
* but with tail recursion the semantics should be the same.
* @param clazz the class in/on which to search
* @param testClass the original test class
* @param handlers the list of handlers found
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly) {
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly);
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly);
}
// 4) Process current class.
processClass(clazz, testClass, handlers);
}
// 5) Process fields in current class.
ReflectionUtils.doWithLocalFields(clazz, field -> processField(field, testClass, handlers));
}
private static void processClass(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers) {
processElement(clazz, testClass, (processor, composedAnnotation) ->
processor.createHandlers(composedAnnotation, testClass).forEach(handlers::add));
}
private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
processElement(field, testClass, (processor, composedAnnotation) -> {
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");
BeanOverride beanOverride = mergedAnnotation.synthesize();
BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value());
Annotation composedAnnotation = metaSource.synthesize();
Assert.state(overrideAnnotationFound.compareAndSet(false, true),
() -> "Multiple @BeanOverride annotations found on field: " + field);
BeanOverrideHandler handler = processor.createHandler(composedAnnotation, testClass, field);
handlers.add(handler);
handlers.add(processor.createHandler(composedAnnotation, testClass, field));
});
}
private static void processElement(AnnotatedElement element, Class<?> testClass,
BiConsumer<BeanOverrideProcessor, Annotation> consumer) {
MergedAnnotations.from(element, DIRECT)
.stream(BeanOverride.class)
.sorted(reversedMetaDistance)
.forEach(mergedAnnotation -> {
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
BeanOverride beanOverride = mergedAnnotation.synthesize();
BeanOverrideProcessor processor = BeanUtils.instantiateClass(beanOverride.value());
Annotation composedAnnotation = metaSource.synthesize();
consumer.accept(processor, composedAnnotation);
});
}
/**
* Get the annotated {@link Field}.
*/
@Nullable
public final Field getField() {
return this.field;
}
@@ -222,20 +310,25 @@ public abstract class BeanOverrideHandler {
!Objects.equals(this.strategy, that.strategy)) {
return false;
}
// by-name lookup
if (this.beanName != null) {
return true;
}
// by-type lookup
return (Objects.equals(this.field.getName(), that.field.getName()) &&
this.fieldAnnotations.equals(that.fieldAnnotations));
if (this.field == null) {
return (that.field == null);
}
return (that.field != null && this.field.getName().equals(that.field.getName()) &&
this.qualifierAnnotations.equals(that.qualifierAnnotations));
}
@Override
public int hashCode() {
int hash = Objects.hash(getClass(), this.beanType.getType(), this.beanName, this.strategy);
return (this.beanName != null ? hash : hash +
Objects.hash(this.field.getName(), this.fieldAnnotations));
Objects.hash((this.field != null ? this.field.getName() : null), this.qualifierAnnotations));
}
@Override
@@ -248,9 +341,27 @@ public abstract class BeanOverrideHandler {
.toString();
}
private static Set<Annotation> annotationSet(Field field) {
Annotation[] annotations = field.getAnnotations();
return (annotations.length != 0 ? new HashSet<>(Arrays.asList(annotations)) : Collections.emptySet());
private static Set<Annotation> getQualifierAnnotations(@Nullable Field field) {
if (field == null) {
return Collections.emptySet();
}
Annotation[] candidates = field.getDeclaredAnnotations();
if (candidates.length == 0) {
return Collections.emptySet();
}
Set<Annotation> annotations = new HashSet<>(candidates.length - 1);
for (Annotation candidate : candidates) {
// Assume all non-BeanOverride annotations are "qualifiers".
if (!isBeanOverrideAnnotation(candidate.annotationType())) {
annotations.add(candidate);
}
}
return annotations;
}
private static boolean isBeanOverrideAnnotation(Class<? extends Annotation> type) {
return MergedAnnotations.from(type).isPresent(BeanOverride.class);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,10 +18,13 @@ package org.springframework.test.context.bean.override;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Collections;
import java.util.List;
/**
* Strategy interface for Bean Override processing, which creates a
* {@link BeanOverrideHandler} that drives how the target bean is overridden.
* Strategy interface for Bean Override processing, which creates
* {@link BeanOverrideHandler} instances that drive how target beans are
* overridden.
*
* <p>At least one composed annotation that is meta-annotated with
* {@link BeanOverride @BeanOverride} must be a companion of this processor and
@@ -36,17 +39,45 @@ import java.lang.reflect.Field;
* @author Sam Brannen
* @since 6.2
*/
@FunctionalInterface
public interface BeanOverrideProcessor {
/**
* Create a {@link BeanOverrideHandler} for the given annotated field.
* <p>This method will only be invoked when a {@link BeanOverride @BeanOverride}
* annotation is declared on a field &mdash; for example, if the supplied field
* is annotated with {@code @MockitoBean}.
* @param overrideAnnotation the composed annotation that declares the
* {@link BeanOverride @BeanOverride} annotation which registers this processor
* {@code @BeanOverride} annotation which registers this processor
* @param testClass the test class to process
* @param field the annotated field
* @return the {@code BeanOverrideHandler} that should handle the given field
* @see #createHandlers(Annotation, Class)
*/
BeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Field field);
/**
* Create a list of {@link BeanOverrideHandler} instances for the given override
* annotation and test class.
* <p>This method will only be invoked when a {@link BeanOverride @BeanOverride}
* annotation is declared at the type level &mdash; for example, if the supplied
* test class is annotated with {@code @MockitoBean}.
* <p>Note that the test class may not be directly annotated with the override
* annotation. For example, the override annotation may have been declared
* on an interface, superclass, or enclosing class within the test class
* hierarchy.
* <p>The default implementation returns an empty list, signaling that this
* {@code BeanOverrideProcessor} does not support type-level {@code @BeanOverride}
* declarations. Can be overridden by concrete implementations to support
* type-level use cases.
* @param overrideAnnotation the composed annotation that declares the
* {@code @BeanOverride} annotation which registers this processor
* @param testClass the test class to process
* @return the list of {@code BeanOverrideHandlers} for the annotated class
* @since 6.2.2
* @see #createHandler(Annotation, Class, Field)
*/
default List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
return Collections.emptyList();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -108,10 +108,11 @@ class BeanOverrideRegistry {
}
void inject(Object target, BeanOverrideHandler handler) {
Field field = handler.getField();
Assert.notNull(field, () -> "BeanOverrideHandler must have a non-null field: " + handler);
String beanName = this.handlerToBeanNameMap.get(handler);
Assert.state(StringUtils.hasLength(beanName),
() -> "No bean found for BeanOverrideHandler: " + handler);
inject(handler.getField(), target, beanName);
Assert.state(StringUtils.hasLength(beanName), () -> "No bean found for BeanOverrideHandler: " + handler);
inject(field, target, beanName);
}
private void inject(Field field, Object target, String beanName) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,24 +44,24 @@ import org.springframework.test.context.bean.override.BeanOverride;
* you can set the {@link #enforceOverride() enforceOverride} attribute to {@code true}
* &mdash; for example, {@code @TestBean(enforceOverride = true)}.
*
* <p>The instance is created from a zero-argument static factory method in the
* test class whose return type is compatible with the annotated field. In the
* case of a nested test class, the enclosing class hierarchy is also searched.
* Similarly, if the test class extends from a base class or implements any
* interfaces, the entire type hierarchy is searched. Alternatively, a factory
* method in an external class can be referenced via its fully-qualified method
* name following the syntax {@code <fully-qualified class name>#<method name>}
* &mdash; for example,
* <p>The instance is created from a zero-argument static factory method whose
* return type is compatible with the annotated field. The factory method can be
* declared directly in the class which declares the {@code @TestBean} field or
* within the type hierarchy above that class, including implemented interfaces.
* If the {@code @TestBean} field is declared in a nested test class, the enclosing
* class hierarchy is also searched. Alternatively, a factory method in an external
* class can be referenced via its fully-qualified method name following the syntax
* {@code <fully-qualified class name>#<method name>} &mdash; for example,
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
*
* <p>The factory method is deduced as follows.
*
* <ul>
* <li>If the {@link #methodName()} is specified, look for a static method with
* that name.</li>
* <li>If a method name is not specified, look for exactly one static method
* named with either the name of the annotated field or the name of the bean
* (if specified).</li>
* <li>If the {@link #methodName methodName} is specified, Spring looks for a static
* method with that name.</li>
* <li>If a method name is not specified, Spring looks for exactly one static method
* whose name is either the name of the annotated field or the {@link #name() name}
* of the bean (if specified).</li>
* </ul>
*
* <p>Consider the following example.
@@ -110,6 +110,10 @@ import org.springframework.test.context.bean.override.BeanOverride;
* {@code protected}, package-private (default visibility), or {@code private}
* depending on the needs or coding practices of the project.
*
* <p>{@code @TestBean} fields will be inherited from an enclosing test class by default. See
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* for details.
*
* @author Simon Baslé
* @author Stephane Nicoll
* @author Sam Brannen
@@ -146,15 +150,17 @@ public @interface TestBean {
/**
* Name of the static factory method that will be used to instantiate the bean
* to override.
* <p>A search will be performed to find the factory method in the test class,
* in one of its superclasses, or in any implemented interfaces. In the case
* of a nested test class, the enclosing class hierarchy will also be searched.
* <p>A search will be performed to find the factory method in the class in
* which the {@code @TestBean} field is declared, in one of its superclasses,
* or in any implemented interfaces. If the {@code @TestBean} field is declared
* in a nested test class, the enclosing class hierarchy will also be searched.
* <p>Alternatively, a factory method in an external class can be referenced
* via its fully-qualified method name following the syntax
* {@code <fully-qualified class name>#<method name>} &mdash; for example,
* {@code @TestBean(methodName = "org.example.TestUtils#createCustomerRepository")}.
* <p>If left unspecified, the name of the factory method will be detected
* based either on the name of the annotated field or the name of the bean.
* based either on the name of the {@code @TestBean} field or the {@link #name() name}
* of the bean.
*/
String methodName() default "";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -67,7 +67,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
Method factoryMethod;
if (!methodName.isBlank()) {
// If the user specified an explicit method name, search for that.
factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), methodName);
factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), methodName);
}
else {
// Otherwise, search for candidate factory methods whose names match either
@@ -78,7 +78,7 @@ class TestBeanOverrideProcessor implements BeanOverrideProcessor {
if (beanName != null) {
candidateMethodNames.add(beanName);
}
factoryMethod = findTestBeanFactoryMethod(testClass, field.getType(), candidateMethodNames);
factoryMethod = findTestBeanFactoryMethod(field.getDeclaringClass(), field.getType(), candidateMethodNames);
}
return new TestBeanOverrideHandler(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +38,7 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
private final MockReset reset;
protected AbstractMockitoBeanOverrideHandler(Field field, ResolvableType beanType,
protected AbstractMockitoBeanOverrideHandler(@Nullable Field field, ResolvableType beanType,
@Nullable String beanName, BeanOverrideStrategy strategy, @Nullable MockReset reset) {
super(field, beanType, beanName, strategy);
@@ -56,20 +56,20 @@ abstract class AbstractMockitoBeanOverrideHandler extends BeanOverrideHandler {
@Override
protected void trackOverrideInstance(Object mock, SingletonBeanRegistry trackingBeanRegistry) {
getMockitoBeans(trackingBeanRegistry).add(mock);
getMockBeans(trackingBeanRegistry).add(mock);
}
private static MockitoBeans getMockitoBeans(SingletonBeanRegistry trackingBeanRegistry) {
String beanName = MockitoBeans.class.getName();
MockitoBeans mockitoBeans = null;
private static MockBeans getMockBeans(SingletonBeanRegistry trackingBeanRegistry) {
String beanName = MockBeans.class.getName();
MockBeans mockBeans = null;
if (trackingBeanRegistry.containsSingleton(beanName)) {
mockitoBeans = (MockitoBeans) trackingBeanRegistry.getSingleton(beanName);
mockBeans = (MockBeans) trackingBeanRegistry.getSingleton(beanName);
}
if (mockitoBeans == null) {
mockitoBeans = new MockitoBeans();
trackingBeanRegistry.registerSingleton(beanName, mockitoBeans);
if (mockBeans == null) {
mockBeans = new MockBeans();
trackingBeanRegistry.registerSingleton(beanName, mockBeans);
}
return mockitoBeans;
return mockBeans;
}
@Override
@@ -0,0 +1,54 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito;
import java.util.ArrayList;
import java.util.List;
import org.mockito.Mockito;
/**
* Beans created using Mockito.
*
* @author Andy Wilkinson
* @author Sam Brannen
* @since 6.2
*/
class MockBeans {
private final List<Object> beans = new ArrayList<>();
void add(Object bean) {
this.beans.add(bean);
}
/**
* Reset all Mockito beans configured with the supplied {@link MockReset} strategy.
* <p>No mocks will be reset if the supplied strategy is {@link MockReset#NONE}.
*/
void resetAll(MockReset reset) {
if (reset != MockReset.NONE) {
for (Object bean : this.beans) {
if (reset == MockReset.get(bean)) {
Mockito.reset(bean);
}
}
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@@ -29,17 +30,37 @@ 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 non-static field
* in a test class to override a bean in the test's
* {@code @MockitoBean} is an annotation that can be used in test classes to
* override beans in a test's
* {@link org.springframework.context.ApplicationContext ApplicationContext}
* using a Mockito mock.
* using Mockito mocks.
*
* <p>By default, the bean to mock is inferred from the type of the annotated
* field. If multiple candidates exist, a {@code @Qualifier} annotation can be
* used to help disambiguate. In the absence of a {@code @Qualifier} annotation,
* the name of the annotated field will be used as a fallback qualifier.
* Alternatively, you can explicitly specify a bean name to mock by setting the
* {@link #value() value} or {@link #name() name} attribute.
* <p>{@code @MockitoBean} can be applied in the following ways.
* <ul>
* <li>On a non-static field in a test class or any of its superclasses.</li>
* <li>On a non-static field in an enclosing class for a {@code @Nested} test class
* or in any class in the type hierarchy or enclosing class hierarchy above the
* {@code @Nested} test class.</li>
* <li>At the type level on a test class or any superclass or implemented interface
* in the type hierarchy above the test class.</li>
* <li>At the type level on an enclosing class for a {@code @Nested} test class
* or on any class or interface in the type hierarchy or enclosing class hierarchy
* above the {@code @Nested} test class.</li>
* </ul>
*
* <p>When {@code @MockitoBean} is declared on a field, the bean to mock is inferred
* from the type of the annotated field. If multiple candidates exist, a
* {@code @Qualifier} annotation can be declared on the field to help disambiguate.
* In the absence of a {@code @Qualifier} annotation, the name of the annotated
* field will be used as a fallback qualifier. Alternatively, you can explicitly
* specify a bean name to mock by setting the {@link #value() value} or
* {@link #name() name} attribute.
*
* <p>When {@code @MockitoBean} is declared at the type level, the type of bean
* to mock must be supplied via the {@link #types() types} attribute. If multiple
* candidates exist, you can explicitly specify a bean name to mock by setting the
* {@link #name() name} attribute. Note, however, that the {@code types} attribute
* must contain a single type if an explicit bean {@code name} is configured.
*
* <p>A bean will be created if a corresponding bean does not exist. However, if
* you would like for the test to fail when a corresponding bean does not exist,
@@ -63,15 +84,29 @@ import org.springframework.test.context.bean.override.BeanOverride;
* (default visibility), or {@code private} depending on the needs or coding
* practices of the project.
*
* <p>{@code @MockitoBean} fields and type-level {@code @MockitoBean} declarations
* will be inherited from an enclosing test class by default. See
* {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* for details.
*
* <p>{@code @MockitoBean} may be used as a <em>meta-annotation</em> to create custom
* <em>composed annotations</em> &mdash; for example, to define common mock
* configuration in a single annotation that can be reused across a test suite.
* {@code @MockitoBean} can also be used as a <em>{@linkplain Repeatable repeatable}</em>
* annotation at the type level &mdash; for example, to mock several beans by
* {@link #name() name}.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
* @see org.springframework.test.context.bean.override.mockito.MockitoBeans @MockitoBeans
* @see org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean
* @see org.springframework.test.context.bean.override.convention.TestBean @TestBean
*/
@Target(ElementType.FIELD)
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(MockitoBeans.class)
@BeanOverride(MockitoBeanOverrideProcessor.class)
public @interface MockitoBean {
@@ -87,13 +122,27 @@ public @interface MockitoBean {
/**
* Name of the bean to mock.
* <p>If left unspecified, the bean to mock is selected according to the
* annotated field's type, taking qualifiers into account if necessary. See
* the {@linkplain MockitoBean class-level documentation} for details.
* configured {@link #types() types} or the annotated field's type, taking
* qualifiers into account if necessary. See the {@linkplain MockitoBean
* class-level documentation} for details.
* @see #value()
*/
@AliasFor("value")
String name() default "";
/**
* One or more types to mock.
* <p>Defaults to none.
* <p>Each type specified will result in a mock being created and registered
* with the {@code ApplicationContext}.
* <p>Types must be omitted when the annotation is used on a field.
* <p>When {@code @MockitoBean} also defines a {@link #name}, this attribute
* can only contain a single value.
* @return the types to mock
* @since 6.2.2
*/
Class<?>[] types() default {};
/**
* Extra interfaces that should also be declared by the mock.
* <p>Defaults to none.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -57,20 +57,24 @@ class MockitoBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
private final boolean serializable;
MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
MockitoBeanOverrideHandler(ResolvableType typeToMock, MockitoBean mockitoBean) {
this(null, typeToMock, mockitoBean);
}
MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, MockitoBean mockitoBean) {
this(field, typeToMock, (!mockitoBean.name().isBlank() ? mockitoBean.name() : null),
(mockitoBean.enforceOverride() ? REPLACE : REPLACE_OR_CREATE),
mockitoBean.reset(), mockitoBean.extraInterfaces(), mockitoBean.answers(), mockitoBean.serializable());
}
private MockitoBeanOverrideHandler(Field field, ResolvableType typeToMock, @Nullable String beanName,
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, @Nullable Answers answers,
private MockitoBeanOverrideHandler(@Nullable Field field, ResolvableType typeToMock, @Nullable String beanName,
BeanOverrideStrategy strategy, MockReset reset, Class<?>[] extraInterfaces, Answers answers,
boolean serializable) {
super(field, typeToMock, beanName, strategy, reset);
Assert.notNull(typeToMock, "'typeToMock' must not be null");
this.extraInterfaces = asClassSet(extraInterfaces);
this.answers = (answers != null ? answers : Answers.RETURNS_DEFAULTS);
this.answers = answers;
this.serializable = serializable;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,15 +18,20 @@ package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideProcessor;
import org.springframework.util.Assert;
/**
* {@link BeanOverrideProcessor} implementation that provides support for
* {@link MockitoBean @MockitoBean} and {@link MockitoSpyBean @MockitoSpyBean}.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
* @see MockitoBean @MockitoBean
* @see MockitoSpyBean @MockitoSpyBean
@@ -35,8 +40,10 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
@Override
public AbstractMockitoBeanOverrideHandler createHandler(Annotation overrideAnnotation, Class<?> testClass, Field field) {
if (overrideAnnotation instanceof MockitoBean mockBean) {
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockBean);
if (overrideAnnotation instanceof MockitoBean mockitoBean) {
Assert.state(mockitoBean.types().length == 0,
"The @MockitoBean 'types' attribute must be omitted when declared on a field");
return new MockitoBeanOverrideHandler(field, ResolvableType.forField(field, testClass), mockitoBean);
}
else if (overrideAnnotation instanceof MockitoSpyBean spyBean) {
return new MockitoSpyBeanOverrideHandler(field, ResolvableType.forField(field, testClass), spyBean);
@@ -47,4 +54,23 @@ class MockitoBeanOverrideProcessor implements BeanOverrideProcessor {
.formatted(field.getDeclaringClass().getName(), field.getName()));
}
@Override
public List<BeanOverrideHandler> createHandlers(Annotation overrideAnnotation, Class<?> testClass) {
if (!(overrideAnnotation instanceof MockitoBean mockitoBean)) {
throw new IllegalStateException("""
Invalid annotation passed to MockitoBeanOverrideProcessor: \
expected @MockitoBean on test class """ + testClass.getName());
}
Class<?>[] types = mockitoBean.types();
Assert.state(types.length > 0,
"The @MockitoBean 'types' attribute must not be empty when declared on a class");
Assert.state(mockitoBean.name().isEmpty() || types.length == 1,
"The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
List<BeanOverrideHandler> handlers = new ArrayList<>();
for (Class<?> type : types) {
handlers.add(new MockitoBeanOverrideHandler(ResolvableType.forClass(type), mockitoBean));
}
return handlers;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,39 +16,26 @@
package org.springframework.test.context.bean.override.mockito;
import java.util.ArrayList;
import java.util.List;
import org.mockito.Mockito;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Beans created using Mockito.
* Container for {@link MockitoBean @MockitoBean} annotations which allows
* {@code @MockitoBean} to be used as a {@linkplain java.lang.annotation.Repeatable
* repeatable annotation} at the type level &mdash; for example, on test classes
* or interfaces implemented by test classes.
*
* @author Andy Wilkinson
* @author Sam Brannen
* @since 6.2
* @since 6.2.2
*/
class MockitoBeans {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MockitoBeans {
private final List<Object> beans = new ArrayList<>();
void add(Object bean) {
this.beans.add(bean);
}
/**
* Reset all Mockito beans configured with the supplied {@link MockReset} strategy.
* <p>No mocks will be reset if the supplied strategy is {@link MockReset#NONE}.
*/
void resetAll(MockReset reset) {
if (reset != MockReset.NONE) {
for (Object bean : this.beans) {
if (reset == MockReset.get(bean)) {
Mockito.reset(bean);
}
}
}
}
MockitoBean[] value();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -116,7 +116,7 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionList
}
}
try {
beanFactory.getBean(MockitoBeans.class).resetAll(reset);
beanFactory.getBean(MockBeans.class).resetAll(reset);
}
catch (NoSuchBeanDefinitionException ex) {
// Continue
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -56,6 +56,10 @@ import org.springframework.test.context.bean.override.BeanOverride;
* (default visibility), or {@code private} depending on the needs or coding
* practices of the project.
*
* <p>{@code @MockitoSpyBean} fields will be inherited from an enclosing test class by default.
* See {@link org.springframework.test.context.NestedTestConfiguration @NestedTestConfiguration}
* for details.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -48,15 +48,9 @@ class MockitoSpyBeanOverrideHandler extends AbstractMockitoBeanOverrideHandler {
new SpringAopBypassingVerificationStartedListener();
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyAnnotation) {
this(field, typeToSpy, (StringUtils.hasText(spyAnnotation.name()) ? spyAnnotation.name() : null),
spyAnnotation.reset());
}
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, @Nullable String beanName,
MockReset reset) {
super(field, typeToSpy, beanName, BeanOverrideStrategy.WRAP, reset);
MockitoSpyBeanOverrideHandler(Field field, ResolvableType typeToSpy, MockitoSpyBean spyBean) {
super(field, typeToSpy, (StringUtils.hasText(spyBean.name()) ? spyBean.name() : null),
BeanOverrideStrategy.WRAP, spyBean.reset());
Assert.notNull(typeToSpy, "typeToSpy must not be null");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -78,8 +78,21 @@ public class HttpHeadersAssert extends AbstractMapAssert<HttpHeadersAssert, Http
* Verify that the actual HTTP headers do not contain any of the headers
* with the given {@code names}.
* @param names the names of HTTP headers that should not be present
* @since 6.2.2
* @see #doesNotContainKeys
*/
public HttpHeadersAssert doesNotContainHeaders(String... names) {
return doesNotContainKeys(names);
}
/**
* Verify that the actual HTTP headers do not contain any of the headers
* with the given {@code names}.
* @param names the names of HTTP headers that should not be present
* @see #doesNotContainKeys
* @deprecated in favor of {@link #doesNotContainHeaders(String...)}
*/
@Deprecated(since = "6.2.2", forRemoval = true)
public HttpHeadersAssert doesNotContainsHeaders(String... names) {
return doesNotContainKeys(names);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -395,7 +395,7 @@ class BeanOverrideBeanFactoryPostProcessorTests {
private static AnnotationConfigApplicationContext createContext(Class<?> testClass) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>(BeanOverrideHandler.forTestClass(testClass));
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>(BeanOverrideTestUtils.findHandlers(testClass));
new BeanOverrideContextCustomizer(handlers).customizeContext(context, mock(MergedContextConfiguration.class));
return context;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -51,14 +51,14 @@ class BeanOverrideHandlerTests {
@Test
void forTestClassWithSingleField() {
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(SingleAnnotation.class);
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(SingleAnnotation.class);
assertThat(handlers).singleElement().satisfies(hasBeanOverrideHandler(
field(SingleAnnotation.class, "message"), String.class, null));
}
@Test
void forTestClassWithMultipleFields() {
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(MultipleAnnotations.class);
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(MultipleAnnotations.class);
assertThat(handlers).hasSize(2)
.anySatisfy(hasBeanOverrideHandler(
field(MultipleAnnotations.class, "message"), String.class, null))
@@ -68,7 +68,7 @@ class BeanOverrideHandlerTests {
@Test
void forTestClassWithMultipleFieldsWithIdenticalMetadata() {
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(MultipleAnnotationsDuplicate.class);
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(MultipleAnnotationsDuplicate.class);
assertThat(handlers).hasSize(2)
.anySatisfy(hasBeanOverrideHandler(
field(MultipleAnnotationsDuplicate.class, "message1"), String.class, "messageBean"))
@@ -81,7 +81,7 @@ class BeanOverrideHandlerTests {
void forTestClassWithCompetingBeanOverrideAnnotationsOnSameField() {
Field faultyField = field(MultipleAnnotationsOnSameField.class, "message");
assertThatIllegalStateException()
.isThrownBy(() -> BeanOverrideHandler.forTestClass(MultipleAnnotationsOnSameField.class))
.isThrownBy(() -> BeanOverrideTestUtils.findHandlers(MultipleAnnotationsOnSameField.class))
.withMessageStartingWith("Multiple @BeanOverride annotations found")
.withMessageContaining(faultyField.toString());
}
@@ -90,7 +90,7 @@ class BeanOverrideHandlerTests {
void forTestClassWithStaticBeanOverrideField() {
Field staticField = field(StaticBeanOverrideField.class, "message");
assertThatIllegalStateException()
.isThrownBy(() -> BeanOverrideHandler.forTestClass(StaticBeanOverrideField.class))
.isThrownBy(() -> BeanOverrideTestUtils.findHandlers(StaticBeanOverrideField.class))
.withMessage("@BeanOverride field must not be static: " + staticField);
}
@@ -0,0 +1,37 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override;
import java.util.List;
/**
* Test utilities for Bean Overrides.
*
* @author Sam Brannen
* @since 6.2.2
*/
public abstract class BeanOverrideTestUtils {
public static List<BeanOverrideHandler> findHandlers(Class<?> testClass) {
return BeanOverrideHandler.forTestClass(testClass);
}
public static List<BeanOverrideHandler> findAllHandlers(Class<?> testClass) {
return BeanOverrideHandler.findAllHandlers(testClass);
}
}
@@ -1,173 +0,0 @@
/*
* 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.test.context.bean.override.convention;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.bean.override.convention.TestBeanForInheritanceIntegrationTests.AbstractTestBeanIntegrationTestCase.FakePojo;
import org.springframework.test.context.bean.override.convention.TestBeanForInheritanceIntegrationTests.AbstractTestBeanIntegrationTestCase.Pojo;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link TestBean} that use inheritance.
*
* <p>Tests inheritance within a class hierarchy as well as "inheritance" within
* an enclosing class hierarchy.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
*/
public class TestBeanForInheritanceIntegrationTests {
static Pojo enclosingClassBeanOverride() {
return new FakePojo("in enclosing test class");
}
@SpringJUnitConfig
public abstract static class AbstractTestBeanIntegrationTestCase {
@TestBean(name = "someBean")
Pojo someBean;
@TestBean(name = "otherBean")
Pojo otherBean;
@TestBean(name = "thirdBean")
Pojo anotherBean;
static Pojo otherBean() {
return new FakePojo("otherBean in superclass");
}
static Pojo thirdBean() {
return new FakePojo("third in superclass");
}
static Pojo commonBeanOverride() {
return new FakePojo("in superclass");
}
public interface Pojo {
default String getValue() {
return "Prod";
}
}
static class ProdPojo implements Pojo { }
static class FakePojo implements Pojo {
final String value;
protected FakePojo(String value) {
this.value = value;
}
@Override
public String getValue() {
return this.value;
}
@Override
public String toString() {
return getValue();
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
Pojo someBean() {
return new ProdPojo();
}
@Bean
Pojo otherBean() {
return new ProdPojo();
}
@Bean
Pojo thirdBean() {
return new ProdPojo();
}
@Bean
Pojo pojo() {
return new ProdPojo();
}
@Bean
Pojo pojo2() {
return new ProdPojo();
}
}
}
@Nested
@DisplayName("Nested, concrete inherited tests with correct @TestBean setup")
class NestedConcreteTestBeanIntegrationTests extends AbstractTestBeanIntegrationTestCase {
@Autowired
ApplicationContext ctx;
@TestBean(name = "pojo", methodName = "commonBeanOverride")
Pojo pojo;
@TestBean(name = "pojo2", methodName = "enclosingClassBeanOverride")
Pojo pojo2;
static Pojo someBean() {
return new FakePojo("someBeanOverride");
}
// Hides otherBean() defined in AbstractTestBeanIntegrationTestCase.
static Pojo otherBean() {
return new FakePojo("otherBean in subclass");
}
@Test
void fieldInSubtypeWithFactoryMethodInSupertype() {
assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("in superclass");
assertThat(this.pojo.getValue()).as("injection point").isEqualTo("in superclass");
}
@Test
void fieldInSupertypeWithFactoryMethodInSubtype() {
assertThat(ctx.getBean("someBean")).as("applicationContext").hasToString("someBeanOverride");
assertThat(this.someBean.getValue()).as("injection point").isEqualTo("someBeanOverride");
}
@Test
void fieldInSupertypeWithPrioritizedFactoryMethodInSubtype() {
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("otherBean in subclass");
assertThat(super.otherBean.getValue()).as("injection point").isEqualTo("otherBean in subclass");
}
@Test
void fieldInNestedClassWithFactoryMethodInEnclosingClass() {
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
assertThat(this.pojo2.getValue()).as("injection point").isEqualTo("in enclosing test class");
}
}
}
@@ -0,0 +1,213 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.convention;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link TestBean} that use inheritance.
*
* <p>Tests inheritance within a class hierarchy as well as "inheritance" within
* an enclosing class hierarchy.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
*/
@SpringJUnitConfig
public class TestBeanInheritanceIntegrationTests {
@TestBean
Pojo puzzleBean;
static Pojo puzzleBean() {
return new FakePojo("puzzle in enclosing class");
}
static Pojo enclosingClassFactoryMethod() {
return new FakePojo("in enclosing test class");
}
abstract static class AbstractTestCase {
@TestBean("otherBean")
Pojo otherBean;
@TestBean
Pojo anotherBean;
@TestBean
Pojo enigmaBean;
static Pojo otherBean() {
return new FakePojo("other in superclass");
}
static Pojo anotherBean() {
return new FakePojo("another in superclass");
}
static Pojo enigmaBean() {
return new FakePojo("enigma in superclass");
}
static Pojo commonBean() {
return new FakePojo("common in superclass");
}
}
@Nested
@DisplayName("Nested, concrete inherited tests with correct @TestBean setup")
class NestedTests extends AbstractTestCase {
@Autowired
ApplicationContext ctx;
@TestBean(methodName = "commonBean")
Pojo pojo;
@TestBean(name = "pojo2", methodName = "enclosingClassFactoryMethod")
Pojo pojo2;
@TestBean
Pojo enigmaBean;
@TestBean
Pojo puzzleBean;
// "Overrides" puzzleBean() defined in TestBeanInheritanceIntegrationTests.
static Pojo puzzleBean() {
return new FakePojo("puzzle in nested class");
}
// "Overrides" enigmaBean() defined in AbstractTestCase.
static Pojo enigmaBean() {
return new FakePojo("enigma in subclass");
}
static Pojo otherBean() {
return new FakePojo("other in subclass");
}
@Test
void fieldInSuperclassWithFactoryMethodInSuperclass() {
assertThat(ctx.getBean("anotherBean")).as("applicationContext").hasToString("another in superclass");
assertThat(super.anotherBean.value()).as("injection point").isEqualTo("another in superclass");
}
@Test // gh-34204
void fieldInSuperclassWithFactoryMethodInSuperclassAndInSubclass() {
// We do not expect "other in subclass", because the @TestBean declaration in
// AbstractTestCase cannot "see" the otherBean() factory method in the subclass.
assertThat(ctx.getBean("otherBean")).as("applicationContext").hasToString("other in superclass");
assertThat(super.otherBean.value()).as("injection point").isEqualTo("other in superclass");
}
@Test
void fieldInSubclassWithFactoryMethodInSuperclass() {
assertThat(ctx.getBean("pojo")).as("applicationContext").hasToString("common in superclass");
assertThat(this.pojo.value()).as("injection point").isEqualTo("common in superclass");
}
@Test
void fieldInNestedClassWithFactoryMethodInEnclosingClass() {
assertThat(ctx.getBean("pojo2")).as("applicationContext").hasToString("in enclosing test class");
assertThat(this.pojo2.value()).as("injection point").isEqualTo("in enclosing test class");
}
@Test // gh-34194, gh-34204
void testBeanInSubclassOverridesTestBeanInSuperclass() {
assertThat(ctx.getBean("enigmaBean")).as("applicationContext").hasToString("enigma in subclass");
assertThat(this.enigmaBean.value()).as("injection point").isEqualTo("enigma in subclass");
}
@Test // gh-34194, gh-34204
void testBeanInNestedClassOverridesTestBeanInEnclosingClass() {
assertThat(ctx.getBean("puzzleBean")).as("applicationContext").hasToString("puzzle in nested class");
assertThat(this.puzzleBean.value()).as("injection point").isEqualTo("puzzle in nested class");
}
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
Pojo otherBean() {
return new ProdPojo();
}
@Bean
Pojo anotherBean() {
return new ProdPojo();
}
@Bean
Pojo enigmaBean() {
return new ProdPojo();
}
@Bean
Pojo puzzleBean() {
return new ProdPojo();
}
@Bean
Pojo pojo() {
return new ProdPojo();
}
@Bean
Pojo pojo2() {
return new ProdPojo();
}
}
interface Pojo {
default String value() {
return "prod";
}
}
static class ProdPojo implements Pojo {
@Override
public String toString() {
return value();
}
}
record FakePojo(String value) implements Pojo {
@Override
public String toString() {
return value();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
@@ -42,21 +43,21 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
class TestBeanOverrideHandlerTests {
@Test
void setsBeanNameToNullIfAnnotationNameIsNull() {
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(SampleOneOverride.class);
void beanNameIsSetToNullIfAnnotationNameIsEmpty() {
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(SampleOneOverride.class);
assertThat(handlers).singleElement().extracting(BeanOverrideHandler::getBeanName).isNull();
}
@Test
void setsBeanNameToAnnotationName() {
List<BeanOverrideHandler> handlers = BeanOverrideHandler.forTestClass(SampleOneOverrideWithName.class);
void beanNameIsSetToAnnotationName() {
List<BeanOverrideHandler> handlers = BeanOverrideTestUtils.findHandlers(SampleOneOverrideWithName.class);
assertThat(handlers).singleElement().extracting(BeanOverrideHandler::getBeanName).isEqualTo("anotherBean");
}
@Test
void failsWithMissingMethod() {
assertThatIllegalStateException()
.isThrownBy(() -> BeanOverrideHandler.forTestClass(SampleMissingMethod.class))
.isThrownBy(() -> BeanOverrideTestUtils.findHandlers(SampleMissingMethod.class))
.withMessage("No static method found named message() in %s with return type %s",
SampleMissingMethod.class.getName(), String.class.getName());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,9 +26,10 @@ import org.springframework.test.context.bean.override.BeanOverrideContextCustomi
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link TestBean}.
* Tests for {@link TestBean @TestBean}.
*
* @author Stephane Nicoll
* @author Sam Brannen
*/
public class TestBeanTests {
@@ -64,8 +65,8 @@ public class TestBeanTests {
.isThrownBy(context::refresh)
.withMessage("""
Unable to override bean: there are no beans of \
type %s (as required by field '%s.example').""".formatted(
String.class.getName(), FailureByTypeLookup.class.getSimpleName()));
type %s (as required by field '%s.example').""",
String.class.getName(), FailureByTypeLookup.class.getSimpleName());
}
@Test
@@ -87,8 +88,8 @@ public class TestBeanTests {
assertThatIllegalStateException()
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
FailureMissingDefaultOverrideMethod.class, context))
.withMessage("No static method found named example() or beanToOverride() in %s with return type %s"
.formatted(FailureMissingDefaultOverrideMethod.class.getName(), String.class.getName()));
.withMessage("No static method found named example() or beanToOverride() in %s with return type %s",
FailureMissingDefaultOverrideMethod.class.getName(), String.class.getName());
}
@Test
@@ -97,8 +98,8 @@ public class TestBeanTests {
assertThatIllegalStateException()
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
FailureMissingExplicitOverrideMethod.class, context))
.withMessage("No static method found named createExample() in %s with return type %s"
.formatted(FailureMissingExplicitOverrideMethod.class.getName(), String.class.getName()));
.withMessage("No static method found named createExample() in %s with return type %s",
FailureMissingExplicitOverrideMethod.class.getName(), String.class.getName());
}
@Test
@@ -108,19 +109,19 @@ public class TestBeanTests {
assertThatIllegalStateException()
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
FailureOverrideInParentWithoutFactoryMethod.class, context))
.withMessage("No static method found named beanToOverride() in %s with return type %s"
.formatted(FailureOverrideInParentWithoutFactoryMethod.class.getName(), String.class.getName()));
.withMessage("No static method found named beanToOverride() in %s with return type %s",
AbstractByNameLookup.class.getName(), String.class.getName());
}
@Test
void contextCustomizerCannotBeCreatedWitCompetingOverrideMethods() {
void contextCustomizerCannotBeCreatedWithCompetingOverrideMethods() {
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean("bean", String.class, () -> "example");
assertThatIllegalStateException()
.isThrownBy(() -> BeanOverrideContextCustomizerTestUtils.customizeApplicationContext(
FailureCompetingOverrideMethods.class, context))
.withMessage("Found 2 competing static methods named example() or beanToOverride() in %s with return type %s"
.formatted(FailureCompetingOverrideMethods.class.getName(), String.class.getName()));
.withMessage("Found 2 competing static methods named example() or beanToOverride() in %s with return type %s",
FailureCompetingOverrideMethods.class.getName(), String.class.getName());
}
@@ -149,9 +150,7 @@ public class TestBeanTests {
@TestBean(name = "beanToOverride")
private String example;
// Expected static String example() { ... }
// or static String beanToOverride() { ... }
// No example() or beanToOverride() method
}
static class FailureMissingExplicitOverrideMethod {
@@ -159,28 +158,22 @@ public class TestBeanTests {
@TestBean(methodName = "createExample")
private String example;
// Expected static String createExample() { ... }
// NO createExample() method
}
abstract static class AbstractByNameLookup {
@TestBean(methodName = "beanToOverride")
protected String beanToOverride;
@TestBean
String beanToOverride;
// No beanToOverride() method
}
static class FailureOverrideInParentWithoutFactoryMethod extends AbstractByNameLookup {
// No beanToOverride() method
}
abstract static class AbstractCompetingMethods {
@TestBean(name = "beanToOverride")
protected String example;
static String example() {
throw new IllegalStateException("Should not be called");
}
@@ -188,10 +181,12 @@ public class TestBeanTests {
static class FailureCompetingOverrideMethods extends AbstractCompetingMethods {
@TestBean(name = "beanToOverride")
String example;
static String beanToOverride() {
throw new IllegalStateException("Should not be called");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,6 +26,7 @@ import org.mockito.Answers;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -34,104 +35,179 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link MockitoBeanOverrideHandler}.
*
* @author Stephane Nicoll
* @author Sam Brannen
* @since 6.2
*/
class MockitoBeanOverrideHandlerTests {
@Test
void forTestClassSetsNameToNullIfAnnotationNameIsNull() {
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneMock.class);
void beanNameIsSetToNullIfAnnotationNameIsEmpty() {
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneMock.class);
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isNull());
}
@Test
void forTestClassSetsNameToAnnotationName() {
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneMockWithName.class);
void beanNameIsSetToAnnotationName() {
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneMockWithName.class);
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isEqualTo("anotherService"));
}
@Test
void isEqualToWithSameInstance() {
MockitoBeanOverrideHandler handler = createBeanOverrideHandler(sampleField("service"));
void isEqualToWithSameInstanceFromField() {
MockitoBeanOverrideHandler handler = createHandler(sampleField("service"));
assertThat(handler).isEqualTo(handler);
assertThat(handler).hasSameHashCodeAs(handler);
}
@Test
void isEqualToWithSameMetadata() {
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service"));
void isEqualToWithSameMetadataFromField() {
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service"));
assertThat(handler1).isEqualTo(handler2);
assertThat(handler1).hasSameHashCodeAs(handler2);
}
@Test // gh-33925
void isEqualToWithSameInstanceFromClassLevel() {
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
assertThat(handler1).isEqualTo(handler1);
assertThat(handler1).hasSameHashCodeAs(handler1);
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class);
assertThat(handler2).isEqualTo(handler2);
assertThat(handler2).hasSameHashCodeAs(handler2);
}
@Test // gh-33925
void isEqualToWithSameByNameLookupMetadataFromClassLevel() {
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName2.class);
assertThat(handler1).isEqualTo(handler2);
assertThat(handler2).isEqualTo(handler1);
assertThat(handler1).hasSameHashCodeAs(handler2);
}
@Test // gh-33925
void isNotEqualToWithDifferentByNameLookupMetadataFromClassLevel() {
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByName1.class);
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName3.class);
assertThat(handler1).isNotEqualTo(handler2);
assertThat(handler2).isNotEqualTo(handler1);
assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
}
@Test // gh-33925
void isEqualToWithSameByTypeLookupMetadataFromClassLevel() {
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class);
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType2.class);
assertThat(handler1).isEqualTo(handler2);
assertThat(handler2).isEqualTo(handler1);
assertThat(handler1).hasSameHashCodeAs(handler2);
}
@Test // gh-33925
void isNotEqualToWithDifferentByTypeLookupMetadataFromClassLevel() {
MockitoBeanOverrideHandler handler1 = createHandler(ClassLevelStringMockByType1.class);
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType3.class);
assertThat(handler1).isNotEqualTo(handler2);
assertThat(handler2).isNotEqualTo(handler1);
assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
}
@Test // gh-33925
void isEqualToWithSameByNameLookupMetadataFromFieldAndClassLevel() {
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service3"));
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByName1.class);
assertThat(handler1).isEqualTo(handler2);
assertThat(handler2).isEqualTo(handler1);
assertThat(handler1).hasSameHashCodeAs(handler2);
}
/**
* Since the "field name as fallback qualifier" is not available for an annotated class,
* what would seem to be "equivalent" handlers are actually not considered "equal" when
* the the lookup is "by type".
*/
@Test // gh-33925
void isNotEqualToWithSameByTypeLookupMetadataFromFieldAndClassLevel() {
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createHandler(ClassLevelStringMockByType1.class);
assertThat(handler1).isNotEqualTo(handler2);
assertThat(handler2).isNotEqualTo(handler1);
assertThat(handler1).doesNotHaveSameHashCodeAs(handler2);
}
@Test
void isNotEqualEqualToByTypeLookupWithSameMetadataButDifferentField() {
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service2"));
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service2"));
assertThat(handler1).isNotEqualTo(handler2);
}
@Test
void isEqualEqualToByNameLookupWithSameMetadataButDifferentField() {
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service3"));
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service4"));
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service3"));
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service4"));
assertThat(handler1).isEqualTo(handler2);
assertThat(handler1).hasSameHashCodeAs(handler2);
}
@Test
void isNotEqualToWithSameMetadataButDifferentBeanName() {
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service3"));
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service3"));
assertThat(handler1).isNotEqualTo(handler2);
}
@Test
void isNotEqualToWithSameMetadataButDifferentExtraInterfaces() {
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service5"));
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service5"));
assertThat(handler1).isNotEqualTo(handler2);
}
@Test
void isNotEqualToWithSameMetadataButDifferentAnswers() {
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service6"));
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service6"));
assertThat(handler1).isNotEqualTo(handler2);
}
@Test
void isNotEqualToWithSameMetadataButDifferentSerializableFlag() {
MockitoBeanOverrideHandler handler1 = createBeanOverrideHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createBeanOverrideHandler(sampleField("service7"));
MockitoBeanOverrideHandler handler1 = createHandler(sampleField("service"));
MockitoBeanOverrideHandler handler2 = createHandler(sampleField("service7"));
assertThat(handler1).isNotEqualTo(handler2);
}
private Field sampleField(String fieldName) {
private static Field sampleField(String fieldName) {
Field field = ReflectionUtils.findField(Sample.class, fieldName);
assertThat(field).isNotNull();
return field;
}
private MockitoBeanOverrideHandler createBeanOverrideHandler(Field field) {
private static MockitoBeanOverrideHandler createHandler(Field field) {
MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(field, MockitoBean.class);
return new MockitoBeanOverrideHandler(field, ResolvableType.forClass(field.getType()), annotation);
}
private MockitoBeanOverrideHandler createHandler(Class<?> clazz) {
MockitoBean annotation = AnnotatedElementUtils.getMergedAnnotation(clazz, MockitoBean.class);
return new MockitoBeanOverrideHandler(null, ResolvableType.forClass(annotation.types()[0]), annotation);
}
static class SampleOneMock {
@MockitoBean
String service;
}
static class SampleOneMockWithName {
@MockitoBean("anotherService")
String service;
}
static class Sample {
@@ -156,7 +232,30 @@ class MockitoBeanOverrideHandlerTests {
@MockitoBean(serializable = true)
private String service7;
}
@MockitoBean(name = "beanToMock", types = String.class)
static class ClassLevelStringMockByName1 {
}
@MockitoBean(name = "beanToMock", types = String.class)
static class ClassLevelStringMockByName2 {
}
@MockitoBean(name = "otherBeanToMock", types = String.class)
static class ClassLevelStringMockByName3 {
}
@MockitoBean(types = String.class)
static class ClassLevelStringMockByType1 {
}
@MockitoBean(types = String.class)
static class ClassLevelStringMockByType2 {
}
@MockitoBean(types = Integer.class)
static class ClassLevelStringMockByType3 {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,62 +18,180 @@ package org.springframework.test.context.bean.override.mockito;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.List;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.lang.Nullable;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link MockitoBeanOverrideProcessorTests}
* Tests for {@link MockitoBeanOverrideProcessor}.
*
* @author Simon Baslé
* @author Sam Brannen
* @since 6.2
*/
public class MockitoBeanOverrideProcessorTests {
class MockitoBeanOverrideProcessorTests {
private final MockitoBeanOverrideProcessor processor = new MockitoBeanOverrideProcessor();
@Test
void mockAnnotationCreatesMockitoBeanOverrideHandler() throws NoSuchFieldException {
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
Class<?> clazz = MockitoConf.class;
Field field = clazz.getField("a");
BeanOverrideHandler object = this.processor.createHandler(annotation, clazz, field);
assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
@Nested
class CreateHandlerTests {
private final Field field = ReflectionUtils.findField(TestCase.class, "number");
@Test
void mockAnnotationCreatesMockitoBeanOverrideHandler() {
MockitoBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoBean.class);
BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
assertThat(object).isExactlyInstanceOf(MockitoBeanOverrideHandler.class);
}
@Test
void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() {
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
BeanOverrideHandler object = processor.createHandler(annotation, TestCase.class, field);
assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
}
@Test
void otherAnnotationThrows() {
Annotation annotation = field.getAnnotation(Nullable.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field))
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
"@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
field.getName());
}
@Test
void typesNotSupportedAtFieldLevel() {
Field field = ReflectionUtils.findField(TestCase.class, "typesNotSupported");
MockitoBean annotation = field.getAnnotation(MockitoBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandler(annotation, TestCase.class, field))
.withMessage("The @MockitoBean 'types' attribute must be omitted when declared on a field");
}
static class TestCase {
@Nullable
@MockitoBean
@MockitoSpyBean
Integer number;
@MockitoBean(types = Integer.class)
String typesNotSupported;
}
@MockitoBean(name = "bogus", types = Integer.class)
static class NameNotSupportedTestCase {
}
}
@Test
void spyAnnotationCreatesMockitoSpyBeanOverrideHandler() throws NoSuchFieldException {
MockitoSpyBean annotation = AnnotationUtils.synthesizeAnnotation(MockitoSpyBean.class);
Class<?> clazz = MockitoConf.class;
Field field = clazz.getField("a");
BeanOverrideHandler object = this.processor.createHandler(annotation, clazz, field);
@Nested
class CreateHandlersTests {
assertThat(object).isExactlyInstanceOf(MockitoSpyBeanOverrideHandler.class);
}
@Test
void missingTypes() {
Class<?> testClass = MissingTypesTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
@Test
void otherAnnotationThrows() throws NoSuchFieldException {
Class<?> clazz = MockitoConf.class;
Field field = clazz.getField("a");
Annotation annotation = field.getAnnotation(Nullable.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoBean 'types' attribute must not be empty when declared on a class");
}
@Test
void nameNotSupportedWithMultipleTypes() {
Class<?> testClass = NameNotSupportedWithMultipleTypesTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
assertThatIllegalStateException()
.isThrownBy(() -> processor.createHandlers(annotation, testClass))
.withMessage("The @MockitoBean 'name' attribute cannot be used when mocking multiple types");
}
@Test
void singleMockByType() {
Class<?> testClass = SingleMockByTypeTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isNull();
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
}
@Test
void singleMockByName() {
Class<?> testClass = SingleMockByNameTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).singleElement().isInstanceOfSatisfying(MockitoBeanOverrideHandler.class, handler -> {
assertThat(handler.getField()).isNull();
assertThat(handler.getBeanName()).isEqualTo("enigma");
assertThat(handler.getBeanType().resolve()).isEqualTo(Integer.class);
});
}
@Test
void multipleMocks() {
Class<?> testClass = MultipleMocksTestCase.class;
MockitoBean annotation = testClass.getAnnotation(MockitoBean.class);
List<BeanOverrideHandler> handlers = processor.createHandlers(annotation, testClass);
assertThat(handlers).satisfiesExactly(
handler1 -> {
assertThat(handler1.getField()).isNull();
assertThat(handler1.getBeanName()).isNull();
assertThat(handler1.getBeanType().resolve()).isEqualTo(Integer.class);
},
handler2 -> {
assertThat(handler2.getField()).isNull();
assertThat(handler2.getBeanName()).isNull();
assertThat(handler2.getBeanType().resolve()).isEqualTo(Float.class);
}
);
}
assertThatIllegalStateException()
.isThrownBy(() -> this.processor.createHandler(annotation, clazz, field))
.withMessage("Invalid annotation passed to MockitoBeanOverrideProcessor: expected either " +
"@MockitoBean or @MockitoSpyBean on field %s.%s", field.getDeclaringClass().getName(),
field.getName());
}
static class MockitoConf {
@Nullable
@MockitoBean
@MockitoSpyBean
public ExampleService a;
static class MissingTypesTestCase {
}
@MockitoBean(name = "bogus", types = { Integer.class, Float.class })
static class NameNotSupportedWithMultipleTypesTestCase {
}
@MockitoBean(types = Integer.class)
static class SingleMockByTypeTestCase {
}
@MockitoBean(name = "enigma", types = Integer.class)
static class SingleMockByNameTestCase {
}
@MockitoBean(types = { Integer.class, Float.class })
static class MultipleMocksTestCase {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
@@ -36,14 +37,14 @@ import static org.assertj.core.api.Assertions.assertThat;
class MockitoSpyBeanOverrideHandlerTests {
@Test
void forTestClassSetsNameToNullIfAnnotationNameIsNull() {
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneSpy.class);
void beanNameIsSetToNullIfAnnotationNameIsEmpty() {
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneSpy.class);
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isNull());
}
@Test
void forTestClassSetsNameToAnnotationName() {
List<BeanOverrideHandler> list = BeanOverrideHandler.forTestClass(SampleOneSpyWithName.class);
void beanNameIsSetToAnnotationName() {
List<BeanOverrideHandler> list = BeanOverrideTestUtils.findHandlers(SampleOneSpyWithName.class);
assertThat(list).singleElement().satisfies(handler -> assertThat(handler.getBeanName()).isEqualTo("anotherService"));
}
@@ -0,0 +1,102 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito.mockbeans;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoBeans;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Integration tests for {@link MockitoBeans @MockitoBeans} and
* {@link MockitoBean @MockitoBean} declared "by name" at the class level as a
* repeatable annotation.
*
* @author Sam Brannen
* @since 6.2.2
* @see <a href="https://github.com/spring-projects/spring-framework/issues/33925">gh-33925</a>
* @see MockitoBeansByTypeIntegrationTests
*/
@SpringJUnitConfig
@MockitoBean(name = "s1", types = ExampleService.class)
@MockitoBean(name = "s2", types = ExampleService.class)
class MockitoBeansByNameIntegrationTests {
@Autowired
ExampleService s1;
@Autowired
ExampleService s2;
@MockitoBean(name = "s3")
ExampleService service3;
@Autowired
@Qualifier("s4")
ExampleService service4;
@BeforeEach
void configureMocks() {
given(s1.greeting()).willReturn("mock 1");
given(s2.greeting()).willReturn("mock 2");
given(service3.greeting()).willReturn("mock 3");
}
@Test
void checkMocksAndStandardBean() {
assertThat(s1.greeting()).isEqualTo("mock 1");
assertThat(s2.greeting()).isEqualTo("mock 2");
assertThat(service3.greeting()).isEqualTo("mock 3");
assertThat(service4.greeting()).isEqualTo("prod 4");
}
@Configuration
static class Config {
@Bean
ExampleService s1() {
return () -> "prod 1";
}
@Bean
ExampleService s2() {
return () -> "prod 2";
}
@Bean
ExampleService s3() {
return () -> "prod 3";
}
@Bean
ExampleService s4() {
return () -> "prod 4";
}
}
}
@@ -0,0 +1,147 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito.mockbeans;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoBeans;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Integration tests for {@link MockitoBeans @MockitoBeans} and
* {@link MockitoBean @MockitoBean} declared "by type" at the class level, as a
* repeatable annotation, and via a custom composed annotation.
*
* @author Sam Brannen
* @since 6.2.2
* @see <a href="https://github.com/spring-projects/spring-framework/issues/33925">gh-33925</a>
* @see MockitoBeansByNameIntegrationTests
*/
@SpringJUnitConfig
@MockitoBean(types = {Service04.class, Service05.class})
@SharedMocks // Intentionally declared between local @MockitoBean declarations
@MockitoBean(types = Service06.class)
class MockitoBeansByTypeIntegrationTests implements TestInterface01 {
@Autowired
Service01 service01;
@Autowired
Service02 service02;
@Autowired
Service03 service03;
@Autowired
Service04 service04;
@Autowired
Service05 service05;
@Autowired
Service06 service06;
@MockitoBean
Service07 service07;
@BeforeEach
void configureMocks() {
given(service01.greeting()).willReturn("mock 01");
given(service02.greeting()).willReturn("mock 02");
given(service03.greeting()).willReturn("mock 03");
given(service04.greeting()).willReturn("mock 04");
given(service05.greeting()).willReturn("mock 05");
given(service06.greeting()).willReturn("mock 06");
given(service07.greeting()).willReturn("mock 07");
}
@Test
void checkMocks() {
assertThat(service01.greeting()).isEqualTo("mock 01");
assertThat(service02.greeting()).isEqualTo("mock 02");
assertThat(service03.greeting()).isEqualTo("mock 03");
assertThat(service04.greeting()).isEqualTo("mock 04");
assertThat(service05.greeting()).isEqualTo("mock 05");
assertThat(service06.greeting()).isEqualTo("mock 06");
assertThat(service07.greeting()).isEqualTo("mock 07");
}
@MockitoBean(types = Service09.class)
static class BaseTestCase implements TestInterface08 {
@Autowired
Service08 service08;
@Autowired
Service09 service09;
@MockitoBean
Service10 service10;
}
@Nested
@MockitoBean(types = Service12.class)
class NestedTests extends BaseTestCase implements TestInterface11 {
@Autowired
Service11 service11;
@Autowired
Service12 service12;
@MockitoBean
Service13 service13;
@BeforeEach
void configureMocks() {
given(service08.greeting()).willReturn("mock 08");
given(service09.greeting()).willReturn("mock 09");
given(service10.greeting()).willReturn("mock 10");
given(service11.greeting()).willReturn("mock 11");
given(service12.greeting()).willReturn("mock 12");
given(service13.greeting()).willReturn("mock 13");
}
@Test
void checkMocks() {
assertThat(service01.greeting()).isEqualTo("mock 01");
assertThat(service02.greeting()).isEqualTo("mock 02");
assertThat(service03.greeting()).isEqualTo("mock 03");
assertThat(service04.greeting()).isEqualTo("mock 04");
assertThat(service05.greeting()).isEqualTo("mock 05");
assertThat(service06.greeting()).isEqualTo("mock 06");
assertThat(service07.greeting()).isEqualTo("mock 07");
assertThat(service08.greeting()).isEqualTo("mock 08");
assertThat(service09.greeting()).isEqualTo("mock 09");
assertThat(service10.greeting()).isEqualTo("mock 10");
assertThat(service11.greeting()).isEqualTo("mock 11");
assertThat(service12.greeting()).isEqualTo("mock 12");
assertThat(service13.greeting()).isEqualTo("mock 13");
}
}
}
@@ -0,0 +1,68 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito.mockbeans;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.springframework.core.ResolvableType;
import org.springframework.test.context.bean.override.BeanOverrideHandler;
import org.springframework.test.context.bean.override.BeanOverrideTestUtils;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.bean.override.mockito.MockitoBeans;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MockitoBeans @MockitoBeans}: {@link MockitoBean @MockitoBean}
* declared at the class level, as a repeatable annotation, and via a custom composed
* annotation.
*
* @author Sam Brannen
* @since 6.2.2
* @see <a href="https://github.com/spring-projects/spring-framework/issues/33925">gh-33925</a>
*/
class MockitoBeansTests {
@Test
void registrationOrderForTopLevelClass() {
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.class);
assertThat(mockedServices).containsExactly(
Service01.class, Service02.class, Service03.class, Service04.class,
Service05.class, Service06.class, Service07.class);
}
@Test
void registrationOrderForNestedClass() {
Stream<Class<?>> mockedServices = getRegisteredMockTypes(MockitoBeansByTypeIntegrationTests.NestedTests.class);
assertThat(mockedServices).containsExactly(
Service01.class, Service02.class, Service03.class, Service04.class,
Service05.class, Service06.class, Service07.class, Service08.class,
Service09.class, Service10.class, Service11.class, Service12.class,
Service13.class);
}
private static Stream<Class<?>> getRegisteredMockTypes(Class<?> testClass) {
return BeanOverrideTestUtils.findAllHandlers(testClass)
.stream()
.map(BeanOverrideHandler::getBeanType)
.map(ResolvableType::getRawClass);
}
}
@@ -0,0 +1,21 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito.mockbeans;
interface Service {
String greeting();
}
@@ -0,0 +1,20 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito.mockbeans;
interface Service01 extends Service {
}
@@ -0,0 +1,20 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.test.context.bean.override.mockito.mockbeans;
interface Service02 extends Service {
}

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