Merge branch 'main' into add-pmd

This commit is contained in:
Vincent Potucek 2025-06-16 16:37:12 +02:00
commit ad78c14cf8
1105 changed files with 32931 additions and 7812 deletions

View File

@ -19,7 +19,7 @@ inputs:
java-version: java-version:
description: 'Java version to compile and test with' description: 'Java version to compile and test with'
required: false required: false
default: '17' default: '24'
publish: publish:
description: 'Whether to publish artifacts ready for deployment to Artifactory' description: 'Whether to publish artifacts ready for deployment to Artifactory'
required: false required: false

View File

@ -15,7 +15,7 @@ runs:
using: composite using: composite
steps: steps:
- name: Generate Changelog - name: Generate Changelog
uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11 uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12
with: with:
config-file: .github/actions/create-github-release/changelog-generator.yml config-file: .github/actions/create-github-release/changelog-generator.yml
milestone: ${{ inputs.milestone }} milestone: ${{ inputs.milestone }}

View File

@ -19,7 +19,7 @@ inputs:
java-version: java-version:
description: 'Java version to use for the build' description: 'Java version to use for the build'
required: false required: false
default: '17' default: '24'
runs: runs:
using: composite using: composite
steps: steps:
@ -31,7 +31,7 @@ runs:
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }} ${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
${{ inputs.java-toolchain == 'true' && '17' || '' }} ${{ inputs.java-toolchain == 'true' && '17' || '' }}
- name: Set Up Gradle - name: Set Up Gradle
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with: with:
cache-read-only: false cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }} develocity-access-key: ${{ inputs.develocity-access-key }}

View File

@ -46,7 +46,7 @@ jobs:
distribution: 'liberica' distribution: 'liberica'
java-version: 17 java-version: 17
- name: Set Up Gradle - name: Set Up Gradle
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2 uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with: with:
cache-read-only: false cache-read-only: false
- name: Configure Gradle Properties - name: Configure Gradle Properties

View File

@ -1,12 +1,12 @@
plugins { plugins {
id 'io.freefair.aspectj' version '8.13' apply false id 'io.freefair.aspectj' version '8.13.1' apply false
// kotlinVersion is managed in gradle.properties // kotlinVersion is managed in gradle.properties
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
id 'org.jetbrains.dokka' version '1.9.20' id 'org.jetbrains.dokka' version '1.9.20'
id 'com.github.bjornvester.xjc' version '1.8.2' apply false id 'com.github.bjornvester.xjc' version '1.8.2' apply false
id 'io.github.goooler.shadow' version '8.1.8' apply false id 'io.github.goooler.shadow' version '8.1.8' apply false
id 'me.champeau.jmh' version '0.7.2' apply false id 'me.champeau.jmh' version '0.7.2' apply false
id "net.ltgt.errorprone" version "4.1.0" apply false id "io.spring.nullability" version "0.0.1" apply false
} }
ext { ext {
@ -75,12 +75,11 @@ configure([rootProject] + javaProjects) { project ->
"https://docs.oracle.com/en/java/javase/17/docs/api/", "https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jakarta.ee/specifications/platform/11/apidocs/", "https://jakarta.ee/specifications/platform/11/apidocs/",
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/", "https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
"https://eclipse.dev/aspectj/doc/latest/runtime-api/",
"https://www.quartz-scheduler.org/api/2.3.0/", "https://www.quartz-scheduler.org/api/2.3.0/",
"https://hc.apache.org/httpcomponents-client-5.4.x/current/httpclient5/apidocs/", "https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/", "https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/", "https://junit.org/junit4/javadoc/4.13.2/",
"https://junit.org/junit5/docs/5.12.1/api/", "https://junit.org/junit5/docs/5.13.1/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/", "https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/", //"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/", "https://r2dbc.io/spec/1.0.0.RELEASE/api/",
@ -88,7 +87,9 @@ configure([rootProject] + javaProjects) { project ->
// but since 6.0 JSR 250 annotations such as @Resource and @PostConstruct have been replaced by their // but since 6.0 JSR 250 annotations such as @Resource and @PostConstruct have been replaced by their
// JakartaEE equivalents in the jakarta.annotation package. // JakartaEE equivalents in the jakarta.annotation package.
//"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/", //"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/",
"https://jspecify.dev/docs/api/" "https://jspecify.dev/docs/api/",
"https://www.javadoc.io/doc/tools.jackson.core/jackson-databind/3.0.0-rc4/"
] as String[] ] as String[]
} }

View File

@ -27,7 +27,9 @@ dependencies {
implementation "io.spring.nohttp:nohttp-gradle:0.0.11" implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
testImplementation("org.assertj:assertj-core:${assertjVersion}") testImplementation("org.assertj:assertj-core:${assertjVersion}")
testImplementation("org.junit.jupiter:junit-jupiter:${junitJupiterVersion}") testImplementation(platform("org.junit:junit-bom:${junitVersion}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
} }
gradlePlugin { gradlePlugin {

View File

@ -1,4 +1,4 @@
org.gradle.caching=true org.gradle.caching=true
javaFormatVersion=0.0.42
junitJupiterVersion=5.11.4
assertjVersion=3.27.3 assertjVersion=3.27.3
javaFormatVersion=0.0.43
junitVersion=5.12.2

View File

@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class); project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("10.21.4"); checkstyle.setToolVersion("10.25.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle")); checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion(); String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies(); DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();

View File

@ -26,7 +26,6 @@ import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.jvm.toolchain.JavaLanguageVersion; import org.gradle.jvm.toolchain.JavaLanguageVersion;
import org.gradle.jvm.toolchain.JvmVendorSpec;
/** /**
* {@link Plugin} that applies conventions for compiling Java sources in Spring Framework. * {@link Plugin} that applies conventions for compiling Java sources in Spring Framework.
@ -42,12 +41,15 @@ public class JavaConventions {
private static final List<String> TEST_COMPILER_ARGS; private static final List<String> TEST_COMPILER_ARGS;
/** /**
* The Java version we should use as the JVM baseline for building the project * The Java version we should use as the JVM baseline for building the project.
* <p>NOTE: If you update this value, you should also update the value used in
* the {@code javadoc} task in {@code framework-api.gradle}.
*/ */
private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(24); private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(24);
/** /**
* The Java version we should use as the baseline for the compiled bytecode (the "-release" compiler argument) * The Java version we should use as the baseline for the compiled bytecode
* (the "-release" compiler argument).
*/ */
private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17); private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17);
@ -83,7 +85,6 @@ public class JavaConventions {
*/ */
private static void applyToolchainConventions(Project project) { private static void applyToolchainConventions(Project project) {
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> { project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION); toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION);
}); });
} }

View File

