mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
225 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2b08e6e1d3 | |||
| 4e39c28b92 | |||
| 381fa10477 | |||
| 14f4deef3a | |||
| e23971efb9 | |||
| 2469aae672 | |||
| 83e29382b3 | |||
| ee5c82a2f8 | |||
| ae7891e797 | |||
| 98ed1dfcf8 | |||
| 9b8a851969 | |||
| 4c6194a2ad | |||
| 2fc99eb12f | |||
| 783388e9f8 | |||
| de4de02a1d | |||
| e6ce2a3c36 | |||
| 481a5743b3 | |||
| 85a8868bae | |||
| dd72b9eb4b | |||
| 2e65c1dfe6 | |||
| ca72cd66e3 | |||
| a3f3ba5685 | |||
| 79f4f76bd2 | |||
| 744d136cf7 | |||
| 00ca23859e | |||
| facc7c5371 | |||
| bd1e7e16a9 | |||
| 25e8395df8 | |||
| 148b7fd8f3 | |||
| 68338aa818 | |||
| fbbc0f487c | |||
| 611c390417 | |||
| cf3196e8de | |||
| 80e9e4f6f2 | |||
| 91655be0db | |||
| 3b030e0431 | |||
| 9870ce1844 | |||
| d87c03a6be | |||
| 1d91982f83 | |||
| 0c25d817bd | |||
| 17ed3e8370 | |||
| 2f458f9093 | |||
| 16e0ed0463 | |||
| e24448118c | |||
| a0ec6656b4 | |||
| 5ab4c5c5d9 | |||
| c3baa01535 | |||
| c0b749479a | |||
| 5bf58cfe05 | |||
| 238a24cb6d | |||
| 9a54f75d6d | |||
| 9db16e4c15 | |||
| ec8f0ca6b2 | |||
| 1a0bd12c34 | |||
| e6590aa1a8 | |||
| 051b09694e | |||
| 39ff8e46ab | |||
| 27bdf24482 | |||
| d9ecf945cc | |||
| b35da5b140 | |||
| 367a62018d | |||
| 1b26f5d1e6 | |||
| 6ff2d187cf | |||
| 72cf389754 | |||
| 3184eb3acc | |||
| 9e4c127eda | |||
| 1f642b973b | |||
| 80c9efd638 | |||
| 0e1a2b4f87 | |||
| 5e6e223686 | |||
| 7a5844f1ce | |||
| 0d14aa5856 | |||
| c6096ff6e5 | |||
| 6ff758391c | |||
| ca9b26eb8f | |||
| 279409acce | |||
| 4f1b4b36bf | |||
| 680f4ee482 | |||
| 59cd577fd1 | |||
| 9f92183710 | |||
| 680854d1f3 | |||
| 1a7161c85e | |||
| 8bce51267a | |||
| d169eb547e | |||
| 9c8535f5e4 | |||
| 829add3aa9 | |||
| 20e57608ba | |||
| 6275f46a66 | |||
| 9a17b5c453 | |||
| 1787d3e885 | |||
| 1f5af8f364 | |||
| a4b33b98df | |||
| 59f9cf8645 | |||
| fb34264169 | |||
| c2cf5e065d | |||
| e0e78257d6 | |||
| 227ddf817d | |||
| 07aa952bed | |||
| 598f0b64f0 | |||
| d4e3d6be58 | |||
| 8fe0eec5bf | |||
| f78264d158 | |||
| 4b0101a9dc | |||
| e940a38014 | |||
| 623bdfb677 | |||
| 8566e7bf55 | |||
| c17f25f939 | |||
| 9da22ecb46 | |||
| f602967dbc | |||
| f7d3556b8c | |||
| 813e113ea9 | |||
| 306a1f6c99 | |||
| 62c6d67615 | |||
| c4c0aca69b | |||
| 6f08c0b473 | |||
| d37d7abb17 | |||
| 2a1678f246 | |||
| 3806315e31 | |||
| 6062363738 | |||
| 31d9fe5f41 | |||
| 2ee4c3a363 | |||
| 82b179f238 | |||
| f993e9710d | |||
| 822001c6a4 | |||
| f2d4d59f5a | |||
| 4709f68446 | |||
| afba74c516 | |||
| fd50c0841c | |||
| cbe8b148b1 | |||
| 2086508924 | |||
| 759b173b1a | |||
| 7590c4c92e | |||
| 596c0df826 | |||
| d5b6f4a7ee | |||
| 5dc4a3a7a0 | |||
| 21f8b6d2f3 | |||
| 6ebaeba1c2 | |||
| 3bf31e45dd | |||
| 5e16e25109 | |||
| 6b5f0e92b1 | |||
| 2c973d3034 | |||
| 94e2f49e9f | |||
| 8b62ea13a1 | |||
| 76ad7c9cc0 | |||
| 8586095370 | |||
| 32970fe10d | |||
| 83c2afb643 | |||
| 7473cd5fbc | |||
| 8de3683ccb | |||
| 208f62ba07 | |||
| a0d51c8f7d | |||
| f4cefb6ec5 | |||
| e7fdbb8339 | |||
| a64b001f6a | |||
| c68470d0fd | |||
| 182e6b744a | |||
| d46e96f1ca | |||
| 51a5489438 | |||
| f9523a785b | |||
| 955f9d3ea9 | |||
| 7502b92392 | |||
| 99e7543a7f | |||
| 82a9d66079 | |||
| 5708b73ea9 | |||
| 5e975f51dd | |||
| ccf0cae18c | |||
| de0dfe5d93 | |||
| aac521c116 | |||
| ab8de8ec4b | |||
| abc3cfc7be | |||
| 44302aca93 | |||
| ac86dc1264 | |||
| 051219c3c9 | |||
| d8916a326a | |||
| 3be51b1c77 | |||
| a3118f276f | |||
| 3d70074089 | |||
| bcc9e27dd0 | |||
| 0f4ee906de | |||
| 5f9dd9e135 | |||
| 5335dbf802 | |||
| 4c1b4f33a8 | |||
| f1a60f1664 | |||
| d66e6571c1 | |||
| dc8b9fff57 | |||
| 0eba6f0da3 | |||
| a5579614d9 | |||
| eca6d91532 | |||
| 354c315f55 | |||
| a302ad88f3 | |||
| 447e1ee0cd | |||
| bef2f4488f | |||
| d5adfd7980 | |||
| 3777a9fcbd | |||
| 7c834224a2 | |||
| c1e90d51ca | |||
| a0dba60fa6 | |||
| 63e01cf6b3 | |||
| 5d45036c09 | |||
| b31f78de50 | |||
| 967fd099f9 | |||
| e655edafec | |||
| 6ec2455e24 | |||
| 003d8b2f80 | |||
| b846a29b17 | |||
| 8b994be381 | |||
| b246fc881b | |||
| 5785923c0e | |||
| 66607cb145 | |||
| 4637da1f36 | |||
| 22e4d84993 | |||
| e2b9b19970 | |||
| 75f964657f | |||
| 23b73168a0 | |||
| 391dd90e84 | |||
| 1345760087 | |||
| 51fccf226f | |||
| a14a20b61b | |||
| fadbd0fa31 | |||
| 40ef084e7f | |||
| a07033f3fb | |||
| 293a9c0ee3 | |||
| 32d1b83f62 | |||
| 227bc196b1 | |||
| 2fcef050e0 |
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 7.0.x
|
||||
- main
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: 'spring-framework-7.0.x'
|
||||
build-name: 'spring-framework-7.1.x'
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-snapshot-local'
|
||||
|
||||
@@ -2,8 +2,8 @@ name: Release Milestone
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.0-M[1-9]
|
||||
- v7.0.0-RC[1-9]
|
||||
- v7.1.0-M[1-9]
|
||||
- v7.1.0-RC[1-9]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.[0-9]+
|
||||
- v7.1.[0-9]+
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Contributing to the Spring Framework
|
||||
# Contributing to the Spring Framework
|
||||
|
||||
First off, thank you for taking the time to contribute! :+1: :tada:
|
||||
|
||||
|
||||
+1
-1
@@ -78,7 +78,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
"https://projectreactor.io/docs/core/release/api/",
|
||||
"https://projectreactor.io/docs/test/release/api/",
|
||||
"https://junit.org/junit4/javadoc/4.13.2/",
|
||||
"https://docs.junit.org/6.0.3/api/",
|
||||
"https://docs.junit.org/6.1.0/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
"https://jspecify.dev/docs/api/"
|
||||
|
||||
@@ -20,7 +20,7 @@ ext {
|
||||
dependencies {
|
||||
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
|
||||
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
|
||||
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.1.0"
|
||||
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.2.0"
|
||||
implementation "com.tngtech.archunit:archunit:1.4.1"
|
||||
implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
|
||||
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
|
||||
|
||||
@@ -31,7 +31,7 @@ asciidoc:
|
||||
spring-org: 'spring-projects'
|
||||
spring-github-org: "https://github.com/{spring-org}"
|
||||
spring-framework-github: "https://github.com/{spring-org}/spring-framework"
|
||||
spring-framework-code: '{spring-framework-github}/tree/7.0.x'
|
||||
spring-framework-code: '{spring-framework-github}/tree/main'
|
||||
spring-framework-issues: '{spring-framework-github}/issues'
|
||||
spring-framework-wiki: '{spring-framework-github}/wiki'
|
||||
# Docs
|
||||
|
||||
@@ -541,7 +541,6 @@ following kinds of expressions cannot be compiled.
|
||||
* Expressions relying on the conversion service
|
||||
* Expressions using custom resolvers
|
||||
* Expressions using overloaded operators
|
||||
* Expressions using `Optional` with the null-safe or Elvis operator
|
||||
* Expressions using array construction syntax
|
||||
* Expressions using selection or projection
|
||||
* Expressions using bean references
|
||||
|
||||
@@ -402,6 +402,27 @@ To serialize only a subset of the object properties, you can specify a {baeldung
|
||||
.toBodilessEntity();
|
||||
----
|
||||
|
||||
==== URL encoded Forms
|
||||
|
||||
URL encoded forms, using the `"application/x-www-form-urlencoded"` media type, are useful for sending String key/values over the wire.
|
||||
This is supported by the `FormHttpMessageConverter`, if the application uses a `MultiValueMap<String, String>` as source instance
|
||||
or a target type.
|
||||
|
||||
For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
|
||||
form.add("project", "Spring Framework");
|
||||
form.add("module", "spring-web");
|
||||
ResponseEntity<Void> response = this.restClient.post()
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.body(form)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
----
|
||||
|
||||
|
||||
==== Multipart
|
||||
|
||||
To send multipart data, you need to provide a `MultiValueMap<String, Object>` whose values may be an `Object` for part content, a `Resource` for a file part, or an `HttpEntity` for part content with headers.
|
||||
@@ -419,18 +440,70 @@ For example:
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
|
||||
// send using RestClient.post or RestTemplate.postForEntity
|
||||
ResponseEntity<Void> response = this.restClient.post()
|
||||
.contentType(MediaType.MULTIPART_FORM_DATA)
|
||||
.body(parts)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
----
|
||||
|
||||
In most cases, you do not have to specify the `Content-Type` for each part.
|
||||
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension.
|
||||
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
|
||||
|
||||
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
|
||||
The `Content-Type` is set to `multipart/form-data` by the `MultipartHttpMessageConverter`.
|
||||
As seen in the previous section, `MultiValueMap` types can also be used for URL encoded forms.
|
||||
It is preferable to explicitly set the media type in the `Content-Type` or `Accept` HTTP request headers to ensure that the expected
|
||||
message converter is used.
|
||||
|
||||
`RestClient` can also receive multipart responses.
|
||||
To decode a multipart response body, use a `ParameterizedTypeReference<MultiValueMap<String, Part>>`.
|
||||
The decoded map contains `Part` instances where `FormFieldPart` represents form field values
|
||||
and `FilePart` represents file parts with a `filename()` and a `transferTo()` method.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, Part> result = this.restClient.get()
|
||||
.uri("https://example.com/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.retrieve()
|
||||
.body(new ParameterizedTypeReference<>() {});
|
||||
|
||||
Part field = result.getFirst("fieldPart");
|
||||
if (field instanceof FormFieldPart formField) {
|
||||
String fieldValue = formField.value();
|
||||
}
|
||||
Part file = result.getFirst("filePart");
|
||||
if (file instanceof FilePart filePart) {
|
||||
filePart.transferTo(Path.of("/tmp/" + filePart.filename()));
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
val result = this.restClient.get()
|
||||
.uri("https://example.com/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.retrieve()
|
||||
.body(object : ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
|
||||
|
||||
val field = result?.getFirst("fieldPart")
|
||||
if (field is FormFieldPart) {
|
||||
val fieldValue = field.value()
|
||||
}
|
||||
val file = result?.getFirst("filePart")
|
||||
if (file is FilePart) {
|
||||
file.transferTo(Path.of("/tmp/" + file.filename()))
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`.
|
||||
If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`.
|
||||
If necessary the `Content-Type` may also be set explicitly.
|
||||
|
||||
[[rest-request-factories]]
|
||||
=== Client Request Factories
|
||||
|
||||
+158
-6
@@ -12,18 +12,20 @@ The annotations can be applied in the following ways.
|
||||
* On a non-static field in a test class or any of its superclasses.
|
||||
* On a non-static field in an enclosing class for a `@Nested` test class or in any class
|
||||
in the type hierarchy or enclosing class hierarchy above the `@Nested` test class.
|
||||
* On a parameter in the constructor for a test class.
|
||||
* At the type level on a test class or any superclass or implemented interface in the
|
||||
type hierarchy above the test class.
|
||||
* At the type level on an enclosing class for a `@Nested` test class or on any class or
|
||||
interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test
|
||||
class.
|
||||
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy
|
||||
is inferred from the type of the annotated field. If multiple candidates exist in the
|
||||
`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help
|
||||
disambiguate. In the absence of a `@Qualifier` annotation, the name of the annotated
|
||||
field will be used as a _fallback qualifier_. Alternatively, you can explicitly specify a
|
||||
bean name to mock or spy by setting the `value` or `name` attribute in the annotation.
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field or constructor parameter,
|
||||
the bean to mock or spy is inferred from the type of the annotated field or parameter. If
|
||||
multiple candidates exist in the `ApplicationContext`, a `@Qualifier` annotation can be
|
||||
declared on the field or parameter to help disambiguate. In the absence of a `@Qualifier`
|
||||
annotation, the name of the annotated field or parameter will be used as a _fallback
|
||||
qualifier_. Alternatively, you can explicitly specify a bean name to mock or spy by
|
||||
setting the `value` or `name` attribute in the annotation.
|
||||
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean
|
||||
(or beans) to mock or spy must be supplied via the `types` attribute in the annotation –
|
||||
@@ -201,6 +203,82 @@ Kotlin::
|
||||
<1> Replace the bean named `service` with a Mockito mock.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoBean` on a constructor parameter for a
|
||||
by-type lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoBean CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into
|
||||
the constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoBean val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean with type `CustomService` with a Mockito mock and inject it into
|
||||
the constructor.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoBean` on a constructor parameter for a
|
||||
by-name lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoBean("service") CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean named `service` with a Mockito mock and inject it into the
|
||||
constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoBean("service") val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean named `service` with a Mockito mock and inject it into the
|
||||
constructor.
|
||||
======
|
||||
|
||||
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
|
||||
|
||||
[tabs]
|
||||
@@ -375,6 +453,80 @@ Kotlin::
|
||||
<1> Wrap the bean named `service` with a Mockito spy.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoSpyBean` on a constructor parameter for
|
||||
a by-type lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoSpyBean CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the
|
||||
constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoSpyBean val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito spy and inject it into the
|
||||
constructor.
|
||||
======
|
||||
|
||||
The following example shows how to use `@MockitoSpyBean` on a constructor parameter for
|
||||
a by-name lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
private final CustomService customService;
|
||||
|
||||
BeanOverrideTests(@MockitoSpyBean("service") CustomService customService) { // <1>
|
||||
this.customService = customService;
|
||||
}
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
class BeanOverrideTests(@MockitoSpyBean("service") val customService: CustomService) { // <1>
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean named `service` with a Mockito spy and inject it into the constructor.
|
||||
======
|
||||
|
||||
The following `@SharedSpies` annotation registers two spies by-type and one spy by-name.
|
||||
|
||||
[tabs]
|
||||
|
||||
@@ -142,6 +142,9 @@ provides two alternative ways to verify the response:
|
||||
1. xref:resttestclient-workflow[Built-in Assertions] extend the request workflow with a chain of expectations
|
||||
2. xref:resttestclient-assertj[AssertJ Integration] to verify the response via `assertThat()` statements
|
||||
|
||||
TIP: See the xref:integration/rest-clients.adoc#rest-message-conversion[HTTP Message Conversion]
|
||||
section for examples on how to prepare a request with any content, including form data and multipart data.
|
||||
|
||||
|
||||
|
||||
[[resttestclient.workflow]]
|
||||
@@ -213,6 +216,16 @@ To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
|
||||
include-code::./JsonTests[tag=jsonPath,indent=0]
|
||||
|
||||
|
||||
[[resttestclient.multipart]]
|
||||
==== Multipart Content
|
||||
|
||||
When testing endpoints that return multipart responses, you can decode the body to a
|
||||
`MultiValueMap<String, Part>` and assert individual parts using the `FormFieldPart`
|
||||
and `FilePart` subtypes.
|
||||
|
||||
include-code::./MultipartTests[tag=multipart,indent=0]
|
||||
|
||||
|
||||
[[resttestclient.assertj]]
|
||||
=== AssertJ Integration
|
||||
|
||||
|
||||
+6
-5
@@ -2,8 +2,9 @@
|
||||
= Bean Overriding in Tests
|
||||
|
||||
Bean overriding in tests refers to the ability to override specific beans in the
|
||||
`ApplicationContext` for a test class, by annotating the test class or one or more
|
||||
non-static fields in the test class.
|
||||
`ApplicationContext` for a test class, by annotating the test class, one or more
|
||||
non-static fields in the test class, or one or more parameters in the constructor for the
|
||||
test class.
|
||||
|
||||
NOTE: This feature is intended as a less risky alternative to the practice of registering
|
||||
a bean via `@Bean` with the `DefaultListableBeanFactory`
|
||||
@@ -42,9 +43,9 @@ The `spring-test` module registers implementations of the latter two
|
||||
{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`
|
||||
properties file].
|
||||
|
||||
The bean overriding infrastructure searches for annotations on test classes as well as
|
||||
annotations on non-static fields in test classes that are meta-annotated with
|
||||
`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
|
||||
The bean overriding infrastructure searches for annotations on test classes, non-static
|
||||
fields in test classes, and parameters in test class constructors that are meta-annotated
|
||||
with `@BeanOverride`, and instantiates the corresponding `BeanOverrideProcessor` which is
|
||||
responsible for creating an appropriate `BeanOverrideHandler`.
|
||||
|
||||
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
|
||||
|
||||
@@ -179,6 +179,10 @@ If a specific parameter in a constructor for a JUnit Jupiter test class is of ty
|
||||
`ApplicationContext` (or a sub-type thereof) or is annotated or meta-annotated with
|
||||
`@Autowired`, `@Qualifier`, or `@Value`, Spring injects the value for that specific
|
||||
parameter with the corresponding bean or value from the test's `ApplicationContext`.
|
||||
Similarly, if a specific parameter is annotated with `@MockitoBean` or `@MockitoSpyBean`,
|
||||
Spring will inject a Mockito mock or spy, respectively — see
|
||||
xref:testing/annotations/integration-spring/annotation-mockitobean.adoc[`@MockitoBean` and `@MockitoSpyBean`]
|
||||
for details.
|
||||
|
||||
Spring can also be configured to autowire all arguments for a test class constructor if
|
||||
the constructor is considered to be _autowirable_. A constructor is considered to be
|
||||
|
||||
@@ -580,8 +580,8 @@ Kotlin::
|
||||
[[webtestclient-stream]]
|
||||
==== Streaming Responses
|
||||
|
||||
To test potentially infinite streams such as `"text/event-stream"` or
|
||||
`"application/x-ndjson"`, start by verifying the response status and headers, and then
|
||||
To test potentially infinite streams such as `"text/event-stream"`,
|
||||
`"application/jsonl"` or `"application/x-ndjson"`, start by verifying the response status and headers, and then
|
||||
obtain a `FluxExchangeResult`:
|
||||
|
||||
[tabs]
|
||||
|
||||
@@ -485,8 +485,8 @@ The `JacksonJsonEncoder` works as follows:
|
||||
* For a multi-value publisher with `application/json`, by default collect the values with
|
||||
`Flux#collectToList()` and then serialize the resulting collection.
|
||||
* For a multi-value publisher with a streaming media type such as
|
||||
`application/x-ndjson` or `application/stream+x-jackson-smile`, encode, write, and
|
||||
flush each value individually using a
|
||||
`application/jsonl`, `application/x-ndjson` or `application/stream+x-jackson-smile`,
|
||||
encode, write, and flush each value individually using a
|
||||
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format. Other
|
||||
streaming media types may be registered with the encoder.
|
||||
* For SSE the `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure
|
||||
@@ -598,7 +598,7 @@ To configure all three in WebFlux, you'll need to supply a pre-configured instan
|
||||
[.small]#xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[See equivalent in the Servlet stack]#
|
||||
|
||||
When streaming to the HTTP response (for example, `text/event-stream`,
|
||||
`application/x-ndjson`), it is important to send data periodically, in order to
|
||||
`application/jsonl`, `application/x-ndjson`), it is important to send data periodically, in order to
|
||||
reliably detect a disconnected client sooner rather than later. Such a send could be a
|
||||
comment-only, empty SSE event or any other "no-op" data that would effectively serve as
|
||||
a heartbeat.
|
||||
|
||||
@@ -23,13 +23,17 @@ For all converters, a default media type is used, but you can override it by set
|
||||
By default, this converter supports all text media types(`text/{asterisk}`) and writes with a `Content-Type` of `text/plain`.
|
||||
|
||||
| `FormHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write form data from the HTTP request and response.
|
||||
| An `HttpMessageConverter` implementation that can read and write URL encoded forms.
|
||||
By default, this converter reads and writes the `application/x-www-form-urlencoded` media type.
|
||||
Form data is read from and written into a `MultiValueMap<String, String>`.
|
||||
The converter can also write (but not read) multipart data read from a `MultiValueMap<String, Object>`.
|
||||
By default, `multipart/form-data` is supported.
|
||||
Additional multipart subtypes can be supported for writing form data.
|
||||
Consult the javadoc for `FormHttpMessageConverter` for further details.
|
||||
`Map<String, String>` is also supported, but multiple values under the same key will be ignored.
|
||||
|
||||
| `MultipartHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write multipart messages.
|
||||
`MultiValueMap<String, Object>` can be written to multipart messages, converting each part independently using
|
||||
the configured message converters. Multipart messages can be read into `MultiValueMap<String, Part>`, each value
|
||||
being a `Part` or one of its subtypes (`FormFieldPart` and `FilePart`).
|
||||
By default, `multipart/form-data` is supported. Additional multipart subtypes can be supported for writing form data.
|
||||
|
||||
| `ByteArrayHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write byte arrays from the HTTP request and response.
|
||||
|
||||
@@ -423,8 +423,8 @@ Reactive return values are handled as follows:
|
||||
|
||||
* A single-value promise is adapted to, similar to using `DeferredResult`. Examples
|
||||
include `CompletionStage` (JDK), `Mono` (Reactor), and `Single` (RxJava).
|
||||
* A multi-value stream with a streaming media type (such as `application/x-ndjson`
|
||||
or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
|
||||
* A multi-value stream with a streaming media type (such as `application/jsonl`,
|
||||
`application/x-ndjson` or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
|
||||
`SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava).
|
||||
Applications can also return `Flux<ServerSentEvent>` or `Observable<ServerSentEvent>`.
|
||||
* A multi-value stream with any other media type (such as `application/json`) is adapted
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2025-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.testing.resttestclient.multipart;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.multipart.FilePart;
|
||||
import org.springframework.http.converter.multipart.FormFieldPart;
|
||||
import org.springframework.http.converter.multipart.Part;
|
||||
import org.springframework.test.web.servlet.client.RestTestClient;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
public class MultipartTests {
|
||||
|
||||
RestTestClient client;
|
||||
|
||||
@Test
|
||||
void multipart() {
|
||||
// tag::multipart[]
|
||||
client.get().uri("/upload")
|
||||
.accept(MediaType.MULTIPART_FORM_DATA)
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(new ParameterizedTypeReference<MultiValueMap<String, Part>>() {})
|
||||
.value(result -> {
|
||||
Part field = result.getFirst("fieldPart");
|
||||
assertThat(field).isInstanceOfSatisfying(FormFieldPart.class,
|
||||
formField -> assertThat(formField.value()).isEqualTo("fieldValue"));
|
||||
Part file = result.getFirst("filePart");
|
||||
assertThat(file).isInstanceOfSatisfying(FilePart.class,
|
||||
filePart -> assertThat(filePart.filename()).isEqualTo("logo.png"));
|
||||
});
|
||||
// end::multipart[]
|
||||
}
|
||||
|
||||
}
|
||||
@@ -7,7 +7,7 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.20.2"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.21.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.6"))
|
||||
api(platform("io.netty:netty-bom:4.2.15.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.6"))
|
||||
@@ -18,14 +18,14 @@ dependencies {
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.9"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.9"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
|
||||
api(platform("org.junit:junit-bom:6.0.3"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.11.0"))
|
||||
api(platform("org.junit:junit-bom:6.1.0"))
|
||||
api(platform("org.mockito:mockito-bom:5.23.0"))
|
||||
api(platform("tools.jackson:jackson-bom:3.0.4"))
|
||||
api(platform("tools.jackson:jackson-bom:3.1.1"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.4")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.fasterxml.woodstox:woodstox-core:7.1.1")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.3")
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
@@ -120,7 +120,7 @@ dependencies {
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.graalvm.sdk:graal-sdk:22.3.1")
|
||||
api("org.hamcrest:hamcrest:3.0")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.17.Final")
|
||||
api("org.hibernate.orm:hibernate-core:7.4.0.Final")
|
||||
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.21.0")
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
version=7.0.8
|
||||
version=7.1.0-SNAPSHOT
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=2.2.21
|
||||
kotlinVersion=2.3.20
|
||||
byteBuddyVersion=1.17.6
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
|
||||
+4
-21
@@ -76,8 +76,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
*/
|
||||
private static final Log logger = LogFactory.getLog(AbstractNestablePropertyAccessor.class);
|
||||
|
||||
private int autoGrowCollectionLimit = Integer.MAX_VALUE;
|
||||
|
||||
@Nullable Object wrappedObject;
|
||||
|
||||
private String nestedPath = "";
|
||||
@@ -156,21 +154,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Specify a limit for array and collection auto-growing.
|
||||
* <p>Default is unlimited on a plain accessor.
|
||||
*/
|
||||
public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
|
||||
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the limit for array and collection auto-growing.
|
||||
*/
|
||||
public int getAutoGrowCollectionLimit() {
|
||||
return this.autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch the target object, replacing the cached introspection results only
|
||||
* if the class of the new object is different to that of the replaced object.
|
||||
@@ -298,7 +281,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
|
||||
componentType, ph.nested(tokens.keys.length));
|
||||
int length = Array.getLength(propValue);
|
||||
if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) {
|
||||
if (arrayIndex >= length && arrayIndex < getAutoGrowCollectionLimit()) {
|
||||
Object newArray = Array.newInstance(componentType, arrayIndex + 1);
|
||||
System.arraycopy(propValue, 0, newArray, 0, length);
|
||||
int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
|
||||
@@ -324,7 +307,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
|
||||
requiredType.getResolvableType().resolve(), requiredType);
|
||||
int size = list.size();
|
||||
if (index >= size && index < this.autoGrowCollectionLimit) {
|
||||
if (index >= size && index < getAutoGrowCollectionLimit()) {
|
||||
for (int i = size; i < index; i++) {
|
||||
try {
|
||||
list.add(null);
|
||||
@@ -761,7 +744,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
return array;
|
||||
}
|
||||
int length = Array.getLength(array);
|
||||
if (index >= length && index < this.autoGrowCollectionLimit) {
|
||||
if (index >= length && index < getAutoGrowCollectionLimit()) {
|
||||
Class<?> componentType = array.getClass().componentType();
|
||||
Object newArray = Array.newInstance(componentType, index + 1);
|
||||
System.arraycopy(array, 0, newArray, 0, length);
|
||||
@@ -785,7 +768,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
return;
|
||||
}
|
||||
int size = collection.size();
|
||||
if (index >= size && index < this.autoGrowCollectionLimit) {
|
||||
if (index >= size && index < getAutoGrowCollectionLimit()) {
|
||||
Class<?> elementType = ph.getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric();
|
||||
if (elementType != null) {
|
||||
for (int i = collection.size(); i < index + 1; i++) {
|
||||
|
||||
@@ -40,6 +40,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
|
||||
|
||||
private boolean autoGrowNestedPaths = false;
|
||||
|
||||
private int autoGrowCollectionLimit = Integer.MAX_VALUE;
|
||||
|
||||
boolean suppressNotWritablePropertyException = false;
|
||||
|
||||
|
||||
@@ -63,6 +65,16 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
|
||||
return this.autoGrowNestedPaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAutoGrowCollectionLimit(int autoGrowCollectionLimit) {
|
||||
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAutoGrowCollectionLimit() {
|
||||
return this.autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void setPropertyValue(PropertyValue pv) throws BeansException {
|
||||
|
||||
+13
@@ -74,4 +74,17 @@ public interface ConfigurablePropertyAccessor extends PropertyAccessor, Property
|
||||
*/
|
||||
boolean isAutoGrowNestedPaths();
|
||||
|
||||
/**
|
||||
* Specify a limit for array and collection auto-growing.
|
||||
* <p>Default is unlimited on a plain accessor.
|
||||
* @since 7.1
|
||||
*/
|
||||
void setAutoGrowCollectionLimit(int autoGrowCollectionLimit);
|
||||
|
||||
/**
|
||||
* Return the limit for array and collection auto-growing.
|
||||
* @since 7.1
|
||||
*/
|
||||
int getAutoGrowCollectionLimit();
|
||||
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ import org.springframework.core.ResolvableType;
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @author Yanming Zhou
|
||||
* @since 13 April 2001
|
||||
* @see BeanNameAware#setBeanName
|
||||
* @see BeanClassLoaderAware#setBeanClassLoader
|
||||
@@ -175,6 +176,29 @@ public interface BeanFactory {
|
||||
*/
|
||||
<T> T getBean(String name, Class<T> requiredType) throws BeansException;
|
||||
|
||||
/**
|
||||
* Return an instance, which may be shared or independent, of the specified bean.
|
||||
* <p>Behaves the same as {@link #getBean(String)}, but provides a measure of type
|
||||
* safety by throwing a BeanNotOfRequiredTypeException if the bean is not of the
|
||||
* required type. This means that ClassCastException can't be thrown on casting
|
||||
* the result correctly, as can happen with {@link #getBean(String)}.
|
||||
* <p>Translates aliases back to the corresponding canonical bean name.
|
||||
* <p>Will ask the parent factory if the bean cannot be found in this factory instance.
|
||||
* @param name the name of the bean to retrieve
|
||||
* @param typeReference the reference to obtain type the bean must match
|
||||
* @return an instance of the bean.
|
||||
* Note that the return value will never be {@code null}. In case of a stub for
|
||||
* {@code null} from a factory method having been resolved for the requested bean, a
|
||||
* {@code BeanNotOfRequiredTypeException} against the NullBean stub will be raised.
|
||||
* Consider using {@link #getBeanProvider(Class)} for resolving optional dependencies.
|
||||
* @throws NoSuchBeanDefinitionException if there is no such bean definition
|
||||
* @throws BeanNotOfRequiredTypeException if the bean is not of the required type
|
||||
* @throws BeansException if the bean could not be created
|
||||
* @since 7.1
|
||||
* @see #getBean(String, Class)
|
||||
*/
|
||||
<T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException;
|
||||
|
||||
/**
|
||||
* Return an instance, which may be shared or independent, of the specified bean.
|
||||
* <p>Allows for specifying explicit constructor arguments / factory method arguments,
|
||||
|
||||
+29
-6
@@ -16,14 +16,17 @@
|
||||
|
||||
package org.springframework.beans.factory;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.core.ResolvableType;
|
||||
|
||||
/**
|
||||
* Thrown when a bean doesn't match the expected type.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @author Yanming Zhou
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
@@ -32,7 +35,7 @@ public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
private final String beanName;
|
||||
|
||||
/** The required type. */
|
||||
private final Class<?> requiredType;
|
||||
private final Type genericRequiredType;
|
||||
|
||||
/** The offending type. */
|
||||
private final Class<?> actualType;
|
||||
@@ -46,10 +49,22 @@ public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
* the expected type
|
||||
*/
|
||||
public BeanNotOfRequiredTypeException(String beanName, Class<?> requiredType, Class<?> actualType) {
|
||||
super("Bean named '" + beanName + "' is expected to be of type '" + ClassUtils.getQualifiedName(requiredType) +
|
||||
"' but was actually of type '" + ClassUtils.getQualifiedName(actualType) + "'");
|
||||
this(beanName, (Type) requiredType, actualType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new BeanNotOfRequiredTypeException.
|
||||
* @param beanName the name of the bean requested
|
||||
* @param requiredType the required type
|
||||
* @param actualType the actual type returned, which did not match
|
||||
* the expected type
|
||||
* @since 7.1
|
||||
*/
|
||||
public BeanNotOfRequiredTypeException(String beanName, Type requiredType, Class<?> actualType) {
|
||||
super("Bean named '" + beanName + "' is expected to be of type '" + requiredType.getTypeName() +
|
||||
"' but was actually of type '" + actualType.getTypeName() + "'");
|
||||
this.beanName = beanName;
|
||||
this.requiredType = requiredType;
|
||||
this.genericRequiredType = requiredType;
|
||||
this.actualType = actualType;
|
||||
}
|
||||
|
||||
@@ -65,7 +80,15 @@ public class BeanNotOfRequiredTypeException extends BeansException {
|
||||
* Return the expected type for the bean.
|
||||
*/
|
||||
public Class<?> getRequiredType() {
|
||||
return this.requiredType;
|
||||
return (this.genericRequiredType instanceof Class<?> clazz ? clazz : ResolvableType.forType(this.genericRequiredType).toClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the expected generic type for the bean.
|
||||
* @since 7.1
|
||||
*/
|
||||
public Type getGenericRequiredType() {
|
||||
return this.genericRequiredType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,21 +19,9 @@ package org.springframework.beans.factory;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Contract for registering beans programmatically, typically imported with an
|
||||
* {@link org.springframework.context.annotation.Import @Import} annotation on
|
||||
* a {@link org.springframework.context.annotation.Configuration @Configuration}
|
||||
* class.
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @Import(MyBeanRegistrar.class)
|
||||
* class MyConfiguration {
|
||||
* }</pre>
|
||||
* Can also be applied to an application context via
|
||||
* {@link org.springframework.context.support.GenericApplicationContext#register(BeanRegistrar...)}.
|
||||
* Contract for registering beans programmatically. Implementations use the
|
||||
* {@link BeanRegistry} and {@link Environment} to register beans:
|
||||
*
|
||||
*
|
||||
* <p>Bean registrar implementations use {@link BeanRegistry} and {@link Environment}
|
||||
* APIs to register beans programmatically in a concise and flexible way.
|
||||
* <pre class="code">
|
||||
* class MyBeanRegistrar implements BeanRegistrar {
|
||||
*
|
||||
@@ -52,9 +40,55 @@ import org.springframework.core.env.Environment;
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <p>{@code BeanRegistrar} implementations are not Spring components: they must have
|
||||
* a no-arg constructor and cannot rely on dependency injection or any other
|
||||
* component-model feature. They can be used in two distinct ways depending on the
|
||||
* application context setup.
|
||||
*
|
||||
* <h3>With the {@code @Configuration} model</h3>
|
||||
*
|
||||
* <p>A {@code BeanRegistrar} must be imported via
|
||||
* {@link org.springframework.context.annotation.Import @Import} on a
|
||||
* {@link org.springframework.context.annotation.Configuration @Configuration} class:
|
||||
*
|
||||
* <pre class="code">
|
||||
* @Configuration
|
||||
* @Import(MyBeanRegistrar.class)
|
||||
* class MyConfiguration {
|
||||
* }</pre>
|
||||
*
|
||||
* <p>This is the only mechanism that triggers bean registration in the annotation-based
|
||||
* configuration model. Annotating an implementation with {@code @Configuration} or
|
||||
* {@code @Component}, or returning an instance from a {@code @Bean} method, registers
|
||||
* it as a bean but does <strong>not</strong> invoke its
|
||||
* {@link #register(BeanRegistry, Environment) register} method.
|
||||
*
|
||||
* <p>When imported, the registrar is invoked in the order it is encountered during
|
||||
* configuration class processing. It can therefore check for and build on beans that
|
||||
* have already been defined, but has no visibility into beans that will be registered
|
||||
* by classes processed later.
|
||||
*
|
||||
* <h3>Programmatic usage</h3>
|
||||
*
|
||||
* <p>A {@code BeanRegistrar} can also be applied directly to a
|
||||
* {@link org.springframework.context.support.GenericApplicationContext}:
|
||||
*
|
||||
* <pre class="code">
|
||||
* GenericApplicationContext context = new GenericApplicationContext();
|
||||
* context.register(new MyBeanRegistrar());
|
||||
* context.registerBean("myBean", MyBean.class);
|
||||
* context.refresh();</pre>
|
||||
*
|
||||
* <p>This mode is primarily intended for fully programmatic application context setups.
|
||||
* Registrars applied this way are invoked before any {@code @Configuration} class is
|
||||
* processed. They can therefore observe beans registered programmatically (e.g., via
|
||||
* one of the {@code GenericApplicationContext#registerBean} methods), but will
|
||||
* <strong>not</strong> see any beans defined in {@code @Configuration} classes also
|
||||
* registered with the context.
|
||||
*
|
||||
* <p>A {@code BeanRegistrar} implementing {@link org.springframework.context.annotation.ImportAware}
|
||||
* can optionally introspect import metadata when used in an import scenario, otherwise the
|
||||
* {@code setImportMetadata} method is simply not being called.
|
||||
* can optionally introspect import metadata when used in an import scenario; otherwise
|
||||
* the {@code setImportMetadata} method is not called.
|
||||
*
|
||||
* <p>In Kotlin, it is recommended to use {@code BeanRegistrarDsl} instead of
|
||||
* implementing {@code BeanRegistrar}.
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.core.env.Environment;
|
||||
* programmatic bean registration capabilities.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.0
|
||||
*/
|
||||
public interface BeanRegistry {
|
||||
@@ -140,6 +141,28 @@ public interface BeanRegistry {
|
||||
*/
|
||||
<T> void registerBean(String name, ParameterizedTypeReference<T> beanType, Consumer<Spec<T>> customizer);
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given name is already registered.
|
||||
* @param name the name of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
boolean containsBean(String name);
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given type is already registered.
|
||||
* @param beanType the type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
boolean containsBean(Class<?> beanType);
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given generics-containing type is
|
||||
* already registered.
|
||||
* @param beanType the generics-containing type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
<T> boolean containsBean(ParameterizedTypeReference<T> beanType);
|
||||
|
||||
|
||||
/**
|
||||
* Specification for customizing a bean.
|
||||
|
||||
+63
-13
@@ -92,16 +92,8 @@ public final class ParameterResolutionDelegate {
|
||||
/**
|
||||
* Resolve the dependency for the supplied {@link Parameter} from the
|
||||
* supplied {@link AutowireCapableBeanFactory}.
|
||||
* <p>Provides comprehensive autowiring support for individual method parameters
|
||||
* on par with Spring's dependency injection facilities for autowired fields and
|
||||
* methods, including support for {@link Autowired @Autowired},
|
||||
* {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
|
||||
* placeholders and SpEL expressions in {@code @Value} declarations.
|
||||
* <p>The dependency is required unless the parameter is annotated or meta-annotated
|
||||
* with {@link Autowired @Autowired} with the {@link Autowired#required required}
|
||||
* flag set to {@code false}.
|
||||
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
|
||||
* will be used as the qualifier for resolving ambiguities.
|
||||
* <p>See {@link #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)}
|
||||
* for details.
|
||||
* @param parameter the parameter whose dependency should be resolved (must not be
|
||||
* {@code null})
|
||||
* @param parameterIndex the index of the parameter in the constructor or method
|
||||
@@ -113,13 +105,49 @@ public final class ParameterResolutionDelegate {
|
||||
* the dependency (must not be {@code null})
|
||||
* @return the resolved object, or {@code null} if none found
|
||||
* @throws BeansException if dependency resolution failed
|
||||
* @see #resolveDependency(Parameter, int, String, Class, AutowireCapableBeanFactory)
|
||||
*/
|
||||
public static @Nullable Object resolveDependency(
|
||||
Parameter parameter, int parameterIndex, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
|
||||
throws BeansException {
|
||||
|
||||
return resolveDependency(parameter, parameterIndex, null, containingClass, beanFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the dependency for the supplied {@link Parameter} from the
|
||||
* supplied {@link AutowireCapableBeanFactory}.
|
||||
* <p>Provides comprehensive autowiring support for individual method parameters
|
||||
* on par with Spring's dependency injection facilities for autowired fields and
|
||||
* methods, including support for {@link Autowired @Autowired},
|
||||
* {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
|
||||
* placeholders and SpEL expressions in {@code @Value} declarations.
|
||||
* <p>The dependency is required unless the parameter is annotated or meta-annotated
|
||||
* with {@link Autowired @Autowired} with the {@link Autowired#required required}
|
||||
* flag set to {@code false}.
|
||||
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
|
||||
* (or a supplied custom name) will be used as the qualifier for resolving ambiguities.
|
||||
* @param parameter the parameter whose dependency should be resolved (must not be
|
||||
* {@code null})
|
||||
* @param parameterIndex the index of the parameter in the constructor or method
|
||||
* that declares the parameter
|
||||
* @param parameterName a custom name for the parameter; or {@code null} to use
|
||||
* the default parameter name discovery logic
|
||||
* @param containingClass the concrete class that contains the parameter; this may
|
||||
* differ from the class that declares the parameter in that it may be a subclass
|
||||
* thereof, potentially substituting type variables (must not be {@code null})
|
||||
* @param beanFactory the {@code AutowireCapableBeanFactory} from which to resolve
|
||||
* the dependency (must not be {@code null})
|
||||
* @return the resolved object, or {@code null} if none found
|
||||
* @throws BeansException if dependency resolution failed
|
||||
* @since 7.1
|
||||
* @see #isAutowirable
|
||||
* @see Autowired#required
|
||||
* @see SynthesizingMethodParameter#forExecutable(Executable, int)
|
||||
* @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)
|
||||
*/
|
||||
public static @Nullable Object resolveDependency(
|
||||
Parameter parameter, int parameterIndex, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
|
||||
public static @Nullable Object resolveDependency(Parameter parameter, int parameterIndex,
|
||||
@Nullable String parameterName, Class<?> containingClass, AutowireCapableBeanFactory beanFactory)
|
||||
throws BeansException {
|
||||
|
||||
Assert.notNull(parameter, "Parameter must not be null");
|
||||
@@ -132,7 +160,7 @@ public final class ParameterResolutionDelegate {
|
||||
|
||||
MethodParameter methodParameter = SynthesizingMethodParameter.forExecutable(
|
||||
parameter.getDeclaringExecutable(), parameterIndex);
|
||||
DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required);
|
||||
DependencyDescriptor descriptor = new NamedParameterDependencyDescriptor(methodParameter, required, parameterName);
|
||||
descriptor.setContainingClass(containingClass);
|
||||
return beanFactory.resolveDependency(descriptor, null);
|
||||
}
|
||||
@@ -171,4 +199,26 @@ public final class ParameterResolutionDelegate {
|
||||
return parameter;
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class NamedParameterDependencyDescriptor extends DependencyDescriptor {
|
||||
|
||||
private final @Nullable String parameterName;
|
||||
|
||||
NamedParameterDependencyDescriptor(MethodParameter methodParameter, boolean required, @Nullable String parameterName) {
|
||||
super(methodParameter, required);
|
||||
this.parameterName = parameterName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDependencyName() {
|
||||
return (this.parameterName != null ? this.parameterName : super.getDependencyName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean usesStandardBeanLookup() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ public interface AutowiredArguments {
|
||||
Object value = getObject(index);
|
||||
if (!ClassUtils.isAssignableValue(requiredType, value)) {
|
||||
throw new IllegalArgumentException("Argument type mismatch: expected '" +
|
||||
ClassUtils.getQualifiedName(requiredType) + "' for value [" + value + "]");
|
||||
requiredType.getTypeName() + "' for value [" + value + "]");
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
|
||||
+8
-6
@@ -26,6 +26,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
@@ -194,30 +195,31 @@ public abstract class YamlProcessor {
|
||||
}
|
||||
|
||||
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
|
||||
int count = 0;
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
try {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loading from YAML: " + resource);
|
||||
}
|
||||
try (Reader reader = new UnicodeReader(resource.getInputStream())) {
|
||||
resource.consumeContent(inputStream -> {
|
||||
Reader reader = new UnicodeReader(inputStream);
|
||||
for (Object object : yaml.loadAll(reader)) {
|
||||
if (object != null && process(asMap(object), callback)) {
|
||||
count++;
|
||||
count.incrementAndGet();
|
||||
if (this.resolutionMethod == ResolutionMethod.FIRST_FOUND) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") +
|
||||
logger.debug("Loaded " + count + " document" + (count.get() > 1 ? "s" : "") +
|
||||
" from YAML resource: " + resource);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleProcessError(resource, ex);
|
||||
}
|
||||
return (count > 0);
|
||||
return (count.get() > 0);
|
||||
}
|
||||
|
||||
private void handleProcessError(Resource resource, IOException ex) {
|
||||
|
||||
+14
-1
@@ -17,6 +17,7 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.beans.PropertyEditor;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -66,6 +67,7 @@ import org.springframework.beans.factory.config.Scope;
|
||||
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
|
||||
import org.springframework.core.DecoratingClassLoader;
|
||||
import org.springframework.core.NamedThreadLocal;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
import org.springframework.core.log.LogMessage;
|
||||
@@ -201,6 +203,17 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
return doGetBean(name, requiredType, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
|
||||
Object bean = getBean(name);
|
||||
Type requiredType = typeReference.getType();
|
||||
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
|
||||
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
|
||||
return doGetBean(name, null, args, false);
|
||||
@@ -413,7 +426,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
catch (TypeMismatchException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Failed to convert bean '" + name + "' to required type '" +
|
||||
ClassUtils.getQualifiedName(requiredType) + "'", ex);
|
||||
requiredType.getTypeName() + "'", ex);
|
||||
}
|
||||
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
|
||||
}
|
||||
|
||||
+17
@@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.BeanRegistrar;
|
||||
import org.springframework.beans.factory.BeanRegistry;
|
||||
import org.springframework.beans.factory.ListableBeanFactory;
|
||||
@@ -176,6 +177,22 @@ public class BeanRegistryAdapter implements BeanRegistry {
|
||||
this.beanRegistry.registerBeanDefinition(name, beanDefinition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsBean(String name) {
|
||||
return this.beanFactory.containsBean(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsBean(Class<?> beanType) {
|
||||
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, beanType).length > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> boolean containsBean(ParameterizedTypeReference<T> beanType) {
|
||||
ResolvableType resolvableType = ResolvableType.forType(beanType);
|
||||
return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this.beanFactory, resolvableType).length > 0;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* {@link RootBeanDefinition} subclass for {@code #registerBean} based
|
||||
|
||||
+2
-3
@@ -17,7 +17,6 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
@@ -256,14 +255,14 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader
|
||||
|
||||
Properties props = new Properties();
|
||||
try {
|
||||
try (InputStream is = encodedResource.getResource().getInputStream()) {
|
||||
encodedResource.getResource().consumeContent(is -> {
|
||||
if (encodedResource.getEncoding() != null) {
|
||||
getPropertiesPersister().load(props, new InputStreamReader(is, encodedResource.getEncoding()));
|
||||
}
|
||||
else {
|
||||
getPropertiesPersister().load(props, is);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
int count = registerBeanDefinitions(props, prefix, encodedResource.getResource().getDescription());
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
+13
@@ -17,6 +17,7 @@
|
||||
package org.springframework.beans.factory.support;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -64,6 +65,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @author Yanming Zhou
|
||||
* @since 06.01.2003
|
||||
* @see DefaultListableBeanFactory
|
||||
*/
|
||||
@@ -149,6 +151,17 @@ public class StaticListableBeanFactory implements ListableBeanFactory {
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
|
||||
Object bean = getBean(name);
|
||||
Type requiredType = typeReference.getType();
|
||||
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
|
||||
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
|
||||
if (!ObjectUtils.isEmpty(args)) {
|
||||
|
||||
+11
-6
@@ -21,6 +21,7 @@ import java.io.InputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.xml.parsers.ParserConfigurationException;
|
||||
|
||||
@@ -337,12 +338,16 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
|
||||
"Detected cyclic loading of " + encodedResource + " - check your import definitions!");
|
||||
}
|
||||
|
||||
try (InputStream inputStream = encodedResource.getResource().getInputStream()) {
|
||||
InputSource inputSource = new InputSource(inputStream);
|
||||
if (encodedResource.getEncoding() != null) {
|
||||
inputSource.setEncoding(encodedResource.getEncoding());
|
||||
}
|
||||
return doLoadBeanDefinitions(inputSource, encodedResource.getResource());
|
||||
try {
|
||||
AtomicInteger count = new AtomicInteger();
|
||||
encodedResource.getResource().consumeContent(inputStream -> {
|
||||
InputSource inputSource = new InputSource(inputStream);
|
||||
if (encodedResource.getEncoding() != null) {
|
||||
inputSource.setEncoding(encodedResource.getEncoding());
|
||||
}
|
||||
count.addAndGet(doLoadBeanDefinitions(inputSource, encodedResource.getResource()));
|
||||
});
|
||||
return count.get();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new BeanDefinitionStoreException(
|
||||
|
||||
+2
-2
@@ -84,8 +84,8 @@ public class ClassArrayEditor extends PropertyEditorSupport {
|
||||
return "";
|
||||
}
|
||||
StringJoiner sj = new StringJoiner(",");
|
||||
for (Class<?> klass : classes) {
|
||||
sj.add(ClassUtils.getQualifiedName(klass));
|
||||
for (Class<?> clazz : classes) {
|
||||
sj.add(clazz.getTypeName());
|
||||
}
|
||||
return sj.toString();
|
||||
}
|
||||
|
||||
+1
-6
@@ -72,12 +72,7 @@ public class ClassEditor extends PropertyEditorSupport {
|
||||
@Override
|
||||
public String getAsText() {
|
||||
Class<?> clazz = (Class<?>) getValue();
|
||||
if (clazz != null) {
|
||||
return ClassUtils.getQualifiedName(clazz);
|
||||
}
|
||||
else {
|
||||
return "";
|
||||
}
|
||||
return (clazz != null ? clazz.getTypeName() : "");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-2
@@ -24,6 +24,7 @@ import org.springframework.core.ResolvableType
|
||||
* This extension is not subject to type erasure and retains actual generic type arguments.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Yanming Zhou
|
||||
* @since 5.0
|
||||
*/
|
||||
inline fun <reified T : Any> BeanFactory.getBean(): T =
|
||||
@@ -31,14 +32,14 @@ inline fun <reified T : Any> BeanFactory.getBean(): T =
|
||||
|
||||
/**
|
||||
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>("foo")` variant.
|
||||
* Like the original Java method, this extension is subject to type erasure.
|
||||
* This extension is not subject to type erasure and retains actual generic type arguments.
|
||||
*
|
||||
* @see BeanFactory.getBean(String, Class<T>)
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.0
|
||||
*/
|
||||
inline fun <reified T : Any> BeanFactory.getBean(name: String): T =
|
||||
getBean(name, T::class.java)
|
||||
getBean(name, (object : ParameterizedTypeReference<T>() {}))
|
||||
|
||||
/**
|
||||
* Extension for [BeanFactory.getBean] providing a `getBean<Foo>(arg1, arg2)` variant.
|
||||
|
||||
@@ -18,8 +18,8 @@ package org.springframework.beans.factory
|
||||
|
||||
import org.springframework.beans.factory.BeanRegistry.SupplierContext
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.core.ResolvableType
|
||||
import org.springframework.core.env.Environment
|
||||
import kotlin.reflect.KClass
|
||||
|
||||
/**
|
||||
* Contract for registering programmatically beans.
|
||||
@@ -364,6 +364,28 @@ open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): Bean
|
||||
return registry.registerBean(object: ParameterizedTypeReference<T>() {}, customizer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given name is already registered.
|
||||
* @param name the name of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
fun containsBean(name: String): Boolean = registry.containsBean(name)
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given type is already registered.
|
||||
* @param beanType the type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
fun containsBean(beanType: KClass<*>): Boolean = registry.containsBean(beanType.java)
|
||||
|
||||
/**
|
||||
* Determine whether a bean of the given type is already registered.
|
||||
* @param T the type of the bean
|
||||
* @since 7.1
|
||||
*/
|
||||
inline fun <reified T : Any> containsBean(): Boolean =
|
||||
registry.containsBean(object: ParameterizedTypeReference<T>() {})
|
||||
|
||||
|
||||
/**
|
||||
* Context available from the bean instance supplier designed to give access
|
||||
|
||||
+36
@@ -79,6 +79,7 @@ import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.beans.testfixture.beans.factory.DummyFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -1682,6 +1683,29 @@ class DefaultListableBeanFactoryTests {
|
||||
lbf.getBean(TestBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanByNameWithTypeReference() {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(StringTemplate.class);
|
||||
RootBeanDefinition bd2 = new RootBeanDefinition(NumberTemplate.class);
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
lbf.registerBeanDefinition("bd2", bd2);
|
||||
|
||||
Template<String> stringTemplate = lbf.getBean("bd1", new ParameterizedTypeReference<>() {});
|
||||
Template<Number> numberTemplate = lbf.getBean("bd2", new ParameterizedTypeReference<>() {});
|
||||
|
||||
assertThat(stringTemplate).isInstanceOf(StringTemplate.class);
|
||||
assertThat(numberTemplate).isInstanceOf(NumberTemplate.class);
|
||||
|
||||
assertThatExceptionOfType(BeanNotOfRequiredTypeException.class)
|
||||
.isThrownBy(() -> lbf.getBean("bd2", new ParameterizedTypeReference<Template<String>>() {}))
|
||||
.satisfies(ex -> {
|
||||
assertThat(ex.getBeanName()).isEqualTo("bd2");
|
||||
assertThat(ex.getRequiredType()).isEqualTo(Template.class);
|
||||
assertThat(ex.getActualType()).isEqualTo(NumberTemplate.class);
|
||||
assertThat(ex.getGenericRequiredType().toString()).endsWith("Template<java.lang.String>");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void getBeanByTypeWithPrimary() {
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(TestBean.class);
|
||||
@@ -3872,4 +3896,16 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
}
|
||||
|
||||
private static class Template<T> {
|
||||
|
||||
}
|
||||
|
||||
private static class StringTemplate extends Template<String> {
|
||||
|
||||
}
|
||||
|
||||
private static class NumberTemplate extends Template<Number> {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+68
-12
@@ -45,9 +45,9 @@ class ParameterResolutionTests {
|
||||
|
||||
@Test
|
||||
void isAutowirablePreconditions() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ParameterResolutionDelegate.isAutowirable(null, 0))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.isAutowirable(null, 0))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -87,29 +87,30 @@ class ParameterResolutionTests {
|
||||
Parameter[] parameters = notAutowirableConstructor.getParameters();
|
||||
for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
|
||||
Parameter parameter = parameters[parameterIndex];
|
||||
assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex)).as("Parameter " + parameter + " must not be autowirable").isFalse();
|
||||
assertThat(ParameterResolutionDelegate.isAutowirable(parameter, parameterIndex))
|
||||
.as("Parameter " + parameter + " must not be autowirable").isFalse();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyPreconditionsForParameter() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, null, mock()))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, null, mock()))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyPreconditionsForContainingClass() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null))
|
||||
.withMessageContaining("Containing class must not be null");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, null, null))
|
||||
.withMessageContaining("Containing class must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyPreconditionsForBeanFactory() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() ->
|
||||
ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null))
|
||||
.withMessageContaining("AutowireCapableBeanFactory must not be null");
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, getClass(), null))
|
||||
.withMessageContaining("AutowireCapableBeanFactory must not be null");
|
||||
}
|
||||
|
||||
private Parameter getParameter() throws NoSuchMethodException {
|
||||
@@ -133,9 +134,64 @@ class ParameterResolutionTests {
|
||||
parameter, parameterIndex, AutowirableClass.class, beanFactory);
|
||||
assertThat(intermediateDependencyDescriptor.getAnnotatedElement()).isEqualTo(constructor);
|
||||
assertThat(intermediateDependencyDescriptor.getMethodParameter().getParameter()).isEqualTo(parameter);
|
||||
assertThat(intermediateDependencyDescriptor.usesStandardBeanLookup()).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterNamePreconditionsForParameter() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(null, 0, "customName", getClass(), mock()))
|
||||
.withMessageContaining("Parameter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterNamePreconditionsForContainingClass() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", null, mock()))
|
||||
.withMessageContaining("Containing class must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterNamePreconditionsForBeanFactory() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ParameterResolutionDelegate.resolveDependency(getParameter(), 0, "customName", getClass(), null))
|
||||
.withMessageContaining("AutowireCapableBeanFactory must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithNullCustomParameterNameFallsBackToDefaultParameterNameDiscovery() throws Exception {
|
||||
Constructor<?> constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class);
|
||||
AutowireCapableBeanFactory beanFactory = mock();
|
||||
given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
Parameter[] parameters = constructor.getParameters();
|
||||
for (int parameterIndex = 0; parameterIndex < parameters.length; parameterIndex++) {
|
||||
Parameter parameter = parameters[parameterIndex];
|
||||
DependencyDescriptor via4ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
|
||||
parameter, parameterIndex, AutowirableClass.class, beanFactory);
|
||||
DependencyDescriptor via5ArgMethod = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
|
||||
parameter, parameterIndex, null, AutowirableClass.class, beanFactory);
|
||||
assertThat(via5ArgMethod.getDependencyName()).isEqualTo(via4ArgMethod.getDependencyName());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDependencyWithCustomParameterName() throws Exception {
|
||||
Constructor<?> constructor = AutowirableClass.class.getConstructor(String.class, String.class, String.class, String.class);
|
||||
AutowireCapableBeanFactory beanFactory = mock();
|
||||
given(beanFactory.resolveDependency(any(), isNull())).willAnswer(invocation -> invocation.getArgument(0));
|
||||
|
||||
Parameter parameter = constructor.getParameters()[0];
|
||||
DependencyDescriptor descriptor = (DependencyDescriptor) ParameterResolutionDelegate.resolveDependency(
|
||||
parameter, 0, "customBeanName", AutowirableClass.class, beanFactory);
|
||||
|
||||
assertThat(descriptor.getAnnotatedElement()).isEqualTo(constructor);
|
||||
assertThat(descriptor.getMethodParameter().getParameter()).isEqualTo(parameter);
|
||||
assertThat(descriptor.getDependencyName()).isEqualTo("customBeanName");
|
||||
assertThat(descriptor.usesStandardBeanLookup()).isTrue();
|
||||
}
|
||||
|
||||
|
||||
void autowirableMethod(
|
||||
@Autowired String firstParameter,
|
||||
|
||||
+11
-1
@@ -21,6 +21,7 @@ import io.mockk.mockk
|
||||
import io.mockk.verify
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.core.ResolvableType
|
||||
|
||||
/**
|
||||
@@ -53,7 +54,16 @@ class BeanFactoryExtensionsTests {
|
||||
fun `getBean with String and reified type parameters`() {
|
||||
val name = "foo"
|
||||
bf.getBean<Foo>(name)
|
||||
verify { bf.getBean(name, Foo::class.java) }
|
||||
verify { bf.getBean(name, ofType<ParameterizedTypeReference<Foo>>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBean with String and reified generic type parameters`() {
|
||||
val name = "foo"
|
||||
val foo = listOf(Foo())
|
||||
every { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) } returns foo
|
||||
assertThat(bf.getBean<List<Foo>>("foo")).isSameAs(foo)
|
||||
verify { bf.getBean(name, ofType<ParameterizedTypeReference<List<Foo>>>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+9
-5
@@ -79,7 +79,7 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
this.to = copyOrNull(original.getTo());
|
||||
this.cc = copyOrNull(original.getCc());
|
||||
this.bcc = copyOrNull(original.getBcc());
|
||||
this.sentDate = original.getSentDate();
|
||||
this.sentDate = copyOrNull(original.sentDate);
|
||||
this.subject = original.getSubject();
|
||||
this.text = original.getText();
|
||||
}
|
||||
@@ -147,11 +147,11 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
|
||||
@Override
|
||||
public void setSentDate(@Nullable Date sentDate) {
|
||||
this.sentDate = sentDate;
|
||||
this.sentDate = copyOrNull(sentDate);
|
||||
}
|
||||
|
||||
public @Nullable Date getSentDate() {
|
||||
return this.sentDate;
|
||||
return copyOrNull(this.sentDate);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -194,8 +194,8 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
if (getBcc() != null) {
|
||||
target.setBcc(copy(getBcc()));
|
||||
}
|
||||
if (getSentDate() != null) {
|
||||
target.setSentDate(getSentDate());
|
||||
if (this.sentDate != null) {
|
||||
target.setSentDate((Date) this.sentDate.clone());
|
||||
}
|
||||
if (getSubject() != null) {
|
||||
target.setSubject(getSubject());
|
||||
@@ -247,6 +247,10 @@ public class SimpleMailMessage implements MailMessage, Serializable {
|
||||
return copy(state);
|
||||
}
|
||||
|
||||
private static @Nullable Date copyOrNull(@Nullable Date date) {
|
||||
return (date != null ? (Date) date.clone() : null);
|
||||
}
|
||||
|
||||
private static String[] copy(String[] state) {
|
||||
return state.clone();
|
||||
}
|
||||
|
||||
+63
@@ -21,11 +21,16 @@ import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
/**
|
||||
* Tests for {@link SimpleMailMessage}.
|
||||
*
|
||||
* @author Dmitriy Kopylenko
|
||||
* @author Juergen Hoeller
|
||||
* @author Rick Evans
|
||||
@@ -98,6 +103,64 @@ class SimpleMailMessageTests {
|
||||
assertThat(copy.getBcc()[0]).isEqualTo("us@mail.org");
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void setSentDateStoresACopy() {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
Date sentDate = new Date(1234L);
|
||||
|
||||
message.setSentDate(sentDate);
|
||||
sentDate.setTime(0L);
|
||||
|
||||
assertThat(message.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void getSentDateReturnsACopy() {
|
||||
SimpleMailMessage message = new SimpleMailMessage();
|
||||
Date sentDate = new Date(1234L);
|
||||
message.setSentDate(sentDate);
|
||||
|
||||
Date exportedDate = message.getSentDate();
|
||||
exportedDate.setTime(0L);
|
||||
|
||||
assertThat(message.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void copyConstructorCopiesSentDate() {
|
||||
Date sentDate = new Date(1234L);
|
||||
SimpleMailMessage original = new SimpleMailMessage();
|
||||
original.setSentDate(sentDate);
|
||||
|
||||
SimpleMailMessage copy = new SimpleMailMessage(original);
|
||||
sentDate.setTime(0L);
|
||||
|
||||
Date copiedDate = copy.getSentDate();
|
||||
assertThat(copiedDate).isNotNull();
|
||||
copiedDate.setTime(1L);
|
||||
|
||||
assertThat(original.getSentDate()).isEqualTo(new Date(1234L));
|
||||
assertThat(copy.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
@Test // gh-36626
|
||||
void copyToCopiesSentDate() {
|
||||
SimpleMailMessage source = new SimpleMailMessage();
|
||||
source.setSentDate(new Date(1234L));
|
||||
|
||||
MailMessage target = mock();
|
||||
source.copyTo(target);
|
||||
|
||||
ArgumentCaptor<Date> dateCaptor = ArgumentCaptor.forClass(Date.class);
|
||||
verify(target).setSentDate(dateCaptor.capture());
|
||||
|
||||
Date copiedDate = dateCaptor.getValue();
|
||||
assertThat(copiedDate).isNotNull();
|
||||
copiedDate.setTime(0L);
|
||||
|
||||
assertThat(source.getSentDate()).isEqualTo(new Date(1234L));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that two equal SimpleMailMessages have equal hash codes.
|
||||
*/
|
||||
|
||||
-17
@@ -291,23 +291,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Convenience method to return a String representation of this Method
|
||||
* for use in logging. Can be overridden in subclasses to provide a
|
||||
* different identifier for the given method.
|
||||
* @param method the method we're interested in
|
||||
* @param targetClass class the method is on
|
||||
* @return log message identifying this method
|
||||
* @see org.springframework.util.ClassUtils#getQualifiedMethodName
|
||||
* @deprecated since 6.2.18 with no replacement, for removal in 7.1
|
||||
*/
|
||||
@Deprecated(since = "6.2.18", forRemoval = true)
|
||||
protected String methodIdentification(Method method, Class<?> targetClass) {
|
||||
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
|
||||
return ClassUtils.getQualifiedMethodName(specificMethod);
|
||||
}
|
||||
|
||||
protected Collection<? extends Cache> getCaches(
|
||||
CacheOperationInvocationContext<CacheOperation> context, CacheResolver cacheResolver) {
|
||||
|
||||
|
||||
+4
-16
@@ -152,12 +152,12 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
private static final Set<Class<? extends Annotation>> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(2);
|
||||
|
||||
static {
|
||||
JAKARTA_RESOURCE_TYPE = loadAnnotationType("jakarta.annotation.Resource");
|
||||
JAKARTA_RESOURCE_TYPE = AnnotationUtils.loadAnnotationType("jakarta.annotation.Resource");
|
||||
if (JAKARTA_RESOURCE_TYPE != null) {
|
||||
resourceAnnotationTypes.add(JAKARTA_RESOURCE_TYPE);
|
||||
}
|
||||
|
||||
EJB_ANNOTATION_TYPE = loadAnnotationType("jakarta.ejb.EJB");
|
||||
EJB_ANNOTATION_TYPE = AnnotationUtils.loadAnnotationType("jakarta.ejb.EJB");
|
||||
if (EJB_ANNOTATION_TYPE != null) {
|
||||
resourceAnnotationTypes.add(EJB_ANNOTATION_TYPE);
|
||||
}
|
||||
@@ -191,8 +191,8 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
setOrder(Ordered.LOWEST_PRECEDENCE - 3);
|
||||
|
||||
// Jakarta EE 9 set of annotations in jakarta.annotation package
|
||||
addInitAnnotationType(loadAnnotationType("jakarta.annotation.PostConstruct"));
|
||||
addDestroyAnnotationType(loadAnnotationType("jakarta.annotation.PreDestroy"));
|
||||
addInitAnnotationType(AnnotationUtils.loadAnnotationType("jakarta.annotation.PostConstruct"));
|
||||
addDestroyAnnotationType(AnnotationUtils.loadAnnotationType("jakarta.annotation.PreDestroy"));
|
||||
|
||||
// java.naming module present on JDK 9+?
|
||||
if (JNDI_PRESENT) {
|
||||
@@ -575,18 +575,6 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static @Nullable Class<? extends Annotation> loadAnnotationType(String name) {
|
||||
try {
|
||||
return (Class<? extends Annotation>)
|
||||
ClassUtils.forName(name, CommonAnnotationBeanPostProcessor.class.getClassLoader());
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Class representing generic injection information about an annotated field
|
||||
* or setter method, supporting @Resource and related annotations.
|
||||
|
||||
+8
-6
@@ -423,12 +423,14 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
}
|
||||
|
||||
private void loadBeanDefinitionsFromBeanRegistrars(MultiValueMap<String, BeanRegistrar> registrars) {
|
||||
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
|
||||
throw new IllegalStateException("Cannot support bean registrars since " +
|
||||
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
|
||||
}
|
||||
registrars.values().forEach(registrarList -> registrarList.forEach(registrar -> registrar.register(new BeanRegistryAdapter(
|
||||
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment)));
|
||||
registrars.values().forEach(registrarList -> registrarList.forEach(registrar -> {
|
||||
if (!(this.registry instanceof ListableBeanFactory beanFactory)) {
|
||||
throw new IllegalStateException("Cannot support bean registrars since " +
|
||||
this.registry.getClass().getName() + " does not implement ListableBeanFactory");
|
||||
}
|
||||
registrar.register(new BeanRegistryAdapter(
|
||||
this.registry, beanFactory, this.environment, registrar.getClass()), this.environment);
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
+7
@@ -132,6 +132,7 @@ import org.springframework.util.ReflectionUtils;
|
||||
* @author Sam Brannen
|
||||
* @author Sebastien Deleuze
|
||||
* @author Brian Clozel
|
||||
* @author Yanming Zhou
|
||||
* @since January 21, 2001
|
||||
* @see #refreshBeanFactory
|
||||
* @see #getBeanFactory
|
||||
@@ -1305,6 +1306,12 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
return getBeanFactory().getBean(name, requiredType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
|
||||
assertBeanFactoryActive();
|
||||
return getBeanFactory().getBean(name, typeReference);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
|
||||
assertBeanFactoryActive();
|
||||
|
||||
+40
-1
@@ -44,6 +44,8 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.PriorityOrdered;
|
||||
import org.springframework.core.io.ProtocolResolver;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
@@ -105,6 +107,10 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class GenericApplicationContext extends AbstractApplicationContext implements BeanDefinitionRegistry {
|
||||
|
||||
private static final String DEFERRED_REGISTRY_POST_PROCESSOR_BEAN_NAME =
|
||||
GenericApplicationContext.class.getName() + ".deferredRegistryPostProcessor";
|
||||
|
||||
|
||||
private final DefaultListableBeanFactory beanFactory;
|
||||
|
||||
private @Nullable ResourceLoader resourceLoader;
|
||||
@@ -604,7 +610,13 @@ public class GenericApplicationContext extends AbstractApplicationContext implem
|
||||
*/
|
||||
public void register(BeanRegistrar... registrars) {
|
||||
for (BeanRegistrar registrar : registrars) {
|
||||
new BeanRegistryAdapter(this.beanFactory, getEnvironment(), registrar.getClass()).register(registrar);
|
||||
DeferredRegistryPostProcessor pp = (DeferredRegistryPostProcessor)
|
||||
this.beanFactory.getSingleton(DEFERRED_REGISTRY_POST_PROCESSOR_BEAN_NAME);
|
||||
if (pp == null) {
|
||||
pp = new DeferredRegistryPostProcessor();
|
||||
this.beanFactory.registerSingleton(DEFERRED_REGISTRY_POST_PROCESSOR_BEAN_NAME, pp);
|
||||
}
|
||||
pp.addRegistrar(registrar);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -648,4 +660,31 @@ public class GenericApplicationContext extends AbstractApplicationContext implem
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Internal post-processor for invoking DeferredBeanRegistrars at the end
|
||||
* of the BeanDefinitionRegistryPostProcessor PriorityOrdered phase,
|
||||
* right before a potential ConfigurationClassPostProcessor.
|
||||
*/
|
||||
private class DeferredRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor, PriorityOrdered {
|
||||
|
||||
private final List<BeanRegistrar> registrars = new ArrayList<>();
|
||||
|
||||
public void addRegistrar(BeanRegistrar registrar) {
|
||||
this.registrars.add(registrar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.LOWEST_PRECEDENCE - 1; // within PriorityOrdered, 1 before ConfigurationClassPostProcessor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
|
||||
for (BeanRegistrar registrar : this.registrars) {
|
||||
new BeanRegistryAdapter(beanFactory, getEnvironment(), registrar.getClass()).register(registrar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-7
@@ -60,9 +60,13 @@ import org.springframework.util.StringUtils;
|
||||
* are treated in a slightly different fashion than the "basenames" property of
|
||||
* {@link ResourceBundleMessageSource}. It follows the basic ResourceBundle rule of not
|
||||
* specifying file extension or language codes, but can refer to any Spring resource
|
||||
* location (instead of being restricted to classpath resources). With a "classpath:"
|
||||
* prefix, resources can still be loaded from the classpath, but "cacheSeconds" values
|
||||
* other than "-1" (caching forever) might not work reliably in this case.
|
||||
* location (instead of being restricted to classpath resources).
|
||||
*
|
||||
* <p>With a "classpath:" prefix, resources can still be loaded from the classpath,
|
||||
* but "cacheSeconds" values other than "-1" (caching forever) are not expected to
|
||||
* be effective in this case. As of 7.1, a "classpath*:" prefix is accepted as well,
|
||||
* loading all classpath resources of the same fully-qualified name: for example,
|
||||
* "classpath*:/messages.properties" or "classpath*:META-INF/messages.properties".
|
||||
*
|
||||
* <p>For a typical web application, message files could be placed in {@code WEB-INF}:
|
||||
* for example, a "WEB-INF/messages" basename would find a "WEB-INF/messages.properties",
|
||||
@@ -562,8 +566,8 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
*/
|
||||
protected Properties loadProperties(Resource resource, String filename) throws IOException {
|
||||
Properties props = newProperties();
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
String resourceFilename = resource.getFilename();
|
||||
String resourceFilename = resource.getFilename();
|
||||
resource.consumeContent(inputStream -> {
|
||||
if (resourceFilename != null && resourceFilename.endsWith(XML_EXTENSION)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Loading properties [" + resource.getFilename() + "]");
|
||||
@@ -594,8 +598,8 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
this.propertiesPersister.load(props, inputStream);
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
});
|
||||
return props;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+13
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.jndi.support;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -59,6 +60,7 @@ import org.springframework.jndi.TypeMismatchNamingException;
|
||||
* in particular if BeanFactory-style type checking is required.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Yanming Zhou
|
||||
* @since 2.5
|
||||
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory
|
||||
* @see org.springframework.context.annotation.CommonAnnotationBeanPostProcessor
|
||||
@@ -132,6 +134,17 @@ public class SimpleJndiBeanFactory extends JndiLocatorSupport implements BeanFac
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> T getBean(String name, ParameterizedTypeReference<T> typeReference) throws BeansException {
|
||||
Object bean = getBean(name);
|
||||
Type requiredType = typeReference.getType();
|
||||
if (!ResolvableType.forType(requiredType).isInstance(bean)) {
|
||||
throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass());
|
||||
}
|
||||
return (T) bean;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException {
|
||||
if (args != null) {
|
||||
|
||||
@@ -273,9 +273,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
* Specify the limit for array and collection auto-growing.
|
||||
* <p>Default is 256, preventing OutOfMemoryErrors in case of large indexes.
|
||||
* Raise this limit if your auto-growing needs are unusually high.
|
||||
* <p>Used for setter injection via {@link #bind(PropertyValues)};
|
||||
* not applicable to field injection, and not to constructor binding
|
||||
* via {@link #construct} either.
|
||||
* <p>Used for setter injection - and as of 7.1 also for field injection -
|
||||
* via {@link #bind(PropertyValues)}; not applicable to constructor binding
|
||||
* via {@link #construct}.
|
||||
* @see #initBeanPropertyAccess()
|
||||
* @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit
|
||||
*/
|
||||
@@ -342,7 +342,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
*/
|
||||
protected AbstractPropertyBindingResult createDirectFieldBindingResult() {
|
||||
DirectFieldBindingResult result = new DirectFieldBindingResult(getTarget(),
|
||||
getObjectName(), isAutoGrowNestedPaths());
|
||||
getObjectName(), isAutoGrowNestedPaths(), getAutoGrowCollectionLimit());
|
||||
|
||||
if (this.conversionService != null) {
|
||||
result.initConversion(this.conversionService);
|
||||
|
||||
+18
@@ -41,6 +41,8 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult {
|
||||
|
||||
private final boolean autoGrowNestedPaths;
|
||||
|
||||
private final int autoGrowCollectionLimit;
|
||||
|
||||
private transient @Nullable ConfigurablePropertyAccessor directFieldAccessor;
|
||||
|
||||
|
||||
@@ -60,9 +62,24 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult {
|
||||
* @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value
|
||||
*/
|
||||
public DirectFieldBindingResult(@Nullable Object target, String objectName, boolean autoGrowNestedPaths) {
|
||||
this(target, objectName, autoGrowNestedPaths, Integer.MAX_VALUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code DirectFieldBindingResult} for the given target.
|
||||
* @param target the target object to bind onto
|
||||
* @param objectName the name of the target object
|
||||
* @param autoGrowNestedPaths whether to "auto-grow" a nested path that contains a null value
|
||||
* @param autoGrowCollectionLimit the limit for array and collection auto-growing
|
||||
* @since 7.1
|
||||
*/
|
||||
public DirectFieldBindingResult(@Nullable Object target, String objectName,
|
||||
boolean autoGrowNestedPaths, int autoGrowCollectionLimit) {
|
||||
|
||||
super(objectName);
|
||||
this.target = target;
|
||||
this.autoGrowNestedPaths = autoGrowNestedPaths;
|
||||
this.autoGrowCollectionLimit = autoGrowCollectionLimit;
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +99,7 @@ public class DirectFieldBindingResult extends AbstractPropertyBindingResult {
|
||||
this.directFieldAccessor = createDirectFieldAccessor();
|
||||
this.directFieldAccessor.setExtractOldValueForEditor(true);
|
||||
this.directFieldAccessor.setAutoGrowNestedPaths(this.autoGrowNestedPaths);
|
||||
this.directFieldAccessor.setAutoGrowCollectionLimit(this.autoGrowCollectionLimit);
|
||||
}
|
||||
return this.directFieldAccessor;
|
||||
}
|
||||
|
||||
+81
@@ -22,8 +22,10 @@ import org.springframework.beans.factory.BeanRegistrar;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.testfixture.beans.factory.BarRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.ConditionalBeanRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.FooRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.GenericBeanRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar;
|
||||
@@ -32,17 +34,28 @@ import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar
|
||||
import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Foo;
|
||||
import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Init;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.BeanRegistrarConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ComponentBeanRegistrar;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ComponentBeanRegistrar.IgnoredFromComponent;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ConditionalBeanRegistrarConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.BeanBeanRegistrar;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromBean;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ConfigurationBeanRegistrar.IgnoredFromConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.GenericBeanRegistrarConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.ImportAwareBeanRegistrarConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.MultipleBeanRegistrarsConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.registrar.TestBeanConfiguration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link BeanRegistrar} imported by @{@link org.springframework.context.annotation.Configuration}.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class BeanRegistrarConfigurationTests {
|
||||
|
||||
@@ -59,6 +72,36 @@ class BeanRegistrarConfigurationTests {
|
||||
assertThat(beanDefinition.getDescription()).isEqualTo("Custom description");
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanRegistrarIgnoreBeans() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigurationBeanRegistrar.class);
|
||||
assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class));
|
||||
assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class));
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class));
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean(IgnoredFromBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanRegistrarWithClasspathScanningIgnoreBeans() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.scan("org.springframework.context.testfixture.context.annotation.registrar");
|
||||
context.refresh();
|
||||
|
||||
assertThatNoException().isThrownBy(() -> context.getBean(ConfigurationBeanRegistrar.class));
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean(IgnoredFromConfiguration.class));
|
||||
|
||||
assertThatNoException().isThrownBy(() -> context.getBean(BeanBeanRegistrar.class));
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean(IgnoredFromBean.class));
|
||||
|
||||
assertThatNoException().isThrownBy(() -> context.getBean(ComponentBeanRegistrar.class));
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean(IgnoredFromComponent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanRegistrarWithProfile() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
@@ -105,4 +148,42 @@ class BeanRegistrarConfigurationTests {
|
||||
assertThat(context.getBean(BarRegistrar.Bar.class)).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void programmaticBeanRegistrarIsInvokedBeforeConfigurationClassPostProcessor() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(TestBeanConfiguration.class);
|
||||
context.register(new ConditionalBeanRegistrar());
|
||||
context.refresh();
|
||||
assertThat(context.containsBean("myTestBean")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void programmaticBeanRegistrarHandlesProgrammaticRegisteredBean() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(new ConditionalBeanRegistrar());
|
||||
context.registerBean("testBean", TestBean.class);
|
||||
context.refresh();
|
||||
assertThat(context.containsBean("myTestBean")).isTrue();
|
||||
assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void importedBeanRegistrarWithConditionNotMet() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(ConditionalBeanRegistrarConfiguration.class);
|
||||
context.register(TestBeanConfiguration.class);
|
||||
context.refresh();
|
||||
assertThat(context.containsBean("myTestBean")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void importedBeanRegistrarWithConditionMet() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
context.register(TestBeanConfiguration.class);
|
||||
context.register(ConditionalBeanRegistrarConfiguration.class);
|
||||
context.refresh();
|
||||
assertThat(context.containsBean("myTestBean")).isTrue();
|
||||
assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+22
-2
@@ -46,9 +46,11 @@ import org.springframework.beans.factory.support.BeanDefinitionOverrideException
|
||||
import org.springframework.beans.factory.support.GenericBeanDefinition;
|
||||
import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.context.testfixture.beans.factory.CircularBeanRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.ConditionalBeanRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.OverridingBeanRegistrar;
|
||||
import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar;
|
||||
@@ -653,8 +655,8 @@ class GenericApplicationContextTests {
|
||||
void beanRegistrarWithDefinitionOverride() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.setAllowBeanDefinitionOverriding(false);
|
||||
assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(
|
||||
() -> context.register(new OverridingBeanRegistrar()));
|
||||
context.register(new OverridingBeanRegistrar());
|
||||
assertThatExceptionOfType(BeanDefinitionOverrideException.class).isThrownBy(context::refresh);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -665,6 +667,24 @@ class GenericApplicationContextTests {
|
||||
assertThat(context.getBean(ImportAwareBeanRegistrar.ClassNameHolder.class).className()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanRegistrarWithConditionNotMet() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.register(new ConditionalBeanRegistrar());
|
||||
context.refresh();
|
||||
assertThat(context.containsBean("myTestBean")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanRegistrarWithConditionMet() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
context.register(new ConditionalBeanRegistrar());
|
||||
context.registerBean("testBean", TestBean.class);
|
||||
context.refresh();
|
||||
assertThat(context.containsBean("myTestBean")).isTrue();
|
||||
assertThat(context.getBean("myTestBean")).isInstanceOf(TestBean.class);
|
||||
}
|
||||
|
||||
|
||||
private MergedBeanDefinitionPostProcessor registerMockMergedBeanDefinitionPostProcessor(GenericApplicationContext context) {
|
||||
MergedBeanDefinitionPostProcessor bpp = mock();
|
||||
|
||||
+33
@@ -17,10 +17,12 @@
|
||||
package org.springframework.validation;
|
||||
|
||||
import java.beans.PropertyEditorSupport;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.InvalidPropertyException;
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.NotWritablePropertyException;
|
||||
import org.springframework.beans.NullValueInNestedPathException;
|
||||
@@ -134,6 +136,25 @@ class DataBinderFieldAccessTests {
|
||||
binder.bind(pvs));
|
||||
}
|
||||
|
||||
@Test
|
||||
void directFieldAccessHonorsDefaultAutoGrowCollectionLimit() {
|
||||
FieldAccessForm target = new FieldAccessForm();
|
||||
DataBinder binder = new DataBinder(target);
|
||||
binder.initDirectFieldAccess();
|
||||
|
||||
MutablePropertyValues pvs = new MutablePropertyValues();
|
||||
pvs.add("items[255].name", "value");
|
||||
binder.bind(pvs);
|
||||
|
||||
assertThat(target.items).hasSize(256);
|
||||
assertThat(target.items.get(255).name).isEqualTo("value");
|
||||
|
||||
MutablePropertyValues outOfBounds = new MutablePropertyValues();
|
||||
outOfBounds.add("items[256].name", "too-far");
|
||||
assertThatExceptionOfType(InvalidPropertyException.class).isThrownBy(() ->
|
||||
binder.bind(outOfBounds));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindingWithErrorsAndCustomEditors() {
|
||||
FieldAccessBean rod = new FieldAccessBean();
|
||||
@@ -176,4 +197,16 @@ class DataBinderFieldAccessTests {
|
||||
assertThat(tb.getSpouse()).isNotNull();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
static class FieldAccessForm {
|
||||
|
||||
public List<FieldAccessItem> items;
|
||||
}
|
||||
|
||||
|
||||
static class FieldAccessItem {
|
||||
|
||||
public String name;
|
||||
}
|
||||
}
|
||||
|
||||
+20
@@ -84,7 +84,13 @@ class BeanRegistrarDslConfigurationTests {
|
||||
assertThat(context.getBeanProvider<Bar>().singleOrNull()).isNotNull
|
||||
}
|
||||
|
||||
@Test
|
||||
fun containsBean() {
|
||||
AnnotationConfigApplicationContext(ContainsBeanRegistrarKotlinConfiguration::class.java)
|
||||
}
|
||||
|
||||
class Foo
|
||||
|
||||
data class Bar(val foo: Foo)
|
||||
data class Baz(val message: String = "")
|
||||
class Init : InitializingBean {
|
||||
@@ -145,4 +151,18 @@ class BeanRegistrarDslConfigurationTests {
|
||||
private class ChainedBeanRegistrar : BeanRegistrarDsl({
|
||||
register(SampleBeanRegistrar())
|
||||
})
|
||||
|
||||
@Configuration
|
||||
@Import(ContainsBeanRegistrar::class)
|
||||
internal class ContainsBeanRegistrarKotlinConfiguration
|
||||
|
||||
private class ContainsBeanRegistrar : BeanRegistrarDsl({
|
||||
assertThat(containsBean("foo")).isFalse()
|
||||
assertThat(containsBean(Foo::class)).isFalse()
|
||||
assertThat(containsBean<Foo>()).isFalse()
|
||||
registerBean<Foo>("foo")
|
||||
assertThat(containsBean("foo")).isTrue()
|
||||
assertThat(containsBean(Foo::class)).isTrue()
|
||||
assertThat(containsBean<Foo>()).isTrue()
|
||||
})
|
||||
}
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.beans.factory;
|
||||
|
||||
import org.springframework.beans.factory.BeanRegistrar;
|
||||
import org.springframework.beans.factory.BeanRegistry;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
public class ConditionalBeanRegistrar implements BeanRegistrar {
|
||||
|
||||
@Override
|
||||
public void register(BeanRegistry registry, Environment env) {
|
||||
if (registry.containsBean("testBean") &&
|
||||
registry.containsBean(TestBean.class) &&
|
||||
registry.containsBean(new ParameterizedTypeReference<Comparable<Object>>() {
|
||||
})) {
|
||||
registry.registerBean("myTestBean", TestBean.class);
|
||||
}
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.context.annotation.registrar;
|
||||
|
||||
import org.springframework.beans.factory.BeanRegistrar;
|
||||
import org.springframework.beans.factory.BeanRegistry;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ComponentBeanRegistrar implements BeanRegistrar {
|
||||
|
||||
@Override
|
||||
public void register(BeanRegistry registry, Environment env) {
|
||||
registry.registerBean(IgnoredFromComponent.class);
|
||||
}
|
||||
|
||||
|
||||
public record IgnoredFromComponent() {}
|
||||
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.context.annotation.registrar;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.testfixture.beans.factory.ConditionalBeanRegistrar;
|
||||
|
||||
@Configuration
|
||||
@Import(ConditionalBeanRegistrar.class)
|
||||
public class ConditionalBeanRegistrarConfiguration {
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.context.annotation.registrar;
|
||||
|
||||
import org.springframework.beans.factory.BeanRegistrar;
|
||||
import org.springframework.beans.factory.BeanRegistry;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
@Configuration
|
||||
public class ConfigurationBeanRegistrar implements BeanRegistrar {
|
||||
|
||||
@Override
|
||||
public void register(BeanRegistry registry, Environment env) {
|
||||
registry.registerBean(IgnoredFromConfiguration.class);
|
||||
}
|
||||
|
||||
@Bean
|
||||
BeanBeanRegistrar beanBeanRegistrar() {
|
||||
return new BeanBeanRegistrar();
|
||||
}
|
||||
|
||||
public static class BeanBeanRegistrar implements BeanRegistrar {
|
||||
@Override
|
||||
public void register(BeanRegistry registry, Environment env) {
|
||||
registry.registerBean(IgnoredFromBean.class);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public record IgnoredFromConfiguration() {}
|
||||
|
||||
public record IgnoredFromBean() {}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.context.annotation.registrar;
|
||||
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class TestBeanConfiguration {
|
||||
|
||||
@Bean
|
||||
public TestBean testBean() {
|
||||
return new TestBean();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import java.util.Objects;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Reference to a Java method, identified by its owner class and the method name.
|
||||
*
|
||||
@@ -43,7 +45,7 @@ public final class MethodReference {
|
||||
}
|
||||
|
||||
public static MethodReference of(Class<?> klass, String methodName) {
|
||||
return new MethodReference(klass.getCanonicalName(), methodName);
|
||||
return new MethodReference(ClassUtils.getCanonicalName(klass), methodName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Record of an invocation of a method relevant to {@link org.springframework.aot.hint.RuntimeHints}.
|
||||
@@ -181,7 +182,7 @@ public final class RecordedInvocation {
|
||||
else {
|
||||
Class<?> instanceType = (getInstance() instanceof Class<?> clazz) ? clazz : getInstance().getClass();
|
||||
return "<%s> invocation of <%s> on type <%s> with arguments %s".formatted(
|
||||
getHintType().hintClassName(), getMethodReference(), instanceType.getCanonicalName(), getArguments());
|
||||
getHintType().hintClassName(), getMethodReference(), ClassUtils.getCanonicalName(instanceType), getArguments());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,11 @@ public class ClassNameReader {
|
||||
private static class EarlyExitException extends RuntimeException {
|
||||
}
|
||||
|
||||
// SPRING PATCH BEGIN
|
||||
public static String getClassName(ClassReader r) {
|
||||
return getClassInfo(r)[0];
|
||||
return r.getClassName().replace('/', '.');
|
||||
}
|
||||
// SPRING PATCH END
|
||||
|
||||
public static String[] getClassInfo(ClassReader r) {
|
||||
final List<String> array = new ArrayList<>();
|
||||
|
||||
@@ -160,6 +160,10 @@ public final class GenericTypeResolver {
|
||||
resolvedTypeVariable = ResolvableType.forVariableBounds(typeVariable);
|
||||
}
|
||||
if (resolvedTypeVariable != ResolvableType.NONE) {
|
||||
Type type = resolvedTypeVariable.getType();
|
||||
if (type instanceof ParameterizedType) {
|
||||
return resolveType(type, contextClass);
|
||||
}
|
||||
Class<?> resolved = resolvedTypeVariable.resolve();
|
||||
if (resolved != null) {
|
||||
return resolved;
|
||||
|
||||
@@ -22,6 +22,7 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
@@ -1152,7 +1153,6 @@ public class ResolvableType implements Serializable {
|
||||
* @see #forClassWithGenerics(Class, ResolvableType...)
|
||||
*/
|
||||
public static ResolvableType forClassWithGenerics(Class<?> clazz, Class<?>... generics) {
|
||||
Assert.notNull(clazz, "Class must not be null");
|
||||
Assert.notNull(generics, "Generics array must not be null");
|
||||
ResolvableType[] resolvableGenerics = new ResolvableType[generics.length];
|
||||
for (int i = 0; i < generics.length; i++) {
|
||||
@@ -1281,6 +1281,18 @@ public class ResolvableType implements Serializable {
|
||||
return forType(null, new FieldTypeProvider(field), owner.asVariableResolver()).getNested(nestingLevel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code ResolvableType} for the specified {@link Parameter}.
|
||||
* <p>This is a convenience factory method for scenarios where a {@code Parameter}
|
||||
* descriptor is already available.
|
||||
* @param parameter the source parameter
|
||||
* @return a {@code ResolvableType} for the specified parameter
|
||||
* @since 7.1
|
||||
*/
|
||||
public static ResolvableType forParameter(Parameter parameter) {
|
||||
return forMethodParameter(MethodParameter.forParameter(parameter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a {@code ResolvableType} for the specified {@link Constructor} parameter.
|
||||
* @param constructor the source constructor (must not be {@code null})
|
||||
@@ -1289,7 +1301,6 @@ public class ResolvableType implements Serializable {
|
||||
* @see #forConstructorParameter(Constructor, int, Class)
|
||||
*/
|
||||
public static ResolvableType forConstructorParameter(Constructor<?> constructor, int parameterIndex) {
|
||||
Assert.notNull(constructor, "Constructor must not be null");
|
||||
return forMethodParameter(new MethodParameter(constructor, parameterIndex));
|
||||
}
|
||||
|
||||
@@ -1307,7 +1318,6 @@ public class ResolvableType implements Serializable {
|
||||
public static ResolvableType forConstructorParameter(Constructor<?> constructor, int parameterIndex,
|
||||
Class<?> implementationClass) {
|
||||
|
||||
Assert.notNull(constructor, "Constructor must not be null");
|
||||
MethodParameter methodParameter = new MethodParameter(constructor, parameterIndex, implementationClass);
|
||||
return forMethodParameter(methodParameter);
|
||||
}
|
||||
@@ -1319,7 +1329,6 @@ public class ResolvableType implements Serializable {
|
||||
* @see #forMethodReturnType(Method, Class)
|
||||
*/
|
||||
public static ResolvableType forMethodReturnType(Method method) {
|
||||
Assert.notNull(method, "Method must not be null");
|
||||
return forMethodParameter(new MethodParameter(method, -1));
|
||||
}
|
||||
|
||||
@@ -1333,7 +1342,6 @@ public class ResolvableType implements Serializable {
|
||||
* @see #forMethodReturnType(Method)
|
||||
*/
|
||||
public static ResolvableType forMethodReturnType(Method method, Class<?> implementationClass) {
|
||||
Assert.notNull(method, "Method must not be null");
|
||||
MethodParameter methodParameter = new MethodParameter(method, -1, implementationClass);
|
||||
return forMethodParameter(methodParameter);
|
||||
}
|
||||
@@ -1347,7 +1355,6 @@ public class ResolvableType implements Serializable {
|
||||
* @see #forMethodParameter(MethodParameter)
|
||||
*/
|
||||
public static ResolvableType forMethodParameter(Method method, int parameterIndex) {
|
||||
Assert.notNull(method, "Method must not be null");
|
||||
return forMethodParameter(new MethodParameter(method, parameterIndex));
|
||||
}
|
||||
|
||||
@@ -1363,7 +1370,6 @@ public class ResolvableType implements Serializable {
|
||||
* @see #forMethodParameter(MethodParameter)
|
||||
*/
|
||||
public static ResolvableType forMethodParameter(Method method, int parameterIndex, Class<?> implementationClass) {
|
||||
Assert.notNull(method, "Method must not be null");
|
||||
MethodParameter methodParameter = new MethodParameter(method, parameterIndex, implementationClass);
|
||||
return forMethodParameter(methodParameter);
|
||||
}
|
||||
|
||||
+2
-1
@@ -24,6 +24,7 @@ import java.util.function.Predicate;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Abstract base class for {@link MergedAnnotation} implementations.
|
||||
@@ -215,7 +216,7 @@ abstract class AbstractMergedAnnotation<A extends Annotation> implements MergedA
|
||||
T value = getAttributeValue(attributeName, type);
|
||||
if (value == null) {
|
||||
throw new NoSuchElementException("No attribute named '" + attributeName +
|
||||
"' present in merged annotation " + getType().getName());
|
||||
"' present in merged annotation " + ClassUtils.getCanonicalName(getType()));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
+2
-1
@@ -25,6 +25,7 @@ import java.util.Map;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -126,7 +127,7 @@ public class AnnotationAttributes extends LinkedHashMap<String, @Nullable Object
|
||||
AnnotationAttributes(Class<? extends Annotation> annotationType, boolean validated) {
|
||||
Assert.notNull(annotationType, "'annotationType' must not be null");
|
||||
this.annotationType = annotationType;
|
||||
this.displayName = annotationType.getName();
|
||||
this.displayName = ClassUtils.getCanonicalName(annotationType);
|
||||
this.validated = validated;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -32,6 +32,7 @@ import java.util.Set;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@@ -649,7 +650,7 @@ final class AnnotationTypeMapping {
|
||||
throw new AnnotationConfigurationException(String.format(
|
||||
"Different @AliasFor mirror values for annotation [%s]%s; attribute '%s' " +
|
||||
"and its alias '%s' are declared with values of [%s] and [%s].",
|
||||
getAnnotationType().getName(), on,
|
||||
ClassUtils.getCanonicalName(getAnnotationType()), on,
|
||||
attributes.get(result).getName(),
|
||||
attribute.getName(),
|
||||
ObjectUtils.nullSafeToString(lastValue),
|
||||
|
||||
+3
-1
@@ -28,6 +28,7 @@ import java.util.Set;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
@@ -124,7 +125,8 @@ final class AnnotationTypeMappings {
|
||||
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
|
||||
if (failureLogger.isEnabled()) {
|
||||
failureLogger.log("Failed to introspect " + (meta ? "meta-annotation @" : "annotation @") +
|
||||
annotationType.getName(), (source != null ? source.getAnnotationType() : null), ex);
|
||||
ClassUtils.getCanonicalName(annotationType),
|
||||
(source != null ? ClassUtils.getCanonicalName(source.getAnnotationType()) : null), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet;
|
||||
import org.springframework.core.annotation.MergedAnnotation.Adapt;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -177,6 +178,23 @@ public abstract class AnnotationUtils {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the specified annotation type, if available.
|
||||
* @param annotationName the fully-qualified name of the annotation type
|
||||
* @return the annotation type as a {@code Class}, or {@code null} if not found
|
||||
* @since 7.1
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static @Nullable Class<? extends Annotation> loadAnnotationType(String annotationName) {
|
||||
try {
|
||||
return (Class<? extends Annotation>)
|
||||
ClassUtils.forName(annotationName, AnnotationUtils.class.getClassLoader());
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single {@link Annotation} of {@code annotationType} from the supplied
|
||||
* annotation: either the given annotation itself or a direct meta-annotation
|
||||
|
||||
@@ -26,6 +26,7 @@ import java.util.Map;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
@@ -143,8 +144,9 @@ final class AttributeMethods {
|
||||
throw ex;
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new IllegalStateException("Could not obtain annotation attribute value for " +
|
||||
get(i).getName() + " declared on @" + getName(annotation.annotationType()), ex);
|
||||
throw new IllegalStateException(
|
||||
"Could not obtain annotation attribute value for " + get(i).getName() +
|
||||
" declared on @" + ClassUtils.getCanonicalName(annotation.annotationType()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -305,13 +307,8 @@ final class AttributeMethods {
|
||||
if (attributeName == null) {
|
||||
return "(none)";
|
||||
}
|
||||
String in = (annotationType != null ? " in annotation [" + annotationType.getName() + "]" : "");
|
||||
String in = (annotationType != null ? " in annotation [" + ClassUtils.getCanonicalName(annotationType) + "]" : "");
|
||||
return "attribute '" + attributeName + "'" + in;
|
||||
}
|
||||
|
||||
private static String getName(Class<?> clazz) {
|
||||
String canonicalName = clazz.getCanonicalName();
|
||||
return (canonicalName != null ? canonicalName : clazz.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-3
@@ -26,6 +26,7 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.lang.Contract;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@@ -312,7 +313,7 @@ public abstract class RepeatableContainers {
|
||||
if (returnType.componentType() != repeatable) {
|
||||
throw new AnnotationConfigurationException(
|
||||
"Container type [%s] must declare a 'value' attribute for an array of type [%s]"
|
||||
.formatted(container.getName(), repeatable.getName()));
|
||||
.formatted(ClassUtils.getCanonicalName(container), ClassUtils.getCanonicalName(repeatable)));
|
||||
}
|
||||
}
|
||||
catch (AnnotationConfigurationException ex) {
|
||||
@@ -321,7 +322,7 @@ public abstract class RepeatableContainers {
|
||||
catch (Throwable ex) {
|
||||
throw new AnnotationConfigurationException(
|
||||
"Invalid declaration of container type [%s] for repeatable annotation [%s]"
|
||||
.formatted(container.getName(), repeatable.getName()), ex);
|
||||
.formatted(ClassUtils.getCanonicalName(container), ClassUtils.getCanonicalName(repeatable)), ex);
|
||||
}
|
||||
this.repeatable = repeatable;
|
||||
this.container = container;
|
||||
@@ -331,7 +332,7 @@ public abstract class RepeatableContainers {
|
||||
private Class<? extends Annotation> deduceContainer(Class<? extends Annotation> repeatable) {
|
||||
Repeatable annotation = repeatable.getAnnotation(Repeatable.class);
|
||||
Assert.notNull(annotation, () -> "Annotation type must be a repeatable annotation: " +
|
||||
"failed to resolve container type for " + repeatable.getName());
|
||||
"failed to resolve container type for " + ClassUtils.getCanonicalName(repeatable));
|
||||
return annotation.value();
|
||||
}
|
||||
|
||||
|
||||
+3
-8
@@ -136,7 +136,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
|
||||
private String annotationToString() {
|
||||
String string = this.string;
|
||||
if (string == null) {
|
||||
StringBuilder builder = new StringBuilder("@").append(getName(this.type)).append('(');
|
||||
StringBuilder builder = new StringBuilder("@").append(ClassUtils.getCanonicalName(this.type)).append('(');
|
||||
if (this.attributes.size() == 1 && this.attributes.get(0).getName().equals(MergedAnnotation.VALUE)) {
|
||||
// Don't prepend "value=" for an annotation that only declares a "value" attribute.
|
||||
builder.append(toString(getAttributeValue(this.attributes.get(0))));
|
||||
@@ -208,7 +208,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
|
||||
return e.name();
|
||||
}
|
||||
if (type == Class.class) {
|
||||
return getName((Class<?>) value) + ".class";
|
||||
return ClassUtils.getCanonicalName((Class<?>) value) + ".class";
|
||||
}
|
||||
return String.valueOf(value);
|
||||
}
|
||||
@@ -218,7 +218,7 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
|
||||
Class<?> type = ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType());
|
||||
return this.annotation.getValue(attributeName, type).orElseThrow(
|
||||
() -> new NoSuchElementException("No value found for attribute named '" + attributeName +
|
||||
"' in merged annotation " + getName(this.annotation.getType())));
|
||||
"' in merged annotation " + ClassUtils.getCanonicalName(this.annotation.getType())));
|
||||
});
|
||||
|
||||
// Clone non-empty arrays so that users cannot alter the contents of values in our cache.
|
||||
@@ -272,9 +272,4 @@ final class SynthesizedMergedAnnotationInvocationHandler<A extends Annotation> i
|
||||
return (A) Proxy.newProxyInstance(classLoader, interfaces, handler);
|
||||
}
|
||||
|
||||
private static String getName(Class<?> clazz) {
|
||||
String canonicalName = clazz.getCanonicalName();
|
||||
return (canonicalName != null ? canonicalName : clazz.getName());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
-8
@@ -450,7 +450,12 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
|
||||
value = clazz.getName();
|
||||
}
|
||||
else if (value instanceof String str && type == Class.class) {
|
||||
value = ClassUtils.resolveClassName(str, getClassLoader());
|
||||
try {
|
||||
value = ClassUtils.forName(str, getClassLoader());
|
||||
}
|
||||
catch (ClassNotFoundException | LinkageError ex) {
|
||||
throw new TypeNotPresentException(str, ex);
|
||||
}
|
||||
}
|
||||
else if (value instanceof Class<?>[] classes && type == String[].class) {
|
||||
String[] names = new String[classes.length];
|
||||
@@ -461,8 +466,14 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
|
||||
}
|
||||
else if (value instanceof String[] names && type == Class[].class) {
|
||||
Class<?>[] classes = new Class<?>[names.length];
|
||||
ClassLoader classLoader = getClassLoader();
|
||||
for (int i = 0; i < names.length; i++) {
|
||||
classes[i] = ClassUtils.resolveClassName(names[i], getClassLoader());
|
||||
try {
|
||||
classes[i] = ClassUtils.forName(names[i], classLoader);
|
||||
}
|
||||
catch (ClassNotFoundException | LinkageError ex) {
|
||||
throw new TypeNotPresentException(names[i], ex);
|
||||
}
|
||||
}
|
||||
value = classes;
|
||||
}
|
||||
@@ -479,7 +490,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
|
||||
}
|
||||
if (!type.isInstance(value)) {
|
||||
throw new IllegalArgumentException("Unable to adapt value of type " +
|
||||
value.getClass().getName() + " to " + type.getName());
|
||||
ClassUtils.getCanonicalName(value.getClass()) + " to " + ClassUtils.getCanonicalName(type));
|
||||
}
|
||||
return (T) value;
|
||||
}
|
||||
@@ -514,8 +525,8 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
|
||||
}
|
||||
if (!attributeType.isInstance(value)) {
|
||||
throw new IllegalStateException("Attribute '" + attribute.getName() +
|
||||
"' in annotation " + getType().getName() + " should be compatible with " +
|
||||
attributeType.getName() + " but a " + value.getClass().getName() +
|
||||
"' in annotation " + ClassUtils.getCanonicalName(getType()) + " should be compatible with " +
|
||||
ClassUtils.getCanonicalName(attributeType) + " but a " + ClassUtils.getCanonicalName(value.getClass()) +
|
||||
" value was returned");
|
||||
}
|
||||
return value;
|
||||
@@ -572,7 +583,7 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
|
||||
int attributeIndex = (isFiltered(attributeName) ? -1 : this.mapping.getAttributes().indexOf(attributeName));
|
||||
if (attributeIndex == -1 && required) {
|
||||
throw new NoSuchElementException("No attribute named '" + attributeName +
|
||||
"' present in merged annotation " + getType().getName());
|
||||
"' present in merged annotation " + ClassUtils.getCanonicalName(getType()));
|
||||
}
|
||||
return attributeIndex;
|
||||
}
|
||||
@@ -649,9 +660,10 @@ final class TypeMappedAnnotation<A extends Annotation> extends AbstractMergedAnn
|
||||
catch (Exception ex) {
|
||||
AnnotationUtils.rethrowAnnotationConfigurationException(ex);
|
||||
if (logger.isEnabled()) {
|
||||
String type = mapping.getAnnotationType().getName();
|
||||
String type = ClassUtils.getCanonicalName(mapping.getAnnotationType());
|
||||
String item = (mapping.getDistance() == 0 ? "annotation " + type :
|
||||
"meta-annotation " + type + " from " + mapping.getRoot().getAnnotationType().getName());
|
||||
"meta-annotation " + type + " from " +
|
||||
ClassUtils.getCanonicalName(mapping.getRoot().getAnnotationType()));
|
||||
logger.log("Failed to introspect " + item, source, ex);
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -541,7 +541,7 @@ public class TypeDescriptor implements Serializable {
|
||||
public String toString() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (Annotation ann : getAnnotations()) {
|
||||
builder.append('@').append(getName(ann.annotationType())).append(' ');
|
||||
builder.append('@').append(ClassUtils.getCanonicalName(ann.annotationType())).append(' ');
|
||||
}
|
||||
builder.append(getResolvableType());
|
||||
return builder.toString();
|
||||
@@ -726,11 +726,6 @@ public class TypeDescriptor implements Serializable {
|
||||
return new TypeDescriptor(property).nested(nestingLevel);
|
||||
}
|
||||
|
||||
private static String getName(Class<?> clazz) {
|
||||
String canonicalName = clazz.getCanonicalName();
|
||||
return (canonicalName != null ? canonicalName : clazz.getName());
|
||||
}
|
||||
|
||||
|
||||
private interface AnnotatedElementSupplier extends Supplier<AnnotatedElementAdapter>, Serializable {
|
||||
}
|
||||
|
||||
@@ -88,13 +88,10 @@ final class ProfilesParser {
|
||||
}
|
||||
case "!" -> elements.add(not(parseTokens(expression, tokens, Context.NEGATE)));
|
||||
case ")" -> {
|
||||
Profiles merged = merge(expression, elements, operator);
|
||||
if (context == Context.PARENTHESIS) {
|
||||
return merged;
|
||||
return merge(expression, elements, operator);
|
||||
}
|
||||
elements.clear();
|
||||
elements.add(merged);
|
||||
operator = null;
|
||||
assertWellFormed(expression, false);
|
||||
}
|
||||
default -> {
|
||||
Profiles value = equals(token);
|
||||
@@ -105,6 +102,7 @@ final class ProfilesParser {
|
||||
}
|
||||
}
|
||||
}
|
||||
assertWellFormed(expression, context != Context.PARENTHESIS);
|
||||
return merge(expression, elements, operator);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,10 +16,19 @@
|
||||
|
||||
package org.springframework.core.io;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.SequenceInputStream;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -30,6 +39,7 @@ import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ResourceUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.function.IOConsumer;
|
||||
|
||||
/**
|
||||
* Default implementation of the {@link ResourceLoader} interface.
|
||||
@@ -158,6 +168,9 @@ public class DefaultResourceLoader implements ResourceLoader {
|
||||
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
|
||||
return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
|
||||
}
|
||||
else if (location.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
|
||||
return new ClassPathAllResource(location.substring(CLASSPATH_ALL_URL_PREFIX.length()), getClassLoader());
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// Try to parse the location as a URL...
|
||||
@@ -187,6 +200,96 @@ public class DefaultResourceLoader implements ResourceLoader {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A multi-content ClassPathResource handle that can expose the content
|
||||
* from all matching resources in the classpath.
|
||||
* @since 7.1
|
||||
*/
|
||||
protected static class ClassPathAllResource extends ClassPathResource {
|
||||
|
||||
public ClassPathAllResource(String path, @Nullable ClassLoader classLoader) {
|
||||
super(path, classLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFile() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URL getURL() throws IOException {
|
||||
throw new FileNotFoundException(
|
||||
getDescription() + " cannot be resolved to single URL or File - use 'classpath:' instead");
|
||||
}
|
||||
|
||||
@Override
|
||||
public long contentLength() throws IOException {
|
||||
long combinedLength = 0;
|
||||
ClassLoader cl = getClassLoader();
|
||||
Enumeration<URL> urls = (cl != null ? cl.getResources(getPath()) : ClassLoader.getSystemResources(getPath()));
|
||||
while (urls.hasMoreElements()) {
|
||||
URLConnection con = urls.nextElement().openConnection();
|
||||
long length = con.getContentLengthLong();
|
||||
if (length < 0) {
|
||||
return -1;
|
||||
}
|
||||
combinedLength += length;
|
||||
}
|
||||
return combinedLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
List<InputStream> streams = new ArrayList<>();
|
||||
ClassLoader cl = getClassLoader();
|
||||
Enumeration<URL> urls = (cl != null ? cl.getResources(getPath()) : ClassLoader.getSystemResources(getPath()));
|
||||
while (urls.hasMoreElements()) {
|
||||
try {
|
||||
streams.add(urls.nextElement().openStream());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
streams.forEach(stream -> {
|
||||
try {
|
||||
stream.close();
|
||||
}
|
||||
catch (IOException ex2) {
|
||||
ex.addSuppressed(ex2);
|
||||
}
|
||||
});
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
return switch (streams.size()) {
|
||||
case 0 -> InputStream.nullInputStream();
|
||||
case 1 -> streams.get(0);
|
||||
default -> new SequenceInputStream(Collections.enumeration(streams));
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void consumeContent(IOConsumer<InputStream> consumer) throws IOException {
|
||||
ClassLoader cl = getClassLoader();
|
||||
Enumeration<URL> urls = (cl != null ? cl.getResources(getPath()) : ClassLoader.getSystemResources(getPath()));
|
||||
while (urls.hasMoreElements()) {
|
||||
try (InputStream inputStream = urls.nextElement().openStream()) {
|
||||
consumer.accept(inputStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Resource createRelative(String relativePath) {
|
||||
String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
|
||||
return new ClassPathAllResource(pathToUse, getClassLoader());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return "'classpath*:' resource [" + getPath() + "]";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ClassPathResource that explicitly expresses a context-relative path
|
||||
* through implementing the ContextResource interface.
|
||||
|
||||
@@ -30,6 +30,7 @@ import java.nio.file.Path;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.function.IOConsumer;
|
||||
|
||||
/**
|
||||
* Interface for a resource descriptor that abstracts from the actual
|
||||
@@ -156,6 +157,26 @@ public interface Resource extends InputStreamSource {
|
||||
return Channels.newChannel(getInputStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the contents of this resource through the given consumer callback.
|
||||
* <p>The given consumer will be invoked a single time by default - but may
|
||||
* also be invoked multiple times in case of a multi-content resource handle,
|
||||
* for example returned from a
|
||||
* {@link ResourceLoader#getResource getResource("classpath*:...")} call.
|
||||
* While {@link #getInputStream()} returns a merged sequence of content
|
||||
* in such a case, this method performs one callback per file content.
|
||||
* @param consumer a consumer for each InputStream
|
||||
* @throws IOException in case of general resolution/reading failures
|
||||
* @since 7.1
|
||||
* @see #getInputStream()
|
||||
* @see ResourceLoader#CLASSPATH_ALL_URL_PREFIX
|
||||
*/
|
||||
default void consumeContent(IOConsumer<InputStream> consumer) throws IOException {
|
||||
try (InputStream inputStream = getInputStream()) {
|
||||
consumer.accept(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the contents of this resource as a byte array.
|
||||
* @return the contents of this resource as byte array
|
||||
|
||||
@@ -42,18 +42,47 @@ import org.springframework.util.ResourceUtils;
|
||||
*/
|
||||
public interface ResourceLoader {
|
||||
|
||||
/** Pseudo URL prefix for loading from the class path: "classpath:". */
|
||||
/**
|
||||
* Pseudo URL prefix for loading from the class path: {@value}.
|
||||
* <p>This retrieves the "nearest" matching resource in the classpath.
|
||||
* @see ClassLoader#getResource
|
||||
*/
|
||||
String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;
|
||||
|
||||
/**
|
||||
* Pseudo URL prefix for all matching resources from the class path: {@value}.
|
||||
* <p>This differs from the common {@link #CLASSPATH_URL_PREFIX "classpath:"} prefix
|
||||
* in that it retrieves all matching resources for a given path. For example, to
|
||||
* locate all "messages.properties" files in the root of all deployed JAR files
|
||||
* you can use the location pattern {@code "classpath*:/messages.properties"}.
|
||||
* <p>As of Spring Framework 6.0, the semantics for the {@code "classpath*:"}
|
||||
* prefix have been expanded to include the module path as well as the class path.
|
||||
* <p>As of Spring Framework 7.1, this prefix is supported for {@link #getResource}
|
||||
* calls as well (exposing a multi-content resource handle), rather than just for
|
||||
* {@link org.springframework.core.io.support.ResourcePatternResolver#getResources}.
|
||||
* @since 7.1 (previously only declared on the
|
||||
* {@link org.springframework.core.io.support.ResourcePatternResolver} sub-interface)
|
||||
* @see ClassLoader#getResources
|
||||
* @see Resource#consumeContent
|
||||
*/
|
||||
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
|
||||
|
||||
|
||||
/**
|
||||
* Return a {@code Resource} handle for the specified resource location.
|
||||
* <p>The handle should always be a reusable resource descriptor,
|
||||
* allowing for multiple {@link Resource#getInputStream()} calls.
|
||||
* <p><ul>
|
||||
* <li>Must support fully qualified URLs, for example, "file:C:/test.dat".
|
||||
* <li>Must support classpath pseudo-URLs, for example, "classpath:test.dat".
|
||||
* <li>Should support relative file paths, for example, "WEB-INF/test.dat".
|
||||
* <ul>
|
||||
* <li>Must support fully qualified URLs, for example, "file:C:/test.properties".
|
||||
* <li>Must support classpath pseudo-URLs, for example, "classpath:test.properties".
|
||||
* (Exposing the "nearest" resource in the classpath; see {@link ClassLoader#getResource}.)
|
||||
* <li>Should support classpath-all URLs, for example, "classpath*:test.properties".
|
||||
* (If supported, the returned {@code Resource} needs to expose the entire content of
|
||||
* all same-named resources in the classpath through {@link Resource#consumeContent};
|
||||
* see {@link ClassLoader#getResources}.
|
||||
* For individual access to each such matching resource in the classpath, use
|
||||
* {@link org.springframework.core.io.support.ResourcePatternResolver#getResources}.)
|
||||
* <li>Should support relative file paths, for example, "WEB-INF/test.properties".
|
||||
* (This will be implementation-specific, typically provided by an
|
||||
* ApplicationContext implementation.)
|
||||
* </ul>
|
||||
@@ -62,8 +91,9 @@ public interface ResourceLoader {
|
||||
* @param location the resource location
|
||||
* @return a corresponding {@code Resource} handle (never {@code null})
|
||||
* @see #CLASSPATH_URL_PREFIX
|
||||
* @see #CLASSPATH_ALL_URL_PREFIX
|
||||
* @see Resource#exists()
|
||||
* @see Resource#getInputStream()
|
||||
* @see Resource#consumeContent
|
||||
*/
|
||||
Resource getResource(String location);
|
||||
|
||||
|
||||
+43
-20
@@ -26,8 +26,10 @@ import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.io.InputStreamSource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.function.IOConsumer;
|
||||
|
||||
/**
|
||||
* Holder that combines a {@link Resource} descriptor with a specific encoding
|
||||
@@ -125,26 +127,6 @@ public class EncodedResource implements InputStreamSource {
|
||||
return (this.encoding != null || this.charset != null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a {@code java.io.Reader} for the specified resource, using the specified
|
||||
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
|
||||
* (if any).
|
||||
* @throws IOException if opening the Reader failed
|
||||
* @see #requiresReader()
|
||||
* @see #getInputStream()
|
||||
*/
|
||||
public Reader getReader() throws IOException {
|
||||
if (this.charset != null) {
|
||||
return new InputStreamReader(this.resource.getInputStream(), this.charset);
|
||||
}
|
||||
else if (this.encoding != null) {
|
||||
return new InputStreamReader(this.resource.getInputStream(), this.encoding);
|
||||
}
|
||||
else {
|
||||
return new InputStreamReader(this.resource.getInputStream());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Open an {@code InputStream} for the specified resource, ignoring any specified
|
||||
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}.
|
||||
@@ -157,6 +139,47 @@ public class EncodedResource implements InputStreamSource {
|
||||
return this.resource.getInputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a {@code java.io.Reader} for the specified resource, using the specified
|
||||
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding}
|
||||
* (if any).
|
||||
* @throws IOException if opening the Reader failed
|
||||
* @see #requiresReader()
|
||||
* @see #getInputStream()
|
||||
*/
|
||||
public Reader getReader() throws IOException {
|
||||
return getReader(this.resource.getInputStream());
|
||||
}
|
||||
|
||||
private Reader getReader(InputStream inputStream) throws IOException {
|
||||
if (this.charset != null) {
|
||||
return new InputStreamReader(inputStream, this.charset);
|
||||
}
|
||||
else if (this.encoding != null) {
|
||||
return new InputStreamReader(inputStream, this.encoding);
|
||||
}
|
||||
else {
|
||||
return new InputStreamReader(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the contents of this resource through the given consumer callback.
|
||||
* <p>The given consumer will be invoked a single time by default - but may
|
||||
* also be invoked multiple times in case of a multi-content resource handle,
|
||||
* for example returned from a
|
||||
* {@link ResourceLoader#getResource getResource("classpath*:...")} call.
|
||||
* While {@link #getReader()} returns a merged sequence of content
|
||||
* in such a case, this method performs one callback per file content.
|
||||
* @param consumer a consumer for each Reader
|
||||
* @throws IOException in case of general resolution/reading failures
|
||||
* @since 7.1
|
||||
* @see Resource#consumeContent
|
||||
*/
|
||||
public void consumeContent(IOConsumer<Reader> consumer) throws IOException {
|
||||
this.resource.consumeContent(inputStream -> consumer.accept(getReader(inputStream)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the contents of the specified resource as a string, using the specified
|
||||
* {@link #getCharset() Charset} or {@linkplain #getEncoding() encoding} (if any).
|
||||
|
||||
-12
@@ -57,18 +57,6 @@ import org.springframework.core.io.ResourceLoader;
|
||||
*/
|
||||
public interface ResourcePatternResolver extends ResourceLoader {
|
||||
|
||||
/**
|
||||
* Pseudo URL prefix for all matching resources from the class path: {@code "classpath*:"}.
|
||||
* <p>This differs from ResourceLoader's {@code "classpath:"} URL prefix in
|
||||
* that it retrieves all matching resources for a given path — for
|
||||
* example, to locate all "beans.xml" files in the root of all deployed JAR
|
||||
* files you can use the location pattern {@code "classpath*:/beans.xml"}.
|
||||
* <p>As of Spring Framework 6.0, the semantics for the {@code "classpath*:"}
|
||||
* prefix have been expanded to include the module path as well as the class path.
|
||||
* @see org.springframework.core.io.ResourceLoader#CLASSPATH_URL_PREFIX
|
||||
*/
|
||||
String CLASSPATH_ALL_URL_PREFIX = "classpath*:";
|
||||
|
||||
/**
|
||||
* Resolve the given location pattern into {@code Resource} objects.
|
||||
* <p>Overlapping resource entries that point to the same physical
|
||||
|
||||
@@ -23,6 +23,7 @@ import java.util.function.Predicate;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ExceptionTypeFilter;
|
||||
import org.springframework.util.backoff.BackOff;
|
||||
|
||||
@@ -96,8 +97,7 @@ class DefaultRetryPolicy implements RetryPolicy {
|
||||
private static String names(Set<Class<? extends Throwable>> types) {
|
||||
StringJoiner result = new StringJoiner(", ", "[", "]");
|
||||
for (Class<? extends Throwable> type : types) {
|
||||
String name = type.getCanonicalName();
|
||||
result.add(name != null? name : type.getName());
|
||||
result.add(ClassUtils.getCanonicalName(type));
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import java.util.StringJoiner;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* {@link ValueStyler} that converts objects to String form — generally for
|
||||
* debugging purposes — using simple styling conventions that mimic the
|
||||
@@ -45,7 +47,7 @@ public class SimpleValueStyler extends DefaultValueStyler {
|
||||
/**
|
||||
* Default {@link Class} styling function: {@link Class#getCanonicalName()}.
|
||||
*/
|
||||
public static final Function<Class<?>, String> DEFAULT_CLASS_STYLER = Class::getCanonicalName;
|
||||
public static final Function<Class<?>, String> DEFAULT_CLASS_STYLER = ClassUtils::getCanonicalName;
|
||||
|
||||
/**
|
||||
* Default {@link Method} styling function: converts the supplied {@link Method}
|
||||
|
||||
@@ -54,6 +54,7 @@ public interface MethodMetadata extends AnnotatedTypeMetadata {
|
||||
/**
|
||||
* Get the fully-qualified name of the underlying method's declared return type.
|
||||
* @since 4.2
|
||||
* @see Class#getTypeName()
|
||||
*/
|
||||
String getReturnTypeName();
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ public class StandardMethodMetadata implements MethodMetadata {
|
||||
|
||||
@Override
|
||||
public String getReturnTypeName() {
|
||||
return this.introspectedMethod.getReturnType().getName();
|
||||
return this.introspectedMethod.getReturnType().getTypeName();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+14
-2
@@ -101,7 +101,13 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
|
||||
@SuppressWarnings("unchecked")
|
||||
public <E extends Enum<E>> void visitEnum(String descriptor, String value, Consumer<E> consumer) {
|
||||
String className = Type.getType(descriptor).getClassName();
|
||||
Class<E> type = (Class<E>) ClassUtils.resolveClassName(className, this.classLoader);
|
||||
Class<E> type = null;
|
||||
try {
|
||||
type = (Class<E>) ClassUtils.forName(className, this.classLoader);
|
||||
}
|
||||
catch (ClassNotFoundException | LinkageError ex) {
|
||||
throw new TypeNotPresentException(className, ex);
|
||||
}
|
||||
consumer.accept(Enum.valueOf(type, value));
|
||||
}
|
||||
|
||||
@@ -113,7 +119,13 @@ class MergedAnnotationReadingVisitor<A extends Annotation> extends AnnotationVis
|
||||
if (AnnotationFilter.PLAIN.matches(className)) {
|
||||
return null;
|
||||
}
|
||||
Class<T> type = (Class<T>) ClassUtils.resolveClassName(className, this.classLoader);
|
||||
Class<T> type = null;
|
||||
try {
|
||||
type = (Class<T>) ClassUtils.forName(className, this.classLoader);
|
||||
}
|
||||
catch (ClassNotFoundException | LinkageError ex) {
|
||||
throw new TypeNotPresentException(className, ex);
|
||||
}
|
||||
return new MergedAnnotationReadingVisitor<>(this.classLoader, this.source, type, consumer);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -16,7 +16,9 @@
|
||||
|
||||
package org.springframework.core.type.classreading;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -57,7 +59,7 @@ final class SimpleAnnotationMetadataReadingVisitor extends ClassVisitor {
|
||||
|
||||
private final Set<String> memberClassNames = new LinkedHashSet<>(4);
|
||||
|
||||
private final Set<MergedAnnotation<?>> annotations = new LinkedHashSet<>(4);
|
||||
private final List<MergedAnnotation<?>> annotations = new ArrayList<>(4);
|
||||
|
||||
private final Set<MethodMetadata> declaredMethods = new LinkedHashSet<>(4);
|
||||
|
||||
|
||||
@@ -1131,11 +1131,26 @@ public abstract class ClassUtils {
|
||||
return (lastDotIndex != -1 ? fqClassName.substring(0, lastDotIndex) : "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@linkplain Class#getCanonicalName() canonical name} of the given
|
||||
* class, or the {@linkplain Class#getTypeName() type name} of the class if the
|
||||
* canonical name does not exist.
|
||||
* @param clazz the class
|
||||
* @return the canonical name of the class, or the type name as a fallback
|
||||
* @since 7.1
|
||||
*/
|
||||
public static String getCanonicalName(Class<?> clazz) {
|
||||
Assert.notNull(clazz, "Class must not be null");
|
||||
String canonicalName = clazz.getCanonicalName();
|
||||
return (canonicalName != null ? canonicalName : clazz.getTypeName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the qualified name of the given class: usually simply
|
||||
* the class name, but component type class name + "[]" for arrays.
|
||||
* @param clazz the class
|
||||
* @return the qualified name of the class
|
||||
* @see Class#getTypeName()
|
||||
*/
|
||||
public static String getQualifiedName(Class<?> clazz) {
|
||||
Assert.notNull(clazz, "Class must not be null");
|
||||
|
||||
@@ -367,7 +367,7 @@ public final class ConcurrentLruCache<K, V> {
|
||||
|
||||
private static final class ReadOperations<K, V> {
|
||||
|
||||
private static final int BUFFER_COUNT = detectNumberOfBuffers();
|
||||
private static final int BUFFER_COUNT = 4;
|
||||
|
||||
private static final int BUFFERS_MASK = BUFFER_COUNT - 1;
|
||||
|
||||
@@ -450,11 +450,6 @@ public final class ConcurrentLruCache<K, V> {
|
||||
this.processedCount.lazySet(bufferIndex, writeCount);
|
||||
}
|
||||
|
||||
private static int detectNumberOfBuffers() {
|
||||
int availableProcessors = Runtime.getRuntime().availableProcessors();
|
||||
int nextPowerOfTwo = 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(availableProcessors - 1));
|
||||
return Math.min(4, nextPowerOfTwo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -252,7 +252,9 @@ public abstract class MimeTypeUtils {
|
||||
if (eqIndex >= 0) {
|
||||
String attribute = parameter.substring(0, eqIndex).trim();
|
||||
String value = parameter.substring(eqIndex + 1).trim();
|
||||
parameters.put(attribute, value);
|
||||
if (parameters.put(attribute, value) != null) {
|
||||
throw new InvalidMimeTypeException(mimeType, "duplicate parameter '" + parameter + "'");
|
||||
}
|
||||
}
|
||||
}
|
||||
index = nextIndex;
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.util.function;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.springframework.core.io.Resource;
|
||||
|
||||
/**
|
||||
* Common functional interface for I/O content consumption, for example
|
||||
* consuming an {@link java.io.InputStream} or a {@link java.io.Reader}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 7.1
|
||||
* @param <C> the type of stream/reader
|
||||
* @see Resource#consumeContent
|
||||
* @see org.springframework.core.io.support.EncodedResource#consumeContent
|
||||
* @see ThrowingConsumer
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface IOConsumer<C> extends Consumer<C> {
|
||||
|
||||
/**
|
||||
* Performs this operation on the given argument, possibly throwing
|
||||
* an {@link IOException}.
|
||||
* @param content the stream/reader
|
||||
* @throws IOException on error
|
||||
*/
|
||||
void acceptWithException(C content) throws IOException;
|
||||
|
||||
/**
|
||||
* Default {@link Consumer#accept(Object)} that wraps any thrown
|
||||
* {@link IOException} in an {@link UncheckedIOException}.
|
||||
* @see java.util.function.Consumer#accept(Object)
|
||||
*/
|
||||
default void accept(C input) {
|
||||
try {
|
||||
acceptWithException(input);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+6
-1
@@ -139,7 +139,12 @@ abstract class ClassFileAnnotationDelegate {
|
||||
|
||||
private static Class<?> loadEnumClass(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) {
|
||||
String className = ClassFileAnnotationMetadata.resolveTypeName(enumValue.classSymbol());
|
||||
return ClassUtils.resolveClassName(className, classLoader);
|
||||
try {
|
||||
return ClassUtils.forName(className, classLoader);
|
||||
}
|
||||
catch (ClassNotFoundException | LinkageError ex) {
|
||||
throw new TypeNotPresentException(className, ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?> resolveArrayElementType(List<AnnotationValue> values, @Nullable ClassLoader classLoader) {
|
||||
|
||||
+1
-1
@@ -255,7 +255,7 @@ final class ClassFileAnnotationMetadata implements AnnotationMetadata {
|
||||
|
||||
private Set<MethodMetadata> declaredMethods = new LinkedHashSet<>(4);
|
||||
|
||||
private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet());
|
||||
private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptyList());
|
||||
|
||||
public Builder(ClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -250,6 +251,15 @@ class GenericTypeResolverTests {
|
||||
assertThat(resolvedType).isEqualTo(InheritsDefaultMethod.ConcreteType.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveTypeFromNestedParameterizedType() {
|
||||
Type resolvedType = resolveType(method(MyInterfaceType.class, "get").getGenericReturnType(), MyCollectionInterfaceType.class);
|
||||
assertThat(resolvedType).isEqualTo(method(MyCollectionInterfaceType.class, "get").getGenericReturnType());
|
||||
|
||||
resolvedType = resolveType(method(MyInterfaceType.class, "get").getGenericReturnType(), MyOptionalInterfaceType.class);
|
||||
assertThat(resolvedType).isEqualTo(method(MyOptionalInterfaceType.class, "get").getGenericReturnType());
|
||||
}
|
||||
|
||||
private static Method method(Class<?> target, String methodName, Class<?>... parameterTypes) {
|
||||
Method method = findMethod(target, methodName, parameterTypes);
|
||||
assertThat(method).describedAs(target.getName() + "#" + methodName).isNotNull();
|
||||
@@ -258,12 +268,29 @@ class GenericTypeResolverTests {
|
||||
|
||||
|
||||
public interface MyInterfaceType<T> {
|
||||
default T get() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public class MySimpleInterfaceType implements MyInterfaceType<String> {
|
||||
}
|
||||
|
||||
public class MyParameterizedInterfaceType<P> implements MyInterfaceType<Collection<P>> {
|
||||
}
|
||||
|
||||
public class MyOptionalInterfaceType extends MyParameterizedInterfaceType<Optional<String>> {
|
||||
@Override
|
||||
public Collection<Optional<String>> get() {
|
||||
return super.get();
|
||||
}
|
||||
}
|
||||
|
||||
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {
|
||||
@Override
|
||||
public Collection<String> get() {
|
||||
return MyInterfaceType.super.get();
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class MyAbstractType<T> implements MyInterfaceType<T> {
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.GenericArrayType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
import java.lang.reflect.TypeVariable;
|
||||
@@ -207,6 +208,29 @@ class ResolvableTypeTests {
|
||||
.withMessage("Field must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forParameterForMethod() throws Exception {
|
||||
Method method = Methods.class.getMethod("charSequenceParameter", List.class);
|
||||
Parameter parameter = method.getParameters()[0];
|
||||
ResolvableType type = ResolvableType.forParameter(parameter);
|
||||
assertThat(type.getType()).isEqualTo(method.getGenericParameterTypes()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forParameterForConstructor() throws Exception {
|
||||
Constructor<Constructors> constructor = Constructors.class.getConstructor(List.class);
|
||||
Parameter parameter = constructor.getParameters()[0];
|
||||
ResolvableType type = ResolvableType.forParameter(parameter);
|
||||
assertThat(type.getType()).isEqualTo(constructor.getGenericParameterTypes()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void forParameterMustNotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ResolvableType.forParameter(null))
|
||||
.withMessage("Parameter must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forConstructorParameter() throws Exception {
|
||||
Constructor<Constructors> constructor = Constructors.class.getConstructor(List.class);
|
||||
@@ -221,6 +245,13 @@ class ResolvableTypeTests {
|
||||
.withMessage("Constructor must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forConstructorParameterWithImplementationClassMustNotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ResolvableType.forConstructorParameter(null, 0, TypedConstructors.class))
|
||||
.withMessage("Executable must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forMethodParameterByIndex() throws Exception {
|
||||
Method method = Methods.class.getMethod("charSequenceParameter", List.class);
|
||||
@@ -235,6 +266,13 @@ class ResolvableTypeTests {
|
||||
.withMessage("Method must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forMethodParameterByIndexWithImplementationClassMustNotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ResolvableType.forMethodParameter(null, 0, TypedMethods.class))
|
||||
.withMessage("Executable must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forMethodParameter() throws Exception {
|
||||
Method method = Methods.class.getMethod("charSequenceParameter", List.class);
|
||||
@@ -302,6 +340,13 @@ class ResolvableTypeTests {
|
||||
.withMessage("Method must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forMethodReturnWithImplementationClassMustNotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ResolvableType.forMethodReturnType(null, TypedMethods.class))
|
||||
.withMessage("Executable must not be null");
|
||||
}
|
||||
|
||||
@Test // gh-27748
|
||||
void genericMatchesReturnType() throws Exception {
|
||||
Method method = SomeRepository.class.getMethod("someMethod", Class.class, Class.class, Class.class);
|
||||
@@ -1307,6 +1352,13 @@ class ResolvableTypeTests {
|
||||
assertThat(type.asMap().toString()).isEqualTo("java.util.Map<java.lang.Integer, java.util.List<java.lang.String>>");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forClassWithGenericsClassMustNotBeNull() {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> ResolvableType.forClassWithGenerics(null, String.class))
|
||||
.withMessage("Class must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void forClassWithMismatchedGenerics() {
|
||||
assertThatIllegalArgumentException()
|
||||
|
||||
+20
-20
@@ -102,7 +102,7 @@ class AnnotationTypeMappingsTests {
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithBothValueAndAttribute.class))
|
||||
.withMessage("In @AliasFor declared on attribute 'test' in annotation [%s], attribute 'attribute' " +
|
||||
"and its alias 'value' are present with values of 'foo' and 'bar', but only one is permitted.",
|
||||
AliasForWithBothValueAndAttribute.class.getName());
|
||||
AliasForWithBothValueAndAttribute.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -111,7 +111,7 @@ class AnnotationTypeMappingsTests {
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfNonExistingAttribute.class))
|
||||
.withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] " +
|
||||
"declares an alias for 'missing' which is not present.",
|
||||
AliasForToSelfNonExistingAttribute.class.getName());
|
||||
AliasForToSelfNonExistingAttribute.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,8 +119,8 @@ class AnnotationTypeMappingsTests {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToOtherNonExistingAttribute.class))
|
||||
.withMessage("Attribute 'test' in annotation [%s] is declared as an @AliasFor nonexistent " +
|
||||
"attribute 'missing' in annotation [%s].", AliasForToOtherNonExistingAttribute.class.getName(),
|
||||
AliasForToOtherNonExistingAttributeTarget.class.getName());
|
||||
"attribute 'missing' in annotation [%s].", AliasForToOtherNonExistingAttribute.class.getCanonicalName(),
|
||||
AliasForToOtherNonExistingAttributeTarget.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -129,7 +129,7 @@ class AnnotationTypeMappingsTests {
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelf.class))
|
||||
.withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] points to itself. " +
|
||||
"Specify 'annotation' to point to a same-named attribute on a meta-annotation.",
|
||||
AliasForToSelf.class.getName());
|
||||
AliasForToSelf.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -147,13 +147,13 @@ class AnnotationTypeMappingsTests {
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithIncompatibleReturnTypes.class))
|
||||
.withMessage("Misconfigured aliases: attribute 'test' in annotation [%s] and attribute 'test' " +
|
||||
"in annotation [%s] must declare the same return type.",
|
||||
AliasForWithIncompatibleReturnTypes.class.getName(),
|
||||
AliasForWithIncompatibleReturnTypesTarget.class.getName());
|
||||
AliasForWithIncompatibleReturnTypes.class.getCanonicalName(),
|
||||
AliasForWithIncompatibleReturnTypesTarget.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void forAnnotationTypeWhenAliasForToSelfAnnotatedToOtherAttribute() {
|
||||
String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getName();
|
||||
String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getCanonicalName();
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfAnnotatedToOtherAttribute.class))
|
||||
.withMessage("Attribute 'b' in annotation [%1$s] must be declared as an @AliasFor attribute 'a' in " +
|
||||
@@ -167,8 +167,8 @@ class AnnotationTypeMappingsTests {
|
||||
}
|
||||
|
||||
private void assertMixedImplicitAndExplicitAliases(Class<? extends Annotation> annotationType, String overriddenAttribute) {
|
||||
String annotationName = annotationType.getName();
|
||||
String metaAnnotationName = AliasPair.class.getName();
|
||||
String annotationName = annotationType.getCanonicalName();
|
||||
String metaAnnotationName = AliasPair.class.getCanonicalName();
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(annotationType))
|
||||
.withMessage("Attribute 'b' in annotation [" + annotationName +
|
||||
@@ -180,8 +180,8 @@ class AnnotationTypeMappingsTests {
|
||||
void forAnnotationTypeWhenAliasForNonMetaAnnotated() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForNonMetaAnnotated.class))
|
||||
.withMessage("@AliasFor declaration on attribute 'test' in annotation [" + AliasForNonMetaAnnotated.class.getName() +
|
||||
"] declares an alias for attribute 'test' in annotation [" + AliasForNonMetaAnnotatedTarget.class.getName() +
|
||||
.withMessage("@AliasFor declaration on attribute 'test' in annotation [" + AliasForNonMetaAnnotated.class.getCanonicalName() +
|
||||
"] declares an alias for attribute 'test' in annotation [" + AliasForNonMetaAnnotatedTarget.class.getCanonicalName() +
|
||||
"] which is not meta-present.");
|
||||
}
|
||||
|
||||
@@ -189,8 +189,8 @@ class AnnotationTypeMappingsTests {
|
||||
void forAnnotationTypeWhenAliasForSelfWithDifferentDefaults() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithDifferentDefaults.class))
|
||||
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() +
|
||||
"] and attribute 'b' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() +
|
||||
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithDifferentDefaults.class.getCanonicalName() +
|
||||
"] and attribute 'b' in annotation [" + AliasForSelfWithDifferentDefaults.class.getCanonicalName() +
|
||||
"] must declare the same default value.");
|
||||
}
|
||||
|
||||
@@ -198,8 +198,8 @@ class AnnotationTypeMappingsTests {
|
||||
void forAnnotationTypeWhenAliasForSelfWithMissingDefault() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithMissingDefault.class))
|
||||
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithMissingDefault.class.getName() +
|
||||
"] and attribute 'b' in annotation [" + AliasForSelfWithMissingDefault.class.getName() +
|
||||
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithMissingDefault.class.getCanonicalName() +
|
||||
"] and attribute 'b' in annotation [" + AliasForSelfWithMissingDefault.class.getCanonicalName() +
|
||||
"] must declare default values.");
|
||||
}
|
||||
|
||||
@@ -207,8 +207,8 @@ class AnnotationTypeMappingsTests {
|
||||
void forAnnotationTypeWhenAliasWithExplicitMirrorAndDifferentDefaults() {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasWithExplicitMirrorAndDifferentDefaults.class))
|
||||
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() +
|
||||
"] and attribute 'c' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() +
|
||||
.withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getCanonicalName() +
|
||||
"] and attribute 'c' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getCanonicalName() +
|
||||
"] must declare the same default value.");
|
||||
}
|
||||
|
||||
@@ -352,8 +352,8 @@ class AnnotationTypeMappingsTests {
|
||||
AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0);
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> resolveMirrorSets(mapping, WithDifferentValueAliasPair.class, AliasPair.class))
|
||||
.withMessage("Different @AliasFor mirror values for annotation [" + AliasPair.class.getName() + "] declared on " +
|
||||
WithDifferentValueAliasPair.class.getName() +
|
||||
.withMessage("Different @AliasFor mirror values for annotation [" + AliasPair.class.getCanonicalName() +
|
||||
"] declared on " + WithDifferentValueAliasPair.class.getName() +
|
||||
"; attribute 'a' and its alias 'b' are declared with values of [test1] and [test2].");
|
||||
}
|
||||
|
||||
|
||||
+20
-10
@@ -718,11 +718,11 @@ class AnnotationUtilsTests {
|
||||
ImplicitAliasesWithMissingDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
|
||||
assertThat(config).isNotNull();
|
||||
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() ->
|
||||
synthesizeAnnotation(config, clazz))
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> synthesizeAnnotation(config, clazz))
|
||||
.withMessageStartingWith("Misconfigured aliases:")
|
||||
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
|
||||
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
|
||||
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
|
||||
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
|
||||
.withMessageContaining("default values");
|
||||
}
|
||||
|
||||
@@ -733,11 +733,11 @@ class AnnotationUtilsTests {
|
||||
ImplicitAliasesWithDifferentDefaultValuesContextConfig.class;
|
||||
ImplicitAliasesWithDifferentDefaultValuesContextConfig config = clazz.getAnnotation(annotationType);
|
||||
assertThat(config).isNotNull();
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() ->
|
||||
synthesizeAnnotation(config, clazz))
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class)
|
||||
.isThrownBy(() -> synthesizeAnnotation(config, clazz))
|
||||
.withMessageStartingWith("Misconfigured aliases:")
|
||||
.withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]")
|
||||
.withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]")
|
||||
.withMessageContaining("attribute 'location1' in annotation [%s]", annotationType.getCanonicalName())
|
||||
.withMessageContaining("attribute 'location2' in annotation [%s]", annotationType.getCanonicalName())
|
||||
.withMessageContaining("same default value");
|
||||
}
|
||||
|
||||
@@ -919,8 +919,18 @@ class AnnotationUtilsTests {
|
||||
Map<String, Object> map = Collections.singletonMap(VALUE, 42L);
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
synthesizeAnnotation(map, Component.class, null).value())
|
||||
.withMessageContaining("Attribute 'value' in annotation org.springframework.core.testfixture.stereotype.Component " +
|
||||
"should be compatible with java.lang.String but a java.lang.Long value was returned");
|
||||
.withMessageContaining("Attribute 'value' in annotation %s should be compatible with " +
|
||||
"java.lang.String but a java.lang.Long value was returned",
|
||||
Component.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void synthesizeAnnotationFromMapWithAttributeOfIncorrectArrayType() {
|
||||
Map<String, Object> map = Collections.singletonMap(VALUE, new int[] {42});
|
||||
assertThatIllegalStateException().isThrownBy(() ->
|
||||
synthesizeAnnotation(map, CharsContainer.class, null).chars())
|
||||
.withMessageContaining("Attribute 'chars' in annotation %s should be compatible with " +
|
||||
"char[] but a int[] value was returned", CharsContainer.class.getCanonicalName());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+7
-7
@@ -183,32 +183,32 @@ class ComposedRepeatableAnnotationsTests {
|
||||
assertThatIllegalArgumentException().isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith("Annotation type must be a repeatable annotation")
|
||||
.withMessageContaining("failed to resolve container type for")
|
||||
.withMessageContaining(NonRepeatable.class.getName());
|
||||
.withMessageContaining(NonRepeatable.class.getCanonicalName());
|
||||
}
|
||||
|
||||
private void expectContainerMissingValueAttribute(ThrowingCallable throwingCallable) {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith("Invalid declaration of container type")
|
||||
.withMessageContaining(ContainerMissingValueAttribute.class.getName())
|
||||
.withMessageContaining(ContainerMissingValueAttribute.class.getCanonicalName())
|
||||
.withMessageContaining("for repeatable annotation")
|
||||
.withMessageContaining(InvalidRepeatable.class.getName())
|
||||
.withMessageContaining(InvalidRepeatable.class.getCanonicalName())
|
||||
.withCauseExactlyInstanceOf(NoSuchMethodException.class);
|
||||
}
|
||||
|
||||
private void expectContainerWithNonArrayValueAttribute(ThrowingCallable throwingCallable) {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith("Container type")
|
||||
.withMessageContaining(ContainerWithNonArrayValueAttribute.class.getName())
|
||||
.withMessageContaining(ContainerWithNonArrayValueAttribute.class.getCanonicalName())
|
||||
.withMessageContaining("must declare a 'value' attribute for an array of type")
|
||||
.withMessageContaining(InvalidRepeatable.class.getName());
|
||||
.withMessageContaining(InvalidRepeatable.class.getCanonicalName());
|
||||
}
|
||||
|
||||
private void expectContainerWithArrayValueAttributeButWrongComponentType(ThrowingCallable throwingCallable) {
|
||||
assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(throwingCallable)
|
||||
.withMessageStartingWith("Container type")
|
||||
.withMessageContaining(ContainerWithArrayValueAttributeButWrongComponentType.class.getName())
|
||||
.withMessageContaining(ContainerWithArrayValueAttributeButWrongComponentType.class.getCanonicalName())
|
||||
.withMessageContaining("must declare a 'value' attribute for an array of type")
|
||||
.withMessageContaining(InvalidRepeatable.class.getName());
|
||||
.withMessageContaining(InvalidRepeatable.class.getCanonicalName());
|
||||
}
|
||||
|
||||
private void assertGetRepeatableAnnotations(AnnotatedElement element) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user