mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-19 10:59:13 +00:00
Compare commits
213
Commits
v6.2.0-RC2
...
v6.2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5024bb7227 | ||
|
|
35b452b458 | ||
|
|
56525da43a | ||
|
|
5145cf476f | ||
|
|
d4f8b5d820 | ||
|
|
6cb41dc5e3 | ||
|
|
cca245020d | ||
|
|
de8a94f30d | ||
|
|
973d815763 | ||
|
|
1e95332f62 | ||
|
|
1c2dbec971 | ||
|
|
fec6ba4dfe | ||
|
|
37b110a181 | ||
|
|
18a3b52d5e | ||
|
|
b523f3caff | ||
|
|
7f9901d35d | ||
|
|
bfde33a514 | ||
|
|
25e2c1b7ce | ||
|
|
14b9865de7 | ||
|
|
62eb21f938 | ||
|
|
01c85b1afb | ||
|
|
df376d9343 | ||
|
|
d92c57f7a5 | ||
|
|
4d792d0e45 | ||
|
|
4ca596b7bc | ||
|
|
90ef7ac514 | ||
|
|
c3991392df | ||
|
|
0a5bd89129 | ||
|
|
357dbc0354 | ||
|
|
525407e4b1 | ||
|
|
53b9a2cb78 | ||
|
|
cbe2f36106 | ||
|
|
9dabfdf0bf | ||
|
|
942d2c5541 | ||
|
|
e78179b96e | ||
|
|
5b5a072351 | ||
|
|
85cf563741 | ||
|
|
e3ba957431 | ||
|
|
caec8f4f36 | ||
|
|
ae16a7fc08 | ||
|
|
43e7921bcc | ||
|
|
49a63e2c37 | ||
|
|
5666e363d1 | ||
|
|
f06853a339 | ||
|
|
8afd01ba2c | ||
|
|
9724f9b9c8 | ||
|
|
8202282975 | ||
|
|
d4719493bd | ||
|
|
59ec871e76 | ||
|
|
37243f44e8 | ||
|
|
e67f892e44 | ||
|
|
1ced8c3856 | ||
|
|
0d9033592b | ||
|
|
fc7b8ae966 | ||
|
|
c457131f1c | ||
|
|
39cfe136da | ||
|
|
fd676ed932 | ||
|
|
05a880e3b5 | ||
|
|
715e38c44f | ||
|
|
c93af1f76d | ||
|
|
2e6c8daec6 | ||
|
|
c5f8dcc245 | ||
|
|
541866fd70 | ||
|
|
0219ee656f | ||
|
|
f6e1a5de09 | ||
|
|
72e7f1552b | ||
|
|
bbe90a57ba | ||
|
|
9b3cb15389 | ||
|
|
4697ae10ee | ||
|
|
fb98d74a47 | ||
|
|
4ef2b429e0 | ||
|
|
7e1d6fe934 | ||
|
|
0beb56a58c | ||
|
|
c6126b2033 | ||
|
|
f1e6482f92 | ||
|
|
e3301dd1c4 | ||
|
|
6bd4687706 | ||
|
|
07b12666b4 | ||
|
|
2b601f9e51 | ||
|
|
438d6de3c1 | ||
|
|
d43126705f | ||
|
|
57bbf0ca0f | ||
|
|
a3b979c5ec | ||
|
|
a8f5848a5d | ||
|
|
97f23dca22 | ||
|
|
86784b61cc | ||
|
|
fdb763eeaf | ||
|
|
27912b1ed1 | ||
|
|
ba692aa3ef | ||
|
|
0846706688 | ||
|
|
bc65f93467 | ||
|
|
38c88e769d | ||
|
|
4a0edc59cc | ||
|
|
c979eddab1 | ||
|
|
864cb25eec | ||
|
|
e23c8bfbb6 | ||
|
|
08e0baac94 | ||
|
|
9166688b6f | ||
|
|
da8925131e | ||
|
|
09d87fac57 | ||
|
|
a005c325f7 | ||
|
|
eaafdea1fb | ||
|
|
f4b3520ab3 | ||
|
|
f427ac383d | ||
|
|
79cf554850 | ||
|
|
8ffbafd384 | ||
|
|
c0c41ddda5 | ||
|
|
7148b28b2b | ||
|
|
1255bd1fc9 | ||
|
|
a1f6098158 | ||
|
|
022fdcd67e | ||
|
|
11ebceee38 | ||
|
|
fa21dffcf8 | ||
|
|
9e3371ef07 | ||
|
|
c2c6bb25c6 | ||
|
|
40960fa85a | ||
|
|
578928de39 | ||
|
|
015beb0058 | ||
|
|
3732c71168 | ||
|
|
323de1208a | ||
|
|
94d46eba3c | ||
|
|
4749d810db | ||
|
|
044da794f4 | ||
|
|
89d56b1fa6 | ||
|
|
f1cfe7a3d4 | ||
|
|
dfaf7a0c6a | ||
|
|
86a42db002 | ||
|
|
d4b31fd4b2 | ||
|
|
a366ea0e15 | ||
|
|
37622a7f90 | ||
|
|
de2c10abcd | ||
|
|
81d89f478a | ||
|
|
52e813d0ad | ||
|
|
f19a1b50e5 | ||
|
|
5532574f56 | ||
|
|
1b3e96b9bf | ||
|
|
e340e45f5a | ||
|
|
9df4fcdc9e | ||
|
|
a06bbccf9e | ||
|
|
d1d6ff8737 | ||
|
|
9f0dbc4051 | ||
|
|
acccbbec3f | ||
|
|
3cc76ef87c | ||
|
|
0221471265 | ||
|
|
3b82733e1f | ||
|
|
ba8024d077 | ||
|
|
efda3f0f99 | ||
|
|
761850816c | ||
|
|
2d028c39b4 | ||
|
|
fe6d9145e5 | ||
|
|
c160e89d9d | ||
|
|
bbe362c0e6 | ||
|
|
bff76d756b | ||
|
|
8fa99dcbdd | ||
|
|
5bf179b851 | ||
|
|
20d21a8251 | ||
|
|
d8a6423c0c | ||
|
|
c85689b308 | ||
|
|
982f7f8f58 | ||
|
|
d05f880d74 | ||
|
|
2b113e3fd0 | ||
|
|
73e5aa38ec | ||
|
|
a0af708c03 | ||
|
|
d8c153a9d1 | ||
|
|
1ba773170d | ||
|
|
4a81f2c904 | ||
|
|
081d0b33d4 | ||
|
|
1c69a3c521 | ||
|
|
b7fc4bc5c8 | ||
|
|
657e002ed7 | ||
|
|
9252e741e1 | ||
|
|
5271f5b8a1 | ||
|
|
626f4279f6 | ||
|
|
fb0a108254 | ||
|
|
c98f314665 | ||
|
|
382b51a8d4 | ||
|
|
2b0c1fa210 | ||
|
|
ec895534c5 | ||
|
|
d22924c728 | ||
|
|
267d3a36ae | ||
|
|
e235e661d8 | ||
|
|
09fe0adb40 | ||
|
|
1784ccb200 | ||
|
|
57eb4823d2 | ||
|
|
ab2f1f8885 | ||
|
|
84e762b470 | ||
|
|
a1dd2e7c96 | ||
|
|
e90a2da05d | ||
|
|
ee1fe8a1e9 | ||
|
|
6f9413ba31 | ||
|
|
d48f388c6a | ||
|
|
6c93c67dd4 | ||
|
|
599d53465b | ||
|
|
bbbb7c396e | ||
|
|
68d9e5d81a | ||
|
|
d72c8b32b7 | ||
|
|
23f19a0fad | ||
|
|
14c476bd42 | ||
|
|
a8b36f7ad5 | ||
|
|
d5e334339a | ||
|
|
c0ad9c0081 | ||
|
|
2aa3f40257 | ||
|
|
3842f12d09 | ||
|
|
5f14703105 | ||
|
|
67d78eb61c | ||
|
|
e02f8ca492 | ||
|
|
73fd9133e9 | ||
|
|
5abe5e1167 | ||
|
|
5e28a25a30 | ||
|
|
4c44b91cf9 | ||
|
|
cee8b52de8 | ||
|
|
912c067e23 | ||
|
|
9a761579c0 |
@@ -1,6 +1,6 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Asking for help
|
||||
- name: Community Support
|
||||
url: https://stackoverflow.com/tags/spring
|
||||
about: The Spring team is using StackOverflow for questions.
|
||||
about: Please ask and answer questions on StackOverflow with the tag `spring`.
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
name: Issue
|
||||
name: General
|
||||
about: Bugs, enhancements, documentation, tasks.
|
||||
title: ''
|
||||
labels: ''
|
||||
|
||||
@@ -20,8 +20,6 @@ jobs:
|
||||
toolchain: false
|
||||
- version: 21
|
||||
toolchain: true
|
||||
- version: 22
|
||||
toolchain: true
|
||||
- version: 23
|
||||
toolchain: true
|
||||
exclude:
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
name: Update on Supported Branches
|
||||
strategy:
|
||||
matrix:
|
||||
branch: [ '6.0.x', '6.1.x', 'main' ]
|
||||
branch: [ '6.1.x' ]
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc
|
||||
name: Update
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.11.2/api/",
|
||||
// "https://junit.org/junit5/docs/5.11.3/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
|
||||
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.18.1");
|
||||
checkstyle.setToolVersion("10.20.1");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -42,7 +42,8 @@ asciidoc:
|
||||
spring-framework-reference: '{spring-framework-docs-root}/{spring-version}/reference'
|
||||
#
|
||||
# Other Spring portfolio projects
|
||||
spring-boot-docs: '{docs-site}/spring-boot/docs/current/reference/html'
|
||||
spring-boot-docs: '{docs-site}/spring-boot'
|
||||
spring-boot-docs-ref: '{spring-boot-docs}/reference'
|
||||
spring-boot-issues: '{spring-github-org}/spring-boot/issues'
|
||||
# TODO add more projects / links or just build up on {docs-site}?
|
||||
# TODO rename the below using new conventions
|
||||
|
||||
@@ -451,5 +451,6 @@
|
||||
** xref:languages/groovy.adoc[]
|
||||
** xref:languages/dynamic.adoc[]
|
||||
* xref:appendix.adoc[]
|
||||
* {spring-framework-wiki}[Wiki]
|
||||
|
||||
* {spring-framework-docs-root}/{spring-version}/javadoc-api/[Java API,window=_blank, role=link-external]
|
||||
* {spring-framework-api-kdoc}/[Kotlin API,window=_blank, role=link-external]
|
||||
* {spring-framework-wiki}[Wiki, window=_blank, role=link-external]
|
||||
|
||||
@@ -23,6 +23,12 @@ The following table lists all currently supported Spring properties.
|
||||
|===
|
||||
| Name | Description
|
||||
|
||||
| `spring.aop.ajc.ignore`
|
||||
| Instructs Spring to ignore ajc-compiled aspects for Spring AOP proxying, restoring traditional
|
||||
Spring behavior for scenarios where both weaving and AspectJ auto-proxying are enabled. See
|
||||
{spring-framework-api}++/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.html#IGNORE_AJC_PROPERTY_NAME++[`AbstractAspectJAdvisorFactory`]
|
||||
for details.
|
||||
|
||||
| `spring.aot.enabled`
|
||||
| Indicates the application should run with AOT generated artifacts. See
|
||||
xref:core/aot.adoc[Ahead of Time Optimizations] and
|
||||
@@ -32,7 +38,7 @@ for details.
|
||||
| `spring.beaninfo.ignore`
|
||||
| Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the
|
||||
JavaBeans `Introspector`. See
|
||||
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`]
|
||||
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`StandardBeanInfoFactory`]
|
||||
for details.
|
||||
|
||||
| `spring.cache.reactivestreams.ignore`
|
||||
@@ -49,15 +55,13 @@ for details.
|
||||
|
||||
| `spring.context.checkpoint`
|
||||
| Property that specifies a common context checkpoint. See
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
|
||||
checkpoint/restore at startup] and
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
|
||||
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#CHECKPOINT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
|
||||
for details.
|
||||
|
||||
| `spring.context.exit`
|
||||
| Property for terminating the JVM when the context reaches a specific phase. See
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
|
||||
checkpoint/restore at startup] and
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
|
||||
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#EXIT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
|
||||
for details.
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ NOTE: At the moment, AOT is focused on allowing Spring applications to be deploy
|
||||
We intend to support more JVM-based use cases in future generations.
|
||||
|
||||
[[aot.basics]]
|
||||
== AOT engine overview
|
||||
== AOT Engine Overview
|
||||
|
||||
The entry point of the AOT engine for processing an `ApplicationContext` is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {spring-framework-api}/aot/generate/GenerationContext.html[`GenerationContext`]:
|
||||
|
||||
@@ -225,7 +225,7 @@ When a `datasource` instance is required, a `BeanInstanceSupplier` is called.
|
||||
This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean.
|
||||
|
||||
[[aot.running]]
|
||||
== Running with AOT optimizations
|
||||
== Running with AOT Optimizations
|
||||
|
||||
AOT is a mandatory step to transform a Spring application to a native executable, so it
|
||||
is automatically enabled when running in this mode. It is possible to use those optimizations
|
||||
@@ -244,7 +244,7 @@ However, keep in mind that some optimizations are made at build time based on a
|
||||
This section lists the best practices that make sure your application is ready for AOT.
|
||||
|
||||
[[aot.bestpractices.bean-registration]]
|
||||
== Programmatic bean registration
|
||||
=== Programmatic Bean Registration
|
||||
|
||||
The AOT engine takes care of the `@Configuration` model and any callback that might be
|
||||
invoked as part of processing your configuration. If you need to register additional
|
||||
@@ -266,7 +266,7 @@ notion of a classpath. For cases like this, it is crucial that the scanning happ
|
||||
build time.
|
||||
|
||||
[[aot.bestpractices.bean-type]]
|
||||
=== Expose The Most Precise Bean Type
|
||||
=== Expose the Most Precise Bean Type
|
||||
|
||||
While your application may interact with an interface that a bean implements, it is still very important to declare the most precise type.
|
||||
The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members or lifecycle callback methods.
|
||||
@@ -290,6 +290,19 @@ Java::
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class UserConfiguration {
|
||||
|
||||
@Bean
|
||||
fun myInterface(): MyInterface = MyImplementation()
|
||||
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
In the example above, the declared type for the `myInterface` bean is `MyInterface`.
|
||||
@@ -314,6 +327,19 @@ Java::
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class UserConfiguration {
|
||||
|
||||
@Bean
|
||||
fun myInterface() = MyImplementation()
|
||||
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If you are registering bean definitions programmatically, consider using `RootBeanBefinition` as it allows to specify a `ResolvableType` that handles generics.
|
||||
@@ -326,21 +352,21 @@ However, this is not a best practice and flagging the preferred constructor with
|
||||
|
||||
In case you are working on a code base that you cannot modify, you can set the {spring-framework-api}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
|
||||
|
||||
[[aot.bestpractices.comlext-data-structure]]
|
||||
=== Avoid Complex Data Structure for Constructor Parameters and Properties
|
||||
[[aot.bestpractices.complex-data-structures]]
|
||||
=== Avoid Complex Data Structures for Constructor Parameters and Properties
|
||||
|
||||
When crafting a `RootBeanDefinition` programmatically, you are not constrained in terms of types that you can use.
|
||||
For instance, you may have a custom `record` with several properties that your bean takes as a constructor argument.
|
||||
|
||||
While this works fine with the regular runtime, AOT does not know how to generate the code of your custom data structure.
|
||||
A good rule of thumb is to keep in mind that bean definitions are an abstraction on top of several models.
|
||||
Rather than using such structure, decomposing to simple types or referring to a bean that is built as such is recommended.
|
||||
Rather than using such structures, decomposing to simple types or referring to a bean that is built as such is recommended.
|
||||
|
||||
As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`.
|
||||
To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key.
|
||||
|
||||
[[aot.bestpractices.custom-arguments]]
|
||||
=== Avoid Creating Bean with Custom Arguments
|
||||
=== Avoid Creating Beans with Custom Arguments
|
||||
|
||||
Spring AOT detects what needs to be done to create a bean and translates that in generated code using an instance supplier.
|
||||
The container also supports creating a bean with {spring-framework-api}++/beans/factory/BeanFactory.html#getBean(java.lang.String,java.lang.Object...)++[custom arguments] that leads to several issues with AOT:
|
||||
@@ -352,6 +378,20 @@ For instance, autowiring on fields and methods will be skipped as they are handl
|
||||
|
||||
Rather than having prototype-scoped beans created with custom arguments, we recommend a manual factory pattern where a bean is responsible for the creation of the instance.
|
||||
|
||||
[[aot.bestpractices.circular-dependencies]]
|
||||
=== Avoid Circular Dependencies
|
||||
|
||||
Certain use cases can result in circular dependencies between one or more beans. With the
|
||||
regular runtime, it may be possible to wire those circular dependencies via `@Autowired`
|
||||
on setter methods or fields. However, an AOT-optimized context will fail to start with
|
||||
explicit circular dependencies.
|
||||
|
||||
In an AOT-optimized application, you should therefore strive to avoid circular
|
||||
dependencies. If that is not possible, you can use `@Lazy` injection points or
|
||||
`ObjectProvider` to lazily access or retrieve the necessary collaborating beans. See
|
||||
xref:core/beans/classpath-scanning.adoc#beans-factorybeans-annotations-lazy-injection-points[this tip]
|
||||
for further information.
|
||||
|
||||
[[aot.bestpractices.factory-bean]]
|
||||
=== FactoryBean
|
||||
|
||||
@@ -371,6 +411,15 @@ Java::
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class ClientFactoryBean<T : AbstractClient> : FactoryBean<T> {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
A concrete client declaration should provide a resolved generic for the client, as shown in the following example:
|
||||
@@ -391,6 +440,19 @@ Java::
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class UserConfiguration {
|
||||
|
||||
@Bean
|
||||
fun myClient() = ClientFactoryBean<MyClient>(...)
|
||||
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
If the `FactoryBean` bean definition is registered programmatically, make sure to follow these steps:
|
||||
@@ -412,6 +474,16 @@ Java::
|
||||
// ...
|
||||
registry.registerBeanDefinition("myClient", beanDefinition);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java)
|
||||
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java));
|
||||
// ...
|
||||
registry.registerBeanDefinition("myClient", beanDefinition)
|
||||
----
|
||||
======
|
||||
|
||||
[[aot.bestpractices.jpa]]
|
||||
@@ -433,6 +505,19 @@ Java::
|
||||
return factoryBean;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Bean
|
||||
fun customDBEntityManagerFactory(dataSource: DataSource): LocalContainerEntityManagerFactoryBean {
|
||||
val factoryBean = LocalContainerEntityManagerFactoryBean()
|
||||
factoryBean.dataSource = dataSource
|
||||
factoryBean.setPackagesToScan("com.example.app")
|
||||
return factoryBean
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
To make sure the scanning occurs ahead of time, a `PersistenceManagedTypes` bean must be declared and used by the
|
||||
@@ -458,6 +543,25 @@ Java::
|
||||
return factoryBean;
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Bean
|
||||
fun persistenceManagedTypes(resourceLoader: ResourceLoader): PersistenceManagedTypes {
|
||||
return PersistenceManagedTypesScanner(resourceLoader)
|
||||
.scan("com.example.app")
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun customDBEntityManagerFactory(dataSource: DataSource, managedTypes: PersistenceManagedTypes): LocalContainerEntityManagerFactoryBean {
|
||||
val factoryBean = LocalContainerEntityManagerFactoryBean()
|
||||
factoryBean.dataSource = dataSource
|
||||
factoryBean.setManagedTypes(managedTypes)
|
||||
return factoryBean
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[aot.hints]]
|
||||
@@ -479,6 +583,13 @@ Java::
|
||||
----
|
||||
runtimeHints.resources().registerPattern("config/app.properties");
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
runtimeHints.resources().registerPattern("config/app.properties")
|
||||
----
|
||||
======
|
||||
|
||||
A number of contracts are handled automatically during AOT processing.
|
||||
|
||||
@@ -480,11 +480,15 @@ factory method and other bean definition properties, such as a qualifier value t
|
||||
the `@Qualifier` annotation. Other method-level annotations that can be specified are
|
||||
`@Scope`, `@Lazy`, and custom qualifier annotations.
|
||||
|
||||
TIP: In addition to its role for component initialization, you can also place the `@Lazy`
|
||||
[[beans-factorybeans-annotations-lazy-injection-points]]
|
||||
[TIP]
|
||||
====
|
||||
In addition to its role for component initialization, you can also place the `@Lazy`
|
||||
annotation on injection points marked with `@Autowired` or `@Inject`. In this context,
|
||||
it leads to the injection of a lazy-resolution proxy. However, such a proxy approach
|
||||
is rather limited. For sophisticated lazy interactions, in particular in combination
|
||||
with optional dependencies, we recommend `ObjectProvider<MyTargetBean>` instead.
|
||||
====
|
||||
|
||||
Autowired fields and methods are supported, as previously discussed, with additional
|
||||
support for autowiring of `@Bean` methods. The following example shows how to do so:
|
||||
|
||||
@@ -883,11 +883,12 @@ for example, for processing all events asynchronously and/or for handling listen
|
||||
== Convenient Access to Low-level Resources
|
||||
|
||||
For optimal usage and understanding of application contexts, you should familiarize
|
||||
yourself with Spring's `Resource` abstraction, as described in xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources].
|
||||
yourself with Spring's `Resource` abstraction, as described in
|
||||
xref:core/resources.adoc[Resources].
|
||||
|
||||
An application context is a `ResourceLoader`, which can be used to load `Resource` objects.
|
||||
A `Resource` is essentially a more feature rich version of the JDK `java.net.URL` class.
|
||||
In fact, the implementations of the `Resource` wrap an instance of `java.net.URL`, where
|
||||
In fact, implementations of `Resource` wrap an instance of `java.net.URL`, where
|
||||
appropriate. A `Resource` can obtain low-level resources from almost any location in a
|
||||
transparent fashion, including from the classpath, a filesystem location, anywhere
|
||||
describable with a standard URL, and some other variations. If the resource location
|
||||
|
||||
+2
-2
@@ -102,8 +102,8 @@ following snippet:
|
||||
----
|
||||
<bean id="theTargetBean" class="..." />
|
||||
|
||||
<bean id="client" class="...">
|
||||
<property name="targetName" value="theTargetBean"/>
|
||||
<bean id="theClientBean" class="...">
|
||||
<property name="targetName" ref="theTargetBean"/>
|
||||
</bean>
|
||||
----
|
||||
|
||||
|
||||
@@ -444,18 +444,27 @@ component. This section discusses both of these options.
|
||||
The compiler can operate in one of three modes, which are captured in the
|
||||
`org.springframework.expression.spel.SpelCompilerMode` enum. The modes are as follows.
|
||||
|
||||
* `OFF` (default): The compiler is switched off.
|
||||
* `IMMEDIATE`: In immediate mode, the expressions are compiled as soon as possible. This
|
||||
is typically after the first interpreted evaluation. If the compiled expression fails
|
||||
(typically due to a type changing, as described earlier), the caller of the expression
|
||||
evaluation receives an exception.
|
||||
* `MIXED`: In mixed mode, the expressions silently switch between interpreted and
|
||||
compiled mode over time. After some number of interpreted runs, they switch to compiled
|
||||
form and, if something goes wrong with the compiled form (such as a type changing, as
|
||||
described earlier), the expression automatically switches back to interpreted form
|
||||
again. Sometime later, it may generate another compiled form and switch to it.
|
||||
Basically, the exception that the user gets in `IMMEDIATE` mode is instead handled
|
||||
internally.
|
||||
`OFF` ::
|
||||
The compiler is switched off, and all expressions will be evaluated in _interpreted_
|
||||
mode. This is the default mode.
|
||||
`IMMEDIATE` ::
|
||||
In immediate mode, expressions are compiled as soon as possible, typically after the
|
||||
first interpreted evaluation. If evaluation of the compiled expression fails (for
|
||||
example, due to a type changing, as described earlier), the caller of the expression
|
||||
evaluation receives an exception. If the types of various expression elements change
|
||||
over time, consider switching to `MIXED` mode or turning off the compiler.
|
||||
`MIXED` ::
|
||||
In mixed mode, expression evaluation silently switches between _interpreted_ and
|
||||
_compiled_ over time. After some number of successful interpreted runs, the expression
|
||||
gets compiled. If evaluation of the compiled expression fails (for example, due to a
|
||||
type changing), that failure will be caught internally, and the system will switch back
|
||||
to interpreted mode for the given expression. Basically, the exception that the caller
|
||||
receives in `IMMEDIATE` mode is instead handled internally. Sometime later, the
|
||||
compiler may generate another compiled form and switch to it. This cycle of switching
|
||||
between interpreted and compiled mode will continue until the system determines that it
|
||||
does not make sense to continue trying — for example, when a certain failure threshold
|
||||
has been reached — at which point the system will permanently switch to interpreted
|
||||
mode for the given expression.
|
||||
|
||||
`IMMEDIATE` mode exists because `MIXED` mode could cause issues for expressions that
|
||||
have side effects. If a compiled expression blows up after partially succeeding, it
|
||||
|
||||
@@ -9,7 +9,7 @@ known as _JUL_ or `java.util.logging`) if neither Log4j 2.x nor SLF4J is availab
|
||||
|
||||
Put Log4j 2.x or Logback (or another SLF4J provider) in your classpath, without any extra
|
||||
bridges, and let the framework auto-adapt to your choice. For further information see the
|
||||
{spring-boot-docs}/features.html#features.logging[Spring
|
||||
{spring-boot-docs-ref}/features/logging.html[Spring
|
||||
Boot Logging Reference Documentation].
|
||||
|
||||
[NOTE]
|
||||
|
||||
@@ -196,7 +196,7 @@ Kotlin::
|
||||
|
||||
When used as `org.springframework.validation.Validator`, `LocalValidatorFactoryBean`
|
||||
invokes the underlying `jakarta.validation.Validator`, and then adapts
|
||||
``ContraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
``ConstraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
passed into the `validate` method.
|
||||
|
||||
|
||||
|
||||
@@ -281,11 +281,11 @@ Kotlin::
|
||||
|
||||
A portable format annotation API exists in the `org.springframework.format.annotation`
|
||||
package. You can use `@NumberFormat` to format `Number` fields such as `Double` and
|
||||
`Long`, `@DurationFormat` to format `Duration` fields in ISO8601 and simplified styles,
|
||||
and `@DateTimeFormat` to format `java.util.Date`, `java.util.Calendar`, `Long`
|
||||
(for millisecond timestamps) as well as JSR-310 `java.time`.
|
||||
`Long`, `@DurationFormat` to format `Duration` fields in ISO-8601 and simplified styles,
|
||||
and `@DateTimeFormat` to format fields such as `java.util.Date`, `java.util.Calendar`,
|
||||
and `Long` (for millisecond timestamps) as well as JSR-310 `java.time` types.
|
||||
|
||||
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO Date
|
||||
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO date
|
||||
(yyyy-MM-dd):
|
||||
|
||||
[tabs]
|
||||
@@ -311,6 +311,29 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
For further details, see the javadoc for
|
||||
{spring-framework-api}/format/annotation/DateTimeFormat.html[`@DateTimeFormat`],
|
||||
{spring-framework-api}/format/annotation/DurationFormat.html[`@DurationFormat`], and
|
||||
{spring-framework-api}/format/annotation/NumberFormat.html[`@NumberFormat`].
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Style-based formatting and parsing rely on locale-sensitive patterns which may change
|
||||
depending on the Java runtime. Specifically, applications that rely on date, time, or
|
||||
number parsing and formatting may encounter incompatible changes in behavior when running
|
||||
on JDK 20 or higher.
|
||||
|
||||
Using an ISO standardized format or a concrete pattern that you control allows for
|
||||
reliable system-independent and locale-independent parsing and formatting of date, time,
|
||||
and number values.
|
||||
|
||||
For `@DateTimeFormat`, the use of fallback patterns can also help to address
|
||||
compatibility issues.
|
||||
|
||||
For further details, see the
|
||||
https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher[Date and Time Formatting with JDK 20 and higher]
|
||||
page in the Spring Framework wiki.
|
||||
====
|
||||
|
||||
[[format-FormatterRegistry-SPI]]
|
||||
== The `FormatterRegistry` SPI
|
||||
|
||||
@@ -198,7 +198,7 @@ methods it offers can be found in the {spring-framework-api}/validation/Errors.h
|
||||
Validators may also get locally invoked for the immediate validation of a given object,
|
||||
not involving a binding process. As of 6.1, this has been simplified through a new
|
||||
`Validator.validateObject(Object)` method which is available by default now, returning
|
||||
a simple ´Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
a simple `Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
or the new `failOnError` method for turning the error summary message into an exception
|
||||
(for example, `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`).
|
||||
|
||||
|
||||
@@ -142,8 +142,7 @@ Alternatively, specify a custom `persistenceXmlLocation` on your
|
||||
META-INF/my-persistence.xml) and include only a descriptor with that name in your
|
||||
application jar files. Because the Jakarta EE server looks only for default
|
||||
`META-INF/persistence.xml` files, it ignores such custom persistence units and, hence,
|
||||
avoids conflicts with a Spring-driven JPA setup upfront. (This applies to Resin 3.1, for
|
||||
example.)
|
||||
avoids conflicts with a Spring-driven JPA setup upfront.
|
||||
|
||||
.When is load-time weaving required?
|
||||
****
|
||||
|
||||
@@ -7,7 +7,7 @@ Metrics can help you to track error rates, usage patterns, performance, and more
|
||||
Traces provide a holistic view of an entire system, crossing application boundaries; you can zoom in on particular user requests and follow their entire completion across applications.
|
||||
|
||||
Spring Framework instruments various parts of its own codebase to publish observations if an `ObservationRegistry` is configured.
|
||||
You can learn more about {spring-boot-docs}/actuator.html#actuator.metrics[configuring the observability infrastructure in Spring Boot].
|
||||
You can learn more about {spring-boot-docs-ref}/actuator/observability.html[configuring the observability infrastructure in Spring Boot].
|
||||
|
||||
|
||||
[[observability.list]]
|
||||
|
||||
@@ -38,6 +38,7 @@ RestClient customClient = RestClient.builder()
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build();
|
||||
@@ -55,6 +56,7 @@ val customClient = RestClient.builder()
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(mapOf("variable" to "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build()
|
||||
|
||||
@@ -13,7 +13,7 @@ Most of the code samples of the reference documentation are
|
||||
provided in Kotlin in addition to Java.
|
||||
|
||||
The easiest way to build a Spring application with Kotlin is to leverage Spring Boot and
|
||||
its {spring-boot-docs}/boot-features-kotlin.html[dedicated Kotlin support].
|
||||
its {spring-boot-docs-ref}/features/kotlin.html[dedicated Kotlin support].
|
||||
{spring-site-guides}/tutorials/spring-boot-kotlin/[This comprehensive tutorial]
|
||||
will teach you how to build Spring Boot applications with Kotlin using https://start.spring.io/#!language=kotlin&type=gradle-project[start.spring.io].
|
||||
|
||||
|
||||
@@ -186,7 +186,7 @@ Therefore, if you wish to use the `@Value` annotation in Kotlin, you need to esc
|
||||
character by writing pass:q[`@Value("\${property}")`].
|
||||
|
||||
NOTE: If you use Spring Boot, you should probably use
|
||||
{spring-boot-docs}/boot-features-external-config.html#boot-features-external-config-typesafe-configuration-properties[`@ConfigurationProperties`]
|
||||
{spring-boot-docs-ref}/features/external-config.html#features.external-config.typesafe-configuration-properties[`@ConfigurationProperties`]
|
||||
instead of `@Value` annotations.
|
||||
|
||||
As an alternative, you can customize the property placeholder prefix by declaring the
|
||||
@@ -323,7 +323,7 @@ The recommended testing framework is https://junit.org/junit5/[JUnit 5] along wi
|
||||
https://mockk.io/[Mockk] for mocking.
|
||||
|
||||
NOTE: If you are using Spring Boot, see
|
||||
{spring-boot-docs}/features.html#features.kotlin.testing[this related documentation].
|
||||
{spring-boot-docs-ref}/features/kotlin.html#features.kotlin.testing[this related documentation].
|
||||
|
||||
|
||||
[[constructor-injection]]
|
||||
|
||||
+11
-2
@@ -39,8 +39,17 @@ xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overrid
|
||||
and the original instance is wrapped in a Mockito spy. This strategy requires that
|
||||
exactly one candidate bean exists.
|
||||
|
||||
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
|
||||
bean will result in an exception.
|
||||
[TIP]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
|
||||
When using `@MockitoBean` to mock a bean created by a `FactoryBean`, the `FactoryBean`
|
||||
will be replaced with a singleton mock of the type of object created by the `FactoryBean`.
|
||||
|
||||
When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be created
|
||||
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
|
||||
====
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
|
||||
|
||||
|
||||
+10
-3
@@ -82,7 +82,7 @@ Java::
|
||||
<2> The result of this static method will be used as the instance and injected into the field.
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
[TIP]
|
||||
====
|
||||
Spring searches for the factory method to invoke in the test class, in the test class
|
||||
hierarchy, and in the enclosing class hierarchy for a `@Nested` test class.
|
||||
@@ -92,5 +92,12 @@ fully-qualified method name following the syntax `<fully-qualified class name>#<
|
||||
– for example, `methodName = "org.example.TestUtils#createCustomService"`.
|
||||
====
|
||||
|
||||
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
|
||||
bean will result in an exception.
|
||||
[TIP]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
|
||||
When overriding a bean created by a `FactoryBean`, the `FactoryBean` will be replaced
|
||||
with a singleton bean corresponding to the value returned from the `@TestBean` factory
|
||||
method.
|
||||
====
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ streams because there is no way to cancel the server stream from the client side
|
||||
To test infinite streams, you'll need to
|
||||
xref:testing/webtestclient.adoc#webtestclient-server-config[bind to] a running server,
|
||||
or when using Spring Boot,
|
||||
{spring-boot-docs}/spring-boot-features.html#boot-features-testing-spring-boot-applications-testing-with-running-server[test with a running server].
|
||||
{spring-boot-docs-ref}/testing/spring-boot-applications.html#testing.spring-boot-applications.with-running-server[test with a running server].
|
||||
|
||||
`MockMvcWebTestClient` does support asynchronous responses, and even streaming responses.
|
||||
The limitation is that it can't influence the server to stop, and therefore the server
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ for rendering JSON, XML, and other formats through `@ResponseBody` methods.
|
||||
|
||||
Alternatively, you may consider the full end-to-end integration testing support from
|
||||
Spring Boot with `@SpringBootTest`. See the
|
||||
{spring-boot-docs}/spring-boot-features.html#boot-features-testing[Spring Boot Reference Guide].
|
||||
{spring-boot-docs-ref}/testing/spring-boot-applications.html[Spring Boot Reference Guide].
|
||||
|
||||
There are pros and cons for each approach. The options provided in Spring MVC Test are
|
||||
different stops on the scale from classic unit testing to full integration testing. To be
|
||||
|
||||
+13
-3
@@ -57,6 +57,19 @@ defined by the corresponding `BeanOverrideStrategy`:
|
||||
`WRAP`::
|
||||
Retrieves the original bean and wraps it.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
|
||||
When replacing a bean created by a `FactoryBean`, the `FactoryBean` itself will be
|
||||
replaced with a singleton bean corresponding to bean override instance created by the
|
||||
applicable `BeanOverrideHandler`.
|
||||
|
||||
When wrapping a bean created by a `FactoryBean`, the object created by the `FactoryBean`
|
||||
will be wrapped, not the `FactoryBean` itself.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
In contrast to Spring's autowiring mechanism (for example, resolution of an `@Autowired`
|
||||
@@ -71,6 +84,3 @@ Alternatively, the user can directly provide the bean name in the custom annotat
|
||||
`BeanOverrideProcessor` implementations may also internally compute a bean name based on
|
||||
a convention or some other method.
|
||||
====
|
||||
|
||||
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
|
||||
bean will result in an exception.
|
||||
|
||||
@@ -10,6 +10,7 @@ by default, exactly in the following order:
|
||||
annotation for "`before`" modes.
|
||||
* `ApplicationEventsTestExecutionListener`: Provides support for
|
||||
xref:testing/testcontext-framework/application-events.adoc[`ApplicationEvents`].
|
||||
* `BeanOverrideTestExecutionListener`: Provides support for xref:testing/testcontext-framework/bean-overriding.adoc[] .
|
||||
* `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test
|
||||
instance.
|
||||
* `MicrometerObservationRegistryTestExecutionListener`: Provides support for
|
||||
@@ -25,7 +26,6 @@ by default, exactly in the following order:
|
||||
* `EventPublishingTestExecutionListener`: Publishes test execution events to the test's
|
||||
`ApplicationContext` (see xref:testing/testcontext-framework/test-execution-events.adoc[Test Execution Events]).
|
||||
* `MockitoResetTestExecutionListener`: Resets mocks as configured by `@MockitoBean` or `@MockitoSpyBean`.
|
||||
* `BeanOverrideTestExecutionListener`: Provides support for xref:testing/testcontext-framework/bean-overriding.adoc[] .
|
||||
|
||||
[[testcontext-tel-config-registering-tels]]
|
||||
== Registering `TestExecutionListener` Implementations
|
||||
|
||||
@@ -47,7 +47,7 @@ Kotlin::
|
||||
NOTE: When using Spring Boot, you may want to use `@Configuration` classes of type `WebFluxConfigurer` but without
|
||||
`@EnableWebFlux` to keep Spring Boot WebFlux customizations. See more details in
|
||||
xref:#webflux-config-customize[the WebFlux config API section] and in
|
||||
{spring-boot-docs}/web.html#web.reactive.webflux.auto-configuration[the dedicated Spring Boot documentation].
|
||||
{spring-boot-docs-ref}/web/reactive.html#web.reactive.webflux.auto-configuration[the dedicated Spring Boot documentation].
|
||||
|
||||
The preceding example registers a number of Spring WebFlux
|
||||
xref:web/webflux/dispatcher-handler.adoc#webflux-special-bean-types[infrastructure beans] and adapts to dependencies
|
||||
@@ -94,7 +94,8 @@ class WebConfig : WebFluxConfigurer {
|
||||
[.small]#xref:web/webmvc/mvc-config/conversion.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
By default, formatters for various number and date types are installed, along with support
|
||||
for customization via `@NumberFormat`, `@DurationFormat` and `@DateTimeFormat` on fields.
|
||||
for customization via `@NumberFormat`, `@DurationFormat`, and `@DateTimeFormat` on fields
|
||||
and parameters.
|
||||
|
||||
To register custom formatters and converters in Java config, use the following:
|
||||
|
||||
@@ -349,7 +350,6 @@ which customizes Jackson's default properties with the following ones:
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {jackson-github-org}/jackson-datatype-joda[`jackson-datatype-joda`]: Support for Joda-Time types.
|
||||
* {jackson-github-org}/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types.
|
||||
* {jackson-github-org}/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`.
|
||||
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
|
||||
|
||||
@@ -419,6 +419,24 @@ controllers. However, when you use it with Spring Security, we advise relying on
|
||||
|
||||
See the section on xref:web/webflux-cors.adoc[CORS] and the xref:web/webflux-cors.adoc#webflux-cors-webfilter[CORS `WebFilter`] for more details.
|
||||
|
||||
[[filters.url-handler]]
|
||||
=== URL Handler
|
||||
[.small]#xref:web/webmvc/filters.adoc#filters.url-handler[See equivalent in the Servlet stack]#
|
||||
|
||||
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
|
||||
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
|
||||
|
||||
Adding trailing slash variants to all mapping declarations is not the best way to handle this use case.
|
||||
The `UrlHandlerFilter` web filter has been designed for this purpose. It can be configured to:
|
||||
|
||||
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
|
||||
* mutate the request to act as if the request was sent without a trailing slash and continue the processing of the request.
|
||||
|
||||
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
|
||||
|
||||
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
|
||||
|
||||
|
||||
|
||||
[[webflux-exception-handler]]
|
||||
== Exceptions
|
||||
|
||||
@@ -9,7 +9,11 @@ The `spring-web` module provides some useful filters:
|
||||
* xref:web/webmvc/filters.adoc#filters-forwarded-headers[Forwarded Headers]
|
||||
* xref:web/webmvc/filters.adoc#filters-shallow-etag[Shallow ETag]
|
||||
* xref:web/webmvc/filters.adoc#filters-cors[CORS]
|
||||
* xref:web/webmvc/filters.adoc#filters.url-handler[URL Handler]
|
||||
|
||||
Servlet filters can be configured in the `web.xml` configuration file or using Servlet annotations.
|
||||
If you are using Spring Boot, you can
|
||||
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare them as beans and configure them as part of your application].
|
||||
|
||||
|
||||
[[filters-http-put]]
|
||||
@@ -109,4 +113,22 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
|
||||
|
||||
|
||||
|
||||
[[filters.url-handler]]
|
||||
== URL Handler
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]#
|
||||
|
||||
In previous Spring Framework versions, Spring MVC could be configured to ignore trailing slashes in URL paths
|
||||
when mapping incoming requests on controller methods. This could be done by enabling the `setUseTrailingSlashMatch`
|
||||
option on the `PathMatchConfigurer`. This means that sending a "GET /home/" request would be handled by a controller
|
||||
method annotated with `@GetMapping("/home")`.
|
||||
|
||||
This option has been retired, but applications are still expected to handle such requests in a safe way.
|
||||
The `UrlHandlerFilter` Servlet filter has been designed for this purpose. It can be configured to:
|
||||
|
||||
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
|
||||
* wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request.
|
||||
|
||||
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
|
||||
|
||||
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
[.small]#xref:web/webflux/config.adoc#webflux-config-conversion[See equivalent in the Reactive stack]#
|
||||
|
||||
By default, formatters for various number and date types are installed, along with support
|
||||
for customization via `@NumberFormat`, `@DurationFormat` and `@DateTimeFormat` on fields.
|
||||
for customization via `@NumberFormat`, `@DurationFormat`, and `@DateTimeFormat` on fields
|
||||
and parameters.
|
||||
|
||||
To register custom formatters and converters, use the following:
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ The caveat to overriding the `/` Servlet mapping is that the `RequestDispatcher`
|
||||
default Servlet must be retrieved by name rather than by path. The
|
||||
`DefaultServletHttpRequestHandler` tries to auto-detect the default Servlet for
|
||||
the container at startup time, using a list of known names for most of the major Servlet
|
||||
containers (including Tomcat, Jetty, GlassFish, JBoss, Resin, WebLogic, and WebSphere).
|
||||
containers (including Tomcat, Jetty, GlassFish, JBoss, WebLogic, and WebSphere).
|
||||
If the default Servlet has been custom-configured with a different name, or if a
|
||||
different Servlet container is being used where the default Servlet name is unknown,
|
||||
then you must explicitly provide the default Servlet's name, as the following example shows:
|
||||
|
||||
@@ -7,7 +7,7 @@ You can use the `@EnableWebMvc` annotation to enable MVC configuration with prog
|
||||
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
NOTE: When using Spring Boot, you may want to use `@Configuration` classes of type `WebMvcConfigurer` but without `@EnableWebMvc` to keep Spring Boot MVC customizations. See more details in xref:web/webmvc/mvc-config/customize.adoc[the MVC Config API section] and in {spring-boot-docs}/web.html#web.servlet.spring-mvc.auto-configuration[the dedicated Spring Boot documentation].
|
||||
NOTE: When using Spring Boot, you may want to use `@Configuration` classes of type `WebMvcConfigurer` but without `@EnableWebMvc` to keep Spring Boot MVC customizations. See more details in xref:web/webmvc/mvc-config/customize.adoc[the MVC Config API section] and in {spring-boot-docs-ref}/web/servlet.html#web.servlet.spring-mvc.auto-configuration[the dedicated Spring Boot documentation].
|
||||
|
||||
The preceding example registers a number of Spring MVC
|
||||
xref:web/webmvc/mvc-servlet/special-bean-types.adoc[infrastructure beans] and adapts to dependencies
|
||||
|
||||
@@ -11,7 +11,7 @@ You can also customize the list of configured message converters at the end by o
|
||||
|
||||
TIP: In a Spring Boot application, the `WebMvcAutoConfiguration` adds any
|
||||
`HttpMessageConverter` beans it detects, in addition to default converters. Hence, in a
|
||||
Boot application, prefer to use the {spring-boot-docs}/web.html#web.servlet.spring-mvc.message-converters[HttpMessageConverters]
|
||||
Boot application, prefer to use the {spring-boot-docs-ref}/web/servlet.html#web.servlet.spring-mvc.message-converters[HttpMessageConverters]
|
||||
mechanism. Or alternatively, use `extendMessageConverters` to modify message converters
|
||||
at the end.
|
||||
|
||||
@@ -35,7 +35,6 @@ This builder customizes Jackson's default properties as follows:
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {jackson-github-org}/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types.
|
||||
* {jackson-github-org}/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types.
|
||||
* {jackson-github-org}/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`.
|
||||
* {jackson-github-org}/jackson-module-kotlin[jackson-module-kotlin]: Support for Kotlin classes and data classes.
|
||||
|
||||
@@ -119,7 +119,7 @@ Some example patterns:
|
||||
* `+"/resources/*.png"+` - match zero or more characters in a path segment
|
||||
* `+"/resources/**"+` - match multiple path segments
|
||||
* `+"/projects/{project}/versions"+` - match a path segment and capture it as a variable
|
||||
* `+"/projects/{project:[a-z]+}/versions"+` - match and capture a variable with a regex
|
||||
* `++"/projects/{project:[a-z]+}/versions"++` - match and capture a variable with a regex
|
||||
|
||||
Captured URI variables can be accessed with `@PathVariable`. For example:
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ the lifecycle of the Servlet container, Spring Boot uses Spring configuration to
|
||||
bootstrap itself and the embedded Servlet container. `Filter` and `Servlet` declarations
|
||||
are detected in Spring configuration and registered with the Servlet container.
|
||||
For more details, see the
|
||||
{spring-boot-docs}/web.html#web.servlet.embedded-container[Spring Boot documentation].
|
||||
{spring-boot-docs-ref}/web/servlet.html#web.servlet.embedded-container[Spring Boot documentation].
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ input is ignored. This is in contrast to property binding which by default binds
|
||||
request parameter for which there is a matching property.
|
||||
|
||||
If neither a dedicated model object nor constructor binding is sufficient, and you must
|
||||
use property binding, we strongy recommend registering `allowedFields` patterns (case
|
||||
use property binding, we strongly recommend registering `allowedFields` patterns (case
|
||||
sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set.
|
||||
For example:
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.11.1",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
}
|
||||
}
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.filters.urlhandler;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.filter.reactive.UrlHandlerFilter;
|
||||
|
||||
public class UrlHandlerFilterConfiguration {
|
||||
|
||||
public void configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will mutate the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").mutateRequest()
|
||||
.build();
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.filters.urlhandler;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.filter.UrlHandlerFilter;
|
||||
|
||||
public class UrlHandlerFilterConfiguration {
|
||||
|
||||
public void configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will wrap the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").wrapRequest()
|
||||
.build();
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.filters.urlhandler
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.filter.reactive.UrlHandlerFilter
|
||||
|
||||
class UrlHandlerFilterConfiguration {
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
fun configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
val urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will mutate the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").mutateRequest()
|
||||
.build()
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.filters.urlhandler
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.filter.UrlHandlerFilter
|
||||
|
||||
class UrlHandlerFilterConfiguration {
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
fun configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
val urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will wrap the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").wrapRequest()
|
||||
.build()
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
@@ -7,32 +7,32 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.0"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.0-RC1"))
|
||||
api(platform("io.netty:netty-bom:4.1.113.Final"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.0"))
|
||||
api(platform("io.netty:netty-bom:4.1.115.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.0-RC1"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.23"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.0"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.4"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.13"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.13"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.15"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.11.2"))
|
||||
api(platform("org.mockito:mockito-bom:5.14.1"))
|
||||
api(platform("org.junit:junit-bom:5.11.3"))
|
||||
api(platform("org.mockito:mockito-bom:5.14.2"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.6.2")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
api("com.google.code.findbugs:jsr305:3.0.2")
|
||||
api("com.google.code.gson:gson:2.10.1")
|
||||
api("com.google.protobuf:protobuf-java-util:4.27.1")
|
||||
api("com.h2database:h2:2.2.224")
|
||||
api("com.google.code.gson:gson:2.11.0")
|
||||
api("com.google.protobuf:protobuf-java-util:4.28.3")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
api("com.rometools:rome:1.19.0")
|
||||
@@ -44,7 +44,7 @@ dependencies {
|
||||
api("com.sun.xml.bind:jaxb-impl:3.0.2")
|
||||
api("com.sun.xml.bind:jaxb-xjc:3.0.2")
|
||||
api("com.thoughtworks.qdox:qdox:2.1.0")
|
||||
api("com.thoughtworks.xstream:xstream:1.4.20")
|
||||
api("com.thoughtworks.xstream:xstream:1.4.21")
|
||||
api("commons-io:commons-io:2.15.0")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("io.micrometer:context-propagation:1.1.1")
|
||||
@@ -54,9 +54,9 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.8")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.9")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.17.Final")
|
||||
api("io.undertow:undertow-core:2.3.18.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.17.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.17.Final")
|
||||
api("io.vavr:vavr:0.10.4")
|
||||
@@ -100,8 +100,8 @@ dependencies {
|
||||
api("org.apache.derby:derby:10.16.1.1")
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
api("org.apache.derby:derbytools:10.16.1.1")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4.1")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.1")
|
||||
api("org.apache.poi:poi-ooxml:5.2.5")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
|
||||
@@ -116,7 +116,7 @@ dependencies {
|
||||
api("org.crac:crac:1.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.easymock:easymock:5.4.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.7")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.8")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -129,25 +129,25 @@ dependencies {
|
||||
api("org.hamcrest:hamcrest:2.2")
|
||||
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.2")
|
||||
api("org.htmlunit:htmlunit:4.2.0")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.6.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jruby:jruby:9.4.8.0")
|
||||
api("org.jruby:jruby:9.4.9.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.mozilla:rhino:1.7.15")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
api("org.python:jython-standalone:2.7.3")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.20.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.20.0")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.26.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.26.0")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.16")
|
||||
api("org.testng:testng:7.9.0")
|
||||
api("org.testng:testng:7.10.2")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.55")
|
||||
api("org.webjars:webjars-locator-lite:1.0.0")
|
||||
api("org.xmlunit:xmlunit-assertj:2.9.1")
|
||||
api("org.xmlunit:xmlunit-matchers:2.9.1")
|
||||
api("org.yaml:snakeyaml:2.2")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.0")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.0")
|
||||
api("org.yaml:snakeyaml:2.3")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.2.0-SNAPSHOT
|
||||
version=6.2.0
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -55,8 +55,9 @@ plugins.withType(JavaPlugin).configureEach {
|
||||
languageVersion = testLanguageVersion
|
||||
}
|
||||
// Enable Java experimental support in Bytebuddy
|
||||
// Remove when JDK 22 is supported by Mockito
|
||||
if (testLanguageVersion == JavaLanguageVersion.of(22)) {
|
||||
// Bytebuddy 1.15.4 supports JDK <= 24
|
||||
// see https://github.com/raphw/byte-buddy/blob/master/release-notes.md
|
||||
if (testLanguageVersion.compareTo(JavaLanguageVersion.of(24)) > 0 ) {
|
||||
jvmArgs("-Dnet.bytebuddy.experimental=true")
|
||||
}
|
||||
}
|
||||
|
||||
+59
-59
@@ -56,33 +56,33 @@ import kotlin.annotation.AnnotationTarget.TYPE
|
||||
@SpringJUnitConfig(InterceptorConfig::class)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
@Autowired val echo: Echo,
|
||||
@Autowired val firstAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val countingAspect: CountingAspect,
|
||||
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
|
||||
@Autowired val echo: Echo,
|
||||
@Autowired val firstAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val countingAspect: CountingAspect,
|
||||
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
|
||||
|
||||
@Test
|
||||
fun `Multiple interceptors with regular function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
assertThat(echo.echo(value)).isEqualTo(value)
|
||||
@Test
|
||||
fun `Multiple interceptors with regular function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
assertThat(echo.echo(value)).isEqualTo(value)
|
||||
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
|
||||
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Multiple interceptors with suspending function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
runBlocking {
|
||||
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
|
||||
}
|
||||
@Test
|
||||
fun `Multiple interceptors with suspending function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
runBlocking {
|
||||
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
|
||||
}
|
||||
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
|
||||
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test // gh-33095
|
||||
fun `Aspect and reactive transactional with suspending function`() {
|
||||
@@ -113,17 +113,17 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
assertThat(countingAspect.counter).`as`("aspect applied once per key").isEqualTo(2)
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
@EnableTransactionManagement
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
@EnableTransactionManagement
|
||||
@EnableCaching
|
||||
open class InterceptorConfig {
|
||||
open class InterceptorConfig {
|
||||
|
||||
@Bean
|
||||
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
|
||||
@Bean
|
||||
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
|
||||
|
||||
@Bean
|
||||
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
|
||||
@Bean
|
||||
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
|
||||
|
||||
@Bean
|
||||
open fun countingAspect() = CountingAspect()
|
||||
@@ -138,34 +138,34 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
return ConcurrentMapCacheManager()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun echo(): Echo {
|
||||
return Echo()
|
||||
}
|
||||
}
|
||||
@Bean
|
||||
open fun echo(): Echo {
|
||||
return Echo()
|
||||
}
|
||||
}
|
||||
|
||||
class TestMethodInterceptor: MethodInterceptor {
|
||||
class TestMethodInterceptor: MethodInterceptor {
|
||||
|
||||
var invocations: MutableList<Class<*>> = mutableListOf()
|
||||
var invocations: MutableList<Class<*>> = mutableListOf()
|
||||
|
||||
@Suppress("RedundantNullableReturnType")
|
||||
override fun invoke(invocation: MethodInvocation): Any? {
|
||||
val result = invocation.proceed()
|
||||
invocations.add(result!!.javaClass)
|
||||
return result
|
||||
}
|
||||
@Suppress("RedundantNullableReturnType")
|
||||
override fun invoke(invocation: MethodInvocation): Any? {
|
||||
val result = invocation.proceed()
|
||||
invocations.add(result!!.javaClass)
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
|
||||
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
|
||||
|
||||
val interceptor: TestMethodInterceptor
|
||||
get() = advice as TestMethodInterceptor
|
||||
val interceptor: TestMethodInterceptor
|
||||
get() = advice as TestMethodInterceptor
|
||||
|
||||
override fun matches(method: Method, targetClass: Class<*>): Boolean {
|
||||
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
|
||||
}
|
||||
}
|
||||
override fun matches(method: Method, targetClass: Class<*>): Boolean {
|
||||
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
|
||||
}
|
||||
}
|
||||
|
||||
@Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@@ -185,16 +185,16 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
}
|
||||
}
|
||||
|
||||
open class Echo {
|
||||
open class Echo {
|
||||
|
||||
open fun echo(value: String): String {
|
||||
return value
|
||||
}
|
||||
open fun echo(value: String): String {
|
||||
return value
|
||||
}
|
||||
|
||||
open suspend fun suspendingEcho(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
open suspend fun suspendingEcho(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Counting
|
||||
@@ -212,6 +212,6 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
return "$value ${cacheCounter++}"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+30
-1
@@ -18,6 +18,7 @@ package org.springframework.aop.aspectj.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
@@ -37,6 +38,7 @@ import org.aspectj.lang.reflect.PerClauseKind;
|
||||
|
||||
import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.SpringProperties;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -58,6 +60,23 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
|
||||
Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
|
||||
|
||||
private static final String AJC_MAGIC = "ajc$";
|
||||
|
||||
/**
|
||||
* System property that instructs Spring to ignore ajc-compiled aspects
|
||||
* for Spring AOP proxying, restoring traditional Spring behavior for
|
||||
* scenarios where both weaving and AspectJ auto-proxying are enabled.
|
||||
* <p>The default is "false". Consider switching this to "true" if you
|
||||
* encounter double execution of your aspects in a given build setup.
|
||||
* Note that we recommend restructuring your AspectJ configuration to
|
||||
* avoid such double exposure of an AspectJ aspect to begin with.
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public static final String IGNORE_AJC_PROPERTY_NAME = "spring.aop.ajc.ignore";
|
||||
|
||||
private static final boolean shouldIgnoreAjcCompiledAspects =
|
||||
SpringProperties.getFlag(IGNORE_AJC_PROPERTY_NAME);
|
||||
|
||||
|
||||
/** Logger available to subclasses. */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
@@ -67,7 +86,8 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
|
||||
@Override
|
||||
public boolean isAspect(Class<?> clazz) {
|
||||
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
|
||||
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null &&
|
||||
(!shouldIgnoreAjcCompiledAspects || !compiledByAjc(clazz)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,6 +134,15 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compiledByAjc(Class<?> clazz) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getName().startsWith(AJC_MAGIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enum for AspectJ annotation types.
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -281,8 +281,8 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware {
|
||||
* @param returnType the declared return type (potentially a {@link Future} variant)
|
||||
* @return the execution result (potentially a corresponding {@link Future} handle)
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Nullable
|
||||
@SuppressWarnings("deprecation")
|
||||
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
|
||||
if (CompletableFuture.class.isAssignableFrom(returnType)) {
|
||||
return executor.submitCompletable(task);
|
||||
|
||||
+27
-49
@@ -16,30 +16,32 @@
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.assertj.core.api.WithAssertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.mockito.BDDMockito.doAnswer;
|
||||
import static org.mockito.BDDMockito.doThrow;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Mikaël Francoeur
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see JdkProxyExceptionHandlingTests
|
||||
* @see CglibProxyExceptionHandlingTests
|
||||
*/
|
||||
abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
abstract class AbstractProxyExceptionHandlingTests {
|
||||
|
||||
private static final RuntimeException uncheckedException = new RuntimeException();
|
||||
|
||||
@@ -47,14 +49,12 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
private static final UndeclaredCheckedException undeclaredCheckedException = new UndeclaredCheckedException();
|
||||
|
||||
protected final MyClass target = mock(MyClass.class);
|
||||
protected final MyClass target = mock();
|
||||
|
||||
protected final ProxyFactory proxyFactory = new ProxyFactory(target);
|
||||
|
||||
@Nullable
|
||||
protected MyInterface proxy;
|
||||
|
||||
@Nullable
|
||||
private Throwable throwableSeenByCaller;
|
||||
|
||||
|
||||
@@ -63,46 +63,24 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
Mockito.clearInvocations(target);
|
||||
}
|
||||
|
||||
protected void assertProxyType(Object proxy) {
|
||||
}
|
||||
|
||||
protected abstract void assertProxyType(Object proxy);
|
||||
|
||||
|
||||
private void invokeProxy() {
|
||||
throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething());
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private Answer<?> sneakyThrow(Throwable throwable) {
|
||||
private static Answer<?> sneakyThrow(Throwable throwable) {
|
||||
return invocation -> {
|
||||
throw throwable;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
static class JdkAopProxyTests extends ProxyExceptionHandlingTests {
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Proxy.isProxyClass(proxy.getClass())).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class CglibAopProxyTests extends ProxyExceptionHandlingTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
proxyFactory.setProxyTargetClass(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Enhancer.isEnhanced(proxy.getClass())).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class WhenThereIsOneInterceptor {
|
||||
class WhenThereIsOneInterceptorTests {
|
||||
|
||||
@Nullable
|
||||
private Throwable throwableSeenByInterceptor;
|
||||
@@ -110,13 +88,13 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
proxyFactory.addAdvice(captureThrowable());
|
||||
proxy = (MyInterface) proxyFactory.getProxy(ProxyExceptionHandlingTests.class.getClassLoader());
|
||||
proxy = (MyInterface) proxyFactory.getProxy(getClass().getClassLoader());
|
||||
assertProxyType(proxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doAnswer(sneakyThrow(undeclaredCheckedException)).when(target).doSomething();
|
||||
willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByInterceptor).isSameAs(undeclaredCheckedException);
|
||||
assertThat(throwableSeenByCaller)
|
||||
@@ -126,7 +104,7 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
@Test
|
||||
void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doThrow(declaredCheckedException).when(target).doSomething();
|
||||
willThrow(declaredCheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByInterceptor).isSameAs(declaredCheckedException);
|
||||
assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException);
|
||||
@@ -134,7 +112,7 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
@Test
|
||||
void targetThrowsUncheckedException() throws DeclaredCheckedException {
|
||||
doThrow(uncheckedException).when(target).doSomething();
|
||||
willThrow(uncheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByInterceptor).isSameAs(uncheckedException);
|
||||
assertThat(throwableSeenByCaller).isSameAs(uncheckedException);
|
||||
@@ -155,17 +133,17 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
|
||||
@Nested
|
||||
class WhenThereAreNoInterceptors {
|
||||
class WhenThereAreNoInterceptorsTests {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
proxy = (MyInterface) proxyFactory.getProxy(ProxyExceptionHandlingTests.class.getClassLoader());
|
||||
proxy = (MyInterface) proxyFactory.getProxy(getClass().getClassLoader());
|
||||
assertProxyType(proxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doAnswer(sneakyThrow(undeclaredCheckedException)).when(target).doSomething();
|
||||
willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByCaller)
|
||||
.isInstanceOf(UndeclaredThrowableException.class)
|
||||
@@ -174,21 +152,21 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
@Test
|
||||
void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doThrow(declaredCheckedException).when(target).doSomething();
|
||||
willThrow(declaredCheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsUncheckedException() throws DeclaredCheckedException {
|
||||
doThrow(uncheckedException).when(target).doSomething();
|
||||
willThrow(uncheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByCaller).isSameAs(uncheckedException);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected interface MyInterface {
|
||||
interface MyInterface {
|
||||
|
||||
void doSomething() throws DeclaredCheckedException;
|
||||
}
|
||||
@@ -202,11 +180,11 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected static class UndeclaredCheckedException extends Exception {
|
||||
private static class UndeclaredCheckedException extends Exception {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected static class DeclaredCheckedException extends Exception {
|
||||
private static class DeclaredCheckedException extends Exception {
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Mikaël Francoeur
|
||||
* @since 6.2
|
||||
* @see JdkProxyExceptionHandlingTests
|
||||
*/
|
||||
class CglibProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
proxyFactory.setProxyTargetClass(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Enhancer.isEnhanced(proxy.getClass())).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Mikaël Francoeur
|
||||
* @since 6.2
|
||||
* @see CglibProxyExceptionHandlingTests
|
||||
*/
|
||||
class JdkProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests {
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Proxy.isProxyClass(proxy.getClass())).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,17 +31,17 @@ import kotlin.coroutines.Continuation
|
||||
*/
|
||||
class AopUtilsKotlinTests {
|
||||
|
||||
@Test
|
||||
fun `Invoking suspending function should return Mono`() {
|
||||
val value = "foo"
|
||||
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
|
||||
@Test
|
||||
fun `Invoking suspending function should return Mono`() {
|
||||
val value = "foo"
|
||||
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
|
||||
String::class. java, Continuation::class.java)!!
|
||||
val continuation = Continuation<Any>(CoroutineName("test")) { }
|
||||
val continuation = Continuation<Any>(CoroutineName("test")) { }
|
||||
val result = AopUtils.invokeJoinpointUsingReflection(WithoutInterface(), method, arrayOf(value, continuation))
|
||||
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
|
||||
assertThat(it.block()).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
|
||||
assertThat(it.block()).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Invoking suspending function on bridged method should return Mono`() {
|
||||
@@ -54,11 +54,11 @@ class AopUtilsKotlinTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
suspend fun suspendingFunction(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
@Suppress("unused")
|
||||
suspend fun suspendingFunction(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
|
||||
class WithoutInterface {
|
||||
suspend fun handle(value: String): String {
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,10 +28,10 @@ import org.springframework.scheduling.annotation.Async;
|
||||
* <p>This aspect routes methods marked with the {@link Async} annotation as well as methods
|
||||
* in classes marked with the same. Any method expected to be routed asynchronously must
|
||||
* return either {@code void}, {@link Future}, or a subtype of {@link Future} (in particular,
|
||||
* Spring's {@link org.springframework.util.concurrent.ListenableFuture}). This aspect,
|
||||
* therefore, will produce a compile-time error for methods that violate this constraint
|
||||
* on the return type. If, however, a class marked with {@code @Async} contains a method
|
||||
* that violates this constraint, it produces only a warning.
|
||||
* {@link java.util.concurrent.CompletableFuture}). This aspect, therefore, will produce a
|
||||
* compile-time error for methods that violate this constraint on the return type. If,
|
||||
* however, a class marked with {@code @Async} contains a method that violates this
|
||||
* constraint, it produces only a warning.
|
||||
*
|
||||
* <p>This aspect needs to be injected with an implementation of a task-oriented
|
||||
* {@link java.util.concurrent.Executor} to activate it for a specific thread pool,
|
||||
|
||||
+33
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -92,9 +92,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* Return a provider for the specified bean, allowing for lazy on-demand retrieval
|
||||
* of instances, including availability and uniqueness options.
|
||||
* @param requiredType type the bean must match; can be an interface or superclass
|
||||
* @param allowEagerInit whether stream-based access may initialize <i>lazy-init
|
||||
* singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods
|
||||
* with a "factory-bean" reference) for the type check
|
||||
* @param allowEagerInit whether stream access may introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return a corresponding provider handle
|
||||
* @since 5.3
|
||||
* @see #getBeanProvider(ResolvableType, boolean)
|
||||
@@ -112,9 +116,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* injection points. For programmatically retrieving a list of beans matching a
|
||||
* specific type, specify the actual bean type as an argument here and subsequently
|
||||
* use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options.
|
||||
* @param allowEagerInit whether stream-based access may initialize <i>lazy-init
|
||||
* singletons</i> and <i>objects created by FactoryBeans</i> (or by factory methods
|
||||
* with a "factory-bean" reference) for the type check
|
||||
* @param allowEagerInit whether stream access may introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return a corresponding provider handle
|
||||
* @since 5.3
|
||||
* @see #getBeanProvider(ResolvableType)
|
||||
@@ -175,11 +183,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* @param type the generically typed class or interface to match
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
* <i>objects created by FactoryBeans</i> (or by factory methods with a
|
||||
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
|
||||
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references.
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return the names of beans (or objects created by FactoryBeans) matching
|
||||
* the given object type (including subclasses), or an empty array if none
|
||||
* @since 5.2
|
||||
@@ -236,11 +246,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* @param type the class or interface to match, or {@code null} for all bean names
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
* <i>objects created by FactoryBeans</i> (or by factory methods with a
|
||||
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
|
||||
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references.
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return the names of beans (or objects created by FactoryBeans) matching
|
||||
* the given object type (including subclasses), or an empty array if none
|
||||
* @see FactoryBean#getObjectType
|
||||
@@ -300,11 +312,13 @@ public interface ListableBeanFactory extends BeanFactory {
|
||||
* @param type the class or interface to match, or {@code null} for all concrete beans
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
* <i>objects created by FactoryBeans</i> (or by factory methods with a
|
||||
* "factory-bean" reference) for the type check. Note that FactoryBeans need to be
|
||||
* @param allowEagerInit whether to introspect <i>lazy-init singletons</i>
|
||||
* and <i>objects created by FactoryBeans</i> - or by factory methods with a
|
||||
* "factory-bean" reference - for the type check. Note that FactoryBeans need to be
|
||||
* eagerly initialized to determine their type: So be aware that passing in "true"
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references.
|
||||
* for this flag will initialize FactoryBeans and "factory-bean" references. Only
|
||||
* actually necessary initialization for type checking purposes will be performed;
|
||||
* constructor and method invocations will still be avoided as far as possible.
|
||||
* @return a Map with the matching beans, containing the bean names as
|
||||
* keys and the corresponding bean instances as values
|
||||
* @throws BeansException if a bean could not be created
|
||||
|
||||
@@ -32,6 +32,8 @@ import org.springframework.lang.Nullable;
|
||||
* <p>In a {@link BeanFactory} environment, every {@code ObjectProvider} obtained
|
||||
* from the factory will be bound to its {@code BeanFactory} for a specific bean
|
||||
* type, matching all provider calls against factory-registered bean definitions.
|
||||
* Note that all such calls dynamically operate on the underlying factory state,
|
||||
* freshly resolving the requested target object on every call.
|
||||
*
|
||||
* <p>As of 5.1, this interface extends {@link Iterable} and provides {@link Stream}
|
||||
* support. It can be therefore be used in {@code for} loops, provides {@link #forEach}
|
||||
|
||||
+21
-12
@@ -154,26 +154,35 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
|
||||
*/
|
||||
@Override
|
||||
public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
|
||||
boolean match = super.isAutowireCandidate(bdHolder, descriptor);
|
||||
if (match) {
|
||||
match = checkQualifiers(bdHolder, descriptor.getAnnotations());
|
||||
if (match) {
|
||||
MethodParameter methodParam = descriptor.getMethodParameter();
|
||||
if (methodParam != null) {
|
||||
Method method = methodParam.getMethod();
|
||||
if (method == null || void.class == method.getReturnType()) {
|
||||
match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
|
||||
if (!super.isAutowireCandidate(bdHolder, descriptor)) {
|
||||
return false;
|
||||
}
|
||||
Boolean checked = checkQualifiers(bdHolder, descriptor.getAnnotations());
|
||||
if (checked != Boolean.FALSE) {
|
||||
MethodParameter methodParam = descriptor.getMethodParameter();
|
||||
if (methodParam != null) {
|
||||
Method method = methodParam.getMethod();
|
||||
if (method == null || void.class == method.getReturnType()) {
|
||||
Boolean methodChecked = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
|
||||
if (methodChecked != null && checked == null) {
|
||||
checked = methodChecked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return match;
|
||||
return (checked == Boolean.TRUE ||
|
||||
(checked == null && ((RootBeanDefinition) bdHolder.getBeanDefinition()).isDefaultCandidate()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Match the given qualifier annotations against the candidate bean definition.
|
||||
* @return {@code false} if a qualifier has been found but not matched,
|
||||
* {@code true} if a qualifier has been found and matched,
|
||||
* {@code null} if no qualifier has been found at all
|
||||
*/
|
||||
protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
|
||||
|
||||
@Nullable
|
||||
protected Boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
|
||||
boolean qualifierFound = false;
|
||||
if (!ObjectUtils.isEmpty(annotationsToSearch)) {
|
||||
SimpleTypeConverter typeConverter = new SimpleTypeConverter();
|
||||
@@ -217,7 +226,7 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
|
||||
}
|
||||
}
|
||||
}
|
||||
return (qualifierFound || ((RootBeanDefinition) bdHolder.getBeanDefinition()).isDefaultCandidate());
|
||||
return (qualifierFound ? true : null);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
-3
@@ -1996,12 +1996,15 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
* @param requiredType the target dependency type to match against
|
||||
* @return the name of the candidate with the highest priority,
|
||||
* or {@code null} if none found
|
||||
* @throws NoUniqueBeanDefinitionException if multiple beans are detected with
|
||||
* the same highest priority value
|
||||
* @see #getPriority(Object)
|
||||
*/
|
||||
@Nullable
|
||||
protected String determineHighestPriorityCandidate(Map<String, Object> candidates, Class<?> requiredType) {
|
||||
String highestPriorityBeanName = null;
|
||||
Integer highestPriority = null;
|
||||
boolean highestPriorityConflictDetected = false;
|
||||
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
|
||||
String candidateBeanName = entry.getKey();
|
||||
Object beanInstance = entry.getValue();
|
||||
@@ -2010,13 +2013,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
if (candidatePriority != null) {
|
||||
if (highestPriority != null) {
|
||||
if (candidatePriority.equals(highestPriority)) {
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
|
||||
"Multiple beans found with the same priority ('" + highestPriority +
|
||||
"') among candidates: " + candidates.keySet());
|
||||
highestPriorityConflictDetected = true;
|
||||
}
|
||||
else if (candidatePriority < highestPriority) {
|
||||
highestPriorityBeanName = candidateBeanName;
|
||||
highestPriority = candidatePriority;
|
||||
highestPriorityConflictDetected = false;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -2026,6 +2028,13 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (highestPriorityConflictDetected) {
|
||||
throw new NoUniqueBeanDefinitionException(requiredType, candidates.size(),
|
||||
"Multiple beans found with the same highest priority (" + highestPriority +
|
||||
") among candidates: " + candidates.keySet());
|
||||
|
||||
}
|
||||
return highestPriorityBeanName;
|
||||
}
|
||||
|
||||
|
||||
+79
-4
@@ -1718,18 +1718,73 @@ class DefaultListableBeanFactoryTests {
|
||||
assertThat(bean.getBeanName()).isEqualTo("bd1");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code determineHighestPriorityCandidate()} should reject duplicate
|
||||
* priorities for the highest priority detected.
|
||||
*
|
||||
* @see #getBeanByTypeWithMultipleNonHighestPriorityCandidates()
|
||||
*/
|
||||
@Test
|
||||
void getBeanByTypeWithMultiplePriority() {
|
||||
void getBeanByTypeWithMultipleHighestPriorityCandidates() {
|
||||
lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
|
||||
RootBeanDefinition bd1 = new RootBeanDefinition(HighPriorityTestBean.class);
|
||||
RootBeanDefinition bd2 = new RootBeanDefinition(HighPriorityTestBean.class);
|
||||
RootBeanDefinition bd2 = new RootBeanDefinition(LowPriorityTestBean.class);
|
||||
RootBeanDefinition bd3 = new RootBeanDefinition(HighPriorityTestBean.class);
|
||||
lbf.registerBeanDefinition("bd1", bd1);
|
||||
lbf.registerBeanDefinition("bd2", bd2);
|
||||
lbf.registerBeanDefinition("bd3", bd3);
|
||||
|
||||
assertThatExceptionOfType(NoUniqueBeanDefinitionException.class)
|
||||
.isThrownBy(() -> lbf.getBean(TestBean.class))
|
||||
.withMessageContaining("Multiple beans found with the same priority")
|
||||
.withMessageContaining("5"); // conflicting priority
|
||||
.withMessageContaining("Multiple beans found with the same highest priority (5) among candidates: ");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@code determineHighestPriorityCandidate()} should ignore duplicate
|
||||
* priorities for any priority other than the highest, and the order in
|
||||
* which beans is declared should not affect the outcome.
|
||||
*
|
||||
* @see #getBeanByTypeWithMultipleHighestPriorityCandidates()
|
||||
*/
|
||||
@Test // gh-33733
|
||||
void getBeanByTypeWithMultipleNonHighestPriorityCandidates() {
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService1.class,
|
||||
PriorityService2A.class,
|
||||
PriorityService2B.class,
|
||||
PriorityService3.class
|
||||
);
|
||||
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService3.class,
|
||||
PriorityService2B.class,
|
||||
PriorityService2A.class,
|
||||
PriorityService1.class
|
||||
);
|
||||
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService2A.class,
|
||||
PriorityService1.class,
|
||||
PriorityService2B.class,
|
||||
PriorityService3.class
|
||||
);
|
||||
|
||||
getBeanByTypeWithMultipleNonHighestPriorityCandidates(
|
||||
PriorityService2A.class,
|
||||
PriorityService3.class,
|
||||
PriorityService1.class,
|
||||
PriorityService2B.class
|
||||
);
|
||||
}
|
||||
|
||||
private void getBeanByTypeWithMultipleNonHighestPriorityCandidates(Class<?>... classes) {
|
||||
lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE);
|
||||
for (Class<?> clazz : classes) {
|
||||
lbf.registerBeanDefinition(clazz.getSimpleName(), new RootBeanDefinition(clazz));
|
||||
}
|
||||
|
||||
PriorityService bean = lbf.getBean(PriorityService.class);
|
||||
assertThat(bean).isExactlyInstanceOf(PriorityService1.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -3519,6 +3574,26 @@ class DefaultListableBeanFactoryTests {
|
||||
}
|
||||
|
||||
|
||||
interface PriorityService {
|
||||
}
|
||||
|
||||
@Priority(1)
|
||||
static class PriorityService1 implements PriorityService {
|
||||
}
|
||||
|
||||
@Priority(2)
|
||||
static class PriorityService2A implements PriorityService {
|
||||
}
|
||||
|
||||
@Priority(2)
|
||||
static class PriorityService2B implements PriorityService {
|
||||
}
|
||||
|
||||
@Priority(3)
|
||||
static class PriorityService3 implements PriorityService {
|
||||
}
|
||||
|
||||
|
||||
@Priority(5)
|
||||
private static class HighPriorityTestBean extends TestBean {
|
||||
}
|
||||
|
||||
+85
-85
@@ -43,36 +43,36 @@ import javax.lang.model.element.Modifier
|
||||
*/
|
||||
class InstanceSupplierCodeGeneratorKotlinTests {
|
||||
|
||||
private val generationContext = TestGenerationContext()
|
||||
private val generationContext = TestGenerationContext()
|
||||
|
||||
private val beanFactory = DefaultListableBeanFactory()
|
||||
|
||||
@Test
|
||||
fun generateWhenHasDefaultConstructor() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
|
||||
}
|
||||
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
|
||||
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
|
||||
}
|
||||
@Test
|
||||
fun generateWhenHasDefaultConstructor() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBean::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean = getBean<KotlinTestBean>(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)")
|
||||
}
|
||||
Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java))
|
||||
.satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateWhenConstructorHasOptionalParameter() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile)
|
||||
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
|
||||
}
|
||||
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
|
||||
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
|
||||
}
|
||||
@Test
|
||||
fun generateWhenConstructorHasOptionalParameter() {
|
||||
val beanDefinition: BeanDefinition = RootBeanDefinition(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
val beanFactory = DefaultListableBeanFactory()
|
||||
compile(beanFactory, beanDefinition) { instanceSupplier, compiled ->
|
||||
val bean: KotlinTestBeanWithOptionalParameter = getBean(beanFactory, beanDefinition, instanceSupplier)
|
||||
Assertions.assertThat(bean).isInstanceOf(KotlinTestBeanWithOptionalParameter::class.java)
|
||||
Assertions.assertThat(compiled.sourceFile)
|
||||
.contains("return BeanInstanceSupplier.<KotlinTestBeanWithOptionalParameter>forConstructor();")
|
||||
}
|
||||
Assertions.assertThat<TypeHint>(getReflectionHints().getTypeHint(KotlinTestBeanWithOptionalParameter::class.java))
|
||||
.satisfies(hasMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun generateWhenHasFactoryMethodWithNoArg() {
|
||||
@@ -107,27 +107,27 @@ class InstanceSupplierCodeGeneratorKotlinTests {
|
||||
}
|
||||
}
|
||||
|
||||
private fun getReflectionHints(): ReflectionHints {
|
||||
return generationContext.runtimeHints.reflection()
|
||||
}
|
||||
private fun getReflectionHints(): ReflectionHints {
|
||||
return generationContext.runtimeHints.reflection()
|
||||
}
|
||||
|
||||
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
|
||||
}
|
||||
}
|
||||
private fun hasConstructorWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.constructors()).anySatisfy(hasMode(mode))
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.memberCategories).contains(category)
|
||||
}
|
||||
}
|
||||
private fun hasMemberCategory(category: MemberCategory): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.memberCategories).contains(category)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.mode).isEqualTo(mode)
|
||||
}
|
||||
}
|
||||
private fun hasMode(mode: ExecutableMode): ThrowingConsumer<ExecutableHint> {
|
||||
return ThrowingConsumer {
|
||||
Assertions.assertThat(it.mode).isEqualTo(mode)
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasMethodWithMode(mode: ExecutableMode): ThrowingConsumer<TypeHint> {
|
||||
return ThrowingConsumer { hint: TypeHint ->
|
||||
@@ -135,48 +135,48 @@ class InstanceSupplierCodeGeneratorKotlinTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
instanceSupplier: InstanceSupplier<*>): T {
|
||||
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
|
||||
beanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
return beanFactory.getBean("testBean") as T
|
||||
}
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T> getBean(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
instanceSupplier: InstanceSupplier<*>): T {
|
||||
(beanDefinition as RootBeanDefinition).instanceSupplier = instanceSupplier
|
||||
beanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
return beanFactory.getBean("testBean") as T
|
||||
}
|
||||
|
||||
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
|
||||
private fun compile(beanFactory: DefaultListableBeanFactory, beanDefinition: BeanDefinition,
|
||||
result: BiConsumer<InstanceSupplier<*>, Compiled>) {
|
||||
|
||||
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
|
||||
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
|
||||
val typeBuilder = DeferredTypeBuilder()
|
||||
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
|
||||
val generator = InstanceSupplierCodeGenerator(
|
||||
generationContext, generateClass.name,
|
||||
generateClass.methods, false
|
||||
)
|
||||
val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor()
|
||||
Assertions.assertThat(instantiationDescriptor).isNotNull()
|
||||
val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor)
|
||||
typeBuilder.set { type: TypeSpec.Builder ->
|
||||
type.addModifiers(Modifier.PUBLIC)
|
||||
type.addSuperinterface(
|
||||
ParameterizedTypeName.get(
|
||||
Supplier::class.java,
|
||||
InstanceSupplier::class.java
|
||||
)
|
||||
)
|
||||
type.addMethod(
|
||||
MethodSpec.methodBuilder("get")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(InstanceSupplier::class.java)
|
||||
.addStatement("return \$L", generatedCode).build()
|
||||
)
|
||||
}
|
||||
generationContext.writeGeneratedContent()
|
||||
TestCompiler.forSystem().with(generationContext).compile {
|
||||
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
|
||||
}
|
||||
}
|
||||
val freshBeanFactory = DefaultListableBeanFactory(beanFactory)
|
||||
freshBeanFactory.registerBeanDefinition("testBean", beanDefinition)
|
||||
val registeredBean = RegisteredBean.of(freshBeanFactory, "testBean")
|
||||
val typeBuilder = DeferredTypeBuilder()
|
||||
val generateClass = generationContext.generatedClasses.addForFeature("TestCode", typeBuilder)
|
||||
val generator = InstanceSupplierCodeGenerator(
|
||||
generationContext, generateClass.name,
|
||||
generateClass.methods, false
|
||||
)
|
||||
val instantiationDescriptor = registeredBean.resolveInstantiationDescriptor()
|
||||
Assertions.assertThat(instantiationDescriptor).isNotNull()
|
||||
val generatedCode = generator.generateCode(registeredBean, instantiationDescriptor)
|
||||
typeBuilder.set { type: TypeSpec.Builder ->
|
||||
type.addModifiers(Modifier.PUBLIC)
|
||||
type.addSuperinterface(
|
||||
ParameterizedTypeName.get(
|
||||
Supplier::class.java,
|
||||
InstanceSupplier::class.java
|
||||
)
|
||||
)
|
||||
type.addMethod(
|
||||
MethodSpec.methodBuilder("get")
|
||||
.addModifiers(Modifier.PUBLIC)
|
||||
.returns(InstanceSupplier::class.java)
|
||||
.addStatement("return \$L", generatedCode).build()
|
||||
)
|
||||
}
|
||||
generationContext.writeGeneratedContent()
|
||||
TestCompiler.forSystem().with(generationContext).compile {
|
||||
result.accept(it.getInstance(Supplier::class.java).get() as InstanceSupplier<*>, it)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Vendored
+12
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -303,6 +303,17 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
this.cacheMap.put(name, adaptCaffeineCache(name, cache));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified cache from this cache manager, applying to
|
||||
* custom caches as well as dynamically registered caches at runtime.
|
||||
* @param name the name of the cache
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public void removeCache(String name) {
|
||||
this.customCacheNames.remove(name);
|
||||
this.cacheMap.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given new native Caffeine Cache instance to Spring's {@link Cache}
|
||||
* abstraction for the specified cache name.
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,7 +47,7 @@ import org.springframework.util.concurrent.ListenableFutureTask;
|
||||
* @see org.springframework.core.task.TaskExecutor
|
||||
* @see SchedulerFactoryBean#setTaskExecutor
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool
|
||||
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, InitializingBean, DisposableBean {
|
||||
|
||||
|
||||
+4
@@ -32,6 +32,10 @@ import org.springframework.lang.Nullable;
|
||||
* {@link #cacheManager()}, {@link #cacheResolver()}, {@link #keyGenerator()}, and
|
||||
* {@link #errorHandler()} for detailed instructions.
|
||||
*
|
||||
* <p><b>NOTE: A {@code CachingConfigurer} will get initialized early.</b>
|
||||
* Do not inject common dependencies into autowired fields directly; instead, consider
|
||||
* declaring a lazy {@link org.springframework.beans.factory.ObjectProvider} for those.
|
||||
*
|
||||
* @author Chris Beams
|
||||
* @author Stephane Nicoll
|
||||
* @since 3.1
|
||||
|
||||
Vendored
+10
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -175,6 +175,15 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
|
||||
return cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified cache from this cache manager.
|
||||
* @param name the name of the cache
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public void removeCache(String name) {
|
||||
this.cacheMap.remove(name);
|
||||
}
|
||||
|
||||
private void recreateCaches() {
|
||||
for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
|
||||
entry.setValue(createConcurrentMapCache(entry.getKey()));
|
||||
|
||||
+99
-42
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.context.annotation;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collection;
|
||||
@@ -27,7 +28,6 @@ import java.util.Set;
|
||||
|
||||
import org.springframework.aop.TargetSource;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver;
|
||||
import org.springframework.beans.factory.config.DependencyDescriptor;
|
||||
@@ -35,7 +35,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Complete implementation of the
|
||||
@@ -85,47 +84,13 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat
|
||||
}
|
||||
|
||||
private Object buildLazyResolutionProxy(
|
||||
final DependencyDescriptor descriptor, @Nullable final String beanName, boolean classOnly) {
|
||||
DependencyDescriptor descriptor, @Nullable String beanName, boolean classOnly) {
|
||||
|
||||
BeanFactory beanFactory = getBeanFactory();
|
||||
Assert.state(beanFactory instanceof DefaultListableBeanFactory,
|
||||
"BeanFactory needs to be a DefaultListableBeanFactory");
|
||||
final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
|
||||
if (!(getBeanFactory() instanceof DefaultListableBeanFactory dlbf)) {
|
||||
throw new IllegalStateException("Lazy resolution only supported with DefaultListableBeanFactory");
|
||||
}
|
||||
|
||||
TargetSource ts = new TargetSource() {
|
||||
@Override
|
||||
public Class<?> getTargetClass() {
|
||||
return descriptor.getDependencyType();
|
||||
}
|
||||
@Override
|
||||
@SuppressWarnings("NullAway")
|
||||
public Object getTarget() {
|
||||
Set<String> autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null);
|
||||
Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, null);
|
||||
if (target == null) {
|
||||
Class<?> type = getTargetClass();
|
||||
if (Map.class == type) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
else if (List.class == type) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
else if (Set.class == type || Collection.class == type) {
|
||||
return Collections.emptySet();
|
||||
}
|
||||
throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
|
||||
"Optional dependency not present for lazy injection point");
|
||||
}
|
||||
if (autowiredBeanNames != null) {
|
||||
for (String autowiredBeanName : autowiredBeanNames) {
|
||||
if (dlbf.containsBean(autowiredBeanName)) {
|
||||
dlbf.registerDependentBean(autowiredBeanName, beanName);
|
||||
}
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
};
|
||||
TargetSource ts = new LazyDependencyTargetSource(dlbf, descriptor, beanName);
|
||||
|
||||
ProxyFactory pf = new ProxyFactory();
|
||||
pf.setTargetSource(ts);
|
||||
@@ -137,4 +102,96 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat
|
||||
return (classOnly ? pf.getProxyClass(classLoader) : pf.getProxy(classLoader));
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
private static class LazyDependencyTargetSource implements TargetSource, Serializable {
|
||||
|
||||
private final DefaultListableBeanFactory beanFactory;
|
||||
|
||||
private final DependencyDescriptor descriptor;
|
||||
|
||||
@Nullable
|
||||
private final String beanName;
|
||||
|
||||
@Nullable
|
||||
private transient volatile Object cachedTarget;
|
||||
|
||||
public LazyDependencyTargetSource(DefaultListableBeanFactory beanFactory,
|
||||
DependencyDescriptor descriptor, @Nullable String beanName) {
|
||||
|
||||
this.beanFactory = beanFactory;
|
||||
this.descriptor = descriptor;
|
||||
this.beanName = beanName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getTargetClass() {
|
||||
return this.descriptor.getDependencyType();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("NullAway")
|
||||
public Object getTarget() {
|
||||
Object cachedTarget = this.cachedTarget;
|
||||
if (cachedTarget != null) {
|
||||
return cachedTarget;
|
||||
}
|
||||
|
||||
Set<String> autowiredBeanNames = new LinkedHashSet<>(2);
|
||||
Object target = this.beanFactory.doResolveDependency(
|
||||
this.descriptor, this.beanName, autowiredBeanNames, null);
|
||||
|
||||
if (target == null) {
|
||||
Class<?> type = getTargetClass();
|
||||
if (Map.class == type) {
|
||||
target = Collections.emptyMap();
|
||||
}
|
||||
else if (List.class == type) {
|
||||
target = Collections.emptyList();
|
||||
}
|
||||
else if (Set.class == type || Collection.class == type) {
|
||||
target = Collections.emptySet();
|
||||
}
|
||||
else {
|
||||
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType(),
|
||||
"Optional dependency not present for lazy injection point");
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (target instanceof Map<?, ?> map && Map.class == getTargetClass()) {
|
||||
target = Collections.unmodifiableMap(map);
|
||||
}
|
||||
else if (target instanceof List<?> list && List.class == getTargetClass()) {
|
||||
target = Collections.unmodifiableList(list);
|
||||
}
|
||||
else if (target instanceof Set<?> set && Set.class == getTargetClass()) {
|
||||
target = Collections.unmodifiableSet(set);
|
||||
}
|
||||
else if (target instanceof Collection<?> coll && Collection.class == getTargetClass()) {
|
||||
target = Collections.unmodifiableCollection(coll);
|
||||
}
|
||||
}
|
||||
|
||||
boolean cacheable = true;
|
||||
for (String autowiredBeanName : autowiredBeanNames) {
|
||||
if (!this.beanFactory.containsBean(autowiredBeanName)) {
|
||||
cacheable = false;
|
||||
}
|
||||
else {
|
||||
if (this.beanName != null) {
|
||||
this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName);
|
||||
}
|
||||
if (!this.beanFactory.isSingleton(autowiredBeanName)) {
|
||||
cacheable = false;
|
||||
}
|
||||
}
|
||||
if (cacheable) {
|
||||
this.cachedTarget = target;
|
||||
}
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,7 +45,8 @@ import java.lang.annotation.Target;
|
||||
* <p>In addition to its role for component initialization, this annotation may also be placed
|
||||
* on injection points marked with {@link org.springframework.beans.factory.annotation.Autowired}
|
||||
* or {@link jakarta.inject.Inject}: In that context, it leads to the creation of a
|
||||
* lazy-resolution proxy for all affected dependencies, as an alternative to using
|
||||
* lazy-resolution proxy for the affected dependency, caching it on first access in case of
|
||||
* a singleton or re-resolving it on every access otherwise. This is an alternative to using
|
||||
* {@link org.springframework.beans.factory.ObjectFactory} or {@link jakarta.inject.Provider}.
|
||||
* Please note that such a lazy-resolution proxy will always be injected; if the target
|
||||
* dependency does not exist, you will only be able to find out through an exception on
|
||||
|
||||
+1
-1
@@ -310,7 +310,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
return new Object[] {event};
|
||||
}
|
||||
|
||||
@SuppressWarnings({"deprecation", "unchecked"})
|
||||
@SuppressWarnings({"removal", "unchecked", "deprecation"})
|
||||
protected void handleResult(Object result) {
|
||||
if (reactiveStreamsPresent && new ReactiveResultHandler().subscribeToPublisher(result)) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
|
||||
+26
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,15 +25,29 @@ import java.lang.annotation.Target;
|
||||
/**
|
||||
* Declares that a field or method parameter should be formatted as a date or time.
|
||||
*
|
||||
* <p>Supports formatting by style pattern, ISO date time pattern, or custom format pattern string.
|
||||
* <p>Formatting applies to parsing a date/time object from a string as well as printing a
|
||||
* date/time object to a string.
|
||||
*
|
||||
* <p>Supports formatting by style pattern, ISO date/time pattern, or custom format pattern string.
|
||||
* Can be applied to {@link java.util.Date}, {@link java.util.Calendar}, {@link Long} (for
|
||||
* millisecond timestamps) as well as JSR-310 {@code java.time} value types.
|
||||
*
|
||||
* <p>For style-based formatting, set the {@link #style} attribute to the desired style pattern code.
|
||||
* The first character of the code is the date style, and the second character is the time style.
|
||||
* Specify a character of 'S' for short style, 'M' for medium, 'L' for long, and 'F' for full.
|
||||
* The date or time may be omitted by specifying the style character '-' — for example,
|
||||
* 'M-' specifies a medium format for the date with no time.
|
||||
* The date or time may be omitted by specifying the style character '-'. For example,
|
||||
* 'M-' specifies a medium format for the date with no time. The supported style pattern codes
|
||||
* correlate to the enum constants defined in {@link java.time.format.FormatStyle}.
|
||||
*
|
||||
* <p><strong>WARNING</strong>: Style-based formatting and parsing rely on locale-sensitive
|
||||
* patterns which may change depending on the Java runtime. Specifically, applications that
|
||||
* rely on date/time parsing and formatting may encounter incompatible changes in behavior
|
||||
* when running on JDK 20 or higher. Using an ISO standardized format or a concrete pattern
|
||||
* that you control allows for reliable system-independent and locale-independent parsing and
|
||||
* formatting of date/time values. The use of {@linkplain #fallbackPatterns() fallback patterns}
|
||||
* can also help to address compatibility issues. For further details, see the
|
||||
* <a href="https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher">
|
||||
* Date and Time Formatting with JDK 20 and higher</a> page in the Spring Framework wiki.
|
||||
*
|
||||
* <p>For ISO-based formatting, set the {@link #iso} attribute to the desired {@link ISO} format,
|
||||
* such as {@link ISO#DATE}.
|
||||
@@ -65,6 +79,8 @@ import java.lang.annotation.Target;
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
* @see java.text.DateFormat
|
||||
* @see java.text.SimpleDateFormat
|
||||
* @see java.time.format.DateTimeFormatter
|
||||
*/
|
||||
@Documented
|
||||
@@ -77,6 +93,8 @@ public @interface DateTimeFormat {
|
||||
* <p>Defaults to 'SS' for short date, short time. Set this attribute when you
|
||||
* wish to format your field or method parameter in accordance with a common
|
||||
* style other than the default style.
|
||||
* <p>See the {@linkplain DateTimeFormat class-level documentation} for further
|
||||
* details.
|
||||
* @see #fallbackPatterns
|
||||
*/
|
||||
String style() default "SS";
|
||||
@@ -93,13 +111,13 @@ public @interface DateTimeFormat {
|
||||
|
||||
/**
|
||||
* The custom pattern to use to format the field or method parameter.
|
||||
* <p>Defaults to empty String, indicating no custom pattern String has been
|
||||
* <p>Defaults to an empty String, indicating no custom pattern String has been
|
||||
* specified. Set this attribute when you wish to format your field or method
|
||||
* parameter in accordance with a custom date time pattern not represented by
|
||||
* a style or ISO format.
|
||||
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat} style,
|
||||
* as also supported by Joda-Time, with strict parsing semantics towards overflows
|
||||
* (for example, rejecting a Feb 29 value for a non-leap-year). As a consequence, 'yy'
|
||||
* <p>Note: This pattern follows the original {@link java.text.SimpleDateFormat}
|
||||
* style, with strict parsing semantics towards overflows (for example, rejecting
|
||||
* a {@code Feb 29} value for a non-leap-year). As a consequence, 'yy'
|
||||
* characters indicate a year in the traditional style, not a "year-of-era" as in the
|
||||
* {@link java.time.format.DateTimeFormatter} specification (i.e. 'yy' turns into 'uu'
|
||||
* when going through a {@code DateTimeFormatter} with strict resolution mode).
|
||||
@@ -121,7 +139,6 @@ public @interface DateTimeFormat {
|
||||
* or {@link #style} attribute is always used for printing. For details on
|
||||
* which time zone is used for fallback patterns, see the
|
||||
* {@linkplain DateTimeFormat class-level documentation}.
|
||||
* <p>Fallback patterns are not supported for Joda-Time value types.
|
||||
* @since 5.3.5
|
||||
*/
|
||||
String[] fallbackPatterns() default {};
|
||||
|
||||
+28
-25
@@ -29,7 +29,7 @@ import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Declares that a field or method parameter should be formatted as a
|
||||
* {@link java.time.Duration}, according to the specified {@code style}.
|
||||
* {@link java.time.Duration}, according to the specified {@link #style style}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
@@ -40,15 +40,15 @@ import org.springframework.lang.Nullable;
|
||||
public @interface DurationFormat {
|
||||
|
||||
/**
|
||||
* Which {@code Style} to use for parsing and printing a {@code Duration}.
|
||||
* Defaults to the JDK style ({@link Style#ISO8601}).
|
||||
* The {@code Style} to use for parsing and printing a {@code Duration}.
|
||||
* <p>Defaults to the JDK style ({@link Style#ISO8601}).
|
||||
*/
|
||||
Style style() default Style.ISO8601;
|
||||
|
||||
/**
|
||||
* Define which {@link Unit} to fall back to in case the {@code style()}
|
||||
* needs a unit for either parsing or printing, and none is explicitly
|
||||
* provided in the input ({@code Unit.MILLIS} if unspecified).
|
||||
* The {@link Unit} to fall back to in case the {@code style()} needs a unit
|
||||
* for either parsing or printing, and none is explicitly provided in the input.
|
||||
* <p>Defaults to {@link Unit#MILLIS} if unspecified.
|
||||
*/
|
||||
Unit defaultUnit() default Unit.MILLIS;
|
||||
|
||||
@@ -59,14 +59,15 @@ public @interface DurationFormat {
|
||||
|
||||
/**
|
||||
* Simple formatting based on a short suffix, for example '1s'.
|
||||
* Supported unit suffixes are: {@code ns, us, ms, s, m, h, d}.
|
||||
* This corresponds to nanoseconds, microseconds, milliseconds, seconds,
|
||||
* minutes, hours and days respectively.
|
||||
* <p>Supported unit suffixes include: {@code ns, us, ms, s, m, h, d}.
|
||||
* Those correspond to nanoseconds, microseconds, milliseconds, seconds,
|
||||
* minutes, hours, and days, respectively.
|
||||
* <p>Note that when printing a {@code Duration}, this style can be
|
||||
* lossy if the selected unit is bigger than the resolution of the
|
||||
* duration. For example, * {@code Duration.ofMillis(5).plusNanos(1234)}
|
||||
* duration. For example, {@code Duration.ofMillis(5).plusNanos(1234)}
|
||||
* would get truncated to {@code "5ms"} when printing using
|
||||
* {@code ChronoUnit.MILLIS}. Fractional durations are not supported.
|
||||
* {@code ChronoUnit.MILLIS}.
|
||||
* <p>Fractional durations are not supported.
|
||||
*/
|
||||
SIMPLE,
|
||||
|
||||
@@ -80,22 +81,24 @@ public @interface DurationFormat {
|
||||
/**
|
||||
* Like {@link #SIMPLE}, but allows multiple segments ordered from
|
||||
* largest-to-smallest units of time, like {@code 1h12m27s}.
|
||||
* <p>
|
||||
* A single minus sign ({@code -}) is allowed to indicate the whole
|
||||
* <p>A single minus sign ({@code -}) is allowed to indicate the whole
|
||||
* duration is negative. Spaces are allowed between segments, and a
|
||||
* negative duration with spaced segments can optionally be surrounded
|
||||
* by parenthesis after the minus sign, like so: {@code -(34m 57s)}.
|
||||
* by parentheses after the minus sign, like so: {@code -(34m 57s)}.
|
||||
*/
|
||||
COMPOSITE
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration format unit, which mirrors a subset of {@link ChronoUnit} and
|
||||
* allows conversion to and from supported {@code ChronoUnit} as well as
|
||||
* converting durations to longs. The enum includes its corresponding suffix
|
||||
* in the {@link Style#SIMPLE simple} Duration format style.
|
||||
* allows conversion to and from a supported {@code ChronoUnit} as well as
|
||||
* conversion from durations to longs.
|
||||
*
|
||||
* <p>The enum includes its corresponding suffix in the {@link Style#SIMPLE simple}
|
||||
* {@code Duration} format style.
|
||||
*/
|
||||
enum Unit {
|
||||
|
||||
/**
|
||||
* Nanoseconds ({@code "ns"}).
|
||||
*/
|
||||
@@ -153,7 +156,7 @@ public @interface DurationFormat {
|
||||
|
||||
/**
|
||||
* Convert this {@code DurationFormat.Unit} to a simple {@code String}
|
||||
* suffix, suitable for the {@link Style#SIMPLE} style.
|
||||
* suffix, suitable for the {@link Style#SIMPLE SIMPLE} style.
|
||||
*/
|
||||
public String asSuffix() {
|
||||
return this.suffix;
|
||||
@@ -173,9 +176,9 @@ public @interface DurationFormat {
|
||||
* Print a {@code Duration} as a {@code String}, converting it to a long
|
||||
* value using this unit's precision via {@link #longValue(Duration)}
|
||||
* and appending this unit's simple {@link #asSuffix() suffix}.
|
||||
* @param value the {@code Duration} to convert to String
|
||||
* @param value the {@code Duration} to convert to a String
|
||||
* @return the String representation of the {@code Duration} in the
|
||||
* {@link Style#SIMPLE SIMPLE style}
|
||||
* {@link Style#SIMPLE SIMPLE} style
|
||||
*/
|
||||
public String print(Duration value) {
|
||||
return longValue(value) + asSuffix();
|
||||
@@ -187,8 +190,8 @@ public @interface DurationFormat {
|
||||
* bigger than the actual resolution of the duration.
|
||||
* <p>For example, {@code Duration.ofMillis(5).plusNanos(1234)} would
|
||||
* get truncated to {@code 5} for unit {@code MILLIS}.
|
||||
* @param value the {@code Duration} to convert to long
|
||||
* @return the long value for the Duration in this Unit
|
||||
* @param value the {@code Duration} to convert to a long
|
||||
* @return the long value for the {@code Duration} in this {@code Unit}
|
||||
*/
|
||||
public long longValue(Duration value) {
|
||||
return this.longValue.apply(value);
|
||||
@@ -196,8 +199,8 @@ public @interface DurationFormat {
|
||||
|
||||
/**
|
||||
* Get the {@code Unit} corresponding to the given {@code ChronoUnit}.
|
||||
* @throws IllegalArgumentException if that particular ChronoUnit isn't
|
||||
* supported
|
||||
* @throws IllegalArgumentException if the given {@code ChronoUnit} is
|
||||
* not supported
|
||||
*/
|
||||
public static Unit fromChronoUnit(@Nullable ChronoUnit chronoUnit) {
|
||||
if (chronoUnit == null) {
|
||||
@@ -213,7 +216,7 @@ public @interface DurationFormat {
|
||||
|
||||
/**
|
||||
* Get the {@code Unit} corresponding to the given {@code String} suffix.
|
||||
* @throws IllegalArgumentException if that particular suffix is unknown
|
||||
* @throws IllegalArgumentException if the given suffix is not supported
|
||||
*/
|
||||
public static Unit fromSuffix(String suffix) {
|
||||
for (Unit candidate : values()) {
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Annotations for declaratively configuring field formatting rules.
|
||||
* Annotations for declaratively configuring field and parameter formatting rules.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
|
||||
@@ -181,7 +181,6 @@ public class DateFormatter implements Formatter<Date> {
|
||||
* <li>'F' = Full</li>
|
||||
* <li>'-' = Omitted</li>
|
||||
* </ul>
|
||||
* This method mimics the styles supported by Joda-Time.
|
||||
* @param stylePattern two characters from the set {"S", "M", "L", "F", "-"}
|
||||
* @since 3.2
|
||||
*/
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -116,7 +116,7 @@ public class DateTimeFormatterFactory {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the two characters to use to format date values, in Joda-Time style.
|
||||
* Set the two characters to use to format date values.
|
||||
* <p>The first character is used for the date style; the second is for
|
||||
* the time style. Supported characters are:
|
||||
* <ul>
|
||||
@@ -126,9 +126,9 @@ public class DateTimeFormatterFactory {
|
||||
* <li>'F' = Full</li>
|
||||
* <li>'-' = Omitted</li>
|
||||
* </ul>
|
||||
* <p>This method mimics the styles supported by Joda-Time. Note that
|
||||
* JSR-310 natively favors {@link java.time.format.FormatStyle} as used for
|
||||
* {@link #setDateStyle}, {@link #setTimeStyle} and {@link #setDateTimeStyle}.
|
||||
* <p>Note that JSR-310 natively favors {@link java.time.format.FormatStyle}
|
||||
* as used for {@link #setDateStyle}, {@link #setTimeStyle}, and
|
||||
* {@link #setDateTimeStyle}.
|
||||
* @param style two characters from the set {"S", "M", "L", "F", "-"}
|
||||
*/
|
||||
public void setStylePattern(String style) {
|
||||
|
||||
+1
-1
@@ -38,7 +38,7 @@ abstract class DateTimeFormatterUtils {
|
||||
* @see ResolverStyle#STRICT
|
||||
*/
|
||||
static DateTimeFormatter createStrictDateTimeFormatter(String pattern) {
|
||||
// Using strict resolution to align with Joda-Time and standard DateFormat behavior:
|
||||
// Using strict resolution to align with standard DateFormat behavior:
|
||||
// otherwise, an overflow like, for example, Feb 29 for a non-leap-year wouldn't get rejected.
|
||||
// However, with strict resolution, a year digit needs to be specified as 'u'...
|
||||
String patternToUse = StringUtils.replace(pattern, "yy", "uu");
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -97,7 +97,7 @@ public class DefaultFormattingConversionService extends FormattingConversionServ
|
||||
|
||||
/**
|
||||
* Add formatters appropriate for most environments: including number formatters,
|
||||
* JSR-354 Money & Currency formatters, JSR-310 Date-Time and/or Joda-Time formatters,
|
||||
* JSR-354 Money & Currency formatters, and JSR-310 Date-Time formatters,
|
||||
* depending on the presence of the corresponding API on the classpath.
|
||||
* @param formatterRegistry the service to register default formatters with
|
||||
*/
|
||||
|
||||
+7
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,24 +32,19 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.StringValueResolver;
|
||||
|
||||
/**
|
||||
* A factory providing convenient access to a {@code FormattingConversionService}
|
||||
* configured with converters and formatters for common types such as numbers and
|
||||
* datetimes.
|
||||
* A factory providing convenient access to a {@link FormattingConversionService}
|
||||
* configured with converters and formatters for common types such as numbers, dates,
|
||||
* and times.
|
||||
*
|
||||
* <p>Additional converters and formatters can be registered declaratively through
|
||||
* {@link #setConverters(Set)} and {@link #setFormatters(Set)}. Another option
|
||||
* is to register converters and formatters in code by implementing the
|
||||
* {@link FormatterRegistrar} interface. You can then configure provide the set
|
||||
* of registrars to use through {@link #setFormatterRegistrars(Set)}.
|
||||
*
|
||||
* <p>A good example for registering converters and formatters in code is
|
||||
* {@code JodaTimeFormatterRegistrar}, which registers a number of
|
||||
* date-related formatters and converters. For a more detailed list of cases
|
||||
* see {@link #setFormatterRegistrars(Set)}
|
||||
* {@link FormatterRegistrar} interface. You can then provide the set of registrars
|
||||
* to use through {@link #setFormatterRegistrars(Set)}.
|
||||
*
|
||||
* <p>Like all {@code FactoryBean} implementations, this class is suitable for
|
||||
* use when configuring a Spring application context using Spring {@code <beans>}
|
||||
* XML. When configuring the container with
|
||||
* XML configuration files. When configuring the container with
|
||||
* {@link org.springframework.context.annotation.Configuration @Configuration}
|
||||
* classes, simply instantiate, configure and return the appropriate
|
||||
* {@code FormattingConversionService} object from a
|
||||
|
||||
+1
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -53,9 +53,6 @@ import org.springframework.util.ReflectionUtils;
|
||||
* web application). There is no direct API dependency between this LoadTimeWeaver
|
||||
* adapter and the underlying ClassLoader, just a 'loose' method contract.
|
||||
*
|
||||
* <p>This is the LoadTimeWeaver to use, for example, with the Resin application server
|
||||
* version 3.1+.
|
||||
*
|
||||
* @author Costin Leau
|
||||
* @author Juergen Hoeller
|
||||
* @since 2.0
|
||||
|
||||
+29
-7
@@ -17,6 +17,7 @@
|
||||
package org.springframework.instrument.classloading.jboss;
|
||||
|
||||
import java.lang.instrument.ClassFileTransformer;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
@@ -26,12 +27,14 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.function.ThrowingFunction;
|
||||
|
||||
/**
|
||||
* {@link LoadTimeWeaver} implementation for JBoss's instrumentable ClassLoader.
|
||||
* Thanks to Ales Justin and Marius Bogoevici for the initial prototype.
|
||||
*
|
||||
* <p>This weaver supports WildFly 13+.
|
||||
* <p>This weaver supports WildFly 13-23 (DelegatingClassFileTransformer) as well as
|
||||
* WildFly 24+ (DelegatingClassTransformer), as of Spring Framework 6.1.15.
|
||||
*
|
||||
* @author Costin Leau
|
||||
* @author Juergen Hoeller
|
||||
@@ -39,9 +42,15 @@ import org.springframework.util.ReflectionUtils;
|
||||
*/
|
||||
public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
|
||||
private static final String DELEGATING_TRANSFORMER_CLASS_NAME =
|
||||
private static final String LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.as.server.deployment.module.DelegatingClassFileTransformer";
|
||||
|
||||
private static final String DELEGATING_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.as.server.deployment.module.DelegatingClassTransformer";
|
||||
|
||||
private static final String CLASS_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.modules.ClassTransformer";
|
||||
|
||||
private static final String WRAPPER_TRANSFORMER_CLASS_NAME =
|
||||
"org.jboss.modules.JLIClassTransformer";
|
||||
|
||||
@@ -52,6 +61,8 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
|
||||
private final Method addTransformer;
|
||||
|
||||
private final ThrowingFunction<Object, Object> adaptTransformer;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance of the {@link JBossLoadTimeWeaver} class using
|
||||
@@ -90,18 +101,29 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
wrappedTransformer.setAccessible(true);
|
||||
suggestedTransformer = wrappedTransformer.get(suggestedTransformer);
|
||||
}
|
||||
if (!suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) {
|
||||
|
||||
Class<?> transformerType = ClassFileTransformer.class;
|
||||
if (suggestedTransformer.getClass().getName().equals(LEGACY_DELEGATING_TRANSFORMER_CLASS_NAME)) {
|
||||
this.adaptTransformer = (t -> t);
|
||||
}
|
||||
else if (suggestedTransformer.getClass().getName().equals(DELEGATING_TRANSFORMER_CLASS_NAME)) {
|
||||
transformerType = classLoader.loadClass(CLASS_TRANSFORMER_CLASS_NAME);
|
||||
Constructor<?> adaptedTransformer = classLoader.loadClass(WRAPPER_TRANSFORMER_CLASS_NAME)
|
||||
.getConstructor(ClassFileTransformer.class);
|
||||
this.adaptTransformer = adaptedTransformer::newInstance;
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(
|
||||
"Transformer not of the expected type DelegatingClassFileTransformer: " +
|
||||
"Transformer not of expected type DelegatingClass(File)Transformer: " +
|
||||
suggestedTransformer.getClass().getName());
|
||||
}
|
||||
this.delegatingTransformer = suggestedTransformer;
|
||||
|
||||
Method addTransformer = ReflectionUtils.findMethod(this.delegatingTransformer.getClass(),
|
||||
"addTransformer", ClassFileTransformer.class);
|
||||
"addTransformer", transformerType);
|
||||
if (addTransformer == null) {
|
||||
throw new IllegalArgumentException(
|
||||
"Could not find 'addTransformer' method on JBoss DelegatingClassFileTransformer: " +
|
||||
"Could not find 'addTransformer' method on JBoss DelegatingClass(File)Transformer: " +
|
||||
this.delegatingTransformer.getClass().getName());
|
||||
}
|
||||
addTransformer.setAccessible(true);
|
||||
@@ -116,7 +138,7 @@ public class JBossLoadTimeWeaver implements LoadTimeWeaver {
|
||||
@Override
|
||||
public void addTransformer(ClassFileTransformer transformer) {
|
||||
try {
|
||||
this.addTransformer.invoke(this.delegatingTransformer, transformer);
|
||||
this.addTransformer.invoke(this.delegatingTransformer, this.adaptTransformer.apply(transformer));
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new IllegalStateException("Could not add transformer on JBoss ClassLoader: " + this.classLoader, ex);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -35,17 +35,18 @@ import org.springframework.aot.hint.annotation.Reflective;
|
||||
* <p>In terms of target method signatures, any parameter types are supported.
|
||||
* However, the return type is constrained to either {@code void} or
|
||||
* {@link java.util.concurrent.Future}. In the latter case, you may declare the
|
||||
* more specific {@link org.springframework.util.concurrent.ListenableFuture} or
|
||||
* {@link java.util.concurrent.CompletableFuture} types which allow for richer
|
||||
* interaction with the asynchronous task and for immediate composition with
|
||||
* further processing steps.
|
||||
* more specific {@link java.util.concurrent.CompletableFuture} type which allows
|
||||
* for richer interaction with the asynchronous task and for immediate composition
|
||||
* with further processing steps.
|
||||
*
|
||||
* <p>A {@code Future} handle returned from the proxy will be an actual asynchronous
|
||||
* {@code Future} that can be used to track the result of the asynchronous method
|
||||
* execution. However, since the target method needs to implement the same signature,
|
||||
* it will have to return a temporary {@code Future} handle that just passes a value
|
||||
* through: for example, Spring's {@link AsyncResult}, EJB 3.1's {@link jakarta.ejb.AsyncResult},
|
||||
* or {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}.
|
||||
* {@code (Completable)Future} that can be used to track the result of the
|
||||
* asynchronous method execution. However, since the target method needs to implement
|
||||
* the same signature, it will have to return a temporary {@code Future} handle that
|
||||
* just passes a value after computation in the execution thread: typically through
|
||||
* {@link java.util.concurrent.CompletableFuture#completedFuture(Object)}. The
|
||||
* provided value will be exposed to the caller through the actual asynchronous
|
||||
* {@code Future} handle at runtime.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
|
||||
+7
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -31,12 +31,10 @@ import org.springframework.util.concurrent.SuccessCallback;
|
||||
* A pass-through {@code Future} handle that can be used for method signatures
|
||||
* which are declared with a {@code Future} return type for asynchronous execution.
|
||||
*
|
||||
* <p>As of Spring 4.1, this class implements {@link ListenableFuture}, not just
|
||||
* <p>As of Spring 4.1, this class implements {@code ListenableFuture}, not just
|
||||
* plain {@link java.util.concurrent.Future}, along with the corresponding support
|
||||
* in {@code @Async} processing.
|
||||
*
|
||||
* <p>As of Spring 4.2, this class also supports passing execution exceptions back
|
||||
* to the caller.
|
||||
* in {@code @Async} processing. As of 7.0, this will be turned back to a plain
|
||||
* {@code Future} in order to focus on compatibility with existing common usage.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
@@ -48,6 +46,7 @@ import org.springframework.util.concurrent.SuccessCallback;
|
||||
* @deprecated as of 6.0, in favor of {@link CompletableFuture}
|
||||
*/
|
||||
@Deprecated(since = "6.0")
|
||||
@SuppressWarnings("removal")
|
||||
public class AsyncResult<V> implements ListenableFuture<V> {
|
||||
|
||||
@Nullable
|
||||
@@ -145,7 +144,7 @@ public class AsyncResult<V> implements ListenableFuture<V> {
|
||||
* @since 4.2
|
||||
* @see Future#get()
|
||||
*/
|
||||
public static <V> ListenableFuture<V> forValue(V value) {
|
||||
public static <V> org.springframework.util.concurrent.ListenableFuture<V> forValue(V value) {
|
||||
return new AsyncResult<>(value, null);
|
||||
}
|
||||
|
||||
@@ -157,7 +156,7 @@ public class AsyncResult<V> implements ListenableFuture<V> {
|
||||
* @since 4.2
|
||||
* @see ExecutionException
|
||||
*/
|
||||
public static <V> ListenableFuture<V> forExecutionException(Throwable ex) {
|
||||
public static <V> org.springframework.util.concurrent.ListenableFuture<V> forExecutionException(Throwable ex) {
|
||||
return new AsyncResult<>(null, ex);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ import org.springframework.util.concurrent.ListenableFuture;
|
||||
* @see DefaultManagedTaskExecutor
|
||||
* @see ThreadPoolTaskExecutor
|
||||
*/
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
|
||||
|
||||
private static final Executor STUB_EXECUTOR = (task -> {
|
||||
|
||||
+4
-5
@@ -41,7 +41,6 @@ import org.springframework.scheduling.support.TaskUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ErrorHandler;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
|
||||
/**
|
||||
* Adapter that takes a {@code java.util.concurrent.ScheduledExecutorService} and
|
||||
@@ -219,15 +218,15 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T
|
||||
return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
@Override
|
||||
public ListenableFuture<?> submitListenable(Runnable task) {
|
||||
public org.springframework.util.concurrent.ListenableFuture<?> submitListenable(Runnable task) {
|
||||
return super.submitListenable(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
@Override
|
||||
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
|
||||
public <T> org.springframework.util.concurrent.ListenableFuture<T> submitListenable(Callable<T> task) {
|
||||
return super.submitListenable(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
|
||||
}
|
||||
|
||||
|
||||
+27
-1
@@ -36,6 +36,8 @@ import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.Lifecycle;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.event.ContextClosedEvent;
|
||||
import org.springframework.core.task.SimpleAsyncTaskExecutor;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
@@ -75,6 +77,8 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac
|
||||
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
|
||||
private boolean virtualThreads = false;
|
||||
|
||||
private ThreadFactory threadFactory = this;
|
||||
|
||||
private boolean threadNamePrefixSet = false;
|
||||
@@ -104,6 +108,25 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac
|
||||
private volatile boolean lateShutdown;
|
||||
|
||||
|
||||
/**
|
||||
* Specify whether to use virtual threads instead of platform threads.
|
||||
* This is off by default, setting up a traditional platform thread pool.
|
||||
* <p>Set this flag to {@code true} on Java 21 or higher for a tightly
|
||||
* managed thread pool setup with virtual threads. In contrast to
|
||||
* {@link SimpleAsyncTaskExecutor}, this is integrated with Spring's
|
||||
* lifecycle management for stopping and restarting execution threads,
|
||||
* including an early stop signal for a graceful shutdown arrangement.
|
||||
* <p>Specify either this or {@link #setThreadFactory}, not both.
|
||||
* @since 6.2
|
||||
* @see #setThreadFactory
|
||||
* @see VirtualThreadTaskExecutor#getVirtualThreadFactory()
|
||||
* @see SimpleAsyncTaskExecutor#setVirtualThreads
|
||||
*/
|
||||
public void setVirtualThreads(boolean virtualThreads) {
|
||||
this.virtualThreads = virtualThreads;
|
||||
this.threadFactory = this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the ThreadFactory to use for the ExecutorService's thread pool.
|
||||
* The default is the underlying ExecutorService's default thread factory.
|
||||
@@ -120,6 +143,7 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac
|
||||
*/
|
||||
public void setThreadFactory(@Nullable ThreadFactory threadFactory) {
|
||||
this.threadFactory = (threadFactory != null ? threadFactory : this);
|
||||
this.virtualThreads = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -282,7 +306,9 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac
|
||||
if (!this.threadNamePrefixSet && this.beanName != null) {
|
||||
setThreadNamePrefix(this.beanName + "-");
|
||||
}
|
||||
this.executor = initializeExecutor(this.threadFactory, this.rejectedExecutionHandler);
|
||||
ThreadFactory factory = (this.virtualThreads ?
|
||||
new VirtualThreadTaskExecutor(getThreadNamePrefix()).getVirtualThreadFactory() : this.threadFactory);
|
||||
this.executor = initializeExecutor(factory, this.rejectedExecutionHandler);
|
||||
this.lifecycleDelegate = new ExecutorLifecycleDelegate(this.executor);
|
||||
}
|
||||
|
||||
|
||||
+7
-6
@@ -44,7 +44,6 @@ import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable;
|
||||
import org.springframework.scheduling.support.TaskUtils;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ErrorHandler;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
|
||||
/**
|
||||
* A simple implementation of Spring's {@link TaskScheduler} interface, using
|
||||
@@ -80,7 +79,9 @@ import org.springframework.util.concurrent.ListenableFuture;
|
||||
* but rather just the hand-off to an execution thread.</b> As a consequence,
|
||||
* a {@link ScheduledFuture} handle (for example, from {@link #schedule(Runnable, Instant)})
|
||||
* represents that hand-off rather than the actual completion of the provided task
|
||||
* (or series of repeated tasks).
|
||||
* (or series of repeated tasks). Also, this scheduler participates in lifecycle
|
||||
* management to a limited degree only, stopping trigger firing and fixed-delay
|
||||
* task execution but not stopping the execution of handed-off tasks.
|
||||
*
|
||||
* <p>As an alternative to the built-in thread-per-task capability, this scheduler
|
||||
* can also be configured with a separate target executor for scheduled task
|
||||
@@ -268,15 +269,15 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements
|
||||
return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
@Override
|
||||
public ListenableFuture<?> submitListenable(Runnable task) {
|
||||
public org.springframework.util.concurrent.ListenableFuture<?> submitListenable(Runnable task) {
|
||||
return super.submitListenable(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false));
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
@Override
|
||||
public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
|
||||
public <T> org.springframework.util.concurrent.ListenableFuture<T> submitListenable(Callable<T> task) {
|
||||
return super.submitListenable(new DelegatingErrorHandlingCallable<>(task, this.errorHandler));
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ import org.springframework.util.concurrent.ListenableFutureTask;
|
||||
* @see ThreadPoolExecutorFactoryBean
|
||||
* @see ConcurrentTaskExecutor
|
||||
*/
|
||||
@SuppressWarnings({"serial", "deprecation"})
|
||||
@SuppressWarnings({"serial", "deprecation", "removal"})
|
||||
public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport
|
||||
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor {
|
||||
|
||||
|
||||
+1
-1
@@ -74,7 +74,7 @@ import org.springframework.util.concurrent.ListenableFutureTask;
|
||||
* @see ThreadPoolTaskExecutor
|
||||
* @see SimpleAsyncTaskScheduler
|
||||
*/
|
||||
@SuppressWarnings({"serial", "deprecation"})
|
||||
@SuppressWarnings({"serial", "deprecation", "removal"})
|
||||
public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport
|
||||
implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler {
|
||||
|
||||
|
||||
+112
-34
@@ -18,16 +18,20 @@ package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.NoProviderFoundException;
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import jakarta.validation.ValidatorFactory;
|
||||
import jakarta.validation.metadata.BeanDescriptor;
|
||||
import jakarta.validation.metadata.ConstraintDescriptor;
|
||||
import jakarta.validation.metadata.ConstructorDescriptor;
|
||||
import jakarta.validation.metadata.MethodDescriptor;
|
||||
import jakarta.validation.metadata.ContainerElementTypeDescriptor;
|
||||
import jakarta.validation.metadata.ExecutableDescriptor;
|
||||
import jakarta.validation.metadata.MethodType;
|
||||
import jakarta.validation.metadata.ParameterDescriptor;
|
||||
import jakarta.validation.metadata.PropertyDescriptor;
|
||||
@@ -36,13 +40,17 @@ import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor;
|
||||
import org.springframework.beans.factory.aot.BeanRegistrationCode;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
/**
|
||||
* AOT {@code BeanRegistrationAotProcessor} that adds additional hints
|
||||
@@ -80,8 +88,8 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
|
||||
|
||||
@Nullable
|
||||
private static Validator getValidatorIfAvailable() {
|
||||
try {
|
||||
return Validation.buildDefaultValidatorFactory().getValidator();
|
||||
try (ValidatorFactory validator = Validation.buildDefaultValidatorFactory()) {
|
||||
return validator.getValidator();
|
||||
}
|
||||
catch (NoProviderFoundException ex) {
|
||||
logger.info("No Bean Validation provider available - skipping validation constraint hint inference");
|
||||
@@ -95,64 +103,134 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
|
||||
return null;
|
||||
}
|
||||
|
||||
Class<?> beanClass = registeredBean.getBeanClass();
|
||||
Set<Class<?>> validatedClasses = new HashSet<>();
|
||||
Set<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses = new HashSet<>();
|
||||
|
||||
processAheadOfTime(beanClass, validatedClasses, constraintValidatorClasses);
|
||||
|
||||
if (!validatedClasses.isEmpty() || !constraintValidatorClasses.isEmpty()) {
|
||||
return new AotContribution(validatedClasses, constraintValidatorClasses);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void processAheadOfTime(Class<?> clazz, Collection<Class<?>> validatedClasses,
|
||||
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
|
||||
|
||||
Assert.notNull(validator, "Validator can't be null");
|
||||
|
||||
BeanDescriptor descriptor;
|
||||
try {
|
||||
descriptor = validator.getConstraintsForClass(registeredBean.getBeanClass());
|
||||
descriptor = validator.getConstraintsForClass(clazz);
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
if (KotlinDetector.isKotlinType(registeredBean.getBeanClass()) && ex instanceof ArrayIndexOutOfBoundsException) {
|
||||
if (KotlinDetector.isKotlinType(clazz) && ex instanceof ArrayIndexOutOfBoundsException) {
|
||||
// See https://hibernate.atlassian.net/browse/HV-1796 and https://youtrack.jetbrains.com/issue/KT-40857
|
||||
logger.warn("Skipping validation constraint hint inference for bean " + registeredBean.getBeanName() +
|
||||
logger.warn("Skipping validation constraint hint inference for class " + clazz +
|
||||
" due to an ArrayIndexOutOfBoundsException at validator level");
|
||||
}
|
||||
else if (ex instanceof TypeNotPresentException) {
|
||||
logger.debug("Skipping validation constraint hint inference for bean " +
|
||||
registeredBean.getBeanName() + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
|
||||
logger.debug("Skipping validation constraint hint inference for class " +
|
||||
clazz + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
|
||||
}
|
||||
else {
|
||||
logger.warn("Skipping validation constraint hint inference for bean " +
|
||||
registeredBean.getBeanName(), ex);
|
||||
logger.warn("Skipping validation constraint hint inference for class " + clazz, ex);
|
||||
}
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
Set<ConstraintDescriptor<?>> constraintDescriptors = new HashSet<>();
|
||||
for (MethodDescriptor methodDescriptor : descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER)) {
|
||||
for (ParameterDescriptor parameterDescriptor : methodDescriptor.getParameterDescriptors()) {
|
||||
constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors());
|
||||
processExecutableDescriptor(descriptor.getConstrainedMethods(MethodType.NON_GETTER, MethodType.GETTER), constraintValidatorClasses);
|
||||
processExecutableDescriptor(descriptor.getConstrainedConstructors(), constraintValidatorClasses);
|
||||
processPropertyDescriptors(descriptor.getConstrainedProperties(), constraintValidatorClasses);
|
||||
if (!constraintValidatorClasses.isEmpty() && shouldProcess(clazz)) {
|
||||
validatedClasses.add(clazz);
|
||||
}
|
||||
|
||||
ReflectionUtils.doWithFields(clazz, field -> {
|
||||
Class<?> type = field.getType();
|
||||
if (Iterable.class.isAssignableFrom(type) || List.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) {
|
||||
ResolvableType resolvableType = ResolvableType.forField(field);
|
||||
Class<?> genericType = resolvableType.getGeneric(0).toClass();
|
||||
if (shouldProcess(genericType)) {
|
||||
validatedClasses.add(clazz);
|
||||
processAheadOfTime(genericType, validatedClasses, constraintValidatorClasses);
|
||||
}
|
||||
}
|
||||
if (Map.class.isAssignableFrom(type)) {
|
||||
ResolvableType resolvableType = ResolvableType.forField(field);
|
||||
Class<?> keyGenericType = resolvableType.getGeneric(0).toClass();
|
||||
Class<?> valueGenericType = resolvableType.getGeneric(1).toClass();
|
||||
if (shouldProcess(keyGenericType)) {
|
||||
validatedClasses.add(clazz);
|
||||
processAheadOfTime(keyGenericType, validatedClasses, constraintValidatorClasses);
|
||||
}
|
||||
if (shouldProcess(valueGenericType)) {
|
||||
validatedClasses.add(clazz);
|
||||
processAheadOfTime(valueGenericType, validatedClasses, constraintValidatorClasses);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean shouldProcess(Class<?> clazz) {
|
||||
return !clazz.getCanonicalName().startsWith("java.");
|
||||
}
|
||||
|
||||
private static void processExecutableDescriptor(Set<? extends ExecutableDescriptor> executableDescriptors,
|
||||
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
|
||||
|
||||
for (ExecutableDescriptor executableDescriptor : executableDescriptors) {
|
||||
for (ParameterDescriptor parameterDescriptor : executableDescriptor.getParameterDescriptors()) {
|
||||
for (ConstraintDescriptor<?> constraintDescriptor : parameterDescriptor.getConstraintDescriptors()) {
|
||||
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
|
||||
}
|
||||
for (ContainerElementTypeDescriptor typeDescriptor : parameterDescriptor.getConstrainedContainerElementTypes()) {
|
||||
for (ConstraintDescriptor<?> constraintDescriptor : typeDescriptor.getConstraintDescriptors()) {
|
||||
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ConstructorDescriptor constructorDescriptor : descriptor.getConstrainedConstructors()) {
|
||||
for (ParameterDescriptor parameterDescriptor : constructorDescriptor.getParameterDescriptors()) {
|
||||
constraintDescriptors.addAll(parameterDescriptor.getConstraintDescriptors());
|
||||
}
|
||||
|
||||
private static void processPropertyDescriptors(Set<PropertyDescriptor> propertyDescriptors,
|
||||
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
|
||||
|
||||
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
|
||||
for (ConstraintDescriptor<?> constraintDescriptor : propertyDescriptor.getConstraintDescriptors()) {
|
||||
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
|
||||
}
|
||||
for (ContainerElementTypeDescriptor typeDescriptor : propertyDescriptor.getConstrainedContainerElementTypes()) {
|
||||
for (ConstraintDescriptor<?> constraintDescriptor : typeDescriptor.getConstraintDescriptors()) {
|
||||
constraintValidatorClasses.addAll(constraintDescriptor.getConstraintValidatorClasses());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (PropertyDescriptor propertyDescriptor : descriptor.getConstrainedProperties()) {
|
||||
constraintDescriptors.addAll(propertyDescriptor.getConstraintDescriptors());
|
||||
}
|
||||
if (!constraintDescriptors.isEmpty()) {
|
||||
return new AotContribution(constraintDescriptors);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class AotContribution implements BeanRegistrationAotContribution {
|
||||
|
||||
private final Collection<ConstraintDescriptor<?>> constraintDescriptors;
|
||||
private final Collection<Class<?>> validatedClasses;
|
||||
private final Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses;
|
||||
|
||||
public AotContribution(Collection<ConstraintDescriptor<?>> constraintDescriptors) {
|
||||
this.constraintDescriptors = constraintDescriptors;
|
||||
public AotContribution(Collection<Class<?>> validatedClasses,
|
||||
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
|
||||
|
||||
this.validatedClasses = validatedClasses;
|
||||
this.constraintValidatorClasses = constraintValidatorClasses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) {
|
||||
for (ConstraintDescriptor<?> constraintDescriptor : this.constraintDescriptors) {
|
||||
for (Class<?> constraintValidatorClass : constraintDescriptor.getConstraintValidatorClasses()) {
|
||||
generationContext.getRuntimeHints().reflection().registerType(constraintValidatorClass,
|
||||
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
|
||||
}
|
||||
ReflectionHints hints = generationContext.getRuntimeHints().reflection();
|
||||
for (Class<?> validatedClass : this.validatedClasses) {
|
||||
hints.registerType(validatedClass, MemberCategory.DECLARED_FIELDS);
|
||||
}
|
||||
for (Class<? extends ConstraintValidator<?, ?>> constraintValidatorClass : this.constraintValidatorClasses) {
|
||||
hints.registerType(constraintValidatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package example.scannable;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -51,9 +52,8 @@ public class AutowiredQualifierFooService implements FooService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public Future<String> asyncFoo(int id) {
|
||||
return new org.springframework.scheduling.annotation.AsyncResult<>(this.fooDao.findFoo(id));
|
||||
return CompletableFuture.completedFuture(this.fooDao.findFoo(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package example.scannable;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
@@ -91,10 +92,9 @@ public abstract class FooServiceImpl implements FooService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public Future<String> asyncFoo(int id) {
|
||||
Assert.state(ServiceInvocationCounter.getThreadLocalCount() != null, "Thread-local counter not exposed");
|
||||
return new org.springframework.scheduling.annotation.AsyncResult<>(fooDao().findFoo(id));
|
||||
return CompletableFuture.completedFuture(fooDao().findFoo(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package example.scannable;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.springframework.context.annotation.Scope;
|
||||
@@ -33,9 +34,8 @@ public class ScopedProxyTestBean implements FooService {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public Future<String> asyncFoo(int id) {
|
||||
return new org.springframework.scheduling.annotation.AsyncResult<>("bar");
|
||||
return CompletableFuture.completedFuture("bar");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+2
-2
@@ -86,7 +86,7 @@ class CacheOperationExpressionEvaluatorTests {
|
||||
AnnotatedClass target = new AnnotatedClass();
|
||||
Method method = ReflectionUtils.findMethod(
|
||||
AnnotatedClass.class, "multipleCaching", Object.class, Object.class);
|
||||
Object[] args = new Object[] {new Object(), new Object()};
|
||||
Object[] args = {"arg1", "arg2"};
|
||||
Collection<ConcurrentMapCache> caches = Collections.singleton(new ConcurrentMapCache("test"));
|
||||
|
||||
EvaluationContext evalCtx = this.eval.createEvaluationContext(caches, method, args,
|
||||
@@ -155,7 +155,7 @@ class CacheOperationExpressionEvaluatorTests {
|
||||
AnnotatedClass target = new AnnotatedClass();
|
||||
Method method = ReflectionUtils.findMethod(
|
||||
AnnotatedClass.class, "multipleCaching", Object.class, Object.class);
|
||||
Object[] args = new Object[] {new Object(), new Object()};
|
||||
Object[] args = new Object[] {"arg1", "arg2"};
|
||||
Collection<ConcurrentMapCache> caches = Collections.singleton(new ConcurrentMapCache("test"));
|
||||
return this.eval.createEvaluationContext(
|
||||
caches, method, args, target, target.getClass(), method, result);
|
||||
|
||||
+31
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,8 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.TargetSource;
|
||||
import org.springframework.aop.framework.Advised;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
|
||||
@@ -67,7 +69,7 @@ class LazyAutowiredAnnotationBeanPostProcessorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyResourceInjectionWithField() {
|
||||
void lazyResourceInjectionWithField() throws Exception {
|
||||
doTestLazyResourceInjection(FieldResourceInjectionBean.class);
|
||||
|
||||
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
|
||||
@@ -84,9 +86,36 @@ class LazyAutowiredAnnotationBeanPostProcessorTests {
|
||||
assertThat(bean.getTestBeans()).isNotEmpty();
|
||||
assertThat(bean.getTestBeans().get(0).getName()).isNull();
|
||||
assertThat(ac.getBeanFactory().containsSingleton("testBean")).isTrue();
|
||||
|
||||
TestBean tb = (TestBean) ac.getBean("testBean");
|
||||
tb.setName("tb");
|
||||
assertThat(bean.getTestBean().getName()).isSameAs("tb");
|
||||
|
||||
assertThat(bean.getTestBeans() instanceof Advised).isTrue();
|
||||
TargetSource targetSource = ((Advised) bean.getTestBeans()).getTargetSource();
|
||||
assertThat(targetSource.getTarget()).isSameAs(targetSource.getTarget());
|
||||
|
||||
ac.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void lazyResourceInjectionWithFieldForPrototype() {
|
||||
doTestLazyResourceInjection(FieldResourceInjectionBean.class);
|
||||
|
||||
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext();
|
||||
RootBeanDefinition abd = new RootBeanDefinition(FieldResourceInjectionBean.class);
|
||||
abd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
ac.registerBeanDefinition("annotatedBean", abd);
|
||||
RootBeanDefinition tbd = new RootBeanDefinition(TestBean.class);
|
||||
tbd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
|
||||
tbd.setLazyInit(true);
|
||||
ac.registerBeanDefinition("testBean", tbd);
|
||||
ac.refresh();
|
||||
|
||||
FieldResourceInjectionBean bean = ac.getBean("annotatedBean", FieldResourceInjectionBean.class);
|
||||
assertThat(bean.getTestBeans()).isNotEmpty();
|
||||
TestBean tb = bean.getTestBeans().get(0);
|
||||
assertThat(bean.getTestBeans().get(0)).isNotSameAs(tb);
|
||||
ac.close();
|
||||
}
|
||||
|
||||
|
||||
+53
@@ -195,6 +195,28 @@ class BeanMethodQualificationTests {
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customWithConstructor() {
|
||||
AnnotationConfigApplicationContext ctx = context(CustomConfig.class, CustomPojoWithConstructor.class);
|
||||
|
||||
CustomPojoWithConstructor pojo = ctx.getBean(CustomPojoWithConstructor.class);
|
||||
assertThat(pojo.plainBean).isNull();
|
||||
assertThat(pojo.testBean.getName()).isEqualTo("interesting");
|
||||
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void customWithMethod() {
|
||||
AnnotationConfigApplicationContext ctx = context(CustomConfig.class, CustomPojoWithMethod.class);
|
||||
|
||||
CustomPojoWithMethod pojo = ctx.getBean(CustomPojoWithMethod.class);
|
||||
assertThat(pojo.plainBean).isNull();
|
||||
assertThat(pojo.testBean.getName()).isEqualTo("interesting");
|
||||
|
||||
ctx.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void beanNamesForAnnotation() {
|
||||
AnnotationConfigApplicationContext ctx = context(StandardConfig.class);
|
||||
@@ -327,6 +349,7 @@ class BeanMethodQualificationTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class EffectivePrimaryConfig {
|
||||
|
||||
@@ -346,6 +369,7 @@ class BeanMethodQualificationTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Component @Lazy
|
||||
static class StandardPojo {
|
||||
|
||||
@@ -418,6 +442,35 @@ class BeanMethodQualificationTests {
|
||||
}
|
||||
|
||||
|
||||
@InterestingPojo
|
||||
static class CustomPojoWithConstructor {
|
||||
|
||||
TestBean plainBean;
|
||||
|
||||
TestBean testBean;
|
||||
|
||||
public CustomPojoWithConstructor(Optional<TestBean> plainBean, @InterestingNeed TestBean testBean) {
|
||||
this.plainBean = plainBean.orElse(null);
|
||||
this.testBean = testBean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@InterestingPojo
|
||||
static class CustomPojoWithMethod {
|
||||
|
||||
TestBean plainBean;
|
||||
|
||||
TestBean testBean;
|
||||
|
||||
@Autowired
|
||||
public void applyDependencies(Optional<TestBean> plainBean, @InterestingNeed TestBean testBean) {
|
||||
this.plainBean = plainBean.orElse(null);
|
||||
this.testBean = testBean;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Qualifier
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Boring {
|
||||
|
||||
+1
-1
@@ -280,7 +280,7 @@ class AnnotationDrivenEventListenerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
void listenableFutureReply() {
|
||||
load(TestEventListener.class, ReplyEventListener.class);
|
||||
org.springframework.util.concurrent.SettableListenableFuture<String> future =
|
||||
|
||||
+91
@@ -339,6 +339,70 @@ class DateFormattingTests {
|
||||
assertThat(bindingResult.getFieldValue(propertyName)).isEqualTo("2021-03-02");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link SimpleDateBean#styleDateTimeWithFallbackPatternsForPreAndPostJdk20}
|
||||
* configures "SS" as the date/time style to use. Thus, we have to be aware
|
||||
* of the following if we do not configure fallback patterns for parsing.
|
||||
*
|
||||
* <ul>
|
||||
* <li>JDK ≤ 19 requires a standard space before the "PM".
|
||||
* <li>JDK ≥ 20 requires a narrow non-breaking space (NNBSP) before the "PM".
|
||||
* </ul>
|
||||
*
|
||||
* <p>To avoid compatibility issues between JDK versions, we have configured
|
||||
* two fallback patterns which emulate the "SS" style: <code>"MM/dd/yy h:mm a"</code>
|
||||
* matches against a standard space before the "PM", and <code>"MM/dd/yy h:mm\u202Fa"</code>
|
||||
* matches against a narrow non-breaking space (NNBSP) before the "PM".
|
||||
*
|
||||
* <p>Thus, the following should theoretically be supported on any JDK (or at least
|
||||
* JDK 17 - 23, where we have tested it).
|
||||
*
|
||||
* @see #patternDateTime(String)
|
||||
*/
|
||||
@ParameterizedTest(name = "input date: {0}") // gh-33151
|
||||
@ValueSource(strings = {"10/31/09, 12:00 PM", "10/31/09, 12:00\u202FPM"})
|
||||
void styleDateTime_PreAndPostJdk20(String propertyValue) {
|
||||
String propertyName = "styleDateTimeWithFallbackPatternsForPreAndPostJdk20";
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add(propertyName, propertyValue);
|
||||
binder.bind(propertyValues);
|
||||
BindingResult bindingResult = binder.getBindingResult();
|
||||
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
|
||||
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
|
||||
// Since the "SS" style is always used for printing and the underlying format
|
||||
// changes depending on the JDK version, we cannot be certain that a normal
|
||||
// space is used before the "PM". Consequently we have to use a regular
|
||||
// expression to match against any Unicode space character (\p{Zs}).
|
||||
assertThat(value).startsWith("10/31/09").matches(".+?12:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
/**
|
||||
* To avoid the use of Locale-based styles (such as "MM") for
|
||||
* {@link SimpleDateBean#patternDateTimeWithFallbackPatternForPreAndPostJdk20}, we have configured a
|
||||
* primary pattern (<code>"MM/dd/yy h:mm a"</code>) that matches against a standard space
|
||||
* before the "PM" and a fallback pattern (<code>"MM/dd/yy h:mm\u202Fa"</code> that matches
|
||||
* against a narrow non-breaking space (NNBSP) before the "PM".
|
||||
*
|
||||
* <p>Thus, the following should theoretically be supported on any JDK (or at least
|
||||
* JDK 17 - 23, where we have tested it).
|
||||
*
|
||||
* @see #styleDateTime(String)
|
||||
*/
|
||||
@ParameterizedTest(name = "input date: {0}") // gh-33151
|
||||
@ValueSource(strings = {"10/31/09 3:45 PM", "10/31/09 3:45\u202FPM"})
|
||||
void patternDateTime_PreAndPostJdk20(String propertyValue) {
|
||||
String propertyName = "patternDateTimeWithFallbackPatternForPreAndPostJdk20";
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add(propertyName, propertyValue);
|
||||
binder.bind(propertyValues);
|
||||
BindingResult bindingResult = binder.getBindingResult();
|
||||
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
|
||||
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
|
||||
// Since the "MM/dd/yy h:mm a" primary pattern is always used for printing, we
|
||||
// can be certain that a normal space is used before the "PM".
|
||||
assertThat(value).matches("10/31/09 3:45 PM");
|
||||
}
|
||||
|
||||
@Test
|
||||
void patternDateWithUnsupportedPattern() {
|
||||
String propertyValue = "210302";
|
||||
@@ -389,12 +453,23 @@ class DateFormattingTests {
|
||||
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
|
||||
private Date styleDateWithFallbackPatterns;
|
||||
|
||||
// "SS" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM,
|
||||
// depending on the version of the JDK.
|
||||
// Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM.
|
||||
@DateTimeFormat(style = "SS", fallbackPatterns = { "M/d/yy, h:mm a", "M/d/yy, h:mm\u202Fa" })
|
||||
private Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
|
||||
@DateTimeFormat(pattern = "M/d/yy h:mm")
|
||||
private Date patternDate;
|
||||
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
|
||||
private Date patternDateWithFallbackPatterns;
|
||||
|
||||
// Primary pattern matches a standard space before AM/PM.
|
||||
// Fallback pattern matches a narrow non-breaking space (NNBSP) before AM/PM.
|
||||
@DateTimeFormat(pattern = "MM/dd/yy h:mm a", fallbackPatterns = "MM/dd/yy h:mm\u202Fa")
|
||||
private Date patternDateTimeWithFallbackPatternForPreAndPostJdk20;
|
||||
|
||||
@DateTimeFormat(iso = ISO.DATE)
|
||||
private Date isoDate;
|
||||
|
||||
@@ -459,6 +534,14 @@ class DateFormattingTests {
|
||||
this.styleDateWithFallbackPatterns = styleDateWithFallbackPatterns;
|
||||
}
|
||||
|
||||
public Date getStyleDateTimeWithFallbackPatternsForPreAndPostJdk20() {
|
||||
return this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public void setStyleDateTimeWithFallbackPatternsForPreAndPostJdk20(Date styleDateTimeWithFallbackPatternsForPreAndPostJdk20) {
|
||||
this.styleDateTimeWithFallbackPatternsForPreAndPostJdk20 = styleDateTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public Date getPatternDate() {
|
||||
return this.patternDate;
|
||||
}
|
||||
@@ -475,6 +558,14 @@ class DateFormattingTests {
|
||||
this.patternDateWithFallbackPatterns = patternDateWithFallbackPatterns;
|
||||
}
|
||||
|
||||
public Date getPatternDateTimeWithFallbackPatternForPreAndPostJdk20() {
|
||||
return this.patternDateTimeWithFallbackPatternForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public void setPatternDateTimeWithFallbackPatternForPreAndPostJdk20(Date patternDateTimeWithFallbackPatternForPreAndPostJdk20) {
|
||||
this.patternDateTimeWithFallbackPatternForPreAndPostJdk20 = patternDateTimeWithFallbackPatternForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public Date getIsoDate() {
|
||||
return this.isoDate;
|
||||
}
|
||||
|
||||
+50
@@ -618,6 +618,41 @@ class DateTimeFormattingTests {
|
||||
assertThat(bindingResult.getFieldValue(propertyName)).asString().matches("12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link DateTimeBean#styleLocalTimeWithFallbackPatternsForPreAndPostJdk20}
|
||||
* configures "-M" as the time style to use. Thus, we have to be aware
|
||||
* of the following if we do not configure fallback patterns for parsing.
|
||||
*
|
||||
* <ul>
|
||||
* <li>JDK ≤ 19 requires a standard space before the "PM".
|
||||
* <li>JDK ≥ 20 requires a narrow non-breaking space (NNBSP) before the "PM".
|
||||
* </ul>
|
||||
*
|
||||
* <p>To avoid compatibility issues between JDK versions, we have configured
|
||||
* two fallback patterns which emulate the "-M" style: <code>"HH:mm:ss a"</code>
|
||||
* matches against a standard space before the "PM", and <code>"HH:mm:ss\u202Fa"</code>
|
||||
* matches against a narrow non-breaking space (NNBSP) before the "PM".
|
||||
*
|
||||
* <p>Thus, the following should theoretically be supported on any JDK (or at least
|
||||
* JDK 17 - 23, where we have tested it).
|
||||
*/
|
||||
@ParameterizedTest(name = "input date: {0}") // gh-33151
|
||||
@ValueSource(strings = { "12:00:00 PM", "12:00:00\u202FPM" })
|
||||
void styleLocalTime_PreAndPostJdk20(String propertyValue) {
|
||||
String propertyName = "styleLocalTimeWithFallbackPatternsForPreAndPostJdk20";
|
||||
MutablePropertyValues propertyValues = new MutablePropertyValues();
|
||||
propertyValues.add(propertyName, propertyValue);
|
||||
binder.bind(propertyValues);
|
||||
BindingResult bindingResult = binder.getBindingResult();
|
||||
assertThat(bindingResult.getErrorCount()).isEqualTo(0);
|
||||
String value = binder.getBindingResult().getFieldValue(propertyName).toString();
|
||||
// Since the "-M" style is always used for printing and the underlying format
|
||||
// changes depending on the JDK version, we cannot be certain that a normal
|
||||
// space is used before the "PM". Consequently we have to use a regular
|
||||
// expression to match against any Unicode space character (\p{Zs}).
|
||||
assertThat(value).matches("12:00:00\\p{Zs}PM");
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "input date: {0}")
|
||||
@ValueSource(strings = {"2021-03-02T12:00:00", "2021-03-02 12:00:00", "3/2/21 12:00"})
|
||||
void isoLocalDateTime(String propertyValue) {
|
||||
@@ -695,6 +730,12 @@ class DateTimeFormattingTests {
|
||||
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"})
|
||||
private LocalTime styleLocalTimeWithFallbackPatterns;
|
||||
|
||||
// "-M" style matches either a standard space or a narrow non-breaking space (NNBSP) before AM/PM,
|
||||
// depending on the version of the JDK.
|
||||
// Fallback patterns match a standard space OR a narrow non-breaking space (NNBSP) before AM/PM.
|
||||
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss a", "HH:mm:ss\u202Fa"})
|
||||
private LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
|
||||
private LocalDateTime localDateTime;
|
||||
|
||||
@DateTimeFormat(style = "MM")
|
||||
@@ -798,6 +839,15 @@ class DateTimeFormattingTests {
|
||||
this.styleLocalTimeWithFallbackPatterns = styleLocalTimeWithFallbackPatterns;
|
||||
}
|
||||
|
||||
public LocalTime getStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20() {
|
||||
return this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public void setStyleLocalTimeWithFallbackPatternsForPreAndPostJdk20(
|
||||
LocalTime styleLocalTimeWithFallbackPatternsForPreAndPostJdk20) {
|
||||
this.styleLocalTimeWithFallbackPatternsForPreAndPostJdk20 = styleLocalTimeWithFallbackPatternsForPreAndPostJdk20;
|
||||
}
|
||||
|
||||
public LocalDateTime getLocalDateTime() {
|
||||
return this.localDateTime;
|
||||
}
|
||||
|
||||
+2
-2
@@ -275,7 +275,7 @@ class AsyncAnnotationBeanPostProcessorTests {
|
||||
|
||||
Future<Object> failWithFuture();
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
org.springframework.util.concurrent.ListenableFuture<Object> failWithListenableFuture();
|
||||
|
||||
void failWithVoid();
|
||||
@@ -310,7 +310,7 @@ class AsyncAnnotationBeanPostProcessorTests {
|
||||
|
||||
@Async
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
public org.springframework.util.concurrent.ListenableFuture<Object> failWithListenableFuture() {
|
||||
throw new UnsupportedOperationException("failWithListenableFuture");
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
*/
|
||||
@SuppressWarnings({ "resource", "deprecation" })
|
||||
@SuppressWarnings({"resource", "deprecation", "removal"})
|
||||
class AsyncExecutionTests {
|
||||
|
||||
private static String originalThreadName;
|
||||
|
||||
+12
-12
@@ -32,7 +32,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
class AsyncResultTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
public void asyncResultWithCallbackAndValue() throws Exception {
|
||||
String value = "val";
|
||||
final Set<String> values = new HashSet<>(1);
|
||||
@@ -54,7 +54,7 @@ class AsyncResultTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
public void asyncResultWithCallbackAndException() {
|
||||
IOException ex = new IOException();
|
||||
final Set<Throwable> values = new HashSet<>(1);
|
||||
@@ -71,15 +71,15 @@ class AsyncResultTests {
|
||||
});
|
||||
assertThat(values).singleElement().isSameAs(ex);
|
||||
assertThatExceptionOfType(ExecutionException.class)
|
||||
.isThrownBy(future::get)
|
||||
.withCause(ex);
|
||||
.isThrownBy(future::get)
|
||||
.withCause(ex);
|
||||
assertThatExceptionOfType(ExecutionException.class)
|
||||
.isThrownBy(future.completable()::get)
|
||||
.withCause(ex);
|
||||
.isThrownBy(future.completable()::get)
|
||||
.withCause(ex);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
public void asyncResultWithSeparateCallbacksAndValue() throws Exception {
|
||||
String value = "val";
|
||||
final Set<String> values = new HashSet<>(1);
|
||||
@@ -92,7 +92,7 @@ class AsyncResultTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
public void asyncResultWithSeparateCallbacksAndException() {
|
||||
IOException ex = new IOException();
|
||||
final Set<Throwable> values = new HashSet<>(1);
|
||||
@@ -100,11 +100,11 @@ class AsyncResultTests {
|
||||
future.addCallback(result -> new AssertionError("Success callback not expected: " + result), values::add);
|
||||
assertThat(values).singleElement().isSameAs(ex);
|
||||
assertThatExceptionOfType(ExecutionException.class)
|
||||
.isThrownBy(future::get)
|
||||
.withCause(ex);
|
||||
.isThrownBy(future::get)
|
||||
.withCause(ex);
|
||||
assertThatExceptionOfType(ExecutionException.class)
|
||||
.isThrownBy(future.completable()::get)
|
||||
.withCause(ex);
|
||||
.isThrownBy(future.completable()::get)
|
||||
.withCause(ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-8
@@ -47,7 +47,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*/
|
||||
abstract class AbstractSchedulingTaskExecutorTests {
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("removal")
|
||||
private org.springframework.core.task.AsyncListenableTaskExecutor executor;
|
||||
|
||||
protected String testName;
|
||||
@@ -64,7 +64,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
this.executor = buildExecutor();
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings("removal")
|
||||
protected abstract org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor();
|
||||
|
||||
@AfterEach
|
||||
@@ -125,7 +125,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void submitListenableRunnable() {
|
||||
TestTask task = new TestTask(this.testName, 1);
|
||||
// Act
|
||||
@@ -156,7 +156,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void submitFailingListenableRunnable() {
|
||||
TestTask task = new TestTask(this.testName, 0);
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future = executor.submitListenable(task);
|
||||
@@ -185,7 +185,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void submitListenableRunnableWithGetAfterShutdown() throws Exception {
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future1 = executor.submitListenable(new TestTask(this.testName, -1));
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future2 = executor.submitListenable(new TestTask(this.testName, -1));
|
||||
@@ -260,7 +260,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void submitListenableCallable() {
|
||||
TestCallable task = new TestCallable(this.testName, 1);
|
||||
// Act
|
||||
@@ -275,7 +275,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void submitFailingListenableCallable() {
|
||||
TestCallable task = new TestCallable(this.testName, 0);
|
||||
// Act
|
||||
@@ -291,7 +291,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
void submitListenableCallableWithGetAfterShutdown() throws Exception {
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future1 = executor.submitListenable(new TestCallable(this.testName, -1));
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future2 = executor.submitListenable(new TestCallable(this.testName, -1));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user