mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
160 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3591f1e9fb | |||
| e2c9dc7138 | |||
| 1818161f58 | |||
| 8916ee9f81 | |||
| d835fe311d | |||
| 0eefac21c9 | |||
| e99791f289 | |||
| 658775b914 | |||
| ea7a1d789e | |||
| 4ae471df01 | |||
| 75e3f44a7b | |||
| adcd7cb4cb | |||
| d0be180a69 | |||
| 3731fed4ca | |||
| bfc02cda0c | |||
| f734d04a0d | |||
| ce4c9ebe3c | |||
| 7c4801304d | |||
| 2731183420 | |||
| 38cf4ab3fc | |||
| 2137ec70d2 | |||
| 61201db704 | |||
| 2643c6212f | |||
| b46328a000 | |||
| cc4c693db7 | |||
| a206ea8b12 | |||
| 9f1d9fe82c | |||
| ab33000750 | |||
| e2ab9cd5da | |||
| 103db5429a | |||
| 68f8139206 | |||
| a9da900b5b | |||
| e7e4b3559a | |||
| feb77f924a | |||
| 12c3dc0cbe | |||
| df27627516 | |||
| 939aa84214 | |||
| 9f77f401ad | |||
| f19f1a667c | |||
| 69207c6c16 | |||
| f61832ecb8 | |||
| 14790493f4 | |||
| 53d9ba879d | |||
| 9d2d9ae0ff | |||
| c74af40288 | |||
| 196c1dd51c | |||
| 44689b9ec0 | |||
| b916dc962e | |||
| 87d0113b25 | |||
| cee185ce3b | |||
| e45727d4fa | |||
| bc088ebdef | |||
| f1cf91c9f9 | |||
| d36475cb4c | |||
| 8b6d7e0da7 | |||
| 0a86a7e3a8 | |||
| 8edde374bb | |||
| 35b58cd0ee | |||
| dfc900c7c4 | |||
| 07c0213f20 | |||
| 667851c0fa | |||
| 96aadc2b12 | |||
| 2c6ccaea05 | |||
| 62d09be2ae | |||
| 47f65b3dff | |||
| c7e24a5c26 | |||
| a493bd959d | |||
| 92e9543ad4 | |||
| 68231aa08c | |||
| 71d18ebabc | |||
| 3ccb0786db | |||
| 15c16d2c3a | |||
| 5dcb85db8d | |||
| 4ae03ecd40 | |||
| 1603045c7d | |||
| ace2b2bac9 | |||
| f082c435e1 | |||
| c6810334ca | |||
| 3b90311c17 | |||
| 946fc65712 | |||
| fd25e2f468 | |||
| 46ceedf992 | |||
| a4b3111928 | |||
| 4c4161c8c0 | |||
| 61d5413c23 | |||
| 08e6d762d2 | |||
| 7a19cbb452 | |||
| 2f1e1d483a | |||
| 24d152cdab | |||
| cc530d4df2 | |||
| 6c3132cb8c | |||
| a15274d431 | |||
| 45d4fd3b7e | |||
| 3fd5d69939 | |||
| 8647c44364 | |||
| 6504177e7b | |||
| c1125699bb | |||
| f58d0f6aae | |||
| c1b6bfb681 | |||
| 9d4abb63d8 | |||
| a9a404266c | |||
| b29f4ed37e | |||
| 4847ee80b0 | |||
| f4ee120a42 | |||
| e625a28f6d | |||
| 45c1cd9295 | |||
| 2641b5d783 | |||
| dc0c463137 | |||
| 83bbf16e6c | |||
| 3686b89ab5 | |||
| 85ca9f46dd | |||
| e2287020c3 | |||
| 9e18c75ff1 | |||
| fb9f31d101 | |||
| 4ee0a8ee33 | |||
| f3908832c5 | |||
| c10266e57e | |||
| 1b6b163f24 | |||
| 8642a39a03 | |||
| f62cc98a97 | |||
| c599775a9e | |||
| 4bc97ad532 | |||
| abec289e9f | |||
| 3026f0a49b | |||
| 9fe4e7798d | |||
| d178930186 | |||
| 91d2a51f3f | |||
| f456674529 | |||
| 35b8fbf901 | |||
| 1d1851f48a | |||
| 23f0cfb925 | |||
| 2cca56064e | |||
| e0353fe75f | |||
| f80b79bc45 | |||
| adffd3dcf6 | |||
| 284a28659d | |||
| 57a1d4007b | |||
| bc3431f435 | |||
| de5b9aab55 | |||
| f72891c24d | |||
| fac8708857 | |||
| 0342cd0904 | |||
| e0fd42981a | |||
| 87d95dc30a | |||
| 47de8b05e6 | |||
| 3b6be3d4d3 | |||
| 6115c3966c | |||
| 09a8bbc0c7 | |||
| ed75906834 | |||
| 4756a4be23 | |||
| 40c5c5d5f0 | |||
| 6b8552b4c1 | |||
| f42eea183e | |||
| ebcfe113d1 | |||
| b133c2b346 | |||
| 648629fab2 | |||
| f2bf227cd0 | |||
| 1260081436 | |||
| 18d8d458e3 | |||
| 1aa520162b |
+1
-1
@@ -38,7 +38,7 @@ bin
|
||||
.springBeans
|
||||
spring-*/src/main/java/META-INF/MANIFEST.MF
|
||||
|
||||
# IDEA artifacts and output dirs
|
||||
# IntelliJ IDEA artifacts and output dirs
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@ source code into your IDE.
|
||||
The wiki pages
|
||||
[Code Style](https://github.com/spring-projects/spring-framework/wiki/Code-Style) and
|
||||
[IntelliJ IDEA Editor Settings](https://github.com/spring-projects/spring-framework/wiki/IntelliJ-IDEA-Editor-Settings)
|
||||
define the source file coding standards we use along with some IDEA editor settings we customize.
|
||||
define the source file coding standards we use along with some IntelliJ editor settings we customize.
|
||||
|
||||
### Reference Docs
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("12.1.2");
|
||||
checkstyle.setToolVersion("12.2.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
+8
-5
@@ -18,6 +18,7 @@ package org.springframework.build.multirelease;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.gradle.api.JavaVersion;
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.artifacts.ConfigurationContainer;
|
||||
@@ -64,10 +65,12 @@ public class MultiReleaseJarPlugin implements Plugin<Project> {
|
||||
dependencies,
|
||||
objects);
|
||||
|
||||
TaskProvider<MultiReleaseJarValidateTask> validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> {
|
||||
task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile));
|
||||
task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25))));
|
||||
});
|
||||
tasks.named("check", task -> task.dependsOn(validateJarTask));
|
||||
if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_25)) {
|
||||
TaskProvider<MultiReleaseJarValidateTask> validateJarTask = tasks.register(VALIDATE_JAR_TASK_NAME, MultiReleaseJarValidateTask.class, (task) -> {
|
||||
task.getJar().set(tasks.named("jar", Jar.class).flatMap(AbstractArchiveTask::getArchiveFile));
|
||||
task.getJavaLauncher().set(task.getJavaToolchainService().launcherFor(spec -> spec.getLanguageVersion().set(JavaLanguageVersion.of(25))));
|
||||
});
|
||||
tasks.named("check", task -> task.dependsOn(validateJarTask));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
@@ -29,6 +29,8 @@ import org.gradle.testkit.runner.GradleRunner;
|
||||
import org.gradle.testkit.runner.UnexpectedBuildFailure;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -43,10 +45,13 @@ public class MultiReleaseJarPluginTests {
|
||||
|
||||
private File buildFile;
|
||||
|
||||
private File propertiesFile;
|
||||
|
||||
@BeforeEach
|
||||
void setup(@TempDir File projectDir) {
|
||||
this.projectDir = projectDir;
|
||||
this.buildFile = new File(this.projectDir, "build.gradle");
|
||||
this.propertiesFile = new File(this.projectDir, "gradle.properties");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,6 +124,7 @@ public class MultiReleaseJarPluginTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(max = JRE.JAVA_24, disabledReason = "'jar --validate' is available as of Java 25")
|
||||
void validateJar() throws IOException {
|
||||
writeBuildFile("""
|
||||
plugins {
|
||||
@@ -131,6 +137,9 @@ public class MultiReleaseJarPluginTests {
|
||||
}
|
||||
multiRelease { releaseVersions 17 }
|
||||
""");
|
||||
writeGradleProperties("""
|
||||
org.gradle.jvmargs=-Duser.language=en
|
||||
""");
|
||||
writeClass("src/main/java17", "Main.java", """
|
||||
public class Main {
|
||||
|
||||
@@ -152,6 +161,12 @@ public class MultiReleaseJarPluginTests {
|
||||
}
|
||||
}
|
||||
|
||||
private void writeGradleProperties(String properties) throws IOException {
|
||||
try (PrintWriter out = new PrintWriter(new FileWriter(this.propertiesFile))) {
|
||||
out.print(properties);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeClass(String path, String fileName, String fileContent) throws IOException {
|
||||
Path folder = this.projectDir.toPath().resolve(path);
|
||||
Files.createDirectories(folder);
|
||||
|
||||
@@ -13,7 +13,9 @@ content:
|
||||
- url: https://github.com/spring-projects/spring-framework
|
||||
# Refname matching:
|
||||
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
|
||||
branches: ['main', '{6..9}.+({1..9}).x']
|
||||
# branches: We include snapshots for main, 6.2.x, and 7.0.x to 9.*.x.
|
||||
branches: ['main', '6.2.x', '{7..9}.+({0..9}).x']
|
||||
# tags: We effectively include all releases from 6.0.9 to 9.*.*.
|
||||
tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})']
|
||||
start_path: framework-docs
|
||||
asciidoc:
|
||||
@@ -36,4 +38,4 @@ runtime:
|
||||
failure_level: warn
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.19/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
|
||||
|
||||
+2
-3
@@ -50,9 +50,8 @@ XML configuration:
|
||||
|
||||
The preceding XML is more succinct. However, typos are discovered at runtime rather than
|
||||
design time, unless you use an IDE (such as https://www.jetbrains.com/idea/[IntelliJ
|
||||
IDEA] or the {spring-site-tools}[Spring Tools for Eclipse])
|
||||
that supports automatic property completion when you create bean definitions. Such IDE
|
||||
assistance is highly recommended.
|
||||
IDEA] or the {spring-site-tools}[Spring Tools]) that supports automatic property
|
||||
completion when you create bean definitions. Such IDE assistance is highly recommended.
|
||||
|
||||
You can also configure a `java.util.Properties` instance, as follows:
|
||||
|
||||
|
||||
+5
-5
@@ -338,11 +338,11 @@ In the preceding scenario, using `@Autowired` works well and provides the desire
|
||||
modularity, but determining exactly where the autowired bean definitions are declared is
|
||||
still somewhat ambiguous. For example, as a developer looking at `ServiceConfig`, how do
|
||||
you know exactly where the `@Autowired AccountRepository` bean is declared? It is not
|
||||
explicit in the code, and this may be just fine. Remember that the
|
||||
{spring-site-tools}[Spring Tools for Eclipse] provides tooling that
|
||||
can render graphs showing how everything is wired, which may be all you need. Also,
|
||||
your Java IDE can easily find all declarations and uses of the `AccountRepository` type
|
||||
and quickly show you the location of `@Bean` methods that return that type.
|
||||
explicit in the code, and this may be just fine. Note that the
|
||||
{spring-site-tools}[Spring Tools] IDE support provides tooling that can render graphs
|
||||
showing how everything is wired, which may be all you need. Also, your Java IDE can
|
||||
easily find all declarations and uses of the `AccountRepository` type and quickly show
|
||||
you the location of `@Bean` methods that return that type.
|
||||
|
||||
In cases where this ambiguity is not acceptable and you wish to have direct navigation
|
||||
from within your IDE from one `@Configuration` class to another, consider autowiring the
|
||||
|
||||
@@ -7,14 +7,14 @@ similar to the https://jakarta.ee/specifications/expression-language/[Jakarta Ex
|
||||
Language] but offers additional features, most notably method invocation and basic string
|
||||
templating functionality.
|
||||
|
||||
While there are several other Java expression languages available -- OGNL, MVEL, and JBoss
|
||||
EL, to name a few -- the Spring Expression Language was created to provide the Spring
|
||||
community with a single well supported expression language that can be used across all
|
||||
the products in the Spring portfolio. Its language features are driven by the
|
||||
requirements of the projects in the Spring portfolio, including tooling requirements
|
||||
for code completion support within the {spring-site-tools}[Spring Tools for Eclipse].
|
||||
That said, SpEL is based on a technology-agnostic API that lets other expression language
|
||||
implementations be integrated, should the need arise.
|
||||
While there are several other Java expression languages available -- OGNL, MVEL, and
|
||||
JBoss EL, to name a few -- the Spring Expression Language was created to provide the
|
||||
Spring community with a single well supported expression language that can be used across
|
||||
all the products in the Spring portfolio. Its language features are driven by the
|
||||
requirements of the projects in the Spring portfolio, including tooling requirements for
|
||||
code completion within the {spring-site-tools}[Spring Tools] IDE support. That said, SpEL
|
||||
is based on a technology-agnostic API that lets other expression language implementations
|
||||
be integrated, should the need arise.
|
||||
|
||||
While SpEL serves as the foundation for expression evaluation within the Spring
|
||||
portfolio, it is not directly tied to Spring and can be used independently. To
|
||||
|
||||
@@ -319,9 +319,17 @@ progresses.
|
||||
== Testing
|
||||
|
||||
This section addresses testing with the combination of Kotlin and Spring Framework.
|
||||
The recommended testing framework is https://junit.org/junit5/[JUnit] along with
|
||||
The recommended testing framework is https://junit.org/[JUnit] along with
|
||||
https://mockk.io/[Mockk] for mocking.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Kotlin lets you specify meaningful test function names between backticks (```).
|
||||
|
||||
For a concrete example, see the `+++`Find all users on HTML page`()+++` test function later
|
||||
in this section.
|
||||
====
|
||||
|
||||
NOTE: If you are using Spring Boot, see
|
||||
{spring-boot-docs-ref}/features/kotlin.html#features.kotlin.testing[this related documentation].
|
||||
|
||||
@@ -352,7 +360,6 @@ file with a `spring.test.constructor.autowire.mode = all` property.
|
||||
[[per_class-lifecycle]]
|
||||
=== `PER_CLASS` Lifecycle
|
||||
|
||||
Kotlin lets you specify meaningful test function names between backticks (```).
|
||||
With JUnit Jupiter, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
|
||||
annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll`
|
||||
and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin.
|
||||
|
||||
@@ -237,8 +237,8 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RSocketStrategies strategies = RSocketStrategies.builder()
|
||||
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
|
||||
.encoders(encoders -> encoders.add(new JacksonCborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
|
||||
.build();
|
||||
|
||||
RSocketRequester requester = RSocketRequester.builder()
|
||||
@@ -251,8 +251,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val strategies = RSocketStrategies.builder()
|
||||
.encoders { it.add(Jackson2CborEncoder()) }
|
||||
.decoders { it.add(Jackson2CborDecoder()) }
|
||||
.encoders { it.add(JacksonCborEncoder()) }
|
||||
.decoders { it.add(JacksonCborDecoder()) }
|
||||
.build()
|
||||
|
||||
val requester = RSocketRequester.builder()
|
||||
@@ -681,8 +681,8 @@ Java::
|
||||
@Bean
|
||||
public RSocketStrategies rsocketStrategies() {
|
||||
return RSocketStrategies.builder()
|
||||
.encoders(encoders -> encoders.add(new Jackson2CborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new Jackson2CborDecoder()))
|
||||
.encoders(encoders -> encoders.add(new JacksonCborEncoder()))
|
||||
.decoders(decoders -> decoders.add(new JacksonCborDecoder()))
|
||||
.routeMatcher(new PathPatternRouteMatcher())
|
||||
.build();
|
||||
}
|
||||
@@ -703,8 +703,8 @@ Kotlin::
|
||||
|
||||
@Bean
|
||||
fun rsocketStrategies() = RSocketStrategies.builder()
|
||||
.encoders { it.add(Jackson2CborEncoder()) }
|
||||
.decoders { it.add(Jackson2CborDecoder()) }
|
||||
.encoders { it.add(JacksonCborEncoder()) }
|
||||
.decoders { it.add(JacksonCborDecoder()) }
|
||||
.routeMatcher(PathPatternRouteMatcher())
|
||||
.build()
|
||||
}
|
||||
|
||||
+1
-1
@@ -187,7 +187,7 @@ default mode may be set via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
|
||||
The default mode may also be configured as a
|
||||
https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
|
||||
https://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
|
||||
|
||||
If the `spring.test.constructor.autowire.mode` property is not set, test class
|
||||
constructors will not be automatically autowired.
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
= Meta-Annotation Support for Testing
|
||||
|
||||
You can use most test-related annotations as
|
||||
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to create custom composed
|
||||
annotations and reduce configuration duplication across a test suite.
|
||||
xref:core/beans/classpath-scanning.adoc#beans-meta-annotations[meta-annotations] to
|
||||
create custom composed annotations and reduce configuration duplication across a test
|
||||
suite.
|
||||
|
||||
You can use each of the following as a meta-annotation in conjunction with the
|
||||
xref:testing/testcontext-framework.adoc[TestContext framework].
|
||||
For example, you can use each of the following as a meta-annotation in conjunction with
|
||||
the xref:testing/testcontext-framework.adoc[TestContext framework].
|
||||
|
||||
* `@BootstrapWith`
|
||||
* `@ContextConfiguration`
|
||||
@@ -37,111 +38,7 @@ xref:testing/testcontext-framework.adoc[TestContext framework].
|
||||
* `@EnabledIf` _(only supported on JUnit Jupiter)_
|
||||
* `@DisabledIf` _(only supported on JUnit Jupiter)_
|
||||
|
||||
Consider the following example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public class OrderRepositoryTests { }
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public class UserRepositoryTests { }
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class OrderRepositoryTests { }
|
||||
|
||||
@RunWith(SpringRunner::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class UserRepositoryTests { }
|
||||
----
|
||||
======
|
||||
|
||||
If we discover that we are repeating the preceding configuration across our JUnit 4-based
|
||||
test suite, we can reduce the duplication by introducing a custom composed annotation
|
||||
that centralizes the common test configuration for Spring, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public @interface TransactionalDevTestConfig { }
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Target(AnnotationTarget.TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
annotation class TransactionalDevTestConfig { }
|
||||
----
|
||||
======
|
||||
|
||||
Then we can use our custom `@TransactionalDevTestConfig` annotation to simplify the
|
||||
configuration of individual JUnit 4 based test classes, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@TransactionalDevTestConfig
|
||||
public class OrderRepositoryTests { }
|
||||
|
||||
@RunWith(SpringRunner.class)
|
||||
@TransactionalDevTestConfig
|
||||
public class UserRepositoryTests { }
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner::class)
|
||||
@TransactionalDevTestConfig
|
||||
class OrderRepositoryTests
|
||||
|
||||
@RunWith(SpringRunner::class)
|
||||
@TransactionalDevTestConfig
|
||||
class UserRepositoryTests
|
||||
----
|
||||
======
|
||||
|
||||
If we write tests that use JUnit Jupiter, we can reduce code duplication even further,
|
||||
since annotations in JUnit Jupiter can also be used as meta-annotations. Consider the
|
||||
following example:
|
||||
Consider the following test classes that use the `SpringExtension` with JUnit Jupiter:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -150,13 +47,13 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class OrderRepositoryTests { }
|
||||
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class UserRepositoryTests { }
|
||||
@@ -167,23 +64,22 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class OrderRepositoryTests { }
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
class UserRepositoryTests { }
|
||||
----
|
||||
======
|
||||
|
||||
If we discover that we are repeating the preceding configuration across our JUnit
|
||||
Jupiter-based test suite, we can reduce the duplication by introducing a custom composed
|
||||
annotation that centralizes the common test configuration for Spring and JUnit Jupiter,
|
||||
as follows:
|
||||
If we discover that we are repeating the preceding configuration across our test suite,
|
||||
we can reduce the duplication by introducing a custom composed annotation that
|
||||
centralizes the common test configuration for Spring and JUnit Jupiter, as follows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -194,7 +90,7 @@ Java::
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextConfiguration({"/app-config.xml", "/test-data-access-config.xml"})
|
||||
@ContextConfiguration(classes = {AppConfig.class, TestDataAccessConfig.class})
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
public @interface TransactionalDevTestConfig { }
|
||||
@@ -207,7 +103,7 @@ Kotlin::
|
||||
@Target(AnnotationTarget.TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextConfiguration("/app-config.xml", "/test-data-access-config.xml")
|
||||
@ContextConfiguration(classes = [AppConfig::class, TestDataAccessConfig::class])
|
||||
@ActiveProfiles("dev")
|
||||
@Transactional
|
||||
annotation class TransactionalDevTestConfig { }
|
||||
|
||||
@@ -5,24 +5,25 @@ It is important to be able to perform some integration testing without requiring
|
||||
deployment to your application server or connecting to other enterprise infrastructure.
|
||||
Doing so lets you test things such as:
|
||||
|
||||
* The correct wiring of your Spring IoC container contexts.
|
||||
* Data access using JDBC or an ORM tool. This can include such things as the correctness
|
||||
of SQL statements, Hibernate queries, JPA entity mappings, and so forth.
|
||||
* The correct wiring of your Spring components.
|
||||
* Data access using JDBC or an ORM tool.
|
||||
** This can include such things as the correctness of SQL statements, Hibernate queries,
|
||||
JPA entity mappings, and so forth.
|
||||
|
||||
The Spring Framework provides first-class support for integration testing in the
|
||||
`spring-test` module. The name of the actual JAR file might include the release version
|
||||
and might also be in the long `org.springframework.test` form, depending on where you get
|
||||
it from (see the xref:core/beans/dependencies.adoc[section on Dependency Management]
|
||||
for an explanation). This library includes the `org.springframework.test` package, which
|
||||
`spring-test` module. The name of the actual JAR file might include the release version,
|
||||
depending on where you get it from (see the
|
||||
{spring-framework-wiki}/Spring-Framework-Artifacts[Spring Framework Artifacts] wiki page
|
||||
for details). This library includes the `org.springframework.test` package, which
|
||||
contains valuable classes for integration testing with a Spring container. This testing
|
||||
does not rely on an application server or other deployment environment. Such tests are
|
||||
slower to run than unit tests but much faster than the equivalent Selenium tests or
|
||||
remote tests that rely on deployment to an application server.
|
||||
|
||||
Unit and integration testing support is provided in the form of the annotation-driven
|
||||
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext framework is
|
||||
agnostic of the actual testing framework in use, which allows instrumentation of tests
|
||||
in various environments, including JUnit, TestNG, and others.
|
||||
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext
|
||||
framework is agnostic of the actual testing framework in use, which allows
|
||||
instrumentation of tests in various environments, including JUnit, TestNG, and others.
|
||||
|
||||
The following section provides an overview of the high-level goals of Spring's
|
||||
integration support, and the rest of this chapter then focuses on dedicated topics:
|
||||
|
||||
@@ -184,7 +184,10 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
standaloneSetup(SimpleController())
|
||||
.alwaysExpect<StandaloneMockMvcBuilder>(status().isOk())
|
||||
.alwaysExpect<StandaloneMockMvcBuilder>(content().contentType("application/json;charset=UTF-8"))
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
mockMvc = standaloneSetup(PersonController()).addFilters<StandaloneMockMvcBuilder>(CharacterEncodingFilter()).build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -159,7 +159,18 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
class MyWebTests {
|
||||
|
||||
lateinit var mockMvc: MockMvc
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
mockMvc = standaloneSetup(AccountController())
|
||||
.defaultRequest<StandaloneMockMvcBuilder>(get("/")
|
||||
.contextPath("/app").servletPath("/main")
|
||||
.accept(MediaType.APPLICATION_JSON)).build()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -25,7 +25,13 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
// static import of MockMvcBuilders.standaloneSetup
|
||||
|
||||
val mockMvc = standaloneSetup(MusicController())
|
||||
.defaultRequest<StandaloneMockMvcBuilder>(get("/").accept(MediaType.APPLICATION_JSON))
|
||||
.alwaysExpect<StandaloneMockMvcBuilder>(status().isOk())
|
||||
.alwaysExpect<StandaloneMockMvcBuilder>(content().contentType("application/json;charset=UTF-8"))
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -53,7 +59,13 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
// static import of SharedHttpSessionConfigurer.sharedHttpSession
|
||||
|
||||
val mockMvc = MockMvcBuilders.standaloneSetup(TestController())
|
||||
.apply<StandaloneMockMvcBuilder>(sharedHttpSession())
|
||||
.build()
|
||||
|
||||
// Use mockMvc to perform requests...
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ use https://www.gebish.org/[Geb] to make our tests even Groovy-er.
|
||||
== Why Geb and MockMvc?
|
||||
|
||||
Geb is backed by WebDriver, so it offers many of the
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from
|
||||
WebDriver. However, Geb makes things even easier by taking care of some of the
|
||||
boilerplate code for us.
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[same benefits]
|
||||
that we get from WebDriver. However, Geb makes things even easier by taking care of some
|
||||
of the boilerplate code for us.
|
||||
|
||||
[[mockmvc-server-htmlunit-geb-setup]]
|
||||
== MockMvc and Geb Setup
|
||||
@@ -28,7 +28,8 @@ def setup() {
|
||||
----
|
||||
|
||||
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
|
||||
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
|
||||
usage, see
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
|
||||
|
||||
This ensures that any URL referencing `localhost` as the server is directed to our
|
||||
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
|
||||
@@ -62,10 +63,10 @@ forwarded to the current page object. This removes a lot of the boilerplate code
|
||||
needed when using WebDriver directly.
|
||||
|
||||
As with direct WebDriver usage, this improves on the design of our
|
||||
xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object
|
||||
Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and
|
||||
WebDriver, but it is even easier with Geb. Consider our new Groovy-based
|
||||
`CreateMessagePage` implementation:
|
||||
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
|
||||
by using the Page Object Pattern. As mentioned previously, we can use the Page Object
|
||||
Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our new
|
||||
Groovy-based `CreateMessagePage` implementation:
|
||||
|
||||
[source,groovy]
|
||||
----
|
||||
|
||||
@@ -7,8 +7,7 @@ to use the raw HtmlUnit libraries.
|
||||
[[mockmvc-server-htmlunit-mah-setup]]
|
||||
== MockMvc and HtmlUnit Setup
|
||||
|
||||
First, make sure that you have included a test dependency on
|
||||
`org.htmlunit:htmlunit`.
|
||||
First, make sure that you have included a test dependency on `org.htmlunit:htmlunit`.
|
||||
|
||||
We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the
|
||||
`MockMvcWebClientBuilder`, as follows:
|
||||
@@ -45,7 +44,7 @@ Kotlin::
|
||||
======
|
||||
|
||||
NOTE: This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage,
|
||||
see xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
|
||||
see <<mockmvc-server-htmlunit-mah-advanced-builder>>.
|
||||
|
||||
This ensures that any URL that references `localhost` as the server is directed to our
|
||||
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
|
||||
@@ -77,7 +76,7 @@ Kotlin::
|
||||
======
|
||||
|
||||
NOTE: The default context path is `""`. Alternatively, we can specify the context path,
|
||||
as described in xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
|
||||
as described in <<mockmvc-server-htmlunit-mah-advanced-builder>>.
|
||||
|
||||
Once we have a reference to the `HtmlPage`, we can then fill out the form and submit it
|
||||
to create a message, as the following example shows:
|
||||
@@ -144,10 +143,10 @@ Kotlin::
|
||||
======
|
||||
|
||||
The preceding code improves on our
|
||||
xref:testing/mockmvc/htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways.
|
||||
First, we no longer have to explicitly verify our form and then create a request that
|
||||
looks like the form. Instead, we request the form, fill it out, and submit it, thereby
|
||||
significantly reducing the overhead.
|
||||
xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-why[MockMvc test] in a
|
||||
number of ways. First, we no longer have to explicitly verify our form and then create a
|
||||
request that looks like the form. Instead, we request the form, fill it out, and submit
|
||||
it, thereby significantly reducing the overhead.
|
||||
|
||||
Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit
|
||||
uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test
|
||||
@@ -267,7 +266,19 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
val mockMvc = MockMvcBuilders
|
||||
.webAppContextSetup(context)
|
||||
.apply<DefaultMockMvcBuilder>(springSecurity())
|
||||
.build()
|
||||
|
||||
webClient = MockMvcWebClientBuilder
|
||||
.mockMvcSetup(mockMvc)
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com", "example.org")
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -203,7 +203,7 @@ Kotlin::
|
||||
======
|
||||
|
||||
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
|
||||
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
|
||||
usage, see <<mockmvc-server-htmlunit-webdriver-advanced-builder>>.
|
||||
|
||||
The preceding example ensures that any URL that references `localhost` as the server is
|
||||
directed to our `MockMvc` instance without the need for a real HTTP connection. Any other
|
||||
@@ -259,10 +259,11 @@ Kotlin::
|
||||
======
|
||||
--
|
||||
|
||||
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
|
||||
This improves on the design of our
|
||||
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
|
||||
by leveraging the Page Object Pattern. As we mentioned in
|
||||
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
|
||||
with HtmlUnit, but it is much easier with WebDriver. Consider the following
|
||||
<<mockmvc-server-htmlunit-webdriver-why>>, we can use the Page Object Pattern with
|
||||
HtmlUnit, but it is much easier with WebDriver. Consider the following
|
||||
`CreateMessagePage` implementation:
|
||||
|
||||
--
|
||||
@@ -562,7 +563,19 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Not possible in Kotlin until {kotlin-issues}/KT-22208 is fixed
|
||||
val mockMvc: MockMvc = MockMvcBuilders
|
||||
.webAppContextSetup(context)
|
||||
.apply<DefaultMockMvcBuilder>(springSecurity())
|
||||
.build()
|
||||
|
||||
driver = MockMvcHtmlUnitDriverBuilder
|
||||
.mockMvcSetup(mockMvc)
|
||||
// for illustration only - defaults to ""
|
||||
.contextPath("")
|
||||
// By default MockMvc is used for localhost only;
|
||||
// the following will use MockMvc for example.com and example.org as well
|
||||
.useMockMvcForHosts("example.com", "example.org")
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ assume our form looks like the following snippet:
|
||||
</form>
|
||||
----
|
||||
|
||||
How do we ensure that our form produce the correct request to create a new message? A
|
||||
How do we ensure that our form produces the correct request to create a new message? A
|
||||
naive attempt might resemble the following:
|
||||
|
||||
[tabs]
|
||||
@@ -154,7 +154,7 @@ validation.
|
||||
[[mockmvc-server-htmlunit-why-integration]]
|
||||
== Integration Testing to the Rescue?
|
||||
|
||||
To resolve the issues mentioned earlier, we could perform end-to-end integration testing,
|
||||
To resolve the issues mentioned above, we could perform end-to-end integration testing,
|
||||
but this has some drawbacks. Consider testing the view that lets us page through the
|
||||
messages. We might need the following tests:
|
||||
|
||||
@@ -171,7 +171,7 @@ leads to a number of additional challenges:
|
||||
* Testing can become slow, since each test would need to ensure that the database is in
|
||||
the correct state.
|
||||
* Since our database needs to be in a specific state, we cannot run tests in parallel.
|
||||
* Performing assertions on such items as auto-generated IDs, timestamps, and others can
|
||||
* Performing assertions on items such as auto-generated IDs, timestamps, and others can
|
||||
be difficult.
|
||||
|
||||
These challenges do not mean that we should abandon end-to-end integration testing
|
||||
|
||||
+11
-8
@@ -8,12 +8,15 @@ in JUnit and TestNG.
|
||||
[[testcontext-junit-jupiter-extension]]
|
||||
== SpringExtension for JUnit Jupiter
|
||||
|
||||
The Spring TestContext Framework offers full integration with the JUnit Jupiter testing
|
||||
framework, originally introduced in JUnit 5. By annotating test classes with
|
||||
`@ExtendWith(SpringExtension.class)`, you can implement standard JUnit Jupiter-based unit
|
||||
and integration tests and simultaneously reap the benefits of the TestContext framework,
|
||||
such as support for loading application contexts, dependency injection of test instances,
|
||||
transactional test method execution, and so on.
|
||||
The `SpringExtension` integrates the Spring TestContext Framework into the JUnit Jupiter
|
||||
testing framework.
|
||||
|
||||
NOTE: As of Spring Framework 7.0, the `SpringExtension` requires JUnit Jupiter 6.0 or higher.
|
||||
|
||||
By annotating test classes with `@ExtendWith(SpringExtension.class)`, you can implement
|
||||
standard JUnit Jupiter-based unit and integration tests and simultaneously reap the
|
||||
benefits of the TestContext framework, such as support for loading application contexts,
|
||||
dependency injection of test instances, transactional test method execution, and so on.
|
||||
|
||||
Furthermore, thanks to the rich extension API in JUnit Jupiter, Spring provides the
|
||||
following features above and beyond the feature set that Spring supports for JUnit 4 and
|
||||
@@ -22,7 +25,7 @@ TestNG:
|
||||
* Dependency injection for test constructors, test methods, and test lifecycle callback
|
||||
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
|
||||
Injection with the `SpringExtension`] for further details.
|
||||
* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
|
||||
* Powerful support for link:https://docs.junit.org/current/user-guide/#extensions-conditions[conditional
|
||||
test execution] based on SpEL expressions, environment variables, system properties,
|
||||
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
|
||||
@@ -160,7 +163,7 @@ for further details.
|
||||
=== Dependency Injection with the `SpringExtension`
|
||||
|
||||
The `SpringExtension` implements the
|
||||
link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
|
||||
link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
|
||||
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
|
||||
constructors, test methods, and test lifecycle callback methods.
|
||||
|
||||
|
||||
@@ -362,7 +362,7 @@ of `PlatformTransactionManager` within the test's `ApplicationContext`, you can
|
||||
qualifier by using `@Transactional("myTxMgr")` or `@Transactional(transactionManager =
|
||||
"myTxMgr")`, or `TransactionManagementConfigurer` can be implemented by an
|
||||
`@Configuration` class. Consult the
|
||||
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc
|
||||
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager(org.springframework.test.context.TestContext,java.lang.String)[javadoc
|
||||
for `TestContextTransactionUtils.retrieveTransactionManager()`] for details on the
|
||||
algorithm used to look up a transaction manager in the test's `ApplicationContext`.
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ infrastructure and controller declarations and use it to handle requests via moc
|
||||
and response objects, without a running server.
|
||||
|
||||
For WebFlux, use the following where the Spring `ApplicationContext` is passed to
|
||||
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder]
|
||||
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext(org.springframework.context.ApplicationContext)[WebHttpHandlerBuilder]
|
||||
to create the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[WebHandler chain] to handle
|
||||
requests:
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the WebFlux Java configuration to declare such mappings, which results in a single,
|
||||
global map passed to all `HandlerMapping` implementations.
|
||||
@@ -57,7 +57,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
|
||||
@@ -123,7 +123,7 @@ Both request and response provide {reactive-streams-site}[Reactive Streams] back
|
||||
against the body streams.
|
||||
The request body is represented with a Reactor `Flux` or `Mono`.
|
||||
The response body is represented with any Reactive Streams `Publisher`, including `Flux` and `Mono`.
|
||||
For more on that, see xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries].
|
||||
For more on that, see xref:web/webflux-reactive-libraries.adoc[Reactive Libraries].
|
||||
|
||||
[[webflux-fn-request]]
|
||||
=== ServerRequest
|
||||
@@ -296,7 +296,8 @@ allPartsEvents.windowUntil(PartEvent::isLast)
|
||||
|
||||
NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
|
||||
|
||||
The following shows how to bind request parameters, including an optional `DataBinder` customization:
|
||||
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
|
||||
and also shows how to customize the `DataBinder`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -374,14 +375,14 @@ Java::
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
|
||||
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin]
|
||||
----
|
||||
ServerResponse.ok().hint(Jackson2CodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
|
||||
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ purposes, it is useful to be able to alternate between rendering a model with an
|
||||
or as other formats (such as JSON or XML), depending on the content type requested by the client.
|
||||
To support doing so, Spring WebFlux provides the `HttpMessageWriterView`, which you can use to
|
||||
plug in any of the available xref:web/webflux/reactive-spring.adoc#webflux-codecs[Codecs] from
|
||||
`spring-web`, such as `Jackson2JsonEncoder`, `Jackson2SmileEncoder`, or `Jaxb2XmlEncoder`.
|
||||
`spring-web`, such as `JacksonJsonEncoder`, `JacksonSmileEncoder`, or `Jaxb2XmlEncoder`.
|
||||
|
||||
Unlike other view technologies, `HttpMessageWriterView` does not require a `ViewResolver` but is
|
||||
instead xref:web/webflux/config.adoc#webflux-config-view-resolvers[configured] as a default view.
|
||||
|
||||
@@ -135,6 +135,10 @@ Message codes and arguments for each error are also resolved via `MessageSource`
|
||||
| `+{0}+` the list of global errors, `+{1}+` the list of field errors.
|
||||
Message codes and arguments for each error are also resolved via `MessageSource`.
|
||||
|
||||
| `NoResourceFoundException`
|
||||
| (default)
|
||||
| `+{0}+` the request path (or portion of) used to find a resource
|
||||
|
||||
|===
|
||||
|
||||
NOTE: Unlike other exceptions, the message arguments for
|
||||
|
||||
@@ -334,19 +334,8 @@ Kotlin::
|
||||
`ServerCodecConfigurer` provides a set of default readers and writers. You can use it to add
|
||||
more readers and writers, customize the default ones, or replace the default ones completely.
|
||||
|
||||
For Jackson JSON and XML, consider using
|
||||
{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`],
|
||||
which customizes Jackson's default properties with the following ones:
|
||||
|
||||
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled.
|
||||
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled.
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {jackson-github-org}/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types.
|
||||
* {jackson-github-org}/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`.
|
||||
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
|
||||
|
||||
For Jackson, consider using a Jackson format-specific builder like `JsonMapper.Builder` to configure Jackson's default
|
||||
properties.
|
||||
|
||||
[[webflux-config-view-resolvers]]
|
||||
== View Resolvers
|
||||
@@ -489,7 +478,7 @@ Java::
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.freeMarker();
|
||||
|
||||
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
|
||||
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
|
||||
registry.defaultViews(new HttpMessageWriterView(encoder));
|
||||
}
|
||||
|
||||
@@ -508,7 +497,7 @@ Kotlin::
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.freeMarker()
|
||||
|
||||
val encoder = Jackson2JsonEncoder()
|
||||
val encoder = JacksonJsonEncoder()
|
||||
registry.defaultViews(HttpMessageWriterView(encoder))
|
||||
}
|
||||
|
||||
@@ -701,7 +690,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
@Configuration
|
||||
class WebConfiguration : WebMvcConfigurer {
|
||||
class WebConfiguration : WebFluxConfigurer {
|
||||
|
||||
override fun configureApiVersioning(configurer: ApiVersionConfigurer) {
|
||||
configurer.useRequestHeader("API-Version")
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
The following table shows the supported controller method arguments.
|
||||
|
||||
Reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other]) are
|
||||
Reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other]) are
|
||||
supported on arguments that require blocking I/O (for example, reading the request body) to
|
||||
be resolved. This is marked in the Description column. Reactive types are not expected
|
||||
on arguments that do not require blocking.
|
||||
@@ -114,6 +114,6 @@ and others) and is equivalent to `required=false`.
|
||||
| Any other argument
|
||||
| If a method argument is not matched to any of the above, it is, by default, resolved as
|
||||
a `@RequestParam` if it is a simple type, as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
|
||||
or as a `@ModelAttribute`, otherwise.
|
||||
|===
|
||||
|
||||
+1
-1
@@ -205,7 +205,7 @@ controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any argument that is not a simple
|
||||
value type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
|
||||
|
||||
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
|
||||
|
||||
+1
-1
@@ -74,6 +74,6 @@ When a `@RequestParam` annotation is declared on a `Map<String, String>` or
|
||||
|
||||
Note that use of `@RequestParam` is optional -- for example, to set its attributes. By
|
||||
default, any argument that is a simple value type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver is treated as if it were annotated
|
||||
with `@RequestParam`.
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
WebFlux supports using a single value xref:web-reactive.adoc#webflux-reactive-libraries[reactive type] to
|
||||
WebFlux supports using a single value xref:web/webflux-reactive-libraries.adoc[reactive type] to
|
||||
produce the `ResponseEntity` asynchronously, and/or single and multi-value reactive types
|
||||
for the body. This allows a variety of async responses with `ResponseEntity` as follows:
|
||||
|
||||
|
||||
+1
-1
@@ -87,6 +87,6 @@ Reactor provides a dedicated operator for that, `Flux#collectList()`.
|
||||
| Other return values
|
||||
| If a return value remains unresolved in any other way, it is treated as a model
|
||||
attribute, unless it is a simple type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
|
||||
in which case it remains unresolved.
|
||||
|===
|
||||
|
||||
@@ -207,7 +207,7 @@ was not provided (for example, model attribute was returned) or an async return
|
||||
view resolution scenarios. Explore the options in your IDE with code completion.
|
||||
* `Model`, `Map`: Extra model attributes to be added to the model for the request.
|
||||
* Any other: Any other return value (except for simple types, as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
|
||||
is treated as a model attribute to be added to the model. The attribute name is derived
|
||||
from the class name by using {spring-framework-api}/core/Conventions.html[conventions],
|
||||
unless a handler method `@ModelAttribute` annotation is present.
|
||||
|
||||
@@ -77,7 +77,7 @@ as input, adapts it to a Reactor type internally, uses that, and returns either
|
||||
`Flux` or a `Mono` as output. So, you can pass any `Publisher` as input and you can apply
|
||||
operations on the output, but you need to adapt the output for use with another reactive library.
|
||||
Whenever feasible (for example, annotated controllers), WebFlux adapts transparently to the use
|
||||
of RxJava or another reactive library. See xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] for more details.
|
||||
of RxJava or another reactive library. See xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] for more details.
|
||||
|
||||
NOTE: In addition to Reactive APIs, WebFlux can also be used with
|
||||
xref:languages/kotlin/coroutines.adoc[Coroutines] APIs in Kotlin which provides a more imperative style of programming.
|
||||
@@ -148,7 +148,7 @@ RxJava to perform blocking calls on a separate thread but you would not be makin
|
||||
most of a non-blocking web stack.
|
||||
|
||||
* If you have a Spring MVC application with calls to remote services, try the reactive `WebClient`.
|
||||
You can return reactive types (Reactor, RxJava, xref:web-reactive.adoc#webflux-reactive-libraries[or other])
|
||||
You can return reactive types (Reactor, RxJava, xref:web/webflux-reactive-libraries.adoc[or other])
|
||||
directly from Spring MVC controller methods. The greater the latency per call or the
|
||||
interdependency among calls, the more dramatic the benefits. Spring MVC controllers
|
||||
can call other reactive components too.
|
||||
|
||||
@@ -459,22 +459,22 @@ xref:web/webflux/config.adoc#webflux-config-message-codecs[HTTP message codecs].
|
||||
JSON and binary JSON ({jackson-github-org}/smile-format-specification[Smile]) are
|
||||
both supported when the Jackson library is present.
|
||||
|
||||
The `Jackson2Decoder` works as follows:
|
||||
The `JacksonJsonDecoder` works as follows:
|
||||
|
||||
* Jackson's asynchronous, non-blocking parser is used to aggregate a stream of byte chunks
|
||||
into ``TokenBuffer``'s each representing a JSON object.
|
||||
* Each `TokenBuffer` is passed to Jackson's `ObjectMapper` to create a higher level object.
|
||||
* Each `TokenBuffer` is passed to Jackson's `JsonMapper` to create a higher level object.
|
||||
* When decoding to a single-value publisher (for example, `Mono`), there is one `TokenBuffer`.
|
||||
* When decoding to a multi-value publisher (for example, `Flux`), each `TokenBuffer` is passed to
|
||||
the `ObjectMapper` as soon as enough bytes are received for a fully formed object. The
|
||||
the `JsonMapper` as soon as enough bytes are received for a fully formed object. The
|
||||
input content can be a JSON array, or any
|
||||
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format such as NDJSON,
|
||||
JSON Lines, or JSON Text Sequences.
|
||||
|
||||
The `Jackson2Encoder` works as follows:
|
||||
The `JacksonJsonEncoder` works as follows:
|
||||
|
||||
* For a single value publisher (for example, `Mono`), simply serialize it through the
|
||||
`ObjectMapper`.
|
||||
`JsonMapper`.
|
||||
* For a multi-value publisher with `application/json`, by default collect the values with
|
||||
`Flux#collectToList()` and then serialize the resulting collection.
|
||||
* For a multi-value publisher with a streaming media type such as
|
||||
@@ -482,12 +482,12 @@ The `Jackson2Encoder` works as follows:
|
||||
flush each value individually using a
|
||||
https://en.wikipedia.org/wiki/JSON_streaming[line-delimited JSON] format. Other
|
||||
streaming media types may be registered with the encoder.
|
||||
* For SSE the `Jackson2Encoder` is invoked per event and the output is flushed to ensure
|
||||
* For SSE the `JacksonJsonEncoder` is invoked per event and the output is flushed to ensure
|
||||
delivery without delay.
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
By default both `Jackson2Encoder` and `Jackson2Decoder` do not support elements of type
|
||||
By default both `JacksonJsonEncoder` and `JacksonJsonDecoder` do not support elements of type
|
||||
`String`. Instead the default assumption is that a string or a sequence of strings
|
||||
represent serialized JSON content, to be rendered by the `CharSequenceEncoder`. If what
|
||||
you need is to render a JSON array from `Flux<String>`, use `Flux#collectToList()` and
|
||||
|
||||
@@ -71,7 +71,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
|
||||
and, consequently, browsers reject them.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
|
||||
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
|
||||
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
|
||||
use the MVC Java configuration or the XML namespace to declare such mappings, which results
|
||||
in a single global map being passed to all `HandlerMapping` instances.
|
||||
@@ -84,7 +84,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
|
||||
The rules for combining global and local configuration are generally additive -- for example,
|
||||
all global and all local origins. For those attributes where only a single value can be
|
||||
accepted, for example, `allowCredentials` and `maxAge`, the local overrides the global value. See
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
|
||||
for more details.
|
||||
|
||||
[TIP]
|
||||
|
||||
@@ -184,7 +184,8 @@ val map = request.params()
|
||||
----
|
||||
======
|
||||
|
||||
The following shows how to bind request parameters, including an optional `DataBinder` customization:
|
||||
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
|
||||
and also shows how to customize the `DataBinder`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
@@ -10,7 +10,7 @@ Spring offers support for the Jackson JSON library.
|
||||
== Jackson-based JSON MVC Views
|
||||
[.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]#
|
||||
|
||||
The `MappingJackson2JsonView` uses the Jackson library's `ObjectMapper` to render the response
|
||||
The `JacksonJsonView` uses the Jackson library's `JsonMapper` to render the response
|
||||
content as JSON. By default, the entire contents of the model map (with the exception of
|
||||
framework-specific classes) are encoded as JSON. For cases where the contents of the
|
||||
map need to be filtered, you can specify a specific set of model attributes to encode
|
||||
@@ -18,17 +18,17 @@ by using the `modelKeys` property. You can also use the `extractValueFromSingleK
|
||||
property to have the value in single-key models extracted and serialized directly rather
|
||||
than as a map of model attributes.
|
||||
|
||||
You can customize JSON mapping as needed by using Jackson's provided
|
||||
annotations. When you need further control, you can inject a custom `ObjectMapper`
|
||||
through the `ObjectMapper` property, for cases where you need to provide custom JSON
|
||||
serializers and deserializers for specific types.
|
||||
You can customize JSON mapping as needed by using Jackson's provided annotations. When
|
||||
you need further control, you can inject a custom `JsonMapper` through the `JsonMapper`
|
||||
or `JsonMapper.Builder` constructor parameters, for cases where you need to provide
|
||||
custom JSON serializers and deserializers for specific types.
|
||||
|
||||
|
||||
[[mvc-view-xml-mapping]]
|
||||
== Jackson-based XML Views
|
||||
[.small]#xref:web/webflux-view.adoc#webflux-view-httpmessagewriter[See equivalent in the Reactive stack]#
|
||||
|
||||
`MappingJackson2XmlView` uses the
|
||||
`JacksonXmlView` uses the
|
||||
{jackson-github-org}/jackson-dataformat-xml[Jackson XML extension's] `XmlMapper`
|
||||
to render the response content as XML. If the model contains multiple entries, you should
|
||||
explicitly set the object to be serialized by using the `modelKey` bean property. If the
|
||||
@@ -36,5 +36,5 @@ model contains a single entry, it is serialized automatically.
|
||||
|
||||
You can customize XML mapping as needed by using JAXB or Jackson's provided
|
||||
annotations. When you need further control, you can inject a custom `XmlMapper`
|
||||
through the `ObjectMapper` property, for cases where custom XML
|
||||
you need to provide serializers and deserializers for specific types.
|
||||
created via `XmlMapper.Builder` for cases where custom XML you need to provide
|
||||
serializers and deserializers for specific types.
|
||||
|
||||
@@ -42,25 +42,25 @@ This converter requires a `Marshaller` and `Unmarshaller` before it can be used.
|
||||
You can inject these through constructor or bean properties.
|
||||
By default, this converter supports `text/xml` and `application/xml`.
|
||||
|
||||
| `MappingJackson2HttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `ObjectMapper`.
|
||||
| `JacksonJsonHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `JsonMapper`.
|
||||
You can customize JSON mapping as needed through the use of Jackson's provided annotations.
|
||||
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `ObjectMapper` through the `ObjectMapper` property.
|
||||
By default, this converter supports `application/json`. This requires the `com.fasterxml.jackson.core:jackson-databind` dependency.
|
||||
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder ` constructor parameters.
|
||||
By default, this converter supports `application/json`. This requires the `tools.jackson.core:jackson-databind` dependency.
|
||||
|
||||
| `MappingJackson2XmlHttpMessageConverter`
|
||||
| `JacksonXmlHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write XML by using {jackson-github-org}/jackson-dataformat-xml[Jackson XML] extension's `XmlMapper`.
|
||||
You can customize XML mapping as needed through the use of JAXB or Jackson's provided annotations.
|
||||
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `ObjectMapper` property.
|
||||
By default, this converter supports `application/xml`. This requires the `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` dependency.
|
||||
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `JsonMapper` or `JsonMapper.Builder` constructor parameters.
|
||||
By default, this converter supports `application/xml`. This requires the `tools.jackson.dataformat:jackson-dataformat-xml` dependency.
|
||||
|
||||
| `KotlinSerializationJsonHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write JSON using `kotlinx.serialization`.
|
||||
This converter is not configured by default, as this conflicts with Jackson.
|
||||
Developers must configure it as an additional converter ahead of the Jackson one.
|
||||
|
||||
| `MappingJackson2CborHttpMessageConverter`
|
||||
| `com.fasterxml.jackson.dataformat:jackson-dataformat-cbor`
|
||||
| `JacksonCborHttpMessageConverter`
|
||||
| `tools.jackson.dataformat:jackson-dataformat-cbor`
|
||||
|
||||
| `SourceHttpMessageConverter`
|
||||
| An `HttpMessageConverter` implementation that can read and write `javax.xml.transform.Source` from the HTTP request and response.
|
||||
|
||||
@@ -414,7 +414,7 @@ customize the status and headers of the response.
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-codecs-streaming[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC supports use of reactive client libraries in a controller (also read
|
||||
xref:web-reactive.adoc#webflux-reactive-libraries[Reactive Libraries] in the WebFlux section).
|
||||
xref:web/webflux-reactive-libraries.adoc[Reactive Libraries] in the WebFlux section).
|
||||
This includes the `WebClient` from `spring-webflux` and others, such as Spring Data
|
||||
reactive data repositories. In such scenarios, it is convenient to be able to return
|
||||
reactive types from the controller method.
|
||||
|
||||
@@ -171,11 +171,11 @@ Message codes and arguments for each error are also resolved via `MessageSource`
|
||||
|
||||
| `NoResourceFoundException`
|
||||
| (default)
|
||||
|
|
||||
| `+{0}+` the request path (or portion of) used to find a resource
|
||||
|
||||
| `TypeMismatchException`
|
||||
| (default)
|
||||
| `+{0}+` property name, `+{1}+` property value
|
||||
| `+{0}+` property name, `+{1}+` property value, `+{2}+` simple name of required type
|
||||
|
||||
| `UnsatisfiedServletRequestParameterException`
|
||||
| (default)
|
||||
|
||||
+1
-1
@@ -215,7 +215,7 @@ the content negotiation during the error handling phase will decide which conten
|
||||
|
||||
| Any other return value
|
||||
| If a return value is not matched to any of the above and is not a simple type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
|
||||
by default, it is treated as a model attribute to be added to the model. If it is a simple type,
|
||||
it remains unresolved.
|
||||
|===
|
||||
|
||||
+1
-1
@@ -134,6 +134,6 @@ and others) and is equivalent to `required=false`.
|
||||
| Any other argument
|
||||
| If a method argument is not matched to any of the earlier values in this table and it is
|
||||
a simple type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
|
||||
it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`.
|
||||
|===
|
||||
|
||||
+1
-1
@@ -250,7 +250,7 @@ xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
||||
TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple
|
||||
value type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
|
||||
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
|
||||
|
||||
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
|
||||
|
||||
+1
-1
@@ -117,6 +117,6 @@ Kotlin::
|
||||
|
||||
Note that use of `@RequestParam` is optional (for example, to set its attributes).
|
||||
By default, any argument that is a simple value type (as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
|
||||
and is not resolved by any other argument resolver, is treated as if it were annotated
|
||||
with `@RequestParam`.
|
||||
|
||||
+1
-1
@@ -98,6 +98,6 @@ supported for all return values.
|
||||
| Other return values
|
||||
| If a return value remains unresolved in any other way, it is treated as a model
|
||||
attribute, unless it is a simple type as determined by
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
|
||||
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
|
||||
in which case it remains unresolved.
|
||||
|===
|
||||
|
||||
@@ -214,7 +214,7 @@ When multiple patterns match a URL, the best match must be selected. This is don
|
||||
one of the following depending on whether use of parsed `PathPattern` is enabled for use or not:
|
||||
|
||||
* {spring-framework-api}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`]
|
||||
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`]
|
||||
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator(java.lang.String)[`AntPathMatcher.getPatternComparator(String path)`]
|
||||
|
||||
Both help to sort patterns with more specific ones on top. A pattern is more specific if
|
||||
it has a lower count of URI variables (counted as 1), single wildcards (counted as 1),
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"@springio/antora-extensions": "1.14.7",
|
||||
"fast-xml-parser": "4.5.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration;
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
|
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter ;
|
||||
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
@@ -34,7 +34,7 @@ class AccountControllerIntegrationTests {
|
||||
|
||||
AccountControllerIntegrationTests(@Autowired WebApplicationContext wac) {
|
||||
this.mockMvc = MockMvcTester.from(wac).withHttpMessageConverters(
|
||||
List.of(wac.getBean(AbstractJackson2HttpMessageConverter.class)));
|
||||
List.of(wac.getBean(AbstractJacksonHttpMessageConverter.class)));
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
+2
-2
@@ -21,7 +21,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@@ -30,7 +30,7 @@ public class FreeMarkerConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.enableContentNegotiation(new MappingJackson2JsonView());
|
||||
registry.enableContentNegotiation(new JacksonJsonView());
|
||||
registry.freeMarker().cache(false);
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@@ -28,7 +28,7 @@ public class WebConfiguration implements WebMvcConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.enableContentNegotiation(new MappingJackson2JsonView());
|
||||
registry.enableContentNegotiation(new JacksonJsonView());
|
||||
registry.jsp();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -20,7 +20,7 @@ package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.conv
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.ApplicationWebConfiguration
|
||||
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter
|
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter
|
||||
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
|
||||
import org.springframework.test.web.servlet.assertj.MockMvcTester
|
||||
import org.springframework.web.context.WebApplicationContext
|
||||
@@ -30,7 +30,7 @@ import org.springframework.web.context.WebApplicationContext
|
||||
class AccountControllerIntegrationTests(@Autowired wac: WebApplicationContext) {
|
||||
|
||||
private val mockMvc = MockMvcTester.from(wac).withHttpMessageConverters(
|
||||
listOf(wac.getBean(AbstractJackson2HttpMessageConverter::class.java)))
|
||||
listOf(wac.getBean(AbstractJacksonHttpMessageConverter::class.java)))
|
||||
|
||||
// ...
|
||||
|
||||
|
||||
+2
-2
@@ -7,14 +7,14 @@ import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView
|
||||
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
class FreeMarkerConfiguration : WebMvcConfigurer {
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.enableContentNegotiation(MappingJackson2JsonView())
|
||||
registry.enableContentNegotiation(JacksonJsonView())
|
||||
registry.freeMarker().cache(false)
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -21,13 +21,13 @@ package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
|
||||
import org.springframework.context.annotation.Configuration
|
||||
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||
import org.springframework.web.servlet.view.json.MappingJackson2JsonView
|
||||
import org.springframework.web.servlet.view.json.JacksonJsonView
|
||||
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
class WebConfiguration : WebMvcConfigurer {
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.enableContentNegotiation(MappingJackson2JsonView())
|
||||
registry.enableContentNegotiation(JacksonJsonView())
|
||||
registry.jsp()
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
<mvc:view-resolvers>
|
||||
<mvc:content-negotiation>
|
||||
<mvc:default-views>
|
||||
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
|
||||
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
|
||||
</mvc:default-views>
|
||||
</mvc:content-negotiation>
|
||||
<mvc:freemarker cache-views="false"/>
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
<mvc:view-resolvers>
|
||||
<mvc:content-negotiation>
|
||||
<mvc:default-views>
|
||||
<bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>
|
||||
<bean class="org.springframework.web.servlet.view.json.JacksonJsonView"/>
|
||||
</mvc:default-views>
|
||||
</mvc:content-negotiation>
|
||||
<mvc:jsp/>
|
||||
|
||||
@@ -8,15 +8,15 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.20.1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.0"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.1"))
|
||||
api(platform("io.netty:netty-bom:4.2.7.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.0"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.1"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.2"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.25.1"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.3"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.25.2"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.6"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.4"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.4"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.5"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.5"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
|
||||
api(platform("org.junit:junit-bom:6.0.1"))
|
||||
@@ -33,7 +33,7 @@ dependencies {
|
||||
api("com.google.code.gson:gson:2.13.2")
|
||||
api("com.google.protobuf:protobuf-java-util:4.32.1")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.jayway.jsonpath:json-path:2.10.0")
|
||||
api("com.networknt:json-schema-validator:1.5.3")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
api("com.rometools:rome:1.19.0")
|
||||
@@ -111,7 +111,7 @@ dependencies {
|
||||
api("org.dom4j:dom4j:2.2.0")
|
||||
api("org.easymock:easymock:5.6.0")
|
||||
api("org.eclipse.angus:angus-mail:2.0.3")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.4")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B11")
|
||||
api("org.eclipse:yasson:3.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -121,7 +121,7 @@ dependencies {
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.graalvm.sdk:graal-sdk:22.3.1")
|
||||
api("org.hamcrest:hamcrest:3.0")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.0.CR2")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.0.CR3")
|
||||
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.18.0")
|
||||
@@ -129,7 +129,7 @@ dependencies {
|
||||
api("org.jboss.logging:jboss-logging:3.6.1.Final")
|
||||
api("org.jruby:jruby:10.0.2.0")
|
||||
api("org.jspecify:jspecify:1.0.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.junit.support:testng-engine:1.1.0")
|
||||
api("org.mozilla:rhino:1.7.15")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
api("org.python:jython-standalone:2.7.4")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=7.0.0
|
||||
version=7.0.2
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -54,4 +54,4 @@ _When instructed to execute `./gradlew` from the command line, be sure to execut
|
||||
|
||||
In any case, please do not check in your own generated `.classpath` file, `.project`
|
||||
file, or `.settings` folder. You'll notice these files are already intentionally in
|
||||
`.gitignore`. The same policy holds for IDEA metadata.
|
||||
`.gitignore`. The same policy holds for IntelliJ IDEA metadata.
|
||||
|
||||
+2
-3
@@ -48,7 +48,6 @@ import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.aop.support.DefaultPointcutAdvisor;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.core.convert.converter.ConvertingComparator;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -84,10 +83,10 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
|
||||
// @AfterThrowing methods due to the fact that AspectJAfterAdvice.invoke(MethodInvocation)
|
||||
// invokes proceed() in a `try` block and only invokes the @After advice method
|
||||
// in a corresponding `finally` block.
|
||||
Comparator<Method> adviceKindComparator = new ConvertingComparator<>(
|
||||
Comparator<Method> adviceKindComparator = new ConvertingComparator<Method, @Nullable Annotation>(
|
||||
new InstanceComparator<>(
|
||||
Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
|
||||
(Converter<Method, Annotation>) method -> {
|
||||
method -> {
|
||||
AspectJAnnotation ann = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
|
||||
return (ann != null ? ann.getAnnotation() : null);
|
||||
});
|
||||
|
||||
+11
-7
@@ -44,7 +44,6 @@ import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor;
|
||||
@@ -296,10 +295,8 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
||||
|
||||
/**
|
||||
* Build a cache key for the given bean class and bean name.
|
||||
* <p>Note: As of 4.2.3, this implementation does not return a concatenated
|
||||
* class/name String anymore but rather the most efficient cache key possible:
|
||||
* a plain bean name, prepended with {@link BeanFactory#FACTORY_BEAN_PREFIX}
|
||||
* in case of a {@code FactoryBean}; or if no bean name specified, then the
|
||||
* <p>Note: As of 7.0.2, this implementation returns a composed cache key
|
||||
* for bean class plus bean name; or if no bean name specified, then the
|
||||
* given bean {@code Class} as-is.
|
||||
* @param beanClass the bean class
|
||||
* @param beanName the bean name
|
||||
@@ -307,8 +304,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
||||
*/
|
||||
protected Object getCacheKey(Class<?> beanClass, @Nullable String beanName) {
|
||||
if (StringUtils.hasLength(beanName)) {
|
||||
return (FactoryBean.class.isAssignableFrom(beanClass) ?
|
||||
BeanFactory.FACTORY_BEAN_PREFIX + beanName : beanName);
|
||||
return new ComposedCacheKey(beanClass, beanName);
|
||||
}
|
||||
else {
|
||||
return beanClass;
|
||||
@@ -615,4 +611,12 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
|
||||
protected abstract Object @Nullable [] getAdvicesAndAdvisorsForBean(Class<?> beanClass, String beanName,
|
||||
@Nullable TargetSource customTargetSource) throws BeansException;
|
||||
|
||||
|
||||
/**
|
||||
* Composed cache key for bean class plus bean name.
|
||||
* @see #getCacheKey(Class, String)
|
||||
*/
|
||||
private record ComposedCacheKey(Class<?> beanClass, String beanName) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-3
@@ -365,9 +365,12 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
|
||||
}
|
||||
MethodParameter methodParam = descriptor.getMethodParameter();
|
||||
if (methodParam != null) {
|
||||
for (Annotation annotation : methodParam.getMethodAnnotations()) {
|
||||
if (isQualifier(annotation.annotationType())) {
|
||||
return true;
|
||||
Method method = methodParam.getMethod();
|
||||
if (method == null || void.class == method.getReturnType()) {
|
||||
for (Annotation annotation : methodParam.getMethodAnnotations()) {
|
||||
if (isQualifier(annotation.annotationType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -203,8 +203,8 @@ public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
|
||||
public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
|
||||
if (value instanceof RuntimeBeanReference runtimeBeanReference &&
|
||||
runtimeBeanReference.getBeanType() != null) {
|
||||
return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class,
|
||||
runtimeBeanReference.getBeanType());
|
||||
return CodeBlock.of("new $T($S, $T.class)", RuntimeBeanReference.class,
|
||||
runtimeBeanReference.getBeanName(), runtimeBeanReference.getBeanType());
|
||||
}
|
||||
else if (value instanceof BeanReference beanReference) {
|
||||
return CodeBlock.of("new $T($S)", RuntimeBeanReference.class,
|
||||
|
||||
+9
@@ -18,6 +18,7 @@ package org.springframework.beans.factory.aot;
|
||||
|
||||
import org.springframework.aot.generate.GeneratedMethods;
|
||||
import org.springframework.aot.generate.MethodReference;
|
||||
import org.springframework.javapoet.ClassName;
|
||||
|
||||
/**
|
||||
* Interface that can be used to configure the code that will be generated to
|
||||
@@ -25,6 +26,7 @@ import org.springframework.aot.generate.MethodReference;
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Stephane Nicoll
|
||||
* @author Sebastien Deleuze
|
||||
* @since 6.0
|
||||
* @see BeanFactoryInitializationAotContribution
|
||||
*/
|
||||
@@ -41,6 +43,13 @@ public interface BeanFactoryInitializationCode {
|
||||
*/
|
||||
GeneratedMethods getMethods();
|
||||
|
||||
/**
|
||||
* Return the name of the class used by the initializing code.
|
||||
* @return the generated class name
|
||||
* @since 7.0.2
|
||||
*/
|
||||
ClassName getClassName();
|
||||
|
||||
/**
|
||||
* Add an initializer method call. An initializer can use a flexible signature,
|
||||
* using any of the following:
|
||||
|
||||
+1
-1
@@ -384,7 +384,7 @@ public class InstanceSupplierCodeGenerator {
|
||||
Visibility visibility = AccessControl.lowest(classAccessControl, memberAccessControl).getVisibility();
|
||||
return (visibility == Visibility.PUBLIC || (visibility != Visibility.PRIVATE &&
|
||||
member.getDeclaringClass().getPackageName().equals(this.className.packageName())));
|
||||
}
|
||||
}
|
||||
|
||||
private CodeBlock generateParameterTypesCode(Class<?>[] parameterTypes) {
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
|
||||
-1
@@ -33,7 +33,6 @@ import org.springframework.util.StringUtils;
|
||||
* @author Juergen Hoeller
|
||||
* @author Rob Harrop
|
||||
* @since 1.1
|
||||
* @see PropertiesBeanDefinitionReader
|
||||
* @see org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader
|
||||
*/
|
||||
public abstract class BeanDefinitionReaderUtils {
|
||||
|
||||
-1
@@ -43,7 +43,6 @@ import org.springframework.core.AliasRegistry;
|
||||
* @see DefaultListableBeanFactory
|
||||
* @see org.springframework.context.support.GenericApplicationContext
|
||||
* @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
|
||||
* @see PropertiesBeanDefinitionReader
|
||||
*/
|
||||
public interface BeanDefinitionRegistry extends AliasRegistry {
|
||||
|
||||
|
||||
+49
-18
@@ -1632,7 +1632,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
|
||||
descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
|
||||
if (Optional.class == descriptor.getDependencyType()) {
|
||||
return createOptionalDependency(descriptor, requestingBeanName);
|
||||
return createOptionalDependency(descriptor, requestingBeanName, autowiredBeanNames, null);
|
||||
}
|
||||
else if (ObjectFactory.class == descriptor.getDependencyType() ||
|
||||
ObjectProvider.class == descriptor.getDependencyType()) {
|
||||
@@ -2330,8 +2330,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
/**
|
||||
* Create an {@link Optional} wrapper for the specified dependency.
|
||||
*/
|
||||
private Optional<?> createOptionalDependency(
|
||||
DependencyDescriptor descriptor, @Nullable String beanName, final @Nullable Object... args) {
|
||||
private Optional<?> createOptionalDependency(DependencyDescriptor descriptor, @Nullable String beanName,
|
||||
@Nullable Set<String> autowiredBeanNames, @Nullable Object @Nullable [] args) {
|
||||
|
||||
DependencyDescriptor descriptorToUse = new NestedDependencyDescriptor(descriptor) {
|
||||
@Override
|
||||
@@ -2348,7 +2348,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
return ObjectUtils.isEmpty(args);
|
||||
}
|
||||
};
|
||||
Object result = doResolveDependency(descriptorToUse, beanName, null, null);
|
||||
Object result = doResolveDependency(descriptorToUse, beanName, autowiredBeanNames, null);
|
||||
return (result instanceof Optional<?> optional ? optional : Optional.ofNullable(result));
|
||||
}
|
||||
|
||||
@@ -2501,12 +2501,18 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
*/
|
||||
private class DependencyObjectProvider implements BeanObjectProvider<Object> {
|
||||
|
||||
private static final Object NOT_CACHEABLE = new Object();
|
||||
|
||||
private static final Object NULL_VALUE = new Object();
|
||||
|
||||
private final DependencyDescriptor descriptor;
|
||||
|
||||
private final boolean optional;
|
||||
|
||||
private final @Nullable String beanName;
|
||||
|
||||
private transient volatile @Nullable Object cachedValue;
|
||||
|
||||
public DependencyObjectProvider(DependencyDescriptor descriptor, @Nullable String beanName) {
|
||||
this.descriptor = new NestedDependencyDescriptor(descriptor);
|
||||
this.optional = (this.descriptor.getDependencyType() == Optional.class);
|
||||
@@ -2515,22 +2521,17 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
|
||||
@Override
|
||||
public Object getObject() throws BeansException {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName);
|
||||
}
|
||||
else {
|
||||
Object result = doResolveDependency(this.descriptor, this.beanName, null, null);
|
||||
if (result == null) {
|
||||
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType());
|
||||
}
|
||||
return result;
|
||||
Object result = getValue();
|
||||
if (result == null) {
|
||||
throw new NoSuchBeanDefinitionException(this.descriptor.getResolvableType());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getObject(final @Nullable Object... args) throws BeansException {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName, args);
|
||||
return createOptionalDependency(this.descriptor, this.beanName, null, args);
|
||||
}
|
||||
else {
|
||||
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
|
||||
@@ -2551,7 +2552,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
public @Nullable Object getIfAvailable() throws BeansException {
|
||||
try {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName);
|
||||
return createOptionalDependency(this.descriptor, this.beanName, null, null);
|
||||
}
|
||||
else {
|
||||
DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) {
|
||||
@@ -2604,7 +2605,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
};
|
||||
try {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(descriptorToUse, this.beanName);
|
||||
return createOptionalDependency(descriptorToUse, this.beanName, null, null);
|
||||
}
|
||||
else {
|
||||
return doResolveDependency(descriptorToUse, this.beanName, null, null);
|
||||
@@ -2630,11 +2631,41 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
|
||||
protected @Nullable Object getValue() throws BeansException {
|
||||
Object value = this.cachedValue;
|
||||
if (value == null) {
|
||||
if (isConfigurationFrozen()) {
|
||||
Set<String> autowiredBeanNames = new LinkedHashSet<>(2);
|
||||
value = resolveValue(autowiredBeanNames);
|
||||
boolean cacheable = false;
|
||||
if (!autowiredBeanNames.isEmpty()) {
|
||||
cacheable = true;
|
||||
for (String autowiredBeanName : autowiredBeanNames) {
|
||||
if (!containsBean(autowiredBeanName) || !isSingleton(autowiredBeanName)) {
|
||||
cacheable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
this.cachedValue = (cacheable ? (value != null ? value : NULL_VALUE) : NOT_CACHEABLE);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
else if (value == NULL_VALUE) {
|
||||
return null;
|
||||
}
|
||||
else if (value != NOT_CACHEABLE) {
|
||||
return value;
|
||||
}
|
||||
|
||||
// Not cacheable -> fresh resolution.
|
||||
return resolveValue(null);
|
||||
}
|
||||
|
||||
private @Nullable Object resolveValue(@Nullable Set<String> autowiredBeanNames) {
|
||||
if (this.optional) {
|
||||
return createOptionalDependency(this.descriptor, this.beanName);
|
||||
return createOptionalDependency(this.descriptor, this.beanName, autowiredBeanNames, null);
|
||||
}
|
||||
else {
|
||||
return doResolveDependency(this.descriptor, this.beanName, null, null);
|
||||
return doResolveDependency(this.descriptor, this.beanName, autowiredBeanNames, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-4
@@ -532,7 +532,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
|
||||
/**
|
||||
* Callback before singleton creation.
|
||||
* <p>The default implementation register the singleton as currently in creation.
|
||||
* <p>The default implementation registers the singleton as currently in creation.
|
||||
* @param beanName the name of the singleton about to be created
|
||||
* @see #isSingletonCurrentlyInCreation
|
||||
*/
|
||||
@@ -582,7 +582,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
public void registerContainedBean(String containedBeanName, String containingBeanName) {
|
||||
synchronized (this.containedBeanMap) {
|
||||
Set<String> containedBeans =
|
||||
this.containedBeanMap.computeIfAbsent(containingBeanName, k -> new LinkedHashSet<>(8));
|
||||
this.containedBeanMap.computeIfAbsent(containingBeanName, key -> new LinkedHashSet<>(8));
|
||||
if (!containedBeans.add(containedBeanName)) {
|
||||
return;
|
||||
}
|
||||
@@ -601,7 +601,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
|
||||
synchronized (this.dependentBeanMap) {
|
||||
Set<String> dependentBeans =
|
||||
this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8));
|
||||
this.dependentBeanMap.computeIfAbsent(canonicalName, key -> new LinkedHashSet<>(8));
|
||||
if (!dependentBeans.add(dependentBeanName)) {
|
||||
return;
|
||||
}
|
||||
@@ -609,7 +609,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
|
||||
synchronized (this.dependenciesForBeanMap) {
|
||||
Set<String> dependenciesForBean =
|
||||
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
|
||||
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, key -> new LinkedHashSet<>(8));
|
||||
dependenciesForBean.add(canonicalName);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ import org.springframework.util.StringUtils;
|
||||
* @since 26.11.2003
|
||||
* @see DefaultListableBeanFactory
|
||||
* @deprecated in favor of Spring's common bean definition formats and/or
|
||||
* custom reader implementations
|
||||
* custom BeanDefinitionReader implementations
|
||||
*/
|
||||
@Deprecated(since = "5.3")
|
||||
public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader {
|
||||
|
||||
+30
-2
@@ -1587,6 +1587,18 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void objectFactoryFieldInjectionAgainstFrozen() {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryFieldInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1596,6 +1608,18 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryConstructorInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryConstructorInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void objectFactoryConstructorInjectionAgainstFrozen() {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryConstructorInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryConstructorInjectionBean bean = bf.getBean("annotatedBean", ObjectFactoryConstructorInjectionBean.class);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2116,8 +2140,8 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
|
||||
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
|
||||
|
||||
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
|
||||
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
|
||||
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
|
||||
@@ -2728,9 +2752,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
bf.registerSingleton("nonNullBean", "Test");
|
||||
bf.registerBeanDefinition("mixedNullableInjectionBean",
|
||||
new RootBeanDefinition(MixedNullableInjectionBean.class));
|
||||
|
||||
MixedNullableInjectionBean mixedNullableInjectionBean = bf.getBean(MixedNullableInjectionBean.class);
|
||||
assertThat(mixedNullableInjectionBean.nonNullBean).isNotNull();
|
||||
assertThat(mixedNullableInjectionBean.nullableBean).isNull();
|
||||
assertThat(bf.getDependentBeans("nonNullBean")).contains("mixedNullableInjectionBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -2738,9 +2764,11 @@ class AutowiredAnnotationBeanPostProcessorTests {
|
||||
bf.registerSingleton("nonNullBean", "Test");
|
||||
bf.registerBeanDefinition("mixedOptionalInjectionBean",
|
||||
new RootBeanDefinition(MixedOptionalInjectionBean.class));
|
||||
|
||||
MixedOptionalInjectionBean mixedOptionalInjectionBean = bf.getBean(MixedOptionalInjectionBean.class);
|
||||
assertThat(mixedOptionalInjectionBean.nonNullBean).isNotNull();
|
||||
assertThat(mixedOptionalInjectionBean.nullableBean).isNull();
|
||||
assertThat(bf.getDependentBeans("nonNullBean")).contains("mixedOptionalInjectionBean");
|
||||
}
|
||||
|
||||
|
||||
|
||||
+44
-3
@@ -370,8 +370,25 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testObjectFactoryWithBeanFieldAgainstFrozen() throws Exception {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryFieldInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.setSerializationId("test");
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryFieldInjectionBean bean = (ObjectFactoryFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -382,8 +399,25 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
ObjectFactoryMethodInjectionBean bean = (ObjectFactoryMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testObjectFactoryWithBeanMethodAgainstFrozen() throws Exception {
|
||||
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectFactoryMethodInjectionBean.class));
|
||||
bf.registerBeanDefinition("testBean", new RootBeanDefinition(TestBean.class));
|
||||
bf.setSerializationId("test");
|
||||
bf.freezeConfiguration();
|
||||
|
||||
ObjectFactoryMethodInjectionBean bean = (ObjectFactoryMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
bean = SerializationTestUtils.serializeAndDeserialize(bean);
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -444,8 +478,8 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
|
||||
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
|
||||
|
||||
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
|
||||
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
|
||||
|
||||
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
|
||||
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
|
||||
@@ -459,6 +493,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
NullableFieldInjectionBean bean = (NullableFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -476,6 +511,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
NullableMethodInjectionBean bean = (NullableMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -494,6 +530,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalFieldInjectionBean bean = (OptionalFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -512,6 +549,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalMethodInjectionBean bean = (OptionalMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -530,6 +568,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalListFieldInjectionBean bean = (OptionalListFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).hasValueSatisfying(list ->
|
||||
assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class)));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -548,6 +587,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
OptionalListMethodInjectionBean bean = (OptionalListMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).hasValueSatisfying(list ->
|
||||
assertThat(list).containsExactly(bf.getBean("testBean", TestBean.class)));
|
||||
assertThat(bf.getDependentBeans("testBean")).contains("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -566,6 +606,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
ProviderOfOptionalFieldInjectionBean bean = (ProviderOfOptionalFieldInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).doesNotContain("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -584,6 +625,7 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
ProviderOfOptionalMethodInjectionBean bean = (ProviderOfOptionalMethodInjectionBean) bf.getBean("annotatedBean");
|
||||
assertThat(bean.getTestBean()).isPresent();
|
||||
assertThat(bean.getTestBean().get()).isSameAs(bf.getBean("testBean"));
|
||||
assertThat(bf.getDependentBeans("testBean")).doesNotContain("annotatedBean");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -788,7 +830,6 @@ class InjectAnnotationBeanPostProcessorTests {
|
||||
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
|
||||
public ConstructorResourceInjectionBean() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
+21
-9
@@ -458,30 +458,42 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
|
||||
class BeanReferenceTests {
|
||||
|
||||
@Test
|
||||
void generatedWhenBeanNameReference() {
|
||||
RuntimeBeanNameReference beanReference = new RuntimeBeanNameReference("test");
|
||||
void generatedWhenRuntimeBeanNameReference() {
|
||||
BeanReference beanReference = new RuntimeBeanNameReference("test");
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
|
||||
assertThat(actual.getBeanType()).as("type").isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedWhenBeanReferenceByName() {
|
||||
RuntimeBeanReference beanReference = new RuntimeBeanReference("test");
|
||||
void generatedWhenRuntimeBeanReferenceByName() {
|
||||
BeanReference beanReference = new RuntimeBeanReference("test");
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanName()).isEqualTo(beanReference.getBeanName());
|
||||
assertThat(actual.getBeanType()).isEqualTo(beanReference.getBeanType());
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
|
||||
assertThat(actual.getBeanType()).as("type").isNull();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void generatedWhenBeanReferenceByType() {
|
||||
void generatedWhenRuntimeBeanReferenceByType() {
|
||||
BeanReference beanReference = new RuntimeBeanReference(String.class);
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanType()).isEqualTo(String.class);
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo(String.class.getName());
|
||||
assertThat(actual.getBeanType()).as("type").isEqualTo(String.class);
|
||||
});
|
||||
}
|
||||
|
||||
@Test // gh-35913
|
||||
void generatedWhenRuntimeBeanReferenceByNameAndType() {
|
||||
BeanReference beanReference = new RuntimeBeanReference("test", String.class);
|
||||
compile(beanReference, (instance, compiler) -> {
|
||||
RuntimeBeanReference actual = (RuntimeBeanReference) instance;
|
||||
assertThat(actual.getBeanName()).as("name").isEqualTo("test");
|
||||
assertThat(actual.getBeanType()).as("type").isEqualTo(String.class);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -46,14 +46,14 @@ class YamlMapFactoryBeanTests {
|
||||
@Test
|
||||
void testSetIgnoreResourceNotFound() {
|
||||
this.factory.setResolutionMethod(YamlMapFactoryBean.ResolutionMethod.OVERRIDE_AND_IGNORE);
|
||||
this.factory.setResources(new FileSystemResource("non-exsitent-file.yml"));
|
||||
this.factory.setResources(new FileSystemResource("non-existent-file.yml"));
|
||||
assertThat(this.factory.getObject()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetBarfOnResourceNotFound() {
|
||||
assertThatIllegalStateException().isThrownBy(() -> {
|
||||
this.factory.setResources(new FileSystemResource("non-exsitent-file.yml"));
|
||||
this.factory.setResources(new FileSystemResource("non-existent-file.yml"));
|
||||
this.factory.getObject().size();
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -51,7 +51,7 @@ class MetadataAttachmentTests {
|
||||
void metadataIsInherited() {
|
||||
BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition("testBean2");
|
||||
assertThat(beanDefinition.getAttribute("foo")).as("Metadata not inherited").isEqualTo("bar");
|
||||
assertThat(beanDefinition.getAttribute("abc")).as("Child metdata not attached").isEqualTo("123");
|
||||
assertThat(beanDefinition.getAttribute("abc")).as("Child metadata not attached").isEqualTo("123");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Vendored
+38
-20
@@ -76,7 +76,7 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
|
||||
private boolean allowNullValues = true;
|
||||
|
||||
private boolean dynamic = true;
|
||||
private volatile boolean dynamic = true;
|
||||
|
||||
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
|
||||
|
||||
@@ -101,10 +101,15 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
|
||||
/**
|
||||
* Specify the set of cache names for this CacheManager's 'static' mode.
|
||||
* <p>The number of caches and their names will be fixed after a call to this method,
|
||||
* with no creation of further cache regions at runtime.
|
||||
* <p>Calling this with a {@code null} collection argument resets the
|
||||
* mode to 'dynamic', allowing for further creation of caches again.
|
||||
* <p>The number of caches and their names will be fixed after a call
|
||||
* to this method, with no creation of further cache regions at runtime.
|
||||
* <p>Note that this method replaces existing caches of the given names
|
||||
* and prevents the creation of further cache regions from here on - but
|
||||
* does <i>not</i> remove unrelated existing caches. For a full reset,
|
||||
* consider calling {@link #resetCaches()} before calling this method.
|
||||
* <p>Calling this method with a {@code null} collection argument resets
|
||||
* the mode to 'dynamic', allowing for further creation of caches again.
|
||||
* @see #resetCaches()
|
||||
*/
|
||||
public void setCacheNames(@Nullable Collection<String> cacheNames) {
|
||||
if (cacheNames != null) {
|
||||
@@ -244,11 +249,6 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
return Collections.unmodifiableSet(this.cacheMap.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Cache getCache(String name) {
|
||||
Cache cache = this.cacheMap.get(name);
|
||||
@@ -258,6 +258,34 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
return cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
return Collections.unmodifiableSet(this.cacheMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this cache manager's caches, removing them completely for on-demand
|
||||
* 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) {
|
||||
this.cacheMap.keySet().retainAll(this.customCacheNames);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified cache from this cache manager, applying to
|
||||
* custom caches as well as dynamically registered caches at runtime.
|
||||
* @param name the name of the cache
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public void removeCache(String name) {
|
||||
this.customCacheNames.remove(name);
|
||||
this.cacheMap.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the given native Caffeine Cache instance with this cache manager,
|
||||
@@ -301,16 +329,6 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
this.cacheMap.put(name, adaptCaffeineCache(name, cache));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified cache from this cache manager, applying to
|
||||
* custom caches as well as dynamically registered caches at runtime.
|
||||
* @param name the name of the cache
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public void removeCache(String name) {
|
||||
this.customCacheNames.remove(name);
|
||||
this.cacheMap.remove(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the given new native Caffeine Cache instance to Spring's {@link Cache}
|
||||
|
||||
Vendored
+13
@@ -130,4 +130,17 @@ public class JCacheCacheManager extends AbstractTransactionSupportingCacheManage
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
CacheManager cacheManager = getCacheManager();
|
||||
if (cacheManager != null && !cacheManager.isClosed()) {
|
||||
for (String cacheName : cacheManager.getCacheNames()) {
|
||||
javax.cache.Cache<Object, Object> jcache = cacheManager.getCache(cacheName);
|
||||
if (jcache != null && !jcache.isClosed()) {
|
||||
jcache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-3
@@ -26,7 +26,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.Aware;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.core.MethodClassKey;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
@@ -95,8 +95,8 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe
|
||||
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
|
||||
return null;
|
||||
}
|
||||
// Skip methods declared on BeanFactoryAware and co.
|
||||
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
|
||||
// Skip setBeanFactory method on BeanFactoryAware.
|
||||
if (method.getDeclaringClass() == BeanFactoryAware.class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+22
-2
@@ -24,7 +24,6 @@ import com.github.benmanes.caffeine.cache.CaffeineSpec;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.support.SimpleValueWrapper;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -42,7 +41,7 @@ class CaffeineCacheManagerTests {
|
||||
@Test
|
||||
@SuppressWarnings("cast")
|
||||
void dynamicMode() {
|
||||
CacheManager cm = new CaffeineCacheManager();
|
||||
CaffeineCacheManager cm = new CaffeineCacheManager();
|
||||
|
||||
Cache cache1 = cm.getCache("c1");
|
||||
assertThat(cache1).isInstanceOf(CaffeineCache.class);
|
||||
@@ -76,6 +75,14 @@ class CaffeineCacheManagerTests {
|
||||
cache1.evict("key3");
|
||||
assertThat(cache1.get("key3", () -> (String) null)).isNull();
|
||||
assertThat(cache1.get("key3", () -> (String) null)).isNull();
|
||||
|
||||
cm.removeCache("c1");
|
||||
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
|
||||
assertThat(cm.getCache("c2")).isSameAs(cache2);
|
||||
|
||||
cm.resetCaches();
|
||||
assertThat(cm.getCache("c1")).isNotSameAs(cache1);
|
||||
assertThat(cm.getCache("c2")).isNotSameAs(cache2);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,11 +138,24 @@ class CaffeineCacheManagerTests {
|
||||
|
||||
cm.setAllowNullValues(true);
|
||||
Cache cache1y = cm.getCache("c1");
|
||||
Cache cache2y = cm.getCache("c2");
|
||||
|
||||
cache1y.put("key3", null);
|
||||
assertThat(cache1y.get("key3").get()).isNull();
|
||||
cache1y.evict("key3");
|
||||
assertThat(cache1y.get("key3")).isNull();
|
||||
cache2y.put("key4", "value4");
|
||||
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
|
||||
|
||||
cm.removeCache("c1");
|
||||
assertThat(cm.getCache("c1")).isNull();
|
||||
assertThat(cm.getCache("c2")).isSameAs(cache2y);
|
||||
assertThat(cache2y.get("key4").get()).isEqualTo("value4");
|
||||
|
||||
cm.resetCaches();
|
||||
assertThat(cm.getCache("c1")).isNull();
|
||||
assertThat(cm.getCache("c2")).isSameAs(cache2y);
|
||||
assertThat(cache2y.get("key4")).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Vendored
+15
-2
@@ -51,8 +51,7 @@ class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests<JCacheCache>
|
||||
this.cacheManager.createCache(CACHE_NAME_NO_NULL, new MutableConfiguration<>());
|
||||
this.nativeCache = this.cacheManager.getCache(CACHE_NAME);
|
||||
this.cache = new JCacheCache(this.nativeCache);
|
||||
Cache<Object, Object> nativeCacheNoNull =
|
||||
this.cacheManager.getCache(CACHE_NAME_NO_NULL);
|
||||
Cache<Object, Object> nativeCacheNoNull = this.cacheManager.getCache(CACHE_NAME_NO_NULL);
|
||||
this.cacheNoNull = new JCacheCache(nativeCacheNoNull, false);
|
||||
}
|
||||
|
||||
@@ -100,4 +99,18 @@ class JCacheEhCacheApiTests extends AbstractValueAdaptingCacheTests<JCacheCache>
|
||||
assertThat(cache.get(key).get()).isEqualTo(value);
|
||||
}
|
||||
|
||||
@Test
|
||||
void resetCaches() {
|
||||
JCacheCacheManager cm = new JCacheCacheManager(cacheManager);
|
||||
org.springframework.cache.Cache cache = cm.getCache(CACHE_NAME);
|
||||
cache.put("key", "value");
|
||||
assertThat(cm.getCacheNames()).contains(CACHE_NAME);
|
||||
assertThat(cm.getCache(CACHE_NAME)).isNotNull().isSameAs(cache);
|
||||
assertThat(cacheManager.getCache(CACHE_NAME).iterator()).hasNext();
|
||||
cm.resetCaches();
|
||||
assertThat(cm.getCacheNames()).contains(CACHE_NAME);
|
||||
assertThat(cm.getCache(CACHE_NAME)).isNotNull().isSameAs(cache);
|
||||
assertThat(cacheManager.getCache(CACHE_NAME).iterator()).isExhausted();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.jspecify.annotations.Nullable;
|
||||
*
|
||||
* @author Costin Leau
|
||||
* @author Sam Brannen
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
*/
|
||||
public interface CacheManager {
|
||||
@@ -47,4 +48,31 @@ public interface CacheManager {
|
||||
*/
|
||||
Collection<String> getCacheNames();
|
||||
|
||||
/**
|
||||
* Remove all registered caches from this cache manager if possible,
|
||||
* re-creating them on demand. After this call, {@link #getCacheNames()}
|
||||
* will possibly be empty and the cache provider will have dropped all
|
||||
* cache management state.
|
||||
* <p>Alternatively, an implementation may perform an equivalent reset
|
||||
* on fixed existing cache regions without actually dropping the cache.
|
||||
* This behavior will be indicated by {@link #getCacheNames()} still
|
||||
* exposing a non-empty set of names, whereas the corresponding cache
|
||||
* regions will not contain cache entries anymore.
|
||||
* <p>The default implementation calls {@link Cache#clear} on all
|
||||
* registered caches, retaining all caches as registered, satisfying
|
||||
* the alternative implementation path above. Custom implementations
|
||||
* may either drop the actual caches (re-creating them on demand) or
|
||||
* perform a more exhaustive reset at the actual cache provider level.
|
||||
* @since 7.0.2
|
||||
* @see Cache#clear()
|
||||
*/
|
||||
default void resetCaches() {
|
||||
for (String cacheName : getCacheNames()) {
|
||||
Cache cache = getCache(cacheName);
|
||||
if (cache != null) {
|
||||
cache.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -177,6 +177,11 @@ public @interface EnableCaching {
|
||||
* be upgraded to subclass proxying at the same time. This approach has no negative
|
||||
* impact in practice unless one is explicitly expecting one type of proxy vs another,
|
||||
* for example, in tests.
|
||||
* <p>It is usually recommendable to rely on a global default proxy configuration
|
||||
* instead, with specific proxy requirements for certain beans expressed through
|
||||
* a {@link org.springframework.context.annotation.Proxyable} annotation on
|
||||
* the affected bean classes.
|
||||
* @see org.springframework.aop.config.AopConfigUtils#forceAutoProxyCreatorToUseClassProxying
|
||||
*/
|
||||
boolean proxyTargetClass() default false;
|
||||
|
||||
|
||||
Vendored
+28
-10
@@ -55,7 +55,7 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
|
||||
|
||||
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
|
||||
|
||||
private boolean dynamic = true;
|
||||
private volatile boolean dynamic = true;
|
||||
|
||||
private boolean allowNullValues = true;
|
||||
|
||||
@@ -82,10 +82,15 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
|
||||
|
||||
/**
|
||||
* Specify the set of cache names for this CacheManager's 'static' mode.
|
||||
* <p>The number of caches and their names will be fixed after a call to this method,
|
||||
* with no creation of further cache regions at runtime.
|
||||
* <p>Calling this with a {@code null} collection argument resets the
|
||||
* mode to 'dynamic', allowing for further creation of caches again.
|
||||
* <p>The number of caches and their names will be fixed after a call
|
||||
* to this method, with no creation of further cache regions at runtime.
|
||||
* <p>Note that this method replaces existing caches of the given names
|
||||
* and prevents the creation of further cache regions from here on - but
|
||||
* does <i>not</i> remove unrelated existing caches. For a full reset,
|
||||
* consider calling {@link #resetCaches()} before calling this method.
|
||||
* <p>Calling this method with a {@code null} collection argument resets
|
||||
* the mode to 'dynamic', allowing for further creation of caches again.
|
||||
* @see #resetCaches()
|
||||
*/
|
||||
public void setCacheNames(@Nullable Collection<String> cacheNames) {
|
||||
if (cacheNames != null) {
|
||||
@@ -160,11 +165,6 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
return Collections.unmodifiableSet(this.cacheMap.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Cache getCache(String name) {
|
||||
Cache cache = this.cacheMap.get(name);
|
||||
@@ -174,6 +174,24 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA
|
||||
return cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
return Collections.unmodifiableSet(this.cacheMap.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset this cache manager's caches, removing them completely for on-demand
|
||||
* 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) {
|
||||
this.cacheMap.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified cache from this cache manager.
|
||||
* @param name the name of the cache
|
||||
|
||||
+3
-3
@@ -113,7 +113,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
|
||||
builder.setUnless(getAttributeValue(opElement, "unless", ""));
|
||||
builder.setSync(Boolean.parseBoolean(getAttributeValue(opElement, "sync", "false")));
|
||||
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
|
||||
col.add(builder.build());
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
|
||||
builder.setBeforeInvocation(Boolean.parseBoolean(after.trim()));
|
||||
}
|
||||
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
|
||||
col.add(builder.build());
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ class CacheAdviceParser extends AbstractSingleBeanDefinitionParser {
|
||||
parserContext.getReaderContext(), new CachePutOperation.Builder());
|
||||
builder.setUnless(getAttributeValue(opElement, "unless", ""));
|
||||
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, k -> new ArrayList<>(2));
|
||||
Collection<CacheOperation> col = cacheOpMap.computeIfAbsent(nameHolder, key -> new ArrayList<>(2));
|
||||
col.add(builder.build());
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -28,7 +28,7 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.beans.factory.Aware;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.core.MethodClassKey;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
@@ -137,8 +137,8 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera
|
||||
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
|
||||
return null;
|
||||
}
|
||||
// Skip methods declared on BeanFactoryAware and co.
|
||||
if (method.getDeclaringClass().isInterface() && Aware.class.isAssignableFrom(method.getDeclaringClass())) {
|
||||
// Skip setBeanFactory method on BeanFactoryAware.
|
||||
if (method.getDeclaringClass() == BeanFactoryAware.class) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
+7
@@ -115,6 +115,13 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing
|
||||
return this.cacheNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
synchronized (this.cacheMap) {
|
||||
this.cacheMap.values().forEach(Cache::clear);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Common cache initialization delegates for subclasses
|
||||
|
||||
|
||||
+7
@@ -119,4 +119,11 @@ public class CompositeCacheManager implements CacheManager, InitializingBean {
|
||||
return Collections.unmodifiableSet(names);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
for (CacheManager manager : this.cacheManagers) {
|
||||
manager.resetCaches();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-24
@@ -18,8 +18,6 @@ package org.springframework.cache.support;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
@@ -33,44 +31,33 @@ import org.springframework.cache.CacheManager;
|
||||
* for disabling caching, typically used for backing cache declarations
|
||||
* without an actual backing store.
|
||||
*
|
||||
* <p>Will simply accept any items into the cache not actually storing them.
|
||||
* <p>This implementation will simply accept any items into the cache,
|
||||
* not actually storing them.
|
||||
*
|
||||
* @author Costin Leau
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
* @see NoOpCache
|
||||
*/
|
||||
public class NoOpCacheManager implements CacheManager {
|
||||
|
||||
private final ConcurrentMap<String, Cache> caches = new ConcurrentHashMap<>(16);
|
||||
|
||||
private final Set<String> cacheNames = new LinkedHashSet<>(16);
|
||||
private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
|
||||
|
||||
|
||||
/**
|
||||
* This implementation always returns a {@link Cache} implementation that will not store items.
|
||||
* Additionally, the request cache will be remembered by the manager for consistency.
|
||||
*/
|
||||
@Override
|
||||
public @Nullable Cache getCache(String name) {
|
||||
Cache cache = this.caches.get(name);
|
||||
if (cache == null) {
|
||||
this.caches.computeIfAbsent(name, NoOpCache::new);
|
||||
synchronized (this.cacheNames) {
|
||||
this.cacheNames.add(name);
|
||||
}
|
||||
}
|
||||
return this.caches.get(name);
|
||||
return this.cacheMap.computeIfAbsent(name, NoOpCache::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* This implementation returns the name of the caches previously requested.
|
||||
*/
|
||||
@Override
|
||||
public Collection<String> getCacheNames() {
|
||||
synchronized (this.cacheNames) {
|
||||
return Collections.unmodifiableSet(this.cacheNames);
|
||||
}
|
||||
return Collections.unmodifiableSet(this.cacheMap.keySet());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetCaches() {
|
||||
this.cacheMap.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -61,9 +61,9 @@ public interface ApplicationContext extends EnvironmentCapable, ListableBeanFact
|
||||
|
||||
/**
|
||||
* Return the unique id of this application context.
|
||||
* @return the unique id of the context, or {@code null} if none
|
||||
* @return the unique id of the context (never null as of 7.0.2)
|
||||
*/
|
||||
@Nullable String getId();
|
||||
String getId();
|
||||
|
||||
/**
|
||||
* Return a name for the deployed application that this context belongs to.
|
||||
|
||||
+58
-9
@@ -42,16 +42,20 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
|
||||
import org.springframework.aot.generate.AccessControl;
|
||||
import org.springframework.aot.generate.GeneratedClass;
|
||||
import org.springframework.aot.generate.GeneratedMethod;
|
||||
import org.springframework.aot.generate.GeneratedMethods;
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.generate.MethodReference;
|
||||
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.ReflectionHints;
|
||||
import org.springframework.aot.hint.ResourceHints;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.TypeReference;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.BeanDefinitionStoreException;
|
||||
@@ -872,11 +876,14 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
@Override
|
||||
public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) {
|
||||
GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add(
|
||||
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder, generationContext));
|
||||
"applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder,
|
||||
generationContext, beanFactoryInitializationCode.getClassName()));
|
||||
beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference());
|
||||
}
|
||||
|
||||
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext) {
|
||||
private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext,
|
||||
ClassName className) {
|
||||
|
||||
ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection();
|
||||
method.addJavadoc("Apply bean registrars.");
|
||||
method.addModifiers(Modifier.PRIVATE);
|
||||
@@ -915,7 +922,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
}
|
||||
}
|
||||
}
|
||||
method.addCode(generateRegisterCode());
|
||||
method.addCode(generateRegisterCode(className, generationContext));
|
||||
}
|
||||
|
||||
private void checkUnsupportedFeatures(AbstractBeanDefinition beanDefinition) {
|
||||
@@ -937,37 +944,79 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
return code.build();
|
||||
}
|
||||
|
||||
private CodeBlock generateRegisterCode() {
|
||||
private CodeBlock generateRegisterCode(ClassName className, GenerationContext generationContext) {
|
||||
Builder code = CodeBlock.builder();
|
||||
Builder metadataReaderFactoryCode = null;
|
||||
NameAllocator nameAllocator = new NameAllocator();
|
||||
for (Map.Entry<String, List<BeanRegistrar>> beanRegistrarEntry : this.beanRegistrars.entrySet()) {
|
||||
for (BeanRegistrar beanRegistrar : beanRegistrarEntry.getValue()) {
|
||||
String beanRegistrarName = nameAllocator.newName(StringUtils.uncapitalize(beanRegistrar.getClass().getSimpleName()));
|
||||
code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass());
|
||||
Constructor<?> constructor = BeanUtils.getResolvableConstructor(beanRegistrar.getClass());
|
||||
boolean visible = isVisible(constructor, className);
|
||||
if (visible) {
|
||||
code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass());
|
||||
}
|
||||
else {
|
||||
try {
|
||||
Class<?> configClass = ClassUtils.forName(beanRegistrarEntry.getKey(), beanRegistrar.getClass().getClassLoader());
|
||||
GeneratedClass generatedClass = generationContext.getGeneratedClasses()
|
||||
.getOrAddForFeatureComponent("BeanRegistrars", configClass, type ->
|
||||
type.addJavadoc("Bean registrars for {@link $T}.", configClass)
|
||||
.addModifiers(Modifier.PUBLIC));
|
||||
GeneratedMethod generatedMethod = generatedClass.getMethods().add(
|
||||
"get" + beanRegistrar.getClass().getSimpleName(),
|
||||
method -> method
|
||||
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
|
||||
.returns(BeanRegistrar.class)
|
||||
.addStatement("return new $T()", beanRegistrar.getClass()));
|
||||
code.addStatement("$T $L = $L", BeanRegistrar.class, beanRegistrarName,
|
||||
generatedMethod.toMethodReference().toInvokeCodeBlock(ArgumentCodeGenerator.none()));
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
if (beanRegistrar instanceof ImportAware) {
|
||||
if (metadataReaderFactoryCode == null) {
|
||||
metadataReaderFactoryCode = CodeBlock.builder();
|
||||
metadataReaderFactoryCode.addStatement("$T metadataReaderFactory = new $T()",
|
||||
MetadataReaderFactory.class, CachingMetadataReaderFactory.class);
|
||||
}
|
||||
CodeBlock setImportMetadataCode;
|
||||
if (visible) {
|
||||
setImportMetadataCode = CodeBlock.builder()
|
||||
.addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
|
||||
beanRegistrarName, beanRegistrarEntry.getKey()).build();
|
||||
}
|
||||
else {
|
||||
setImportMetadataCode = CodeBlock.builder()
|
||||
.addStatement("(($T)$L).setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
|
||||
ImportAware.class, beanRegistrarName, beanRegistrarEntry.getKey()).build();
|
||||
}
|
||||
code.beginControlFlow("try")
|
||||
.addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())",
|
||||
beanRegistrarName, beanRegistrarEntry.getKey())
|
||||
.add(setImportMetadataCode)
|
||||
.nextControlFlow("catch ($T ex)", IOException.class)
|
||||
.addStatement("throw new $T(\"Failed to read metadata for '$L'\", ex)",
|
||||
IllegalStateException.class, beanRegistrarEntry.getKey())
|
||||
.endControlFlow();
|
||||
}
|
||||
code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName,
|
||||
code.addStatement("$L.register(new $T(($T)$L, $L, $L, $L.getClass(), $L), $L)", beanRegistrarName,
|
||||
BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE,
|
||||
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(),
|
||||
BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrarName,
|
||||
CUSTOMIZER_MAP_VARIABLE, ENVIRONMENT_VARIABLE);
|
||||
}
|
||||
}
|
||||
return (metadataReaderFactoryCode == null ? code.build() : metadataReaderFactoryCode.add(code.build()).build());
|
||||
}
|
||||
|
||||
private boolean isVisible(Constructor<?> ctor, ClassName className) {
|
||||
AccessControl classAccessControl = AccessControl.forClass(ctor.getDeclaringClass());
|
||||
AccessControl memberAccessControl = AccessControl.forMember(ctor);
|
||||
AccessControl.Visibility visibility = AccessControl.lowest(classAccessControl, memberAccessControl).getVisibility();
|
||||
return (visibility == AccessControl.Visibility.PUBLIC || (visibility != AccessControl.Visibility.PRIVATE &&
|
||||
ctor.getDeclaringClass().getPackageName().equals(className.packageName())));
|
||||
}
|
||||
|
||||
private CodeBlock generateInitDestroyMethods(String beanName, AbstractBeanDefinition beanDefinition,
|
||||
String[] methodNames, String method, ReflectionHints reflectionHints) {
|
||||
|
||||
|
||||
+16
-13
@@ -168,22 +168,25 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotat
|
||||
}
|
||||
}
|
||||
|
||||
boolean cacheable = true;
|
||||
for (String autowiredBeanName : autowiredBeanNames) {
|
||||
if (!this.beanFactory.containsBean(autowiredBeanName)) {
|
||||
cacheable = false;
|
||||
}
|
||||
else {
|
||||
if (this.beanName != null) {
|
||||
this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName);
|
||||
}
|
||||
if (!this.beanFactory.isSingleton(autowiredBeanName)) {
|
||||
boolean cacheable = false;
|
||||
if (!autowiredBeanNames.isEmpty()) {
|
||||
cacheable = true;
|
||||
for (String autowiredBeanName : autowiredBeanNames) {
|
||||
if (!this.beanFactory.containsBean(autowiredBeanName)) {
|
||||
cacheable = false;
|
||||
}
|
||||
else {
|
||||
if (this.beanName != null) {
|
||||
this.beanFactory.registerDependentBean(autowiredBeanName, this.beanName);
|
||||
}
|
||||
if (!this.beanFactory.isSingleton(autowiredBeanName)) {
|
||||
cacheable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cacheable) {
|
||||
this.cachedTarget = target;
|
||||
}
|
||||
}
|
||||
if (cacheable) {
|
||||
this.cachedTarget = target;
|
||||
}
|
||||
|
||||
return target;
|
||||
|
||||
@@ -54,5 +54,4 @@ public @interface Proxyable {
|
||||
*/
|
||||
Class<?>[] interfaces() default {};
|
||||
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -57,7 +57,7 @@ public class ApplicationContextAotGenerator {
|
||||
new ApplicationContextInitializationCodeGenerator(applicationContext, generationContext);
|
||||
DefaultListableBeanFactory beanFactory = applicationContext.getDefaultListableBeanFactory();
|
||||
new BeanFactoryInitializationAotContributions(beanFactory).applyTo(generationContext, codeGenerator);
|
||||
return codeGenerator.getGeneratedClass().getName();
|
||||
return codeGenerator.getClassName();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -127,8 +127,9 @@ class ApplicationContextInitializationCodeGenerator implements BeanFactoryInitia
|
||||
return ArgumentCodeGenerator.from(new InitializerMethodArgumentCodeGenerator());
|
||||
}
|
||||
|
||||
GeneratedClass getGeneratedClass() {
|
||||
return this.generatedClass;
|
||||
@Override
|
||||
public ClassName getClassName() {
|
||||
return this.generatedClass.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+11
-1
@@ -623,12 +623,22 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
finishRefresh();
|
||||
}
|
||||
|
||||
catch (RuntimeException | Error ex ) {
|
||||
catch (RuntimeException | Error ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Exception encountered during context initialization - " +
|
||||
"cancelling refresh attempt: " + ex);
|
||||
}
|
||||
|
||||
// Stop already started Lifecycle beans to avoid dangling resources.
|
||||
if (this.lifecycleProcessor != null && this.lifecycleProcessor.isRunning()) {
|
||||
try {
|
||||
this.lifecycleProcessor.stop();
|
||||
}
|
||||
catch (Throwable ex2) {
|
||||
logger.warn("Exception thrown from LifecycleProcessor on cancelled refresh", ex2);
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy already created singletons to avoid dangling resources.
|
||||
destroyBeans();
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user