@ -21,6 +21,8 @@ import java.util.Map;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.plugins.JavaBasePlugin; import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.testing.Test; import org.gradle.api.tasks.testing.Test;
import org.gradle.api.tasks.testing.TestFrameworkOptions;
import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions;
import org.gradle.testretry.TestRetryPlugin; import org.gradle.testretry.TestRetryPlugin;
import org.gradle.testretry.TestRetryTaskExtension; import org.gradle.testretry.TestRetryTaskExtension;
@ -34,6 +36,7 @@ import org.gradle.testretry.TestRetryTaskExtension;
* *
* @author Brian Clozel * @author Brian Clozel
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Sam Brannen
*/ */
class TestConventions { class TestConventions {
@ -50,7 +53,12 @@ class TestConventions {
} }
private void configureTests(Project project, Test test) { private void configureTests(Project project, Test test) {
test.useJUnitPlatform(); TestFrameworkOptions existingOptions = test.getOptions();
test.useJUnitPlatform(options -> {
if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) {
options.copyFrom(junitPlatformOptions);
}
});
test.include("**/*Tests.class", "**/*Test.class"); test.include("**/*Tests.class", "**/*Test.class");
test.setSystemProperties(Map.of( test.setSystemProperties(Map.of(
"java.awt.headless", "true", "java.awt.headless", "true",

View File

@ -1,6 +1,6 @@
plugins { plugins {
id 'java-platform' id 'java-platform'
id 'io.freefair.aggregate-javadoc' version '8.3' id 'io.freefair.aggregate-javadoc' version '8.13.1'
} }
description = "Spring Framework API Docs" description = "Spring Framework API Docs"
@ -20,7 +20,12 @@ dependencies {
} }
javadoc { javadoc {
javadocTool.set(javaToolchains.javadocToolFor({
languageVersion = JavaLanguageVersion.of(24)
}))
title = "${rootProject.description} ${version} API" title = "${rootProject.description} ${version} API"
failOnError = true
options { options {
encoding = "UTF-8" encoding = "UTF-8"
memberLevel = JavadocMemberLevel.PROTECTED memberLevel = JavadocMemberLevel.PROTECTED
@ -31,8 +36,13 @@ javadoc {
destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile
splitIndex = true splitIndex = true
links(rootProject.ext.javadocLinks) links(rootProject.ext.javadocLinks)
addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint // Check for 'syntax' and 'reference' during linting.
addBooleanOption('Werror', true) // fail build on Javadoc warnings addBooleanOption('Xdoclint:syntax,reference', true)
// Change modularity mismatch from warn to info.
// See https://github.com/spring-projects/spring-framework/issues/27497
addStringOption("-link-modularity-mismatch", "info")
// Fail build on Javadoc warnings.
addBooleanOption('Werror', true)
} }
maxMemory = "1024m" maxMemory = "1024m"
doFirst { doFirst {

View File

@ -49,35 +49,35 @@ tasks.named('compileKotlin', KotlinCompilationTask.class) {
} }
dependencies { dependencies {
api(project(":spring-aspects")) implementation(project(":spring-aspects"))
api(project(":spring-context")) implementation(project(":spring-context"))
api(project(":spring-context-support")) implementation(project(":spring-context-support"))
api(project(":spring-core-test")) implementation(project(":spring-core-test"))
api(project(":spring-jdbc")) implementation(project(":spring-jdbc"))
api(project(":spring-jms")) implementation(project(":spring-jms"))
api(project(":spring-test")) implementation(project(":spring-test"))
api(project(":spring-web")) implementation(project(":spring-web"))
api(project(":spring-webflux")) implementation(project(":spring-webflux"))
api(project(":spring-webmvc")) implementation(project(":spring-webmvc"))
api(project(":spring-websocket")) implementation(project(":spring-websocket"))
api("com.fasterxml.jackson.core:jackson-databind")
api("com.fasterxml.jackson.module:jackson-module-parameter-names")
api("com.mchange:c3p0:0.9.5.5")
api("com.oracle.database.jdbc:ojdbc11")
api("io.projectreactor.netty:reactor-netty-http")
api("jakarta.jms:jakarta.jms-api")
api("jakarta.servlet:jakarta.servlet-api")
api("jakarta.resource:jakarta.resource-api")
api("jakarta.validation:jakarta.validation-api")
api("jakarta.websocket:jakarta.websocket-client-api")
api("javax.cache:cache-api")
api("org.apache.activemq:activemq-ra:6.1.2")
api("org.apache.commons:commons-dbcp2:2.11.0")
api("org.aspectj:aspectjweaver")
api("org.assertj:assertj-core")
api("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
api("org.jetbrains.kotlin:kotlin-stdlib")
api("org.junit.jupiter:junit-jupiter-api")
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("com.fasterxml.jackson.module:jackson-module-parameter-names")
implementation("com.github.ben-manes.caffeine:caffeine")
implementation("com.mchange:c3p0:0.9.5.5")
implementation("com.oracle.database.jdbc:ojdbc11")
implementation("io.projectreactor.netty:reactor-netty-http")
implementation("jakarta.jms:jakarta.jms-api")
implementation("jakarta.servlet:jakarta.servlet-api")
implementation("jakarta.resource:jakarta.resource-api")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.websocket:jakarta.websocket-client-api")
implementation("javax.cache:cache-api")
implementation("org.apache.activemq:activemq-ra:6.1.2")
implementation("org.apache.commons:commons-dbcp2:2.11.0")
implementation("org.aspectj:aspectjweaver")
implementation("org.assertj:assertj-core")
implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.junit.jupiter:junit-jupiter-api")
} }

View File

@ -197,6 +197,7 @@
*** xref:web/webmvc/mvc-uri-building.adoc[] *** xref:web/webmvc/mvc-uri-building.adoc[]
*** xref:web/webmvc/mvc-ann-async.adoc[] *** xref:web/webmvc/mvc-ann-async.adoc[]
*** xref:web/webmvc-cors.adoc[] *** xref:web/webmvc-cors.adoc[]
*** xref:web/webmvc-versioning.adoc[]
*** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[] *** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[]
*** xref:web/webmvc/mvc-security.adoc[] *** xref:web/webmvc/mvc-security.adoc[]
*** xref:web/webmvc/mvc-caching.adoc[] *** xref:web/webmvc/mvc-caching.adoc[]
@ -225,6 +226,7 @@
**** xref:web/webmvc/mvc-config/static-resources.adoc[] **** xref:web/webmvc/mvc-config/static-resources.adoc[]
**** xref:web/webmvc/mvc-config/default-servlet-handler.adoc[] **** xref:web/webmvc/mvc-config/default-servlet-handler.adoc[]
**** xref:web/webmvc/mvc-config/path-matching.adoc[] **** xref:web/webmvc/mvc-config/path-matching.adoc[]
**** xref:web/webmvc/mvc-config/api-version.adoc[]
**** xref:web/webmvc/mvc-config/advanced-java.adoc[] **** xref:web/webmvc/mvc-config/advanced-java.adoc[]
**** xref:web/webmvc/mvc-config/advanced-xml.adoc[] **** xref:web/webmvc/mvc-config/advanced-xml.adoc[]
*** xref:web/webmvc/mvc-http2.adoc[] *** xref:web/webmvc/mvc-http2.adoc[]
@ -292,6 +294,7 @@
*** xref:web/webflux-functional.adoc[] *** xref:web/webflux-functional.adoc[]
*** xref:web/webflux/uri-building.adoc[] *** xref:web/webflux/uri-building.adoc[]
*** xref:web/webflux-cors.adoc[] *** xref:web/webflux-cors.adoc[]
*** xref:web/webflux-versioning.adoc[]
*** xref:web/webflux/ann-rest-exceptions.adoc[] *** xref:web/webflux/ann-rest-exceptions.adoc[]
*** xref:web/webflux/security.adoc[] *** xref:web/webflux/security.adoc[]
*** xref:web/webflux/caching.adoc[] *** xref:web/webflux/caching.adoc[]
@ -432,8 +435,8 @@
*** xref:integration/cache/plug.adoc[] *** xref:integration/cache/plug.adoc[]
*** xref:integration/cache/specific-config.adoc[] *** xref:integration/cache/specific-config.adoc[]
** xref:integration/observability.adoc[] ** xref:integration/observability.adoc[]
** xref:integration/aot-cache.adoc[]
** xref:integration/checkpoint-restore.adoc[] ** xref:integration/checkpoint-restore.adoc[]
** xref:integration/cds.adoc[]
** xref:integration/appendix.adoc[] ** xref:integration/appendix.adoc[]
* xref:languages.adoc[] * xref:languages.adoc[]
** xref:languages/kotlin.adoc[] ** xref:languages/kotlin.adoc[]

View File

@ -92,11 +92,25 @@ the repeated JNDI lookup overhead. See
{spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`] {spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`]
for details. for details.
| `spring.locking.strict`
| Instructs Spring to enforce strict locking during bean creation, rather than the mix of
strict and lenient locking that 6.2 applies by default. See
{spring-framework-api}++/beans/factory/support/DefaultListableBeanFactory.html#STRICT_LOCKING_PROPERTY_NAME++[`DefaultListableBeanFactory`]
for details.
| `spring.objenesis.ignore` | `spring.objenesis.ignore`
| Instructs Spring to ignore Objenesis, not even attempting to use it. See | Instructs Spring to ignore Objenesis, not even attempting to use it. See
{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`] {spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
for details. for details.
| `spring.placeholder.escapeCharacter.default`
| The default escape character for property placeholder support. If not set, `'\'` will
be used. Can be set to a custom escape character or an empty string to disable support
for an escape character. The default escape character be explicitly overridden in
`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See
{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`]
for details.
| `spring.test.aot.processing.failOnError` | `spring.test.aot.processing.failOnError`
| A boolean flag that controls whether errors encountered during AOT processing in the | A boolean flag that controls whether errors encountered during AOT processing in the
_Spring TestContext Framework_ should result in an exception that fails the overall process. _Spring TestContext Framework_ should result in an exception that fails the overall process.

View File

@ -101,8 +101,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig
Using the above configuration ensures Spring initialization failure if any `${}` Using the above configuration ensures Spring initialization failure if any `${}`
placeholder could not be resolved. It is also possible to use methods like placeholder could not be resolved. It is also possible to use methods like
`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or `setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or
`setEscapeCharacter` to customize placeholders. `setEscapeCharacter()` to customize the placeholder syntax. In addition, the default
escape character can be changed or disabled globally by setting the
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that
will get properties from `application.properties` and `application.yml` files. will get properties from `application.properties` and `application.yml` files.

View File

@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the
[[beans-factory-placeholderconfigurer]] [[beans-factory-placeholderconfigurer]]
=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer` === Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer`
You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values
from a bean definition in a separate file by using the standard Java `Properties` format. from a bean definition in a separate file by using the standard Java `Properties` format.
@ -341,8 +341,8 @@ with placeholder values is defined:
The example shows properties configured from an external `Properties` file. At runtime, The example shows properties configured from an external `Properties` file. At runtime,
a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some
properties of the DataSource. The values to replace are specified as placeholders of the properties of the `DataSource`. The values to replace are specified as placeholders of the
form pass:q[`${property-name}`], which follows the Ant and log4j and JSP EL style. form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style.
The actual values come from another file in the standard Java `Properties` format: The actual values come from another file in the standard Java `Properties` format:
@ -355,11 +355,15 @@ jdbc.password=root
---- ----
Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and
the same applies for other placeholder values that match keys in the properties file. the same applies for other placeholder values that match keys in the properties file. The
The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
attributes of a bean definition. Furthermore, you can customize the placeholder prefix and suffix. attributes of a bean definition. Furthermore, you can customize the placeholder prefix,
suffix, default value separator, and escape character. In addition, the default escape
character can be changed or disabled globally by setting the
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
With the `context` namespace introduced in Spring 2.5, you can configure property placeholders With the `context` namespace, you can configure property placeholders
with a dedicated configuration element. You can provide one or more locations as a with a dedicated configuration element. You can provide one or more locations as a
comma-separated list in the `location` attribute, as the following example shows: comma-separated list in the `location` attribute, as the following example shows:

View File

@ -1,56 +1,56 @@
[[null-safety]] [[null-safety]]
= Null-safety = Null-safety
Although Java does not let you express null-safety with its type system, the Spring Framework codebase is annotated with Although Java does not let you express nullness markers with its type system yet, the Spring Framework codebase is
https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullness of APIs, fields and related type annotated with https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullability of its APIs,
usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly recommended in order to get fields, and related type usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly
familiar with those annotations and semantics. recommended in order to get familiar with those annotations and semantics.
The primary goal of this explicit null-safety arrangement is to prevent `NullPointerException` to be thrown at runtime via The primary goal of this null-safety arrangement is to prevent a `NullPointerException` from being thrown at runtime via build
build time checks and to turn explicit nullness into a way to express the possible absence of value. It is useful in time checks and to use explicit nullability as a way to express the possible absence of value. It is useful in both
both Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting null-safety Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting JSpecify annotations
annotations such as IntelliJ IDEA or Eclipse) and Kotlin where JSpecify annotations are automatically translated to such as IntelliJ IDEA) and Kotlin where JSpecify annotations are automatically translated to
{kotlin-docs}/null-safety.html[Kotlin's null safety]. {kotlin-docs}/null-safety.html[Kotlin's null safety].
The {spring-framework-api}/core/Nullness.html[`Nullness` Spring API] can be used at runtime to detect the nullness of a The {spring-framework-api}/core/Nullness.html[`Nullness` Spring API] can be used at runtime to detect the nullness of a
type usage, a field, a method return type or a parameter. It provides full support for JSpecify annotations, type usage, a field, a method return type, or a parameter. It provides full support for JSpecify annotations,
Kotlin null safety, Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the Kotlin null safety, and Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the
package). package).
[[null-safety-libraries]] [[null-safety-libraries]]
== Annotating libraries with JSpecify annotations == Annotating libraries with JSpecify annotations
As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and
to check the consistency of those null-safety declarations with https://github.com/uber/NullAway[NullAway] as part of to check the consistency of those nullability declarations with https://github.com/uber/NullAway[NullAway] as part of
its build. It is recommended for each library depending on Spring Framework (Spring portfolio projects), as its build. It is recommended for each library depending on Spring Framework and Spring portfolio projects, as
well as other libraries related to the Spring ecosystem (Reactor, Micrometer and Spring community projects), to do the well as other libraries related to the Spring ecosystem (Reactor, Micrometer, and Spring community projects), to do the
same. same.
[[null-safety-applications]] [[null-safety-applications]]
== Leveraging JSpecify annotations in Spring applications == Leveraging JSpecify annotations in Spring applications
Developing applications with IDEs supporting null-safety annotations, such as IntelliJ IDEA or Eclipse, will provide Developing applications with IDEs that support nullness annotations will provide warnings in Java and errors in Kotlin
warnings in Java and errors in Kotlin when the null-safety contracts are not honored, allowing Spring application when the nullability contracts are not honored, allowing Spring application developers to refine their null handling to
developers to refine their null handling to prevent `NullPointerException` to be thrown at runtime. prevent a `NullPointerException` from being thrown at runtime.
Optionally, Spring application developers can annotate their codebase and use https://github.com/uber/NullAway[NullAway] Optionally, Spring application developers can annotate their codebase and use build plugins like
to enforce null-safety during build time at application level. https://github.com/uber/NullAway[NullAway] to enforce null-safety at the application level during build time.
[[null-safety-guidelines]] [[null-safety-guidelines]]
== Guidelines == Guidelines
The purpose of this section is to share some guidelines proposed for specifying explicitly the nullness of Spring-related The purpose of this section is to share some proposed guidelines for explicitly specifying the nullability of
libraries or applications. Spring-related libraries or applications.
[[null-safety-guidelines-jpecify]] [[null-safety-guidelines-jspecify]]
=== JSpecify === JSpecify
The key points to understand is that by default, the nullness of types is unknown in Java, and that non-null type The key points to understand are that the nullness of types is unknown in Java by default and that non-null type
usages are by far more frequent than nullable ones. In order to keep codebases readable, we typically want to define usage is by far more frequent than nullable usage. In order to keep codebases readable, we typically want to define
that by default, type usages are non-null unless marked as nullable for a specific scope. This is exactly the purpose of by default that type usage is non-null unless marked as nullable for a specific scope. This is exactly the purpose of
https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] that is typically set with Spring https://jspecify.dev/docs/api/org/jspecify/annotations/NullMarked.html[`@NullMarked`] which is typically set in Spring
at package level via a `package-info.java` file, for example: projects at the package level via a `package-info.java` file, for example:
[source,java,subs="verbatim,quotes",chomp="-packages",fold="none"] [source,java,subs="verbatim,quotes",chomp="-packages",fold="none"]
---- ----
@ -60,9 +60,9 @@ package org.springframework.core;
import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.NullMarked;
---- ----
In the various Java files belonging to the package, nullable type usages are defined explicitly with In the various Java files belonging to the package, nullable type usage is defined explicitly with
https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`]. It is recommended that this https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`]. It is recommended that this
annotation is specified just before the related type. annotation is specified just before the related type on the same line.
For example, for a field: For example, for a field:
@ -71,7 +71,7 @@ For example, for a field:
private @Nullable String fileEncoding; private @Nullable String fileEncoding;
---- ----
Or for method parameters and return value: Or for method parameters and method return types:
[source,java,subs="verbatim,quotes"] [source,java,subs="verbatim,quotes"]
---- ----
@ -81,20 +81,23 @@ public static @Nullable String buildMessage(@Nullable String message,
} }
---- ----
When overriding a method, nullness annotations are not inherited from the superclass method. That means those [NOTE]
nullness annotations should be repeated if you just want to override the implementation and keep the same API ====
nullness. When overriding a method, JSpecify annotations are not inherited from the original
method. That means the JSpecify annotations should be copied to the overriding method if
you want to override the implementation and keep the same nullability semantics.
====
With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of With arrays and varargs, you need to be able to differentiate the nullness of the elements from the nullness of
the array itself. Pay attention to the syntax the array itself. Pay attention to the syntax
https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[defined by the Java specification] which may be https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[defined by the Java specification] which may be
initially surprising: initially surprising:
- `@Nullable Object[] array` means individual elements can be null but the array itself can't. - `@Nullable Object[] array` means individual elements can be null but the array itself cannot.
- `Object @Nullable [] array` means individual elements can't be null but the array itself can. - `Object @Nullable [] array` means individual elements cannot be null but the array itself can.
- `@Nullable Object @Nullable [] array` means both individual elements and the array can be null. - `@Nullable Object @Nullable [] array` means both individual elements and the array can be null.
The Java specifications also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify The Java specification also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify
`@Nullable` should be specified after the last `.` with inner or fully qualified types: `@Nullable` should be specified after the last `.` with inner or fully qualified types:
- `Cache.@Nullable ValueWrapper` - `Cache.@Nullable ValueWrapper`
@ -111,15 +114,15 @@ typical use cases.
The recommended configuration is: The recommended configuration is:
- `NullAway:OnlyNullMarked=true` in order to perform nullness checks only for packages annotated with `@NullMarked`. - `NullAway:OnlyNullMarked=true` in order to perform nullability checks only for packages annotated with `@NullMarked`.
- `NullAway:CustomContractAnnotations=org.springframework.lang.Contract` which makes NullAway aware of the - `NullAway:CustomContractAnnotations=org.springframework.lang.Contract` which makes NullAway aware of the
{spring-framework-api}/lang/Contract.html[@Contract] annotation in the `org.springframework.lang` package which {spring-framework-api}/lang/Contract.html[@Contract] annotation in the `org.springframework.lang` package which
can be used to express complementary semantics to avoid non-relevant null-safety warnings in your codebase. can be used to express complementary semantics to avoid irrelevant warnings in your codebase.
A good example of `@Contract` benefits is A good example of the benefits of a `@Contract` declaration can be seen with
{spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert#notnull`] which is annotated {spring-framework-api}/util/Assert.html#notNull(java.lang.Object,java.lang.String)[`Assert.notNull()`] which is annotated
with `@Contract("null, _ -> fail")`. With the configuration above, NullAway will understand that after a successful with `@Contract("null, _ -> fail")`. With that contract declaration, NullAway will understand that the value passed as a
invocation, the value passed as a parameter is not null. parameter cannot be null after a successful invocation of `Assert.notNull()`.
Optionally, it is possible to set `NullAway:JSpecifyMode=true` to enable Optionally, it is possible to set `NullAway:JSpecifyMode=true` to enable
https://github.com/uber/NullAway/wiki/JSpecify-Support[checks on the full JSpecify semantics], including annotations on https://github.com/uber/NullAway/wiki/JSpecify-Support[checks on the full JSpecify semantics], including annotations on
@ -127,26 +130,26 @@ generic types. Be aware that this mode is
https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[still under development] and requires https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[still under development] and requires
using JDK 22 or later (typically combined with the `--release` Java compiler flag to configure the using JDK 22 or later (typically combined with the `--release` Java compiler flag to configure the
expected baseline). It is recommended to enable the JSpecify mode only as a second step, after making sure the codebase expected baseline). It is recommended to enable the JSpecify mode only as a second step, after making sure the codebase
generates no warning with the recommended configuration mentioned above. generates no warning with the recommended configuration mentioned previously in this section.
==== Warnings suppression ==== Warnings suppression
There are a few valid use cases where NullAway will wrongly detect nullness problems. In such case, it is recommended There are a few valid use cases where NullAway will incorrectly detect nullability problems. In such case, it is recommended
to suppress related warnings and to document the reason: to suppress related warnings and to document the reason:
- `@SuppressWarnings("NullAway.Init")` at field, constructor or class level can be used to avoid unnecessary warnings - `@SuppressWarnings("NullAway.Init")` at field, constructor, or class level can be used to avoid unnecessary warnings
due to the lazy initialization of fields, for example due to a class implementing due to the lazy initialization of fields for example, due to a class implementing
{spring-framework-api}/beans/factory/InitializingBean.html[`InitializingBean`]. {spring-framework-api}/beans/factory/InitializingBean.html[`InitializingBean`].
- `@SuppressWarnings("NullAway") // Dataflow analysis limitation` can be used when NullAway dataflow analysis is not - `@SuppressWarnings("NullAway") // Dataflow analysis limitation` can be used when NullAway dataflow analysis is not
able to detect that the path involving a nullness problem will never happen. able to detect that the path involving a nullability problem will never happen.
- `@SuppressWarnings("NullAway") // Lambda` can be used when NullAway does not take into account assertions performed - `@SuppressWarnings("NullAway") // Lambda` can be used when NullAway does not take into account assertions performed
outside of a lambda for the code path within the lambda. outside of a lambda for the code path within the lambda.
- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known returning - `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known to return
non-null values even if that can't be expressed by the API. non-null values even if that cannot be expressed by the API.
- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are done with keys known - `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are performed with keys that are known
to be present and non-null related values inserted previously. to be present and when non-null related values have been inserted previously.
- `@SuppressWarnings("NullAway") // Overridden method does not define nullness` can be used when the super class does - `@SuppressWarnings("NullAway") // Overridden method does not define nullability` can be used when the superclass does
not define nullness (typically when the super class is coming from a dependency). not define nullability (typically when the superclass comes from a dependency).
[[null-safety-migrating]] [[null-safety-migrating]]
@ -155,30 +158,30 @@ not define nullness (typically when the super class is coming from a dependency)
Spring null-safety annotations {spring-framework-api}/lang/Nullable.html[`@Nullable`], Spring null-safety annotations {spring-framework-api}/lang/Nullable.html[`@Nullable`],
{spring-framework-api}/lang/NonNull.html[`@NonNull`], {spring-framework-api}/lang/NonNull.html[`@NonNull`],
{spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`], and {spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`], and
{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package have been {spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package were
introduced in Spring Framework 5 when JSpecify did not exist and the best option was to leverage JSR 305 (a dormant introduced in Spring Framework 5 when JSpecify did not exist, and the best option at that time was to leverage
but widespread JSR) meta-annotations. They are deprecated as of Spring Framework 7 in favor of meta-annotations from JSR 305 (a dormant but widespread JSR). They are deprecated as of Spring Framework 7 in favor of
https://jspecify.dev/docs/start-here/[JSpecify] annotations, which provide significant enhancements such as properly https://jspecify.dev/docs/start-here/[JSpecify] annotations, which provide significant enhancements such as properly
defined specifications, a canonical dependency with no split-package issue, better tooling, better Kotlin integration defined specifications, a canonical dependency with no split-package issues, better tooling, better Kotlin integration,
and the capability to specify the nullness more precisely for more use cases. and the capability to specify nullability more precisely for more use cases.
A key difference is that Spring null-safety annotations, following JSR 305 semantics, apply to fields, A key difference is that Spring's deprecated null-safety annotations, which follow JSR 305 semantics, apply to fields,
parameters and return values while JSpecify annotations apply to type usages. This subtle difference parameters, and return values; while JSpecify annotations apply to type usage. This subtle difference
is in practice pretty significant, as it allows for example to differentiate the nullness of elements from the is in practice pretty significant, as it allows developers to differentiate between the nullness of elements and the
nullness of arrays/varargs as well as defining the nullness of generic types. nullness of arrays/varargs as well as to define the nullness of generic types.
That means array and varargs null-safety declarations have to be updated to keep the same semantic. For example That means array and varargs null-safety declarations have to be updated to keep the same semantics. For example
`@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify `@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify
annotations. Same for varargs. annotations. The same applies to varargs.
It is also recommended to move field and return value annotations closer to the type, for example: It is also recommended to move field and return value annotations closer to the type and on the same line, for example:
- For fields, instead of `@Nullable private String field` with Spring annotations, use `private @Nullable String field` - For fields, instead of `@Nullable private String field` with Spring annotations, use `private @Nullable String field`
with JSpecify annotations. with JSpecify annotations.
- For return values, instead of `@Nullable public String method()` with Spring annotations, use - For method return types, instead of `@Nullable public String method()` with Spring annotations, use
`public @Nullable String method()` with JSpecify annotations. `public @Nullable String method()` with JSpecify annotations.
Also, with JSpecify, you don't need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the Also, with JSpecify, you do not need to specify `@NonNull` when overriding a type usage annotated with `@Nullable` in the
super method to "undo" the nullable declaration in null-marked code. Just declare it unannotated and the null-marked super method to "undo" the nullable declaration in null-marked code. Just declare it unannotated and the null-marked
defaults (a type usage is considered non-null unless explicitly annotated as nullable) will apply. defaults (a type usage is considered non-null unless explicitly annotated as nullable) will apply.

View File

@ -399,7 +399,7 @@ A `ConstraintViolation` on the `degrees` method parameter is adapted to a
`MessageSourceResolvable` with the following: `MessageSourceResolvable` with the following:
- Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"` - Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"`
- Message arguments "degrees2 and 2 (the field name and the constraint attribute) - Message arguments "degrees" and 2 (the field name and the constraint attribute)
- Default message "must be less than or equal to 2" - Default message "must be less than or equal to 2"
To customize the above default message, you can add a property such as: To customize the above default message, you can add a property such as:

View File

@ -0,0 +1,105 @@
[[aot-cache]]
= JVM AOT Cache
:page-aliases: integration/class-data-sharing.adoc
:page-aliases: integration/cds.adoc
The ahead-of-time cache is a JVM feature introduced in Java 24 via the
https://openjdk.org/jeps/483[JEP 483] that can help reduce the startup time and memory
footprint of Java applications. AOT cache is a natural evolution of https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[Class Data Sharing (CDS)].
Spring Framework supports both CDS and AOT cache, and it is recommended that you use the
later if available in the JVM version your are using (Java 24+).
To use this feature, an AOT cache should be created for the particular classpath of the
application. It is possible to create this cache on the deployed instance, or during a
training run performed for example when packaging the application thanks to an hook-point
provided by the Spring Framework to ease such use case. Once the cache is available, users
should opt in to use it via a JVM flag.
NOTE: If you are using Spring Boot, it is highly recommended to leverage its
{spring-boot-docs-ref}/packaging/efficient.html#packaging.efficient.unpacking[executable JAR unpacking support]
which is designed to fulfill the class loading requirements of both AOT cache and CDS.
== Creating the cache
An AOT cache can typically be created when the application exits. The Spring Framework
provides a mode of operation where the process can exit automatically once the
`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons
have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been
invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet
been published.
To create the cache during the training run, it is possible to specify the `-Dspring.context.exit=onRefresh`
JVM flag to start then exit your Spring application once the
`ApplicationContext` has refreshed:
--
[tabs]
======
AOT cache::
+
[source,bash,subs="verbatim,quotes"]
----
# Both commands need to be run with the same classpath
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -Dspring.context.exit=onRefresh ...
java -XX:AOTMode=create -XX:AOTConfiguration=app.aotconf -XX:AOTCache=app.aot ...
----
CDS::
+
[source,bash,subs="verbatim,quotes"]
----
# To create a CDS archive, your JDK/JRE must have a base image
java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh ...
----
======
--
== Using the cache
Once the cache file has been created, you can use it to start your application faster:
--
[tabs]
======
AOT cache::
+
[source,bash,subs="verbatim"]
----
# With the same classpath (or a superset) tan the training run
java -XX:AOTCache=app.aot ...
----
CDS::
+
[source,bash,subs="verbatim"]
----
# With the same classpath (or a superset) tan the training run
java -XX:SharedArchiveFile=app.jsa ...
----
======
--
Pay attention to the logs and the startup time to check if the AOT cache is used successfully.
To figure out how effective the cache is, you can enable class loading logs by adding
an extra attribute: `-Xlog:class+load:file=aot-cache.log`. This creates a `aot-cache.log` with
every attempt to load a class and its source. Classes that are loaded from the cache should have
a "shared objects file" source, as shown in the following example:
[source,shell,subs="verbatim"]
----
[0.151s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file
[0.151s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file
[0.151s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file
[0.151s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file
[0.151s][info][class,load] org.springframework.context.MessageSource source: shared objects file
----
If the AOT cache can't be enabled or if you have a large number of classes that are not loaded from
the cache, make sure that the following conditions are fulfilled when creating and using the cache:
- The very same JVM must be used.
- The classpath must be specified as a JAR or a list of JARs, and avoid the usage of directories and `*` wildcard characters.
- The timestamps of the JARs must be preserved.
- When using the cache, the classpath must be the same than the one used to create it, in the same order.
Additional JARs or directories can be specified *at the end* (but won't be cached).

View File

@ -1,72 +0,0 @@
[[cds]]
= CDS
:page-aliases: integration/class-data-sharing.adoc
Class Data Sharing (CDS) is a https://docs.oracle.com/en/java/javase/17/vm/class-data-sharing.html[JVM feature]
that can help reduce the startup time and memory footprint of Java applications.
To use this feature, a CDS archive should be created for the particular classpath of the
application. The Spring Framework provides a hook-point to ease the creation of the
archive. Once the archive is available, users should opt in to use it via a JVM flag.
== Creating the CDS Archive
A CDS archive for an application can be created when the application exits. The Spring
Framework provides a mode of operation where the process can exit automatically once the
`ApplicationContext` has refreshed. In this mode, all non-lazy initialized singletons
have been instantiated, and `InitializingBean#afterPropertiesSet` callbacks have been
invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has not yet
been published.
To create the archive, two additional JVM flags must be specified:
* `-XX:ArchiveClassesAtExit=application.jsa`: creates the CDS archive on exit
* `-Dspring.context.exit=onRefresh`: starts and then immediately exits your Spring
application as described above
To create a CDS archive, your JDK/JRE must have a base image. If you add the flags above to
your startup script, you may get a warning that looks like this:
[source,shell,indent=0,subs="verbatim"]
----
-XX:ArchiveClassesAtExit is unsupported when base CDS archive is not loaded. Run with -Xlog:cds for more info.
----
The base CDS archive is usually provided out-of-the-box, but can also be created if needed by issuing the following
command:
[source,shell,indent=0,subs="verbatim"]
----
$ java -Xshare:dump
----
== Using the Archive
Once the archive is available, add `-XX:SharedArchiveFile=application.jsa` to your startup
script to use it, assuming an `application.jsa` file in the working directory.
To check if the CDS cache is effective, you can use (for testing purposes only, not in production) `-Xshare:on` which
prints an error message and exits if CDS can't be enabled.
To figure out how effective the cache is, you can enable class loading logs by adding
an extra attribute: `-Xlog:class+load:file=cds.log`. This creates a `cds.log` with every
attempt to load a class and its source. Classes that are loaded from the cache should have
a "shared objects file" source, as shown in the following example:
[source,shell,indent=0,subs="verbatim"]
----
[0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top)
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
----
If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that
the following conditions are fulfilled when creating and using the archive:
- The very same JVM must be used.
- The classpath must be specified as a list of JARs, and avoid the usage of directories and `*` wildcard characters.
- The timestamps of the JARs must be preserved.
- When using the archive, the classpath must be the same than the one used to create the archive, in the same order.
Additional JARs or directories can be specified *at the end* (but won't be cached).

View File

@ -3,26 +3,34 @@
The Spring Framework provides the following choices for making calls to REST endpoints: The Spring Framework provides the following choices for making calls to REST endpoints:
* xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] - synchronous client with a fluent API. * xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] -- synchronous client with a fluent API
* xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] - non-blocking, reactive client with fluent API. * xref:integration/rest-clients.adoc#rest-webclient[`WebClient`] -- non-blocking, reactive client with fluent API
* xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] - synchronous client with template method API. * xref:integration/rest-clients.adoc#rest-resttemplate[`RestTemplate`] -- synchronous client with template method API
* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface] - annotated interface with generated, dynamic proxy implementation. * xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface Clients] -- annotated interface backed by generated proxy
[[rest-restclient]] [[rest-restclient]]
== `RestClient` == `RestClient`
The `RestClient` is a synchronous HTTP client that offers a modern, fluent API. `RestClient` is a synchronous HTTP client that provides a fluent API to perform requests.
It offers an abstraction over HTTP libraries that allows for convenient conversion from a Java object to an HTTP request, and the creation of objects from an HTTP response. It serves as an abstraction over HTTP libraries, and handles conversion of HTTP request and response content to and from higher level Java objects.
=== Creating a `RestClient` === Create a `RestClient`
The `RestClient` is created using one of the static `create` methods. `RestClient` has static `create` shortcut methods.
You can also use `builder()` to get a builder with further options, such as specifying which HTTP library to use (see <<rest-request-factories>>) and which message converters to use (see <<rest-message-conversion>>), setting a default URI, default path variables, default request headers, or `uriBuilderFactory`, or registering interceptors and initializers. It also exposes a `builder()` with further options:
Once created (or built), the `RestClient` can be used safely by multiple threads. - select the HTTP library to use, see <<rest-request-factories>>
- configure message converters, see <<rest-message-conversion>>
- set a baseUrl
- set default request headers, cookies, path variables, API version
- configure an `ApiVersionInserter`
- register interceptors
- register request initializers
The following sample shows how to create a default `RestClient`, and how to build a custom one. Once created, a `RestClient` is safe to use in multiple threads.
The below shows how to create or build a `RestClient`:
[tabs] [tabs]
====== ======
@ -30,15 +38,17 @@ Java::
+ +
[source,java,indent=0,subs="verbatim"] [source,java,indent=0,subs="verbatim"]
---- ----
RestClient defaultClient = RestClient.create(); RestClient defaultClient = RestClient.create();
RestClient customClient = RestClient.builder() RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory()) .requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter())) .messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com") .baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo")) .defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo") .defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar") .defaultCookie("My-Cookie", "Bar")
.defaultVersion("1.2")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.requestInterceptor(myCustomInterceptor) .requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer) .requestInitializer(myCustomInitializer)
.build(); .build();
@ -48,32 +58,34 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim"] [source,kotlin,indent=0,subs="verbatim"]
---- ----
val defaultClient = RestClient.create() val defaultClient = RestClient.create()
val customClient = RestClient.builder() val customClient = RestClient.builder()
.requestFactory(HttpComponentsClientHttpRequestFactory()) .requestFactory(HttpComponentsClientHttpRequestFactory())
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) } .messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
.baseUrl("https://example.com") .baseUrl("https://example.com")
.defaultUriVariables(mapOf("variable" to "foo")) .defaultUriVariables(mapOf("variable" to "foo"))
.defaultHeader("My-Header", "Foo") .defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar") .defaultCookie("My-Cookie", "Bar")
.defaultVersion("1.2")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.requestInterceptor(myCustomInterceptor) .requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer) .requestInitializer(myCustomInitializer)
.build() .build()
---- ----
====== ======
=== Using the `RestClient` === Use the `RestClient`
When making an HTTP request with the `RestClient`, the first thing to specify is which HTTP method to use. To perform an HTTP request, first specify the HTTP method to use.
This can be done with `method(HttpMethod)` or with the convenience methods `get()`, `head()`, `post()`, and so on. Use the convenience methods like `get()`, `head()`, `post()`, and others, or `method(HttpMethod)`.
==== Request URL ==== Request URL
Next, the request URI can be specified with the `uri` methods. Next, specify the request URI with the `uri` methods.
This step is optional and can be skipped if the `RestClient` is configured with a default URI. This is optional, and you can skip this step if you configured a baseUrl through the builder.
The URL is typically specified as a `String`, with optional URI template variables. The URL is typically specified as a `String`, with optional URI template variables.
The following example configures a GET request to `https://example.com/orders/42`: The following shows how to perform a request:
[tabs] [tabs]
====== ======
@ -81,20 +93,20 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
int id = 42; int id = 42;
restClient.get() restClient.get()
.uri("https://example.com/orders/{id}", id) .uri("https://example.com/orders/{id}", id)
.... // ...
---- ----
Kotlin:: Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val id = 42 val id = 42
restClient.get() restClient.get()
.uri("https://example.com/orders/{id}", id) .uri("https://example.com/orders/{id}", id)
... // ...
---- ----
====== ======
@ -108,6 +120,7 @@ For more details on working with and encoding URIs, see xref:web/webmvc/mvc-uri-
If necessary, the HTTP request can be manipulated by adding request headers with `header(String, String)`, `headers(Consumer<HttpHeaders>`, or with the convenience methods `accept(MediaType...)`, `acceptCharset(Charset...)` and so on. If necessary, the HTTP request can be manipulated by adding request headers with `header(String, String)`, `headers(Consumer<HttpHeaders>`, or with the convenience methods `accept(MediaType...)`, `acceptCharset(Charset...)` and so on.
For HTTP requests that can contain a body (`POST`, `PUT`, and `PATCH`), additional methods are available: `contentType(MediaType)`, and `contentLength(long)`. For HTTP requests that can contain a body (`POST`, `PUT`, and `PATCH`), additional methods are available: `contentType(MediaType)`, and `contentLength(long)`.
You can set an API version for the request if the client is configured with `ApiVersionInserter`.
The request body itself can be set by `body(Object)`, which internally uses <<rest-message-conversion>>. The request body itself can be set by `body(Object)`, which internally uses <<rest-message-conversion>>.
Alternatively, the request body can be set using a `ParameterizedTypeReference`, allowing you to use generics. Alternatively, the request body can be set using a `ParameterizedTypeReference`, allowing you to use generics.
@ -133,12 +146,12 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
String result = restClient.get() <1> String result = restClient.get() <1>
.uri("https://example.com") <2> .uri("https://example.com") <2>
.retrieve() <3> .retrieve() <3>
.body(String.class); <4> .body(String.class); <4>
System.out.println(result); <5> System.out.println(result); <5>
---- ----
<1> Set up a GET request <1> Set up a GET request
<2> Specify the URL to connect to <2> Specify the URL to connect to
@ -150,12 +163,12 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val result= restClient.get() <1> val result= restClient.get() <1>
.uri("https://example.com") <2> .uri("https://example.com") <2>
.retrieve() <3> .retrieve() <3>
.body<String>() <4> .body<String>() <4>
println(result) <5> println(result) <5>
---- ----
<1> Set up a GET request <1> Set up a GET request
<2> Specify the URL to connect to <2> Specify the URL to connect to
@ -172,14 +185,14 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
ResponseEntity<String> result = restClient.get() <1> ResponseEntity<String> result = restClient.get() <1>
.uri("https://example.com") <1> .uri("https://example.com") <1>
.retrieve() .retrieve()
.toEntity(String.class); <2> .toEntity(String.class); <2>
System.out.println("Response status: " + result.getStatusCode()); <3> System.out.println("Response status: " + result.getStatusCode()); <3>
System.out.println("Response headers: " + result.getHeaders()); <3> System.out.println("Response headers: " + result.getHeaders()); <3>
System.out.println("Contents: " + result.getBody()); <3> System.out.println("Contents: " + result.getBody()); <3>
---- ----
<1> Set up a GET request for the specified URL <1> Set up a GET request for the specified URL
<2> Convert the response into a `ResponseEntity` <2> Convert the response into a `ResponseEntity`
@ -189,14 +202,14 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val result = restClient.get() <1> val result = restClient.get() <1>
.uri("https://example.com") <1> .uri("https://example.com") <1>
.retrieve() .retrieve()
.toEntity<String>() <2> .toEntity<String>() <2>
println("Response status: " + result.statusCode) <3> println("Response status: " + result.statusCode) <3>
println("Response headers: " + result.headers) <3> println("Response headers: " + result.headers) <3>
println("Contents: " + result.body) <3> println("Contents: " + result.body) <3>
---- ----
<1> Set up a GET request for the specified URL <1> Set up a GET request for the specified URL
<2> Convert the response into a `ResponseEntity` <2> Convert the response into a `ResponseEntity`
@ -212,8 +225,8 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
int id = ...; int id = ...;
Pet pet = restClient.get() Pet pet = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) <1> .uri("https://petclinic.example.com/pets/{id}", id) <1>
.accept(APPLICATION_JSON) <2> .accept(APPLICATION_JSON) <2>
.retrieve() .retrieve()
@ -227,8 +240,8 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val id = ... val id = ...
val pet = restClient.get() val pet = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) <1> .uri("https://petclinic.example.com/pets/{id}", id) <1>
.accept(APPLICATION_JSON) <2> .accept(APPLICATION_JSON) <2>
.retrieve() .retrieve()
@ -247,8 +260,8 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
Pet pet = ... <1> Pet pet = ... <1>
ResponseEntity<Void> response = restClient.post() <2> ResponseEntity<Void> response = restClient.post() <2>
.uri("https://petclinic.example.com/pets/new") <2> .uri("https://petclinic.example.com/pets/new") <2>
.contentType(APPLICATION_JSON) <3> .contentType(APPLICATION_JSON) <3>
.body(pet) <4> .body(pet) <4>
@ -265,8 +278,8 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val pet: Pet = ... <1> val pet: Pet = ... <1>
val response = restClient.post() <2> val response = restClient.post() <2>
.uri("https://petclinic.example.com/pets/new") <2> .uri("https://petclinic.example.com/pets/new") <2>
.contentType(APPLICATION_JSON) <3> .contentType(APPLICATION_JSON) <3>
.body(pet) <4> .body(pet) <4>
@ -291,7 +304,7 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
String result = restClient.get() <1> String result = restClient.get() <1>
.uri("https://example.com/this-url-does-not-exist") <1> .uri("https://example.com/this-url-does-not-exist") <1>
.retrieve() .retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2> .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2>
@ -307,7 +320,7 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val result = restClient.get() <1> val result = restClient.get() <1>
.uri("https://example.com/this-url-does-not-exist") <1> .uri("https://example.com/this-url-does-not-exist") <1>
.retrieve() .retrieve()
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2> .onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2>
@ -330,7 +343,7 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
Pet result = restClient.get() Pet result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) .uri("https://petclinic.example.com/pets/{id}", id)
.accept(APPLICATION_JSON) .accept(APPLICATION_JSON)
.exchange((request, response) -> { <1> .exchange((request, response) -> { <1>
@ -351,7 +364,7 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val result = restClient.get() val result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) .uri("https://petclinic.example.com/pets/{id}", id)
.accept(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON)
.exchange { request, response -> <1> .exchange { request, response -> <1>
@ -380,15 +393,14 @@ To serialize only a subset of the object properties, you can specify a {baeldung
[source,java,indent=0,subs="verbatim"] [source,java,indent=0,subs="verbatim"]
---- ----
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23")); MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class); value.setSerializationView(User.WithoutPasswordView.class);
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
.contentType(APPLICATION_JSON) .contentType(APPLICATION_JSON)
.body(value) .body(value)
.retrieve() .retrieve()
.toBodilessEntity(); .toBodilessEntity();
---- ----
==== Multipart ==== Multipart
@ -398,17 +410,17 @@ For example:
[source,java,indent=0,subs="verbatim"] [source,java,indent=0,subs="verbatim"]
---- ----
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue"); parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png")); parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason")); parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML); headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers)); parts.add("xmlPart", new HttpEntity<>(myBean, headers));
// send using RestClient.post or RestTemplate.postForEntity // send using RestClient.post or RestTemplate.postForEntity
---- ----
In most cases, you do not have to specify the `Content-Type` for each part. In most cases, you do not have to specify the `Content-Type` for each part.
@ -845,15 +857,17 @@ It can be used to migrate from the latter to the former.
[[rest-http-interface]] [[rest-http-interface]]
== HTTP Interface == HTTP Interface Clients
The Spring Framework lets you define an HTTP service as a Java interface with You can define an HTTP Service as a Java interface with `@HttpExchange` methods, and use
`@HttpExchange` methods. You can pass such an interface to `HttpServiceProxyFactory` `HttpServiceProxyFactory` to create a client proxy from it for remote access over HTTP via
to create a proxy which performs requests through an HTTP client such as `RestClient` `RestClient`, `WebClient`, or `RestTemplate`. On the server side, an `@Controller` class
or `WebClient`. You can also implement the interface from an `@Controller` for server can implement the same interface to handle requests with
request handling. xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-httpexchange-annotation[@HttpExchange]
controller methods.
Start by creating the interface with `@HttpExchange` methods:
First, create the Java interface:
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
@ -867,43 +881,7 @@ Start by creating the interface with `@HttpExchange` methods:
} }
---- ----
Now you can create a proxy that performs requests when methods are called. Optionally, use `@HttpExchange` at the type level to declare common attributes for all methods:
For `RestClient`:
[source,java,indent=0,subs="verbatim,quotes"]
----
RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RepositoryService service = factory.createClient(RepositoryService.class);
----
For `WebClient`:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient webClient = WebClient.builder().baseUrl("https://api.github.com/").build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RepositoryService service = factory.createClient(RepositoryService.class);
----
For `RestTemplate`:
[source,java,indent=0,subs="verbatim,quotes"]
----
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("https://api.github.com/"));
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RepositoryService service = factory.createClient(RepositoryService.class);
----
`@HttpExchange` is supported at the type level where it applies to all methods:
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
@ -921,15 +899,46 @@ For `RestTemplate`:
---- ----
Next, configure the client and create the `HttpServiceProxyFactory`:
[source,java,indent=0,subs="verbatim,quotes"]
----
// Using RestClient...
RestClient restClient = RestClient.create("...");
RestClientAdapter adapter = RestClientAdapter.create(restClient);
// or WebClient...
WebClient webClient = WebClient.create("...");
WebClientAdapter adapter = WebClientAdapter.create(webClient);
// or RestTemplate...
RestTemplate restTemplate = new RestTemplate();
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
----
Now, you're ready to create client proxies:
[source,java,indent=0,subs="verbatim,quotes"]
----
RepositoryService service = factory.createClient(RepositoryService.class);
// Use service methods for remote calls...
----
[[rest-http-interface-method-parameters]] [[rest-http-interface-method-parameters]]
=== Method Parameters === Method Parameters
Annotated, HTTP exchange methods support flexible method signatures with the following `@HttpExchange` methods support flexible method signatures with the following inputs:
method parameters:
[cols="1,2", options="header"] [cols="1,2", options="header"]
|=== |===
| Method argument | Description | Method parameter | Description
| `URI` | `URI`
| Dynamically set the URL for the request, overriding the annotation's `url` attribute. | Dynamically set the URL for the request, overriding the annotation's `url` attribute.
@ -990,29 +999,33 @@ Method parameters cannot be `null` unless the `required` attribute (where availa
parameter annotation) is set to `false`, or the parameter is marked optional as determined by parameter annotation) is set to `false`, or the parameter is marked optional as determined by
{spring-framework-api}/core/MethodParameter.html#isOptional()[`MethodParameter#isOptional`]. {spring-framework-api}/core/MethodParameter.html#isOptional()[`MethodParameter#isOptional`].
`RestClientAdapter` provides additional support for a method parameter of type
`StreamingHttpOutputMessage.Body` that allows sending the request body by writing to an
`OutputStream`.
[[rest-http-interface.custom-resolver]] [[rest-http-interface.custom-resolver]]
=== Custom argument resolver === Custom Arguments
For more complex cases, HTTP interfaces do not support `RequestEntity` types as method parameters. You can configure a custom `HttpServiceArgumentResolver`. The example interface below
This would take over the entire HTTP request and not improve the semantics of the interface. uses a custom `Search` method parameter type:
Instead of adding many method parameters, developers can combine them into a custom type
and configure a dedicated `HttpServiceArgumentResolver` implementation.
In the following HTTP interface, we are using a custom `Search` type as a parameter:
include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0] include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0]
We can implement our own `HttpServiceArgumentResolver` that supports our custom `Search` type A custom argument resolver could be implemented like this:
and writes its data in the outgoing HTTP request.
include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0] include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0]
Finally, we can use this argument resolver during the setup and use our HTTP interface. To configure the custom argument resolver:
include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0] include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0]
TIP: By default, `RequestEntity` is not supported as a method parameter, instead encouraging
the use of more fine-grained method parameters for individual parts of the request.
[[rest-http-interface-return-values]] [[rest-http-interface-return-values]]
=== Return Values === Return Values
@ -1085,65 +1098,180 @@ depends on how the underlying HTTP client is configured. You can set a `blockTim
value on the adapter level as well, but we recommend relying on timeout settings of the value on the adapter level as well, but we recommend relying on timeout settings of the
underlying HTTP client, which operates at a lower level and provides more control. underlying HTTP client, which operates at a lower level and provides more control.
`RestClientAdapter` provides supports additional support for a return value of type
`InputStream` or `ResponseEntity<InputStream>` that provides access to the raw response
body content.
[[rest-http-interface-exceptions]] [[rest-http-interface-exceptions]]
=== Error Handling === Error Handling
To customize error response handling, you need to configure the underlying HTTP client. To customize error handling for HTTP Service client proxies, you can configure the
underlying client as needed. By default, clients raise an exception for 4xx and 5xx HTTP
For `RestClient`: status codes. To customize this, register a response status handler that applies to all
responses performed through the client as follows:
By default, `RestClient` raises `RestClientException` for 4xx and 5xx HTTP status codes.
To customize this, register a response status handler that applies to all responses
performed through the client:
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
// For RestClient
RestClient restClient = RestClient.builder() RestClient restClient = RestClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...) .defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...)
.build(); .build();
RestClientAdapter adapter = RestClientAdapter.create(restClient); RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
----
For more details and options, such as suppressing error status codes, see the Javadoc of // or for WebClient...
`defaultStatusHandler` in `RestClient.Builder`.
For `WebClient`:
By default, `WebClient` raises `WebClientResponseException` for 4xx and 5xx HTTP status codes.
To customize this, register a response status handler that applies to all responses
performed through the client:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebClient webClient = WebClient.builder() WebClient webClient = WebClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, resp -> ...) .defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
.build(); .build();
WebClientAdapter adapter = WebClientAdapter.create(webClient); WebClientAdapter adapter = WebClientAdapter.create(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(adapter).build();
----
For more details and options, such as suppressing error status codes, see the Javadoc of // or for RestTemplate...
`defaultStatusHandler` in `WebClient.Builder`.
For `RestTemplate`:
By default, `RestTemplate` raises `RestClientException` for 4xx and 5xx HTTP status codes.
To customize this, register an error handler that applies to all responses
performed through the client:
[source,java,indent=0,subs="verbatim,quotes"]
----
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(myErrorHandler); restTemplate.setErrorHandler(myErrorHandler);
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
---- ----
For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and For more details and options such as suppressing error status codes, see the reference
the `ResponseErrorHandler` hierarchy. documentation for each client, as well as the Javadoc of `defaultStatusHandler` in
`RestClient.Builder` or `WebClient.Builder`, and the `setErrorHandler` of `RestTemplate`.
[[rest-http-interface-group-config]]
=== HTTP Interface Groups
It's trivial to create client proxies with `HttpServiceProxyFactory`, but to have them
declared as beans leads to repetitive configuration. You may also have multiple
target hosts, and therefore multiple clients to configure, and even more client proxy
beans to create.
To make it easier to work with interface clients at scale the Spring Framework provides
dedicated configuration support. It lets applications focus on identifying HTTP Services
by group, and customizing the client for each group, while the framework transparently
creates a registry of client proxies, and declares each proxy as a bean.
An HTTP Service group is simply a set of interfaces that share the same client setup and
`HttpServiceProxyFactory` instance to create proxies. Typically, that means one group per
host, but you can have more than one group for the same target host in case the
underlying client needs to be configured differently.
One way to declare HTTP Service groups is via `@ImportHttpServices` annotations in
`@Configuration` classes as shown below:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) // <1>
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) // <2>
public class ClientConfig {
}
----
<1> Manually list interfaces for group "echo"
<2> Detect interfaces for group "greeting" under a base package
It is also possible to declare groups programmatically by creating an HTTP Service
registrar and then importing it:
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyHttpServiceRegistrar extends AbstractHttpServiceRegistrar { // <1>
@Override
protected void registerHttpServices(GroupRegistry registry, AnnotationMetadata metadata) {
registry.forGroup("echo").register(EchoServiceA.class, EchoServiceB.class); // <2>
registry.forGroup("greeting").detectInBasePackages(GreetServiceA.class); // <3>
}
}
@Configuration
@Import(MyHttpServiceRegistrar.class) // <4>
public class ClientConfig {
}
----
<1> Create extension class of `AbstractHttpServiceRegistrar`
<2> Manually list interfaces for group "echo"
<3> Detect interfaces for group "greeting" under a base package
<4> Import the registrar
TIP: You can mix and match `@ImportHttpService` annotations with programmatic registrars,
and you can spread the imports across multiple configuration classes. All imports
contribute collaboratively the same, shared `HttpServiceProxyRegistry` instance.
Once HTTP Service groups are declared, add an `HttpServiceGroupConfigurer` bean to
customize the client for each group. For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class})
@ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class)
public class ClientConfig {
@Bean
public RestClientHttpServiceGroupConfigurer groupConfigurer() {
return groups -> {
// configure client for group "echo"
groups.filterByName("echo").forEachClient((group, clientBuilder) -> ...);
// configure the clients for all groups
groups.forEachClient((group, clientBuilder) -> ...);
// configure client and proxy factory for each group
groups.forEachGroup((group, clientBuilder, factoryBuilder) -> ...);
};
}
}
----
TIP: Spring Boot uses an `HttpServiceGroupConfigurer` to add support for client properties
by HTTP Service group, Spring Security to add OAuth support, and Spring Cloud to add load
balancing.
As a result of the above, each client proxy is available as a bean that you can
conveniently autowire by type:
[source,java,indent=0,subs="verbatim,quotes"]
----
@RestController
public class EchoController {
private final EchoService echoService;
public EchoController(EchoService echoService) {
this.echoService = echoService;
}
// ...
}
----
However, if there are multiple client proxies of the same type, e.g. the same interface
in multiple groups, then there is no unique bean of that type, and you cannot autowire by
type only. For such cases, you can work directly with the `HttpServiceProxyRegistry` that
holds all proxies, and obtain the ones you need by group:
[source,java,indent=0,subs="verbatim,quotes"]
----
@RestController
public class EchoController {
private final EchoService echoService1;
private final EchoService echoService2;
public EchoController(HttpServiceProxyRegistry registry) {
this.echoService1 = registry.getClient("echo1", EchoService.class); // <1>
this.echoService2 = registry.getClient("echo2", EchoService.class); // <2>
}
// ...
}
----
<1> Access the `EchoService` client proxy for group "echo1"
<2> Access the `EchoService` client proxy for group "echo2"

View File

@ -255,5 +255,3 @@ For Kotlin `Flow`, a `Flow<T>.transactional` extension is provided.
---- ----

View File

@ -190,7 +190,7 @@ NOTE: If you use Spring Boot, you should probably use
instead of `@Value` annotations. instead of `@Value` annotations.
As an alternative, you can customize the property placeholder prefix by declaring the As an alternative, you can customize the property placeholder prefix by declaring the
following configuration beans: following `PropertySourcesPlaceholderConfigurer` bean:
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
@ -200,8 +200,10 @@ following configuration beans:
} }
---- ----
You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`) You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use
that uses the `${...}` syntax, with configuration beans, as the following example shows: the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by
declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example
shows:
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer() fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
---- ----
In addition, the default escape character can be changed or disabled globally by setting
the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or
via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
[[checked-exceptions]] [[checked-exceptions]]
@ -296,17 +301,17 @@ for example when writing a `org.springframework.core.convert.converter.Converter
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> { class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
// ... // ...
} }
---- ----
When converting any kind of objects, star projection with `*` can be used instead of `out Any`. When converting any kind of objects, star projection with `*` can be used instead of `out Any`.
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> { class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
// ... // ...
} }
---- ----
NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans, NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans,
@ -319,7 +324,7 @@ progresses.
== Testing == Testing
This section addresses testing with the combination of Kotlin and Spring Framework. This section addresses testing with the combination of Kotlin and Spring Framework.
The recommended testing framework is https://junit.org/junit5/[JUnit 5] along with The recommended testing framework is https://junit.org/junit5/[JUnit] along with
https://mockk.io/[Mockk] for mocking. https://mockk.io/[Mockk] for mocking.
NOTE: If you are using Spring Boot, see NOTE: If you are using Spring Boot, see
@ -330,7 +335,7 @@ NOTE: If you are using Spring Boot, see
=== Constructor injection === Constructor injection
As described in the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[dedicated section], As described in the xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[dedicated section],
JUnit Jupiter (JUnit 5) allows constructor injection of beans which is pretty useful with Kotlin JUnit Jupiter allows constructor injection of beans which is pretty useful with Kotlin
in order to use `val` instead of `lateinit var`. You can use in order to use `val` instead of `lateinit var`. You can use
{spring-framework-api}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`] {spring-framework-api}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`]
to enable autowiring for all parameters. to enable autowiring for all parameters.
@ -340,13 +345,14 @@ file with a `spring.test.constructor.autowire.mode = all` property.
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
@SpringJUnitConfig(TestConfig::class) @SpringJUnitConfig(TestConfig::class)
@TestConstructor(autowireMode = AutowireMode.ALL) @TestConstructor(autowireMode = AutowireMode.ALL)
class OrderServiceIntegrationTests(val orderService: OrderService, class OrderServiceIntegrationTests(
val orderService: OrderService,
val customerService: CustomerService) { val customerService: CustomerService) {
// tests that use the injected OrderService and CustomerService // tests that use the injected OrderService and CustomerService
} }
---- ----
@ -354,7 +360,7 @@ class OrderServiceIntegrationTests(val orderService: OrderService,
=== `PER_CLASS` Lifecycle === `PER_CLASS` Lifecycle
Kotlin lets you specify meaningful test function names between backticks (```). Kotlin lets you specify meaningful test function names between backticks (```).
With JUnit Jupiter (JUnit 5), Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)` 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` 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. and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin.
@ -398,16 +404,17 @@ class IntegrationTests {
[[specification-like-tests]] [[specification-like-tests]]
=== Specification-like Tests === Specification-like Tests
You can create specification-like tests with JUnit 5 and Kotlin. You can create specification-like tests with Kotlin and JUnit Jupiter's `@Nested` test
The following example shows how to do so: class support. The following example shows how to do so:
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
class SpecificationLikeTests { class SpecificationLikeTests {
@Nested @Nested
@DisplayName("a calculator") @DisplayName("a calculator")
inner class Calculator { inner class Calculator {
val calculator = SampleCalculator() val calculator = SampleCalculator()
@Test @Test
@ -422,7 +429,7 @@ class SpecificationLikeTests {
assertEquals(2, subtract) assertEquals(2, subtract)
} }
} }
} }
---- ----

View File

@ -1,8 +1,6 @@
[[kotlin-web]] [[kotlin-web]]
= Web = Web
[[router-dsl]] [[router-dsl]]
== Router DSL == Router DSL
@ -16,8 +14,8 @@ These DSL let you write clean and idiomatic Kotlin code to build a `RouterFuncti
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
@Configuration @Configuration
class RouterRouterConfiguration { class RouterRouterConfiguration {
@Bean @Bean
fun mainRouter(userHandler: UserHandler) = router { fun mainRouter(userHandler: UserHandler) = router {
@ -36,7 +34,7 @@ class RouterRouterConfiguration {
} }
resources("/**", ClassPathResource("static/")) resources("/**", ClassPathResource("static/"))
} }
} }
---- ----
NOTE: This DSL is programmatic, meaning that it allows custom registration logic of beans NOTE: This DSL is programmatic, meaning that it allows custom registration logic of beans
@ -55,22 +53,22 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met
[source,kotlin,indent=0] [source,kotlin,indent=0]
---- ----
val mockMvc: MockMvc = ... val mockMvc: MockMvc = ...
mockMvc.get("/person/{name}", "Lee") { mockMvc.get("/person/{name}", "Lee") {
secure = true secure = true
accept = APPLICATION_JSON accept = APPLICATION_JSON
headers { headers {
contentLanguage = Locale.FRANCE contentLanguage = Locale.FRANCE
} }
principal = Principal { "foo" } principal = Principal { "foo" }
}.andExpect { }.andExpect {
status { isOk } status { isOk }
content { contentType(APPLICATION_JSON) } content { contentType(APPLICATION_JSON) }
jsonPath("$.name") { value("Lee") } jsonPath("$.name") { value("Lee") }
content { json("""{"someBoolean": false}""", false) } content { json("""{"someBoolean": false}""", false) }
}.andDo { }.andDo {
print() print()
} }
---- ----

