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