Merge branch 'main' into add-pmd
This commit is contained in:
commit
ad78c14cf8
|
@ -19,7 +19,7 @@ inputs:
|
|||
java-version:
|
||||
description: 'Java version to compile and test with'
|
||||
required: false
|
||||
default: '17'
|
||||
default: '24'
|
||||
publish:
|
||||
description: 'Whether to publish artifacts ready for deployment to Artifactory'
|
||||
required: false
|
||||
|
|
|
@ -15,7 +15,7 @@ runs:
|
|||
using: composite
|
||||
steps:
|
||||
- name: Generate Changelog
|
||||
uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11
|
||||
uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12
|
||||
with:
|
||||
config-file: .github/actions/create-github-release/changelog-generator.yml
|
||||
milestone: ${{ inputs.milestone }}
|
||||
|
|
|
@ -19,7 +19,7 @@ inputs:
|
|||
java-version:
|
||||
description: 'Java version to use for the build'
|
||||
required: false
|
||||
default: '17'
|
||||
default: '24'
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
|
@ -31,7 +31,7 @@ runs:
|
|||
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
|
||||
${{ inputs.java-toolchain == 'true' && '17' || '' }}
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
cache-read-only: false
|
||||
develocity-access-key: ${{ inputs.develocity-access-key }}
|
||||
|
|
|
@ -46,7 +46,7 @@ jobs:
|
|||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
with:
|
||||
cache-read-only: false
|
||||
- name: Configure Gradle Properties
|
||||
|
|
13
build.gradle
13
build.gradle
|
@ -1,12 +1,12 @@
|
|||
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
|
||||
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
|
||||
id 'org.jetbrains.dokka' version '1.9.20'
|
||||
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
|
||||
id 'io.github.goooler.shadow' version '8.1.8' 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 {
|
||||
|
@ -75,12 +75,11 @@ configure([rootProject] + javaProjects) { project ->
|
|||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jakarta.ee/specifications/platform/11/apidocs/",
|
||||
"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://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://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://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"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
|
||||
// JakartaEE equivalents in the jakarta.annotation package.
|
||||
//"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[]
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,9 @@ dependencies {
|
|||
implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
|
||||
|
||||
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 {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
org.gradle.caching=true
|
||||
javaFormatVersion=0.0.42
|
||||
junitJupiterVersion=5.11.4
|
||||
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.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
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"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
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.tasks.compile.JavaCompile;
|
||||
import org.gradle.jvm.toolchain.JavaLanguageVersion;
|
||||
import org.gradle.jvm.toolchain.JvmVendorSpec;
|
||||
|
||||
/**
|
||||
* {@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;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
||||
|
@ -83,7 +85,6 @@ public class JavaConventions {
|
|||
*/
|
||||
private static void applyToolchainConventions(Project project) {
|
||||
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
|
||||
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
|
||||
toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ import java.util.Map;
|
|||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
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.TestRetryTaskExtension;
|
||||
|
||||
|
@ -34,6 +36,7 @@ import org.gradle.testretry.TestRetryTaskExtension;
|
|||
*
|
||||
* @author Brian Clozel
|
||||
* @author Andy Wilkinson
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class TestConventions {
|
||||
|
||||
|
@ -50,7 +53,12 @@ class TestConventions {
|
|||
}
|
||||
|
||||
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.setSystemProperties(Map.of(
|
||||
"java.awt.headless", "true",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
plugins {
|
||||
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"
|
||||
|
@ -20,7 +20,12 @@ dependencies {
|
|||
}
|
||||
|
||||
javadoc {
|
||||
javadocTool.set(javaToolchains.javadocToolFor({
|
||||
languageVersion = JavaLanguageVersion.of(24)
|
||||
}))
|
||||
|
||||
title = "${rootProject.description} ${version} API"
|
||||
failOnError = true
|
||||
options {
|
||||
encoding = "UTF-8"
|
||||
memberLevel = JavadocMemberLevel.PROTECTED
|
||||
|
@ -31,8 +36,13 @@ javadoc {
|
|||
destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile
|
||||
splitIndex = true
|
||||
links(rootProject.ext.javadocLinks)
|
||||
addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
// Check for 'syntax' and 'reference' during linting.
|
||||
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"
|
||||
doFirst {
|
||||
|
|
|
@ -49,35 +49,35 @@ tasks.named('compileKotlin', KotlinCompilationTask.class) {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":spring-aspects"))
|
||||
api(project(":spring-context"))
|
||||
api(project(":spring-context-support"))
|
||||
api(project(":spring-core-test"))
|
||||
api(project(":spring-jdbc"))
|
||||
api(project(":spring-jms"))
|
||||
api(project(":spring-test"))
|
||||
api(project(":spring-web"))
|
||||
api(project(":spring-webflux"))
|
||||
api(project(":spring-webmvc"))
|
||||
api(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(project(":spring-aspects"))
|
||||
implementation(project(":spring-context"))
|
||||
implementation(project(":spring-context-support"))
|
||||
implementation(project(":spring-core-test"))
|
||||
implementation(project(":spring-jdbc"))
|
||||
implementation(project(":spring-jms"))
|
||||
implementation(project(":spring-test"))
|
||||
implementation(project(":spring-web"))
|
||||
implementation(project(":spring-webflux"))
|
||||
implementation(project(":spring-webmvc"))
|
||||
implementation(project(":spring-websocket"))
|
||||
|
||||
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-ann-async.adoc[]
|
||||
*** xref:web/webmvc-cors.adoc[]
|
||||
*** xref:web/webmvc-versioning.adoc[]
|
||||
*** xref:web/webmvc/mvc-ann-rest-exceptions.adoc[]
|
||||
*** xref:web/webmvc/mvc-security.adoc[]
|
||||
*** xref:web/webmvc/mvc-caching.adoc[]
|
||||
|
@ -225,6 +226,7 @@
|
|||
**** xref:web/webmvc/mvc-config/static-resources.adoc[]
|
||||
**** xref:web/webmvc/mvc-config/default-servlet-handler.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-xml.adoc[]
|
||||
*** xref:web/webmvc/mvc-http2.adoc[]
|
||||
|
@ -292,6 +294,7 @@
|
|||
*** xref:web/webflux-functional.adoc[]
|
||||
*** xref:web/webflux/uri-building.adoc[]
|
||||
*** xref:web/webflux-cors.adoc[]
|
||||
*** xref:web/webflux-versioning.adoc[]
|
||||
*** xref:web/webflux/ann-rest-exceptions.adoc[]
|
||||
*** xref:web/webflux/security.adoc[]
|
||||
*** xref:web/webflux/caching.adoc[]
|
||||
|
@ -432,8 +435,8 @@
|
|||
*** xref:integration/cache/plug.adoc[]
|
||||
*** xref:integration/cache/specific-config.adoc[]
|
||||
** xref:integration/observability.adoc[]
|
||||
** xref:integration/aot-cache.adoc[]
|
||||
** xref:integration/checkpoint-restore.adoc[]
|
||||
** xref:integration/cds.adoc[]
|
||||
** xref:integration/appendix.adoc[]
|
||||
* xref:languages.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`]
|
||||
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`
|
||||
| Instructs Spring to ignore Objenesis, not even attempting to use it. See
|
||||
{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
|
||||
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`
|
||||
| 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.
|
||||
|
|
|
@ -101,8 +101,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig
|
|||
|
||||
Using the above configuration ensures Spring initialization failure if any `${}`
|
||||
placeholder could not be resolved. It is also possible to use methods like
|
||||
`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or
|
||||
`setEscapeCharacter` to customize placeholders.
|
||||
`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or
|
||||
`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
|
||||
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]]
|
||||
=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer`
|
||||
=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer`
|
||||
|
||||
You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values
|
||||
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,
|
||||
a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some
|
||||
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.
|
||||
properties of the `DataSource`. The values to replace are specified as placeholders of the
|
||||
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:
|
||||
|
||||
|
@ -355,11 +355,15 @@ jdbc.password=root
|
|||
----
|
||||
|
||||
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 `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
|
||||
attributes of a bean definition. Furthermore, you can customize the placeholder prefix and suffix.
|
||||
the same applies for other placeholder values that match keys in the properties file. The
|
||||
`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
|
||||
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
|
||||
comma-separated list in the `location` attribute, as the following example shows:
|
||||
|
||||
|
|
|
@ -1,56 +1,56 @@
|
|||
[[null-safety]]
|
||||
= Null-safety
|
||||
|
||||
Although Java does not let you express null-safety with its type system, the Spring Framework codebase is annotated with
|
||||
https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullness of APIs, fields and related type
|
||||
usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly recommended in order to get
|
||||
familiar with those annotations and semantics.
|
||||
Although Java does not let you express nullness markers with its type system yet, the Spring Framework codebase is
|
||||
annotated with https://jspecify.dev/docs/start-here/[JSpecify] annotations to declare the nullability of its APIs,
|
||||
fields, and related type usages. Reading the https://jspecify.dev/docs/user-guide/[JSpecify user guide] is highly
|
||||
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
|
||||
build time checks and to turn explicit nullness into a way to express the possible absence of value. It is useful in
|
||||
both Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting null-safety
|
||||
annotations such as IntelliJ IDEA or Eclipse) and Kotlin where JSpecify annotations are automatically translated to
|
||||
The primary goal of this null-safety arrangement is to prevent a `NullPointerException` from being thrown at runtime via build
|
||||
time checks and to use explicit nullability as a way to express the possible absence of value. It is useful in both
|
||||
Java by leveraging some tooling (https://github.com/uber/NullAway[NullAway] or IDEs supporting JSpecify annotations
|
||||
such as IntelliJ IDEA) and Kotlin where JSpecify annotations are automatically translated to
|
||||
{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
|
||||
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
|
||||
type usage, a field, a method return type, or a parameter. It provides full support for JSpecify annotations,
|
||||
Kotlin null safety, and Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the
|
||||
package).
|
||||
|
||||
[[null-safety-libraries]]
|
||||
== Annotating libraries with JSpecify annotations
|
||||
|
||||
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
|
||||
its build. It is recommended for each library depending on Spring Framework (Spring portfolio projects), as
|
||||
well as other libraries related to the Spring ecosystem (Reactor, Micrometer and Spring community projects), to do the
|
||||
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 and Spring portfolio projects, as
|
||||
well as other libraries related to the Spring ecosystem (Reactor, Micrometer, and Spring community projects), to do the
|
||||
same.
|
||||
|
||||
[[null-safety-applications]]
|
||||
== Leveraging JSpecify annotations in Spring applications
|
||||
|
||||
Developing applications with IDEs supporting null-safety annotations, such as IntelliJ IDEA or Eclipse, will provide
|
||||
warnings in Java and errors in Kotlin when the null-safety contracts are not honored, allowing Spring application
|
||||
developers to refine their null handling to prevent `NullPointerException` to be thrown at runtime.
|
||||
Developing applications with IDEs that support nullness annotations will provide warnings in Java and errors in Kotlin
|
||||
when the nullability contracts are not honored, allowing Spring application developers to refine their null handling to
|
||||
prevent a `NullPointerException` from being thrown at runtime.
|
||||
|
||||
Optionally, Spring application developers can annotate their codebase and use https://github.com/uber/NullAway[NullAway]
|
||||
to enforce null-safety during build time at application level.
|
||||
Optionally, Spring application developers can annotate their codebase and use build plugins like
|
||||
https://github.com/uber/NullAway[NullAway] to enforce null-safety at the application level during build time.
|
||||
|
||||
[[null-safety-guidelines]]
|
||||
== Guidelines
|
||||
|
||||
The purpose of this section is to share some guidelines proposed for specifying explicitly the nullness of Spring-related
|
||||
libraries or applications.
|
||||
The purpose of this section is to share some proposed guidelines for explicitly specifying the nullability of
|
||||
Spring-related libraries or applications.
|
||||
|
||||
|
||||
[[null-safety-guidelines-jpecify]]
|
||||
[[null-safety-guidelines-jspecify]]
|
||||
=== JSpecify
|
||||
|
||||
The key points to understand is that by default, the nullness of types is unknown in Java, 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
|
||||
that by default, type usages are 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
|
||||
at package level via a `package-info.java` file, for example:
|
||||
The key points to understand are that the nullness of types is unknown in Java by default and that non-null type
|
||||
usage is by far more frequent than nullable usage. In order to keep codebases readable, we typically want to define
|
||||
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`] which is typically set in Spring
|
||||
projects at the package level via a `package-info.java` file, for example:
|
||||
|
||||
[source,java,subs="verbatim,quotes",chomp="-packages",fold="none"]
|
||||
----
|
||||
|
@ -60,9 +60,9 @@ package org.springframework.core;
|
|||
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
|
||||
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:
|
||||
|
||||
|
@ -71,7 +71,7 @@ For example, for a field:
|
|||
private @Nullable String fileEncoding;
|
||||
----
|
||||
|
||||
Or for method parameters and return value:
|
||||
Or for method parameters and method return types:
|
||||
|
||||
[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
|
||||
nullness annotations should be repeated if you just want to override the implementation and keep the same API
|
||||
nullness.
|
||||
[NOTE]
|
||||
====
|
||||
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
|
||||
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
|
||||
initially surprising:
|
||||
|
||||
- `@Nullable Object[] array` means individual elements can be null but the array itself can't.
|
||||
- `Object @Nullable [] array` means individual elements can't be null but the array itself can.
|
||||
- `@Nullable Object[] array` means individual elements can be null but the array itself cannot.
|
||||
- `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.
|
||||
|
||||
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:
|
||||
|
||||
- `Cache.@Nullable ValueWrapper`
|
||||
|
@ -111,15 +114,15 @@ typical use cases.
|
|||
|
||||
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
|
||||
{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
|
||||
{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
|
||||
invocation, the value passed as a parameter is not null.
|
||||
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
|
||||
with `@Contract("null, _ -> fail")`. With that contract declaration, NullAway will understand that the value passed as a
|
||||
parameter cannot be null after a successful invocation of `Assert.notNull()`.
|
||||
|
||||
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
|
||||
|
@ -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
|
||||
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
|
||||
generates no warning with the recommended configuration mentioned above.
|
||||
generates no warning with the recommended configuration mentioned previously in this section.
|
||||
|
||||
==== 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:
|
||||
|
||||
- `@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
|
||||
- `@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
|
||||
{spring-framework-api}/beans/factory/InitializingBean.html[`InitializingBean`].
|
||||
- `@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
|
||||
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
|
||||
non-null values even if that can't be expressed by the API.
|
||||
- `@SuppressWarnings("NullAway") // Well-known map keys` can be used when `Map#get` invocations are done with keys known
|
||||
to be present and non-null related values inserted previously.
|
||||
- `@SuppressWarnings("NullAway") // Overridden method does not define nullness` can be used when the super class does
|
||||
not define nullness (typically when the super class is coming from a dependency).
|
||||
- `@SuppressWarnings("NullAway") // Reflection` can be used for some reflection operations that are known to return
|
||||
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 performed with keys that are known
|
||||
to be present and when non-null related values have been inserted previously.
|
||||
- `@SuppressWarnings("NullAway") // Overridden method does not define nullability` can be used when the superclass does
|
||||
not define nullability (typically when the superclass comes from a dependency).
|
||||
|
||||
|
||||
[[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-framework-api}/lang/NonNull.html[`@NonNull`],
|
||||
{spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`], and
|
||||
{spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`] in the `org.springframework.lang` package have been
|
||||
introduced in Spring Framework 5 when JSpecify did not exist and the best option was to leverage JSR 305 (a dormant
|
||||
but widespread JSR) meta-annotations. They are deprecated as of Spring Framework 7 in favor of
|
||||
{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 at that time was to leverage
|
||||
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
|
||||
defined specifications, a canonical dependency with no split-package issue, better tooling, better Kotlin integration
|
||||
and the capability to specify the nullness more precisely for more use cases.
|
||||
defined specifications, a canonical dependency with no split-package issues, better tooling, better Kotlin integration,
|
||||
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,
|
||||
parameters and return values while JSpecify annotations apply to type usages. This subtle difference
|
||||
is in practice pretty significant, as it allows for example to differentiate the nullness of elements from the
|
||||
nullness of arrays/varargs as well as defining the nullness of generic types.
|
||||
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 usage. This subtle difference
|
||||
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 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
|
||||
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`
|
||||
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.
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
- 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"
|
||||
|
||||
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:
|
||||
|
||||
* 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-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-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-resttemplate[`RestTemplate`] -- synchronous client with template method API
|
||||
* xref:integration/rest-clients.adoc#rest-http-interface[HTTP Interface Clients] -- annotated interface backed by generated proxy
|
||||
|
||||
|
||||
[[rest-restclient]]
|
||||
== `RestClient`
|
||||
|
||||
The `RestClient` is a synchronous HTTP client that offers a modern, fluent API.
|
||||
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.
|
||||
`RestClient` is a synchronous HTTP client that provides a fluent API to perform requests.
|
||||
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.
|
||||
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.
|
||||
`RestClient` has static `create` shortcut methods.
|
||||
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]
|
||||
======
|
||||
|
@ -30,15 +38,17 @@ Java::
|
|||
+
|
||||
[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())
|
||||
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build();
|
||||
|
@ -48,32 +58,34 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
val defaultClient = RestClient.create()
|
||||
val defaultClient = RestClient.create()
|
||||
|
||||
val customClient = RestClient.builder()
|
||||
val customClient = RestClient.builder()
|
||||
.requestFactory(HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(mapOf("variable" to "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.defaultVersion("1.2")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.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.
|
||||
This can be done with `method(HttpMethod)` or with the convenience methods `get()`, `head()`, `post()`, and so on.
|
||||
To perform an HTTP request, first specify the HTTP method to use.
|
||||
Use the convenience methods like `get()`, `head()`, `post()`, and others, or `method(HttpMethod)`.
|
||||
|
||||
==== Request URL
|
||||
|
||||
Next, the request URI can be specified with the `uri` methods.
|
||||
This step is optional and can be skipped if the `RestClient` is configured with a default URI.
|
||||
Next, specify the request URI with the `uri` methods.
|
||||
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 following example configures a GET request to `https://example.com/orders/42`:
|
||||
The following shows how to perform a request:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
@ -81,20 +93,20 @@ Java::
|
|||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
int id = 42;
|
||||
restClient.get()
|
||||
int id = 42;
|
||||
restClient.get()
|
||||
.uri("https://example.com/orders/{id}", id)
|
||||
....
|
||||
// ...
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val id = 42
|
||||
restClient.get()
|
||||
val id = 42
|
||||
restClient.get()
|
||||
.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.
|
||||
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>>.
|
||||
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"]
|
||||
----
|
||||
String result = restClient.get() <1>
|
||||
String result = restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body(String.class); <4>
|
||||
|
||||
System.out.println(result); <5>
|
||||
System.out.println(result); <5>
|
||||
----
|
||||
<1> Set up a GET request
|
||||
<2> Specify the URL to connect to
|
||||
|
@ -150,12 +163,12 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result= restClient.get() <1>
|
||||
val result= restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body<String>() <4>
|
||||
|
||||
println(result) <5>
|
||||
println(result) <5>
|
||||
----
|
||||
<1> Set up a GET request
|
||||
<2> Specify the URL to connect to
|
||||
|
@ -172,14 +185,14 @@ Java::
|
|||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ResponseEntity<String> result = restClient.get() <1>
|
||||
ResponseEntity<String> result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity(String.class); <2>
|
||||
|
||||
System.out.println("Response status: " + result.getStatusCode()); <3>
|
||||
System.out.println("Response headers: " + result.getHeaders()); <3>
|
||||
System.out.println("Contents: " + result.getBody()); <3>
|
||||
System.out.println("Response status: " + result.getStatusCode()); <3>
|
||||
System.out.println("Response headers: " + result.getHeaders()); <3>
|
||||
System.out.println("Contents: " + result.getBody()); <3>
|
||||
----
|
||||
<1> Set up a GET request for the specified URL
|
||||
<2> Convert the response into a `ResponseEntity`
|
||||
|
@ -189,14 +202,14 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result = restClient.get() <1>
|
||||
val result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity<String>() <2>
|
||||
|
||||
println("Response status: " + result.statusCode) <3>
|
||||
println("Response headers: " + result.headers) <3>
|
||||
println("Contents: " + result.body) <3>
|
||||
println("Response status: " + result.statusCode) <3>
|
||||
println("Response headers: " + result.headers) <3>
|
||||
println("Contents: " + result.body) <3>
|
||||
----
|
||||
<1> Set up a GET request for the specified URL
|
||||
<2> Convert the response into a `ResponseEntity`
|
||||
|
@ -212,8 +225,8 @@ Java::
|
|||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
int id = ...;
|
||||
Pet pet = restClient.get()
|
||||
int id = ...;
|
||||
Pet pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
|
@ -227,8 +240,8 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val id = ...
|
||||
val pet = restClient.get()
|
||||
val id = ...
|
||||
val pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
|
@ -247,8 +260,8 @@ Java::
|
|||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Pet pet = ... <1>
|
||||
ResponseEntity<Void> response = restClient.post() <2>
|
||||
Pet pet = ... <1>
|
||||
ResponseEntity<Void> response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
|
@ -265,8 +278,8 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val pet: Pet = ... <1>
|
||||
val response = restClient.post() <2>
|
||||
val pet: Pet = ... <1>
|
||||
val response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
|
@ -291,7 +304,7 @@ Java::
|
|||
+
|
||||
[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>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2>
|
||||
|
@ -307,7 +320,7 @@ Kotlin::
|
|||
+
|
||||
[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>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2>
|
||||
|
@ -330,7 +343,7 @@ Java::
|
|||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Pet result = restClient.get()
|
||||
Pet result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(APPLICATION_JSON)
|
||||
.exchange((request, response) -> { <1>
|
||||
|
@ -351,7 +364,7 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result = restClient.get()
|
||||
val result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.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"]
|
||||
----
|
||||
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
|
||||
value.setSerializationView(User.WithoutPasswordView.class);
|
||||
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
|
||||
value.setSerializationView(User.WithoutPasswordView.class);
|
||||
|
||||
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
|
||||
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
|
||||
.contentType(APPLICATION_JSON)
|
||||
.body(value)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
|
||||
----
|
||||
|
||||
==== Multipart
|
||||
|
@ -398,17 +410,17 @@ For example:
|
|||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
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.
|
||||
|
@ -845,15 +857,17 @@ It can be used to migrate from the latter to the former.
|
|||
|
||||
|
||||
[[rest-http-interface]]
|
||||
== HTTP Interface
|
||||
== HTTP Interface Clients
|
||||
|
||||
The Spring Framework lets you define an HTTP service as a Java interface with
|
||||
`@HttpExchange` methods. You can pass such an interface to `HttpServiceProxyFactory`
|
||||
to create a proxy which performs requests through an HTTP client such as `RestClient`
|
||||
or `WebClient`. You can also implement the interface from an `@Controller` for server
|
||||
request handling.
|
||||
You can define an HTTP Service as a Java interface with `@HttpExchange` methods, and use
|
||||
`HttpServiceProxyFactory` to create a client proxy from it for remote access over HTTP via
|
||||
`RestClient`, `WebClient`, or `RestTemplate`. On the server side, an `@Controller` class
|
||||
can implement the same interface to handle requests with
|
||||
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"]
|
||||
----
|
||||
|
@ -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.
|
||||
|
||||
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:
|
||||
Optionally, use `@HttpExchange` at the type level to declare common attributes for all methods:
|
||||
|
||||
[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]]
|
||||
=== Method Parameters
|
||||
|
||||
Annotated, HTTP exchange methods support flexible method signatures with the following
|
||||
method parameters:
|
||||
`@HttpExchange` methods support flexible method signatures with the following inputs:
|
||||
|
||||
[cols="1,2", options="header"]
|
||||
|===
|
||||
| Method argument | Description
|
||||
| Method parameter | Description
|
||||
|
||||
| `URI`
|
||||
| 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
|
||||
{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]]
|
||||
=== Custom argument resolver
|
||||
=== Custom Arguments
|
||||
|
||||
For more complex cases, HTTP interfaces do not support `RequestEntity` types as method parameters.
|
||||
This would take over the entire HTTP request and not improve the semantics of the interface.
|
||||
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:
|
||||
You can configure a custom `HttpServiceArgumentResolver`. The example interface below
|
||||
uses a custom `Search` method parameter type:
|
||||
|
||||
include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0]
|
||||
|
||||
We can implement our own `HttpServiceArgumentResolver` that supports our custom `Search` type
|
||||
and writes its data in the outgoing HTTP request.
|
||||
A custom argument resolver could be implemented like this:
|
||||
|
||||
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]
|
||||
|
||||
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]]
|
||||
=== 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
|
||||
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]]
|
||||
=== Error Handling
|
||||
|
||||
To customize error response handling, you need to configure the underlying HTTP client.
|
||||
|
||||
For `RestClient`:
|
||||
|
||||
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:
|
||||
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
|
||||
status codes. To customize this, register a response status handler that applies to all
|
||||
responses performed through the client as follows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// For RestClient
|
||||
RestClient restClient = RestClient.builder()
|
||||
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...)
|
||||
.build();
|
||||
|
||||
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
|
||||
`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"]
|
||||
----
|
||||
// or for WebClient...
|
||||
WebClient webClient = WebClient.builder()
|
||||
.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
|
||||
.build();
|
||||
|
||||
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
|
||||
`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"]
|
||||
----
|
||||
// or for RestTemplate...
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setErrorHandler(myErrorHandler);
|
||||
|
||||
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
|
||||
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
----
|
||||
|
||||
For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and
|
||||
the `ResponseErrorHandler` hierarchy.
|
||||
For more details and options such as suppressing error status codes, see the reference
|
||||
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.
|
||||
|
||||
As an alternative, you can customize the property placeholder prefix by declaring the
|
||||
following configuration beans:
|
||||
following `PropertySourcesPlaceholderConfigurer` bean:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
|
@ -200,8 +200,10 @@ following configuration beans:
|
|||
}
|
||||
----
|
||||
|
||||
You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`)
|
||||
that uses the `${...}` syntax, with configuration beans, as the following example shows:
|
||||
You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use
|
||||
the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by
|
||||
declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example
|
||||
shows:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
|
@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl
|
|||
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]]
|
||||
|
@ -296,17 +301,17 @@ for example when writing a `org.springframework.core.convert.converter.Converter
|
|||
|
||||
[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`.
|
||||
[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,
|
||||
|
@ -319,7 +324,7 @@ progresses.
|
|||
== Testing
|
||||
|
||||
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.
|
||||
|
||||
NOTE: If you are using Spring Boot, see
|
||||
|
@ -330,7 +335,7 @@ NOTE: If you are using Spring Boot, see
|
|||
=== Constructor injection
|
||||
|
||||
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
|
||||
{spring-framework-api}/test/context/TestConstructor.html[`@TestConstructor(autowireMode = AutowireMode.ALL)`]
|
||||
to enable autowiring for all parameters.
|
||||
|
@ -340,13 +345,14 @@ file with a `spring.test.constructor.autowire.mode = all` property.
|
|||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
@TestConstructor(autowireMode = AutowireMode.ALL)
|
||||
class OrderServiceIntegrationTests(val orderService: OrderService,
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
@TestConstructor(autowireMode = AutowireMode.ALL)
|
||||
class OrderServiceIntegrationTests(
|
||||
val orderService: OrderService,
|
||||
val customerService: CustomerService) {
|
||||
|
||||
// tests that use the injected OrderService and CustomerService
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
@ -354,7 +360,7 @@ class OrderServiceIntegrationTests(val orderService: OrderService,
|
|||
=== `PER_CLASS` Lifecycle
|
||||
|
||||
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`
|
||||
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
|
||||
|
||||
You can create specification-like tests with JUnit 5 and Kotlin.
|
||||
The following example shows how to do so:
|
||||
You can create specification-like tests with Kotlin and JUnit Jupiter's `@Nested` test
|
||||
class support. The following example shows how to do so:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
class SpecificationLikeTests {
|
||||
class SpecificationLikeTests {
|
||||
|
||||
@Nested
|
||||
@DisplayName("a calculator")
|
||||
inner class Calculator {
|
||||
|
||||
val calculator = SampleCalculator()
|
||||
|
||||
@Test
|
||||
|
@ -422,7 +429,7 @@ class SpecificationLikeTests {
|
|||
assertEquals(2, subtract)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
[[kotlin-web]]
|
||||
= Web
|
||||
|
||||
|
||||
|
||||
[[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]
|
||||
----
|
||||
@Configuration
|
||||
class RouterRouterConfiguration {
|
||||
@Configuration
|
||||
class RouterRouterConfiguration {
|
||||
|
||||
@Bean
|
||||
fun mainRouter(userHandler: UserHandler) = router {
|
||||
|
@ -36,7 +34,7 @@ class RouterRouterConfiguration {
|
|||
}
|
||||
resources("/**", ClassPathResource("static/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
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]
|
||||
----
|
||||
val mockMvc: MockMvc = ...
|
||||
mockMvc.get("/person/{name}", "Lee") {
|
||||
val mockMvc: MockMvc = ...
|
||||
mockMvc.get("/person/{name}", "Lee") {
|
||||
secure = true
|
||||
accept = APPLICATION_JSON
|
||||
headers {
|
||||
contentLanguage = Locale.FRANCE
|
||||
}
|
||||
principal = Principal { "foo" }
|
||||
}.andExpect {
|
||||
}.andExpect {
|
||||
status { isOk }
|
||||
content { contentType(APPLICATION_JSON) }
|
||||
jsonPath("$.name") { value("Lee") }
|
||||
content { json("""{"someBoolean": false}""", false) }
|
||||
}.andDo {
|
||||
}.andDo {
|
||||
print()
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
= Spring JUnit Jupiter Testing Annotations
|
||||
|
||||
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
|
||||
(that is, the programming model in JUnit 5):
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-extension[`SpringExtension`]
|
||||
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-springjunitwebconfig[`@SpringJUnitWebConfig`]
|
||||
|
|
|
@ -1,9 +1,17 @@
|
|||
[[integration-testing-annotations-junit4]]
|
||||
= 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
|
||||
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]
|
||||
, 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-runner[SpringRunner],
|
||||
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-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`]
|
||||
|
@ -205,6 +213,3 @@ Kotlin::
|
|||
----
|
||||
<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,
|
||||
since annotations in JUnit 5 can also be used as meta-annotations. Consider the following
|
||||
example:
|
||||
since annotations in JUnit Jupiter can also be used as meta-annotations. Consider the
|
||||
following example:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
|
|
@ -47,6 +47,21 @@ the same bean in several test classes, make sure to name the fields consistently
|
|||
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.
|
||||
|
||||
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.
|
||||
====
|
||||
|
||||
[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]
|
||||
====
|
||||
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
|
||||
|
||||
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
|
||||
`MockMvcHtmlUnitDriverBuilder` as the following example shows:
|
||||
|
|
|
@ -1,10 +1,26 @@
|
|||
[[spring-mvc-test-client]]
|
||||
= Testing Client Applications
|
||||
|
||||
You can use client-side tests to test code that internally uses the `RestTemplate`. The
|
||||
idea is to declare expected requests and to provide "`stub`" responses so that you can
|
||||
focus on testing the code in isolation (that is, without running a server). The following
|
||||
example shows how to do so:
|
||||
To test code that uses the `RestClient` or `RestTemplate`, you can use a mock web server, such as
|
||||
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or
|
||||
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.
|
||||
|
||||
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]
|
||||
======
|
||||
|
|
|
@ -9,11 +9,11 @@ deal of importance on convention over configuration, with reasonable defaults th
|
|||
can override through annotation-based configuration.
|
||||
|
||||
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
|
||||
TestNG, Spring provides `abstract` support classes. Furthermore, Spring provides a custom
|
||||
JUnit `Runner` and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit
|
||||
Jupiter that let you write so-called POJO test classes. POJO test classes are not
|
||||
required to extend a particular class hierarchy, such as the `abstract` support classes.
|
||||
explicit support for JUnit Jupiter, JUnit 4, and TestNG. For JUnit 4 and TestNG, Spring
|
||||
provides `abstract` support classes. Furthermore, Spring provides a custom JUnit `Runner`
|
||||
and custom JUnit `Rules` for JUnit 4 and a custom `Extension` for JUnit Jupiter that let
|
||||
you write so-called POJO test classes. POJO test classes are not required to extend a
|
||||
particular class hierarchy, such as the `abstract` support classes.
|
||||
|
||||
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
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
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
|
||||
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
|
||||
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`)
|
||||
* `contextInitializerClasses` (from `@ContextConfiguration`)
|
||||
* `contextCustomizers` (from `ContextCustomizerFactory`) – this includes
|
||||
`@DynamicPropertySource` methods as well as various features from Spring Boot's
|
||||
testing support such as `@MockBean` and `@SpyBean`.
|
||||
`@DynamicPropertySource` methods, bean overrides (such as `@TestBean`, `@MockitoBean`,
|
||||
`@MockitoSpyBean` etc.), as well as various features from Spring Boot's testing support.
|
||||
* `contextLoader` (from `@ContextConfiguration`)
|
||||
* `parent` (from `@ContextHierarchy`)
|
||||
* `activeProfiles` (from `@ActiveProfiles`)
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[[testcontext-context-customizers]]
|
||||
= Configuration Configuration with Context Customizers
|
||||
= Context Configuration with Context Customizers
|
||||
|
||||
A `ContextCustomizer` is responsible for customizing the supplied
|
||||
`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
|
||||
have different levels in a context hierarchy configured using different resource types.
|
||||
|
||||
The remaining JUnit Jupiter based examples in this section show common configuration
|
||||
scenarios for integration tests that require the use of context hierarchies.
|
||||
[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.
|
||||
====
|
||||
|
||||
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**
|
||||
--
|
||||
|
@ -229,12 +240,118 @@ Kotlin::
|
|||
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({
|
||||
@Sql(scripts = "/test-schema.sql", config = @SqlConfig(commentPrefix = "`")),
|
||||
@Sql("/test-user-data.sql")
|
||||
)}
|
||||
})
|
||||
void userTest() {
|
||||
// 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 `@MockitoBean` or `@MockitoSpyBean` support.
|
||||
* Use Spring Boot's `@MockBean` or `@SpyBean` support.
|
||||
* Use JUnit 4's `@FixMethodOrder` support or any testing framework feature
|
||||
that is designed to ensure that test methods run in a particular order. Note,
|
||||
however, that this does not apply if entire test classes are run in parallel.
|
||||
* Use JUnit Jupiter's `@TestMethodOrder` support or any testing framework feature that is
|
||||
designed to ensure that test methods run in a particular order. Note, however, that
|
||||
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,
|
||||
filesystem, and others. This applies to both embedded and external systems.
|
||||
|
||||
|
|
|
@ -1,172 +1,15 @@
|
|||
[[testcontext-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]]
|
||||
== SpringExtension for JUnit Jupiter
|
||||
|
||||
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
|
||||
and integration tests and simultaneously reap the benefits of the TestContext framework,
|
||||
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:
|
||||
|
||||
* 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
|
||||
test execution] based on SpEL expressions, environment variables, system properties,
|
||||
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] 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
|
||||
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
|
||||
`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
|
||||
`@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the
|
||||
Since you can also use annotations in JUnit Jupiter as meta-annotations, Spring provides
|
||||
the `@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the
|
||||
configuration of the test `ApplicationContext` and JUnit Jupiter.
|
||||
|
||||
The following example uses `@SpringJUnitConfig` to reduce the amount of configuration
|
||||
|
@ -307,7 +153,8 @@ Kotlin::
|
|||
======
|
||||
|
||||
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]]
|
||||
=== 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.
|
||||
|
||||
Specifically, the `SpringExtension` can inject dependencies from the test's
|
||||
`ApplicationContext` into test constructors and methods that are annotated with
|
||||
Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`,
|
||||
`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`,
|
||||
and others.
|
||||
`ApplicationContext` into test constructors and methods that are annotated with Spring's
|
||||
`@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@AfterAll`,
|
||||
`@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and others.
|
||||
|
||||
|
||||
[[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`.
|
||||
* 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
|
||||
`@TestConstructor` and how to change the global _test constructor autowire mode_.
|
||||
See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]
|
||||
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
|
||||
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.
|
||||
|
||||
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
|
||||
`@Autowired` on the constructor in the previous example, resulting in the following.
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]),
|
||||
we can omit the declaration of `@Autowired` on the constructor in the previous example,
|
||||
resulting in the following.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
@ -553,17 +401,19 @@ honor `@NestedTestConfiguration` semantics.
|
|||
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
|
||||
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"]
|
||||
note for details.
|
||||
classpath. See the
|
||||
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
|
||||
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
|
||||
inherited. Each nested test class provides its own set of active profiles, resulting in a
|
||||
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/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] to see
|
||||
which annotations can be inherited in `@Nested` test classes.
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details).
|
||||
Consult the list of
|
||||
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]
|
||||
======
|
||||
|
@ -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]]
|
||||
== TestNG Support Classes
|
||||
== TestNG Support
|
||||
|
||||
The `org.springframework.test.context.testng` package provides the following support
|
||||
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
|
||||
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].
|
||||
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],
|
||||
`AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that
|
||||
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
|
||||
|
|
|
@ -265,6 +265,7 @@ Java::
|
|||
client = WebTestClient.bindToController(new TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
|
||||
.build();
|
||||
----
|
||||
|
||||
|
@ -275,6 +276,7 @@ Kotlin::
|
|||
client = WebTestClient.bindToController(TestController())
|
||||
.configureClient()
|
||||
.baseUrl("/test")
|
||||
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").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"]
|
||||
----
|
||||
Resource resource = ...
|
||||
Mono<String> result = webClient
|
||||
Resource resource = ...
|
||||
Mono<String> result = webClient
|
||||
.post()
|
||||
.uri("https://example.com")
|
||||
.body(Flux.concat(
|
||||
|
@ -322,8 +322,8 @@ Kotlin::
|
|||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
var resource: Resource = ...
|
||||
var result: Mono<String> = webClient
|
||||
var resource: Resource = ...
|
||||
var result: Mono<String> = webClient
|
||||
.post()
|
||||
.uri("https://example.com")
|
||||
.body(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[[webflux-client-builder]]
|
||||
= 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(String baseUrl)`
|
||||
|
@ -12,10 +12,12 @@ You can also use `WebClient.builder()` with further options:
|
|||
* `defaultUriVariables`: default values to use when expanding URI templates.
|
||||
* `defaultHeader`: Headers for every request.
|
||||
* `defaultCookie`: Cookies for every request.
|
||||
* `defaultApiVersion`: API version for every request.
|
||||
* `defaultRequest`: `Consumer` to customize every request.
|
||||
* `filter`: Client filter for every request.
|
||||
* `exchangeStrategies`: HTTP message reader/writer customizations.
|
||||
* `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].
|
||||
* `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"]
|
||||
----
|
||||
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
||||
|
@ -186,14 +186,14 @@ public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
|
|||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class MultipartExchangeFilterFunction : ExchangeFilterFunction {
|
||||
class MultipartExchangeFilterFunction : ExchangeFilterFunction {
|
||||
|
||||
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
|
||||
return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
|
||||
|
@ -217,6 +217,6 @@ class MultipartExchangeFilterFunction : ExchangeFilterFunction {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
|
@ -2,9 +2,16 @@
|
|||
= Testing
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
To test code that uses the `WebClient`, you can use a mock web server, such as the
|
||||
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer]. To see an example
|
||||
of its use, check out
|
||||
To test code that uses the `WebClient`, you can use a mock web server, such as
|
||||
https://github.com/square/okhttp#mockwebserver[OkHttp MockWebServer] or
|
||||
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`]
|
||||
in the Spring Framework test suite or the
|
||||
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]]
|
||||
== Blocking Execution
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ Java::
|
|||
|
||||
private String name;
|
||||
|
||||
private MultipartFile file;
|
||||
private FilePart file;
|
||||
|
||||
// ...
|
||||
|
||||
|
@ -42,7 +42,7 @@ Kotlin::
|
|||
----
|
||||
class MyForm(
|
||||
val name: String,
|
||||
val file: MultipartFile)
|
||||
val file: FilePart)
|
||||
|
||||
@Controller
|
||||
class FileUploadController {
|
||||
|
|
|
@ -34,11 +34,7 @@ Controllers can then return a `Flux<List<B>>`; Reactor provides a dedicated oper
|
|||
| `HttpHeaders`
|
||||
| For returning a response with headers and no body.
|
||||
|
||||
| `ErrorResponse`
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webflux/ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `ProblemDetail`
|
||||
| `ErrorResponse`, `ProblemDetail`
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
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
|
||||
through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. You can use this to, for example, parameterize a base URL based on
|
||||
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. You can use this, for example, to parameterize a base URL based on
|
||||
some external configuration.
|
||||
|
||||
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]]
|
||||
== HTTP HEAD, OPTIONS
|
||||
|
|
|
@ -81,7 +81,7 @@ The following table describes server dependencies (also see
|
|||
|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*
|
||||
[tabs]
|
||||
|
@ -176,17 +176,16 @@ Java::
|
|||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
HttpHandler handler = ...
|
||||
Servlet servlet = new JettyHttpHandlerAdapter(handler);
|
||||
JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler);
|
||||
|
||||
Server server = new Server();
|
||||
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
|
||||
contextHandler.addServlet(new ServletHolder(servlet), "/");
|
||||
contextHandler.start();
|
||||
server.setHandler(adapter);
|
||||
|
||||
ServerConnector connector = new ServerConnector(server);
|
||||
connector.setHost(host);
|
||||
connector.setPort(port);
|
||||
server.addConnector(connector);
|
||||
|
||||
server.start();
|
||||
----
|
||||
|
||||
|
@ -195,27 +194,27 @@ Kotlin::
|
|||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val handler: HttpHandler = ...
|
||||
val servlet = JettyHttpHandlerAdapter(handler)
|
||||
val adapter = JettyCoreHttpHandlerAdapter(handler)
|
||||
|
||||
val server = Server()
|
||||
val contextHandler = ServletContextHandler(server, "")
|
||||
contextHandler.addServlet(ServletHolder(servlet), "/")
|
||||
contextHandler.start();
|
||||
server.setHandler(adapter)
|
||||
|
||||
val connector = ServerConnector(server)
|
||||
connector.host = host
|
||||
connector.port = port
|
||||
server.addConnector(connector)
|
||||
|
||||
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
|
||||
{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`]
|
||||
in the WAR. That class wraps an `HttpHandler` with `ServletHttpHandlerAdapter` and registers
|
||||
that as a `Servlet`.
|
||||
To deploy as a WAR to a Servlet container instead, use
|
||||
{spring-framework-api}/web/server/adapter/AbstractReactiveWebInitializer.html[`AbstractReactiveWebInitializer`],
|
||||
to adapt `HttpHandler` to a `Servlet` via `ServletHttpHandlerAdapter`.
|
||||
|
||||
|
||||
|
||||
|
@ -800,4 +799,3 @@ Kotlin::
|
|||
.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
|
||||
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
|
||||
starter.
|
||||
|
||||
The following example shows a WebFlux Java configuration:
|
||||
The following example shows a WebMvc Java configuration:
|
||||
|
||||
[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
|
||||
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`]
|
||||
return values in controller methods provide basic support for a single asynchronous
|
||||
return value.
|
||||
* xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-deferredresult[`DeferredResult`],
|
||||
xref:web/webmvc/mvc-ann-async.adoc#mvc-ann-async-callable[`Callable`], and
|
||||
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
|
||||
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
|
||||
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]]
|
||||
== Processing
|
||||
|
||||
|
@ -281,7 +324,7 @@ invokes the configured exception resolvers and completes the request.
|
|||
=== SSE
|
||||
|
||||
`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
|
||||
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:
|
||||
|
||||
* 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`
|
||||
or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
|
||||
`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.
|
||||
|
||||
`@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]
|
||||
. `@RestControllerAdvice` is meta-annotated with `@ControllerAdvice`
|
||||
and `@ResponseBody`, and that means `@ExceptionHandler` methods will have their return
|
||||
value rendered via response body message conversion, rather than via HTML views.
|
||||
a Spring bean through xref:core/beans/java/instantiating-container.adoc#beans-java-instantiating-container-scan[component scanning].
|
||||
|
||||
`@RestControllerAdvice` is a shortcut annotation that combines `@ControllerAdvice`
|
||||
with `@ResponseBody`, in effect simply an `@ControllerAdvice` whose exception handler
|
||||
methods render to the response body.
|
||||
|
||||
On startup, `RequestMappingHandlerMapping` and `ExceptionHandlerExceptionResolver` detect
|
||||
controller advice beans and apply them at runtime. Global `@ExceptionHandler` methods,
|
||||
from an `@ControllerAdvice`, are applied _after_ local ones, from the `@Controller`.
|
||||
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
|
||||
and handlers that they apply to. For example:
|
||||
By default, both `@ControllerAdvice` and `@RestControllerAdvice` apply to any controller,
|
||||
including `@Controller` and `@RestController`. Use attributes of the annotation to narrow
|
||||
the set of controllers and handlers that they apply to. For example:
|
||||
|
||||
[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.
|
||||
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,
|
||||
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]
|
||||
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `String`
|
||||
| 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
|
||||
`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,
|
||||
then `HandlerMethodValidationException` is raised instead. For more details, see the section
|
||||
xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
|
||||
|
|
|
@ -22,11 +22,7 @@ supported for all return values.
|
|||
| `HttpHeaders`
|
||||
| For returning a response with headers and no body.
|
||||
|
||||
| `ErrorResponse`
|
||||
| To render an RFC 9457 error response with details in the body,
|
||||
see xref:web/webmvc/mvc-ann-rest-exceptions.adoc[Error Responses]
|
||||
|
||||
| `ProblemDetail`
|
||||
| `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]
|
||||
|
||||
|
|
|
@ -429,6 +429,86 @@ xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-co
|
|||
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]]
|
||||
== HTTP HEAD, OPTIONS
|
||||
[.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
|
||||
application.
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -38,7 +38,7 @@ to inform the server that the original port was `443`.
|
|||
==== X-Forwarded-Proto
|
||||
|
||||
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
|
||||
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`.
|
||||
|
|
|
@ -25,6 +25,7 @@ import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
|
|||
import org.springframework.test.web.servlet.assertj.MockMvcTester;
|
||||
import org.springframework.web.context.WebApplicationContext;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@SpringJUnitWebConfig(ApplicationWebConfiguration.class)
|
||||
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.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
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.json.MappingJackson2JsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
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.view.json.MappingJackson2JsonView;
|
||||
|
||||
@SuppressWarnings("removal")
|
||||
// tag::snippet[]
|
||||
@Configuration
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.springframework.docs.testing.mockmvc.assertj.mockmvctestersetup.converter
|
||||
|
||||
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
|
||||
|
||||
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
|
||||
|
||||
import org.springframework.context.annotation.Bean
|
||||
|
|
|
@ -14,6 +14,8 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigviewresolvers
|
||||
|
||||
import org.springframework.context.annotation.Configuration
|
||||
|
|
|
@ -7,20 +7,21 @@ javaPlatform {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.3"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.5"))
|
||||
api(platform("io.netty:netty-bom:4.1.119.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.0-M1"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.15.1"))
|
||||
api(platform("io.netty:netty-bom:4.2.2.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.0-M4"))
|
||||
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.assertj:assertj-bom:3.27.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.0.alpha1"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.alpha1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.0"))
|
||||
api(platform("org.junit:junit-bom:5.12.1"))
|
||||
api(platform("org.mockito:mockito-bom:5.16.1"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.0.beta0"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.0.beta0"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.8.1"))
|
||||
api(platform("org.junit:junit-bom:5.13.1"))
|
||||
api(platform("org.mockito:mockito-bom:5.18.0"))
|
||||
api(platform("tools.jackson:jackson-bom:3.0.0-rc5"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
|
@ -29,8 +30,8 @@ dependencies {
|
|||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
api("com.google.code.findbugs:jsr305:3.0.2")
|
||||
api("com.google.code.gson:gson:2.12.1")
|
||||
api("com.google.protobuf:protobuf-java-util:4.30.0")
|
||||
api("com.google.code.gson:gson:2.13.0")
|
||||
api("com.google.protobuf:protobuf-java-util:4.30.2")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
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.xstream:xstream:1.4.21")
|
||||
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("io.micrometer:context-propagation:1.1.1")
|
||||
api("io.mockk:mockk:1.13.4")
|
||||
api("io.projectreactor.tools:blockhound:1.0.8.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:derbyclient: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.core5:httpcore5-reactive:5.3.3")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.5")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
|
||||
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-websocket:11.0.1")
|
||||
api("org.apache.tomcat:tomcat-util:11.0.1")
|
||||
api("org.apache.tomcat:tomcat-websocket:11.0.1")
|
||||
api("org.aspectj:aspectjrt:1.9.23")
|
||||
api("org.aspectj:aspectjtools:1.9.23")
|
||||
api("org.aspectj:aspectjweaver:1.9.23")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.7")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.7")
|
||||
api("org.apache.tomcat:tomcat-util:11.0.7")
|
||||
api("org.apache.tomcat:tomcat-websocket:11.0.7")
|
||||
api("org.aspectj:aspectjrt:1.9.24")
|
||||
api("org.aspectj:aspectjtools:1.9.24")
|
||||
api("org.aspectj:aspectjweaver:1.9.24")
|
||||
api("org.awaitility:awaitility:4.3.0")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
|
@ -124,11 +124,12 @@ dependencies {
|
|||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.graalvm.sdk:graal-sdk:22.3.1")
|
||||
api("org.hamcrest:hamcrest:3.0")
|
||||
api("org.hibernate.orm:hibernate-core:7.0.0.Beta4")
|
||||
api("org.hibernate.validator:hibernate-validator:9.0.0.CR1")
|
||||
api("org.hibernate.orm:hibernate-core:7.0.0.Final")
|
||||
api("org.hibernate.validator:hibernate-validator:9.0.0.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.10.0")
|
||||
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.jspecify:jspecify:1.0.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
|
|
|
@ -4,7 +4,7 @@ org.gradle.caching=true
|
|||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.parallel=true
|
||||
|
||||
kotlinVersion=2.1.20
|
||||
kotlinVersion=2.2.0-RC2
|
||||
|
||||
kotlin.jvm.target.validation.mode=ignore
|
||||
kotlin.stdlib.default.dependency=false
|
||||
|
|
|
@ -73,7 +73,7 @@ eclipse.classpath.file.whenMerged {
|
|||
// within Eclipse. Consequently, Java 21 features managed via the
|
||||
// me.champeau.mrjar plugin cannot be built or tested within Eclipse.
|
||||
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
|
||||
|
|
|
@ -6,15 +6,13 @@ apply plugin: 'org.springframework.build.optional-dependencies'
|
|||
// apply plugin: 'io.github.goooler.shadow'
|
||||
apply plugin: 'me.champeau.jmh'
|
||||
apply from: "$rootDir/gradle/publications.gradle"
|
||||
apply plugin: 'net.ltgt.errorprone'
|
||||
apply plugin: "io.spring.nullability"
|
||||
|
||||
dependencies {
|
||||
jmh 'org.openjdk.jmh:jmh-core:1.37'
|
||||
jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37'
|
||||
jmh 'org.openjdk.jmh:jmh-generator-bytecode:1.37'
|
||||
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") {
|
||||
|
@ -69,20 +67,32 @@ normalization {
|
|||
|
||||
javadoc {
|
||||
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"
|
||||
options.memberLevel = JavadocMemberLevel.PROTECTED
|
||||
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'.
|
||||
// Attempt to suppress warnings due to cross-module @see and @link references.
|
||||
// Note that the global 'framework-api:javadoc' task displays all warnings.
|
||||
logging.captureStandardError LogLevel.INFO
|
||||
logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message
|
||||
}
|
||||
|
@ -114,15 +124,3 @@ publishing {
|
|||
components.java.withVariantsFromConfiguration(configurations.testFixturesApiElements) { 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
|
||||
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
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
|
@ -114,7 +114,7 @@ case "$( uname )" in #(
|
|||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
CLASSPATH="\\\"\\\""
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
|
@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
-classpath "$CLASSPATH" \
|
||||
org.gradle.wrapper.GradleWrapperMain \
|
||||
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||
"$@"
|
||||
|
||||
# Stop when "xargs" is not available.
|
||||
|
|
|
@ -70,11 +70,11 @@ goto fail
|
|||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
set CLASSPATH=
|
||||
|
||||
|
||||
@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
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
|
|
|
@ -158,8 +158,7 @@ class AdvisorAutoProxyCreatorIntegrationTests {
|
|||
try {
|
||||
rb.echoException(new ServletException());
|
||||
}
|
||||
catch (ServletException ex) {
|
||||
|
||||
catch (ServletException ignored) {
|
||||
}
|
||||
assertThat(txMan.commits).as("Transaction counts match").isEqualTo(1);
|
||||
}
|
||||
|
@ -272,7 +271,7 @@ class OrderedTxCheckAdvisor extends StaticMethodMatcherPointcutAdvisor implement
|
|||
TransactionInterceptor.currentTransactionStatus();
|
||||
throw new RuntimeException("Shouldn't have a transaction");
|
||||
}
|
||||
catch (NoTransactionException ex) {
|
||||
catch (NoTransactionException ignored) {
|
||||
// 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");
|
||||
* 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 {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation")
|
||||
@SuppressWarnings({"deprecation", "removal"})
|
||||
void test() {
|
||||
GenericApplicationContext ctx = new GenericApplicationContext();
|
||||
ctx.registerBeanDefinition("ppc",
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
plugins {
|
||||
id "io.spring.develocity.conventions" version "0.0.22"
|
||||
id "org.gradle.toolchains.foojay-resolver-convention" version "0.9.0"
|
||||
}
|
||||
|
||||
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");
|
||||
* 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 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.
|
||||
*/
|
||||
private PointcutExpression obtainPointcutExpression() {
|
||||
if (this.pointcutExpression == null) {
|
||||
this.pointcutClassLoader = determinePointcutClassLoader();
|
||||
this.pointcutExpression = buildPointcutExpression(this.pointcutClassLoader);
|
||||
PointcutExpression pointcutExpression = this.pointcutExpression;
|
||||
if (pointcutExpression == null) {
|
||||
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) {
|
||||
ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(this, targetMethod);
|
||||
ShadowMatchKey key = new ShadowMatchKey(this, targetMethod);
|
||||
ShadowMatch shadowMatch = ShadowMatchUtils.getShadowMatch(key);
|
||||
if (shadowMatch == null) {
|
||||
PointcutExpression pointcutExpression = obtainPointcutExpression();
|
||||
synchronized (pointcutExpression) {
|
||||
shadowMatch = ShadowMatchUtils.getShadowMatch(key);
|
||||
if (shadowMatch != null) {
|
||||
return shadowMatch;
|
||||
}
|
||||
PointcutExpression fallbackExpression = null;
|
||||
Method methodToMatch = targetMethod;
|
||||
try {
|
||||
try {
|
||||
shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch);
|
||||
shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch);
|
||||
}
|
||||
catch (ReflectionWorldException ex) {
|
||||
// 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.
|
||||
methodToMatch = originalMethod;
|
||||
try {
|
||||
shadowMatch = obtainPointcutExpression().matchesMethodExecution(methodToMatch);
|
||||
shadowMatch = pointcutExpression.matchesMethodExecution(methodToMatch);
|
||||
}
|
||||
catch (ReflectionWorldException ex) {
|
||||
// Could neither introspect the target class nor the proxy class ->
|
||||
|
@ -518,7 +528,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
|||
shadowMatch = new DefensiveShadowMatch(shadowMatch,
|
||||
fallbackExpression.matchesMethodExecution(methodToMatch));
|
||||
}
|
||||
shadowMatch = ShadowMatchUtils.setShadowMatch(this, targetMethod, shadowMatch);
|
||||
shadowMatch = ShadowMatchUtils.setShadowMatch(key, shadowMatch);
|
||||
}
|
||||
}
|
||||
return shadowMatch;
|
||||
}
|
||||
|
@ -616,14 +627,14 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
|
|||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Deprecated
|
||||
@Deprecated(since = "4.0") // deprecated by AspectJ
|
||||
public boolean couldMatchJoinPointsInType(Class someClass) {
|
||||
return (contextMatch(someClass) == FuzzyBoolean.YES);
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("rawtypes")
|
||||
@Deprecated
|
||||
@Deprecated(since = "4.0") // deprecated by AspectJ
|
||||
public boolean couldMatchJoinPointsInType(Class someClass, MatchingContext context) {
|
||||
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
|
||||
@Deprecated
|
||||
@Deprecated(since = "4.0") // deprecated by AspectJ
|
||||
public int getColumn() {
|
||||
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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,24 +16,46 @@
|
|||
|
||||
package org.springframework.aop.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.aspectj.weaver.tools.ShadowMatch;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.aop.support.ExpressionPointcut;
|
||||
|
||||
/**
|
||||
* Internal {@link ShadowMatch} utilities.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Juergen Hoeller
|
||||
* @since 6.2
|
||||
*/
|
||||
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.
|
||||
|
@ -42,33 +64,4 @@ public abstract class ShadowMatchUtils {
|
|||
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
|
||||
* {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances,
|
||||
* 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
|
||||
* @see AspectJExpressionPointcut#setBeanFactory
|
||||
* @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.TargetSource;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.aot.AotDetector;
|
||||
import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy;
|
||||
import org.springframework.cglib.core.CodeGenerationException;
|
||||
import org.springframework.cglib.core.GeneratorStrategy;
|
||||
|
@ -203,7 +204,7 @@ class CglibAopProxy implements AopProxy, Serializable {
|
|||
enhancer.setSuperclass(proxySuperClass);
|
||||
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
|
||||
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
|
||||
enhancer.setAttemptLoad(true);
|
||||
enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts());
|
||||
enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ?
|
||||
new ClassLoaderAwareGeneratorStrategy(classLoader) :
|
||||
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");
|
||||
* 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);
|
||||
for (String mappedName : this.beanNames) {
|
||||
if (isFactoryBean) {
|
||||
if (!mappedName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
|
||||
if (mappedName.isEmpty() || mappedName.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) {
|
||||
continue;
|
||||
}
|
||||
mappedName = mappedName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
|
||||
mappedName = mappedName.substring(1); // length of '&'
|
||||
}
|
||||
if (isMatch(beanName, mappedName)) {
|
||||
return true;
|
||||
|
|
|
@ -29,7 +29,6 @@ import org.mockito.Mockito;
|
|||
import org.mockito.stubbing.Answer;
|
||||
|
||||
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.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
@ -69,7 +68,12 @@ abstract class AbstractProxyExceptionHandlingTests {
|
|||
|
||||
|
||||
private void invokeProxy() {
|
||||
throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething());
|
||||
try {
|
||||
Objects.requireNonNull(proxy).doSomething();
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
throwableSeenByCaller = throwable;
|
||||
}
|
||||
}
|
||||
|
||||
@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");
|
||||
* 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;
|
||||
|
||||
/**
|
||||
* Abstract superclass for counting advices etc.
|
||||
* Abstract superclass for counting advice, etc.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @author Chris Beams
|
||||
|
@ -62,7 +62,7 @@ public class MethodCounter implements Serializable {
|
|||
*/
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (other != null && other.getClass() == this.getClass());
|
||||
return (other != null && getClass() == other.getClass());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -33,6 +33,7 @@ import java.util.Map;
|
|||
import java.util.Set;
|
||||
|
||||
import kotlin.jvm.JvmClassMappingKt;
|
||||
import kotlin.jvm.internal.DefaultConstructorMarker;
|
||||
import kotlin.reflect.KClass;
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.KParameter;
|
||||
|
@ -94,10 +95,9 @@ public abstract class BeanUtils {
|
|||
* @return the new instance
|
||||
* @throws BeanInstantiationException if the bean cannot be instantiated
|
||||
* @see Class#newInstance()
|
||||
* @deprecated as of Spring 5.0, following the deprecation of
|
||||
* {@link Class#newInstance()} in JDK 9
|
||||
* @deprecated following the deprecation of {@link Class#newInstance()} in JDK 9
|
||||
*/
|
||||
@Deprecated
|
||||
@Deprecated(since = "5.0")
|
||||
public static <T> T instantiate(Class<T> clazz) throws BeanInstantiationException {
|
||||
Assert.notNull(clazz, "Class must not be null");
|
||||
if (clazz.isInterface()) {
|
||||
|
@ -659,7 +659,9 @@ public abstract class BeanUtils {
|
|||
ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class);
|
||||
@Nullable String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(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);
|
||||
return paramNames;
|
||||
}
|
||||
|
@ -928,6 +930,11 @@ public abstract class BeanUtils {
|
|||
}
|
||||
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");
|
||||
* 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.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class MethodInvocationException extends PropertyAccessException {
|
||||
|
@ -41,7 +42,9 @@ public class MethodInvocationException extends PropertyAccessException {
|
|||
* @param cause the Throwable raised by the invoked method
|
||||
*/
|
||||
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
|
||||
|
|
|
@ -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");
|
||||
* 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
|
||||
* {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject}
|
||||
* will return the factory, not the instance returned by the factory.
|
||||
* @see #FACTORY_BEAN_PREFIX_CHAR
|
||||
*/
|
||||
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.
|
||||
|
|
|
@ -73,7 +73,7 @@ public abstract class BeanFactoryUtils {
|
|||
* @see BeanFactory#FACTORY_BEAN_PREFIX
|
||||
*/
|
||||
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) {
|
||||
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 transformedBeanNameCache.computeIfAbsent(name, beanName -> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
|
|
@ -55,9 +55,8 @@ public interface BeanRegistry {
|
|||
void registerAlias(String name, String alias);
|
||||
|
||||
/**
|
||||
* Register a bean from the given bean class, which will be instantiated
|
||||
* using the related {@link BeanUtils#getResolvableConstructor resolvable constructor}
|
||||
* if any.
|
||||
* Register a bean from the given bean class, which will be instantiated using the
|
||||
* related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any.
|
||||
* @param beanClass the class of the bean
|
||||
* @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
|
||||
* callback. The bean will be instantiated using the supplier that can be
|
||||
* configured in the customizer callback, or will be tentatively instantiated
|
||||
* with its {@link BeanUtils#getResolvableConstructor resolvable constructor}
|
||||
* otherwise.
|
||||
* callback. The bean will be instantiated using the supplier that can be configured
|
||||
* in the customizer callback, or will be tentatively instantiated with its
|
||||
* {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise.
|
||||
* @param beanClass the class of the bean
|
||||
* @param customizer callback to customize other bean properties than the name
|
||||
* @return the generated bean name
|
||||
|
@ -76,9 +74,8 @@ public interface BeanRegistry {
|
|||
<T> String registerBean(Class<T> beanClass, Consumer<Spec<T>> customizer);
|
||||
|
||||
/**
|
||||
* Register a bean from the given bean class, which will be instantiated
|
||||
* using the related {@link BeanUtils#getResolvableConstructor resolvable constructor}
|
||||
* if any.
|
||||
* Register a bean from the given bean class, which will be instantiated using the
|
||||
* related {@link BeanUtils#getResolvableConstructor resolvable constructor} if any.
|
||||
* @param name the name 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
|
||||
* callback. The bean will be instantiated using the supplier that can be
|
||||
* configured in the customizer callback, or will be tentatively instantiated with its
|
||||
* callback. The bean will be instantiated using the supplier that can be configured
|
||||
* in the customizer callback, or will be tentatively instantiated with its
|
||||
* {@link BeanUtils#getResolvableConstructor resolvable constructor} otherwise.
|
||||
* @param name the name of the bean
|
||||
* @param beanClass the class of the bean
|
||||
|
@ -122,8 +119,8 @@ public interface BeanRegistry {
|
|||
Spec<T> fallback();
|
||||
|
||||
/**
|
||||
* Hint that this bean has an infrastructure role, meaning it has no
|
||||
* relevance to the end-user.
|
||||
* Hint that this bean has an infrastructure role, meaning it has no relevance
|
||||
* to the end-user.
|
||||
* @see BeanDefinition#setRole(int)
|
||||
* @see BeanDefinition#ROLE_INFRASTRUCTURE
|
||||
*/
|
||||
|
@ -136,8 +133,7 @@ public interface BeanRegistry {
|
|||
Spec<T> lazyInit();
|
||||
|
||||
/**
|
||||
* Configure this bean as not a candidate for getting autowired into some
|
||||
* other bean.
|
||||
* Configure this bean as not a candidate for getting autowired into another bean.
|
||||
* @see BeanDefinition#setAutowireCandidate(boolean)
|
||||
*/
|
||||
Spec<T> notAutowirable();
|
||||
|
@ -184,6 +180,7 @@ public interface BeanRegistry {
|
|||
Spec<T> targetType(ResolvableType type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Context available from the bean instance supplier designed to give access
|
||||
* to bean dependencies.
|
||||
|
@ -191,10 +188,8 @@ public interface BeanRegistry {
|
|||
interface SupplierContext {
|
||||
|
||||
/**
|
||||
* Return the bean instance that uniquely matches the given object type,
|
||||
* if any.
|
||||
* @param requiredType type the bean must match; can be an interface or
|
||||
* superclass
|
||||
* Return the bean instance that uniquely matches the given object type, if any.
|
||||
* @param requiredType type the bean must match; can be an interface or superclass
|
||||
* @return an instance of the single bean matching the required type
|
||||
* @see BeanFactory#getBean(String)
|
||||
*/
|
||||
|
@ -240,4 +235,5 @@ public interface BeanRegistry {
|
|||
*/
|
||||
<T> ObjectProvider<T> beanProvider(ResolvableType requiredType);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -208,8 +208,8 @@ public abstract class BeanFactoryAnnotationUtils {
|
|||
}
|
||||
}
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException ex) {
|
||||
// Ignore - can't compare qualifiers for a manually registered singleton object
|
||||
catch (NoSuchBeanDefinitionException ignored) {
|
||||
// can't compare qualifiers for a manually registered singleton object
|
||||
}
|
||||
}
|
||||
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");
|
||||
* 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++) {
|
||||
code.add(i > startIndex ? ", " : "");
|
||||
if (!ambiguous) {
|
||||
code.add("$L.get($L)", variableName, i - startIndex);
|
||||
code.add("$L.get($L)", variableName, i);
|
||||
}
|
||||
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();
|
||||
|
|
|
@ -38,7 +38,6 @@ import org.jspecify.annotations.Nullable;
|
|||
import org.springframework.aot.generate.GeneratedMethods;
|
||||
import org.springframework.aot.generate.ValueCodeGenerator;
|
||||
import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
|
||||
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
|
||||
import org.springframework.aot.hint.ExecutableMode;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
|
@ -103,12 +102,12 @@ class BeanDefinitionPropertiesCodeGenerator {
|
|||
|
||||
this.hints = hints;
|
||||
this.attributeFilter = attributeFilter;
|
||||
List<Delegate> allDelegates = new ArrayList<>();
|
||||
allDelegates.add((valueCodeGenerator, value) -> customValueCodeGenerator.apply(PropertyNamesStack.peek(), value));
|
||||
allDelegates.addAll(additionalDelegates);
|
||||
allDelegates.addAll(BeanDefinitionPropertyValueCodeGeneratorDelegates.INSTANCES);
|
||||
allDelegates.addAll(ValueCodeGeneratorDelegates.INSTANCES);
|
||||
this.valueCodeGenerator = ValueCodeGenerator.with(allDelegates).scoped(generatedMethods);
|
||||
List<Delegate> customDelegates = new ArrayList<>();
|
||||
customDelegates.add((valueCodeGenerator, value) ->
|
||||
customValueCodeGenerator.apply(PropertyNamesStack.peek(), value));
|
||||
customDelegates.addAll(additionalDelegates);
|
||||
this.valueCodeGenerator = BeanDefinitionPropertyValueCodeGeneratorDelegates
|
||||
.createValueCodeGenerator(generatedMethods, customDelegates);
|
||||
}
|
||||
|
||||
@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");
|
||||
* you may not use this file except in compliance with the License.
|
||||
|
@ -16,6 +16,7 @@
|
|||
|
||||
package org.springframework.beans.factory.aot;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
@ -46,7 +47,7 @@ import org.springframework.javapoet.CodeBlock;
|
|||
* @author Stephane Nicoll
|
||||
* @since 6.1.2
|
||||
*/
|
||||
abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
|
||||
public abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
@ -155,6 +176,8 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
|
|||
.builder(SuppressWarnings.class)
|
||||
.addMember("value", "{\"rawtypes\", \"unchecked\"}")
|
||||
.build());
|
||||
method.addModifiers(javax.lang.model.element.Modifier.PRIVATE,
|
||||
javax.lang.model.element.Modifier.STATIC);
|
||||
method.returns(Map.class);
|
||||
method.addStatement("$T map = new $T($L)", Map.class,
|
||||
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");
|
||||
* 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
|
||||
* through introspection of the bean class.
|
||||
* @see #autowire
|
||||
* @deprecated as of Spring 3.0: If you are using mixed autowiring strategies,
|
||||
* prefer annotation-based autowiring for clearer demarcation of autowiring needs.
|
||||
* @deprecated If you are using mixed autowiring strategies, prefer
|
||||
* annotation-based autowiring for clearer demarcation of autowiring needs.
|
||||
*/
|
||||
@Deprecated
|
||||
@Deprecated(since = "3.0")
|
||||
int AUTOWIRE_AUTODETECT = 4;
|
||||
|
||||
/**
|
||||
|
@ -188,7 +188,7 @@ public interface AutowireCapableBeanFactory extends BeanFactory {
|
|||
* @see #AUTOWIRE_BY_NAME
|
||||
* @see #AUTOWIRE_BY_TYPE
|
||||
* @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")
|
||||
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.ObjectInputStream;
|
||||
import java.io.Serializable;
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import kotlin.reflect.KProperty;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.InjectionPoint;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
import org.springframework.core.KotlinDetector;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.core.Nullness;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
|
@ -162,28 +159,13 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable
|
|||
}
|
||||
|
||||
if (this.field != null) {
|
||||
return !(this.field.getType() == Optional.class || hasNullableAnnotation() ||
|
||||
(KotlinDetector.isKotlinType(this.field.getDeclaringClass()) && KotlinDelegate.isNullable(this.field)));
|
||||
return !(this.field.getType() == Optional.class || Nullness.forField(this.field) == Nullness.NULLABLE);
|
||||
}
|
||||
else {
|
||||
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
|
||||
* 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