View File

@ -2,8 +2,8 @@
= Spring JUnit Jupiter Testing Annotations = Spring JUnit Jupiter Testing Annotations
The following annotations are supported when used in conjunction with the The following annotations are supported when used in conjunction with the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`] and JUnit Jupiter xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
(that is, the programming model in JUnit 5): and JUnit Jupiter (that is, the programming model in JUnit):
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitconfig[`@SpringJUnitConfig`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitconfig[`@SpringJUnitConfig`]
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitwebconfig[`@SpringJUnitWebConfig`] * xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitwebconfig[`@SpringJUnitWebConfig`]

View File

@ -1,9 +1,17 @@
[[integration-testing-annotations-junit4]] [[integration-testing-annotations-junit4]]
= Spring JUnit 4 Testing Annotations = Spring JUnit 4 Testing Annotations
[WARNING]
====
JUnit 4 support is deprecated since Spring Framework 7.0 in favor of the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
and JUnit Jupiter.
====
The following annotations are supported only when used in conjunction with the The following annotations are supported only when used in conjunction with the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules] xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner],
, or xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]: xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules], or
xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]:
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-ifprofilevalue[`@IfProfileValue`] * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-ifprofilevalue[`@IfProfileValue`]
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`] * xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`]
@ -205,6 +213,3 @@ Kotlin::
---- ----
<1> Repeat this test ten times. <1> Repeat this test ten times.
====== ======

View File

@ -140,8 +140,8 @@ Kotlin::
====== ======
If we write tests that use JUnit Jupiter, we can reduce code duplication even further, If we write tests that use JUnit Jupiter, we can reduce code duplication even further,
since annotations in JUnit 5 can also be used as meta-annotations. Consider the following since annotations in JUnit Jupiter can also be used as meta-annotations. Consider the
example: following example:
[tabs] [tabs]
====== ======

View File

@ -47,6 +47,21 @@ the same bean in several test classes, make sure to name the fields consistently
creating unnecessary contexts. creating unnecessary contexts.
==== ====
[WARNING]
====
Using `@MockitoBean` or `@MockitoSpyBean` in conjunction with `@ContextHierarchy` can
lead to undesirable results since each `@MockitoBean` or `@MockitoSpyBean` will be
applied to all context hierarchy levels by default. To ensure that a particular
`@MockitoBean` or `@MockitoSpyBean` is applied to a single context hierarchy level, set
the `contextName` attribute to match a configured `@ContextConfiguration` name for
example, `@MockitoBean(contextName = "app-config")` or
`@MockitoSpyBean(contextName = "app-config")`.
See
xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
hierarchies with bean overrides] for further details and examples.
====
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior. Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE` The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`

View File

@ -31,6 +31,19 @@ same bean in several tests, make sure to name the field consistently to avoid cr
unnecessary contexts. unnecessary contexts.
==== ====
[WARNING]
====
Using `@TestBean` in conjunction with `@ContextHierarchy` can lead to undesirable results
since each `@TestBean` will be applied to all context hierarchy levels by default. To
ensure that a particular `@TestBean` is applied to a single context hierarchy level, set
the `contextName` attribute to match a configured `@ContextConfiguration` name for
example, `@TestBean(contextName = "app-config")`.
See
xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
hierarchies with bean overrides] for further details and examples.
====
[NOTE] [NOTE]
==== ====
There are no restrictions on the visibility of `@TestBean` fields or factory methods. There are no restrictions on the visibility of `@TestBean` fields or factory methods.

View File

@ -166,7 +166,7 @@ following sections to make this pattern much easier to implement.
== MockMvc and WebDriver Setup == MockMvc and WebDriver Setup
To use Selenium WebDriver with `MockMvc`, make sure that your project includes a test To use Selenium WebDriver with `MockMvc`, make sure that your project includes a test
dependency on `org.seleniumhq.selenium:selenium-htmlunit3-driver`. dependency on `org.seleniumhq.selenium:htmlunit3-driver`.
We can easily create a Selenium WebDriver that integrates with MockMvc by using the We can easily create a Selenium WebDriver that integrates with MockMvc by using the
`MockMvcHtmlUnitDriverBuilder` as the following example shows: `MockMvcHtmlUnitDriverBuilder` as the following example shows:

View File

@ -1,10 +1,26 @@
[[spring-mvc-test-client]] [[spring-mvc-test-client]]
= Testing Client Applications = Testing Client Applications
You can use client-side tests to test code that internally uses the `RestTemplate`. The To test code that uses the `RestClient` or `RestTemplate`, you can use a mock web server, such as
idea is to declare expected requests and to provide "`stub`" responses so that you can https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or
focus on testing the code in isolation (that is, without running a server). The following https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular
example shows how to do so: server, and that means you can test with the same HTTP client that is also configured in
the same way as in production, which is important because there are often subtle
differences in the way different clients handle network I/O. Another advantage of mock
web servers is the ability to simulate specific network issues and conditions at the
transport level, in combination with the client used in production.
In addition to dedicated mock web servers, historically the Spring Framework has provided
a built-in option to test `RestClient` or `RestTemplate` through `MockRestServiceServer`.
This relies on configuring the client under test with a custom `ClientHttpRequestFactory`
backed by the mock server that is in turn set up to expect requests and send "`stub`"
responses so that you can focus on testing the code in isolation, without running a server.
TIP: `MockRestServiceServer` predates the existence of mock web servers. At present, we
recommend using mock web servers for more complete testing of the transport layer and
network conditions.
The following example shows an example of using `MockRestServiceServer`:
[tabs] [tabs]
====== ======

View File

@ -9,11 +9,11 @@ deal of importance on convention over configuration, with reasonable defaults th
can override through annotation-based configuration. can override through annotation-based configuration.
In addition to generic testing infrastructure, the TestContext framework provides In addition to generic testing infrastructure, the TestContext framework provides
explicit support for JUnit 4, JUnit Jupiter (AKA JUnit 5), and TestNG. For JUnit 4 and explicit support for JUnit Jupiter, JUnit 4, and TestNG. For JUnit 4 and TestNG, Spring
TestNG, Spring provides `abstract` support classes. Furthermore, Spring provides a custom provides `abstract` support classes. Furthermore, Spring provides a custom JUnit `Runner`
JUnit `Runner` and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit Jupiter that let
Jupiter that let you write so-called POJO test classes. POJO test classes are not you write so-called POJO test classes. POJO test classes are not required to extend a
required to extend a particular class hierarchy, such as the `abstract` support classes. particular class hierarchy, such as the `abstract` support classes.
The following section provides an overview of the internals of the TestContext framework. The following section provides an overview of the internals of the TestContext framework.
If you are interested only in using the framework and are not interested in extending it If you are interested only in using the framework and are not interested in extending it

View File

@ -4,7 +4,7 @@
Once the TestContext framework loads an `ApplicationContext` (or `WebApplicationContext`) Once the TestContext framework loads an `ApplicationContext` (or `WebApplicationContext`)
for a test, that context is cached and reused for all subsequent tests that declare the for a test, that context is cached and reused for all subsequent tests that declare the
same unique context configuration within the same test suite. To understand how caching same unique context configuration within the same test suite. To understand how caching
works, it is important to understand what is meant by "`unique`" and "`test suite.`" works, it is important to understand what is meant by "unique" and "test suite."
An `ApplicationContext` can be uniquely identified by the combination of configuration An `ApplicationContext` can be uniquely identified by the combination of configuration
parameters that is used to load it. Consequently, the unique combination of configuration parameters that is used to load it. Consequently, the unique combination of configuration
@ -15,8 +15,8 @@ framework uses the following configuration parameters to build the context cache
* `classes` (from `@ContextConfiguration`) * `classes` (from `@ContextConfiguration`)
* `contextInitializerClasses` (from `@ContextConfiguration`) * `contextInitializerClasses` (from `@ContextConfiguration`)
* `contextCustomizers` (from `ContextCustomizerFactory`) this includes * `contextCustomizers` (from `ContextCustomizerFactory`) this includes
`@DynamicPropertySource` methods as well as various features from Spring Boot's `@DynamicPropertySource` methods, bean overrides (such as `@TestBean`, `@MockitoBean`,
testing support such as `@MockBean` and `@SpyBean`. `@MockitoSpyBean` etc.), as well as various features from Spring Boot's testing support.
* `contextLoader` (from `@ContextConfiguration`) * `contextLoader` (from `@ContextConfiguration`)
* `parent` (from `@ContextHierarchy`) * `parent` (from `@ContextHierarchy`)
* `activeProfiles` (from `@ActiveProfiles`) * `activeProfiles` (from `@ActiveProfiles`)

View File

@ -1,5 +1,5 @@
[[testcontext-context-customizers]] [[testcontext-context-customizers]]
= Configuration Configuration with Context Customizers = Context Configuration with Context Customizers
A `ContextCustomizer` is responsible for customizing the supplied A `ContextCustomizer` is responsible for customizing the supplied
`ConfigurableApplicationContext` after bean definitions have been loaded into the context `ConfigurableApplicationContext` after bean definitions have been loaded into the context

View File

@ -22,8 +22,19 @@ given level in the hierarchy, the configuration resource type (that is, XML conf
files or component classes) must be consistent. Otherwise, it is perfectly acceptable to files or component classes) must be consistent. Otherwise, it is perfectly acceptable to
have different levels in a context hierarchy configured using different resource types. have different levels in a context hierarchy configured using different resource types.
The remaining JUnit Jupiter based examples in this section show common configuration [NOTE]
scenarios for integration tests that require the use of context hierarchies. ====
If you use `@DirtiesContext` in a test whose context is configured as part of a context
hierarchy, you can use the `hierarchyMode` flag to control how the context cache is
cleared.
For further details, see the discussion of `@DirtiesContext` in
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations]
and the {spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
====
The JUnit Jupiter based examples in this section show common configuration scenarios for
integration tests that require the use of context hierarchies.
**Single test class with context hierarchy** **Single test class with context hierarchy**
-- --
@ -229,12 +240,118 @@ Kotlin::
class ExtendedTests : BaseTests() {} class ExtendedTests : BaseTests() {}
---- ----
====== ======
.Dirtying a context within a context hierarchy
NOTE: If you use `@DirtiesContext` in a test whose context is configured as part of a
context hierarchy, you can use the `hierarchyMode` flag to control how the context cache
is cleared. For further details, see the discussion of `@DirtiesContext` in
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] and the
{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
-- --
[[testcontext-ctx-management-ctx-hierarchies-with-bean-overrides]]
**Context hierarchies with bean overrides**
--
When `@ContextHierarchy` is used in conjunction with
xref:testing/testcontext-framework/bean-overriding.adoc[bean overrides] such as
`@TestBean`, `@MockitoBean`, or `@MockitoSpyBean`, it may be desirable or necessary to
have the override applied to a single level in the context hierarchy. To achieve that,
the bean override must specify a context name that matches a name configured via the
`name` attribute in `@ContextConfiguration`.
The following test class configures the name of the second hierarchy level to be
`"user-config"` and simultaneously specifies that the `UserService` should be wrapped in
a Mockito spy in the context named `"user-config"`. Consequently, Spring will only
attempt to create the spy in the `"user-config"` context and will not attempt to create
the spy in the parent context.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(classes = AppConfig.class),
@ContextConfiguration(classes = UserConfig.class, name = "user-config")
})
class IntegrationTests {
@MockitoSpyBean(contextName = "user-config")
UserService userService;
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
ContextConfiguration(classes = [AppConfig::class]),
ContextConfiguration(classes = [UserConfig::class], name = "user-config"))
class IntegrationTests {
@MockitoSpyBean(contextName = "user-config")
lateinit var userService: UserService
// ...
}
----
======
When applying bean overrides in different levels of the context hierarchy, you may need
to have all of the bean override instances injected into the test class in order to
interact with them — for example, to configure stubbing for mocks. However, `@Autowired`
will always inject a matching bean found in the lowest level of the context hierarchy.
Thus, to inject bean override instances from specific levels in the context hierarchy,
you need to annotate fields with appropriate bean override annotations and configure the
name of the context level.
The following test class configures the names of the hierarchy levels to be `"parent"`
and `"child"`. It also declares two `PropertyService` fields that are configured to
create or replace `PropertyService` beans with Mockito mocks in the respective contexts,
named `"parent"` and `"child"`. Consequently, the mock from the `"parent"` context will
be injected into the `propertyServiceInParent` field, and the mock from the `"child"`
context will be injected into the `propertyServiceInChild` field.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(classes = ParentConfig.class, name = "parent"),
@ContextConfiguration(classes = ChildConfig.class, name = "child")
})
class IntegrationTests {
@MockitoBean(contextName = "parent")
PropertyService propertyServiceInParent;
@MockitoBean(contextName = "child")
PropertyService propertyServiceInChild;
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
ContextConfiguration(classes = [ParentConfig::class], name = "parent"),
ContextConfiguration(classes = [ChildConfig::class], name = "child"))
class IntegrationTests {
@MockitoBean(contextName = "parent")
lateinit var propertyServiceInParent: PropertyService
@MockitoBean(contextName = "child")
lateinit var propertyServiceInChild: PropertyService
// ...
}
----
======
--

View File

@ -250,7 +250,7 @@ Java::
@SqlGroup({ @SqlGroup({
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")), @Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
@Sql("/test-user-data.sql") @Sql("/test-user-data.sql")
)} })
void userTest() { void userTest() {
// run code that uses the test schema and test data // run code that uses the test schema and test data
} }

View File

@ -18,9 +18,9 @@ Do not run tests in parallel if the tests:
* Use Spring Framework's `@DirtiesContext` support. * Use Spring Framework's `@DirtiesContext` support.
* Use Spring Framework's `@MockitoBean` or `@MockitoSpyBean` support. * Use Spring Framework's `@MockitoBean` or `@MockitoSpyBean` support.
* Use Spring Boot's `@MockBean` or `@SpyBean` support. * Use Spring Boot's `@MockBean` or `@SpyBean` support.
* Use JUnit 4's `@FixMethodOrder` support or any testing framework feature * Use JUnit Jupiter's `@TestMethodOrder` support or any testing framework feature that is
that is designed to ensure that test methods run in a particular order. Note, designed to ensure that test methods run in a particular order. Note, however, that
however, that this does not apply if entire test classes are run in parallel. this does not apply if entire test classes are run in parallel.
* Change the state of shared services or systems such as a database, message broker, * Change the state of shared services or systems such as a database, message broker,
filesystem, and others. This applies to both embedded and external systems. filesystem, and others. This applies to both embedded and external systems.

View File

@ -1,172 +1,15 @@
[[testcontext-support-classes]] [[testcontext-support-classes]]
= TestContext Framework Support Classes = TestContext Framework Support Classes
This section describes the various classes that support the Spring TestContext Framework. This section describes the various classes that support the Spring TestContext Framework
in JUnit and TestNG.
[[testcontext-junit4-runner]]
== Spring JUnit 4 Runner
The Spring TestContext Framework offers full integration with JUnit 4 through a custom
runner (supported on JUnit 4.12 or higher). By annotating test classes with
`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
variant, developers can implement standard JUnit 4-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. If you want to use the Spring TestContext Framework with an
alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
(such as the `MockitoJUnitRunner`), you can, optionally, use
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] instead.
The following code listing shows the minimal requirements for configuring a test class to
run with the custom Spring `Runner`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {
@Test
fun testMethod() {
// test logic...
}
}
----
======
In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
disable the default listeners, which otherwise would require an `ApplicationContext` to
be configured through `@ContextConfiguration`.
[[testcontext-junit4-rules]]
== Spring JUnit 4 Rules
The `org.springframework.test.context.junit4.rules` package provides the following JUnit
4 rules (supported on JUnit 4.12 or higher):
* `SpringClassRule`
* `SpringMethodRule`
`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
instance-level and method-level features of the Spring TestContext Framework.
In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
third-party runners (such as the `MockitoJUnitRunner`).
To support the full functionality of the TestContext framework, you must combine a
`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
to declare these rules in an integration test:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {
@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
class IntegrationTest {
@Rule
val springMethodRule = SpringMethodRule()
@Test
fun testMethod() {
// test logic...
}
companion object {
@ClassRule
val springClassRule = SpringClassRule()
}
}
----
======
[[testcontext-support-classes-junit4]]
== JUnit 4 Support Classes
The `org.springframework.test.context.junit4` package provides the following support
classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
* `AbstractJUnit4SpringContextTests`
* `AbstractTransactionalJUnit4SpringContextTests`
`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
Spring TestContext Framework with explicit `ApplicationContext` testing support in a
JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
`protected` `applicationContext` instance variable that you can use to perform explicit
bean lookups or to test the state of the context as a whole.
`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
access. This class expects a `javax.sql.DataSource` bean and a
`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
`jdbcTemplate` instance variable that you can use to run SQL statements to query the
database. You can use such queries to confirm database state both before and after
running database-related application code, and Spring ensures that such queries run in
the scope of the same transaction as the application code. When used in conjunction with
an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
TIP: These classes are a convenience for extension. If you do not want your test classes
to be tied to a Spring-specific class hierarchy, you can configure your own custom test
classes by using `@RunWith(SpringRunner.class)` or xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules]
.
[[testcontext-junit-jupiter-extension]] [[testcontext-junit-jupiter-extension]]
== SpringExtension for JUnit Jupiter == SpringExtension for JUnit Jupiter
The Spring TestContext Framework offers full integration with the JUnit Jupiter testing The Spring TestContext Framework offers full integration with the JUnit Jupiter testing
framework, introduced in JUnit 5. By annotating test classes with framework, originally introduced in JUnit 5. By annotating test classes with
`@ExtendWith(SpringExtension.class)`, you can implement standard JUnit Jupiter-based unit `@ExtendWith(SpringExtension.class)`, you can implement standard JUnit Jupiter-based unit
and integration tests and simultaneously reap the benefits of the TestContext framework, and integration tests and simultaneously reap the benefits of the TestContext framework,
such as support for loading application contexts, dependency injection of test instances, such as support for loading application contexts, dependency injection of test instances,
@ -177,14 +20,17 @@ following features above and beyond the feature set that Spring supports for JUn
TestNG: TestNG:
* Dependency injection for test constructors, test methods, and test lifecycle callback * 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. 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://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
test execution] based on SpEL expressions, environment variables, system properties, test execution] based on SpEL expressions, environment variables, system properties,
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details and examples. xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
for further details and examples.
* Custom composed annotations that combine annotations from Spring and JUnit Jupiter. See * Custom composed annotations that combine annotations from Spring and JUnit Jupiter. See
the `@TransactionalDevTestConfig` and `@TransactionalIntegrationTest` examples in the `@TransactionalDevTestConfig` and `@TransactionalIntegrationTest` examples in
xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for further details. xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for
further details.
The following code listing shows how to configure a test class to use the The following code listing shows how to configure a test class to use the
`SpringExtension` in conjunction with `@ContextConfiguration`: `SpringExtension` in conjunction with `@ContextConfiguration`:
@ -226,8 +72,8 @@ Kotlin::
---- ----
====== ======
Since you can also use annotations in JUnit 5 as meta-annotations, Spring provides the Since you can also use annotations in JUnit Jupiter as meta-annotations, Spring provides
`@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the the `@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the
configuration of the test `ApplicationContext` and JUnit Jupiter. configuration of the test `ApplicationContext` and JUnit Jupiter.
The following example uses `@SpringJUnitConfig` to reduce the amount of configuration The following example uses `@SpringJUnitConfig` to reduce the amount of configuration
@ -307,7 +153,8 @@ Kotlin::
====== ======
See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details. xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
for further details.
[[testcontext-junit-jupiter-di]] [[testcontext-junit-jupiter-di]]
=== Dependency Injection with the `SpringExtension` === Dependency Injection with the `SpringExtension`
@ -318,10 +165,9 @@ extension API from JUnit Jupiter, which lets Spring provide dependency injection
constructors, test methods, and test lifecycle callback methods. constructors, test methods, and test lifecycle callback methods.
Specifically, the `SpringExtension` can inject dependencies from the test's Specifically, the `SpringExtension` can inject dependencies from the test's
`ApplicationContext` into test constructors and methods that are annotated with `ApplicationContext` into test constructors and methods that are annotated with Spring's
Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@AfterAll`,
`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and others.
and others.
[[testcontext-junit-jupiter-di-constructor]] [[testcontext-junit-jupiter-di-constructor]]
@ -341,8 +187,9 @@ autowirable if one of the following conditions is met (in order of precedence).
attribute set to `ALL`. attribute set to `ALL`.
* The default _test constructor autowire mode_ has been changed to `ALL`. * The default _test constructor autowire mode_ has been changed to `ALL`.
See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] for details on the use of See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]
`@TestConstructor` and how to change the global _test constructor autowire mode_. for details on the use of `@TestConstructor` and how to change the global _test
constructor autowire mode_.
WARNING: If the constructor for a test class is considered to be _autowirable_, Spring WARNING: If the constructor for a test class is considered to be _autowirable_, Spring
assumes the responsibility for resolving arguments for all parameters in the constructor. assumes the responsibility for resolving arguments for all parameters in the constructor.
@ -407,8 +254,9 @@ Kotlin::
Note that this feature lets test dependencies be `final` and therefore immutable. Note that this feature lets test dependencies be `final` and therefore immutable.
If the `spring.test.constructor.autowire.mode` property is to `all` (see If the `spring.test.constructor.autowire.mode` property is to `all` (see
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), we can omit the declaration of xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]),
`@Autowired` on the constructor in the previous example, resulting in the following. we can omit the declaration of `@Autowired` on the constructor in the previous example,
resulting in the following.
[tabs] [tabs]
====== ======
@ -553,17 +401,19 @@ honor `@NestedTestConfiguration` semantics.
In order to allow development teams to change the default to `OVERRIDE` for example, In order to allow development teams to change the default to `OVERRIDE` for example,
for compatibility with Spring Framework 5.0 through 5.2 the default mode can be changed for compatibility with Spring Framework 5.0 through 5.2 the default mode can be changed
globally via a JVM system property or a `spring.properties` file in the root of the globally via a JVM system property or a `spring.properties` file in the root of the
classpath. See the xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"] classpath. See the
note for details. xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"]
note for details.
Although the following "Hello World" example is very simplistic, it shows how to declare Although the following "Hello World" example is very simplistic, it shows how to declare
common configuration on a top-level class that is inherited by its `@Nested` test common configuration on a top-level class that is inherited by its `@Nested` test
classes. In this particular example, only the `TestConfig` configuration class is classes. In this particular example, only the `TestConfig` configuration class is
inherited. Each nested test class provides its own set of active profiles, resulting in a inherited. Each nested test class provides its own set of active profiles, resulting in a
distinct `ApplicationContext` for each nested test class (see distinct `ApplicationContext` for each nested test class (see
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). Consult the list of xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details).
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] to see Consult the list of
which annotations can be inherited in `@Nested` test classes. xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations]
to see which annotations can be inherited in `@Nested` test classes.
[tabs] [tabs]
====== ======
@ -626,8 +476,198 @@ Kotlin::
---- ----
====== ======
[[testcontext-junit4-support]]
== JUnit 4 Support
[[testcontext-junit4-runner]]
=== Spring JUnit 4 Runner
[WARNING]
====
JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated
since Spring Framework 7.0 in favor of the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
and JUnit Jupiter.
====
The Spring TestContext Framework offers full integration with JUnit 4 through a custom
runner (supported on JUnit 4.12 or higher). By annotating test classes with
`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
variant, developers can implement standard JUnit 4-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. If you want to use the Spring TestContext Framework with an
alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
(such as the `MockitoJUnitRunner`), you can, optionally, use
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules]
instead.
The following code listing shows the minimal requirements for configuring a test class to
run with the custom Spring `Runner`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {
@Test
fun testMethod() {
// test logic...
}
}
----
======
In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
disable the default listeners, which otherwise would require an `ApplicationContext` to
be configured through `@ContextConfiguration`.
[[testcontext-junit4-rules]]
=== Spring JUnit 4 Rules
[WARNING]
====
JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated
since Spring Framework 7.0 in favor of the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
and JUnit Jupiter.
====
The `org.springframework.test.context.junit4.rules` package provides the following JUnit
4 rules (supported on JUnit 4.12 or higher):
* `SpringClassRule`
* `SpringMethodRule`
`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
instance-level and method-level features of the Spring TestContext Framework.
In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
third-party runners (such as the `MockitoJUnitRunner`).
To support the full functionality of the TestContext framework, you must combine a
`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
to declare these rules in an integration test:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {
@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
class IntegrationTest {
@Rule
val springMethodRule = SpringMethodRule()
@Test
fun testMethod() {
// test logic...
}
companion object {
@ClassRule
val springClassRule = SpringClassRule()
}
}
----
======
[[testcontext-support-classes-junit4]]
=== JUnit 4 Base Classes
[WARNING]
====
JUnit 4 is officially in maintenance mode, and JUnit 4 support in Spring is deprecated
since Spring Framework 7.0 in favor of the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
and JUnit Jupiter.
====
The `org.springframework.test.context.junit4` package provides the following support
classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
* `AbstractJUnit4SpringContextTests`
* `AbstractTransactionalJUnit4SpringContextTests`
`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
Spring TestContext Framework with explicit `ApplicationContext` testing support in a
JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
`protected` `applicationContext` instance variable that you can use to perform explicit
bean lookups or to test the state of the context as a whole.
`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
access. This class expects a `javax.sql.DataSource` bean and a
`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
`jdbcTemplate` instance variable that you can use to run SQL statements to query the
database. You can use such queries to confirm database state both before and after
running database-related application code, and Spring ensures that such queries run in
the scope of the same transaction as the application code. When used in conjunction with
an ORM tool, be sure to avoid
xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
TIP: These classes are a convenience for extension. If you do not want your test classes
to be tied to a Spring-specific class hierarchy, you can configure your own custom test
classes by using `@RunWith(SpringRunner.class)` or
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules].
[[testcontext-support-classes-testng]] [[testcontext-support-classes-testng]]
== TestNG Support Classes == TestNG Support
The `org.springframework.test.context.testng` package provides the following support The `org.springframework.test.context.testng` package provides the following support
classes for TestNG based test cases: classes for TestNG based test cases:
@ -650,7 +690,8 @@ extend `AbstractTransactionalTestNGSpringContextTests`, you can access a `protec
database. You can use such queries to confirm database state both before and after database. You can use such queries to confirm database state both before and after
running database-related application code, and Spring ensures that such queries run in running database-related application code, and Spring ensures that such queries run in
the scope of the same transaction as the application code. When used in conjunction with the scope of the same transaction as the application code. When used in conjunction with
an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives]. an ORM tool, be sure to avoid
xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support], As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
`AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that `AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`. delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.

View File

@ -265,6 +265,7 @@ Java::
client = WebTestClient.bindToController(new TestController()) client = WebTestClient.bindToController(new TestController())
.configureClient() .configureClient()
.baseUrl("/test") .baseUrl("/test")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.build(); .build();
---- ----
@ -275,6 +276,7 @@ Kotlin::
client = WebTestClient.bindToController(TestController()) client = WebTestClient.bindToController(TestController())
.configureClient() .configureClient()
.baseUrl("/test") .baseUrl("/test")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.build() .build()
---- ----
====== ======

View File

@ -0,0 +1,85 @@
[[webflux-versioning]]
= API Versioning
:page-section-summary-toc: 1
[.small]#xref:web/webmvc-versioning.adoc[See equivalent in the Servlet stack]#
Spring WebFlux supports API versioning. This section provides an overview of the support
and underlying strategies.
Please, see also related content in:
- Use xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[API Versions]
to map requests to annotated controller methods
- Configure API versioning in xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config]
TIP: API versioning is also supported on the client side in `RestClient`, `WebClient`, and
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as
for testing with `WebTestClient`.
[[webflux-versioning-strategy]]
== ApiVersionStrategy
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[See equivalent in the Servlet stack]#
This strategy holds all application preferences about how to manage versioning.
It delegates to xref:#webflux-versioning-resolver[ApiVersionResolver] to resolve versions
from requests, and to xref:#webflux-versioning-parser[ApiVersionParser] to parse raw version
values into `Comparable<?>`. It also helps to xref:#webflux-versioning-validation[validate]
request versions.
NOTE: `ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods,
and is initialized by the WebFlux config. Typically, applications do not interact directly with it.
[[webflux-versioning-resolver]]
== ApiVersionResolver
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Servlet stack]#
This strategy resolves the API version from a request. The WebFlux config provides built-in
options to resolve from a header, a request parameter, or from the URL path.
You can also use a custom `ApiVersionResolver`.
[[webflux-versioning-parser]]
== ApiVersionParser
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-parser[See equivalent in the Servlet stack]#
This strategy helps to parse raw version values into `Comparable<?>`, which helps to
compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser`
parses a version into `major`, `minor`, and `patch` integer values. Minor and patch
values are set to 0 if not present.
[[webflux-versioning-validation]]
== Validation
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-validation[See equivalent in the Servlet stack]#
If a request version is not supported, `InvalidApiVersionException` is raised resulting
in a 400 response. By default, the list of supported versions is initialized from declared
versions in annotated controller mappings. You can add to that list, or set it explicitly
to a fixed set of versions (i.e. ignoring declared ones) through the MVC config.
By default, a version is required when API versioning is enabled, but you can turn that
off in which case the highest available version is used. You can also specify a default
version. `MissingApiVersionException` is raised resulting in a 400 response when a
version is required but not present.
[[webflux-versioning-mapping]]
== Request Mapping
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-mapping[See equivalent in the Servlet stack]#
`ApiVersionStrategy` supports the mapping of requests to annotated controller methods.
See xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[API Versions]
for more details.

View File

@ -306,8 +306,8 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
Resource resource = ... Resource resource = ...
Mono<String> result = webClient Mono<String> result = webClient
.post() .post()
.uri("https://example.com") .uri("https://example.com")
.body(Flux.concat( .body(Flux.concat(
@ -322,8 +322,8 @@ Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
var resource: Resource = ... var resource: Resource = ...
var result: Mono<String> = webClient var result: Mono<String> = webClient
.post() .post()
.uri("https://example.com") .uri("https://example.com")
.body( .body(

View File

@ -1,7 +1,7 @@
[[webflux-client-builder]] [[webflux-client-builder]]
= Configuration = Configuration
The simplest way to create a `WebClient` is through one of the static factory methods: The simplest way to create `WebClient` is through one of the static factory methods:
* `WebClient.create()` * `WebClient.create()`
* `WebClient.create(String baseUrl)` * `WebClient.create(String baseUrl)`
@ -12,10 +12,12 @@ You can also use `WebClient.builder()` with further options:
* `defaultUriVariables`: default values to use when expanding URI templates. * `defaultUriVariables`: default values to use when expanding URI templates.
* `defaultHeader`: Headers for every request. * `defaultHeader`: Headers for every request.
* `defaultCookie`: Cookies for every request. * `defaultCookie`: Cookies for every request.
* `defaultApiVersion`: API version for every request.
* `defaultRequest`: `Consumer` to customize every request. * `defaultRequest`: `Consumer` to customize every request.
* `filter`: Client filter for every request. * `filter`: Client filter for every request.
* `exchangeStrategies`: HTTP message reader/writer customizations. * `exchangeStrategies`: HTTP message reader/writer customizations.
* `clientConnector`: HTTP client library settings. * `clientConnector`: HTTP client library settings.
* `apiVersionInserter`: to insert API version values in the request
* `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#http-client.webclient[Observability support]. * `observationRegistry`: the registry to use for enabling xref:integration/observability.adoc#http-client.webclient[Observability support].
* `observationConvention`: xref:integration/observability.adoc#config[an optional, custom convention to extract metadata] for recorded observations. * `observationConvention`: xref:integration/observability.adoc#config[an optional, custom convention to extract metadata] for recorded observations.

