Merge branch 'main' into add-pmd
This commit is contained in:
commit
ad78c14cf8
|
@ -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
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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 }}
|
||||||
|
|
|
@ -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
|
||||||
|
|
13
build.gradle
13
build.gradle
|
@ -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[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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[]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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).
|
|
@ -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).
|
|
|
@ -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"
|
||||||
|
|
|
@ -255,5 +255,3 @@ For Kotlin `Flow`, a `Flow<T>.transactional` extension is provided.
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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`]
|
||||||
|
|
|
@ -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.
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
======
|
======
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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]
|
||||||
======
|
======
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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`)
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
----
|
||||||
|
======
|
||||||
|
--
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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()
|
||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
|
@ -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.
|
|
@ -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(
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
======
|
======
|
|
@ -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`]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
----
|
----
|
||||||
======
|
======
|
||||||
|
|
||||||
|
|
|
@ -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]
|
||||||
======
|
======
|
||||||
|
|
|
@ -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.
|
|
@ -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).
|
||||||
|
|
|
@ -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.
|
|
@ -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]
|
||||||
======
|
======
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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].
|
||||||
|
|
|
@ -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]
|
||||||
|
|
||||||
|
|
|
@ -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]#
|
||||||
|
|
|
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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`.
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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[]
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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[]
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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
Loading…
Reference in New Issue