mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
122 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3591f1e9fb | |||
| e2c9dc7138 | |||
| 1818161f58 | |||
| 8916ee9f81 | |||
| d835fe311d | |||
| 0eefac21c9 | |||
| e99791f289 | |||
| 658775b914 | |||
| ea7a1d789e | |||
| 4ae471df01 | |||
| 75e3f44a7b | |||
| adcd7cb4cb | |||
| d0be180a69 | |||
| 3731fed4ca | |||
| bfc02cda0c | |||
| f734d04a0d | |||
| ce4c9ebe3c | |||
| 7c4801304d | |||
| 2731183420 | |||
| 38cf4ab3fc | |||
| 2137ec70d2 | |||
| 61201db704 | |||
| 2643c6212f | |||
| b46328a000 | |||
| cc4c693db7 | |||
| a206ea8b12 | |||
| 9f1d9fe82c | |||
| ab33000750 | |||
| e2ab9cd5da | |||
| 103db5429a | |||
| 68f8139206 | |||
| a9da900b5b | |||
| e7e4b3559a | |||
| feb77f924a | |||
| 12c3dc0cbe | |||
| df27627516 | |||
| 939aa84214 | |||
| 9f77f401ad | |||
| f19f1a667c | |||
| 69207c6c16 | |||
| f61832ecb8 | |||
| 14790493f4 | |||
| 53d9ba879d | |||
| 9d2d9ae0ff | |||
| c74af40288 | |||
| 196c1dd51c | |||
| 44689b9ec0 | |||
| b916dc962e | |||
| 87d0113b25 | |||
| cee185ce3b | |||
| e45727d4fa | |||
| bc088ebdef | |||
| f1cf91c9f9 | |||
| d36475cb4c | |||
| 8b6d7e0da7 | |||
| 0a86a7e3a8 | |||
| 8edde374bb | |||
| 35b58cd0ee | |||
| dfc900c7c4 | |||
| 07c0213f20 | |||
| 667851c0fa | |||
| 96aadc2b12 | |||
| 2c6ccaea05 | |||
| 62d09be2ae | |||
| 47f65b3dff | |||
| c7e24a5c26 | |||
| a493bd959d | |||
| 92e9543ad4 | |||
| 68231aa08c | |||
| 71d18ebabc | |||
| 3ccb0786db | |||
| 15c16d2c3a | |||
| 5dcb85db8d | |||
| 4ae03ecd40 | |||
| 1603045c7d | |||
| ace2b2bac9 | |||
| f082c435e1 | |||
| c6810334ca | |||
| 3b90311c17 | |||
| 946fc65712 | |||
| fd25e2f468 | |||
| 46ceedf992 | |||
| a4b3111928 | |||
| 4c4161c8c0 | |||
| 61d5413c23 | |||
| 08e6d762d2 | |||
| 7a19cbb452 | |||
| 2f1e1d483a | |||
| 24d152cdab | |||
| cc530d4df2 | |||
| 6c3132cb8c | |||
| a15274d431 | |||
| 45d4fd3b7e | |||
| 3fd5d69939 | |||
| 8647c44364 | |||
| 6504177e7b | |||
| c1125699bb | |||
| f58d0f6aae | |||
| c1b6bfb681 | |||
| 9d4abb63d8 | |||
| a9a404266c | |||
| b29f4ed37e | |||
| 4847ee80b0 | |||
| f4ee120a42 | |||
| e625a28f6d | |||
| 45c1cd9295 | |||
| 2641b5d783 | |||
| dc0c463137 | |||
| 83bbf16e6c | |||
| 3686b89ab5 | |||
| 85ca9f46dd | |||
| e2287020c3 | |||
| 9e18c75ff1 | |||
| fb9f31d101 | |||
| 4ee0a8ee33 | |||
| f3908832c5 | |||
| c10266e57e | |||
| 1b6b163f24 | |||
| 8642a39a03 | |||
| f62cc98a97 | |||
| c599775a9e | |||
| 4bc97ad532 |
+1
-1
@@ -38,7 +38,7 @@ bin
|
||||
.springBeans
|
||||
spring-*/src/main/java/META-INF/MANIFEST.MF
|
||||
|
||||
# IDEA artifacts and output dirs
|
||||
# IntelliJ IDEA artifacts and output dirs
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ source code into your IDE.
|
||||
The wiki pages
|
||||
[Code Style](https://github.com/spring-projects/spring-framework/wiki/Code-Style) and
|
||||
[IntelliJ IDEA Editor Settings](https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings)
|
||||
define the source file coding standards we use along with some IDEA editor settings we customize.
|
||||
define the source file coding standards we use along with some IntelliJ editor settings we customize.
|
||||
|
||||
### Reference Docs
|
||||
|
||||
|
||||
@@ -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("12.1.2");
|
||||
checkstyle.setToolVersion("12.2.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -13,7 +13,9 @@ content:
|
||||
- url: https://github.com/spring-projects/spring-framework
|
||||
# Refname matching:
|
||||
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
|
||||
branches: ['main', '{6..9}.+({1..9}).x']
|
||||
# branches: We include snapshots for main, 6.2.x, and 7.0.x to 9.*.x.
|
||||
branches: ['main', '6.2.x', '{7..9}.+({0..9}).x']
|
||||
# tags: We effectively include all releases from 6.0.9 to 9.*.*.
|
||||
tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})']
|
||||
start_path: framework-docs
|
||||
asciidoc:
|
||||
@@ -36,4 +38,4 @@ runtime:
|
||||
failure_level: warn
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.20/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
|
||||
|
||||
+2
-3
@@ -50,9 +50,8 @@ XML configuration:
|
||||
|
||||
The preceding XML is more succinct. However, typos are discovered at runtime rather than
|
||||
design time, unless you use an IDE (such as https://www.jetbrains.com/idea/[IntelliJ
|
||||
IDEA] or the {spring-site-tools}[Spring Tools for Eclipse])
|
||||
that supports automatic property completion when you create bean definitions. Such IDE
|
||||
assistance is highly recommended.
|
||||
IDEA] or the {spring-site-tools}[Spring Tools]) that supports automatic property
|
||||
completion when you create bean definitions. Such IDE assistance is highly recommended.
|
||||
|
||||
You can also configure a `java.util.Properties` instance, as follows:
|
||||
|
||||
|
||||
+5
-5
@@ -338,11 +338,11 @@ In the preceding scenario, using `@Autowired` works well and provides the desire
|
||||
modularity, but determining exactly where the autowired bean definitions are declared is
|
||||
still somewhat ambiguous. For example, as a developer looking at `ServiceConfig`, how do
|
||||
you know exactly where the `@Autowired AccountRepository` bean is declared? It is not
|
||||
explicit in the code, and this may be just fine. Remember that the
|
||||
{spring-site-tools}[Spring Tools for Eclipse] provides tooling that
|
||||
can render graphs showing how everything is wired, which may be all you need. Also,
|
||||
your Java IDE can easily find all declarations and uses of the `AccountRepository` type
|
||||
and quickly show you the location of `@Bean` methods that return that type.
|
||||
explicit in the code, and this may be just fine. Note that the
|
||||
{spring-site-tools}[Spring Tools] IDE support provides tooling that can render graphs
|
||||
showing how everything is wired, which may be all you need. Also, your Java IDE can
|
||||
easily find all declarations and uses of the `AccountRepository` type and quickly show
|
||||
you the location of `@Bean` methods that return that type.
|
||||
|
||||
In cases where this ambiguity is not acceptable and you wish to have direct navigation
|
||||
from within your IDE from one `@Configuration` class to another, consider autowiring the
|
||||
|
||||
@@ -7,14 +7,14 @@ similar to the https://jakarta.ee/specifications/expression-language/[Jakarta Ex
|
||||
Language] but offers additional features, most notably method invocation and basic string
|
||||
templating functionality.
|
||||
|
||||
While there are several other Java expression languages available -- OGNL, MVEL, and JBoss
|
||||
EL, to name a few -- the Spring Expression Language was created to provide the Spring
|
||||
community with a single well supported expression language that can be used across all
|
||||
the products in the Spring portfolio. Its language features are driven by the
|
||||
requirements of the projects in the Spring portfolio, including tooling requirements
|
||||
for code completion support within the {spring-site-tools}[Spring Tools for Eclipse].
|
||||
That said, SpEL is based on a technology-agnostic API that lets other expression language
|
||||
implementations be integrated, should the need arise.
|
||||
While there are several other Java expression languages available -- OGNL, MVEL, and
|
||||
JBoss EL, to name a few -- the Spring Expression Language was created to provide the
|
||||
Spring community with a single well supported expression language that can be used across
|
||||
all the products in the Spring portfolio. Its language features are driven by the
|
||||
requirements of the projects in the Spring portfolio, including tooling requirements for
|
||||
code completion within the {spring-site-tools}[Spring Tools] IDE support. That said, SpEL
|
||||
is based on a technology-agnostic API that lets other expression language implementations
|
||||
be integrated, should the need arise.
|
||||
|
||||
While SpEL serves as the foundation for expression evaluation within the Spring
|
||||
portfolio, it is not directly tied to Spring and can be used independently. To
|
||||
|
||||
@@ -319,9 +319,17 @@ progresses.
|
||||
== Testing
|
||||
|
||||
This section addresses testing with the combination of Kotlin and Spring Framework.
|
||||
The recommended testing framework is https://junit.org/junit5/[JUnit] along with
|
||||
The recommended testing framework is https://junit.org/[JUnit] along with
|
||||
https://mockk.io/[Mockk] for mocking.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Kotlin lets you specify meaningful test function names between backticks (```).
|
||||
|
||||
For a concrete example, see the `+++`Find all users on HTML page`()+++` test function later
|
||||
in this section.
|
||||
====
|
||||
|
||||
NOTE: If you are using Spring Boot, see
|
||||
{spring-boot-docs-ref}/features/kotlin.html#features.kotlin.testing[this related documentation].
|
||||
|
||||
@@ -352,7 +360,6 @@ file with a `spring.test.constructor.autowire.mode = all` property.
|
||||
[[per_class-lifecycle]]
|
||||
=== `PER_CLASS` Lifecycle
|
||||
|
||||
Kotlin lets you specify meaningful test function names between backticks (```).
|
||||
With JUnit Jupiter, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
|
||||
annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll`
|
||||
and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin.
|
||||
|
||||
@@ -237,8 +237,8 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RSocketStrategies strategies = RSocketStrategies.builder()
|
||||
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
|
||||
.encoders(encoders -> encoders.add(new JacksonCborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
|
||||
.build();
|
||||
|
||||
RSocketRequester requester = RSocketRequester.builder()
|
||||
@@ -251,8 +251,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val strategies = RSocketStrategies.builder()
|
||||
.encoders { it.add(Jackson2CborEncoder()) }
|
||||
.decoders { it.add(Jackson2CborDecoder()) }
|
||||
.encoders { it.add(JacksonCborEncoder()) }
|
||||
.decoders { it.add(JacksonCborDecoder()) }
|
||||
.build()
|
||||
|
||||
val requester = RSocketRequester.builder()
|
||||
@@ -681,8 +681,8 @@ Java::
|
||||
@Bean
|
||||
public RSocketStrategies rsocketStrategies() {
|
||||
return RSocketStrategies.builder()
|
||||
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
|
||||
.encoders(encoders -> encoders.add(new JacksonCborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
|
||||
.routeMatcher(new PathPatternRouteMatcher())
|
||||
.build();
|
||||
}
|
||||
@@ -703,8 +703,8 @@ Kotlin::
|
||||
|
||||
@Bean
|
||||
fun rsocketStrategies() = RSocketStrategies.builder()
|
||||
.encoders { it.add(Jackson2CborEncoder()) }
|
||||
.decoders { it.add(Jackson2CborDecoder()) }
|
||||
.encoders { it.add(JacksonCborEncoder()) }
|
||||
.decoders { it.add(JacksonCborDecoder()) }
|
||||
.routeMatcher(PathPatternRouteMatcher())
|
||||
.build()
|
||||
}
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ default mode may be set via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
|
||||
The default mode may also be configured as a
|
||||
https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
|
||||
https://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
|
||||
|
||||
If the `spring.test.constructor.autowire.mode` property is not set, test class
|
||||
constructors will not be automatically autowired.
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
= Meta-Annotation Support for Testing
|
||||
|
||||
You can use most test-related annotations as
|
||||
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to create custom composed
|
||||
annotations and reduce configuration duplication across a test suite.
|
||||
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to
|
||||
create custom composed annotations and reduce configuration duplication across a test
|
||||
suite.
|
||||
|
||||
You can use each of the following as a meta-annotation in conjunction with the
|
||||
xref:testing/testcontext-framework.adoc[TestContext framework].
|
||||
For example, you can use each of the following as a meta-annotation in conjunction with
|
||||
the xref:testing/testcontext-framework.adoc[TestContext framework].
|
||||
|
||||
* `@BootstrapWith`
|
||||
* `@ContextConfiguration`
|
||||
@@ -37,111 +38,7 @@ xref:testing/testcontext-framework.adoc[TestContext framework].
|
||||
* `@EnabledIf` _(only supported on JUnit Jupiter)_
|
||||
* `@DisabledIf` _(only supported on JUnit Jupiter)_
|
||||
|
||||
Consider the following example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public class OrderRepositoryTests { }
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public class UserRepositoryTests { }
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class OrderRepositoryTests { }
|
||||
|
||||
@RunWith(SpringRunner::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class UserRepositoryTests { }
|
||||
----
|
||||
======
|
||||
|
||||
If we discover that we are repeating the preceding configuration across our JUnit 4-based
|
||||
test suite, we can reduce the duplication by introducing a custom composed annotation
|
||||
that centralizes the common test configuration for Spring, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public @interface TransactionalDevTestConfig { }
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Target(AnnotationTarget.TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
annotation class TransactionalDevTestConfig { }
|
||||
----
|
||||
======
|
||||
|
||||
Then we can use our custom `@TransactionalDevTestConfig` annotation to simplify the
|
||||
configuration of individual JUnit 4 based test classes, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@TransactionalDevTestConfig
|
||||
public class OrderRepositoryTests { }
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@TransactionalDevTestConfig
|
||||
public class UserRepositoryTests { }
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner::class)
|
||||
@TransactionalDevTestConfig
|
||||
class OrderRepositoryTests
|
||||
|
||||
@RunWith(SpringRunner::class)
|
||||
@TransactionalDevTestConfig
|
||||
class UserRepositoryTests
|
||||
----
|
||||
======
|
||||
|
||||
If we write tests that use JUnit Jupiter, we can reduce code duplication even further,
|
||||
since annotations in JUnit Jupiter can also be used as meta-annotations. Consider the
|
||||
following example:
|
||||
Consider the following test classes that use the `SpringExtension` with JUnit Jupiter:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -150,13 +47,13 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class OrderRepositoryTests { }
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class UserRepositoryTests { }
|
||||
@@ -167,23 +64,22 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class OrderRepositoryTests { }
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class UserRepositoryTests { }
|
||||
----
|
||||
======
|
||||
|
||||
If we discover that we are repeating the preceding configuration across our JUnit
|
||||
Jupiter-based test suite, we can reduce the duplication by introducing a custom composed
|
||||
annotation that centralizes the common test configuration for Spring and JUnit Jupiter,
|
||||
as follows:
|
||||
If we discover that we are repeating the preceding configuration across our test suite,
|
||||
we can reduce the duplication by introducing a custom composed annotation that
|
||||
centralizes the common test configuration for Spring and JUnit Jupiter, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -194,7 +90,7 @@ Java::
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public @interface TransactionalDevTestConfig { }
|
||||
@@ -207,7 +103,7 @@ Kotlin::
|
||||
@Target(AnnotationTarget.TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
annotation class TransactionalDevTestConfig { }
|
||||
|
||||
@@ -5,24 +5,25 @@ It is important to be able to perform some integration testing without requiring
|
||||
deployment to your application server or connecting to other enterprise infrastructure.
|
||||
Doing so lets you test things such as:
|
||||
|
||||
* The correct wiring of your Spring IoC container contexts.
|
||||
* Data access using JDBC or an ORM tool. This can include such things as the correctness
|
||||
of SQL statements, Hibernate queries, JPA entity mappings, and so forth.
|
||||
* The correct wiring of your Spring components.
|
||||
* Data access using JDBC or an ORM tool.
|
||||
** This can include such things as the correctness of SQL statements, Hibernate queries,
|
||||
JPA entity mappings, and so forth.
|
||||
|
||||
The Spring Framework provides first-class support for integration testing in the
|
||||
`spring-test` module. The name of the actual JAR file might include the release version
|
||||
and might also be in the long `org.springframework.test` form, depending on where you get
|
||||
it from (see the xref:core/beans/dependencies.adoc[section on Dependency Management]
|
||||
for an explanation). This library includes the `org.springframework.test` package, which
|
||||
`spring-test` module. The name of the actual JAR file might include the release version,
|
||||
depending on where you get it from (see the
|
||||
{spring-framework-wiki}/Spring-Framework-Artifacts[Spring Framework Artifacts] wiki page
|
||||
for details). This library includes the `org.springframework.test` package, which
|
||||
contains valuable classes for integration testing with a Spring container. This testing
|
||||
does not rely on an application server or other deployment environment. Such tests are
|
||||
slower to run than unit tests but much faster than the equivalent Selenium tests or
|
||||
remote tests that rely on deployment to an application server.
|
||||
|
||||
Unit and integration testing support is provided in the form of the annotation-driven
|
||||
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext framework is
|
||||
agnostic of the actual testing framework in use, which allows instrumentation of tests
|
||||
in various environments, including JUnit, TestNG, and others.
|
||||
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext
|
||||
framework is agnostic of the actual testing framework in use, which allows
|
||||
instrumentation of tests in various environments, including JUnit, TestNG, and others.
|
||||
|
||||
The following section provides an overview of the high-level goals of Spring's
|
||||
integration support, and the rest of this chapter then focuses on dedicated topics:
|
||||
|
||||
+11
-8
@@ -8,12 +8,15 @@ in JUnit and TestNG.
|
||||
[[testcontext-junit-jupiter-extension]]
|
||||
== SpringExtension for JUnit Jupiter
|
||||
|
||||
The Spring TestContext Framework offers full integration with the JUnit Jupiter testing
|
||||
framework, originally introduced in JUnit 5. By annotating test classes with
|
||||
`@ExtendWith(SpringExtension.class)`, you can implement standard JUnit Jupiter-based unit
|
||||
and integration tests and simultaneously reap the benefits of the TestContext framework,
|
||||
such as support for loading application contexts, dependency injection of test instances,
|
||||
transactional test method execution, and so on.
|
||||
The `SpringExtension` integrates the Spring TestContext Framework into the JUnit Jupiter
|
||||
testing framework.
|
||||
|
||||
NOTE: As of Spring Framework 7.0, the `SpringExtension` requires JUnit Jupiter 6.0 or higher.
|
||||
|
||||
By annotating test classes with `@ExtendWith(SpringExtension.class)`, you can implement
|
||||
standard JUnit Jupiter-based unit and integration tests and simultaneously reap the
|
||||
benefits of the TestContext framework, such as support for loading application contexts,
|
||||
dependency injection of test instances, transactional test method execution, and so on.
|
||||
|
||||
Furthermore, thanks to the rich extension API in JUnit Jupiter, Spring provides the
|
||||
following features above and beyond the feature set that Spring supports for JUnit 4 and
|
||||
@@ -22,7 +25,7 @@ TestNG:
|
||||
* Dependency injection for test constructors, test methods, and test lifecycle callback
|
||||
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
|
||||
Injection with the `SpringExtension`] for further details.
|
||||
* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
|
||||
* Powerful support for link:https://docs.junit.org/current/user-guide/#extensions-conditions[conditional
|
||||
test execution] based on SpEL expressions, environment variables, system properties,
|
||||
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
|
||||
@@ -160,7 +163,7 @@ for further details.
|
||||
=== Dependency Injection with the `SpringExtension`
|
||||
|
||||
The `SpringExtension` implements the
|
||||
link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
|
||||
link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
|
||||
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
|
||||
constructors, test methods, and test lifecycle callback methods.
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ of `PlatformTransactionManager` within the test's `ApplicationContext`, you can
|
||||
qualifier by using `@Transactional("myTxMgr")` or `@Transactional(transactionManager =
|
||||
"myTxMgr")`, or `TransactionManagementConfigurer` can be implemented by an
|
||||
`@Configuration` class. Consult the
|
||||
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc
|
||||
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager(org.springframework.test.context.TestContext,java.lang.String)[javadoc
|
||||
for `TestContextTransactionUtils.retrieveTransactionManager()`] for details on the
|
||||
algorithm used to look up a transaction manager in the test's `ApplicationContext`.
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ infrastructure and controller declarations and use it to handle requests via moc
|
||||
and response objects, without a running server.
|
||||
|
||||
For WebFlux, use the following where the Spring `ApplicationContext` is passed to
|
||||
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder]
|
||||
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext(org.springframework.context.ApplicationContext)[WebHttpHandlerBuilder]
|
||||
to create the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[WebHandler chain] to handle
|
||||
requests:
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the WebFlux Java configuration to declare such mappings, which results in a single,
|
||||
global map passed to all `HandlerMapping` implementations.
|
||||
@@ -57,7 +57,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
|
||||
@@ -123,7 +123,7 @@ Both request and response provide {reactive-streams-site}[Reactive Streams] back
|
||||
against the body streams.
|
||||
The request body is represented with a Reactor `Flux` or `Mono`.
|
||||
The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`.
|
||||
For more on that, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries].
|
||||
For more on that, see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries].
|
||||
|
||||
[[webflux-fn-request]]
|
||||
=== ServerRequest
|
||||
@@ -296,7 +296,8 @@ allPartsEvents.windowUntil(PartEvent::isLast)
|
||||
|
||||
NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
|
||||
|
||||
The following shows how to bind request parameters, including an optional `DataBinder` customization:
|
||||
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
|
||||
and also shows how to customize the `DataBinder`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -374,14 +375,14 @@ Java::
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
|
||||
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin]
|
||||
----
|
||||
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
|
||||
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ purposes, it is useful to be able to alternate between rendering a model with an
|
||||
or as other formats (such as JSON or XML), depending on the content type requested by the client.
|
||||
To support doing so, Spring WebFlux provides the `HttpMessageWriterView`, which you can use to
|
||||
plug in any of the available xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] from
|
||||
`spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`, or `Jaxb2XmlEncoder`.
|
||||
`spring-web`, such as `JacksonJsonEncoder`, `JacksonSmileEncoder`, or `Jaxb2XmlEncoder`.
|
||||
|
||||
Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver` but is
|
||||
instead xref:web/webflux/config.adoc#webflux-config-view-resolvers[configured] as a default view.
|
||||
|
||||
@@ -135,6 +135,10 @@ Message codes and arguments for each error are also resolved via `MessageSource`
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
| `NoResourceFoundException`
|
||||
| (default)
|
||||
| `+{0}+` the request path (or portion of) used to find a resource
|
||||
|
||||
|===
|
||||
|
||||
NOTE: Unlike other exceptions, the message arguments for
|
||||
|
||||
@@ -334,19 +334,8 @@ Kotlin::
|
||||
`ServerCodecConfigurer` provides a set of default readers and writers. You can use it to add
|
||||
more readers and writers, customize the default ones, or replace the default ones completely.
|
||||
|
||||
For Jackson JSON and XML, consider using
|
||||
{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`],
|
||||
which customizes Jackson's default properties with the following ones:
|
||||
|
||||
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled.
|
||||
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled.
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {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.
|
||||
|
||||
For Jackson, consider using a Jackson format-specific builder like `JsonMapper.Builder` to configure Jackson's default
|
||||
properties.
|
||||
|
||||
[[webflux-config-view-resolvers]]
|
||||
== View Resolvers
|
||||
@@ -489,7 +478,7 @@ Java::
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.freeMarker();
|
||||
|
||||
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
|
||||
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
|
||||
registry.defaultViews(new HttpMessageWriterView(encoder));
|
||||
}
|
||||
|
||||
@@ -508,7 +497,7 @@ Kotlin::
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.freeMarker()
|
||||
|
||||
val encoder = Jackson2JsonEncoder()
|
||||
val encoder = JacksonJsonEncoder()
|
||||
registry.defaultViews(HttpMessageWriterView(encoder))
|
||||
}
|
||||
|
||||
@@ -701,7 +690,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
@Configuration
|
||||
class WebConfiguration : WebMvcConfigurer {
|
||||
class WebConfiguration : WebFluxConfigurer {
|
||||
|
||||
override fun configureApiVersioning(configurer: ApiVersionConfigurer) {
|
||||
configurer.useRequestHeader("API-Version")
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
The following table shows the supported controller method arguments.
|
||||
|
||||
Reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other]) are
|
||||
Reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other]) are
|
||||
supported on arguments that require blocking I/O (for example, reading the request body) to
|
||||
be resolved. This is marked in the Description column. Reactive types are not expected
|
||||
on arguments that do not require blocking.
|
||||
@@ -114,6 +114,6 @@ and others) and is equivalent to `required=false`.
|
||||
| Any other argument
|
||||
| If a method argument is not matched to any of the above, it is, by default, resolved as
|
||||
a `@RequestParam` if it is a simple type, as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
|
||||
or as a `@ModelAttribute`, otherwise.
|
||||
|===
|
||||
|
||||
+1
-1
@@ -205,7 +205,7 @@ controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any argument that is not a simple
|
||||
value type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
|
||||
|
||||
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
|
||||
|
||||
+1
-1
@@ -74,6 +74,6 @@ When a `@RequestParam` annotation is declared on a `Map<String, String>` or
|
||||
|
||||
Note that use of `@RequestParam` is optional -- for example, to set its attributes. By
|
||||
default, any argument that is a simple value type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver is treated as if it were annotated
|
||||
with `@RequestParam`.
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
WebFlux supports using a single value xref:web-reactive.adoc#webflux-reactive-libraries[reactive type] to
|
||||
WebFlux supports using a single value xref:web/webflux-reactive-libraries.adoc[reactive type] to
|
||||
produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types
|
||||
for the body. This allows a variety of async responses with `ResponseEntity` as follows:
|
||||
|
||||
|
||||
+1
-1
@@ -87,6 +87,6 @@ Reactor provides a dedicated operator for that, `Flux#collectList()`.
|
||||
| Other return values
|
||||
| If a return value remains unresolved in any other way, it is treated as a model
|
||||
attribute, unless it is a simple type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
|
||||
in which case it remains unresolved.
|
||||
|===
|
||||
|
||||
@@ -207,7 +207,7 @@ was not provided (for example, model attribute was returned) or an async return
|
||||
view resolution scenarios. Explore the options in your IDE with code completion.
|
||||
* `Model`, `Map`: Extra model attributes to be added to the model for the request.
|
||||
* Any other: Any other return value (except for simple types, as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
|
||||
is treated as a model attribute to be added to the model. The attribute name is derived
|
||||
from the class name by using {spring-framework-api}/core/Conventions.html[conventions],
|
||||
unless a handler method `@ModelAttribute` annotation is present.
|
||||
|
||||
@@ -77,7 +77,7 @@ as input, adapts it to a Reactor type internally, uses that, and returns either
|
||||
`Flux` or a `Mono` as output. So, you can pass any `Publisher` as input and you can apply
|
||||
operations on the output, but you need to adapt the output for use with another reactive library.
|
||||
Whenever feasible (for example, annotated controllers), WebFlux adapts transparently to the use
|
||||
of RxJava or another reactive library. See xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] for more details.
|
||||
of RxJava or another reactive library. See xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] for more details.
|
||||
|
||||
NOTE: In addition to Reactive APIs, WebFlux can also be used with
|
||||
xref:languages/kotlin/coroutines.adoc[Coroutines] APIs in Kotlin which provides a more imperative style of programming.
|
||||
@@ -148,7 +148,7 @@ RxJava to perform blocking calls on a separate thread but you would not be makin
|
||||
most of a non-blocking web stack.
|
||||
|
||||
* If you have a Spring MVC application with calls to remote services, try the reactive `WebClient`.
|
||||
You can return reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other])
|
||||
You can return reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other])
|
||||
directly from Spring MVC controller methods. The greater the latency per call or the
|
||||
interdependency among calls, the more dramatic the benefits. Spring MVC controllers
|
||||
can call other reactive components too.
|
||||
|
||||
@@ -459,22 +459,22 @@ xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs].
|
||||
JSON and binary JSON ({jackson-github-org}/smile-format-specification[Smile]) are
|
||||
both supported when the Jackson library is present.
|
||||
|
||||
The `Jackson2Decoder` works as follows:
|
||||
The `JacksonJsonDecoder` works as follows:
|
||||
|
||||
* Jackson's asynchronous, non-blocking parser is used to aggregate a stream of byte chunks
|
||||
into ``TokenBuffer``'s each representing a JSON object.
|
||||
* Each `TokenBuffer` is passed to Jackson's `ObjectMapper` to create a higher level object.
|
||||
* Each `TokenBuffer` is passed to Jackson's `JsonMapper` to create a higher level object.
|
||||
* When decoding to a single-value publisher (for example, `Mono`), there is one `TokenBuffer`.
|
||||
* When decoding to a multi-value publisher (for example, `Flux`), each `TokenBuffer` is passed to
|
||||
the `ObjectMapper` as soon as enough bytes are received for a fully formed object. The
|
||||
the `JsonMapper` as soon as enough bytes are received for a fully formed object. The
|
||||
input content can be a JSON array, or any
|
||||
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format such as NDJSON,
|
||||
JSON Lines, or JSON Text Sequences.
|
||||
|
||||
The `Jackson2Encoder` works as follows:
|
||||
The `JacksonJsonEncoder` works as follows:
|
||||
|
||||
* For a single value publisher (for example, `Mono`), simply serialize it through the
|
||||
`ObjectMapper`.
|
||||
`JsonMapper`.
|
||||
* For a multi-value publisher with `application/json`, by default collect the values with
|
||||
`Flux#collectToList()` and then serialize the resulting collection.
|
||||
* For a multi-value publisher with a streaming media type such as
|
||||
@@ -482,12 +482,12 @@ The `Jackson2Encoder` works as follows:
|
||||
flush each value individually using a
|
||||
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format. Other
|
||||
streaming media types may be registered with the encoder.
|
||||
* For SSE the `Jackson2Encoder` is invoked per event and the output is flushed to ensure
|
||||
* For SSE the `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure
|
||||
delivery without delay.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
By default both `Jackson2Encoder` and `Jackson2Decoder` do not support elements of type
|
||||
By default both `JacksonJsonEncoder` and `JacksonJsonDecoder` do not support elements of type
|
||||
`String`. Instead the default assumption is that a string or a sequence of strings
|
||||
represent serialized JSON content, to be rendered by the `CharSequenceEncoder`. If what
|
||||
you need is to render a JSON array from `Flux<String>`, use `Flux#collectToList()` and
|
||||
|
||||
@@ -71,7 +71,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the MVC Java configuration or the XML namespace to declare such mappings, which results
|
||||
in a single global map being passed to all `HandlerMapping` instances.
|
||||
@@ -84,7 +84,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, for example, `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
|
||||
@@ -184,7 +184,8 @@ val map = request.params()
|
||||
----
|
||||
======
|
||||
|
||||
The following shows how to bind request parameters, including an optional `DataBinder` customization:
|
||||
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
|
||||
and also shows how to customize the `DataBinder`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
@@ -10,7 +10,7 @@ Spring offers support for the Jackson JSON library.
|
||||
== Jackson-based JSON MVC Views
|
||||
[.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]#
|
||||
|
||||
The `MappingJackson2JsonView` uses the Jackson library's `ObjectMapper` to render the response
|
||||
The `JacksonJsonView` uses the Jackson library's `JsonMapper` to render the response
|
||||
content as JSON. By default, the entire contents of the model map (with the exception of
|
||||
framework-specific classes) are encoded as JSON. For cases where the contents of the
|
||||
map need to be filtered, you can specify a specific set of model attributes to encode
|
||||
@@ -18,17 +18,17 @@ by using the `modelKeys` property. You can also use the `extractValueFromSingleK
|
||||
property to have the value in single-key models extracted and serialized directly rather
|
||||
than as a map of model attributes.
|
||||
|
||||
You can customize JSON mapping as needed by using Jackson's provided
|
||||
annotations. When you need further control, you can inject a custom `ObjectMapper`
|
||||
through the `ObjectMapper` property, for cases where you need to provide custom JSON
|
||||
serializers and deserializers for specific types.
|
||||
You can customize JSON mapping as needed by using Jackson's provided annotations. When
|
||||
you need further control, you can inject a custom `JsonMapper` through the `JsonMapper`
|
||||
or `JsonMapper.Builder` constructor parameters, for cases where you need to provide
|
||||
custom JSON serializers and deserializers for specific types.
|
||||
|
||||
|
||||
[[mvc-view-xml-mapping]]
|
||||
== Jackson-based XML Views
|
||||
[.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]#
|
||||
|
||||
`MappingJackson2XmlView` uses the
|
||||
`JacksonXmlView` uses the
|
||||
{jackson-github-org}/jackson-dataformat-xml[Jackson XML extension's] `XmlMapper`
|
||||
to render the response content as XML. If the model contains multiple entries, you should
|
||||
explicitly set the object to be serialized by using the `modelKey` bean property. If the
|
||||
@@ -36,5 +36,5 @@ model contains a single entry, it is serialized automatically.
|
||||
|
||||
You can customize XML mapping as needed by using JAXB or Jackson's provided
|
||||
annotations. When you need further control, you can inject a custom `XmlMapper`
|
||||
through the `ObjectMapper` property, for cases where custom XML
|
||||
you need to provide serializers and deserializers for specific types.
|
||||
created via `XmlMapper.Builder` for cases where custom XML you need to provide
|
||||
serializers and deserializers for specific types.
|
||||
|
||||
@@ -42,25 +42,25 @@ This converter requires a `Marshaller` and `Unmarshaller` before it can be used.
|
||||
You can inject these through constructor or bean properties.
|
||||
By default, this converter supports `text/xml` and `application/xml`.
|
||||
|
||||
| `MappingJackson2HttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `ObjectMapper`.
|
||||
| `JacksonJsonHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `JsonMapper`.
|
||||
You can customize JSON mapping as needed through the use of Jackson's provided annotations.
|
||||
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper` through the `ObjectMapper` property.
|
||||
By default, this converter supports `application/json`. This requires the `com.fasterxml.jackson.core:jackson-databind` dependency.
|
||||
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder ` constructor parameters.
|
||||
By default, this converter supports `application/json`. This requires the `tools.jackson.core:jackson-databind` dependency.
|
||||
|
||||
| `MappingJackson2XmlHttpMessageConverter`
|
||||
| `JacksonXmlHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write XML by using {jackson-github-org}/jackson-dataformat-xml[Jackson XML] extension's `XmlMapper`.
|
||||
You can customize XML mapping as needed through the use of JAXB or Jackson's provided annotations.
|
||||
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `ObjectMapper` property.
|
||||
By default, this converter supports `application/xml`. This requires the `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` dependency.
|
||||
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `JsonMapper` or `JsonMapper.Builder` constructor parameters.
|
||||
By default, this converter supports `application/xml`. This requires the `tools.jackson.dataformat:jackson-dataformat-xml` dependency.
|
||||
|
||||
| `KotlinSerializationJsonHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON using `kotlinx.serialization`.
|
||||
This converter is not configured by default, as this conflicts with Jackson.
|
||||
Developers must configure it as an additional converter ahead of the Jackson one.
|
||||
|
||||
| `MappingJackson2CborHttpMessageConverter`
|
||||
| `com.fasterxml.jackson.dataformat:jackson-dataformat-cbor`
|
||||
| `JacksonCborHttpMessageConverter`
|
||||
| `tools.jackson.dataformat:jackson-dataformat-cbor`
|
||||
|
||||
| `SourceHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write `javax.xml.transform.Source` from the HTTP request and response.
|
||||
|
||||
@@ -414,7 +414,7 @@ customize the status and headers of the response.
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs-streaming[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC supports use of reactive client libraries in a controller (also read
|
||||
xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] in the WebFlux section).
|
||||
xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] in the WebFlux section).
|
||||
This includes the `WebClient` from `spring-webflux` and others, such as Spring Data
|
||||
reactive data repositories. In such scenarios, it is convenient to be able to return
|
||||
reactive types from the controller method.
|
||||
|
||||
@@ -171,11 +171,11 @@ Message codes and arguments for each error are also resolved via `MessageSource`
|
||||
|
||||
| `NoResourceFoundException`
|
||||
| (default)
|
||||
|
|
||||
| `+{0}+` the request path (or portion of) used to find a resource
|
||||
|
||||
| `TypeMismatchException`
|
||||
| (default)
|
||||
| `+{0}+` property name, `+{1}+` property value
|
||||
| `+{0}+` property name, `+{1}+` property value, `+{2}+` simple name of required type
|
||||
|
||||
| `UnsatisfiedServletRequestParameterException`
|
||||
| (default)
|
||||
|
||||
+1
-1
@@ -215,7 +215,7 @@ the content negotiation during the error handling phase will decide which conten
|
||||
|
||||
| Any other return value
|
||||
| If a return value is not matched to any of the above and is not a simple type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
|
||||
by default, it is treated as a model attribute to be added to the model. If it is a simple type,
|
||||
it remains unresolved.
|
||||
|===
|
||||
|
||||
+1
-1
@@ -134,6 +134,6 @@ and others) and is equivalent to `required=false`.
|
||||
| Any other argument
|
||||
| If a method argument is not matched to any of the earlier values in this table and it is
|
||||
a simple type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
|
||||
it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`.
|
||||
|===
|
||||
|
||||
+1
-1
@@ -250,7 +250,7 @@ xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple
|
||||
value type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
|
||||
|
||||
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
|
||||
|
||||
+1
-1
@@ -117,6 +117,6 @@ Kotlin::
|
||||
|
||||
Note that use of `@RequestParam` is optional (for example, to set its attributes).
|
||||
By default, any argument that is a simple value type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver, is treated as if it were annotated
|
||||
with `@RequestParam`.
|
||||
|
||||
+1
-1
@@ -98,6 +98,6 @@ supported for all return values.
|
||||
| Other return values
|
||||
| If a return value remains unresolved in any other way, it is treated as a model
|
||||
attribute, unless it is a simple type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
|
||||
in which case it remains unresolved.
|
||||
|===
|
||||
|
||||
@@ -214,7 +214,7 @@ When multiple patterns match a URL, the best match must be selected. This is don
|
||||
one of the following depending on whether use of parsed `PathPattern` is enabled for use or not:
|
||||
|
||||
* {spring-framework-api}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`]
|
||||
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`]
|
||||
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator(java.lang.String)[`AntPathMatcher.getPatternComparator(String path)`]
|
||||
|
||||
Both help to sort patterns with more specific ones on top. A pattern is more specific if
|
||||
it has a lower count of URI variables (counted as 1), single wildcards (counted as 1),
|
||||
|
||||
@@ -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.14.2",
|
||||
"@springio/antora-extensions": "1.14.7",
|
||||
"fast-xml-parser": "4.5.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter ;
|
||||
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
@@ -34,7 +34,7 @@ class AccountControllerIntegrationTests {
|
||||
|
||||
AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) {
|
||||
this.mockMvc = MockMvcTester.from(wac).withHttpMessageConverters(
|
||||
List.of(wac.getBean(AbstractJackson2HttpMessageConverter.class)));
|
||||
List.of(wac.getBean(AbstractJacksonHttpMessageConverter.class)));
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@@ -30,7 +30,7 @@ public class FreeMarkerConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.enableContentNegotiation(new MappingJackson2JsonView());
|
||||
registry.enableContentNegotiation(new JacksonJsonView());
|
||||
registry.freeMarker().cache(false);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@@ -28,7 +28,7 @@ public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.enableContentNegotiation(new MappingJackson2JsonView());
|
||||
registry.enableContentNegotiation(new JacksonJsonView());
|
||||
registry.jsp();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.conv
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
|
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter
|
||||
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester
|
||||
import org.springframework.web.context.WebApplicationContext
|
||||
@@ -30,7 +30,7 @@ import org.springframework.web.context.WebApplicationContext
|
||||
class AccountControllerIntegrationTests(@Autowired wac: WebApplicationContext) {
|
||||
|
||||
private val mockMvc = MockMvcTester.from(wac).withHttpMessageConverters(
|
||||
listOf(wac.getBean(AbstractJackson2HttpMessageConverter::class.java)))
|
||||
listOf(wac.getBean(AbstractJacksonHttpMessageConverter::class.java)))
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
+2
-2
@@ -7,14 +7,14 @@ import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView
|
||||
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
class FreeMarkerConfiguration : WebMvcConfigurer {
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.enableContentNegotiation(MappingJackson2JsonView())
|
||||
registry.enableContentNegotiation(JacksonJsonView())
|
||||
registry.freeMarker().cache(false)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -21,13 +21,13 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView
|
||||
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
class WebConfiguration : WebMvcConfigurer {
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.enableContentNegotiation(MappingJackson2JsonView())
|
||||
registry.enableContentNegotiation(JacksonJsonView())
|
||||
registry.jsp()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
<mvc:view-resolvers>
|
||||
<mvc:content-negotiation>
|
||||
<mvc:default-views>
|
||||
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
|
||||
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
|
||||
</mvc:default-views>
|
||||
</mvc:content-negotiation>
|
||||
<mvc:freemarker cache-views="false"/>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
<mvc:view-resolvers>
|
||||
<mvc:content-negotiation>
|
||||
<mvc:default-views>
|
||||
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
|
||||
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
|
||||
</mvc:default-views>
|
||||
</mvc:content-negotiation>
|
||||
<mvc:jsp/>
|
||||
|
||||
@@ -8,15 +8,15 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.20.1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.0"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.1"))
|
||||
api(platform("io.netty:netty-bom:4.2.7.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.0"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.1"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.2"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.25.1"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.3"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.25.2"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.6"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.4"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.4"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.5"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.5"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
|
||||
api(platform("org.junit:junit-bom:6.0.1"))
|
||||
@@ -33,7 +33,7 @@ dependencies {
|
||||
api("com.google.code.gson:gson:2.13.2")
|
||||
api("com.google.protobuf:protobuf-java-util:4.32.1")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.jayway.jsonpath:json-path:2.10.0")
|
||||
api("com.networknt:json-schema-validator:1.5.3")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
api("com.rometools:rome:1.19.0")
|
||||
@@ -111,7 +111,7 @@ dependencies {
|
||||
api("org.dom4j:dom4j:2.2.0")
|
||||
api("org.easymock:easymock:5.6.0")
|
||||
api("org.eclipse.angus:angus-mail:2.0.3")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.4")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B11")
|
||||
api("org.eclipse:yasson:3.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -121,7 +121,7 @@ dependencies {
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.graalvm.sdk:graal-sdk:22.3.1")
|
||||
api("org.hamcrest:hamcrest:3.0")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.0.CR2")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.0.CR3")
|
||||
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.18.0")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=7.0.1
|
||||
version=7.0.2
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -54,4 +54,4 @@ _When instructed to execute `./gradlew` from the command line, be sure to execut
|
||||
|
||||
In any case, please do not check in your own generated `.classpath` file, `.project`
|
||||
file, or `.settings` folder. You'll notice these files are already intentionally in
|
||||
`.gitignore`. The same policy holds for IDEA metadata.
|
||||
`.gitignore`. The same policy holds for IntelliJ IDEA metadata.
|
||||
|
||||
+2
-3
@@ -48,7 +48,6 @@ import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConvertingComparator;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -84,10 +83,10 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
|
||||
// @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation)
|
||||
// invokes proceed() in a `try` block and only invokes the @After advice method
|
||||
// in a corresponding `finally` block.
|
||||
Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
|
||||
Comparator<Method> adviceKindComparator = new ConvertingComparator<Method, @Nullable Annotation>(
|
||||
new InstanceComparator<>(
|
||||
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
|
||||
(Converter<Method, Annotation>) method -> {
|
||||
method -> {
|
||||
AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
|
||||
return (ann != null ? ann.getAnnotation() : null);
|
||||
});
|
||||
|
||||
+11
-7
@@ -44,7 +44,6 @@ import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
|
||||
@@ -296,10 +295,8 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
||||
|
||||
/**
|
||||
* Build a cache key for the given bean class and bean name.
|
||||
* <p>Note: As of 4.2.3, this implementation does not return a concatenated
|
||||
* class/name String anymore but rather the most efficient cache key possible:
|
||||
* a plain bean name, prepended with {@link BeanFactory#FACTORY_BEAN_PREFIX}
|
||||
* in case of a {@code FactoryBean}; or if no bean name specified, then the
|
||||
* <p>Note: As of 7.0.2, this implementation returns a composed cache key
|
||||
* for bean class plus bean name; or if no bean name specified, then the
|
||||
* given bean {@code Class} as-is.
|
||||
* @param beanClass the bean class
|
||||
* @param beanName the bean name
|
||||
@@ -307,8 +304,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
||||
*/
|
||||
protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
|
||||
if (StringUtils.hasLength(beanName)) {
|
||||
return (FactoryBean.class.isAssignableFrom(beanClass) ?
|
||||
BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
|
||||
return new ComposedCacheKey(beanClass, beanName);
|
||||
}
|
||||
else {
|
||||
return beanClass;
|
||||
@@ -615,4 +611,12 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
||||
protected abstract Object @Nullable [] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName,
|
||||
@Nullable TargetSource customTargetSource) throws BeansException;
|
||||
|
||||
|
||||
/**
|
||||
* Composed cache key for bean class plus bean name.
|
||||
* @see #getCacheKey(Class, String)
|
||||
*/
|
||||
private record ComposedCacheKey(Class<?> beanClass, String beanName) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -365,9 +365,12 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
|
||||
}
|
||||
MethodParameter methodParam = descriptor.getMethodParameter();
|
||||
if (methodParam != null) {
|
||||
for (Annotation annotation : methodParam.getMethodAnnotations()) {
|
||||
if (isQualifier(annotation.annotationType())) {
|
||||
return true;
|
||||
Method method = methodParam.getMethod();
|
||||
if (method == null || void.class == method.getReturnType()) {
|
||||
for (Annotation annotation : methodParam.getMethodAnnotations()) {
|
||||
if (isQualifier(annotation.annotationType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -203,8 +203,8 @@ public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
|
||||
public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
|
||||
if (value instanceof RuntimeBeanReference runtimeBeanReference &&
|
||||
runtimeBeanReference.getBeanType() != null) {
|
||||
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
|
||||
runtimeBeanReference.getBeanType());
|
||||
return CodeBlock.of("new $T($S, $T.class)", RuntimeBeanReference.class,
|
||||
runtimeBeanReference.getBeanName(), runtimeBeanReference.getBeanType());
|
||||
}
|
||||
else if (value instanceof BeanReference beanReference) {
|
||||
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
|
||||
|
||||
+9
@@ -18,6 +18,7 @@ package org.springframework.beans.factory.aot;
|
||||
|
||||
import org.springframework.aot.generate.GeneratedMethods;
|
||||
import org.springframework.aot.generate.MethodReference;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
|
||||
/**
|
||||
* Interface that can be used to configure the code that will be generated to
|
||||
@@ -25,6 +26,7 @@ import org.springframework.aot.generate.MethodReference;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
* @see BeanFactoryInitializationAotContribution
|
||||
*/
|
||||
@@ -41,6 +43,13 @@ public interface BeanFactoryInitializationCode {
|
||||
*/
|
||||
GeneratedMethods getMethods();
|
||||
|
||||
/**
|
||||
* Return the name of the class used by the initializing code.
|
||||
* @return the generated class name
|
||||
* @since 7.0.2
|
||||
*/
|
||||
ClassName getClassName();
|
||||
|
||||
/**
|
||||
* Add an initializer method call. An initializer can use a flexible signature,
|
||||
* using any of the following:
|
||||
|
||||
+1
-1
@@ -384,7 +384,7 @@ public class InstanceSupplierCodeGenerator {
|
||||
Visibility visibility = AccessControl.lowest(classAccessControl, memberAccessControl).getVisibility();
|
||||
return (visibility == Visibility.PUBLIC || (visibility != Visibility.PRIVATE &&
|
||||
member.getDeclaringClass().getPackageName().equals(this.className.packageName())));
|
||||
}
|
||||
}
|
||||
|
||||
private CodeBlock generateParameterTypesCode(Class<?>[] parameterTypes) {
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
|
||||
+49
-18
@@ -1632,7 +1632,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
|
||||
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
|
||||
if (Optional.class == descriptor.getDependencyType()) {
|
||||
return createOptionalDependency(descriptor, requestingBeanName);
|
||||
return createOptionalDependency(descriptor, requestingBeanName, autowiredBeanNames, null);
|
||||
}
|
||||
else if (ObjectFactory.class == descriptor.getDependencyType() ||
|
||||
ObjectProvider.class == descriptor.getDependencyType()) {
|
||||
@@ -2330,8 +2330,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
/**
|
||||
* Create an {@link Optional} wrapper for the specified dependency.
|
||||
*/
|
||||
private Optional<?> createOptionalDependency(
|
||||
DependencyDescriptor descriptor, @Nullable String beanName, final @Nullable Object... args) {
|
||||
private Optional<?> createOptionalDependency(DependencyDescriptor descriptor, @Nullable String beanName,
|
||||
@Nullable Set<String> autowiredBeanNames, @Nullable Object @Nullable [] args) {
|
||||
|
||||
DependencyDescriptor descriptorToUse = new NestedDependencyDescriptor(descriptor) {
|
||||
@Override
|
||||
@@ -2348,7 +2348,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
return ObjectUtils.isEmpty(args);
|
||||
}
|
||||
};
|
||||
Object result = doResolveDependency(descriptorToUse, beanName, null, null);
|
||||
Object result = doResolveDependency(descriptorToUse, beanName, autowiredBeanNames, null);
|
||||
return (result instanceof Optional<?> optional ? optional : Optional.ofNullable(result));
|
||||
}
|
||||
|
||||
@@ -2501,12 +2501,18 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
*/
|
||||
private class DependencyObjectProvider implements BeanObjectProvider<Object> {
|
||||
|
||||
private static final Object NOT_CACHEABLE = new Object();
|
||||
|
||||
private static final Object NULL_VALUE = new Object();
|
||||
|
||||
private final DependencyDescriptor descriptor;
|
||||
|
||||
private final boolean optional;
|
||||
|
||||
private final @Nullable String beanName;
|
||||
|
||||
private transient volatile @Nullable Object cachedValue;
|
||||
|
||||
public DependencyObjectProvider(DependencyDescriptor descriptor, @Nullable String beanName) {
|
||||
this.descriptor = new NestedDependencyDescriptor(descriptor);
|
||||
this.optional = (this.descriptor.getDependencyType() == Optional.class);
|
||||
@@ -2515,22 +2521,17 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
|
||||
@Override
|
||||
public Object getObject() throws BeansException {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName);
|
||||
}
|
||||
else {
|
||||
Object result = doResolveDependency(this.descriptor, this.beanName, null, null);
|
||||
if (result == null) {
|
||||
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType());
|
||||
}
|
||||
return result;
|
||||
Object result = getValue();
|
||||
if (result == null) {
|
||||
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getObject(final @Nullable Object... args) throws BeansException {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName, args);
|
||||
return createOptionalDependency(this.descriptor, this.beanName, null, args);
|
||||
}
|
||||
else {
|
||||
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
|
||||
@@ -2551,7 +2552,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
public @Nullable Object getIfAvailable() throws BeansException {
|
||||
try {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName);
|
||||
return createOptionalDependency(this.descriptor, this.beanName, null, null);
|
||||
}
|
||||
else {
|
||||
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
|
||||
@@ -2604,7 +2605,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
};
|
||||
try {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(descriptorToUse, this.beanName);
|
||||
return createOptionalDependency(descriptorToUse, this.beanName, null, null);
|
||||
}
|
||||
else {
|
||||
return doResolveDependency(descriptorToUse, this.beanName, null, null);
|
||||
@@ -2630,11 +2631,41 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
|
||||
protected @Nullable Object getValue() throws BeansException {
|
||||
Object value = this.cachedValue;
|
||||
if (value == null) {
|
||||
if (isConfigurationFrozen()) {
|
||||
Set<String> autowiredBeanNames = new LinkedHashSet<>(2);
|
||||
value = resolveValue(autowiredBeanNames);
|
||||
boolean cacheable = false;
|
||||
if (!autowiredBeanNames.isEmpty()) {
|
||||
cacheable = true;
|
||||
for (String autowiredBeanName : autowiredBeanNames) {
|
||||
if (!containsBean(autowiredBeanName) || !isSingleton(autowiredBeanName)) {
|
||||
cacheable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cachedValue = (cacheable ? (value != null ? value : NULL_VALUE) : NOT_CACHEABLE);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
else if (value == NULL_VALUE) {
|
||||
return null;
|
||||
}
|
||||
else if (value != NOT_CACHEABLE) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Not cacheable -> fresh resolution.
|
||||
return resolveValue(null);
|
||||
}
|
||||
|
||||
private @Nullable Object resolveValue(@Nullable Set<String> autowiredBeanNames) {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName);
|
||||
return createOptionalDependency(this.descriptor, this.beanName, autowiredBeanNames, null);
|
||||
}
|
||||
else {
|
||||
return doResolveDependency(this.descriptor, this.beanName, null, null);
|
||||
return doResolveDependency(this.descriptor, this.beanName, autowiredBeanNames, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -532,7 +532,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
|
||||
/**
|
||||
* Callback before singleton creation.
|
||||
* <p>The default implementation register the singleton as currently in creation.
|
||||
* <p>The default implementation registers the singleton as currently in creation.
|
||||
* @param beanName the name of the singleton about to be created
|
||||
* @see #isSingletonCurrentlyInCreation
|
||||
*/
|
||||
@@ -582,7 +582,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
public void registerContainedBean(String containedBeanName, String containingBeanName) {
|
||||
synchronized (this.containedBeanMap) {
|
||||
Set<String> containedBeans =
|
||||
this.containedBeanMap.computeIfAbsent(containingBeanName, k -> new LinkedHashSet<>(8));
|
||||
this.containedBeanMap.computeIfAbsent(containingBeanName, key -> new LinkedHashSet<>(8));
|
||||
if (!containedBeans.add(containedBeanName)) {
|
||||
return;
|
||||
}
|
||||
@@ -601,7 +601,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
|
||||
synchronized (this.dependentBeanMap) {
|
||||
Set<String> dependentBeans =
|
||||
this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8));
|
||||
this.dependentBeanMap.computeIfAbsent(canonicalName, key -> new LinkedHashSet<>(8));
|
||||
if (!dependentBeans.add(dependentBeanName)) {
|
||||
return;
|
||||
}
|
||||
@@ -609,7 +609,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
|
||||
synchronized (this.dependenciesForBeanMap) {
|
||||
Set<String> dependenciesForBean =
|
||||
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
|
||||
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, key -> new LinkedHashSet<>(8));
|
||||
dependenciesForBean.add(canonicalName);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-2
@@ -1587,6 +1587,18 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void objectFactoryFieldInjectionAgainstFrozen() {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1596,6 +1608,18 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryConstructorInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryConstructorInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void objectFactoryConstructorInjectionAgainstFrozen() {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryConstructorInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryConstructorInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryConstructorInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2116,8 +2140,8 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
|
||||
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
|
||||
|
||||
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
|
||||
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
|
||||
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
|
||||
@@ -2728,9 +2752,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
bf.registerSingleton("nonNullBean", "Test");
|
||||
bf.registerBeanDefinition("mixedNullableInjectionBean",
|
||||
new RootBeanDefinition(MixedNullableInjectionBean.class));
|
||||
|
||||
MixedNullableInjectionBean mixedNullableInjectionBean = bf.getBean(MixedNullableInjectionBean.class);
|
||||
assertThat(mixedNullableInjectionBean.nonNullBean).isNotNull();
|
||||
assertThat(mixedNullableInjectionBean.nullableBean).isNull();
|
||||
assertThat(bf.getDependentBeans("nonNullBean")).contains("mixedNullableInjectionBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2738,9 +2764,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
bf.registerSingleton("nonNullBean", "Test");
|
||||
bf.registerBeanDefinition("mixedOptionalInjectionBean",
|
||||
new RootBeanDefinition(MixedOptionalInjectionBean.class));
|
||||
|
||||
MixedOptionalInjectionBean mixedOptionalInjectionBean = bf.getBean(MixedOptionalInjectionBean.class);
|
||||
assertThat(mixedOptionalInjectionBean.nonNullBean).isNotNull();
|
||||
assertThat(mixedOptionalInjectionBean.nullableBean).isNull();
|
||||
assertThat(bf.getDependentBeans("nonNullBean")).contains("mixedOptionalInjectionBean");
|
||||
}
|
||||
|
||||
|
||||
|
||||
+44
-3
@@ -370,8 +370,25 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testObjectFactoryWithBeanFieldAgainstFrozen() throws Exception {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.setSerializationId("test");
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -382,8 +399,25 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryMethodInjectionBean bean = (ObjectFactoryMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testObjectFactoryWithBeanMethodAgainstFrozen() throws Exception {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMethodInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.setSerializationId("test");
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryMethodInjectionBean bean = (ObjectFactoryMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -444,8 +478,8 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
|
||||
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
|
||||
|
||||
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
|
||||
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
|
||||
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
|
||||
@@ -459,6 +493,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -476,6 +511,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -494,6 +530,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalFieldInjectionBean bean = (OptionalFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -512,6 +549,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalMethodInjectionBean bean = (OptionalMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -530,6 +568,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalListFieldInjectionBean bean = (OptionalListFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).hasValueSatisfying(list ->
|
||||
assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class)));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -548,6 +587,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalListMethodInjectionBean bean = (OptionalListMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).hasValueSatisfying(list ->
|
||||
assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class)));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -566,6 +606,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
ProviderOfOptionalFieldInjectionBean bean = (ProviderOfOptionalFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).doesNotContain("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -584,6 +625,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
ProviderOfOptionalMethodInjectionBean bean = (ProviderOfOptionalMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).doesNotContain("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -788,7 +830,6 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
|
||||
public ConstructorResourceInjectionBean() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
+21
-9
@@ -458,30 +458,42 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
|
||||
class BeanReferenceTests {
|
||||
|
||||
@Test
|
||||
void generatedWhenBeanNameReference() {
|
||||
RuntimeBeanNameReference beanReference = new RuntimeBeanNameReference("test");
|
||||
void generatedWhenRuntimeBeanNameReference() {
|
||||
BeanReference beanReference = new RuntimeBeanNameReference("test");
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
|
||||
assertThat(actual.getBeanType()).as("type").isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedWhenBeanReferenceByName() {
|
||||
RuntimeBeanReference beanReference = new RuntimeBeanReference("test");
|
||||
void generatedWhenRuntimeBeanReferenceByName() {
|
||||
BeanReference beanReference = new RuntimeBeanReference("test");
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
|
||||
assertThat(actual.getBeanType()).isEqualTo(beanReference.getBeanType());
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
|
||||
assertThat(actual.getBeanType()).as("type").isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedWhenBeanReferenceByType() {
|
||||
void generatedWhenRuntimeBeanReferenceByType() {
|
||||
BeanReference beanReference = new RuntimeBeanReference(String.class);
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanType()).isEqualTo(String.class);
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo(String.class.getName());
|
||||
assertThat(actual.getBeanType()).as("type").isEqualTo(String.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test // gh-35913
|
||||
void generatedWhenRuntimeBeanReferenceByNameAndType() {
|
||||
BeanReference beanReference = new RuntimeBeanReference("test", String.class);
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
|
||||
assertThat(actual.getBeanType()).as("type").isEqualTo(String.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
@@ -268,6 +268,7 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
* re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
|
||||
* @since 6.2.14
|
||||
*/
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
this.cacheMap.values().forEach(Cache::clear);
|
||||
if (this.dynamic) {
|
||||
|
||||
Vendored
+13
@@ -130,4 +130,17 @@ public class JCacheCacheManager extends AbstractTransactionSupportingCacheManage
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
CacheManager cacheManager = getCacheManager();
|
||||
if (cacheManager != null && !cacheManager.isClosed()) {
|
||||
for (String cacheName : cacheManager.getCacheNames()) {
|
||||
javax.cache.Cache<Object, Object> jcache = cacheManager.getCache(cacheName);
|
||||
if (jcache != null && !jcache.isClosed()) {
|
||||
jcache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Vendored
+15
-2
@@ -51,8 +51,7 @@ class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests<JCacheCache>
|
||||
this.cacheManager.createCache(CACHE_NAME_NO_NULL, new MutableConfiguration<>());
|
||||
this.nativeCache = this.cacheManager.getCache(CACHE_NAME);
|
||||
this.cache = new JCacheCache(this.nativeCache);
|
||||
Cache<Object, Object> nativeCacheNoNull =
|
||||
this.cacheManager.getCache(CACHE_NAME_NO_NULL);
|
||||
Cache<Object, Object> nativeCacheNoNull = this.cacheManager.getCache(CACHE_NAME_NO_NULL);
|
||||
this.cacheNoNull = new JCacheCache(nativeCacheNoNull, false);
|
||||
}
|
||||
|
||||
@@ -100,4 +99,18 @@ class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests<JCacheCache>
|
||||
assertThat(cache.get(key).get()).isEqualTo(value);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetCaches() {
|
||||
JCacheCacheManager cm = new JCacheCacheManager(cacheManager);
|
||||
org.springframework.cache.Cache cache = cm.getCache(CACHE_NAME);
|
||||
cache.put("key", "value");
|
||||
assertThat(cm.getCacheNames()).contains(CACHE_NAME);
|
||||
assertThat(cm.getCache(CACHE_NAME)).isNotNull().isSameAs(cache);
|
||||
assertThat(cacheManager.getCache(CACHE_NAME).iterator()).hasNext();
|
||||
cm.resetCaches();
|
||||
assertThat(cm.getCacheNames()).contains(CACHE_NAME);
|
||||
assertThat(cm.getCache(CACHE_NAME)).isNotNull().isSameAs(cache);
|
||||
assertThat(cacheManager.getCache(CACHE_NAME).iterator()).isExhausted();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jspecify.annotations.Nullable;
|
||||
*
|
||||
* @author Costin Leau
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
*/
|
||||
public interface CacheManager {
|
||||
@@ -47,4 +48,31 @@ public interface CacheManager {
|
||||
*/
|
||||
Collection<String> getCacheNames();
|
||||
|
||||
/**
|
||||
* Remove all registered caches from this cache manager if possible,
|
||||
* re-creating them on demand. After this call, {@link #getCacheNames()}
|
||||
* will possibly be empty and the cache provider will have dropped all
|
||||
* cache management state.
|
||||
* <p>Alternatively, an implementation may perform an equivalent reset
|
||||
* on fixed existing cache regions without actually dropping the cache.
|
||||
* This behavior will be indicated by {@link #getCacheNames()} still
|
||||
* exposing a non-empty set of names, whereas the corresponding cache
|
||||
* regions will not contain cache entries anymore.
|
||||
* <p>The default implementation calls {@link Cache#clear} on all
|
||||
* registered caches, retaining all caches as registered, satisfying
|
||||
* the alternative implementation path above. Custom implementations
|
||||
* may either drop the actual caches (re-creating them on demand) or
|
||||
* perform a more exhaustive reset at the actual cache provider level.
|
||||
* @since 7.0.2
|
||||
* @see Cache#clear()
|
||||
*/
|
||||
default void resetCaches() {
|
||||
for (String cacheName : getCacheNames()) {
|
||||
Cache cache = getCache(cacheName);
|
||||
if (cache != null) {
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -177,6 +177,11 @@ public @interface EnableCaching {
|
||||
* be upgraded to subclass proxying at the same time. This approach has no negative
|
||||
* impact in practice unless one is explicitly expecting one type of proxy vs another,
|
||||
* for example, in tests.
|
||||
* <p>It is usually recommendable to rely on a global default proxy configuration
|
||||
* instead, with specific proxy requirements for certain beans expressed through
|
||||
* a {@link org.springframework.context.annotation.Proxyable} annotation on
|
||||
* the affected bean classes.
|
||||
* @see org.springframework.aop.config.AopConfigUtils#forceAutoProxyCreatorToUseClassProxying
|
||||
*/
|
||||
boolean proxyTargetClass() default false;
|
||||
|
||||
|
||||
Vendored
+1
@@ -184,6 +184,7 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
|
||||
* re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
|
||||
* @since 6.2.14
|
||||
*/
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
this.cacheMap.values().forEach(Cache::clear);
|
||||
if (this.dynamic) {
|
||||
|
||||
+3
-3
@@ -113,7 +113,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
|
||||
builder.setUnless(getAttributeValue(opElement, "unless", ""));
|
||||
builder.setSync(Boolean.parseBoolean(getAttributeValue(opElement, "sync", "false")));
|
||||
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
|
||||
col.add(builder.build());
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
|
||||
builder.setBeforeInvocation(Boolean.parseBoolean(after.trim()));
|
||||
}
|
||||
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
|
||||
col.add(builder.build());
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
|
||||
parserContext.getReaderContext(), new CachePutOperation.Builder());
|
||||
builder.setUnless(getAttributeValue(opElement, "unless", ""));
|
||||
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
|
||||
col.add(builder.build());
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -115,6 +115,13 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing
|
||||
return this.cacheNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
synchronized (this.cacheMap) {
|
||||
this.cacheMap.values().forEach(Cache::clear);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Common cache initialization delegates for subclasses
|
||||
|
||||
|
||||
+7
@@ -119,4 +119,11 @@ public class CompositeCacheManager implements CacheManager, InitializingBean {
|
||||
return Collections.unmodifiableSet(names);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
for (CacheManager manager : this.cacheManagers) {
|
||||
manager.resetCaches();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -55,4 +55,9 @@ public class NoOpCacheManager implements CacheManager {
|
||||
return Collections.unmodifiableSet(this.cacheMap.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
this.cacheMap.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ public interface ApplicationContext extends EnvironmentCapable, ListableBeanFact
|
||||
|
||||
/**
|
||||
* Return the unique id of this application context.
|
||||
* @return the unique id of the context, or {@code null} if none
|
||||
* @return the unique id of the context (never null as of 7.0.2)
|
||||
*/
|
||||
@Nullable String getId();
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Return a name for the deployed application that this context belongs to.
|
||||
|
||||
+58
-9
@@ -42,16 +42,20 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
|
||||
import org.springframework.aot.generate.AccessControl;
|
||||
import org.springframework.aot.generate.GeneratedClass;
|
||||
import org.springframework.aot.generate.GeneratedMethod;
|
||||
import org.springframework.aot.generate.GeneratedMethods;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.generate.MethodReference;
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.ResourceHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
@@ -872,11 +876,14 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
@Override
|
||||
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
|
||||
GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add(
|
||||
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder, generationContext));
|
||||
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder,
|
||||
generationContext, beanFactoryInitializationCode.getClassName()));
|
||||
beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference());
|
||||
}
|
||||
|
||||
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext) {
|
||||
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext,
|
||||
ClassName className) {
|
||||
|
||||
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
|
||||
method.addJavadoc("Apply bean registrars.");
|
||||
method.addModifiers(Modifier.PRIVATE);
|
||||
@@ -915,7 +922,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
}
|
||||
}
|
||||
}
|
||||
method.addCode(generateRegisterCode());
|
||||
method.addCode(generateRegisterCode(className, generationContext));
|
||||
}
|
||||
|
||||
private void checkUnsupportedFeatures(AbstractBeanDefinition beanDefinition) {
|
||||
@@ -937,37 +944,79 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
return code.build();
|
||||
}
|
||||
|
||||
private CodeBlock generateRegisterCode() {
|
||||
private CodeBlock generateRegisterCode(ClassName className, GenerationContext generationContext) {
|
||||
Builder code = CodeBlock.builder();
|
||||
Builder metadataReaderFactoryCode = null;
|
||||
NameAllocator nameAllocator = new NameAllocator();
|
||||
for (Map.Entry<String, List<BeanRegistrar>> beanRegistrarEntry : this.beanRegistrars.entrySet()) {
|
||||
for (BeanRegistrar beanRegistrar : beanRegistrarEntry.getValue()) {
|
||||
String beanRegistrarName = nameAllocator.newName(StringUtils.uncapitalize(beanRegistrar.getClass().getSimpleName()));
|
||||
code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass());
|
||||
Constructor<?> constructor = BeanUtils.getResolvableConstructor(beanRegistrar.getClass());
|
||||
boolean visible = isVisible(constructor, className);
|
||||
if (visible) {
|
||||
code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass());
|
||||
}
|
||||
else {
|
||||
try {
|
||||
Class<?> configClass = ClassUtils.forName(beanRegistrarEntry.getKey(), beanRegistrar.getClass().getClassLoader());
|
||||
GeneratedClass generatedClass = generationContext.getGeneratedClasses()
|
||||
.getOrAddForFeatureComponent("BeanRegistrars", configClass, type ->
|
||||
type.addJavadoc("Bean registrars for {@link $T}.", configClass)
|
||||
.addModifiers(Modifier.PUBLIC));
|
||||
GeneratedMethod generatedMethod = generatedClass.getMethods().add(
|
||||
"get" + beanRegistrar.getClass().getSimpleName(),
|
||||
method -> method
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.returns(BeanRegistrar.class)
|
||||
.addStatement("return new $T()", beanRegistrar.getClass()));
|
||||
code.addStatement("$T $L = $L", BeanRegistrar.class, beanRegistrarName,
|
||||
generatedMethod.toMethodReference().toInvokeCodeBlock(ArgumentCodeGenerator.none()));
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
if (beanRegistrar instanceof ImportAware) {
|
||||
if (metadataReaderFactoryCode == null) {
|
||||
metadataReaderFactoryCode = CodeBlock.builder();
|
||||
metadataReaderFactoryCode.addStatement("$T metadataReaderFactory = new $T()",
|
||||
MetadataReaderFactory.class, CachingMetadataReaderFactory.class);
|
||||
}
|
||||
CodeBlock setImportMetadataCode;
|
||||
if (visible) {
|
||||
setImportMetadataCode = CodeBlock.builder()
|
||||
.addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
|
||||
beanRegistrarName, beanRegistrarEntry.getKey()).build();
|
||||
}
|
||||
else {
|
||||
setImportMetadataCode = CodeBlock.builder()
|
||||
.addStatement("(($T)$L).setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
|
||||
ImportAware.class, beanRegistrarName, beanRegistrarEntry.getKey()).build();
|
||||
}
|
||||
code.beginControlFlow("try")
|
||||
.addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
|
||||
beanRegistrarName, beanRegistrarEntry.getKey())
|
||||
.add(setImportMetadataCode)
|
||||
.nextControlFlow("catch ($T ex)", IOException.class)
|
||||
.addStatement("throw new $T(\"Failed to read metadata for '$L'\", ex)",
|
||||
IllegalStateException.class, beanRegistrarEntry.getKey())
|
||||
.endControlFlow();
|
||||
}
|
||||
code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName,
|
||||
code.addStatement("$L.register(new $T(($T)$L, $L, $L, $L.getClass(), $L), $L)", beanRegistrarName,
|
||||
BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE,
|
||||
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(),
|
||||
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrarName,
|
||||
CUSTOMIZER_MAP_VARIABLE, ENVIRONMENT_VARIABLE);
|
||||
}
|
||||
}
|
||||
return (metadataReaderFactoryCode == null ? code.build() : metadataReaderFactoryCode.add(code.build()).build());
|
||||
}
|
||||
|
||||
private boolean isVisible(Constructor<?> ctor, ClassName className) {
|
||||
AccessControl classAccessControl = AccessControl.forClass(ctor.getDeclaringClass());
|
||||
AccessControl memberAccessControl = AccessControl.forMember(ctor);
|
||||
AccessControl.Visibility visibility = AccessControl.lowest(classAccessControl, memberAccessControl).getVisibility();
|
||||
return (visibility == AccessControl.Visibility.PUBLIC || (visibility != AccessControl.Visibility.PRIVATE &&
|
||||
ctor.getDeclaringClass().getPackageName().equals(className.packageName())));
|
||||
}
|
||||
|
||||
private CodeBlock generateInitDestroyMethods(String beanName, AbstractBeanDefinition beanDefinition,
|
||||
String[] methodNames, String method, ReflectionHints reflectionHints) {
|
||||
|
||||
|
||||
+16
-13
@@ -168,22 +168,25 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
boolean cacheable = false;
|
||||
if (!autowiredBeanNames.isEmpty()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
if (cacheable) {
|
||||
this.cachedTarget = target;
|
||||
}
|
||||
|
||||
return target;
|
||||
|
||||
@@ -54,5 +54,4 @@ public @interface Proxyable {
|
||||
*/
|
||||
Class<?>[] interfaces() default {};
|
||||
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ public class ApplicationContextAotGenerator {
|
||||
new ApplicationContextInitializationCodeGenerator(applicationContext, generationContext);
|
||||
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
|
||||
new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator);
|
||||
return codeGenerator.getGeneratedClass().getName();
|
||||
return codeGenerator.getClassName();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -127,8 +127,9 @@ class ApplicationContextInitializationCodeGenerator implements BeanFactoryInitia
|
||||
return ArgumentCodeGenerator.from(new InitializerMethodArgumentCodeGenerator());
|
||||
}
|
||||
|
||||
GeneratedClass getGeneratedClass() {
|
||||
return this.generatedClass;
|
||||
@Override
|
||||
public ClassName getClassName() {
|
||||
return this.generatedClass.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+11
-1
@@ -623,12 +623,22 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
finishRefresh();
|
||||
}
|
||||
|
||||
catch (RuntimeException | Error ex ) {
|
||||
catch (RuntimeException | Error ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Exception encountered during context initialization - " +
|
||||
"cancelling refresh attempt: " + ex);
|
||||
}
|
||||
|
||||
// Stop already started Lifecycle beans to avoid dangling resources.
|
||||
if (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()) {
|
||||
try {
|
||||
this.lifecycleProcessor.stop();
|
||||
}
|
||||
catch (Throwable ex2) {
|
||||
logger.warn("Exception thrown from LifecycleProcessor on cancelled refresh", ex2);
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy already created singletons to avoid dangling resources.
|
||||
destroyBeans();
|
||||
|
||||
|
||||
+1
-2
@@ -81,8 +81,7 @@ final class DateTimeConverters {
|
||||
return gc.toZonedDateTime();
|
||||
}
|
||||
else {
|
||||
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(source.getTimeInMillis()),
|
||||
source.getTimeZone().toZoneId());
|
||||
return Instant.ofEpochMilli(source.getTimeInMillis()).atZone(source.getTimeZone().toZoneId());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -48,12 +48,14 @@ public @interface EnableResilientMethods {
|
||||
* Indicate whether subclass-based (CGLIB) proxies are to be created as opposed
|
||||
* to standard Java interface-based proxies.
|
||||
* <p>The default is {@code false}.
|
||||
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
|
||||
* Spring-managed beans requiring proxying, not just those marked with {@code @Retryable}
|
||||
* or {@code @ConcurrencyLimit}. For example, other beans marked with Spring's
|
||||
* {@code @Transactional} annotation will be upgraded to subclass proxying at
|
||||
* the same time. This approach has no negative impact in practice unless one is
|
||||
* explicitly expecting one type of proxy vs. another — for example, in tests.
|
||||
* <p>Note that setting this attribute to {@code true} will only affect
|
||||
* {@link RetryAnnotationBeanPostProcessor} and
|
||||
* {@link ConcurrencyLimitBeanPostProcessor}.
|
||||
* <p>It is usually recommendable to rely on a global default proxy configuration
|
||||
* instead, with specific proxy requirements for certain beans expressed through
|
||||
* a {@link org.springframework.context.annotation.Proxyable} annotation on
|
||||
* the affected bean classes.
|
||||
* @see org.springframework.aop.config.AopConfigUtils#forceAutoProxyCreatorToUseClassProxying
|
||||
*/
|
||||
boolean proxyTargetClass() default false;
|
||||
|
||||
|
||||
+3
-1
@@ -52,7 +52,9 @@ public class ResilientMethodsConfiguration implements ImportAware {
|
||||
|
||||
private void configureProxySupport(ProxyProcessorSupport proxySupport) {
|
||||
if (this.enableResilientMethods != null) {
|
||||
proxySupport.setProxyTargetClass(this.enableResilientMethods.getBoolean("proxyTargetClass"));
|
||||
if (this.enableResilientMethods.getBoolean("proxyTargetClass")) {
|
||||
proxySupport.setProxyTargetClass(true);
|
||||
}
|
||||
proxySupport.setOrder(this.enableResilientMethods.getNumber("order"));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -99,6 +99,7 @@ public class RetryAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd
|
||||
Arrays.asList(retryable.includes()), Arrays.asList(retryable.excludes()),
|
||||
instantiatePredicate(retryable.predicate()),
|
||||
parseLong(retryable.maxRetries(), retryable.maxRetriesString()),
|
||||
parseDuration(retryable.timeout(), retryable.timeoutString(), timeUnit),
|
||||
parseDuration(retryable.delay(), retryable.delayString(), timeUnit),
|
||||
parseDuration(retryable.jitter(), retryable.jitterString(), timeUnit),
|
||||
parseDouble(retryable.multiplier(), retryable.multiplierString()),
|
||||
|
||||
@@ -122,6 +122,39 @@ public @interface Retryable {
|
||||
*/
|
||||
String maxRetriesString() default "";
|
||||
|
||||
/**
|
||||
* The maximum amount of elapsed time allowed for the initial invocation and
|
||||
* any subsequent retry attempts, including delays.
|
||||
* <p>The default is {@code 0}, which signals that no timeout should be applied.
|
||||
* <p>The time unit is milliseconds by default but can be overridden via
|
||||
* {@link #timeUnit}.
|
||||
* <p>Must be greater than or equal to zero.
|
||||
* @since 7.0.2
|
||||
*/
|
||||
long timeout() default 0;
|
||||
|
||||
/**
|
||||
* The timeout, as a duration String.
|
||||
* <p>A non-empty value specified here overrides the {@link #timeout()} attribute.
|
||||
* <p>The duration String can be in several formats:
|
||||
* <ul>
|
||||
* <li>a plain integer — which is interpreted to represent a duration in
|
||||
* milliseconds by default unless overridden via {@link #timeUnit()} (prefer
|
||||
* using {@link #delay()} in that case)</li>
|
||||
* <li>any of the known {@link org.springframework.format.annotation.DurationFormat.Style
|
||||
* DurationFormat.Style}: the {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 ISO8601}
|
||||
* style or the {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE SIMPLE} style
|
||||
* — using the {@link #timeUnit()} as fallback if the string doesn't contain an explicit unit</li>
|
||||
* <li>one of the above, with Spring-style "${...}" placeholders as well as SpEL expressions</li>
|
||||
* </ul>
|
||||
* @return the timeout as a String value — for example, a placeholder, a
|
||||
* {@link org.springframework.format.annotation.DurationFormat.Style#ISO8601 java.time.Duration} compliant value,
|
||||
* or a {@link org.springframework.format.annotation.DurationFormat.Style#SIMPLE simple format} compliant value
|
||||
* @since 7.0.2
|
||||
* @see #timeout()
|
||||
*/
|
||||
String timeoutString() default "";
|
||||
|
||||
/**
|
||||
* The base delay after the initial invocation. If a multiplier is specified,
|
||||
* this serves as the initial delay to multiply from.
|
||||
|
||||
+25
-3
@@ -17,10 +17,13 @@
|
||||
package org.springframework.resilience.retry;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -49,6 +52,8 @@ import org.springframework.util.ClassUtils;
|
||||
*/
|
||||
public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AbstractRetryInterceptor.class);
|
||||
|
||||
/**
|
||||
* Reactive Streams API present on the classpath?
|
||||
*/
|
||||
@@ -94,12 +99,14 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
.excludes(spec.excludes())
|
||||
.predicate(spec.predicate().forMethod(method))
|
||||
.maxRetries(spec.maxRetries())
|
||||
.timeout(spec.timeout())
|
||||
.delay(spec.delay())
|
||||
.jitter(spec.jitter())
|
||||
.multiplier(spec.multiplier())
|
||||
.maxDelay(spec.maxDelay())
|
||||
.build();
|
||||
RetryTemplate retryTemplate = new RetryTemplate(retryPolicy);
|
||||
String methodName = ClassUtils.getQualifiedMethodName(method, (target != null ? target.getClass() : null));
|
||||
|
||||
try {
|
||||
return retryTemplate.execute(new Retryable<@Nullable Object>() {
|
||||
@@ -110,11 +117,14 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
}
|
||||
@Override
|
||||
public String getName() {
|
||||
return ClassUtils.getQualifiedMethodName(method, (target != null ? target.getClass() : null));
|
||||
return methodName;
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (RetryException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("@Retryable operation '%s' failed".formatted(methodName), ex);
|
||||
}
|
||||
throw ex.getCause();
|
||||
}
|
||||
}
|
||||
@@ -142,8 +152,20 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
|
||||
.multiplier(spec.multiplier())
|
||||
.maxBackoff(spec.maxDelay())
|
||||
.filter(spec.combinedPredicate().forMethod(method));
|
||||
publisher = (adapter.isMultiValue() ? Flux.from(publisher).retryWhen(retry) :
|
||||
Mono.from(publisher).retryWhen(retry));
|
||||
|
||||
Duration timeout = spec.timeout();
|
||||
boolean timeoutIsPositive = (!timeout.isNegative() && !timeout.isZero());
|
||||
if (adapter.isMultiValue()) {
|
||||
publisher = (timeoutIsPositive ?
|
||||
Flux.from(publisher).retryWhen(retry).timeout(timeout) :
|
||||
Flux.from(publisher).retryWhen(retry));
|
||||
}
|
||||
else {
|
||||
publisher = (timeoutIsPositive ?
|
||||
Mono.from(publisher).retryWhen(retry).timeout(timeout) :
|
||||
Mono.from(publisher).retryWhen(retry));
|
||||
}
|
||||
|
||||
return adapter.fromPublisher(publisher);
|
||||
}
|
||||
|
||||
|
||||
+25
-2
@@ -28,11 +28,14 @@ import org.springframework.util.ExceptionTypeFilter;
|
||||
* on {@link org.springframework.resilience.annotation.Retryable}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 7.0
|
||||
* @param includes applicable exception types to attempt a retry for
|
||||
* @param excludes non-applicable exception types to avoid a retry for
|
||||
* @param predicate a predicate for filtering exceptions from applicable methods
|
||||
* @param maxRetries the maximum number of retry attempts
|
||||
* @param timeout the maximum amount of elapsed time allowed for the initial
|
||||
* invocation and any subsequent retry attempts, including delays
|
||||
* @param delay the base delay after the initial invocation
|
||||
* @param jitter a jitter value for the next retry attempt
|
||||
* @param multiplier a multiplier for a delay for the next retry attempt
|
||||
@@ -46,20 +49,40 @@ public record MethodRetrySpec(
|
||||
Collection<Class<? extends Throwable>> excludes,
|
||||
MethodRetryPredicate predicate,
|
||||
long maxRetries,
|
||||
Duration timeout,
|
||||
Duration delay,
|
||||
Duration jitter,
|
||||
double multiplier,
|
||||
Duration maxDelay) {
|
||||
|
||||
/**
|
||||
* Construct a new {@code MethodRetryPredicate} with the supplied arguments.
|
||||
*/
|
||||
public MethodRetrySpec(MethodRetryPredicate predicate, long maxRetries, Duration delay) {
|
||||
this(predicate, maxRetries, delay, Duration.ZERO, 1.0, Duration.ofMillis(Long.MAX_VALUE));
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code MethodRetryPredicate} with the supplied arguments.
|
||||
*/
|
||||
public MethodRetrySpec(MethodRetryPredicate predicate, long maxRetries, Duration delay,
|
||||
Duration jitter, double multiplier, Duration maxDelay) {
|
||||
|
||||
this(Collections.emptyList(), Collections.emptyList(), predicate, maxRetries, delay,
|
||||
jitter, multiplier, maxDelay);
|
||||
this(Collections.emptyList(), Collections.emptyList(), predicate, maxRetries, Duration.ZERO,
|
||||
delay, jitter, multiplier, maxDelay);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code MethodRetryPredicate} with the supplied arguments.
|
||||
* @deprecated as of Spring Framework 7.0.2, in favor of
|
||||
* {@link #MethodRetrySpec(Collection, Collection, MethodRetryPredicate, long, Duration, Duration, Duration, double, Duration)}
|
||||
*/
|
||||
@Deprecated(since = "7.0.2", forRemoval = true)
|
||||
public MethodRetrySpec(Collection<Class<? extends Throwable>> includes,
|
||||
Collection<Class<? extends Throwable>> excludes, MethodRetryPredicate predicate,
|
||||
long maxRetries, Duration delay, Duration jitter, double multiplier, Duration maxDelay) {
|
||||
|
||||
this(includes, excludes, predicate, maxRetries, Duration.ZERO, delay, jitter, multiplier, maxDelay);
|
||||
}
|
||||
|
||||
|
||||
|
||||
+7
-6
@@ -183,12 +183,13 @@ public @interface EnableAsync {
|
||||
* to standard Java interface-based proxies.
|
||||
* <p><strong>Applicable only if the {@link #mode} is set to {@link AdviceMode#PROXY}</strong>.
|
||||
* <p>The default is {@code false}.
|
||||
* <p>Note that setting this attribute to {@code true} will affect <em>all</em>
|
||||
* Spring-managed beans requiring proxying, not just those marked with {@code @Async}.
|
||||
* For example, other beans marked with Spring's {@code @Transactional} annotation
|
||||
* will be upgraded to subclass proxying at the same time. This approach has no
|
||||
* negative impact in practice unless one is explicitly expecting one type of proxy
|
||||
* vs. another — for example, in tests.
|
||||
* <p>Note that setting this attribute to {@code true} will only affect
|
||||
* {@link AsyncAnnotationBeanPostProcessor}.
|
||||
* <p>It is usually recommendable to rely on a global default proxy configuration
|
||||
* instead, with specific proxy requirements for certain beans expressed through
|
||||
* a {@link org.springframework.context.annotation.Proxyable} annotation on
|
||||
* the affected bean classes.
|
||||
* @see org.springframework.aop.config.AopConfigUtils#forceAutoProxyCreatorToUseClassProxying
|
||||
*/
|
||||
boolean proxyTargetClass() default false;
|
||||
|
||||
|
||||
+3
-1
@@ -51,7 +51,9 @@ public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
|
||||
if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
|
||||
bpp.setAsyncAnnotationType(customAsyncAnnotation);
|
||||
}
|
||||
bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
|
||||
if (this.enableAsync.getBoolean("proxyTargetClass")) {
|
||||
bpp.setProxyTargetClass(true);
|
||||
}
|
||||
bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
|
||||
return bpp;
|
||||
}
|
||||
|
||||
+1
-1
@@ -377,7 +377,7 @@ public class ScheduledAnnotationBeanPostProcessor
|
||||
try {
|
||||
task = ScheduledAnnotationReactiveSupport.createSubscriptionRunnable(method, bean, scheduled,
|
||||
this.registrar::getObservationRegistry,
|
||||
this.reactiveSubscriptions.computeIfAbsent(bean, k -> new CopyOnWriteArrayList<>()));
|
||||
this.reactiveSubscriptions.computeIfAbsent(bean, key -> new CopyOnWriteArrayList<>()));
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
throw new IllegalStateException("Could not create recurring task for @Scheduled method '" +
|
||||
|
||||
+1
-1
@@ -115,7 +115,7 @@ public class CronTrigger implements Trigger {
|
||||
public @Nullable Instant nextExecution(TriggerContext triggerContext) {
|
||||
Instant timestamp = determineLatestTimestamp(triggerContext);
|
||||
ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone());
|
||||
ZonedDateTime zonedTimestamp = ZonedDateTime.ofInstant(timestamp, zone);
|
||||
ZonedDateTime zonedTimestamp = timestamp.atZone(zone);
|
||||
ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp);
|
||||
return (nextTimestamp != null ? nextTimestamp.toInstant() : null);
|
||||
}
|
||||
|
||||
+71
-5
@@ -62,6 +62,7 @@ import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.io.support.DefaultPropertySourceFactory;
|
||||
import org.springframework.core.test.tools.CompileWithForkedClassLoader;
|
||||
import org.springframework.core.test.tools.Compiled;
|
||||
import org.springframework.core.test.tools.TestCompiler;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
@@ -503,7 +504,7 @@ public class ConfigurationClassPostProcessorAotContributionTests {
|
||||
@Test
|
||||
void applyToWhenIsImportAware() {
|
||||
BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class,
|
||||
ImportAwareBeanRegistrarConfiguration.class);
|
||||
ImportAwareConfiguration.class);
|
||||
assertThat(contribution).isNotNull();
|
||||
contribution.applyTo(generationContext, beanFactoryInitializationCode);
|
||||
compile((initializer, compiled) -> {
|
||||
@@ -511,7 +512,42 @@ public class ConfigurationClassPostProcessorAotContributionTests {
|
||||
initializer.accept(freshContext);
|
||||
freshContext.refresh();
|
||||
assertThat(freshContext.getBean(ClassNameHolder.class).className())
|
||||
.isEqualTo(ImportAwareBeanRegistrarConfiguration.class.getName());
|
||||
.isEqualTo(ImportAwareConfiguration.class.getName());
|
||||
freshContext.close();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@CompileWithForkedClassLoader
|
||||
void applyToWhenIsPackagePrivate() throws NoSuchMethodException {
|
||||
BeanFactoryInitializationAotContribution contribution = getContribution(PackagePrivateConfiguration.class);
|
||||
assertThat(contribution).isNotNull();
|
||||
contribution.applyTo(generationContext, beanFactoryInitializationCode);
|
||||
Constructor<Foo> fooConstructor = Foo.class.getDeclaredConstructor();
|
||||
compile((initializer, compiled) -> {
|
||||
GenericApplicationContext freshContext = new GenericApplicationContext();
|
||||
initializer.accept(freshContext);
|
||||
freshContext.refresh();
|
||||
assertThat(freshContext.getBean(Foo.class)).isNotNull();
|
||||
assertThat(RuntimeHintsPredicates.reflection().onConstructorInvocation(fooConstructor))
|
||||
.accepts(generationContext.getRuntimeHints());
|
||||
freshContext.close();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@CompileWithForkedClassLoader
|
||||
void applyToWhenIsPackagePrivateAndImportAware() {
|
||||
BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class,
|
||||
PackagePrivateAndImportAwareConfiguration.class);
|
||||
assertThat(contribution).isNotNull();
|
||||
contribution.applyTo(generationContext, beanFactoryInitializationCode);
|
||||
compile((initializer, compiled) -> {
|
||||
GenericApplicationContext freshContext = new GenericApplicationContext();
|
||||
initializer.accept(freshContext);
|
||||
freshContext.refresh();
|
||||
assertThat(freshContext.getBean(ClassNameHolder.class).className())
|
||||
.isEqualTo(PackagePrivateAndImportAwareConfiguration.class.getName());
|
||||
freshContext.close();
|
||||
});
|
||||
}
|
||||
@@ -578,7 +614,7 @@ public class ConfigurationClassPostProcessorAotContributionTests {
|
||||
}
|
||||
|
||||
@Import(ImportAwareBeanRegistrar.class)
|
||||
public static class ImportAwareBeanRegistrarConfiguration {
|
||||
public static class ImportAwareConfiguration {
|
||||
}
|
||||
|
||||
public static class ImportAwareBeanRegistrar implements BeanRegistrar, ImportAware {
|
||||
@@ -596,9 +632,39 @@ public class ConfigurationClassPostProcessorAotContributionTests {
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
this.importMetadata = importMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable AnnotationMetadata getImportMetadata() {
|
||||
return this.importMetadata;
|
||||
@Configuration
|
||||
@Import(PackagePrivateBeanRegistrar.class)
|
||||
static class PackagePrivateConfiguration {
|
||||
}
|
||||
|
||||
static class PackagePrivateBeanRegistrar implements BeanRegistrar {
|
||||
|
||||
@Override
|
||||
public void register(BeanRegistry registry, Environment env) {
|
||||
registry.registerBean(Foo.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Import(PackagePrivateAndImportAwareBeanRegistrar.class)
|
||||
static class PackagePrivateAndImportAwareConfiguration {
|
||||
}
|
||||
|
||||
static class PackagePrivateAndImportAwareBeanRegistrar implements BeanRegistrar, ImportAware {
|
||||
|
||||
@Nullable
|
||||
private AnnotationMetadata importMetadata;
|
||||
|
||||
@Override
|
||||
public void register(BeanRegistry registry, Environment env) {
|
||||
registry.registerBean(ClassNameHolder.class, spec -> spec.supplier(context ->
|
||||
new ClassNameHolder(this.importMetadata == null ? null : this.importMetadata.getClassName())));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setImportMetadata(AnnotationMetadata importMetadata) {
|
||||
this.importMetadata = importMetadata;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
@@ -19,6 +19,7 @@ package org.springframework.context.annotation.configuration;
|
||||
import java.io.IOException;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -29,6 +30,7 @@ import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.beans.factory.parsing.BeanDefinitionParsingException;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
@@ -100,6 +102,16 @@ class AutowiredConfigurationTests {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAutowiredConfigurationMethodDependenciesWithQualifier() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
QualifiedAutowiredMethodConfig.class);
|
||||
|
||||
assertThat(context.getBeansOfType(Colour.class)).isEmpty();
|
||||
assertThat(context.getBean(TestBean.class).getName()).isEmpty();
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAutowiredSingleConstructorSupported() {
|
||||
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
|
||||
@@ -297,6 +309,25 @@ class AutowiredConfigurationTests {
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class QualifiedAutowiredMethodConfig {
|
||||
|
||||
@Bean
|
||||
@Qualifier("testBean")
|
||||
public TestBean testBean(Optional<Colour> colour, Optional<List<Colour>> colours) {
|
||||
if (!colour.isEmpty() || !colours.isEmpty()) {
|
||||
throw new IllegalStateException("Unexpected match: " + colour + " " + colours);
|
||||
}
|
||||
return new TestBean("");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public List<?> someList() {
|
||||
return Collections.singletonList(new TestBean("shouldNotMatch"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Configuration
|
||||
static class AutowiredConstructorConfig {
|
||||
|
||||
|
||||
+69
@@ -18,17 +18,25 @@ package org.springframework.context.support;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
|
||||
/**
|
||||
* @author Mark Fisher
|
||||
* @author Chris Beams
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class ApplicationContextLifecycleTests {
|
||||
|
||||
@Test
|
||||
void beansStart() {
|
||||
AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass());
|
||||
|
||||
context.start();
|
||||
LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1");
|
||||
LifecycleTestBean bean2 = (LifecycleTestBean) context.getBean("bean2");
|
||||
@@ -39,12 +47,14 @@ class ApplicationContextLifecycleTests {
|
||||
assertThat(bean2.isRunning()).as(error).isTrue();
|
||||
assertThat(bean3.isRunning()).as(error).isTrue();
|
||||
assertThat(bean4.isRunning()).as(error).isTrue();
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void beansStop() {
|
||||
AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass());
|
||||
|
||||
context.start();
|
||||
LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1");
|
||||
LifecycleTestBean bean2 = (LifecycleTestBean) context.getBean("bean2");
|
||||
@@ -55,18 +65,21 @@ class ApplicationContextLifecycleTests {
|
||||
assertThat(bean2.isRunning()).as(startError).isTrue();
|
||||
assertThat(bean3.isRunning()).as(startError).isTrue();
|
||||
assertThat(bean4.isRunning()).as(startError).isTrue();
|
||||
|
||||
context.stop();
|
||||
String stopError = "bean was not stopped";
|
||||
assertThat(bean1.isRunning()).as(stopError).isFalse();
|
||||
assertThat(bean2.isRunning()).as(stopError).isFalse();
|
||||
assertThat(bean3.isRunning()).as(stopError).isFalse();
|
||||
assertThat(bean4.isRunning()).as(stopError).isFalse();
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void startOrder() {
|
||||
AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass());
|
||||
|
||||
context.start();
|
||||
LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1");
|
||||
LifecycleTestBean bean2 = (LifecycleTestBean) context.getBean("bean2");
|
||||
@@ -81,12 +94,14 @@ class ApplicationContextLifecycleTests {
|
||||
assertThat(bean2.getStartOrder()).as(orderError).isGreaterThan(bean1.getStartOrder());
|
||||
assertThat(bean3.getStartOrder()).as(orderError).isGreaterThan(bean2.getStartOrder());
|
||||
assertThat(bean4.getStartOrder()).as(orderError).isGreaterThan(bean2.getStartOrder());
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stopOrder() {
|
||||
AbstractApplicationContext context = new ClassPathXmlApplicationContext("lifecycleTests.xml", getClass());
|
||||
|
||||
context.start();
|
||||
context.stop();
|
||||
LifecycleTestBean bean1 = (LifecycleTestBean) context.getBean("bean1");
|
||||
@@ -102,7 +117,61 @@ class ApplicationContextLifecycleTests {
|
||||
assertThat(bean2.getStopOrder()).as(orderError).isLessThan(bean1.getStopOrder());
|
||||
assertThat(bean3.getStopOrder()).as(orderError).isLessThan(bean2.getStopOrder());
|
||||
assertThat(bean4.getStopOrder()).as(orderError).isLessThan(bean2.getStopOrder());
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void autoStartup() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
new XmlBeanDefinitionReader(context).loadBeanDefinitions(new ClassPathResource("smartLifecycleTests.xml", getClass()));
|
||||
|
||||
context.refresh();
|
||||
LifecycleTestBean bean1 = (LifecycleTestBean) context.getBeanFactory().getBean("bean1");
|
||||
LifecycleTestBean bean2 = (LifecycleTestBean) context.getBeanFactory().getBean("bean2");
|
||||
LifecycleTestBean bean3 = (LifecycleTestBean) context.getBeanFactory().getBean("bean3");
|
||||
LifecycleTestBean bean4 = (LifecycleTestBean) context.getBeanFactory().getBean("bean4");
|
||||
|
||||
context.close();
|
||||
String notStoppedError = "bean was not stopped";
|
||||
assertThat(bean1.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
assertThat(bean2.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
assertThat(bean3.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
assertThat(bean4.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
String orderError = "dependent bean must stop before the bean it depends on";
|
||||
assertThat(bean2.getStopOrder()).as(orderError).isLessThan(bean1.getStopOrder());
|
||||
assertThat(bean3.getStopOrder()).as(orderError).isLessThan(bean2.getStopOrder());
|
||||
assertThat(bean4.getStopOrder()).as(orderError).isLessThan(bean2.getStopOrder());
|
||||
}
|
||||
|
||||
@Test
|
||||
void cancelledRefresh() {
|
||||
GenericApplicationContext context = new GenericApplicationContext();
|
||||
new XmlBeanDefinitionReader(context).loadBeanDefinitions(new ClassPathResource("smartLifecycleTests.xml", getClass()));
|
||||
context.registerBean(FailingContextRefreshedListener.class);
|
||||
LifecycleTestBean bean1 = (LifecycleTestBean) context.getBeanFactory().getBean("bean1");
|
||||
LifecycleTestBean bean2 = (LifecycleTestBean) context.getBeanFactory().getBean("bean2");
|
||||
LifecycleTestBean bean3 = (LifecycleTestBean) context.getBeanFactory().getBean("bean3");
|
||||
LifecycleTestBean bean4 = (LifecycleTestBean) context.getBeanFactory().getBean("bean4");
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(context::refresh);
|
||||
String notStoppedError = "bean was not stopped";
|
||||
assertThat(bean1.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
assertThat(bean2.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
assertThat(bean3.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
assertThat(bean4.getStopOrder()).as(notStoppedError).isGreaterThan(0);
|
||||
String orderError = "dependent bean must stop before the bean it depends on";
|
||||
assertThat(bean2.getStopOrder()).as(orderError).isLessThan(bean1.getStopOrder());
|
||||
assertThat(bean3.getStopOrder()).as(orderError).isLessThan(bean2.getStopOrder());
|
||||
assertThat(bean4.getStopOrder()).as(orderError).isLessThan(bean2.getStopOrder());
|
||||
}
|
||||
|
||||
|
||||
private static class FailingContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.support;
|
||||
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
|
||||
/**
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
public class SmartLifecycleTestBean extends LifecycleTestBean implements SmartLifecycle {
|
||||
|
||||
}
|
||||
+162
-23
@@ -21,9 +21,11 @@ import java.nio.charset.MalformedInputException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.FileSystemException;
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.assertj.core.api.ThrowingConsumer;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.Exceptions;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -65,17 +67,12 @@ class ReactiveRetryInterceptorTests {
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("6");
|
||||
assertThat(target.counter.get()).isEqualTo(6);
|
||||
assertThat(target.counter).hasValue(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethod() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class);
|
||||
AnnotatedMethodBean proxy = getProxiedAnnotatedMethodBean();
|
||||
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThatIllegalStateException()
|
||||
@@ -84,7 +81,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("6");
|
||||
assertThat(target.counter.get()).isEqualTo(6);
|
||||
assertThat(target.counter).hasValue(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -105,7 +102,7 @@ class ReactiveRetryInterceptorTests {
|
||||
// 3 = 1 initial invocation + 2 retry attempts
|
||||
// Not 3 retry attempts, because RejectMalformedInputException3Predicate rejects
|
||||
// a retry if the last exception was a MalformedInputException with message "3".
|
||||
assertThat(target.counter.get()).isEqualTo(3);
|
||||
assertThat(target.counter).hasValue(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,7 +116,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.satisfies(isRetryExhaustedException())
|
||||
.withCauseInstanceOf(FileSystemException.class);
|
||||
// 1 initial attempt + 3 retries
|
||||
assertThat(target.counter.get()).isEqualTo(4);
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
@Test // gh-35583
|
||||
@@ -135,7 +132,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.isExactlyInstanceOf(RuntimeException.class)
|
||||
.withCauseExactlyInstanceOf(FileSystemException.class);
|
||||
// 1 initial attempt + 3 retries
|
||||
assertThat(target.counter.get()).isEqualTo(4);
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -151,7 +148,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.satisfies(isReactiveException())
|
||||
.withCauseInstanceOf(AccessDeniedException.class);
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter.get()).isEqualTo(1);
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -170,7 +167,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.isThrownBy(() -> proxy.arithmeticOperation().block())
|
||||
.withMessage("1");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter.get()).isEqualTo(1);
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -184,7 +181,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.satisfies(isRetryExhaustedException())
|
||||
.withCauseInstanceOf(IOException.class);
|
||||
// 1 initial attempt + 1 retry
|
||||
assertThat(target.counter.get()).isEqualTo(2);
|
||||
assertThat(target.counter).hasValue(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -204,7 +201,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("2");
|
||||
assertThat(target.counter.get()).isEqualTo(2);
|
||||
assertThat(target.counter).hasValue(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -224,7 +221,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("1");
|
||||
assertThat(target.counter.get()).isEqualTo(1);
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -243,7 +240,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("4");
|
||||
assertThat(target.counter.get()).isEqualTo(4);
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -262,7 +259,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("4");
|
||||
assertThat(target.counter.get()).isEqualTo(4);
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -281,7 +278,7 @@ class ReactiveRetryInterceptorTests {
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("4");
|
||||
assertThat(target.counter.get()).isEqualTo(4);
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -297,7 +294,7 @@ class ReactiveRetryInterceptorTests {
|
||||
String result = proxy.retryOperation().block();
|
||||
assertThat(result).isEqualTo("success");
|
||||
// Should execute only once because of successful return
|
||||
assertThat(target.counter.get()).isEqualTo(1);
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -317,7 +314,84 @@ class ReactiveRetryInterceptorTests {
|
||||
.isInstanceOf(NumberFormatException.class)
|
||||
.withMessage("always fails");
|
||||
// 1 initial attempt + 3 retries
|
||||
assertThat(target.counter.get()).isEqualTo(4);
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class TimeoutTests {
|
||||
|
||||
private final AnnotatedMethodBean proxy = getProxiedAnnotatedMethodBean();
|
||||
private final AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAfterInitialSuccess() {
|
||||
String result = proxy.retryOperationWithTimeoutNotExceededAfterInitialSuccess().block();
|
||||
assertThat(result).isEqualTo("success");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAndRetriesExhausted() {
|
||||
assertThatIllegalStateException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutNotExceededAndRetriesExhausted().block())
|
||||
.satisfies(isRetryExhaustedException())
|
||||
.havingCause()
|
||||
.isInstanceOf(IOException.class)
|
||||
.withMessage("4");
|
||||
// 1 initial attempt + 3 retries
|
||||
assertThat(target.counter).hasValue(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterInitialFailure() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterInitialFailure().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 20ms");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstDelayButBeforeFirstRetry() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 20ms");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).hasValue(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstRetry() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterFirstRetry().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 20ms");
|
||||
// 1 initial attempt + 1 retry
|
||||
assertThat(target.counter).hasValue(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterSecondRetry() {
|
||||
assertThatRuntimeException()
|
||||
.isThrownBy(() -> proxy.retryOperationWithTimeoutExceededAfterSecondRetry().block())
|
||||
.satisfies(isReactiveException())
|
||||
.havingCause()
|
||||
.isInstanceOf(TimeoutException.class)
|
||||
.withMessageContaining("within 20ms");
|
||||
// 1 initial attempt + 2 retries
|
||||
assertThat(target.counter).hasValue(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -329,13 +403,23 @@ class ReactiveRetryInterceptorTests {
|
||||
return ex -> assertThat(ex).matches(Exceptions::isRetryExhausted, "is RetryExhaustedException");
|
||||
}
|
||||
|
||||
private static AnnotatedMethodBean getProxiedAnnotatedMethodBean() {
|
||||
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBean.class);
|
||||
return bf.getBean(AnnotatedMethodBean.class);
|
||||
}
|
||||
|
||||
private static AnnotatedClassBean getProxiedAnnotatedClassBean() {
|
||||
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedClassBean.class);
|
||||
return bf.getBean(AnnotatedClassBean.class);
|
||||
}
|
||||
|
||||
private static DefaultListableBeanFactory createBeanFactoryFor(Class<?> beanClass) {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
return bf.getBean(AnnotatedClassBean.class);
|
||||
return bf;
|
||||
}
|
||||
|
||||
|
||||
@@ -363,6 +447,61 @@ class ReactiveRetryInterceptorTests {
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public Mono<String> retryOperationWithTimeoutNotExceededAfterInitialSuccess() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
return "success";
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public Mono<Object> retryOperationWithTimeoutNotExceededAndRetriesExhausted() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 0)
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterInitialFailure() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
Thread.sleep(100);
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 100) // Delay > Timeout
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 0)
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterFirstRetry() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
if (counter.get() == 2) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 0)
|
||||
public Mono<Object> retryOperationWithTimeoutExceededAfterSecondRetry() {
|
||||
return Mono.fromCallable(() -> {
|
||||
counter.incrementAndGet();
|
||||
if (counter.get() == 3) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
throw new IOException(counter.toString());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+154
-24
@@ -27,10 +27,11 @@ import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.config.AopConfigUtils;
|
||||
import org.springframework.aop.framework.AopProxyUtils;
|
||||
import org.springframework.aop.framework.ProxyConfig;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
|
||||
import org.springframework.aop.interceptor.SimpleTraceInterceptor;
|
||||
@@ -99,11 +100,7 @@ class RetryInterceptorTests {
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethod() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBean.class));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBean.class);
|
||||
AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class);
|
||||
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@@ -113,11 +110,7 @@ class RetryInterceptorTests {
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithInterface() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBeanWithInterface.class);
|
||||
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
|
||||
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@@ -128,11 +121,8 @@ class RetryInterceptorTests {
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithInterfaceAndDefaultTargetClass() {
|
||||
ProxyConfig defaultProxyConfig = new ProxyConfig();
|
||||
defaultProxyConfig.setProxyTargetClass(true);
|
||||
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
|
||||
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(bf);
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
@@ -164,11 +154,8 @@ class RetryInterceptorTests {
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithInterfaceAndExposeInterfaces() {
|
||||
ProxyConfig defaultProxyConfig = new ProxyConfig();
|
||||
defaultProxyConfig.setProxyTargetClass(true);
|
||||
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
|
||||
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(bf);
|
||||
RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class);
|
||||
bd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, AutoProxyUtils.ALL_INTERFACES_ATTRIBUTE_VALUE);
|
||||
bf.registerBeanDefinition("bean", bd);
|
||||
@@ -185,11 +172,7 @@ class RetryInterceptorTests {
|
||||
|
||||
@Test
|
||||
void withPostProcessorForClass() {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedClassBean.class);
|
||||
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class);
|
||||
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@@ -284,6 +267,37 @@ class RetryInterceptorTests {
|
||||
assertThat(target.threadChange).hasValue(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withEnableAnnotationAndDefaultTargetClass() {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(ctx);
|
||||
ctx.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
|
||||
ctx.registerBeanDefinition("config", new RootBeanDefinition(EnablingConfig.class));
|
||||
ctx.refresh();
|
||||
AnnotatedInterface proxy = ctx.getBean(AnnotatedInterface.class);
|
||||
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withEnableAnnotationAndPreserveTargetClass() {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class);
|
||||
bd.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
|
||||
ctx.registerBeanDefinition("bean", bd);
|
||||
ctx.registerBeanDefinition("config", new RootBeanDefinition(EnablingConfig.class));
|
||||
ctx.refresh();
|
||||
AnnotatedInterface proxy = ctx.getBean(AnnotatedInterface.class);
|
||||
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
assertThat(AopUtils.isCglibProxy(proxy)).isTrue();
|
||||
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
|
||||
assertThat(target.counter).isEqualTo(6);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withAsyncAnnotation() {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
|
||||
@@ -299,6 +313,79 @@ class RetryInterceptorTests {
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class TimeoutTests {
|
||||
|
||||
private final DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBean.class);
|
||||
private final AnnotatedMethodBean proxy = bf.getBean(AnnotatedMethodBean.class);
|
||||
private final AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAfterInitialSuccess() {
|
||||
String result = proxy.retryOperationWithTimeoutNotExceededAfterInitialSuccess();
|
||||
assertThat(result).isEqualTo("success");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutNotExceededAndRetriesExhausted() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutNotExceededAndRetriesExhausted)
|
||||
.withMessage("4");
|
||||
// 1 initial attempt + 3 retries
|
||||
assertThat(target.counter).isEqualTo(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterInitialFailure() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterInitialFailure)
|
||||
.withMessage("1");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstDelayButBeforeFirstRetry() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry)
|
||||
.withMessage("1");
|
||||
// 1 initial attempt + 0 retries
|
||||
assertThat(target.counter).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterFirstRetry() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterFirstRetry)
|
||||
.withMessage("2");
|
||||
// 1 initial attempt + 1 retry
|
||||
assertThat(target.counter).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void timeoutExceededAfterSecondRetry() {
|
||||
assertThatIOException()
|
||||
.isThrownBy(proxy::retryOperationWithTimeoutExceededAfterSecondRetry)
|
||||
.withMessage("3");
|
||||
// 1 initial attempt + 2 retries
|
||||
assertThat(target.counter).isEqualTo(3);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static DefaultListableBeanFactory createBeanFactoryFor(Class<?> beanClass) {
|
||||
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
|
||||
bf.registerBeanDefinition("bean", new RootBeanDefinition(beanClass));
|
||||
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
|
||||
bpp.setBeanFactory(bf);
|
||||
bf.addBeanPostProcessor(bpp);
|
||||
return bf;
|
||||
}
|
||||
|
||||
|
||||
static class NonAnnotatedBean implements PlainInterface {
|
||||
|
||||
int counter = 0;
|
||||
@@ -326,6 +413,49 @@ class RetryInterceptorTests {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public String retryOperationWithTimeoutNotExceededAfterInitialSuccess() {
|
||||
counter++;
|
||||
return "success";
|
||||
}
|
||||
|
||||
@Retryable(timeout = 555, delay = 10)
|
||||
public void retryOperationWithTimeoutNotExceededAndRetriesExhausted() throws Exception {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 0)
|
||||
public void retryOperationWithTimeoutExceededAfterInitialFailure() throws Exception {
|
||||
counter++;
|
||||
Thread.sleep(100);
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 100) // Delay > Timeout
|
||||
public void retryOperationWithTimeoutExceededAfterFirstDelayButBeforeFirstRetry() throws IOException {
|
||||
counter++;
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 0)
|
||||
public void retryOperationWithTimeoutExceededAfterFirstRetry() throws Exception {
|
||||
counter++;
|
||||
if (counter == 2) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
|
||||
@Retryable(timeout = 20, delay = 0)
|
||||
public void retryOperationWithTimeoutExceededAfterSecondRetry() throws Exception {
|
||||
counter++;
|
||||
if (counter == 3) {
|
||||
Thread.sleep(100);
|
||||
}
|
||||
throw new IOException(Integer.toString(counter));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+4
-5
@@ -18,7 +18,6 @@ package org.springframework.scheduling.concurrent;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -53,28 +52,28 @@ class DefaultManagedTaskSchedulerTests {
|
||||
void scheduleAtFixedRateWithStartTimeAndDurationAndNoScheduledExecutorProvidesDedicatedException() {
|
||||
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
|
||||
assertNoExecutorException(() -> scheduler.scheduleAtFixedRate(
|
||||
NO_OP, Instant.now(), Duration.of(1, ChronoUnit.MINUTES)));
|
||||
NO_OP, Instant.now(), Duration.ofMinutes(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleAtFixedRateWithDurationAndNoScheduledExecutorProvidesDedicatedException() {
|
||||
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
|
||||
assertNoExecutorException(() -> scheduler.scheduleAtFixedRate(
|
||||
NO_OP, Duration.of(1, ChronoUnit.MINUTES)));
|
||||
NO_OP, Duration.ofMinutes(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleWithFixedDelayWithStartTimeAndDurationAndNoScheduledExecutorProvidesDedicatedException() {
|
||||
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
|
||||
assertNoExecutorException(() -> scheduler.scheduleWithFixedDelay(
|
||||
NO_OP, Instant.now(), Duration.of(1, ChronoUnit.MINUTES)));
|
||||
NO_OP, Instant.now(), Duration.ofMinutes(1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleWithFixedDelayWithDurationAndNoScheduledExecutorProvidesDedicatedException() {
|
||||
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
|
||||
assertNoExecutorException(() -> scheduler.scheduleWithFixedDelay(
|
||||
NO_OP, Duration.of(1, ChronoUnit.MINUTES)));
|
||||
NO_OP, Duration.ofMinutes(1)));
|
||||
}
|
||||
|
||||
private void assertNoExecutorException(ThrowingCallable callable) {
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<beans xmlns="http://www.springframework.org/schema/beans"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.springframework.org/schema/beans
|
||||
https://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
|
||||
|
||||
<bean id="bean4" class="org.springframework.context.support.SmartLifecycleTestBean" depends-on="bean2"/>
|
||||
|
||||
<bean id="bean3" class="org.springframework.context.support.SmartLifecycleTestBean" depends-on="bean2"/>
|
||||
|
||||
<bean id="bean1" class="org.springframework.context.support.SmartLifecycleTestBean"/>
|
||||
|
||||
<bean id="bean2" class="org.springframework.context.support.SmartLifecycleTestBean" depends-on="bean1"/>
|
||||
|
||||
<bean id="bean2Proxy" class="org.springframework.aop.framework.ProxyFactoryBean">
|
||||
<property name="target" ref="bean2"/>
|
||||
</bean>
|
||||
|
||||
</beans>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user