View File

@ -158,7 +158,7 @@ Java::
+ +
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction { public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
@Override @Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) { public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
@ -186,14 +186,14 @@ public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
}); });
} }
} }
} }
---- ----
Kotlin:: Kotlin::
+ +
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
class MultipartExchangeFilterFunction : ExchangeFilterFunction { class MultipartExchangeFilterFunction : ExchangeFilterFunction {
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> { override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType()) return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
@ -217,6 +217,6 @@ class MultipartExchangeFilterFunction : ExchangeFilterFunction {
} }
} }
} }
} }
---- ----
====== ======

View File

@ -2,9 +2,16 @@
= Testing = Testing
:page-section-summary-toc: 1 :page-section-summary-toc: 1
To test code that uses the `WebClient`, you can use a mock web server, such as the To test code that uses the `WebClient`, you can use a mock web server, such as
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or
of its use, check out https://wiremock.org/[WireMock]. Mock web servers accept requests over HTTP like a regular
server, and that means you can test with the same HTTP client that is also configured in
the same way as in production, which is important because there are often subtle
differences in the way different clients handle network I/O. Another advantage of mock
web servers is the ability to simulate specific network issues and conditions at the
transport level, in combination with the client used in production.
For example use of MockWebServer, see
{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`] {spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/function/client/WebClientIntegrationTests.java[`WebClientIntegrationTests`]
in the Spring Framework test suite or the in the Spring Framework test suite or the
https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`] https://github.com/square/okhttp/tree/master/samples/static-server[`static-server`]

