Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Clozel b038beb854 Release v7.0.1 2025-11-20 09:57:36 +01:00
277 changed files with 1347 additions and 4001 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ bin
.springBeans
spring-*/src/main/java/META-INF/MANIFEST.MF
# IntelliJ IDEA artifacts and output dirs
# IDEA artifacts and output dirs
*.iml
*.ipr
*.iws
+1 -1
View File
@@ -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 IntelliJ editor settings we customize.
define the source file coding standards we use along with some IDEA 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.2.0");
checkstyle.setToolVersion("12.1.2");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+2 -4
View File
@@ -13,9 +13,7 @@ content:
- url: https://github.com/spring-projects/spring-framework
# Refname matching:
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
# 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.*.*.
branches: ['main', '{6..9}.+({1..9}).x']
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:
@@ -38,4 +36,4 @@ runtime:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.20/ui-bundle.zip
@@ -50,8 +50,9 @@ 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]) that supports automatic property
completion when you create bean definitions. Such IDE assistance is highly recommended.
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.
You can also configure a `java.util.Properties` instance, as follows:
@@ -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. 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.
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.
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 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 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 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,17 +319,9 @@ progresses.
== Testing
This section addresses testing with the combination of Kotlin and Spring Framework.
The recommended testing framework is https://junit.org/[JUnit] along with
The recommended testing framework is https://junit.org/junit5/[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].
@@ -360,6 +352,7 @@ 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 JacksonCborEncoder()))
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.build();
RSocketRequester requester = RSocketRequester.builder()
@@ -251,8 +251,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val strategies = RSocketStrategies.builder()
.encoders { it.add(JacksonCborEncoder()) }
.decoders { it.add(JacksonCborDecoder()) }
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.build()
val requester = RSocketRequester.builder()
@@ -681,8 +681,8 @@ Java::
@Bean
public RSocketStrategies rsocketStrategies() {
return RSocketStrategies.builder()
.encoders(encoders -> encoders.add(new JacksonCborEncoder()))
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
.routeMatcher(new PathPatternRouteMatcher())
.build();
}
@@ -703,8 +703,8 @@ Kotlin::
@Bean
fun rsocketStrategies() = RSocketStrategies.builder()
.encoders { it.add(JacksonCborEncoder()) }
.decoders { it.add(JacksonCborDecoder()) }
.encoders { it.add(Jackson2CborEncoder()) }
.decoders { it.add(Jackson2CborDecoder()) }
.routeMatcher(PathPatternRouteMatcher())
.build()
}
@@ -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://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
https://junit.org/junit5/docs/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,12 +2,11 @@
= 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.
For example, you can use each of the following as a meta-annotation in conjunction with
the xref:testing/testcontext-framework.adoc[TestContext framework].
You can use each of the following as a meta-annotation in conjunction with the
xref:testing/testcontext-framework.adoc[TestContext framework].
* `@BootstrapWith`
* `@ContextConfiguration`
@@ -38,7 +37,111 @@ the xref:testing/testcontext-framework.adoc[TestContext framework].
* `@EnabledIf` _(only supported on JUnit Jupiter)_
* `@DisabledIf` _(only supported on JUnit Jupiter)_
Consider the following test classes that use the `SpringExtension` with 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:
[tabs]
======
@@ -47,13 +150,13 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
class UserRepositoryTests { }
@@ -64,22 +167,23 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
class OrderRepositoryTests { }
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::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 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 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:
[tabs]
======
@@ -90,7 +194,7 @@ Java::
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
@ActiveProfiles("dev")
@Transactional
public @interface TransactionalDevTestConfig { }
@@ -103,7 +207,7 @@ Kotlin::
@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
@ActiveProfiles("dev")
@Transactional
annotation class TransactionalDevTestConfig { }
@@ -5,25 +5,24 @@ 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 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 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 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,
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
`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
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:
@@ -8,15 +8,12 @@ in JUnit and TestNG.
[[testcontext-junit-jupiter-extension]]
== SpringExtension for JUnit Jupiter
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.
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.
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
@@ -25,7 +22,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://docs.junit.org/current/user-guide/#extensions-conditions[conditional
* Powerful support for link:https://junit.org/junit5/docs/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]
@@ -163,7 +160,7 @@ for further details.
=== Dependency Injection with the `SpringExtension`
The `SpringExtension` implements the
link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
link:https://junit.org/junit5/docs/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/webflux-reactive-libraries.adoc[Reactive Libraries].
For more on that, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries].
[[webflux-fn-request]]
=== ServerRequest
@@ -296,8 +296,7 @@ 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, URI variables, or headers via `DataBinder`,
and also shows how to customize the `DataBinder`:
The following shows how to bind request parameters, including an optional `DataBinder` customization:
[tabs]
======
@@ -375,14 +374,14 @@ Java::
+
[source,java]
----
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
----
Kotlin::
+
[source,kotlin]
----
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
ServerResponse.ok().hint(Jackson2CodecSupport.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 `JacksonJsonEncoder`, `JacksonSmileEncoder`, or `Jaxb2XmlEncoder`.
`spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`, 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,10 +135,6 @@ 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,8 +334,19 @@ 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, consider using a Jackson format-specific builder like `JsonMapper.Builder` to configure Jackson's default
properties.
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.
[[webflux-config-view-resolvers]]
== View Resolvers
@@ -478,7 +489,7 @@ Java::
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
registry.defaultViews(new HttpMessageWriterView(encoder));
}
@@ -497,7 +508,7 @@ Kotlin::
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
val encoder = JacksonJsonEncoder()
val encoder = Jackson2JsonEncoder()
registry.defaultViews(HttpMessageWriterView(encoder))
}
@@ -690,7 +701,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim"]
----
@Configuration
class WebConfiguration : WebFluxConfigurer {
class WebConfiguration : WebMvcConfigurer {
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/webflux-reactive-libraries.adoc[or other]) are
Reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[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.
|===
@@ -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`
@@ -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`.
@@ -33,7 +33,7 @@ Kotlin::
----
======
WebFlux supports using a single value xref:web/webflux-reactive-libraries.adoc[reactive type] to
WebFlux supports using a single value xref:web-reactive.adoc#webflux-reactive-libraries[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:
@@ -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/webflux-reactive-libraries.adoc[Reactive Libraries] for more details.
of RxJava or another reactive library. See xref:web-reactive.adoc#webflux-reactive-libraries[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/webflux-reactive-libraries.adoc[or other])
You can return reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[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 `JacksonJsonDecoder` works as follows:
The `Jackson2Decoder` 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 `JsonMapper` to create a higher level object.
* Each `TokenBuffer` is passed to Jackson's `ObjectMapper` 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 `JsonMapper` as soon as enough bytes are received for a fully formed object. The
the `ObjectMapper` 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 `JacksonJsonEncoder` works as follows:
The `Jackson2Encoder` works as follows:
* For a single value publisher (for example, `Mono`), simply serialize it through the
`JsonMapper`.
`ObjectMapper`.
* 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 `JacksonJsonEncoder` 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 `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure
* For SSE the `Jackson2Encoder` is invoked per event and the output is flushed to ensure
delivery without delay.
[NOTE]
====
By default both `JacksonJsonEncoder` and `JacksonJsonDecoder` do not support elements of type
By default both `Jackson2Encoder` and `Jackson2Decoder` 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,8 +184,7 @@ val map = request.params()
----
======
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
and also shows how to customize the `DataBinder`:
The following shows how to bind request parameters, including an optional `DataBinder` customization:
[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 `JacksonJsonView` uses the Jackson library's `JsonMapper` to render the response
The `MappingJackson2JsonView` uses the Jackson library's `ObjectMapper` 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 `JsonMapper` through the `JsonMapper`
or `JsonMapper.Builder` constructor parameters, 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 `ObjectMapper`
through the `ObjectMapper` property, 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]#
`JacksonXmlView` uses the
`MappingJackson2XmlView` 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`
created via `XmlMapper.Builder` for cases where custom XML you need to provide
serializers and deserializers for specific types.
through the `ObjectMapper` property, 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`.
| `JacksonJsonHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `JsonMapper`.
| `MappingJackson2HttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `ObjectMapper`.
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 `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.
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.
| `JacksonXmlHttpMessageConverter`
| `MappingJackson2XmlHttpMessageConverter`
| 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 `JsonMapper` or `JsonMapper.Builder` constructor parameters.
By default, this converter supports `application/xml`. This requires the `tools.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 `ObjectMapper` property.
By default, this converter supports `application/xml`. This requires the `com.fasterxml.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.
| `JacksonCborHttpMessageConverter`
| `tools.jackson.dataformat:jackson-dataformat-cbor`
| `MappingJackson2CborHttpMessageConverter`
| `com.fasterxml.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/webflux-reactive-libraries.adoc[Reactive Libraries] in the WebFlux section).
xref:web-reactive.adoc#webflux-reactive-libraries[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, `+{2}+` simple name of required type
| `+{0}+` property name, `+{1}+` property value
| `UnsatisfiedServletRequestParameterException`
| (default)
@@ -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.
|===
@@ -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`.
|===
@@ -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`
@@ -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`.
@@ -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),
+1 -1
View File
@@ -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.7",
"@springio/antora-extensions": "1.14.2",
"fast-xml-parser": "4.5.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
}
@@ -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.AbstractJacksonHttpMessageConverter ;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
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(AbstractJacksonHttpMessageConverter.class)));
List.of(wac.getBean(AbstractJackson2HttpMessageConverter.class)));
}
// ...
@@ -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.JacksonJsonView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
@SuppressWarnings("removal")
// tag::snippet[]
@@ -30,7 +30,7 @@ public class FreeMarkerConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new JacksonJsonView());
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.freeMarker().cache(false);
}
@@ -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.JacksonJsonView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
@SuppressWarnings("removal")
// tag::snippet[]
@@ -28,7 +28,7 @@ public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.enableContentNegotiation(new JacksonJsonView());
registry.enableContentNegotiation(new MappingJackson2JsonView());
registry.jsp();
}
}
@@ -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.AbstractJacksonHttpMessageConverter
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
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(AbstractJacksonHttpMessageConverter::class.java)))
listOf(wac.getBean(AbstractJackson2HttpMessageConverter::class.java)))
// ...
@@ -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.JacksonJsonView
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
// tag::snippet[]
@Configuration
class FreeMarkerConfiguration : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(JacksonJsonView())
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.freeMarker().cache(false)
}
@@ -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.JacksonJsonView
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
// tag::snippet[]
@Configuration
class WebConfiguration : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.enableContentNegotiation(JacksonJsonView())
registry.enableContentNegotiation(MappingJackson2JsonView())
registry.jsp()
}
}
@@ -12,7 +12,7 @@
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:freemarker cache-views="false"/>
@@ -12,7 +12,7 @@
<mvc:view-resolvers>
<mvc:content-negotiation>
<mvc:default-views>
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
</mvc:default-views>
</mvc:content-negotiation>
<mvc:jsp/>
+9 -9
View File
@@ -8,15 +8,15 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.20.1"))
api(platform("io.micrometer:micrometer-bom:1.16.1"))
api(platform("io.micrometer:micrometer-bom:1.16.0"))
api(platform("io.netty:netty-bom:4.2.7.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.1"))
api(platform("io.projectreactor:reactor-bom:2025.0.0"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:5.0.3"))
api(platform("org.apache.logging.log4j:log4j-bom:2.25.2"))
api(platform("org.apache.groovy:groovy-bom:5.0.2"))
api(platform("org.apache.logging.log4j:log4j-bom:2.25.1"))
api(platform("org.assertj:assertj-bom:3.27.6"))
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.eclipse.jetty:jetty-bom:12.1.4"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.4"))
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.10.0")
api("com.jayway.jsonpath:json-path:2.9.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.4")
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.0")
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.CR3")
api("org.hibernate.orm:hibernate-core:7.2.0.CR2")
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
View File
@@ -1,4 +1,4 @@
version=7.0.2
version=7.0.1
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -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 IntelliJ IDEA metadata.
`.gitignore`. The same policy holds for IDEA metadata.
@@ -48,6 +48,7 @@ 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;
@@ -83,10 +84,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<Method, @Nullable Annotation>(
Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
new InstanceComparator<>(
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
method -> {
(Converter<Method, Annotation>) method -> {
AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
return (ann != null ? ann.getAnnotation() : null);
});
@@ -44,6 +44,7 @@ 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;
@@ -295,8 +296,10 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
/**
* Build a cache key for the given bean class and bean name.
* <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
* <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
* given bean {@code Class} as-is.
* @param beanClass the bean class
* @param beanName the bean name
@@ -304,7 +307,8 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
*/
protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
if (StringUtils.hasLength(beanName)) {
return new ComposedCacheKey(beanClass, beanName);
return (FactoryBean.class.isAssignableFrom(beanClass) ?
BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
}
else {
return beanClass;
@@ -611,12 +615,4 @@ 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) {
}
}
@@ -365,12 +365,9 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
for (Annotation annotation : methodParam.getMethodAnnotations()) {
if (isQualifier(annotation.annotationType())) {
return true;
}
for (Annotation annotation : methodParam.getMethodAnnotations()) {
if (isQualifier(annotation.annotationType())) {
return true;
}
}
}
@@ -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($S, $T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanName(), runtimeBeanReference.getBeanType());
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
runtimeBeanReference.getBeanType());
}
else if (value instanceof BeanReference beanReference) {
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
@@ -18,7 +18,6 @@ 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
@@ -26,7 +25,6 @@ import org.springframework.javapoet.ClassName;
*
* @author Phillip Webb
* @author Stephane Nicoll
* @author Sebastien Deleuze
* @since 6.0
* @see BeanFactoryInitializationAotContribution
*/
@@ -43,13 +41,6 @@ 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:
@@ -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();
@@ -1632,7 +1632,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
if (Optional.class == descriptor.getDependencyType()) {
return createOptionalDependency(descriptor, requestingBeanName, autowiredBeanNames, null);
return createOptionalDependency(descriptor, requestingBeanName);
}
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,
@Nullable Set<String> autowiredBeanNames, @Nullable Object @Nullable [] args) {
private Optional<?> createOptionalDependency(
DependencyDescriptor descriptor, @Nullable String beanName, final @Nullable Object... 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, autowiredBeanNames, null);
Object result = doResolveDependency(descriptorToUse, beanName, null, null);
return (result instanceof Optional<?> optional ? optional : Optional.ofNullable(result));
}
@@ -2501,18 +2501,12 @@ 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);
@@ -2521,17 +2515,22 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
@Override
public Object getObject() throws BeansException {
Object result = getValue();
if (result == null) {
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType());
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;
}
return result;
}
@Override
public Object getObject(final @Nullable Object... args) throws BeansException {
if (this.optional) {
return createOptionalDependency(this.descriptor, this.beanName, null, args);
return createOptionalDependency(this.descriptor, this.beanName, args);
}
else {
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
@@ -2552,7 +2551,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
public @Nullable Object getIfAvailable() throws BeansException {
try {
if (this.optional) {
return createOptionalDependency(this.descriptor, this.beanName, null, null);
return createOptionalDependency(this.descriptor, this.beanName);
}
else {
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
@@ -2605,7 +2604,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
};
try {
if (this.optional) {
return createOptionalDependency(descriptorToUse, this.beanName, null, null);
return createOptionalDependency(descriptorToUse, this.beanName);
}
else {
return doResolveDependency(descriptorToUse, this.beanName, null, null);
@@ -2631,41 +2630,11 @@ 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, autowiredBeanNames, null);
return createOptionalDependency(this.descriptor, this.beanName);
}
else {
return doResolveDependency(this.descriptor, this.beanName, autowiredBeanNames, null);
return doResolveDependency(this.descriptor, this.beanName, null, null);
}
}
@@ -532,7 +532,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
/**
* Callback before singleton creation.
* <p>The default implementation registers the singleton as currently in creation.
* <p>The default implementation register 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, key -> new LinkedHashSet<>(8));
this.containedBeanMap.computeIfAbsent(containingBeanName, k -> 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, key -> new LinkedHashSet<>(8));
this.dependentBeanMap.computeIfAbsent(canonicalName, k -> 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, key -> new LinkedHashSet<>(8));
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
dependenciesForBean.add(canonicalName);
}
}
@@ -1587,18 +1587,6 @@ 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
@@ -1608,18 +1596,6 @@ 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
@@ -2140,8 +2116,8 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
final 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();
@@ -2752,11 +2728,9 @@ 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
@@ -2764,11 +2738,9 @@ 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");
}
@@ -370,25 +370,8 @@ 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
@@ -399,25 +382,8 @@ 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
@@ -478,8 +444,8 @@ class InjectAnnotationBeanPostProcessorTests {
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
final 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();
@@ -493,7 +459,6 @@ class InjectAnnotationBeanPostProcessorTests {
NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -511,7 +476,6 @@ class InjectAnnotationBeanPostProcessorTests {
NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean");
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
}
@Test
@@ -530,7 +494,6 @@ 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
@@ -549,7 +512,6 @@ 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
@@ -568,7 +530,6 @@ 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
@@ -587,7 +548,6 @@ 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
@@ -606,7 +566,6 @@ 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
@@ -625,7 +584,6 @@ 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
@@ -830,6 +788,7 @@ class InjectAnnotationBeanPostProcessorTests {
private ConfigurableListableBeanFactory beanFactory;
public ConstructorResourceInjectionBean() {
throw new UnsupportedOperationException();
}
@@ -458,42 +458,30 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
class BeanReferenceTests {
@Test
void generatedWhenRuntimeBeanNameReference() {
BeanReference beanReference = new RuntimeBeanNameReference("test");
void generatedWhenBeanNameReference() {
RuntimeBeanNameReference beanReference = new RuntimeBeanNameReference("test");
compile(beanReference, (instance, compiler) -> {
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
assertThat(actual.getBeanType()).as("type").isNull();
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
});
}
@Test
void generatedWhenRuntimeBeanReferenceByName() {
BeanReference beanReference = new RuntimeBeanReference("test");
void generatedWhenBeanReferenceByName() {
RuntimeBeanReference beanReference = new RuntimeBeanReference("test");
compile(beanReference, (instance, compiler) -> {
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
assertThat(actual.getBeanType()).as("type").isNull();
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
assertThat(actual.getBeanType()).isEqualTo(beanReference.getBeanType());
});
}
@Test
void generatedWhenRuntimeBeanReferenceByType() {
void generatedWhenBeanReferenceByType() {
BeanReference beanReference = new RuntimeBeanReference(String.class);
compile(beanReference, (instance, compiler) -> {
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
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);
assertThat(actual.getBeanType()).isEqualTo(String.class);
});
}
@@ -268,7 +268,6 @@ 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) {
@@ -130,17 +130,4 @@ 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();
}
}
}
}
}
@@ -51,7 +51,8 @@ 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);
}
@@ -99,18 +100,4 @@ 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,7 +27,6 @@ import org.jspecify.annotations.Nullable;
*
* @author Costin Leau
* @author Sam Brannen
* @author Juergen Hoeller
* @since 3.1
*/
public interface CacheManager {
@@ -48,31 +47,4 @@ 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();
}
}
}
}
@@ -177,11 +177,6 @@ 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;
@@ -184,7 +184,6 @@ 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) {
@@ -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, key -> new ArrayList<>(2));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> 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, key -> new ArrayList<>(2));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> 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, key -> new ArrayList<>(2));
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
col.add(builder.build());
}
@@ -115,13 +115,6 @@ 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
@@ -119,11 +119,4 @@ public class CompositeCacheManager implements CacheManager, InitializingBean {
return Collections.unmodifiableSet(names);
}
@Override
public void resetCaches() {
for (CacheManager manager : this.cacheManagers) {
manager.resetCaches();
}
}
}
@@ -55,9 +55,4 @@ 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 (never null as of 7.0.2)
* @return the unique id of the context, or {@code null} if none
*/
String getId();
@Nullable String getId();
/**
* Return a name for the deployed application that this context belongs to.
@@ -42,20 +42,16 @@ 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;
@@ -876,14 +872,11 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
@Override
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add(
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder,
generationContext, beanFactoryInitializationCode.getClassName()));
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder, generationContext));
beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference());
}
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext,
ClassName className) {
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext) {
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
method.addJavadoc("Apply bean registrars.");
method.addModifiers(Modifier.PRIVATE);
@@ -922,7 +915,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
}
}
}
method.addCode(generateRegisterCode(className, generationContext));
method.addCode(generateRegisterCode());
}
private void checkUnsupportedFeatures(AbstractBeanDefinition beanDefinition) {
@@ -944,79 +937,37 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
return code.build();
}
private CodeBlock generateRegisterCode(ClassName className, GenerationContext generationContext) {
private CodeBlock generateRegisterCode() {
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()));
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);
}
}
code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass());
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")
.add(setImportMetadataCode)
.addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
beanRegistrarName, beanRegistrarEntry.getKey())
.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, $L.getClass(), $L), $L)", beanRegistrarName,
code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName,
BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE,
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrarName,
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(),
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) {
@@ -168,25 +168,22 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat
}
}
boolean cacheable = false;
if (!autowiredBeanNames.isEmpty()) {
cacheable = true;
for (String autowiredBeanName : autowiredBeanNames) {
if (!this.beanFactory.containsBean(autowiredBeanName)) {
boolean cacheable = true;
for (String autowiredBeanName : autowiredBeanNames) {
if (!this.beanFactory.containsBean(autowiredBeanName)) {
cacheable = false;
}
else {
if (this.beanName != null) {
this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName);
}
if (!this.beanFactory.isSingleton(autowiredBeanName)) {
cacheable = false;
}
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,4 +54,5 @@ public @interface Proxyable {
*/
Class<?>[] interfaces() default {};
}
@@ -57,7 +57,7 @@ public class ApplicationContextAotGenerator {
new ApplicationContextInitializationCodeGenerator(applicationContext, generationContext);
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator);
return codeGenerator.getClassName();
return codeGenerator.getGeneratedClass().getName();
});
}
@@ -127,9 +127,8 @@ class ApplicationContextInitializationCodeGenerator implements BeanFactoryInitia
return ArgumentCodeGenerator.from(new InitializerMethodArgumentCodeGenerator());
}
@Override
public ClassName getClassName() {
return this.generatedClass.getName();
GeneratedClass getGeneratedClass() {
return this.generatedClass;
}
@Override
@@ -623,22 +623,12 @@ 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();
@@ -81,7 +81,8 @@ final class DateTimeConverters {
return gc.toZonedDateTime();
}
else {
return Instant.ofEpochMilli(source.getTimeInMillis()).atZone(source.getTimeZone().toZoneId());
return ZonedDateTime.ofInstant(Instant.ofEpochMilli(source.getTimeInMillis()),
source.getTimeZone().toZoneId());
}
}
@@ -48,14 +48,12 @@ 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 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
* <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 &mdash; for example, in tests.
*/
boolean proxyTargetClass() default false;
@@ -52,9 +52,7 @@ public class ResilientMethodsConfiguration implements ImportAware {
private void configureProxySupport(ProxyProcessorSupport proxySupport) {
if (this.enableResilientMethods != null) {
if (this.enableResilientMethods.getBoolean("proxyTargetClass")) {
proxySupport.setProxyTargetClass(true);
}
proxySupport.setProxyTargetClass(this.enableResilientMethods.getBoolean("proxyTargetClass"));
proxySupport.setOrder(this.enableResilientMethods.getNumber("order"));
}
}
@@ -99,7 +99,6 @@ 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,39 +122,6 @@ 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 &mdash; 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
* &mdash; 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 &mdash; 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.
@@ -17,13 +17,10 @@
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;
@@ -52,8 +49,6 @@ 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?
*/
@@ -99,14 +94,12 @@ 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>() {
@@ -117,14 +110,11 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
}
@Override
public String getName() {
return methodName;
return ClassUtils.getQualifiedMethodName(method, (target != null ? target.getClass() : null));
}
});
}
catch (RetryException ex) {
if (logger.isDebugEnabled()) {
logger.debug("@Retryable operation '%s' failed".formatted(methodName), ex);
}
throw ex.getCause();
}
}
@@ -152,20 +142,8 @@ public abstract class AbstractRetryInterceptor implements MethodInterceptor {
.multiplier(spec.multiplier())
.maxBackoff(spec.maxDelay())
.filter(spec.combinedPredicate().forMethod(method));
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));
}
publisher = (adapter.isMultiValue() ? Flux.from(publisher).retryWhen(retry) :
Mono.from(publisher).retryWhen(retry));
return adapter.fromPublisher(publisher);
}
@@ -28,14 +28,11 @@ 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
@@ -49,40 +46,20 @@ 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, 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);
this(Collections.emptyList(), Collections.emptyList(), predicate, maxRetries, delay,
jitter, multiplier, maxDelay);
}
@@ -183,13 +183,12 @@ 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 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
* <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 &mdash; for example, in tests.
*/
boolean proxyTargetClass() default false;
@@ -51,9 +51,7 @@ public class ProxyAsyncConfiguration extends AbstractAsyncConfiguration {
if (customAsyncAnnotation != AnnotationUtils.getDefaultValue(EnableAsync.class, "annotation")) {
bpp.setAsyncAnnotationType(customAsyncAnnotation);
}
if (this.enableAsync.getBoolean("proxyTargetClass")) {
bpp.setProxyTargetClass(true);
}
bpp.setProxyTargetClass(this.enableAsync.getBoolean("proxyTargetClass"));
bpp.setOrder(this.enableAsync.<Integer>getNumber("order"));
return bpp;
}
@@ -377,7 +377,7 @@ public class ScheduledAnnotationBeanPostProcessor
try {
task = ScheduledAnnotationReactiveSupport.createSubscriptionRunnable(method, bean, scheduled,
this.registrar::getObservationRegistry,
this.reactiveSubscriptions.computeIfAbsent(bean, key -> new CopyOnWriteArrayList<>()));
this.reactiveSubscriptions.computeIfAbsent(bean, k -> new CopyOnWriteArrayList<>()));
}
catch (IllegalArgumentException ex) {
throw new IllegalStateException("Could not create recurring task for @Scheduled method '" +
@@ -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 = timestamp.atZone(zone);
ZonedDateTime zonedTimestamp = ZonedDateTime.ofInstant(timestamp, zone);
ZonedDateTime nextTimestamp = this.expression.next(zonedTimestamp);
return (nextTimestamp != null ? nextTimestamp.toInstant() : null);
}
@@ -62,7 +62,6 @@ 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;
@@ -504,7 +503,7 @@ public class ConfigurationClassPostProcessorAotContributionTests {
@Test
void applyToWhenIsImportAware() {
BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class,
ImportAwareConfiguration.class);
ImportAwareBeanRegistrarConfiguration.class);
assertThat(contribution).isNotNull();
contribution.applyTo(generationContext, beanFactoryInitializationCode);
compile((initializer, compiled) -> {
@@ -512,42 +511,7 @@ public class ConfigurationClassPostProcessorAotContributionTests {
initializer.accept(freshContext);
freshContext.refresh();
assertThat(freshContext.getBean(ClassNameHolder.class).className())
.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());
.isEqualTo(ImportAwareBeanRegistrarConfiguration.class.getName());
freshContext.close();
});
}
@@ -614,7 +578,7 @@ public class ConfigurationClassPostProcessorAotContributionTests {
}
@Import(ImportAwareBeanRegistrar.class)
public static class ImportAwareConfiguration {
public static class ImportAwareBeanRegistrarConfiguration {
}
public static class ImportAwareBeanRegistrar implements BeanRegistrar, ImportAware {
@@ -632,39 +596,9 @@ public class ConfigurationClassPostProcessorAotContributionTests {
public void setImportMetadata(AnnotationMetadata importMetadata) {
this.importMetadata = 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;
public @Nullable AnnotationMetadata getImportMetadata() {
return this.importMetadata;
}
}
@@ -19,7 +19,6 @@ 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;
@@ -30,7 +29,6 @@ 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;
@@ -102,16 +100,6 @@ 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();
@@ -309,25 +297,6 @@ 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 {
@@ -18,25 +18,17 @@ 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");
@@ -47,14 +39,12 @@ 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");
@@ -65,21 +55,18 @@ 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");
@@ -94,14 +81,12 @@ 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");
@@ -117,61 +102,7 @@ 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();
}
}
}
@@ -1,26 +0,0 @@
/*
* 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 {
}
@@ -21,11 +21,9 @@ 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;
@@ -67,12 +65,17 @@ class ReactiveRetryInterceptorTests {
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("6");
assertThat(target.counter).hasValue(6);
assertThat(target.counter.get()).isEqualTo(6);
}
@Test
void withPostProcessorForMethod() {
AnnotatedMethodBean proxy = getProxiedAnnotatedMethodBean();
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 target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
assertThatIllegalStateException()
@@ -81,7 +84,7 @@ class ReactiveRetryInterceptorTests {
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("6");
assertThat(target.counter).hasValue(6);
assertThat(target.counter.get()).isEqualTo(6);
}
@Test
@@ -102,7 +105,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).hasValue(3);
assertThat(target.counter.get()).isEqualTo(3);
}
@Test
@@ -116,7 +119,7 @@ class ReactiveRetryInterceptorTests {
.satisfies(isRetryExhaustedException())
.withCauseInstanceOf(FileSystemException.class);
// 1 initial attempt + 3 retries
assertThat(target.counter).hasValue(4);
assertThat(target.counter.get()).isEqualTo(4);
}
@Test // gh-35583
@@ -132,7 +135,7 @@ class ReactiveRetryInterceptorTests {
.isExactlyInstanceOf(RuntimeException.class)
.withCauseExactlyInstanceOf(FileSystemException.class);
// 1 initial attempt + 3 retries
assertThat(target.counter).hasValue(4);
assertThat(target.counter.get()).isEqualTo(4);
}
@Test
@@ -148,7 +151,7 @@ class ReactiveRetryInterceptorTests {
.satisfies(isReactiveException())
.withCauseInstanceOf(AccessDeniedException.class);
// 1 initial attempt + 0 retries
assertThat(target.counter).hasValue(1);
assertThat(target.counter.get()).isEqualTo(1);
}
@Test
@@ -167,7 +170,7 @@ class ReactiveRetryInterceptorTests {
.isThrownBy(() -> proxy.arithmeticOperation().block())
.withMessage("1");
// 1 initial attempt + 0 retries
assertThat(target.counter).hasValue(1);
assertThat(target.counter.get()).isEqualTo(1);
}
@Test
@@ -181,7 +184,7 @@ class ReactiveRetryInterceptorTests {
.satisfies(isRetryExhaustedException())
.withCauseInstanceOf(IOException.class);
// 1 initial attempt + 1 retry
assertThat(target.counter).hasValue(2);
assertThat(target.counter.get()).isEqualTo(2);
}
@Test
@@ -201,7 +204,7 @@ class ReactiveRetryInterceptorTests {
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("2");
assertThat(target.counter).hasValue(2);
assertThat(target.counter.get()).isEqualTo(2);
}
@Test
@@ -221,7 +224,7 @@ class ReactiveRetryInterceptorTests {
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("1");
assertThat(target.counter).hasValue(1);
assertThat(target.counter.get()).isEqualTo(1);
}
@Test
@@ -240,7 +243,7 @@ class ReactiveRetryInterceptorTests {
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("4");
assertThat(target.counter).hasValue(4);
assertThat(target.counter.get()).isEqualTo(4);
}
@Test
@@ -259,7 +262,7 @@ class ReactiveRetryInterceptorTests {
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("4");
assertThat(target.counter).hasValue(4);
assertThat(target.counter.get()).isEqualTo(4);
}
@Test
@@ -278,7 +281,7 @@ class ReactiveRetryInterceptorTests {
.havingCause()
.isInstanceOf(IOException.class)
.withMessage("4");
assertThat(target.counter).hasValue(4);
assertThat(target.counter.get()).isEqualTo(4);
}
@Test
@@ -294,7 +297,7 @@ class ReactiveRetryInterceptorTests {
String result = proxy.retryOperation().block();
assertThat(result).isEqualTo("success");
// Should execute only once because of successful return
assertThat(target.counter).hasValue(1);
assertThat(target.counter.get()).isEqualTo(1);
}
@Test
@@ -314,84 +317,7 @@ class ReactiveRetryInterceptorTests {
.isInstanceOf(NumberFormatException.class)
.withMessage("always fails");
// 1 initial attempt + 3 retries
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);
}
assertThat(target.counter.get()).isEqualTo(4);
}
@@ -403,23 +329,13 @@ 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(beanClass));
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
bf.addBeanPostProcessor(bpp);
return bf;
return bf.getBean(AnnotatedClassBean.class);
}
@@ -447,61 +363,6 @@ 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());
});
}
}
@@ -27,11 +27,10 @@ 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;
@@ -100,7 +99,11 @@ class RetryInterceptorTests {
@Test
void withPostProcessorForMethod() {
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBean.class);
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 target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
@@ -110,7 +113,11 @@ class RetryInterceptorTests {
@Test
void withPostProcessorForMethodWithInterface() {
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedMethodBeanWithInterface.class);
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
bf.addBeanPostProcessor(bpp);
AnnotatedInterface proxy = bf.getBean(AnnotatedInterface.class);
AnnotatedMethodBeanWithInterface target = (AnnotatedMethodBeanWithInterface) AopProxyUtils.getSingletonTarget(proxy);
@@ -121,8 +128,11 @@ class RetryInterceptorTests {
@Test
void withPostProcessorForMethodWithInterfaceAndDefaultTargetClass() {
ProxyConfig defaultProxyConfig = new ProxyConfig();
defaultProxyConfig.setProxyTargetClass(true);
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(bf);
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class));
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
@@ -154,8 +164,11 @@ class RetryInterceptorTests {
@Test
void withPostProcessorForMethodWithInterfaceAndExposeInterfaces() {
ProxyConfig defaultProxyConfig = new ProxyConfig();
defaultProxyConfig.setProxyTargetClass(true);
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(bf);
bf.registerSingleton(AutoProxyUtils.DEFAULT_PROXY_CONFIG_BEAN_NAME, defaultProxyConfig);
RootBeanDefinition bd = new RootBeanDefinition(AnnotatedMethodBeanWithInterface.class);
bd.setAttribute(AutoProxyUtils.EXPOSED_INTERFACES_ATTRIBUTE, AutoProxyUtils.ALL_INTERFACES_ATTRIBUTE_VALUE);
bf.registerBeanDefinition("bean", bd);
@@ -172,7 +185,11 @@ class RetryInterceptorTests {
@Test
void withPostProcessorForClass() {
DefaultListableBeanFactory bf = createBeanFactoryFor(AnnotatedClassBean.class);
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("bean", new RootBeanDefinition(AnnotatedClassBean.class));
RetryAnnotationBeanPostProcessor bpp = new RetryAnnotationBeanPostProcessor();
bpp.setBeanFactory(bf);
bf.addBeanPostProcessor(bpp);
AnnotatedClassBean proxy = bf.getBean(AnnotatedClassBean.class);
AnnotatedClassBean target = (AnnotatedClassBean) AopProxyUtils.getSingletonTarget(proxy);
@@ -267,37 +284,6 @@ 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();
@@ -313,79 +299,6 @@ 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;
@@ -413,49 +326,6 @@ 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));
}
}
@@ -18,6 +18,7 @@ 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;
@@ -52,28 +53,28 @@ class DefaultManagedTaskSchedulerTests {
void scheduleAtFixedRateWithStartTimeAndDurationAndNoScheduledExecutorProvidesDedicatedException() {
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
assertNoExecutorException(() -> scheduler.scheduleAtFixedRate(
NO_OP, Instant.now(), Duration.ofMinutes(1)));
NO_OP, Instant.now(), Duration.of(1, ChronoUnit.MINUTES)));
}
@Test
void scheduleAtFixedRateWithDurationAndNoScheduledExecutorProvidesDedicatedException() {
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
assertNoExecutorException(() -> scheduler.scheduleAtFixedRate(
NO_OP, Duration.ofMinutes(1)));
NO_OP, Duration.of(1, ChronoUnit.MINUTES)));
}
@Test
void scheduleWithFixedDelayWithStartTimeAndDurationAndNoScheduledExecutorProvidesDedicatedException() {
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
assertNoExecutorException(() -> scheduler.scheduleWithFixedDelay(
NO_OP, Instant.now(), Duration.ofMinutes(1)));
NO_OP, Instant.now(), Duration.of(1, ChronoUnit.MINUTES)));
}
@Test
void scheduleWithFixedDelayWithDurationAndNoScheduledExecutorProvidesDedicatedException() {
DefaultManagedTaskScheduler scheduler = new DefaultManagedTaskScheduler();
assertNoExecutorException(() -> scheduler.scheduleWithFixedDelay(
NO_OP, Duration.ofMinutes(1)));
NO_OP, Duration.of(1, ChronoUnit.MINUTES)));
}
private void assertNoExecutorException(ThrowingCallable callable) {
@@ -1,19 +0,0 @@
<?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