View File

@ -686,6 +686,63 @@ reliance on it.
[[webflux-config-api-version]]
== API Version
[.small]#xref:web/webmvc/mvc-config/api-version.adoc[See equivalent in the Servlet stack]#
To enable API versioning with a request header, use the following:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim"]
----
@Configuration
public class WebConfiguration implements WebFluxConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("X-API-Version");
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim"]
----
@Configuration
class WebConfiguration : WebMvcConfigurer {
override fun configureApiVersioning(configurer: ApiVersionConfigurer) {
configurer.useRequestHeader("X-API-Version")
}
}
----
======
Alternatively, the version can be resolved from a request parameter, from a path segment,
or through a custom `ApiVersionResolver`.
TIP: When resolving from a path segment, consider configuring a path prefix once in
xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options.
Raw version values are parsed with `SemanticVersionParser` by default, but you can use
a custom xref:web/webflux-versioning.adoc#webflux-versioning-parser[ApiVersionParser].
"Supported" versions are transparently detected from versions declared in request mappings
for convenience, but you can also set the list of supported versions explicitly, and
ignore declared ones. Requests with a version that is not supported are rejected with an
`InvalidApiVersionException` resulting in a 400 response.
Once API versioning is configured, you can begin to map requests to
xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[controller methods]
according to the request version.
[[webflux-config-blocking-execution]] [[webflux-config-blocking-execution]]
== Blocking Execution == Blocking Execution

View File

@ -19,7 +19,7 @@ Java::
private String name; private String name;
private MultipartFile file; private FilePart file;
// ... // ...
@ -42,7 +42,7 @@ Kotlin::
---- ----
class MyForm( class MyForm(
val name: String, val name: String,
val file: MultipartFile) val file: FilePart)
@Controller @Controller
class FileUploadController { class FileUploadController {

View File

@ -34,11 +34,7 @@ Controllers can then return a `Flux<List<B>>`; Reactor provides a dedicated oper
| `HttpHeaders` | `HttpHeaders`
| For returning a response with headers and no body. | For returning a response with headers and no body.
| `ErrorResponse` | `ErrorResponse`, `ProblemDetail`
| To render an RFC 9457 error response with details in the body,
see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses]
| `ProblemDetail`
| To render an RFC 9457 error response with details in the body, | To render an RFC 9457 error response with details in the body,
see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses] see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses]

View File

@ -234,8 +234,8 @@ Kotlin::
-- --
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this to, for example, parameterize a base URL based on other property sources. You can use this, for example, to parameterize a base URL based on
some external configuration. some external configuration.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support. NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
@ -408,6 +408,85 @@ Kotlin::
====== ======
[[webflux-ann-requestmapping-version]]
== API Version
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[See equivalent in the Servlet stack]#
There is no standard way to specify an API version, so you need to configure that first
through the xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config] along with other
config options. This results in the creation of an
xref:web/webflux-versioning.adoc#webflux-versioning-strategy[ApiVersionStrategy] that in
supports request mapping.
Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` version attribute supports the following:
- No value -- match any version
- Fixed version ("1.2") -- match the given version only
- Baseline version ("1.2+") -- match the given version and above
If multiple controller methods have a version less than or equal to the request version,
the one closest to the request version is considered for mapping purposes,
in effect superseding the rest.
To illustrate this, consider the following controller mappings:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RestController
@RequestMapping("/account/{id}")
public class AccountController {
@GetMapping // <1>
public Account getAccount() {
}
@GetMapping(version = "1.1") // <2>
public Account getAccount1_1() {
}
@GetMapping(version = "1.2+") // <3>
public Account getAccount1_2() {
}
@GetMapping(version = "1.5") // <4>
public Account getAccount1_5() {
}
}
----
<1> match any version
<2> match version 1.1
<3> match version 1.2 and above
<4> match version 1.5
======
For request with version `"1.3"`:
- (1) matches as it matches any version
- (2) does not match
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
- (4) is higher and does not match
For request with version `"1.5"`:
- (1) matches as it matches any version
- (2) does not match
- (3) matches as it matches 1.2 and above
- (4) matches and is *chosen* as the highest match
A request with version `"1.6"` does not have a match. (1) and (3) do match, but are
superseded by (4), which does not match. In this scenario, `NotAcceptableApiVersionException`
is raised resulting in a 400 response.
NOTE: The above assumes the request version is a "supported" versions. If not it would
fail xref:web/webflux-versioning.adoc#webflux-versioning-validation[Validation].
[[webflux-ann-requestmapping-head-options]] [[webflux-ann-requestmapping-head-options]]
== HTTP HEAD, OPTIONS == HTTP HEAD, OPTIONS

View File

@ -81,7 +81,7 @@ The following table describes server dependencies (also see
|jetty-server, jetty-servlet |jetty-server, jetty-servlet
|=== |===
The code snippets below show using the `HttpHandler` adapters with each server API: The code snippets below show using the `HttpHandler` adapters with each server API.
*Reactor Netty* *Reactor Netty*
[tabs] [tabs]
@ -176,17 +176,16 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"] [source,java,indent=0,subs="verbatim,quotes"]
---- ----
HttpHandler handler = ... HttpHandler handler = ...
Servlet servlet = new JettyHttpHandlerAdapter(handler); JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler);
Server server = new Server(); Server server = new Server();
ServletContextHandler contextHandler = new ServletContextHandler(server, ""); server.setHandler(adapter);
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server); ServerConnector connector = new ServerConnector(server);
connector.setHost(host); connector.setHost(host);
connector.setPort(port); connector.setPort(port);
server.addConnector(connector); server.addConnector(connector);
server.start(); server.start();
---- ----
@ -195,27 +194,27 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"] [source,kotlin,indent=0,subs="verbatim,quotes"]
---- ----
val handler: HttpHandler = ... val handler: HttpHandler = ...
val servlet = JettyHttpHandlerAdapter(handler) val adapter = JettyCoreHttpHandlerAdapter(handler)
val server = Server() val server = Server()
val contextHandler = ServletContextHandler(server, "") server.setHandler(adapter)
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server) val connector = ServerConnector(server)
connector.host = host connector.host = host
connector.port = port connector.port = port
server.addConnector(connector) server.addConnector(connector)
server.start() server.start()
---- ----
====== ======
*Servlet Container* TIP: In Spring Framework 6.2, `JettyHttpHandlerAdapter` was deprecated in favor of
`JettyCoreHttpHandlerAdapter`, which integrates directly with Jetty 12 APIs
without a Servlet layer.
To deploy as a WAR to any Servlet container, you can extend and include To deploy as a WAR to a Servlet container instead, use
{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`] {spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`],
in the WAR. That class wraps an `HttpHandler` with `ServletHttpHandlerAdapter` and registers to adapt `HttpHandler` to a `Servlet` via `ServletHttpHandlerAdapter`.
that as a `Servlet`.
@ -800,4 +799,3 @@ Kotlin::
.build() .build()
---- ----
====== ======

View File

@ -276,7 +276,7 @@ ServerResponse.async(asyncResponse);
---- ----
====== ======
https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events] can be provided via the
static `sse` method on `ServerResponse`. The builder provided by that method static `sse` method on `ServerResponse`. The builder provided by that method
allows you to send Strings, or other objects as JSON. For example: allows you to send Strings, or other objects as JSON. For example:
@ -846,7 +846,7 @@ processing lifecycle and also (potentially) run side by side with annotated cont
any are declared. It is also how functional endpoints are enabled by the Spring Boot Web any are declared. It is also how functional endpoints are enabled by the Spring Boot Web
starter. starter.
The following example shows a WebFlux Java configuration: The following example shows a WebMvc Java configuration:
[tabs] [tabs]
====== ======

View File

@ -0,0 +1,85 @@
[[mvc-versioning]]
= API Versioning
:page-section-summary-toc: 1
[.small]#xref:web/webflux-versioning.adoc[See equivalent in the Reactive stack]#
Spring MVC supports API versioning. This section provides an overview of the support
and underlying strategies.
Please, see also related content in:
- Use xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[API Version]
to map requests to annotated controller methods
- Configure API versioning in xref:web/webmvc/mvc-config/api-version.adoc[MVC Config]
TIP: API versioning is also supported on the client side in `RestClient`, `WebClient`, and
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as
for testing with `WebTestClient`.
[[mvc-versioning-strategy]]
== ApiVersionStrategy
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-strategy[See equivalent in the Reactive stack]#
This strategy holds all application preferences about how to manage versioning.
It delegates to xref:#mvc-versioning-resolver[ApiVersionResolver] to resolve versions
from requests, and to xref:#mvc-versioning-parser[ApiVersionParser] to parse raw version
values into `Comparable<?>`. It also helps to xref:#mvc-versioning-validation[validate]
request versions.
NOTE: `ApiVersionStrategy` helps to map requests to `@RequestMapping` controller methods,
and is initialized by the MVC config. Typically, applications do not interact directly with it.
[[mvc-versioning-resolver]]
== ApiVersionResolver
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-resolver[See equivalent in the Reactive stack]#
This strategy resolves the API version from a request. The MVC config provides built-in
options to resolve from a header, from a request parameter, or from the URL path.
You can also use a custom `ApiVersionResolver`.
[[mvc-versioning-parser]]
== ApiVersionParser
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-parser[See equivalent in the Reactive stack]#
This strategy helps to parse raw version values into `Comparable<?>`, which helps to
compare, sort, and select versions. By default, the built-in `SemanticApiVersionParser`
parses a version into `major`, `minor`, and `patch` integer values. Minor and patch
values are set to 0 if not present.
[[mvc-versioning-validation]]
== Validation
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-validation[See equivalent in the Reactive stack]#
If a request version is not supported, `InvalidApiVersionException` is raised resulting
in a 400 response. By default, the list of supported versions is initialized from declared
versions in annotated controller mappings. You can add to that list, or set it explicitly
to a fixed set of versions (i.e. ignoring declared ones) through the MVC config.
By default, a version is required when API versioning is enabled, but you can turn that
off in which case the highest available version is used. You can also specify a default
version. `MissingApiVersionException` is raised resulting in a 400 response when a
version is required but not present.
[[mvc-versioning-mapping]]
== Request Mapping
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-mapping[See equivalent in the Reactive stack]#
`ApiVersionStrategy` supports the mapping of requests to annotated controller methods.
See xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[API Version]
for more details.

View File

@ -4,11 +4,13 @@
Spring MVC has an extensive integration with Servlet asynchronous request Spring MVC has an extensive integration with Servlet asynchronous request
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-processing[processing]: xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-processing[processing]:
* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`] * xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`],
return values in controller methods provide basic support for a single asynchronous xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`], and
return value. xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-webasynctask[`WebAsyncTask`] return values
in controller methods provide support for a single asynchronous return value.
* Controllers can xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[stream] multiple values, including * Controllers can xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-http-streaming[stream] multiple values, including
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data]. xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-sse[SSE] and
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-output-stream[raw data].
* Controllers can use reactive clients and return * Controllers can use reactive clients and return
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive types] for response handling. xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-reactive-types[reactive types] for response handling.
@ -96,6 +98,47 @@ xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-configuration-spring-mvc[config
[[mvc-ann-async-webasynctask]]
== `WebAsyncTask`
`WebAsyncTask` is comparable to using xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[Callable]
but allows customizing additional settings such a request timeout value, and the
`AsyncTaskExecutor` to execute the `java.util.concurrent.Callable` with instead
of the defaults set up globally for Spring MVC. Below is an example of using `WebAsyncTask`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@GetMapping("/callable")
WebAsyncTask<String> handle() {
return new WebAsyncTask<String>(20000L,()->{
Thread.sleep(10000); //simulate long-running task
return "asynchronous request completed";
});
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@GetMapping("/callable")
fun handle(): WebAsyncTask<String> {
return WebAsyncTask(20000L) {
Thread.sleep(10000) // simulate long-running task
"asynchronous request completed"
}
}
----
======
[[mvc-ann-async-processing]] [[mvc-ann-async-processing]]
== Processing == Processing
@ -281,7 +324,7 @@ invokes the configured exception resolvers and completes the request.
=== SSE === SSE
`SseEmitter` (a subclass of `ResponseBodyEmitter`) provides support for `SseEmitter` (a subclass of `ResponseBodyEmitter`) provides support for
https://www.w3.org/TR/eventsource/[Server-Sent Events], where events sent from the server https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events], where events sent from the server
are formatted according to the W3C SSE specification. To produce an SSE are formatted according to the W3C SSE specification. To produce an SSE
stream from a controller, return `SseEmitter`, as the following example shows: stream from a controller, return `SseEmitter`, as the following example shows:
@ -390,7 +433,7 @@ reactive types from the controller method.
Reactive return values are handled as follows: Reactive return values are handled as follows:
* A single-value promise is adapted to, similar to using `DeferredResult`. Examples * A single-value promise is adapted to, similar to using `DeferredResult`. Examples
include `Mono` (Reactor) or `Single` (RxJava). include `CompletionStage` (JDK), Mono` (Reactor), and `Single` (RxJava).
* A multi-value stream with a streaming media type (such as `application/x-ndjson` * A multi-value stream with a streaming media type (such as `application/x-ndjson`
or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
`SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava). `SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava).

View File

@ -0,0 +1,26 @@
[[mvc-config-api-version]]
= API Version
[.small]#xref:web/webflux/config.adoc#webflux-config-api-version[See equivalent in the Reactive stack]#
To enable API versioning with a request header, use the following:
include-code::./WebConfiguration[tag=snippet,indent=0]
Alternatively, the version can be resolved from a request parameter, from a path segment,
or through a custom `ApiVersionResolver`.
TIP: When resolving from a path segment, consider configuring a path prefix once in
xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options.
Raw version values are parsed with `SemanticVersionParser` by default, but you can use
a custom xref:web/webmvc-versioning.adoc#mvc-versioning-parser[ApiVersionParser].
"Supported" versions are transparently detected from versions declared in request mappings
for convenience, but you can also set the list of supported versions explicitly, and
ignore declared ones. Requests with a version that is not supported are rejected with an
`InvalidApiVersionException` resulting in a 400 response.
Once API versioning is configured, you can begin to map requests to
xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[controller methods]
according to the request version.

View File

@ -10,18 +10,20 @@ to any controller. Moreover, as of 5.3, `@ExceptionHandler` methods in `@Control
can be used to handle exceptions from any `@Controller` or any other handler. can be used to handle exceptions from any `@Controller` or any other handler.
`@ControllerAdvice` is meta-annotated with `@Component` and therefore can be registered as `@ControllerAdvice` is meta-annotated with `@Component` and therefore can be registered as
a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning] a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning].
. `@RestControllerAdvice` is meta-annotated with `@ControllerAdvice`
and `@ResponseBody`, and that means `@ExceptionHandler` methods will have their return `@RestControllerAdvice` is a shortcut annotation that combines `@ControllerAdvice`
value rendered via response body message conversion, rather than via HTML views. with `@ResponseBody`, in effect simply an `@ControllerAdvice` whose exception handler
methods render to the response body.
On startup, `RequestMappingHandlerMapping` and `ExceptionHandlerExceptionResolver` detect On startup, `RequestMappingHandlerMapping` and `ExceptionHandlerExceptionResolver` detect
controller advice beans and apply them at runtime. Global `@ExceptionHandler` methods, controller advice beans and apply them at runtime. Global `@ExceptionHandler` methods,
from an `@ControllerAdvice`, are applied _after_ local ones, from the `@Controller`. from an `@ControllerAdvice`, are applied _after_ local ones, from the `@Controller`.
By contrast, global `@ModelAttribute` and `@InitBinder` methods are applied _before_ local ones. By contrast, global `@ModelAttribute` and `@InitBinder` methods are applied _before_ local ones.
The `@ControllerAdvice` annotation has attributes that let you narrow the set of controllers By default, both `@ControllerAdvice` and `@RestControllerAdvice` apply to any controller,
and handlers that they apply to. For example: including `@Controller` and `@RestController`. Use attributes of the annotation to narrow
the set of controllers and handlers that they apply to. For example:
[tabs] [tabs]
====== ======

View File

@ -177,13 +177,9 @@ the content negotiation during the error handling phase will decide which conten
be converted through `HttpMessageConverter` instances and written to the response. be converted through `HttpMessageConverter` instances and written to the response.
See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity]. See xref:web/webmvc/mvc-controller/ann-methods/responseentity.adoc[ResponseEntity].
| `ErrorResponse` | `ErrorResponse`, `ProblemDetail`
| To render an RFC 9457 error response with details in the body, | To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
| `ProblemDetail`
| To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
| `String` | `String`
| A view name to be resolved with `ViewResolver` implementations and used together with the | A view name to be resolved with `ViewResolver` implementations and used together with the

View File

@ -243,7 +243,7 @@ Kotlin::
====== ======
If there is no `BindingResult` parameter after the `@ModelAttribute`, then If there is no `BindingResult` parameter after the `@ModelAttribute`, then
`MethodArgumentNotValueException` is raised with the validation errors. However, if method a `MethodArgumentNotValidException` is raised with the validation errors. However, if method
validation applies because other parameters have `@jakarta.validation.Constraint` annotations, validation applies because other parameters have `@jakarta.validation.Constraint` annotations,
then `HandlerMethodValidationException` is raised instead. For more details, see the section then `HandlerMethodValidationException` is raised instead. For more details, see the section
xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]. xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].

View File

@ -22,11 +22,7 @@ supported for all return values.
| `HttpHeaders` | `HttpHeaders`
| For returning a response with headers and no body. | For returning a response with headers and no body.
| `ErrorResponse` | `ErrorResponse`, `ProblemDetail`
| To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
| `ProblemDetail`
| To render an RFC 9457 error response with details in the body, | To render an RFC 9457 error response with details in the body,
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses] see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]

View File

@ -429,6 +429,86 @@ xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-co
instead. instead.
[[mvc-ann-requestmapping-version]]
== API Version
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[See equivalent in the Reactive stack]#
There is no standard way to specify an API version, so you need to configure that first
through the xref:web/webmvc/mvc-config/api-version.adoc[MVC Config] along with other
config options. This results in the creation of an
xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[ApiVersionStrategy] that in turn
supports request mapping.
Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` version attribute supports the following:
- No value -- match any version
- Fixed version ("1.2") -- match the given version only
- Baseline version ("1.2+") -- match the given version and above
If multiple controller methods have a version less than or equal to the request version,
the one closest to the request version is considered for mapping purposes,
in effect superseding the rest.
To illustrate this, consider the following controller mappings:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RestController
@RequestMapping("/account/{id}")
public class AccountController {
@GetMapping // <1>
public Account getAccount() {
}
@GetMapping(version = "1.1") // <2>
public Account getAccount1_1() {
}
@GetMapping(version = "1.2+") // <3>
public Account getAccount1_2() {
}
@GetMapping(version = "1.5") // <4>
public Account getAccount1_5() {
}
}
----
<1> match any version
<2> match version 1.1
<3> match version 1.2 and above
<4> match version 1.5
======
For request with version `"1.3"`:
- (1) matches as it matches any version
- (2) does not match
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
- (4) is higher and does not match
For request with version `"1.5"`:
- (1) matches as it matches any version
- (2) does not match
- (3) matches as it matches 1.2 and above
- (4) matches and is *chosen* as the highest match
A request with version `"1.6"` does not have a match. (1) and (3) do match, but are
superseded by (4), which does not match. In this scenario, `NotAcceptableApiVersionException`
is raised resulting in a 400 response.
NOTE: The above assumes the request version is a "supported" versions. If not it would
fail xref:web/webmvc-versioning.adoc#mvc-versioning-validation[Validation].
[[mvc-ann-requestmapping-head-options]] [[mvc-ann-requestmapping-head-options]]
== HTTP HEAD, OPTIONS == HTTP HEAD, OPTIONS
[.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-head-options[See equivalent in the Reactive stack]# [.small]#xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-head-options[See equivalent in the Reactive stack]#

View File

@ -47,5 +47,3 @@ interactive web application] -- a getting started guide.
* https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample * https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample
application. application.

View File

@ -38,7 +38,7 @@ to inform the server that the original port was `443`.
==== X-Forwarded-Proto ==== X-Forwarded-Proto
While not standard, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto[`X-Forwarded-Proto: (https|http)`] While not standard, https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto[`X-Forwarded-Proto: (https|http)`]
is a de-facto standard header that is used to communicate the original protocol (for example, https / https) is a de-facto standard header that is used to communicate the original protocol (for example, https / http)
to a downstream server. For example, if a request of `https://example.com/resource` is sent to to a downstream server. For example, if a request of `https://example.com/resource` is sent to
a proxy which forwards the request to `http://localhost:8080/resource`, then a header of a proxy which forwards the request to `http://localhost:8080/resource`, then a header of
`X-Forwarded-Proto: https` can be sent to inform the server that the original protocol was `https`. `X-Forwarded-Proto: https` can be sent to inform the server that the original protocol was `https`.

View File

@ -25,6 +25,7 @@ import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.assertj.MockMvcTester; import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.WebApplicationContext;
@SuppressWarnings("removal")
// tag::snippet[] // tag::snippet[]
@SpringJUnitWebConfig(ApplicationWebConfiguration.class) @SpringJUnitWebConfig(ApplicationWebConfiguration.class)
class AccountControllerIntegrationTests { class AccountControllerIntegrationTests {

View File

@ -0,0 +1,32 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// tag::snippet[]
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureApiVersioning(ApiVersionConfigurer configurer) {
configurer.useRequestHeader("X-API-Version");
}
}
// end::snippet[]

View File

@ -28,6 +28,7 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert
import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@SuppressWarnings("removal")
// tag::snippet[] // tag::snippet[]
@Configuration @Configuration
public class WebConfiguration implements WebMvcConfigurer { public class WebConfiguration implements WebMvcConfigurer {

View File

@ -23,6 +23,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
@SuppressWarnings("removal")
// tag::snippet[] // tag::snippet[]
@Configuration @Configuration
public class FreeMarkerConfiguration implements WebMvcConfigurer { public class FreeMarkerConfiguration implements WebMvcConfigurer {

View File

@ -21,6 +21,7 @@ import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView; import org.springframework.web.servlet.view.json.MappingJackson2JsonView;
@SuppressWarnings("removal")
// tag::snippet[] // tag::snippet[]
@Configuration @Configuration
public class WebConfiguration implements WebMvcConfigurer { public class WebConfiguration implements WebMvcConfigurer {

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
@file:Suppress("DEPRECATION")
package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.converter package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.converter
import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Autowired

View File

@ -0,0 +1,31 @@
/*
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion
import org.springframework.context.annotation.Configuration
import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
// tag::snippet[]
@Configuration
class WebConfiguration : WebMvcConfigurer {
override fun configureApiVersioning(configurer: ApiVersionConfigurer) {
configurer.useRequestHeader("X-API-Version")
}
}
// end::snippet[]

View File

@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule import com.fasterxml.jackson.module.paramnames.ParameterNamesModule

View File

@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Bean

View File

@ -14,6 +14,8 @@
* limitations under the License. * limitations under the License.
*/ */
@file:Suppress("DEPRECATION")
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
import org.springframework.context.annotation.Configuration import org.springframework.context.annotation.Configuration

View File

@ -7,20 +7,21 @@ javaPlatform {
} }
dependencies { dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.3")) api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
api(platform("io.micrometer:micrometer-bom:1.14.5")) api(platform("io.micrometer:micrometer-bom:1.15.1"))
api(platform("io.netty:netty-bom:4.1.119.Final")) api(platform("io.netty:netty-bom:4.2.2.Final"))
api(platform("io.projectreactor:reactor-bom:2025.0.0-M1")) api(platform("io.projectreactor:reactor-bom:2025.0.0-M4"))
api(platform("io.rsocket:rsocket-bom:1.1.5")) api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.26")) api(platform("org.apache.groovy:groovy-bom:4.0.27"))
api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3")) api(platform("org.apache.logging.log4j:log4j-bom:3.0.0-beta3"))
api(platform("org.assertj:assertj-bom:3.27.3")) api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.1.0.alpha1")) api(platform("org.eclipse.jetty:jetty-bom:12.1.0.beta0"))
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.alpha1")) api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.beta0"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1")) api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0")) api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.1"))
api(platform("org.junit:junit-bom:5.12.1")) api(platform("org.junit:junit-bom:5.13.1"))
api(platform("org.mockito:mockito-bom:5.16.1")) api(platform("org.mockito:mockito-bom:5.18.0"))
api(platform("tools.jackson:jackson-bom:3.0.0-rc5"))
constraints { constraints {
api("com.fasterxml:aalto-xml:1.3.2") api("com.fasterxml:aalto-xml:1.3.2")
@ -29,8 +30,8 @@ dependencies {
api("com.github.librepdf:openpdf:1.3.43") api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1") api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2") api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.12.1") api("com.google.code.gson:gson:2.13.0")
api("com.google.protobuf:protobuf-java-util:4.30.0") api("com.google.protobuf:protobuf-java-util:4.30.2")
api("com.h2database:h2:2.3.232") api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0") api("com.jayway.jsonpath:json-path:2.9.0")
api("com.networknt:json-schema-validator:1.5.3") api("com.networknt:json-schema-validator:1.5.3")
@ -44,9 +45,8 @@ dependencies {
api("com.thoughtworks.qdox:qdox:2.2.0") api("com.thoughtworks.qdox:qdox:2.2.0")
api("com.thoughtworks.xstream:xstream:1.4.21") api("com.thoughtworks.xstream:xstream:1.4.21")
api("commons-io:commons-io:2.15.0") api("commons-io:commons-io:2.15.0")
api("commons-logging:commons-logging:1.3.4") api("commons-logging:commons-logging:1.3.5")
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2") api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
api("io.micrometer:context-propagation:1.1.1")
api("io.mockk:mockk:1.13.4") api("io.mockk:mockk:1.13.4")
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE") api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
@ -97,16 +97,16 @@ dependencies {
api("org.apache.derby:derby:10.16.1.1") api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1") api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1") api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.4.2") api("org.apache.httpcomponents.client5:httpclient5:5.5")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.3") api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
api("org.apache.poi:poi-ooxml:5.2.5") api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.1") api("org.apache.tomcat.embed:tomcat-embed-core:11.0.7")
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.1") api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.7")
api("org.apache.tomcat:tomcat-util:11.0.1") api("org.apache.tomcat:tomcat-util:11.0.7")
api("org.apache.tomcat:tomcat-websocket:11.0.1") api("org.apache.tomcat:tomcat-websocket:11.0.7")
api("org.aspectj:aspectjrt:1.9.23") api("org.aspectj:aspectjrt:1.9.24")
api("org.aspectj:aspectjtools:1.9.23") api("org.aspectj:aspectjtools:1.9.24")
api("org.aspectj:aspectjweaver:1.9.23") api("org.aspectj:aspectjweaver:1.9.24")
api("org.awaitility:awaitility:4.3.0") api("org.awaitility:awaitility:4.3.0")
api("org.bouncycastle:bcpkix-jdk18on:1.72") api("org.bouncycastle:bcpkix-jdk18on:1.72")
api("org.codehaus.jettison:jettison:1.5.4") api("org.codehaus.jettison:jettison:1.5.4")
@ -124,11 +124,12 @@ dependencies {
api("org.glassfish:jakarta.el:4.0.2") api("org.glassfish:jakarta.el:4.0.2")
api("org.graalvm.sdk:graal-sdk:22.3.1") api("org.graalvm.sdk:graal-sdk:22.3.1")
api("org.hamcrest:hamcrest:3.0") api("org.hamcrest:hamcrest:3.0")
api("org.hibernate.orm:hibernate-core:7.0.0.Beta4") api("org.hibernate.orm:hibernate-core:7.0.0.Final")
api("org.hibernate.validator:hibernate-validator:9.0.0.CR1") api("org.hibernate.validator:hibernate-validator:9.0.0.Final")
api("org.hsqldb:hsqldb:2.7.4") api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.10.0") api("org.htmlunit:htmlunit:4.10.0")
api("org.javamoney:moneta:1.4.4") api("org.javamoney:moneta:1.4.4")
api("org.jboss.logging:jboss-logging:3.6.1.Final")
api("org.jruby:jruby:9.4.12.0") api("org.jruby:jruby:9.4.12.0")
api("org.jspecify:jspecify:1.0.0") api("org.jspecify:jspecify:1.0.0")
api("org.junit.support:testng-engine:1.0.5") api("org.junit.support:testng-engine:1.0.5")

View File

@ -4,7 +4,7 @@ org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m org.gradle.jvmargs=-Xmx2048m
org.gradle.parallel=true org.gradle.parallel=true
kotlinVersion=2.1.20 kotlinVersion=2.2.0-RC2
kotlin.jvm.target.validation.mode=ignore kotlin.jvm.target.validation.mode=ignore
kotlin.stdlib.default.dependency=false kotlin.stdlib.default.dependency=false

View File

@ -73,7 +73,7 @@ eclipse.classpath.file.whenMerged {
// within Eclipse. Consequently, Java 21 features managed via the // within Eclipse. Consequently, Java 21 features managed via the
// me.champeau.mrjar plugin cannot be built or tested within Eclipse. // me.champeau.mrjar plugin cannot be built or tested within Eclipse.
eclipse.classpath.file.whenMerged { classpath -> eclipse.classpath.file.whenMerged { classpath ->
classpath.entries.removeAll { it.path =~ /src\/(main|test)\/java21/ } classpath.entries.removeAll { it.path =~ /src\/(main|test)\/java(21|24)/ }
} }
// Remove classpath entries for non-existent libraries added by the me.champeau.mrjar // Remove classpath entries for non-existent libraries added by the me.champeau.mrjar

View File

@ -6,15 +6,13 @@ apply plugin: 'org.springframework.build.optional-dependencies'
// apply plugin: 'io.github.goooler.shadow' // apply plugin: 'io.github.goooler.shadow'
apply plugin: 'me.champeau.jmh' apply plugin: 'me.champeau.jmh'
apply from: "$rootDir/gradle/publications.gradle" apply from: "$rootDir/gradle/publications.gradle"
apply plugin: 'net.ltgt.errorprone' apply plugin: "io.spring.nullability"
dependencies { dependencies {
jmh 'org.openjdk.jmh:jmh-core:1.37' jmh 'org.openjdk.jmh:jmh-core:1.37'
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37' jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37'
jmh 'net.sf.jopt-simple:jopt-simple' jmh 'net.sf.jopt-simple:jopt-simple'
errorprone 'com.uber.nullaway:nullaway:0.12.4'
errorprone 'com.google.errorprone:error_prone_core:2.36.0'
} }
pluginManager.withPlugin("kotlin") { pluginManager.withPlugin("kotlin") {
@ -69,20 +67,32 @@ normalization {
javadoc { javadoc {
description = "Generates project-level javadoc for use in -javadoc jar" description = "Generates project-level javadoc for use in -javadoc jar"
failOnError = true
options {
encoding = "UTF-8"
memberLevel = JavadocMemberLevel.PROTECTED
author = true
header = project.name
use = true
links(project.ext.javadocLinks)
setOutputLevel(JavadocOutputLevel.QUIET)
// Check for 'syntax' during linting. Note that the global
// 'framework-api:javadoc' task checks for 'reference' in addition
// to 'syntax'.
addBooleanOption("Xdoclint:syntax,-reference", true)
// Change modularity mismatch from warn to info.
// See https://github.com/spring-projects/spring-framework/issues/27497
addStringOption("-link-modularity-mismatch", "info")
// With the javadoc tool on Java 24, it appears that the 'reference'
// group is always active and the '-reference' flag is not honored.
// Thus, we do NOT fail the build on Javadoc warnings due to
// cross-module @see and @link references which are only reachable
// when running the global 'framework-api:javadoc' task.
addBooleanOption('Werror', false)
}
options.encoding = "UTF-8" // Attempt to suppress warnings due to cross-module @see and @link references.
options.memberLevel = JavadocMemberLevel.PROTECTED // Note that the global 'framework-api:javadoc' task displays all warnings.
options.author = true
options.header = project.name
options.use = true
options.links(project.ext.javadocLinks)
options.setOutputLevel(JavadocOutputLevel.QUIET)
// Check for syntax during linting.
options.addBooleanOption("Xdoclint:syntax", true)
// Suppress warnings due to cross-module @see and @link references.
// Note that global 'api' task does display all warnings, and
// checks for 'reference' on top of 'syntax'.
logging.captureStandardError LogLevel.INFO logging.captureStandardError LogLevel.INFO
logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message
} }
@ -114,15 +124,3 @@ publishing {
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() } components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { skip() }
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() } components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
tasks.withType(JavaCompile).configureEach {
options.errorprone {
disableAllChecks = true
option("NullAway:OnlyNullMarked", "true")
option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract")
option("NullAway:JSpecifyMode", "true")
}
}
tasks.compileJava {
// The check defaults to a warning, bump it up to an error for the main sources
options.errorprone.error("NullAway")
}

Binary file not shown.

View File

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

4
gradlew vendored
View File

@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;; NONSTOP* ) nonstop=true ;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \ -classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@" "$@"
# Stop when "xargs" is not available. # Stop when "xargs" is not available.

4
gradlew.bat vendored
View File

@ -70,11 +70,11 @@ goto fail
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell

View File

@ -158,8 +158,7 @@ class AdvisorAutoProxyCreatorIntegrationTests {
try { try {
rb.echoException(new ServletException()); rb.echoException(new ServletException());
} }
catch (ServletException ex) { catch (ServletException ignored) {
} }
assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1); assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1);
} }
@ -272,7 +271,7 @@ class OrderedTxCheckAdvisor extends StaticMethodMatcherPointcutAdvisor implement
TransactionInterceptor.currentTransactionStatus(); TransactionInterceptor.currentTransactionStatus();
throw new RuntimeException("Shouldn't have a transaction"); throw new RuntimeException("Shouldn't have a transaction");
} }
catch (NoTransactionException ex) { catch (NoTransactionException ignored) {
// this is Ok // this is Ok
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2019 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,7 +25,7 @@ import static org.springframework.beans.factory.support.BeanDefinitionBuilder.ro
class PropertyPlaceholderConfigurerEnvironmentIntegrationTests { class PropertyPlaceholderConfigurerEnvironmentIntegrationTests {
@Test @Test
@SuppressWarnings("deprecation") @SuppressWarnings({"deprecation", "removal"})
void test() { void test() {
GenericApplicationContext ctx = new GenericApplicationContext(); GenericApplicationContext ctx = new GenericApplicationContext();
ctx.registerBeanDefinition("ppc", ctx.registerBeanDefinition("ppc",

View File

@ -1,6 +1,5 @@
plugins { plugins {
id "io.spring.develocity.conventions" version "0.0.22" id "io.spring.develocity.conventions" version "0.0.22"
id "org.gradle.toolchains.foojay-resolver-convention" version "0.9.0"
} }
include "spring-aop" include "spring-aop"

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -109,11 +109,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
private @Nullable BeanFactory beanFactory; private @Nullable BeanFactory beanFactory;
private transient @Nullable ClassLoader pointcutClassLoader; private transient volatile @Nullable ClassLoader pointcutClassLoader;
private transient @Nullable PointcutExpression pointcutExpression; private transient volatile @Nullable PointcutExpression pointcutExpression;
private transient boolean pointcutParsingFailed = false; private transient volatile boolean pointcutParsingFailed;
/** /**
@ -193,11 +193,14 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
* Lazily build the underlying AspectJ pointcut expression. * Lazily build the underlying AspectJ pointcut expression.
*/ */
private PointcutExpression obtainPointcutExpression() { private PointcutExpression obtainPointcutExpression() {
if (this.pointcutExpression == null) { PointcutExpression pointcutExpression = this.pointcutExpression;
this.pointcutClassLoader = determinePointcutClassLoader(); if (pointcutExpression == null) {
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader); ClassLoader pointcutClassLoader = determinePointcutClassLoader();
pointcutExpression = buildPointcutExpression(pointcutClassLoader);
this.pointcutClassLoader = pointcutClassLoader;
this.pointcutExpression = pointcutExpression;
} }
return this.pointcutExpression; return pointcutExpression;
} }
/** /**
@ -460,13 +463,20 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
} }
private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) {
ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(this, targetMethod); ShadowMatchKey key = new ShadowMatchKey(this, targetMethod);
ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(key);
if (shadowMatch == null) { if (shadowMatch == null) {
PointcutExpression pointcutExpression = obtainPointcutExpression();
synchronized (pointcutExpression) {
shadowMatch = ShadowMatchUtils.getShadowMatch(key);
if (shadowMatch != null) {
return shadowMatch;
}
PointcutExpression fallbackExpression = null; PointcutExpression fallbackExpression = null;
Method methodToMatch = targetMethod; Method methodToMatch = targetMethod;
try { try {
try { try {
shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch);
} }
catch (ReflectionWorldException ex) { catch (ReflectionWorldException ex) {
// Failed to introspect target method, probably because it has been loaded // Failed to introspect target method, probably because it has been loaded
@ -489,7 +499,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
// redeclared methods), as well as for annotation pointcuts. // redeclared methods), as well as for annotation pointcuts.
methodToMatch = originalMethod; methodToMatch = originalMethod;
try { try {
shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch); shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch);
} }
catch (ReflectionWorldException ex) { catch (ReflectionWorldException ex) {
// Could neither introspect the target class nor the proxy class -> // Could neither introspect the target class nor the proxy class ->
@ -518,7 +528,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
shadowMatch = new DefensiveShadowMatch(shadowMatch, shadowMatch = new DefensiveShadowMatch(shadowMatch,
fallbackExpression.matchesMethodExecution(methodToMatch)); fallbackExpression.matchesMethodExecution(methodToMatch));
} }
shadowMatch = ShadowMatchUtils.setShadowMatch(this, targetMethod, shadowMatch); shadowMatch = ShadowMatchUtils.setShadowMatch(key, shadowMatch);
}
} }
return shadowMatch; return shadowMatch;
} }
@ -616,14 +627,14 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Override @Override
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
@Deprecated @Deprecated(since = "4.0") // deprecated by AspectJ
public boolean couldMatchJoinPointsInType(Class someClass) { public boolean couldMatchJoinPointsInType(Class someClass) {
return (contextMatch(someClass) == FuzzyBoolean.YES); return (contextMatch(someClass) == FuzzyBoolean.YES);
} }
@Override @Override
@SuppressWarnings("rawtypes") @SuppressWarnings("rawtypes")
@Deprecated @Deprecated(since = "4.0") // deprecated by AspectJ
public boolean couldMatchJoinPointsInType(Class someClass, MatchingContext context) { public boolean couldMatchJoinPointsInType(Class someClass, MatchingContext context) {
return (contextMatch(someClass) == FuzzyBoolean.YES); return (contextMatch(someClass) == FuzzyBoolean.YES);
} }
@ -713,4 +724,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
} }
} }
private record ShadowMatchKey(AspectJExpressionPointcut expression, Method method) {
}
} }

View File

@ -319,7 +319,7 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
} }
@Override @Override
@Deprecated @Deprecated(since = "4.0") // deprecated by AspectJ
public int getColumn() { public int getColumn() {
throw new UnsupportedOperationException(); throw new UnsupportedOperationException();
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,24 +16,46 @@
package org.springframework.aop.aspectj; package org.springframework.aop.aspectj;
import java.lang.reflect.Method;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.aspectj.weaver.tools.ShadowMatch; import org.aspectj.weaver.tools.ShadowMatch;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.aop.support.ExpressionPointcut;
/** /**
* Internal {@link ShadowMatch} utilities. * Internal {@link ShadowMatch} utilities.
* *
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Juergen Hoeller
* @since 6.2 * @since 6.2
*/ */
public abstract class ShadowMatchUtils { public abstract class ShadowMatchUtils {
private static final Map<Key, ShadowMatch> shadowMatchCache = new ConcurrentHashMap<>(256); private static final Map<Object, ShadowMatch> shadowMatchCache = new ConcurrentHashMap<>(256);
/**
* Find a {@link ShadowMatch} for the specified key.
* @param key the key to use
* @return the {@code ShadowMatch} to use for the specified key,
* or {@code null} if none found
*/
static @Nullable ShadowMatch getShadowMatch(Object key) {
return shadowMatchCache.get(key);
}
/**
* Associate the {@link ShadowMatch} with the specified key.
* If an entry already exists, the given {@code shadowMatch} is ignored.
* @param key the key to use
* @param shadowMatch the shadow match to use for this key
* if none already exists
* @return the shadow match to use for the specified key
*/
static ShadowMatch setShadowMatch(Object key, ShadowMatch shadowMatch) {
ShadowMatch existing = shadowMatchCache.putIfAbsent(key, shadowMatch);
return (existing != null ? existing : shadowMatch);
}
/** /**
* Clear the cache of computed {@link ShadowMatch} instances. * Clear the cache of computed {@link ShadowMatch} instances.
@ -42,33 +64,4 @@ public abstract class ShadowMatchUtils {
shadowMatchCache.clear(); shadowMatchCache.clear();
} }
/**
* Return the {@link ShadowMatch} for the specified {@link ExpressionPointcut}
* and {@link Method} or {@code null} if none is found.
* @param expression the expression
* @param method the method
* @return the {@code ShadowMatch} to use for the specified expression and method
*/
static @Nullable ShadowMatch getShadowMatch(ExpressionPointcut expression, Method method) {
return shadowMatchCache.get(new Key(expression, method));
}
/**
* Associate the {@link ShadowMatch} to the specified {@link ExpressionPointcut}
* and method. If an entry already exists, the given {@code shadowMatch} is
* ignored.
* @param expression the expression
* @param method the method
* @param shadowMatch the shadow match to use for this expression and method
* if none already exists
* @return the shadow match to use for the specified expression and method
*/
static ShadowMatch setShadowMatch(ExpressionPointcut expression, Method method, ShadowMatch shadowMatch) {
ShadowMatch existing = shadowMatchCache.putIfAbsent(new Key(expression, method), shadowMatch);
return (existing != null ? existing : shadowMatch);
}
private record Key(ExpressionPointcut expression, Method method) {}
} }

View File

@ -110,7 +110,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
* Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given * Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given
* {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances, * {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances,
* for bean pointcut handling as well as consistent {@link ClassLoader} resolution. * for bean pointcut handling as well as consistent {@link ClassLoader} resolution.
* @param beanFactory the BeanFactory to propagate (may be {@code null}} * @param beanFactory the BeanFactory to propagate (may be {@code null})
* @since 4.3.6 * @since 4.3.6
* @see AspectJExpressionPointcut#setBeanFactory * @see AspectJExpressionPointcut#setBeanFactory
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader() * @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader()

View File

@ -36,6 +36,7 @@ import org.springframework.aop.AopInvocationException;
import org.springframework.aop.RawTargetAccess; import org.springframework.aop.RawTargetAccess;
import org.springframework.aop.TargetSource; import org.springframework.aop.TargetSource;
import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.AopUtils;
import org.springframework.aot.AotDetector;
import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy; import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy;
import org.springframework.cglib.core.CodeGenerationException; import org.springframework.cglib.core.CodeGenerationException;
import org.springframework.cglib.core.GeneratorStrategy; import org.springframework.cglib.core.GeneratorStrategy;
@ -203,7 +204,7 @@ class CglibAopProxy implements AopProxy, Serializable {
enhancer.setSuperclass(proxySuperClass); enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(true); enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts());
enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ? enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ?
new ClassLoaderAwareGeneratorStrategy(classLoader) : new ClassLoaderAwareGeneratorStrategy(classLoader) :
new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy) new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy)

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2020 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -112,10 +112,10 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator {
boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass); boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass);
for (String mappedName : this.beanNames) { for (String mappedName : this.beanNames) {
if (isFactoryBean) { if (isFactoryBean) {
if (!mappedName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { if (mappedName.isEmpty() || mappedName.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) {
continue; continue;
} }
mappedName = mappedName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); mappedName = mappedName.substring(1); // length of '&'
} }
if (isMatch(beanName, mappedName)) { if (isMatch(beanName, mappedName)) {
return true; return true;

View File

@ -29,7 +29,6 @@ import org.mockito.Mockito;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.mockito.BDDMockito.willAnswer; import static org.mockito.BDDMockito.willAnswer;
import static org.mockito.BDDMockito.willThrow; import static org.mockito.BDDMockito.willThrow;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
@ -69,7 +68,12 @@ abstract class AbstractProxyExceptionHandlingTests {
private void invokeProxy() { private void invokeProxy() {
throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething()); try {
Objects.requireNonNull(proxy).doSomething();
}
catch (Throwable throwable) {
throwableSeenByCaller = throwable;
}
} }
@SuppressWarnings("SameParameterValue") @SuppressWarnings("SameParameterValue")

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,7 +24,7 @@ import java.util.Map;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
/** /**
* Abstract superclass for counting advices etc. * Abstract superclass for counting advice, etc.
* *
* @author Rod Johnson * @author Rod Johnson
* @author Chris Beams * @author Chris Beams
@ -62,7 +62,7 @@ public class MethodCounter implements Serializable {
*/ */
@Override @Override
public boolean equals(@Nullable Object other) { public boolean equals(@Nullable Object other) {
return (other != null && other.getClass() == this.getClass()); return (other != null && getClass() == other.getClass());
} }
@Override @Override

View File

@ -33,6 +33,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import kotlin.jvm.JvmClassMappingKt; import kotlin.jvm.JvmClassMappingKt;
import kotlin.jvm.internal.DefaultConstructorMarker;
import kotlin.reflect.KClass; import kotlin.reflect.KClass;
import kotlin.reflect.KFunction; import kotlin.reflect.KFunction;
import kotlin.reflect.KParameter; import kotlin.reflect.KParameter;
@ -94,10 +95,9 @@ public abstract class BeanUtils {
* @return the new instance * @return the new instance
* @throws BeanInstantiationException if the bean cannot be instantiated * @throws BeanInstantiationException if the bean cannot be instantiated
* @see Class#newInstance() * @see Class#newInstance()
* @deprecated as of Spring 5.0, following the deprecation of * @deprecated following the deprecation of {@link Class#newInstance()} in JDK 9
* {@link Class#newInstance()} in JDK 9
*/ */
@Deprecated @Deprecated(since = "5.0")
public static <T> T instantiate(Class<T> clazz) throws BeanInstantiationException { public static <T> T instantiate(Class<T> clazz) throws BeanInstantiationException {
Assert.notNull(clazz, "Class must not be null"); Assert.notNull(clazz, "Class must not be null");
if (clazz.isInterface()) { if (clazz.isInterface()) {
@ -659,7 +659,9 @@ public abstract class BeanUtils {
ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
@Nullable String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); @Nullable String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor));
Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor); Assert.state(paramNames != null, () -> "Cannot resolve parameter names for constructor " + ctor);
Assert.state(paramNames.length == ctor.getParameterCount(), int parameterCount = (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasDefaultConstructorMarker(ctor) ?
ctor.getParameterCount() - 1 : ctor.getParameterCount());
Assert.state(paramNames.length == parameterCount,
() -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor); () -> "Invalid number of parameter names: " + paramNames.length + " for constructor " + ctor);
return paramNames; return paramNames;
} }
@ -928,6 +930,11 @@ public abstract class BeanUtils {
} }
return kotlinConstructor.callBy(argParameters); return kotlinConstructor.callBy(argParameters);
} }
public static boolean hasDefaultConstructorMarker(Constructor<?> ctor) {
int parameterCount = ctor.getParameterCount();
return parameterCount > 0 && ctor.getParameters()[parameterCount -1].getType() == DefaultConstructorMarker.class;
}
} }
} }

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2018 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -25,6 +25,7 @@ import org.jspecify.annotations.Nullable;
* analogous to an InvocationTargetException. * analogous to an InvocationTargetException.
* *
* @author Rod Johnson * @author Rod Johnson
* @author Juergen Hoeller
*/ */
@SuppressWarnings("serial") @SuppressWarnings("serial")
public class MethodInvocationException extends PropertyAccessException { public class MethodInvocationException extends PropertyAccessException {
@ -41,7 +42,9 @@ public class MethodInvocationException extends PropertyAccessException {
* @param cause the Throwable raised by the invoked method * @param cause the Throwable raised by the invoked method
*/ */
public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) { public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) {
super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause); super(propertyChangeEvent,
"Property '" + propertyChangeEvent.getPropertyName() + "' threw exception: " + cause,
cause);
} }
@Override @Override

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -125,9 +125,16 @@ public interface BeanFactory {
* beans <i>created</i> by the FactoryBean. For example, if the bean named * beans <i>created</i> by the FactoryBean. For example, if the bean named
* {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject} * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject}
* will return the factory, not the instance returned by the factory. * will return the factory, not the instance returned by the factory.
* @see #FACTORY_BEAN_PREFIX_CHAR
*/ */
String FACTORY_BEAN_PREFIX = "&"; String FACTORY_BEAN_PREFIX = "&";
/**
* Character variant of {@link #FACTORY_BEAN_PREFIX}.
* @since 6.2.6
*/
char FACTORY_BEAN_PREFIX_CHAR = '&';
/** /**
* Return an instance, which may be shared or independent, of the specified bean. * Return an instance, which may be shared or independent, of the specified bean.

View File

@ -73,7 +73,7 @@ public abstract class BeanFactoryUtils {
* @see BeanFactory#FACTORY_BEAN_PREFIX * @see BeanFactory#FACTORY_BEAN_PREFIX
*/ */
public static boolean isFactoryDereference(@Nullable String name) { public static boolean isFactoryDereference(@Nullable String name) {
return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); return (name != null && !name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR);
} }
/** /**
@ -85,14 +85,14 @@ public abstract class BeanFactoryUtils {
*/ */
public static String transformedBeanName(String name) { public static String transformedBeanName(String name) {
Assert.notNull(name, "'name' must not be null"); Assert.notNull(name, "'name' must not be null");
if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { if (name.isEmpty() || name.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) {
return name; return name;
} }
return transformedBeanNameCache.computeIfAbsent(name, beanName -> { return transformedBeanNameCache.computeIfAbsent(name, beanName -> {
do { do {
beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); beanName = beanName.substring(1); // length of '&'
} }
while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)); while (beanName.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR);
return beanName; return beanName;
}); });
} }

View File

@ -55,9 +55,8 @@ public interface BeanRegistry {
void registerAlias(String name, String alias); void registerAlias(String name, String alias);
/** /**
* Register a bean from the given bean class, which will be instantiated * Register a bean from the given bean class, which will be instantiated using the
* using the related {@link BeanUtils#getResolvableConstructor resolvable constructor} * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any.
* if any.
* @param beanClass the class of the bean * @param beanClass the class of the bean
* @return the generated bean name * @return the generated bean name
*/ */
@ -65,10 +64,9 @@ public interface BeanRegistry {
/** /**
* Register a bean from the given bean class, customizing it with the customizer * Register a bean from the given bean class, customizing it with the customizer
* callback. The bean will be instantiated using the supplier that can be * callback. The bean will be instantiated using the supplier that can be configured
* configured in the customizer callback, or will be tentatively instantiated * in the customizer callback, or will be tentatively instantiated with its
* with its {@link BeanUtils#getResolvableConstructor resolvable constructor} * {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise.
* otherwise.
* @param beanClass the class of the bean * @param beanClass the class of the bean
* @param customizer callback to customize other bean properties than the name * @param customizer callback to customize other bean properties than the name
* @return the generated bean name * @return the generated bean name
@ -76,9 +74,8 @@ public interface BeanRegistry {
<T> String registerBean(Class<T> beanClass, Consumer<Spec<T>> customizer); <T> String registerBean(Class<T> beanClass, Consumer<Spec<T>> customizer);
/** /**
* Register a bean from the given bean class, which will be instantiated * Register a bean from the given bean class, which will be instantiated using the
* using the related {@link BeanUtils#getResolvableConstructor resolvable constructor} * related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any.
* if any.
* @param name the name of the bean * @param name the name of the bean
* @param beanClass the class of the bean * @param beanClass the class of the bean
*/ */
@ -86,8 +83,8 @@ public interface BeanRegistry {
/** /**
* Register a bean from the given bean class, customizing it with the customizer * Register a bean from the given bean class, customizing it with the customizer
* callback. The bean will be instantiated using the supplier that can be * callback. The bean will be instantiated using the supplier that can be configured
* configured in the customizer callback, or will be tentatively instantiated with its * in the customizer callback, or will be tentatively instantiated with its
* {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise. * {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise.
* @param name the name of the bean * @param name the name of the bean
* @param beanClass the class of the bean * @param beanClass the class of the bean
@ -122,8 +119,8 @@ public interface BeanRegistry {
Spec<T> fallback(); Spec<T> fallback();
/** /**
* Hint that this bean has an infrastructure role, meaning it has no * Hint that this bean has an infrastructure role, meaning it has no relevance
* relevance to the end-user. * to the end-user.
* @see BeanDefinition#setRole(int) * @see BeanDefinition#setRole(int)
* @see BeanDefinition#ROLE_INFRASTRUCTURE * @see BeanDefinition#ROLE_INFRASTRUCTURE
*/ */
@ -136,8 +133,7 @@ public interface BeanRegistry {
Spec<T> lazyInit(); Spec<T> lazyInit();
/** /**
* Configure this bean as not a candidate for getting autowired into some * Configure this bean as not a candidate for getting autowired into another bean.
* other bean.
* @see BeanDefinition#setAutowireCandidate(boolean) * @see BeanDefinition#setAutowireCandidate(boolean)
*/ */
Spec<T> notAutowirable(); Spec<T> notAutowirable();
@ -184,6 +180,7 @@ public interface BeanRegistry {
Spec<T> targetType(ResolvableType type); Spec<T> targetType(ResolvableType type);
} }
/** /**
* Context available from the bean instance supplier designed to give access * Context available from the bean instance supplier designed to give access
* to bean dependencies. * to bean dependencies.
@ -191,10 +188,8 @@ public interface BeanRegistry {
interface SupplierContext { interface SupplierContext {
/** /**
* Return the bean instance that uniquely matches the given object type, * Return the bean instance that uniquely matches the given object type, if any.
* if any. * @param requiredType type the bean must match; can be an interface or superclass
* @param requiredType type the bean must match; can be an interface or
* superclass
* @return an instance of the single bean matching the required type * @return an instance of the single bean matching the required type
* @see BeanFactory#getBean(String) * @see BeanFactory#getBean(String)
*/ */
@ -240,4 +235,5 @@ public interface BeanRegistry {
*/ */
<T> ObjectProvider<T> beanProvider(ResolvableType requiredType); <T> ObjectProvider<T> beanProvider(ResolvableType requiredType);
} }
} }

View File

@ -208,8 +208,8 @@ public abstract class BeanFactoryAnnotationUtils {
} }
} }
} }
catch (NoSuchBeanDefinitionException ex) { catch (NoSuchBeanDefinitionException ignored) {
// Ignore - can't compare qualifiers for a manually registered singleton object // can't compare qualifiers for a manually registered singleton object
} }
} }
return false; return false;

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -68,10 +68,10 @@ public class AutowiredArgumentsCodeGenerator {
for (int i = startIndex; i < parameterTypes.length; i++) { for (int i = startIndex; i < parameterTypes.length; i++) {
code.add(i > startIndex ? ", " : ""); code.add(i > startIndex ? ", " : "");
if (!ambiguous) { if (!ambiguous) {
code.add("$L.get($L)", variableName, i - startIndex); code.add("$L.get($L)", variableName, i);
} }
else { else {
code.add("$L.get($L, $T.class)", variableName, i - startIndex, parameterTypes[i]); code.add("$L.get($L, $T.class)", variableName, i, parameterTypes[i]);
} }
} }
return code.build(); return code.build();

View File

@ -38,7 +38,6 @@ import org.jspecify.annotations.Nullable;
import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.GeneratedMethods;
import org.springframework.aot.generate.ValueCodeGenerator; import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGenerator.Delegate; import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHints;
@ -103,12 +102,12 @@ class BeanDefinitionPropertiesCodeGenerator {
this.hints = hints; this.hints = hints;
this.attributeFilter = attributeFilter; this.attributeFilter = attributeFilter;
List<Delegate> allDelegates = new ArrayList<>(); List<Delegate> customDelegates = new ArrayList<>();
allDelegates.add((valueCodeGenerator, value) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), value)); customDelegates.add((valueCodeGenerator, value) ->
allDelegates.addAll(additionalDelegates); customValueCodeGenerator.apply(PropertyNamesStack.peek(), value));
allDelegates.addAll(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES); customDelegates.addAll(additionalDelegates);
allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES); this.valueCodeGenerator = BeanDefinitionPropertyValueCodeGeneratorDelegates
this.valueCodeGenerator = ValueCodeGenerator.with(allDelegates).scoped(generatedMethods); .createValueCodeGenerator(generatedMethods, customDelegates);
} }
@SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2024 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.beans.factory.aot; package org.springframework.beans.factory.aot;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -46,7 +47,7 @@ import org.springframework.javapoet.CodeBlock;
* @author Stephane Nicoll * @author Stephane Nicoll
* @since 6.1.2 * @since 6.1.2
*/ */
abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates { public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
/** /**
* A list of {@link Delegate} implementations for the following common bean * A list of {@link Delegate} implementations for the following common bean
@ -73,6 +74,26 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
); );
/**
* Create a {@link ValueCodeGenerator} instance with both these
* {@link #INSTANCES delegate} and the {@link ValueCodeGeneratorDelegates#INSTANCES
* core delegates}.
* @param generatedMethods the {@link GeneratedMethods} to use
* @param customDelegates additional delegates that should be considered first
* @return a configured value code generator
* @since 7.0
* @see ValueCodeGenerator#add(List)
*/
public static ValueCodeGenerator createValueCodeGenerator(
GeneratedMethods generatedMethods, List<Delegate> customDelegates) {
List<Delegate> allDelegates = new ArrayList<>();
allDelegates.addAll(customDelegates);
allDelegates.addAll(INSTANCES);
allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES);
return ValueCodeGenerator.with(allDelegates).scoped(generatedMethods);
}
/** /**
* {@link Delegate} for {@link ManagedList} types. * {@link Delegate} for {@link ManagedList} types.
*/ */
@ -155,6 +176,8 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
.builder(SuppressWarnings.class) .builder(SuppressWarnings.class)
.addMember("value", "{\"rawtypes\", \"unchecked\"}") .addMember("value", "{\"rawtypes\", \"unchecked\"}")
.build()); .build());
method.addModifiers(javax.lang.model.element.Modifier.PRIVATE,
javax.lang.model.element.Modifier.STATIC);
method.returns(Map.class); method.returns(Map.class);
method.addStatement("$T map = new $T($L)", Map.class, method.addStatement("$T map = new $T($L)", Map.class,
LinkedHashMap.class, map.size()); LinkedHashMap.class, map.size());

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2023 the original author or authors. * Copyright 2002-2025 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -97,10 +97,10 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
* Constant that indicates determining an appropriate autowire strategy * Constant that indicates determining an appropriate autowire strategy
* through introspection of the bean class. * through introspection of the bean class.
* @see #autowire * @see #autowire
* @deprecated as of Spring 3.0: If you are using mixed autowiring strategies, * @deprecated If you are using mixed autowiring strategies, prefer
* prefer annotation-based autowiring for clearer demarcation of autowiring needs. * annotation-based autowiring for clearer demarcation of autowiring needs.
*/ */
@Deprecated @Deprecated(since = "3.0")
int AUTOWIRE_AUTODETECT = 4; int AUTOWIRE_AUTODETECT = 4;
/** /**
@ -188,7 +188,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
* @see #AUTOWIRE_BY_NAME * @see #AUTOWIRE_BY_NAME
* @see #AUTOWIRE_BY_TYPE * @see #AUTOWIRE_BY_TYPE
* @see #AUTOWIRE_CONSTRUCTOR * @see #AUTOWIRE_CONSTRUCTOR
* @deprecated as of 6.1, in favor of {@link #createBean(Class)} * @deprecated in favor of {@link #createBean(Class)}
*/ */
@Deprecated(since = "6.1") @Deprecated(since = "6.1")
Object createBean(Class<?> beanClass, int autowireMode, boolean dependencyCheck) throws BeansException; Object createBean(Class<?> beanClass, int autowireMode, boolean dependencyCheck) throws BeansException;

View File

@ -19,21 +19,18 @@ package org.springframework.beans.factory.config;
import java.io.IOException; import java.io.IOException;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.Serializable; import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import kotlin.reflect.KProperty;
import kotlin.reflect.jvm.ReflectJvmMapping;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.InjectionPoint; import org.springframework.beans.factory.InjectionPoint;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.core.KotlinDetector;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.Nullness;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableType;
import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.TypeDescriptor;
@ -162,28 +159,13 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable
} }
if (this.field != null) { if (this.field != null) {
return !(this.field.getType() == Optional.class || hasNullableAnnotation() || return !(this.field.getType() == Optional.class || Nullness.forField(this.field) == Nullness.NULLABLE);
(KotlinDetector.isKotlinType(this.field.getDeclaringClass()) && KotlinDelegate.isNullable(this.field)));
} }
else { else {
return !obtainMethodParameter().isOptional(); return !obtainMethodParameter().isOptional();
} }
} }
/**
* Check whether the underlying field is annotated with any variant of a
* {@code Nullable} annotation, for example, {@code jakarta.annotation.Nullable} or
* {@code edu.umd.cs.findbugs.annotations.Nullable}.
*/
private boolean hasNullableAnnotation() {
for (Annotation ann : getAnnotations()) {
if ("Nullable".equals(ann.annotationType().getSimpleName())) {
return true;
}
}
return false;
}
/** /**
* Return whether this dependency is 'eager' in the sense of * Return whether this dependency is 'eager' in the sense of
* eagerly resolving potential target beans for type matching. * eagerly resolving potential target beans for type matching.
@ -445,19 +427,4 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable
} }
} }
/**
* Inner class to avoid a hard dependency on Kotlin at runtime.
*/
private static class KotlinDelegate {
/**
* Check whether the specified {@link Field} represents a nullable Kotlin type or not.
*/
public static boolean isNullable(Field field) {
KProperty<?> property = ReflectJvmMapping.getKotlinProperty(field);
return (property != null && property.getReturnType().isMarkedNullable());
}
}
} }

Some files were not shown because too many files have changed in this diff Show More