Compare commits

..

27 Commits

Author SHA1 Message Date
Stéphane Nicoll 8f64480c9f Release v6.2.10 2025-08-14 09:15:43 +02:00
Stéphane Nicoll edda4731e1 Build against Java 24
Closes gh-35326
2025-08-14 09:06:18 +02:00
Stéphane Nicoll 9fa2d7d190 Upgrade to Jackson 2.18.4.1
Closes gh-35322
2025-08-14 08:38:58 +02:00
Stéphane Nicoll c30427fd4e Upgrade to Netty 4.1.124.Final
Closes gh-35321
2025-08-14 08:38:34 +02:00
Juergen Hoeller 1d908f1847 Upgrade to Reactor 2024.0.9 and Micrometer 1.14.10
Includes Groovy 4.0.28, JRuby 9.4.13, Jetty 12.0.25, Caffeine 3.2.2, Protobuf 4.31.1, Selenium 4.35, HtmlUnit 4.14

Closes gh-35312
Closes gh-35313
2025-08-13 00:04:31 +02:00
Sam Brannen 37b076be51 Support multiple result sets in ScriptUtils.executeSqlScript()
Prior to this commit, ScriptUtils.executeSqlScript() treated every
statement within the script as if it were a single insert/update/delete
statement. This disregarded the fact that the execution of a JDBC
Statement can result in multiple individual statements, some of which
result in a ResultSet and others that result in an update count.

For example, when executing a stored procedure on Sybase, ScriptUtils
did not execute all statements within the stored procedure.

To address that, this commit revises the implementation of
ScriptUtils.executeSqlScript() so that it handles multiple results and
differentiates between result sets and update counts.

Closes gh-35248
2025-08-11 17:44:39 +03:00
Juergen Hoeller a9453a5959 Polishing 2025-08-11 14:32:45 +02:00
Juergen Hoeller 3781ba223e Optimize NIO path resolution in PathEditor
Closes gh-35304
2025-08-11 14:32:39 +02:00
Sam Brannen f11a1e6f82 Polish tests 2025-08-11 11:27:10 +03:00
rstoyanchev ffc785471b Fix checkstyle error 2025-08-08 12:31:13 +01:00
rstoyanchev 6e2fbfe108 Polishing contribution
Closes gh-35232
2025-08-08 11:50:07 +01:00
秦利斌 968e037503 Add documentation of RequestMapping about SpEL
Signed-off-by: 秦利斌 <68638598+Allan-QLB@users.noreply.github.com>
2025-08-08 11:37:25 +01:00
rstoyanchev f0a9f649c1 Allow null in ProblemDetail#type
See gh-35294
2025-08-08 11:36:53 +01:00
rstoyanchev 600d6c6fc0 Update contribution
Closes gh-34721
2025-08-08 11:31:21 +01:00
giampaolo 7a55ce48a9 Handle CancellationException in JdkClientHttpRequest
Handle CancellationException in order to throw an HttpTimeoutException
when the timeout handler caused the cancellation.

See gh-34721

Signed-off-by: giampaolo <giampaorr@gmail.com>

fix: use timeoutHandler with a flag isTimeout

    Closes gh-33973

    Signed-off-by: giampaolo <giampaorr@gmail.com>
2025-08-08 11:31:21 +01:00
Juergen Hoeller 5df9fd4eff Polishing (aligned with main) 2025-08-06 19:01:18 +02:00
Juergen Hoeller da13a24604 Allow any @Transactional propagation for listener with BEFORE_COMMIT phase
Closes gh-35150
2025-08-01 21:15:56 +02:00
Juergen Hoeller 67e88f3c20 Align task execution tracking and thread interruption on shutdown
Closes gh-35254
2025-08-01 21:15:25 +02:00
Patrick Strawderman 24e66b63d1 Refine StringUtils#uriDecode and update documentation
Refine the StringUtils#uriDecode method in the following ways:
- Use a StringBuilder instead of ByteArrayOutputStream, and only decode
  %-encoded sequences.
- Use HexFormat.fromHexDigits to decode hex sequences.
- Decode to a byte array that is only allocated if encoded sequences are
  encountered.

This commit adds another optimization mainly for the use case where
there is no encoded sequence, and updates the Javadoc of both
StringUtils#uriDecode and UriUtils#decode to match the implementation.

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
Co-Authored-by: Sebastien Deleuze <sebastien.deleuze@broadcom.com>

Closes gh-35253
2025-07-29 21:44:26 +02:00
Juergen Hoeller f3832c7262 Add note on SQL types with SqlBinaryValue/SqlCharacterValue
Closes gh-34786
2025-07-28 22:06:38 +02:00
Juergen Hoeller 16e99f289c Accept support for generated keys column name array on HSQLDB/Derby
Closes gh-34790
2025-07-28 22:04:18 +02:00
Juergen Hoeller 642e554c52 Process PostgreSQL-returned catalog/schema names in given case
Closes gh-35064
2025-07-28 20:28:45 +02:00
Juergen Hoeller 4f6304707d Polishing 2025-07-25 22:40:15 +02:00
Juergen Hoeller 3c112703d9 Introduce useCaches flag on UrlResource (for URLConnection access)
Propagated from PathMatchingResourcePatternResolver's setUseCaches.

Closes gh-35218
2025-07-25 22:40:09 +02:00
Juergen Hoeller 8c44a61033 Invalidate cache entries for matching types after singleton creation
Closes gh-35239
2025-07-25 22:38:57 +02:00
Sébastien Deleuze 5e338ef1b8 Make MessageSource locale parameter nullable
Closes gh-35230
2025-07-23 16:02:38 +02:00
Sam Brannen 445da24631 Upgrade to JUnit 5.13.4
Closes gh-35229
2025-07-21 13:17:38 +03:00
4637 changed files with 59358 additions and 66908 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ inputs:
java-version:
description: 'Java version to compile and test with'
required: false
default: '24'
default: '17'
publish:
description: 'Whether to publish artifacts ready for deployment to Artifactory'
required: false
@@ -19,7 +19,7 @@ inputs:
java-version:
description: 'Java version to use for the build'
required: false
default: '24'
default: '17'
runs:
using: composite
steps:
-3
View File
@@ -1,3 +0,0 @@
require:
members: false
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
on:
push:
branches:
- main
- 6.2.x
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
@@ -27,7 +27,7 @@ jobs:
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
/**/framework-api-*-docs.zip::zip.type=docs
/**/framework-api-*-schema.zip::zip.type=schema
build-name: 'spring-framework-7.0.x'
build-name: 'spring-framework-6.2.x'
folder: 'deployment-repository'
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository: 'libs-snapshot-local'
+3 -2
View File
@@ -1,7 +1,8 @@
name: CI
on:
schedule:
- cron: '30 9 * * *'
push:
branches:
- 6.2.x
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
-95
View File
@@ -1,95 +0,0 @@
name: Release Milestone
on:
push:
tags:
- v7.0.0-M[1-9]
- v7.0.0-RC[1-9]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build-and-stage-release:
name: Build and Stage Release
if: ${{ github.repository == 'spring-projects/spring-framework' }}
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
with:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
/**/framework-api-*-docs.zip::zip.type=docs
/**/framework-api-*-schema.zip::zip.type=schema
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
folder: 'deployment-repository'
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository: 'libs-staging-local'
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
uri: 'https://repo.spring.io'
username: ${{ secrets.ARTIFACTORY_USERNAME }}
outputs:
version: ${{ steps.build-and-publish.outputs.version }}
verify:
name: Verify
needs: build-and-stage-release
uses: ./.github/workflows/verify.yml
secrets:
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
with:
staging: true
version: ${{ needs.build-and-stage-release.outputs.version }}
sync-to-maven-central:
name: Sync to Maven Central
needs:
- build-and-stage-release
- verify
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
central-token-password: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
central-token-username: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }}
spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }}
promote-release:
name: Promote Release
needs:
- build-and-stage-release
- sync-to-maven-central
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local
create-github-release:
name: Create GitHub Release
needs:
- build-and-stage-release
- promote-release
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
milestone: ${{ needs.build-and-stage-release.outputs.version }}
pre-release: true
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
+1 -1
View File
@@ -2,7 +2,7 @@ name: Release
on:
push:
tags:
- v7.0.[0-9]+
- v6.2.[0-9]+
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
-1
View File
@@ -54,4 +54,3 @@ atlassian-ide-plugin.xml
cached-antora-playbook.yml
node_modules
/.kotlin/
+1 -1
View File
@@ -1,3 +1,3 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=24.0.1-librca
java=17.0.13-librca
+28 -9
View File
@@ -1,12 +1,15 @@
plugins {
id 'io.freefair.aspectj' version '8.13.1' apply false
id 'io.freefair.aspectj' version '8.4' apply false
// kotlinVersion is managed in gradle.properties
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
id 'org.jetbrains.dokka'
id 'org.jetbrains.dokka' version '1.9.20'
id 'com.github.ben-manes.versions' version '0.51.0'
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
id 'de.undercouch.download' version '5.4.0'
id 'io.github.goooler.shadow' version '8.1.8' apply false
id 'me.champeau.jmh' version '0.7.2' apply false
id "io.spring.nullability" version "0.0.1" apply false
id 'me.champeau.mrjar' version '0.1.1'
id "net.ltgt.errorprone" version "4.1.0" apply false
}
ext {
@@ -21,6 +24,13 @@ configure(allprojects) { project ->
group = "org.springframework"
repositories {
mavenCentral()
maven {
url = "https://repo.spring.io/milestone"
content {
// Netty 5 optional support
includeGroup 'io.projectreactor.netty'
}
}
if (version.contains('-')) {
maven { url = "https://repo.spring.io/milestone" }
}
@@ -54,6 +64,7 @@ configure([rootProject] + javaProjects) { project ->
apply plugin: "java"
apply plugin: "java-test-fixtures"
apply plugin: 'org.springframework.build.conventions'
apply from: "${rootDir}/gradle/toolchains.gradle"
apply from: "${rootDir}/gradle/ide.gradle"
dependencies {
@@ -65,27 +76,35 @@ configure([rootProject] + javaProjects) { project ->
testImplementation("org.assertj:assertj-core")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("org.apache.logging.log4j:log4j-core")
testRuntimeOnly("org.apache.logging.log4j:log4j-jul")
testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl")
// JSR-305 only used for non-required meta-annotations
compileOnly("com.google.code.findbugs:jsr305")
testCompileOnly("com.google.code.findbugs:jsr305")
}
ext.javadocLinks = [
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jakarta.ee/specifications/platform/11/apidocs/",
"https://jakarta.ee/specifications/platform/9/apidocs/",
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.14/",
"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://docs.junit.org/5.13.3/api/",
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.13.4/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/",
// Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages,
// 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://www.javadoc.io/doc/tools.jackson.core/jackson-databind/3.0.0-rc4/"
//"https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/"
] as String[]
}
+1 -31
View File
@@ -9,18 +9,7 @@ The `org.springframework.build.conventions` plugin applies all conventions to th
* Configuring the Java compiler, see `JavaConventions`
* Configuring the Kotlin compiler, see `KotlinConventions`
* Configuring testing in the build with `TestConventions`
* Configuring the ArchUnit rules for the project, see `org.springframework.build.architecture.ArchitectureRules`
This plugin also provides a DSL extension to optionally enable Java preview features for
compiling and testing sources in a module. This can be applied with the following in a
module build file:
```groovy
springFramework {
enableJavaPreviewFeatures = true
}
```
* Configuring testing in the build with `TestConventions`
## Build Plugins
@@ -33,25 +22,6 @@ but doesn't affect the classpath of dependent projects.
This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly`
configurations are preferred.
### MultiRelease Jar
The `org.springframework.build.multiReleaseJar` plugin configures the project with MultiRelease JAR support.
It creates a new SourceSet and dedicated tasks for each Java variant considered.
This can be configured with the DSL, by setting a list of Java variants to configure:
```groovy
plugins {
id 'org.springframework.build.multiReleaseJar'
}
multiRelease {
releaseVersions 21, 24
}
```
Note, Java classes will be compiled with the toolchain pre-configured by the project, assuming that its
Java language version is equal or higher than all variants we consider. Each compilation task will only
set the "-release" compilation option accordingly to produce the expected bytecode version.
### RuntimeHints Java Agent
+2 -22
View File
@@ -20,24 +20,14 @@ ext {
dependencies {
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
implementation "org.jetbrains.dokka:dokka-gradle-plugin:2.0.0"
implementation "com.tngtech.archunit:archunit:1.4.0"
implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}"
implementation "org.gradle:test-retry-gradle-plugin:1.5.6"
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
testImplementation("org.assertj:assertj-core:${assertjVersion}")
testImplementation(platform("org.junit:junit-bom:${junitVersion}"))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
gradlePlugin {
plugins {
architecturePlugin {
id = "org.springframework.architecture"
implementationClass = "org.springframework.build.architecture.ArchitecturePlugin"
}
conventionsPlugin {
id = "org.springframework.build.conventions"
implementationClass = "org.springframework.build.ConventionsPlugin"
@@ -46,10 +36,6 @@ gradlePlugin {
id = "org.springframework.build.localdev"
implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin"
}
multiReleasePlugin {
id = "org.springframework.build.multiReleaseJar"
implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin"
}
optionalDependenciesPlugin {
id = "org.springframework.build.optional-dependencies"
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
@@ -60,9 +46,3 @@ gradlePlugin {
}
}
}
test {
useJUnitPlatform()
}
jar.dependsOn check
+7 -6
View File
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="com.puppycrawl.tools.checkstyle.Checker">
<!-- Root Checks -->
<module name="io.spring.javaformat.checkstyle.check.SpringHeaderCheck">
@@ -12,15 +12,16 @@
<module name="com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck"/>
<!-- TreeWalker Checks -->
<module name="TreeWalker">
<module name="com.puppycrawl.tools.checkstyle.TreeWalker">
<!-- Imports -->
<module name="AvoidStarImport"/>
<module name="UnusedImports"/>
<module name="RedundantImport"/>
<module name="com.puppycrawl.tools.checkstyle.checks.imports.UnusedImportsCheck">
<property name="processJavadoc" value="true"/>
</module>
<!-- Modifiers -->
<module name="com.puppycrawl.tools.checkstyle.checks.modifier.ModifierOrderCheck"/>
</module>
</module>
</module>
-2
View File
@@ -1,4 +1,2 @@
org.gradle.caching=true
assertjVersion=3.27.3
javaFormatVersion=0.0.43
junitVersion=5.12.2
@@ -63,8 +63,8 @@ public class CheckstyleConventions {
project.getPlugins().apply(NoHttpPlugin.class);
NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class);
noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines"));
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", "**/.classpath",
"**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**", "buildSrc/build/**");
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**",
"**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "buildSrc/build/**");
List<String> buildFolders = List.of("bin", "build", "out");
project.allprojects(subproject -> {
Path rootPath = project.getRootDir().toPath();
@@ -21,15 +21,12 @@ import org.gradle.api.Project;
import org.gradle.api.plugins.JavaBasePlugin;
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin;
import org.springframework.build.architecture.ArchitecturePlugin;
/**
* Plugin to apply conventions to projects that are part of Spring Framework's build.
* Conventions are applied in response to various plugins being applied.
*
* <p>When the {@link JavaBasePlugin} is applied, the conventions in {@link CheckstyleConventions},
* {@link TestConventions} and {@link JavaConventions} are applied.
* The {@link ArchitecturePlugin} plugin is also applied.
* When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions}
* are applied.
*
@@ -39,8 +36,6 @@ public class ConventionsPlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getExtensions().create("springFramework", SpringFrameworkExtension.class);
new ArchitecturePlugin().apply(project);
new CheckstyleConventions().apply(project);
new JavaConventions().apply(project);
new KotlinConventions().apply(project);
@@ -17,6 +17,7 @@
package org.springframework.build;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.gradle.api.Plugin;
@@ -26,6 +27,7 @@ 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.
@@ -40,21 +42,8 @@ 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.
* <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).
*/
private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17);
static {
List<String> commonCompilerArgs = List.of(
List<String> commonCompilerArgs = Arrays.asList(
"-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann",
"-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides",
"-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options",
@@ -62,74 +51,43 @@ public class JavaConventions {
);
COMPILER_ARGS = new ArrayList<>();
COMPILER_ARGS.addAll(commonCompilerArgs);
COMPILER_ARGS.addAll(List.of(
COMPILER_ARGS.addAll(Arrays.asList(
"-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation",
"-Xlint:unchecked", "-Werror"
));
TEST_COMPILER_ARGS = new ArrayList<>();
TEST_COMPILER_ARGS.addAll(commonCompilerArgs);
TEST_COMPILER_ARGS.addAll(List.of("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes",
"-Xlint:-deprecation", "-Xlint:-unchecked"));
}
public void apply(Project project) {
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> {
applyToolchainConventions(project);
applyJavaCompileConventions(project);
});
project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project));
}
/**
* Configure the Toolchain support for the project.
* @param project the current project
*/
private static void applyToolchainConventions(Project project) {
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION);
});
}
/**
* Apply the common Java compiler options for main sources, test fixture sources, and
* Applies the common Java compiler options for main sources, test fixture sources, and
* test sources.
* @param project the current project
*/
private void applyJavaCompileConventions(Project project) {
project.afterEvaluate(p -> {
p.getTasks().withType(JavaCompile.class)
.matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_JAVA_TASK_NAME))
.forEach(compileTask -> {
compileTask.getOptions().setCompilerArgs(COMPILER_ARGS);
compileTask.getOptions().setEncoding("UTF-8");
setJavaRelease(compileTask);
});
p.getTasks().withType(JavaCompile.class)
.matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME)
|| compileTask.getName().equals("compileTestFixturesJava"))
.forEach(compileTask -> {
compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS);
compileTask.getOptions().setEncoding("UTF-8");
setJavaRelease(compileTask);
});
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
toolchain.getLanguageVersion().set(JavaLanguageVersion.of(17));
});
}
/**
* We should pick the {@link #DEFAULT_RELEASE_VERSION} for all compiled classes,
* unless the current task is compiling multi-release JAR code with a higher version.
*/
private void setJavaRelease(JavaCompile task) {
int defaultVersion = DEFAULT_RELEASE_VERSION.asInt();
int releaseVersion = defaultVersion;
int compilerVersion = task.getJavaCompiler().get().getMetadata().getLanguageVersion().asInt();
for (int version = defaultVersion ; version <= compilerVersion ; version++) {
if (task.getName().contains("Java" + version)) {
releaseVersion = version;
break;
}
}
task.getOptions().getRelease().set(releaseVersion);
project.getTasks().withType(JavaCompile.class)
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME))
.forEach(compileTask -> {
compileTask.getOptions().setCompilerArgs(COMPILER_ARGS);
compileTask.getOptions().setEncoding("UTF-8");
});
project.getTasks().withType(JavaCompile.class)
.matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME)
|| compileTask.getName().equals("compileTestFixturesJava"))
.forEach(compileTask -> {
compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS);
compileTask.getOptions().setEncoding("UTF-8");
});
}
}
@@ -16,78 +16,33 @@
package org.springframework.build;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.Project;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.jetbrains.dokka.gradle.DokkaExtension;
import org.jetbrains.dokka.gradle.DokkaPlugin;
import org.jetbrains.kotlin.gradle.dsl.JvmTarget;
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion;
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions;
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile;
/**
* @author Brian Clozel
* @author Sebastien Deleuze
*/
public class KotlinConventions {
void apply(Project project) {
project.getPlugins().withId("org.jetbrains.kotlin.jvm", _ -> {
project.getTasks().withType(KotlinCompile.class, this::configure);
if (project.getLayout().getProjectDirectory().dir("src/main/kotlin").getAsFile().exists()) {
project.getPlugins().apply(DokkaPlugin.class);
project.getExtensions().configure(DokkaExtension.class, dokka -> configure(project, dokka));
project.project(":framework-api").getDependencies().add("dokka", project);
}
});
project.getPlugins().withId("org.jetbrains.kotlin.jvm",
(plugin) -> project.getTasks().withType(KotlinCompile.class, this::configure));
}
private void configure(KotlinCompile compile) {
compile.compilerOptions(options -> {
options.getApiVersion().set(KotlinVersion.KOTLIN_2_2);
options.getLanguageVersion().set(KotlinVersion.KOTLIN_2_2);
options.getJvmTarget().set(JvmTarget.JVM_17);
options.getJavaParameters().set(true);
options.getAllWarningsAsErrors().set(true);
options.getFreeCompilerArgs().addAll(
"-Xsuppress-version-warnings",
"-Xjsr305=strict", // For dependencies using JSR 305
"-opt-in=kotlin.RequiresOptIn",
"-Xjdk-release=17", // Needed due to https://youtrack.jetbrains.com/issue/KT-49746
"-Xannotation-default-target=param-property" // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255
);
});
}
private void configure(Project project, DokkaExtension dokka) {
dokka.getDokkaSourceSets().forEach(sourceSet -> {
sourceSet.getSourceRoots().setFrom(project.file("src/main/kotlin"));
sourceSet.getClasspath()
.from(project.getExtensions()
.getByType(SourceSetContainer.class)
.getByName(SourceSet.MAIN_SOURCE_SET_NAME)
.getOutput());
var externalDocumentationLinks = sourceSet.getExternalDocumentationLinks();
var springVersion = project.getVersion();
externalDocumentationLinks.register("spring-framework", spec -> {
spec.url("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/");
spec.packageListUrl("https://docs.spring.io/spring-framework/docs/" + springVersion + "/javadoc-api/element-list");
});
externalDocumentationLinks.register("reactor-core", spec ->
spec.url("https://projectreactor.io/docs/core/release/api/"));
externalDocumentationLinks.register("reactive-streams", spec ->
spec.url("https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/"));
externalDocumentationLinks.register("kotlinx-coroutines", spec ->
spec.url("https://kotlinlang.org/api/kotlinx.coroutines/"));
externalDocumentationLinks.register("hamcrest", spec ->
spec.url("https://javadoc.io/doc/org.hamcrest/hamcrest/2.1/"));
externalDocumentationLinks.register("jakarta-servlet", spec -> {
spec.url("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/");
spec.packageListUrl("https://javadoc.io/doc/jakarta.servlet/jakarta.servlet-api/latest/element-list");
});
externalDocumentationLinks.register("rsocket-core", spec ->
spec.url("https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/"));
});
KotlinJvmOptions kotlinOptions = compile.getKotlinOptions();
kotlinOptions.setApiVersion("1.7");
kotlinOptions.setLanguageVersion("1.7");
kotlinOptions.setJvmTarget("17");
kotlinOptions.setJavaParameters(true);
kotlinOptions.setAllWarningsAsErrors(true);
List<String> freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs());
freeCompilerArgs.addAll(List.of("-Xsuppress-version-warnings", "-Xjsr305=strict", "-opt-in=kotlin.RequiresOptIn"));
compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs);
}
}
@@ -1,53 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build;
import java.util.Collections;
import java.util.List;
import org.gradle.api.Project;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.api.tasks.testing.Test;
import org.gradle.process.CommandLineArgumentProvider;
public class SpringFrameworkExtension {
private final Property<Boolean> enableJavaPreviewFeatures;
public SpringFrameworkExtension(Project project) {
this.enableJavaPreviewFeatures = project.getObjects().property(Boolean.class);
project.getTasks().withType(JavaCompile.class).configureEach(javaCompile ->
javaCompile.getOptions().getCompilerArgumentProviders().add(asArgumentProvider()));
project.getTasks().withType(Test.class).configureEach(test ->
test.getJvmArgumentProviders().add(asArgumentProvider()));
}
public Property<Boolean> getEnableJavaPreviewFeatures() {
return this.enableJavaPreviewFeatures;
}
private CommandLineArgumentProvider asArgumentProvider() {
return () -> {
if (getEnableJavaPreviewFeatures().getOrElse(false)) {
return List.of("--enable-preview");
}
return Collections.emptyList();
};
}
}
@@ -16,9 +16,9 @@
package org.springframework.build;
import java.util.Map;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.Dependency;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.testing.Test;
import org.gradle.api.tasks.testing.TestFrameworkOptions;
@@ -26,16 +26,12 @@ import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions;
import org.gradle.testretry.TestRetryPlugin;
import org.gradle.testretry.TestRetryTaskExtension;
import java.util.Map;
/**
* Conventions that are applied in the presence of the {@link JavaBasePlugin}. When the
* plugin is applied:
* <ul>
* <li>The {@link TestRetryPlugin Test Retry} plugin is applied so that flaky tests
* are retried 3 times when running on the CI server.
* <li>Common test properties are configured
* <li>The ByteBuddy Java agent is configured on test tasks.
* </ul>
*
* @author Brian Clozel
@@ -49,7 +45,6 @@ class TestConventions {
}
private void configureTestConventions(Project project) {
configureByteBuddyAgent(project);
project.getTasks().withType(Test.class,
test -> {
configureTests(project, test);
@@ -68,7 +63,9 @@ class TestConventions {
test.setSystemProperties(Map.of(
"java.awt.headless", "true",
"io.netty.leakDetection.level", "paranoid",
"junit.platform.discovery.issue.severity.critical", "INFO"
"io.netty5.leakDetectionLevel", "paranoid",
"io.netty5.leakDetection.targetRecords", "32",
"io.netty5.buffer.lifecycleTracingEnabled", "true"
));
if (project.hasProperty("testGroups")) {
test.systemProperty("testGroups", project.getProperties().get("testGroups"));
@@ -80,20 +77,6 @@ class TestConventions {
);
}
private void configureByteBuddyAgent(Project project) {
if (project.hasProperty("byteBuddyVersion")) {
String byteBuddyVersion = (String) project.getProperties().get("byteBuddyVersion");
Configuration byteBuddyAgentConfig = project.getConfigurations().create("byteBuddyAgentConfig");
byteBuddyAgentConfig.setTransitive(false);
Dependency byteBuddyAgent = project.getDependencies().create("net.bytebuddy:byte-buddy-agent:" + byteBuddyVersion);
byteBuddyAgentConfig.getDependencies().add(byteBuddyAgent);
project.afterEvaluate(p -> {
p.getTasks().withType(Test.class, test -> test
.jvmArgs("-javaagent:" + byteBuddyAgentConfig.getAsPath()));
});
}
}
private void configureTestRetryPlugin(Project project, Test test) {
project.getPlugins().withType(TestRetryPlugin.class, testRetryPlugin -> {
TestRetryTaskExtension testRetry = test.getExtensions().getByType(TestRetryTaskExtension.class);
@@ -1,137 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.architecture;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.EvaluationResult;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.util.List;
import org.gradle.api.DefaultTask;
import org.gradle.api.GradleException;
import org.gradle.api.Task;
import org.gradle.api.file.DirectoryProperty;
import org.gradle.api.file.FileCollection;
import org.gradle.api.file.FileTree;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.tasks.IgnoreEmptyDirectories;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.PathSensitive;
import org.gradle.api.tasks.PathSensitivity;
import org.gradle.api.tasks.SkipWhenEmpty;
import org.gradle.api.tasks.TaskAction;
import static org.springframework.build.architecture.ArchitectureRules.allPackagesShouldBeFreeOfTangles;
import static org.springframework.build.architecture.ArchitectureRules.classesShouldNotImportForbiddenTypes;
import static org.springframework.build.architecture.ArchitectureRules.javaClassesShouldNotImportKotlinAnnotations;
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToLowerCaseWithoutLocale;
import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToUpperCaseWithoutLocale;
import static org.springframework.build.architecture.ArchitectureRules.packageInfoShouldBeNullMarked;
/**
* {@link Task} that checks for architecture problems.
*
* @author Andy Wilkinson
* @author Scott Frederick
*/
public abstract class ArchitectureCheck extends DefaultTask {
private FileCollection classes;
public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getProhibitObjectsRequireNonNull().convention(true);
getRules().addAll(packageInfoShouldBeNullMarked(),
classesShouldNotImportForbiddenTypes(),
javaClassesShouldNotImportKotlinAnnotations(),
allPackagesShouldBeFreeOfTangles(),
noClassesShouldCallStringToLowerCaseWithoutLocale(),
noClassesShouldCallStringToUpperCaseWithoutLocale());
getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList()));
}
@TaskAction
void checkArchitecture() throws IOException {
JavaClasses javaClasses = new ClassFileImporter()
.importPaths(this.classes.getFiles().stream().map(File::toPath).toList());
List<EvaluationResult> violations = getRules().get()
.stream()
.map((rule) -> rule.evaluate(javaClasses))
.filter(EvaluationResult::hasViolation)
.toList();
File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile();
outputFile.getParentFile().mkdirs();
if (!violations.isEmpty()) {
StringBuilder report = new StringBuilder();
for (EvaluationResult violation : violations) {
report.append(violation.getFailureReport());
report.append(String.format("%n"));
}
Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING);
throw new GradleException("Architecture check failed. See '" + outputFile + "' for details.");
}
else {
outputFile.createNewFile();
}
}
public void setClasses(FileCollection classes) {
this.classes = classes;
}
@Internal
public FileCollection getClasses() {
return this.classes;
}
@InputFiles
@SkipWhenEmpty
@IgnoreEmptyDirectories
@PathSensitive(PathSensitivity.RELATIVE)
final FileTree getInputClasses() {
return this.classes.getAsFileTree();
}
@Optional
@InputFiles
@PathSensitive(PathSensitivity.RELATIVE)
public abstract DirectoryProperty getResourcesDirectory();
@OutputDirectory
public abstract DirectoryProperty getOutputDirectory();
@Internal
public abstract ListProperty<ArchRule> getRules();
@Internal
public abstract Property<Boolean> getProhibitObjectsRequireNonNull();
@Input
// The rules themselves can't be an input as they aren't serializable so we use
// their descriptions instead
abstract ListProperty<String> getRuleDescriptions();
}
@@ -1,74 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.architecture;
import java.util.ArrayList;
import java.util.List;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.Task;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
/**
* {@link Plugin} for verifying a project's architecture.
*
* @author Andy Wilkinson
*/
public class ArchitecturePlugin implements Plugin<Project> {
@Override
public void apply(Project project) {
project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerTasks(project));
}
private void registerTasks(Project project) {
JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class);
List<TaskProvider<ArchitectureCheck>> architectureChecks = new ArrayList<>();
for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) {
if (sourceSet.getName().contains("test")) {
// skip test source sets.
continue;
}
TaskProvider<ArchitectureCheck> checkArchitecture = project.getTasks()
.register(taskName(sourceSet), ArchitectureCheck.class,
(task) -> {
task.setClasses(sourceSet.getOutput().getClassesDirs());
task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir());
task.dependsOn(sourceSet.getProcessResourcesTaskName());
task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName()
+ " source set.");
task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
});
architectureChecks.add(checkArchitecture);
}
if (!architectureChecks.isEmpty()) {
TaskProvider<Task> checkTask = project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME);
checkTask.configure((check) -> check.dependsOn(architectureChecks));
}
}
private static String taskName(SourceSet sourceSet) {
return "checkArchitecture"
+ sourceSet.getName().substring(0, 1).toUpperCase()
+ sourceSet.getName().substring(1);
}
}
@@ -1,107 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.architecture;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
import com.tngtech.archunit.library.dependencies.SliceAssignment;
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
import java.util.List;
abstract class ArchitectureRules {
static ArchRule allPackagesShouldBeFreeOfTangles() {
return SlicesRuleDefinition.slices()
.assignedFrom(new SpringSlices()).should().beFreeOfCycles();
}
static ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() {
return ArchRuleDefinition.noClasses()
.should()
.callMethod(String.class, "toLowerCase")
.because("String.toLowerCase(Locale.ROOT) should be used instead");
}
static ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() {
return ArchRuleDefinition.noClasses()
.should()
.callMethod(String.class, "toUpperCase")
.because("String.toUpperCase(Locale.ROOT) should be used instead");
}
static ArchRule packageInfoShouldBeNullMarked() {
return ArchRuleDefinition.classes()
.that().haveSimpleName("package-info")
.should().beAnnotatedWith("org.jspecify.annotations.NullMarked")
.allowEmptyShould(true);
}
static ArchRule classesShouldNotImportForbiddenTypes() {
return ArchRuleDefinition.noClasses()
.should().dependOnClassesThat()
.haveFullyQualifiedName("reactor.core.support.Assert")
.orShould().dependOnClassesThat()
.haveFullyQualifiedName("org.slf4j.LoggerFactory")
.orShould().dependOnClassesThat()
.haveFullyQualifiedName("org.springframework.lang.NonNull")
.orShould().dependOnClassesThat()
.haveFullyQualifiedName("org.springframework.lang.Nullable");
}
static ArchRule javaClassesShouldNotImportKotlinAnnotations() {
return ArchRuleDefinition.noClasses()
.that(new DescribedPredicate<JavaClass>("is not a Kotlin class") {
@Override
public boolean test(JavaClass javaClass) {
return javaClass.getSourceCodeLocation()
.getSourceFileName().endsWith(".java");
}
}
)
.should().dependOnClassesThat()
.resideInAnyPackage("org.jetbrains.annotations..")
.allowEmptyShould(true);
}
static class SpringSlices implements SliceAssignment {
private final List<String> ignoredPackages = List.of("org.springframework.asm",
"org.springframework.cglib",
"org.springframework.javapoet",
"org.springframework.objenesis");
@Override
public SliceIdentifier getIdentifierOf(JavaClass javaClass) {
String packageName = javaClass.getPackageName();
for (String ignoredPackage : ignoredPackages) {
if (packageName.startsWith(ignoredPackage)) {
return SliceIdentifier.ignore();
}
}
return SliceIdentifier.of("spring framework");
}
@Override
public String getDescription() {
return "Spring Framework Slices";
}
}
}
@@ -1,139 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.multirelease;
import javax.inject.Inject;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.attributes.LibraryElements;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.java.archives.Attributes;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.SourceSetContainer;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.api.tasks.TaskProvider;
import org.gradle.api.tasks.bundling.Jar;
import org.gradle.api.tasks.compile.JavaCompile;
import org.gradle.api.tasks.testing.Test;
import org.gradle.language.base.plugins.LifecycleBasePlugin;
/**
* @author Cedric Champeau
* @author Brian Clozel
*/
public abstract class MultiReleaseExtension {
private final TaskContainer tasks;
private final SourceSetContainer sourceSets;
private final DependencyHandler dependencies;
private final ObjectFactory objects;
private final ConfigurationContainer configurations;
@Inject
public MultiReleaseExtension(SourceSetContainer sourceSets,
ConfigurationContainer configurations,
TaskContainer tasks,
DependencyHandler dependencies,
ObjectFactory objectFactory) {
this.sourceSets = sourceSets;
this.configurations = configurations;
this.tasks = tasks;
this.dependencies = dependencies;
this.objects = objectFactory;
}
public void releaseVersions(int... javaVersions) {
releaseVersions("src/main/", "src/test/", javaVersions);
}
private void releaseVersions(String mainSourceDirectory, String testSourceDirectory, int... javaVersions) {
for (int javaVersion : javaVersions) {
addLanguageVersion(javaVersion, mainSourceDirectory, testSourceDirectory);
}
}
private void addLanguageVersion(int javaVersion, String mainSourceDirectory, String testSourceDirectory) {
String javaN = "java" + javaVersion;
SourceSet langSourceSet = sourceSets.create(javaN, srcSet -> srcSet.getJava().srcDir(mainSourceDirectory + javaN));
SourceSet testSourceSet = sourceSets.create(javaN + "Test", srcSet -> srcSet.getJava().srcDir(testSourceDirectory + javaN));
SourceSet sharedSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME);
SourceSet sharedTestSourceSet = sourceSets.findByName(SourceSet.TEST_SOURCE_SET_NAME);
FileCollection mainClasses = objects.fileCollection().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs());
dependencies.add(javaN + "Implementation", mainClasses);
tasks.named(langSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
task.getOptions().getRelease().set(javaVersion)
);
tasks.named(testSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
task.getOptions().getRelease().set(javaVersion)
);
TaskProvider<Test> testTask = createTestTask(javaVersion, testSourceSet, sharedTestSourceSet, langSourceSet, sharedSourceSet);
tasks.named("check", task -> task.dependsOn(testTask));
configureMultiReleaseJar(javaVersion, langSourceSet);
}
private TaskProvider<Test> createTestTask(int javaVersion, SourceSet testSourceSet, SourceSet sharedTestSourceSet, SourceSet langSourceSet, SourceSet sharedSourceSet) {
Configuration testImplementation = configurations.getByName(testSourceSet.getImplementationConfigurationName());
testImplementation.extendsFrom(configurations.getByName(sharedTestSourceSet.getImplementationConfigurationName()));
Configuration testCompileOnly = configurations.getByName(testSourceSet.getCompileOnlyConfigurationName());
testCompileOnly.extendsFrom(configurations.getByName(sharedTestSourceSet.getCompileOnlyConfigurationName()));
testCompileOnly.getDependencies().add(dependencies.create(langSourceSet.getOutput().getClassesDirs()));
testCompileOnly.getDependencies().add(dependencies.create(sharedSourceSet.getOutput().getClassesDirs()));
Configuration testRuntimeClasspath = configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName());
// so here's the deal. MRjars are JARs! Which means that to execute tests, we need
// the JAR on classpath, not just classes + resources as Gradle usually does
testRuntimeClasspath.getAttributes()
.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR));
TaskProvider<Test> testTask = tasks.register("java" + javaVersion + "Test", Test.class, test -> {
test.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
ConfigurableFileCollection testClassesDirs = objects.fileCollection();
testClassesDirs.from(testSourceSet.getOutput());
testClassesDirs.from(sharedTestSourceSet.getOutput());
test.setTestClassesDirs(testClassesDirs);
ConfigurableFileCollection classpath = objects.fileCollection();
// must put the MRJar first on classpath
classpath.from(tasks.named("jar"));
// then we put the specific test sourceset tests, so that we can override
// the shared versions
classpath.from(testSourceSet.getOutput());
// then we add the shared tests
classpath.from(sharedTestSourceSet.getRuntimeClasspath());
test.setClasspath(classpath);
});
return testTask;
}
private void configureMultiReleaseJar(int version, SourceSet languageSourceSet) {
tasks.named("jar", Jar.class, jar -> {
jar.into("META-INF/versions/" + version, s -> s.from(languageSourceSet.getOutput()));
Attributes attributes = jar.getManifest().getAttributes();
attributes.put("Multi-Release", "true");
});
}
}
@@ -1,61 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.multirelease;
import javax.inject.Inject;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.ConfigurationContainer;
import org.gradle.api.artifacts.dsl.DependencyHandler;
import org.gradle.api.model.ObjectFactory;
import org.gradle.api.plugins.ExtensionContainer;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginExtension;
import org.gradle.api.tasks.TaskContainer;
import org.gradle.jvm.toolchain.JavaToolchainService;
/**
* A plugin which adds support for building multi-release jars
* with Gradle.
* @author Cedric Champeau
* @author Brian Clozel
* @see <a href="https://github.com/melix/mrjar-gradle-plugin">original project</a>
*/
public class MultiReleaseJarPlugin implements Plugin<Project> {
@Inject
protected JavaToolchainService getToolchains() {
throw new UnsupportedOperationException();
}
public void apply(Project project) {
project.getPlugins().apply(JavaPlugin.class);
ExtensionContainer extensions = project.getExtensions();
JavaPluginExtension javaPluginExtension = extensions.getByType(JavaPluginExtension.class);
ConfigurationContainer configurations = project.getConfigurations();
TaskContainer tasks = project.getTasks();
DependencyHandler dependencies = project.getDependencies();
ObjectFactory objects = project.getObjects();
extensions.create("multiRelease", MultiReleaseExtension.class,
javaPluginExtension.getSourceSets(),
configurations,
tasks,
dependencies,
objects);
}
}
@@ -1,137 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.build.multirelease;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link MultiReleaseJarPlugin}
*/
public class MultiReleaseJarPluginTests {
private File projectDir;
private File buildFile;
@BeforeEach
void setup(@TempDir File projectDir) {
this.projectDir = projectDir;
this.buildFile = new File(this.projectDir, "build.gradle");
}
@Test
void configureSourceSets() throws IOException {
writeBuildFile("""
plugins {
id 'java'
id 'org.springframework.build.multiReleaseJar'
}
multiRelease { releaseVersions 21, 24 }
task printSourceSets {
doLast {
sourceSets.all { println it.name }
}
}
""");
BuildResult buildResult = runGradle("printSourceSets");
assertThat(buildResult.getOutput()).contains("main", "test", "java21", "java21Test", "java24", "java24Test");
}
@Test
void configureToolchainReleaseVersion() throws IOException {
writeBuildFile("""
plugins {
id 'java'
id 'org.springframework.build.multiReleaseJar'
}
multiRelease { releaseVersions 21 }
task printReleaseVersion {
doLast {
tasks.all { println it.name }
tasks.named("compileJava21Java") {
println "compileJava21Java releaseVersion: ${it.options.release.get()}"
}
tasks.named("compileJava21TestJava") {
println "compileJava21TestJava releaseVersion: ${it.options.release.get()}"
}
}
}
""");
BuildResult buildResult = runGradle("printReleaseVersion");
assertThat(buildResult.getOutput()).contains("compileJava21Java releaseVersion: 21")
.contains("compileJava21TestJava releaseVersion: 21");
}
@Test
void packageInJar() throws IOException {
writeBuildFile("""
plugins {
id 'java'
id 'org.springframework.build.multiReleaseJar'
}
version = '1.2.3'
multiRelease { releaseVersions 17 }
""");
writeClass("src/main/java17", "Main.java", """
public class Main {}
""");
BuildResult buildResult = runGradle("assemble");
File file = new File(this.projectDir, "/build/libs/" + this.projectDir.getName() + "-1.2.3.jar");
assertThat(file).exists();
try (JarFile jar = new JarFile(file)) {
Attributes mainAttributes = jar.getManifest().getMainAttributes();
assertThat(mainAttributes.getValue("Multi-Release")).isEqualTo("true");
assertThat(jar.entries().asIterator()).toIterable()
.anyMatch(entry -> entry.getName().equals("META-INF/versions/17/Main.class"));
}
}
private void writeBuildFile(String buildContent) throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
out.print(buildContent);
}
}
private void writeClass(String path, String fileName, String fileContent) throws IOException {
Path folder = this.projectDir.toPath().resolve(path);
Files.createDirectories(folder);
Path filePath = folder.resolve(fileName);
Files.createFile(filePath);
Files.writeString(filePath, fileContent);
}
private BuildResult runGradle(String... args) {
return GradleRunner.create().withProjectDir(this.projectDir).withArguments(args).withPluginClasspath().build();
}
}
+12 -17
View File
@@ -1,7 +1,6 @@
plugins {
id 'java-platform'
id 'io.freefair.aggregate-javadoc' version '8.13.1'
id 'org.jetbrains.dokka'
}
description = "Spring Framework API Docs"
@@ -21,10 +20,6 @@ dependencies {
}
javadoc {
javadocTool.set(javaToolchains.javadocToolFor({
languageVersion = JavaLanguageVersion.of(24)
}))
title = "${rootProject.description} ${version} API"
failOnError = true
options {
@@ -39,11 +34,7 @@ javadoc {
links(rootProject.ext.javadocLinks)
// 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)
addBooleanOption('Werror', true) // fail build on Javadoc warnings
}
maxMemory = "1024m"
doFirst {
@@ -55,19 +46,23 @@ javadoc {
}
}
dokka {
moduleName = "spring-framework"
dokkaPublications.html {
outputDirectory = project.java.docsDir.dir("kdoc-api")
includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md")
/**
* Produce KDoc for all Spring Framework modules in "build/docs/kdoc"
*/
rootProject.tasks.dokkaHtmlMultiModule.configure {
dependsOn {
tasks.named("javadoc")
}
moduleName.set("spring-framework")
outputDirectory.set(project.java.docsDir.dir("kdoc-api").get().asFile)
includes.from("$rootProject.rootDir/framework-docs/src/docs/api/dokka-overview.md")
}
/**
* Zip all Java docs (javadoc & kdoc) into a single archive
*/
tasks.register('docsZip', Zip) {
dependsOn = ['javadoc', 'dokkaGenerate']
dependsOn = ['javadoc', rootProject.tasks.dokkaHtmlMultiModule]
group = "distribution"
description = "Builds -${archiveClassifier} archive containing api and reference " +
"for deployment at https://docs.spring.io/spring-framework/docs/."
@@ -80,7 +75,7 @@ tasks.register('docsZip', Zip) {
from(javadoc) {
into "javadoc-api"
}
from(project.java.docsDir.dir("kdoc-api")) {
from(rootProject.tasks.dokkaHtmlMultiModule.outputDirectory) {
into "kdoc-api"
}
}
+2 -11
View File
@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
plugins {
id 'kotlin'
id 'io.spring.antora.generate-antora-yml' version '0.0.1'
@@ -43,12 +41,6 @@ repositories {
}
}
// To avoid a redeclaration error with Kotlin compiler
tasks.named('compileKotlin', KotlinCompilationTask.class) {
javaSources.from = []
compilerOptions.freeCompilerArgs = [ "-Xannotation-default-target=param-property" ] // Upcoming default, see https://youtrack.jetbrains.com/issue/KT-73255
}
dependencies {
implementation(project(":spring-aspects"))
implementation(project(":spring-context"))
@@ -62,7 +54,8 @@ dependencies {
implementation(project(":spring-webmvc"))
implementation(project(":spring-websocket"))
implementation("com.github.ben-manes.caffeine:caffeine")
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("com.fasterxml.jackson.module:jackson-module-parameter-names")
implementation("com.mchange:c3p0:0.9.5.5")
implementation("com.oracle.database.jdbc:ojdbc11")
implementation("io.projectreactor.netty:reactor-netty-http")
@@ -79,6 +72,4 @@ dependencies {
implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.junit.jupiter:junit-jupiter-api")
implementation("tools.jackson.core:jackson-databind")
implementation("tools.jackson.dataformat:jackson-dataformat-xml")
}
+5 -10
View File
@@ -32,7 +32,6 @@
**** xref:core/beans/java/bean-annotation.adoc[]
**** xref:core/beans/java/configuration-annotation.adoc[]
**** xref:core/beans/java/composing-configuration-classes.adoc[]
**** xref:core/beans/java/programmatic-bean-registration.adoc[]
*** xref:core/beans/environment.adoc[]
*** xref:core/beans/context-load-time-weaver.adoc[]
*** xref:core/beans/context-introduction.adoc[]
@@ -40,8 +39,8 @@
** xref:core/resources.adoc[]
** xref:core/validation.adoc[]
*** xref:core/validation/validator.adoc[]
*** xref:core/validation/data-binding.adoc[]
*** xref:core/validation/error-code-resolution.adoc[]
*** xref:core/validation/beans-beans.adoc[]
*** xref:core/validation/conversion.adoc[]
*** xref:core/validation/convert.adoc[]
*** xref:core/validation/format.adoc[]
*** xref:core/validation/format-configuring-formatting-globaldatetimeformat.adoc[]
@@ -100,9 +99,9 @@
*** xref:core/aop-api/autoproxy.adoc[]
*** xref:core/aop-api/targetsource.adoc[]
*** xref:core/aop-api/extensibility.adoc[]
** xref:core/resilience.adoc[]
** xref:core/null-safety.adoc[]
** xref:core/databuffer-codec.adoc[]
** xref:core/spring-jcl.adoc[]
** xref:core/aot.adoc[]
** xref:core/appendix.adoc[]
*** xref:core/appendix/xsd-schemas.adoc[]
@@ -162,6 +161,7 @@
**** xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[]
**** xref:web/webmvc/mvc-servlet/viewresolver.adoc[]
**** xref:web/webmvc/mvc-servlet/localeresolver.adoc[]
**** xref:web/webmvc/mvc-servlet/themeresolver.adoc[]
**** xref:web/webmvc/mvc-servlet/multipart.adoc[]
**** xref:web/webmvc/mvc-servlet/logging.adoc[]
*** xref:web/webmvc/filters.adoc[]
@@ -197,9 +197,7 @@
*** xref:web/webmvc-functional.adoc[]
*** xref:web/webmvc/mvc-uri-building.adoc[]
*** xref:web/webmvc/mvc-ann-async.adoc[]
*** xref:web/webmvc/mvc-range.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[]
@@ -228,7 +226,6 @@
**** 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[]
@@ -295,9 +292,7 @@
**** xref:web/webflux/controller/ann-advice.adoc[]
*** xref:web/webflux-functional.adoc[]
*** xref:web/webflux/uri-building.adoc[]
*** xref:web/webflux/range.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[]
@@ -438,8 +433,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[]
@@ -202,17 +202,18 @@ another file or files. The following example shows how to do so:
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
</beans>
----
In the preceding example, external bean definitions are loaded from the files
`services.xml` and `messageSource.xml`. All location paths are
In the preceding example, external bean definitions are loaded from three files:
`services.xml`, `messageSource.xml`, and `themeSource.xml`. All location paths are
relative to the definition file doing the importing, so `services.xml` must be in the
same directory or classpath location as the file doing the importing, while
`messageSource.xml` must be in a `resources` location below the
`messageSource.xml` and `themeSource.xml` must be in a `resources` location below the
location of the importing file. As you can see, a leading slash is ignored. However, given
that these paths are relative, it is better form not to use the slash at all. The
contents of the files being imported, including the top level `<beans/>` element, must
@@ -1,24 +1,26 @@
[[beans-classpath-scanning]]
= Classpath Scanning and Managed Components
Most examples in this chapter use XML to specify the configuration metadata that produces
each `BeanDefinition` within the Spring container. The previous section
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration]) demonstrates how to provide a lot of the configuration
metadata through source-level annotations. Even in those examples, however, the "base"
bean definitions are explicitly defined in the XML file, while the annotations drive only
the dependency injection. This section describes an option for implicitly detecting the
candidate components by scanning the classpath. Candidate components are classes that
match against a filter criteria and have a corresponding bean definition registered with
the container. This removes the need to use XML to perform bean registration. Instead, you
can use annotations (for example, `@Component`), AspectJ type expressions, or your own
Most examples in this chapter use XML to specify the configuration metadata that
produces each `BeanDefinition` within the Spring container. The previous section
(xref:core/beans/annotation-config.adoc[Annotation-based Container Configuration])
demonstrates how to provide a lot of the configuration metadata through source-level
annotations. Even in those examples, however, the "base" bean definitions are explicitly
defined in the XML file, while the annotations drive only the dependency injection.
This section describes an option for implicitly detecting the candidate components by
scanning the classpath. Candidate components are classes that match against a filter
criteria and have a corresponding bean definition registered with the container.
This removes the need to use XML to perform bean registration. Instead, you can use
annotations (for example, `@Component`), AspectJ type expressions, or your own
custom filter criteria to select which classes have bean definitions registered with
the container.
[NOTE]
====
You can define beans using Java rather than using XML files. Take a look at the
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples of how to
use these features.
`@Configuration`, `@Bean`, `@Import`, and `@DependsOn` annotations for examples
of how to use these features.
====
@@ -668,7 +670,9 @@ By default, the `AnnotationBeanNameGenerator` is used. For Spring
xref:core/beans/classpath-scanning.adoc#beans-stereotype-annotations[stereotype annotations],
if you supply a name via the annotation's `value` attribute that name will be used as
the name in the corresponding bean definition. This convention also applies when the
`@jakarta.inject.Named` annotation is used instead of Spring stereotype annotations.
following JSR-250 and JSR-330 annotations are used instead of Spring stereotype
annotations: `@jakarta.annotation.ManagedBean`, `@javax.annotation.ManagedBean`,
`@jakarta.inject.Named`, and `@javax.inject.Named`.
As of Spring Framework 6.1, the name of the annotation attribute that is used to specify
the bean name is no longer required to be `value`. Custom stereotype annotations can
@@ -828,10 +832,10 @@ definitions, there is no notion of bean definition inheritance, and inheritance
hierarchies at the class level are irrelevant for metadata purposes.
For details on web-specific scopes such as "`request`" or "`session`" in a Spring context,
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes]. As with the pre-built annotations for those scopes,
you may also compose your own scoping annotations by using Spring's meta-annotation
approach: for example, a custom annotation meta-annotated with `@Scope("prototype")`,
possibly also declaring a custom scoped-proxy mode.
see xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other[Request, Session, Application, and WebSocket Scopes].
As with the pre-built annotations for those scopes, you may also compose your own scoping
annotations by using Spring's meta-annotation approach: for example, a custom annotation
meta-annotated with `@Scope("prototype")`, possibly also declaring a custom scoped-proxy mode.
NOTE: To provide a custom strategy for scope resolution rather than relying on the
annotation-based approach, you can implement the
@@ -873,7 +877,8 @@ Kotlin::
----
When using certain non-singleton scopes, it may be necessary to generate proxies for the
scoped objects. The reasoning is described in xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
scoped objects. The reasoning is described in
xref:core/beans/factory-scopes.adoc#beans-factory-scopes-other-injection[Scoped Beans as Dependencies].
For this purpose, a scoped-proxy attribute is available on the component-scan
element. The three possible values are: `no`, `interfaces`, and `targetClass`. For example,
the following configuration results in standard JDK dynamic proxies:
@@ -1,88 +0,0 @@
[[beans-java-programmatic-registration]]
= Programmatic Bean Registration
As of Spring Framework 7, a first-class support for programmatic bean registration is
provided via the {spring-framework-api}/beans/factory/BeanRegistrar.html[`BeanRegistrar`]
interface that can be implemented to register beans programmatically in a flexible and
efficient way.
Those bean registrar implementations are typically imported with an `@Import` annotation
on `@Configuration` classes.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@Import(MyBeanRegistrar.class)
class MyConfiguration {
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@Import(MyBeanRegistrar::class)
class MyConfiguration {
}
----
======
NOTE: You can leverage type-level conditional annotations ({spring-framework-api}/context/annotation/Conditional.html[`@Conditional`],
but also other variants) to conditionally import the related bean registrars.
The bean registrar implementation uses {spring-framework-api}/beans/factory/BeanRegistry.html[`BeanRegistry`] and
{spring-framework-api}/core/env/Environment.html[`Environment`] APIs to register beans programmatically in a concise
and flexible way. For example, it allows custom registration through an `if` expression, a
`for` loop, etc.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
class MyBeanRegistrar implements BeanRegistrar {
@Override
public void register(BeanRegistry registry, Environment env) {
registry.registerBean("foo", Foo.class);
registry.registerBean("bar", Bar.class, spec -> spec
.prototype()
.lazyInit()
.description("Custom description")
.supplier(context -> new Bar(context.bean(Foo.class))));
if (env.matchesProfiles("baz")) {
registry.registerBean(Baz.class, spec -> spec
.supplier(context -> new Baz("Hello World!")));
}
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyBeanRegistrar : BeanRegistrarDsl({
registerBean<Foo>()
registerBean(
name = "bar",
prototype = true,
lazyInit = true,
description = "Custom description") {
Bar(bean<Foo>())
}
profile("baz") {
registerBean { Baz("Hello World!") }
}
})
----
======
NOTE: Bean registrars are supported with xref:core/aot.adoc[Ahead of Time Optimizations],
either on the JVM or with GraalVM native images, including when instance suppliers are used.
@@ -1,19 +1,19 @@
[[expressions-operator-elvis]]
= The Elvis Operator
The Elvis operator (`?:`) is a shortening of the ternary operator syntax and is used in
the https://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language. With the
ternary operator syntax, you often have to repeat a variable twice, as the following Java
example shows:
The Elvis operator is a shortening of the ternary operator syntax and is used in the
https://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language.
With the ternary operator syntax, you usually have to repeat a variable twice, as the
following example shows:
[source,java,indent=0,subs="verbatim,quotes"]
[source,groovy,indent=0,subs="verbatim,quotes"]
----
String name = "Elvis Presley";
String displayName = (name != null ? name : "Unknown");
----
Instead, you can use the Elvis operator (named for the resemblance to Elvis' hair style).
The following example shows how to use the Elvis operator in a SpEL expression:
The following example shows how to use the Elvis operator:
[tabs]
======
@@ -23,7 +23,7 @@ Java::
----
ExpressionParser parser = new SpelExpressionParser();
String name = parser.parseExpression("name ?: 'Unknown'").getValue(new Inventor(), String.class);
String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class);
System.out.println(name); // 'Unknown'
----
@@ -33,29 +33,14 @@ Kotlin::
----
val parser = SpelExpressionParser()
val name = parser.parseExpression("name ?: 'Unknown'").getValue(Inventor(), String::class.java)
val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java)
println(name) // 'Unknown'
----
======
[NOTE]
====
The SpEL Elvis operator also treats an _empty_ String like a `null` object. Thus, the
original Java example is only close to emulating the semantics of the operator: it would
need to use `name != null && !name.isEmpty()` as the predicate to be compatible with the
semantics of the SpEL Elvis operator.
====
[TIP]
====
As of Spring Framework 7.0, the SpEL Elvis operator supports `java.util.Optional` with
transparent unwrapping semantics.
For example, given the expression `A ?: B`, if `A` is `null` or an _empty_ `Optional`,
the expression evaluates to `B`. However, if `A` is a non-empty `Optional` the expression
evaluates to the object contained in the `Optional`, thereby effectively unwrapping the
`Optional` which correlates to `A.get()`.
====
NOTE: The SpEL Elvis operator also checks for _empty_ Strings in addition to `null` objects.
The original snippet is thus only close to emulating the semantics of the operator (it would need an
additional `!name.isEmpty()` check).
The following listing shows a more complex example:
@@ -69,11 +54,11 @@ Java::
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Inventor tesla = new Inventor("Nikola Tesla", "Serbian");
String name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class);
String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Nikola Tesla
tesla.setName("");
name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class);
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class);
System.out.println(name); // Elvis Presley
----
@@ -85,16 +70,16 @@ Kotlin::
val context = SimpleEvaluationContext.forReadOnlyDataBinding().build()
val tesla = Inventor("Nikola Tesla", "Serbian")
var name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java)
var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name) // Nikola Tesla
tesla.setName("")
name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java)
name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java)
println(name) // Elvis Presley
----
======
[TIP]
[NOTE]
=====
You can use the Elvis operator to apply default values in expressions. The following
example shows how to use the Elvis operator in a `@Value` expression:
@@ -104,6 +89,7 @@ example shows how to use the Elvis operator in a `@Value` expression:
@Value("#{systemProperties['pop3.port'] ?: 25}")
----
This will inject the value of the system property named `pop3.port` if it is defined or
`25` if the property is not defined.
This will inject a system property `pop3.port` if it is defined or 25 if not.
=====
@@ -252,6 +252,7 @@ Kotlin::
<1> Use "null-safe select first" operator on potentially null `members` list
======
The following example shows how to use the "null-safe select last" operator for
collections (`?.$`).
@@ -350,50 +351,6 @@ Kotlin::
<2> Use null-safe projection operator on null `members` list
======
[[expressions-operator-safe-navigation-optional]]
== Null-safe Operations on `Optional`
As of Spring Framework 7.0, null-safe operations are supported on instances of
`java.util.Optional` with transparent unwrapping semantics.
Specifically, when a null-safe operator is applied to an _empty_ `Optional`, it will be
treated as if the `Optional` were `null`, and the subsequent operation will evaluate to
`null`. However, if a null-safe operator is applied to a non-empty `Optional`, the
subsequent operation will be applied to the object contained in the `Optional`, thereby
effectively unwrapping the `Optional`.
For example, if `user` is of type `Optional<User>`, the expression `user?.name` will
evaluate to `null` if `user` is either `null` or an _empty_ `Optional` and will otherwise
evaluate to the `name` of the `user`, effectively `user.get().getName()` or
`user.get().name` for property or field access, respectively.
[NOTE]
====
Invocations of methods defined in the `Optional` API are still supported on an _empty_
`Optional`. For example, if `name` is of type `Optional<String>`, the expression
`name?.orElse('Unknown')` will evaluate to `"Unknown"` if `name` is an empty `Optional`
and will otherwise evaluate to the `String` contained in the `Optional` if `name` is a
non-empty `Optional`, effectively `name.get()`.
====
// NOTE: &#8288; is the Unicode Character 'WORD JOINER', which prevents undesired line wraps.
Similarly, if `names` is of type `Optional<List<String>>`, the expression
`names?.?&#8288;[#this.length > 5]` will evaluate to `null` if `names` is `null` or an _empty_
`Optional` and will otherwise evaluate to a sequence containing the names whose lengths
are greater than 5, effectively
`names.get().stream().filter(s -> s.length() > 5).toList()`.
The same semantics apply to all of the null-safe operators mentioned previously in this
chapter.
For further details and examples, consult the javadoc for the following operators.
* {spring-framework-api}/expression/spel/ast/PropertyOrFieldReference.html[`PropertyOrFieldReference`]
* {spring-framework-api}/expression/spel/ast/MethodReference.html[`MethodReference`]
* {spring-framework-api}/expression/spel/ast/Indexer.html[`Indexer`]
* {spring-framework-api}/expression/spel/ast/Selection.html[`Selection`]
* {spring-framework-api}/expression/spel/ast/Projection.html[`Projection`]
[[expressions-operator-safe-navigation-compound-expressions]]
== Null-safe Operations in Compound Expressions
@@ -1,216 +1,53 @@
[[null-safety]]
= Null-safety
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.
Although Java does not let you express null-safety with its type system, the Spring Framework
provides the following annotations in the `org.springframework.lang` package to let you
declare nullability of APIs and fields:
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].
* {spring-framework-api}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a
specific parameter, return value, or field can be `null`.
* {spring-framework-api}/lang/NonNull.html[`@NonNull`]: Annotation to indicate that a specific
parameter, return value, or field cannot be `null` (not needed on parameters, return values,
and fields where `@NonNullApi` and `@NonNullFields` apply, respectively).
* {spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`]: Annotation at the package level
that declares non-null as the default semantics for parameters and return values.
* {spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`]: Annotation at the package
level that declares non-null as the default semantics for fields.
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, and Java primitive types, as well as a pragmatic check on any
`@Nullable` annotation (regardless of the package).
The Spring Framework itself leverages these annotations, but they can also be used in any
Spring-based Java project to declare null-safe APIs and optionally null-safe fields.
Nullability declarations for generic type arguments, varargs, and array elements are not supported yet.
Nullability declarations are expected to be fine-tuned between Spring Framework releases,
including minor ones. Nullability of types used inside method bodies is outside the
scope of this feature.
NOTE: Other common libraries such as Reactor and Spring Data provide null-safe APIs that
use a similar nullability arrangement, delivering a consistent overall experience for
Spring application developers.
[[null-safety-libraries]]
== Annotating libraries with JSpecify annotations
[[use-cases]]
== Use cases
As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs
and 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.
In addition to providing an explicit declaration for Spring Framework API nullability,
these annotations can be used by an IDE (such as IDEA or Eclipse) to provide useful
warnings related to null-safety in order to avoid `NullPointerException` at runtime.
They are also used to make Spring APIs null-safe in Kotlin projects, since Kotlin natively
supports {kotlin-docs}/null-safety.html[null-safety]. More details
are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support documentation].
[[null-safety-applications]]
== Leveraging JSpecify annotations in Spring applications
[[jsr-305-meta-annotations]]
== JSR-305 meta-annotations
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.
Spring annotations are meta-annotated with {JSR}305[JSR 305]
annotations (a dormant but widespread JSR). JSR-305 meta-annotations let tooling vendors
like IDEA or Kotlin provide null-safety support in a generic way, without having to
hard-code support for Spring annotations.
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 proposed guidelines for explicitly specifying the nullability of
Spring-related libraries or applications.
[[null-safety-guidelines-jspecify]]
=== JSpecify
==== Defaults to non-null
A key point to understand is that the nullness of types is unknown by default in Java 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"]
----
@NullMarked
package org.springframework.core;
import org.jspecify.annotations.NullMarked;
----
==== Explicit nullability
In `@NullMarked` code, nullable type usage is defined explicitly with
https://jspecify.dev/docs/api/org/jspecify/annotations/Nullable.html[`@Nullable`].
A key difference between JSpecify `@Nullable` / `@NonNull` annotations and most other variants is that the JSpecify
annotations are meta-annotated with `@Target(ElementType.TYPE_USE)`, so they apply only to type usage. This impacts
where such annotations should be placed, either to comply with
https://docs.oracle.com/javase/specs/jls/se17/html/jls-9.html#jls-9.7.4[related Java specifications] or to follow code
style best practices. From a style perspective, it is recommended to embrace the type-use nature of those annotations
by placing them on the same line as and immediately preceding the annotated type.
For example, for a field:
[source,java,subs="verbatim,quotes"]
----
private @Nullable String fileEncoding;
----
Or for method parameters and method return types:
[source,java,subs="verbatim,quotes"]
----
public @Nullable String buildMessage(@Nullable String message,
@Nullable Throwable cause) {
// ...
}
----
[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.
====
https://jspecify.dev/docs/api/org/jspecify/annotations/NonNull.html[`@NonNull`] and
https://jspecify.dev/docs/api/org/jspecify/annotations/NullUnmarked.html[`@NullUnmarked`] should rarely be needed for
typical use cases.
==== Arrays and varargs
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. For example, in `@NullMarked` code:
- `@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`.
==== Generics
JSpecify annotations apply to generics as well. For example, in `@NullMarked` code:
- `List<String>` means a list of non-null elements (equivalent of `List<@NonNull String>`)
- `List<@Nullable String>` means a list of nullable elements
Things are a bit more complicated when you are declaring generic types or generic methods. See the related
https://jspecify.dev/docs/user-guide/#generics[JSpecify generics documentation] for more details.
WARNING: The nullability of generic types and generic methods
https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[is not yet fully supported by NullAway].
==== Nested and fully qualified types
The Java specification also enforces that annotations defined with `@Target(ElementType.TYPE_USE)` like JSpecify's
`@Nullable` annotation must be declared after the last dot (`.`) within inner or fully qualified type names:
- `Cache.@Nullable ValueWrapper`
- `jakarta.validation.@Nullable Validator`
[[null-safety-guidelines-nullaway]]
=== NullAway
==== Configuration
The recommended configuration is:
- `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 irrelevant warnings in your codebase.
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
arrays, varargs, and generics. Be aware that this mode is
https://github.com/uber/NullAway/issues?q=is%3Aissue+is%3Aopen+label%3Ajspecify[still under development] and requires
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 previously in this section.
==== Warnings suppression
There are a few valid use cases where NullAway will incorrectly detect nullability problems. In such cases,
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
{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 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 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 an external dependency).
- `@SuppressWarnings("NullAway") // See https://github.com/uber/NullAway/issues/1075` can be used when NullAway is not able to detect type variable nullness in generic methods.
[[null-safety-migrating]]
== Migrating from Spring null-safety annotations
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 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 issues, better tooling, better Kotlin integration,
and the capability to specify nullability more precisely for more use cases.
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 pretty significant in practice, since 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 semantics. For example
`@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify
annotations. The same applies to varargs.
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 method return types, instead of `@Nullable public String method()` with Spring annotations, use
`public @Nullable String method()` with JSpecify annotations.
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 will apply (type usage is considered non-null unless explicitly annotated as nullable).
It is neither necessary nor recommended to add a JSR-305 dependency to the project classpath to
take advantage of Spring's null-safe APIs. Only projects such as Spring-based libraries that use
null-safety annotations in their codebase should add `com.google.code.findbugs:jsr305:3.0.2`
with `compileOnly` Gradle configuration or Maven `provided` scope to avoid compiler warnings.
@@ -1,115 +0,0 @@
[[resilience]]
= Resilience Features
As of 7.0, the core Spring Framework includes a couple of common resilience features,
in particular `@Retryable` and `@ConcurrencyLimit` annotations for method invocations.
[[resilience-retryable]]
== Using `@Retryable`
`@Retryable` is a common annotation that specifies retry characteristics for an individual
method (with the annotation declared at the method level), or for all proxy-invoked
methods in a given class hierarchy (with the annotation declared at the type level).
[source,java,indent=0,subs="verbatim,quotes"]
----
@Retryable
public void sendNotification() {
this.jmsClient.destination("notifications").send(...);
}
----
By default, the method invocation will be retried for any exception thrown: with at
most 3 retry attempts after an initial failure, and a delay of 1 second between attempts.
This can be specifically adapted for every method if necessary for example, by narrowing
the exceptions to retry:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Retryable(MessageDeliveryException.class)
public void sendNotification() {
this.jmsClient.destination("notifications").send(...);
}
----
Or for 5 retry attempts and an exponential back-off strategy with a bit of jitter:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Retryable(maxAttempts = 5, delay = 100, jitter = 10, multiplier = 2, maxDelay = 1000)
public void sendNotification() {
this.jmsClient.destination("notifications").send(...);
}
----
Last but not least, `@Retryable` also works for reactive methods with a reactive
return type, decorating the pipeline with Reactor's retry capabilities:
[source,java,indent=0,subs="verbatim,quotes"]
----
@Retryable(maxAttempts = 5, delay = 100, jitter = 10, multiplier = 2, maxDelay = 1000)
public Mono<Void> sendNotification() {
return Mono.from(...); // <1>
}
----
<1> This raw `Mono` will get decorated with a retry spec.
For details on the various characteristics, see the available annotation attributes
in {spring-framework-api}/resilience/annotation/Retryable.html[`@Retryable`].
NOTE: There a `String` variants with placeholder support available for several attributes
as well, as an alternative to the specifically typed annotation attributes used in the
above examples.
[[resilience-concurrency]]
== Using `@ConcurrencyLimit`
`@ConcurrencyLimit` is an annotation that specifies a concurrency limit for an individual
method (with the annotation declared at the method level), or for all proxy-invoked
methods in a given class hierarchy (with the annotation declared at the type level).
[source,java,indent=0,subs="verbatim,quotes"]
----
@ConcurrencyLimit(10)
public void sendNotification() {
this.jmsClient.destination("notifications").send(...);
}
----
This is meant to protect the target resource from being accessed from too many threads at
the same time, similar to the effect of a pool size limit for a thread pool or a
connection pool that blocks access if its limit is reached.
You may optionally set the limit to 1, effectively locking access to the target bean
instance:
[source,java,indent=0,subs="verbatim,quotes"]
----
@ConcurrencyLimit(1) // <1>
public void sendNotification() {
this.jmsClient.destination("notifications").send(...);
}
----
<1> 1 is the default, but specifying it makes the intent clearer.
Such limiting is particularly useful with Virtual Threads where there is generally
no thread pool limit in place. For asynchronous tasks, this can be constrained on
{spring-framework-api}/core/task/SimpleAsyncTaskExecutor.html[`SimpleAsyncTaskExecutor`].
For synchronous invocations, this annotation provides equivalent behavior through
{spring-framework-api}/aop/interceptor/ConcurrencyThrottleInterceptor.html[`ConcurrencyThrottleInterceptor`]
which has been available since Spring Framework 1.0 for programmatic use with the AOP
framework.
[[resilience-enable]]
== Configuring `@EnableResilientMethods`
Note that like many of Spring's core annotation-based features, `@Retryable` and
`@ConcurrencyLimit` are designed as metadata that you can choose to honor or ignore.
The most convenient way to enable actual processing of the resilience annotations
through AOP interception is to declare `@EnableResilientMethods` on a corresponding
configuration class. Alternatively, you may declare `RetryAnnotationBeanPostProcessor`
and/or `ConcurrencyLimitBeanPostProcessor` individually.
@@ -0,0 +1,47 @@
[[spring-jcl]]
= Logging
Spring comes with its own Commons Logging bridge implemented
in the `spring-jcl` module. The implementation checks for the presence of the Log4j 2.x
API and the SLF4J 1.7 API in the classpath and uses the first one of those found as the
logging implementation, falling back to the Java platform's core logging facilities (also
known as _JUL_ or `java.util.logging`) if neither Log4j 2.x nor SLF4J is available.
Put Log4j 2.x or Logback (or another SLF4J provider) in your classpath, without any extra
bridges, and let the framework auto-adapt to your choice. For further information see the
{spring-boot-docs-ref}/features/logging.html[Spring
Boot Logging Reference Documentation].
[NOTE]
====
Spring's Commons Logging variant is only meant to be used for infrastructure logging
purposes in the core framework and in extensions.
For logging needs within application code, prefer direct use of Log4j 2.x, SLF4J, or JUL.
====
A `Log` implementation may be retrieved via `org.apache.commons.logging.LogFactory` as in
the following example.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyBean {
private final Log log = LogFactory.getLog(getClass());
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyBean {
private val log = LogFactory.getLog(javaClass)
// ...
}
----
======
@@ -1,20 +1,20 @@
[[data-binding]]
[[beans-binding]]
= Data Binding
Data binding is useful for binding user input to a target object where user input is a map
with property paths as keys, following xref:data-binding-conventions[JavaBeans conventions].
with property paths as keys, following xref:beans-beans-conventions[JavaBeans conventions].
`DataBinder` is the main class that supports this, and it provides two ways to bind user
input:
- xref:data-binding-constructor-binding[Constructor binding] - bind user input to a
public data constructor, looking up constructor argument values in the user input.
- xref:data-binding-property-binding[Property binding] - bind user input to setters,
matching keys from the user input to properties of the target object structure.
- xref:beans-constructor-binding[Constructor binding] - bind user input to a public data
constructor, looking up constructor argument values in the user input.
- xref:beans-beans[Property binding] - bind user input to setters, matching keys from the
user input to properties of the target object structure.
You can apply both constructor and property binding or only one.
[[data-binding-constructor-binding]]
[[beans-constructor-binding]]
== Constructor Binding
To use constructor binding:
@@ -32,7 +32,7 @@ WebFlux support a custom name mapping through the `@BindParam` annotation on con
parameters or fields if present. If necessary, you can also configure a `NameResolver` on
`DataBinder` to customize the argument name to use.
xref:data-binding-conventions[Type conversion] is applied as needed to convert user input.
xref:beans-beans-conventions[Type conversion] is applied as needed to convert user input.
If the constructor parameter is an object, it is constructed recursively in the same
manner, but through a nested property path. That means constructor binding creates both
the target object and any objects it contains.
@@ -46,7 +46,9 @@ If the target is created successfully, then `target` is set to the created insta
after the call to `construct`.
[[data-binding-property-binding]]
[[beans-beans]]
== Property Binding with `BeanWrapper`
The `org.springframework.beans` package adheres to the JavaBeans standard.
@@ -72,14 +74,15 @@ The way the `BeanWrapper` works is partly indicated by its name: it wraps a bean
perform actions on that bean, such as setting and retrieving properties.
[[data-binding-conventions]]
[[beans-beans-conventions]]
=== Setting and Getting Basic and Nested Properties
Setting and getting properties is done through the `setPropertyValue` and
`getPropertyValue` overloaded method variants of `BeanWrapper`. See their Javadoc for
details. The below table shows some examples of these conventions:
[[data-binding-conventions-properties-tbl]]
[[beans-beans-conventions-properties-tbl]]
.Examples of properties
|===
| Expression| Explanation
@@ -103,7 +106,7 @@ details. The below table shows some examples of these conventions:
(This next section is not vitally important to you if you do not plan to work with
the `BeanWrapper` directly. If you use only the `DataBinder` and the `BeanFactory`
and their default implementations, you should skip ahead to the
xref:core/validation/data-binding.adoc#data-binding-conversion[section on `PropertyEditors`].)
xref:core/validation/beans-beans.adoc#beans-beans-conversion[section on `PropertyEditors`].)
The following two example classes use the `BeanWrapper` to get and set
properties:
@@ -236,8 +239,9 @@ Kotlin::
======
[[data-binding-conversion]]
== ``PropertyEditor``s
[[beans-beans-conversion]]
== ``PropertyEditor``'s
Spring uses the concept of a `PropertyEditor` to effect the conversion between an
`Object` and a `String`. It can be handy
@@ -268,7 +272,7 @@ package. Most, (but not all, as indicated in the following table) are, by defaul
still register your own variant to override the default one. The following table describes
the various `PropertyEditor` implementations that Spring provides:
[[data-binding-property-editors-tbl]]
[[beans-beans-property-editors-tbl]]
.Built-in `PropertyEditor` Implementations
[cols="30%,70%"]
|===
@@ -422,8 +426,8 @@ Kotlin::
======
[[data-binding-conversion-customeditor-registration]]
=== Custom ``PropertyEditor``s
[[beans-beans-conversion-customeditor-registration]]
=== Custom ``PropertyEditor``'s
When setting bean properties as string values, a Spring IoC container ultimately uses
standard JavaBeans `PropertyEditor` implementations to convert these strings to the complex type of the
@@ -447,7 +451,7 @@ where it can be automatically detected and applied.
Note that all bean factories and application contexts automatically use a number of
built-in property editors, through their use of a `BeanWrapper` to
handle property conversions. The standard property editors that the `BeanWrapper`
registers are listed in the xref:core/validation/data-binding.adoc#data-binding-conversion[previous section].
registers are listed in the xref:core/validation/beans-beans.adoc#beans-beans-conversion[previous section].
Additionally, ``ApplicationContext``s also override or add additional editors to handle
resource lookups in a manner appropriate to the specific application context type.
@@ -565,7 +569,7 @@ Finally, the following example shows how to use `CustomEditorConfigurer` to regi
</bean>
----
[[data-binding-conversion-customeditor-registration-per]]
[[beans-beans-conversion-customeditor-registration-per]]
=== `PropertyEditorRegistrar`
Another mechanism for registering property editors with the Spring container is to
@@ -576,7 +580,7 @@ You can write a corresponding registrar and reuse it in each case.
`PropertyEditorRegistry`, an interface that is implemented by the Spring `BeanWrapper`
(and `DataBinder`). `PropertyEditorRegistrar` instances are particularly convenient
when used in conjunction with `CustomEditorConfigurer` (described
xref:core/validation/data-binding.adoc#data-binding-conversion-customeditor-registration[here]), which exposes a property
xref:core/validation/beans-beans.adoc#beans-beans-conversion-customeditor-registration[here]), which exposes a property
called `setPropertyEditorRegistrars(..)`. `PropertyEditorRegistrar` instances added
to a `CustomEditorConfigurer` in this fashion can easily be shared with `DataBinder` and
Spring MVC controllers. Furthermore, it avoids the need for synchronization on custom
@@ -1,7 +1,7 @@
[[validation-error-code-resolution]]
= Resolving Error Codes to Error Messages
[[validation-conversion]]
= Resolving Codes to Error Messages
We covered data binding and validation. This section covers outputting messages that correspond
We covered databinding and validation. This section covers outputting messages that correspond
to validation errors. In the example shown in the xref:core/validation/validator.adoc[preceding section],
we rejected the `name` and `age` fields. If we want to output the error messages by using a
`MessageSource`, we can do so using the error code we provide when rejecting the field
@@ -310,11 +310,9 @@ Java::
List<Integer> input = ...
cs.convert(input,
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(Integer.class)), // <1>
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class))); // <2>
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List.class, TypeDescriptor.valueOf(String.class)));
----
<1> `List<Integer>` type descriptor
<2> `List<String>` type descriptor
Kotlin::
+
@@ -324,11 +322,9 @@ Kotlin::
val input: List<Integer> = ...
cs.convert(input,
TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(Integer::class.java)), // <1>
TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java))) // <2>
TypeDescriptor.forObject(input), // List<Integer> type descriptor
TypeDescriptor.collection(List::class.java, TypeDescriptor.valueOf(String::class.java)))
----
<1> `List<Integer>` type descriptor
<2> `List<String>` type descriptor
======
Note that `DefaultConversionService` automatically registers converters that are
@@ -1,5 +1,5 @@
[[validator]]
= Validation Using Spring's Validator Interface
= Validation by Using Spring's Validator Interface
Spring features a `Validator` interface that you can use to validate objects. The
`Validator` interface works by using an `Errors` object so that, while validating,
@@ -1,7 +1,7 @@
[[orm-hibernate]]
= Hibernate
We start with a coverage of https://hibernate.org/[Hibernate] in a Spring environment,
We start with a coverage of https://hibernate.org/[Hibernate 5] in a Spring environment,
using it to demonstrate the approach that Spring takes towards integrating OR mappers.
This section covers many issues in detail and shows different variations of DAO
implementations and transaction demarcation. Most of these patterns can be directly
@@ -10,12 +10,13 @@ cover the other ORM technologies and show brief examples.
[NOTE]
====
As of Spring Framework 7.0, Spring requires Hibernate ORM 7.0 for Spring's
As of Spring Framework 6.0, Spring requires Hibernate ORM 5.5+ for Spring's
`HibernateJpaVendorAdapter` as well as for a native Hibernate `SessionFactory` setup.
We recommend Hibernate ORM 5.6 as the last feature branch in that Hibernate generation.
The `org.springframework.orm.jpa.hibernate` package supersedes the former `orm.hibernate5`:
now for use with Hibernate ORM 7.0, tightly integrated with `HibernateJpaVendorAdapter`
as well as supporting Hibernate's native `SessionFactory.getCurrentSession()` style.
Hibernate ORM 6.x is primarily supported as a JPA provider (`HibernateJpaVendorAdapter`).
Plain `SessionFactory` setup with the `orm.hibernate5` package is tolerated for migration
purposes. We recommend Hibernate ORM 6.x with JPA-style setup for new development projects.
====
@@ -42,7 +43,7 @@ JDBC `DataSource` and a Hibernate `SessionFactory` on top of it:
<property name="password" value=""/>
</bean>
<bean id="mySessionFactory" class="org.springframework.orm.jpa.hibernate.LocalSessionFactoryBean">
<bean id="mySessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="myDataSource"/>
<property name="mappingResources">
<list>
@@ -270,7 +271,7 @@ processing at runtime. The following example shows how to do so:
<!-- SessionFactory, DataSource, etc. omitted -->
<bean id="transactionManager"
class="org.springframework.orm.jpa.hibernate.HibernateTransactionManager">
class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
@@ -300,7 +301,7 @@ and an example for a business method implementation:
----
<beans>
<bean id="myTxManager" class="org.springframework.orm.jpa.hibernate.HibernateTransactionManager">
<bean id="myTxManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="mySessionFactory"/>
</bean>
@@ -212,7 +212,7 @@ example declares `sessionFactory` and `txManager` beans:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="sessionFactory" class="org.springframework.orm.jpa.hibernate.LocalSessionFactoryBean">
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
@@ -226,7 +226,7 @@ example declares `sessionFactory` and `txManager` beans:
</property>
</bean>
<bean id="txManager" class="org.springframework.orm.jpa.hibernate.HibernateTransactionManager">
<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
----
@@ -238,7 +238,7 @@ transaction coordinator and possibly also its connection release mode configurat
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="sessionFactory" class="org.springframework.orm.jpa.hibernate.LocalSessionFactoryBean">
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
@@ -262,7 +262,7 @@ for enforcing the same defaults:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="sessionFactory" class="org.springframework.orm.jpa.hibernate.LocalSessionFactoryBean">
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
@@ -1,105 +0,0 @@
[[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).
@@ -0,0 +1,74 @@
[[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).
@@ -11,7 +11,9 @@ Spring Framework's email support:
* The https://jakartaee.github.io/mail-api/[Jakarta Mail] library
This library is freely available on the web -- for example, in Maven Central as
`org.eclipse.angus:angus-mail`.
`com.sun.mail:jakarta.mail`. Please make sure to use the latest 2.x version (which uses
the `jakarta.mail` package namespace) rather than Jakarta Mail 1.6.x (which uses the
`javax.mail` package namespace).
****
The Spring Framework provides a helpful utility library for sending email that shields
@@ -7,8 +7,8 @@ This describes how to receive messages with JMS in Spring.
[[jms-receiving-sync]]
== Synchronous Receipt
While JMS is typically associated with asynchronous processing, you can consume messages
synchronously. The `receive(..)` methods on `JmsTemplate` and `JmsClient` provide this
While JMS is typically associated with asynchronous processing, you can
consume messages synchronously. The overloaded `receive(..)` methods provide this
functionality. During a synchronous receive, the calling thread blocks until a message
becomes available. This can be a dangerous operation, since the calling thread can
potentially be blocked indefinitely. The `receiveTimeout` property specifies how long
@@ -120,37 +120,6 @@ MapMessage={
}
----
NOTE: This JMS-specific `org.springframework.jms.support.converter.MessageConverter`
arrangement operates on JMS message types and is responsible for immediate conversion
to `jakarta.jms.TextMessage`, `jakarta.jms.BytesMessage`, etc. For a contract supporting
generic message payloads, use `org.springframework.messaging.converter.MessageConverter`
with `JmsMessagingTemplate` or preferably `JmsClient` as your central delegate instead.
[[jms-sending-jmsclient]]
== Sending a Message with `JmsClient`
[source,java,indent=0,subs="verbatim,quotes"]
----
// Reusable handle, typically created through JmsClient.create(ConnectionFactory)
// For custom conversion, use JmsClient.create(ConnectionFactory, MessageConverter)
private JmsClient jmsClient;
public void sendWithConversion() {
this.jmsClient.destination("myQueue")
.withTimeToLive(1000)
.send("myPayload"); // optionally with a headers Map next to the payload
}
public void sendCustomMessage() {
Message<?> message =
MessageBuilder.withPayload("myPayload").build(); // optionally with headers
this.jmsClient.destination("myQueue")
.withTimeToLive(1000)
.send(message);
}
----
[[jms-sending-callbacks]]
== Using `SessionCallback` and `ProducerCallback` on `JmsTemplate`
@@ -4,20 +4,13 @@
This section describes how to use Spring's JMS components.
[[jms-jmstemplate-jmsclient]]
== `JmsTemplate` and `JmsClient`
[[jms-jmstemplate]]
== Using `JmsTemplate`
The `JmsTemplate` class is the central class in the JMS core package. It simplifies the
use of JMS, since it handles the creation and release of resources when sending or
synchronously receiving messages.
`JmsClient` is a new API variant in Spring Framework 7.0, following the design of
`JdbcClient` and co. `JmsClient` builds on `JmsTemplate` for straightforward send
and receive operations with customization options per operation.
[[jms-jmstemplate]]
=== Using `JmsTemplate`
Code that uses the `JmsTemplate` needs only to implement callback interfaces that give them
a clearly defined high-level contract. The `MessageCreator` callback interface creates a
message when given a `Session` provided by the calling code in `JmsTemplate`. To
@@ -50,23 +43,10 @@ and then safely inject this shared reference into multiple collaborators. To be
clear, the `JmsTemplate` is stateful, in that it maintains a reference to a
`ConnectionFactory`, but this state is not conversational state.
[[jms-jmsclient]]
=== Using `JmsClient`
As of Spring Framework 4.1, `JmsMessagingTemplate` is built on top of `JmsTemplate`
and provides an integration with the Spring's common messaging abstraction -- that is,
handling `org.springframework.messaging.Message` for sending and receiving,
throwing `org.springframework.messaging.MessagingException` and with payload conversion
going through `org.springframework.messaging.converter.MessageConverter` (with many
common converter implementations available).
As of Spring Framework 7.0, a fluent API called `JmsClient` is available. This provides
customizable operations around `org.springframework.messaging.Message` and throwing
`org.springframework.messaging.MessagingException`, similar to `JmsMessagingTemplate`,
as well as integration with `org.springframework.messaging.converter.MessageConverter`.
A `JmsClient can either be created for a given `ConnectionFactory` or for a given
`JmsTemplate`, in the latter case reusing its settings by default. See
{spring-framework-api}/jms/core/JmsClient.html[`JmsClient`] for usage examples.
and provides an integration with the messaging abstraction -- that is,
`org.springframework.messaging.Message`. This lets you create the message to
send in a generic manner.
[[jms-connections]]
@@ -3,34 +3,26 @@
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 Clients] -- annotated interface backed by generated proxy
* 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.
[[rest-restclient]]
== `RestClient`
`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.
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.
=== Create a `RestClient`
=== Creating a `RestClient`
`RestClient` has static `create` shortcut methods.
It also exposes a `builder()` with further options:
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.
- 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
Once created (or built), the `RestClient` can be used safely by multiple threads.
Once created, a `RestClient` is safe to use in multiple threads.
The below shows how to create or build a `RestClient`:
The following sample shows how to create a default `RestClient`, and how to build a custom one.
[tabs]
======
@@ -47,8 +39,6 @@ Java::
.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();
@@ -67,25 +57,23 @@ Kotlin::
.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()
----
======
=== Use the `RestClient`
=== Using the `RestClient`
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)`.
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.
==== Request URL
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.
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.
The URL is typically specified as a `String`, with optional URI template variables.
The following shows how to perform a request:
The following example configures a GET request to `https://example.com/orders/42`:
[tabs]
======
@@ -120,7 +108,6 @@ 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.
@@ -856,17 +843,15 @@ It can be used to migrate from the latter to the former.
[[rest-http-interface]]
== HTTP Interface Clients
== HTTP Interface
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.
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.
First, create the Java interface:
Start by creating the interface with `@HttpExchange` methods:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -880,7 +865,43 @@ First, create the Java interface:
}
----
Optionally, use `@HttpExchange` at the type level to declare common attributes for all 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:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -897,45 +918,15 @@ Optionally, use `@HttpExchange` at the type level to declare common attributes f
}
----
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
`@HttpExchange` methods support flexible method signatures with the following inputs:
Annotated, HTTP exchange methods support flexible method signatures with the following
method parameters:
[cols="1,2", options="header"]
|===
| Method parameter | Description
| Method argument | Description
| `URI`
| Dynamically set the URL for the request, overriding the annotation's `url` attribute.
@@ -996,31 +987,27 @@ 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 Arguments
=== Custom argument resolver
You can configure a custom `HttpServiceArgumentResolver`. The example interface below
uses a custom `Search` method parameter type:
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:
include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0]
A custom argument resolver could be implemented like this:
We can implement our own `HttpServiceArgumentResolver` that supports our custom `Search` type
and writes its data in the outgoing HTTP request.
include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0]
To configure the custom argument resolver:
Finally, we can use this argument resolver during the setup and use our HTTP interface.
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
@@ -1093,206 +1080,63 @@ 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 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:
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:
[source,java,indent=0,subs="verbatim,quotes"]
----
// For RestClient
RestClient restClient = RestClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, (request, response) -> ...)
.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
// or for WebClient...
WebClient webClient = WebClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
.build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);
// 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 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`.
For more details and options, such as suppressing error status codes, see the Javadoc of
`defaultStatusHandler` in `RestClient.Builder`.
For `WebClient`:
[[rest-http-interface-adapter-decorator]]
=== Decorating the Adapter
`HttpExchangeAdapter` and `ReactorHttpExchangeAdapter` are contracts that decouple HTTP
Interface client infrastructure from the details of invoking the underlying
client. There are adapter implementations for `RestClient`, `WebClient`, and
`RestTemplate`.
Occasionally, it may be useful to intercept client invocations through a decorator
configurable in the `HttpServiceProxyFactory.Builder`. For example, you can apply
built-in decorators to suppress 404 exceptions and return a `ResponseEntity` with
`NOT_FOUND` and a `null` body:
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"]
----
// For RestClient
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(restCqlientAdapter)
.exchangeAdapterDecorator(NotFoundRestClientAdapterDecorator::new)
WebClient webClient = WebClient.builder()
.defaultStatusHandler(HttpStatusCode::isError, resp -> ...)
.build();
// or for WebClient...
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builderFor(webClientAdapter)
.exchangeAdapterDecorator(NotFoundWebClientAdapterDecorator::new)
.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`:
[[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:
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"]
----
@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) -> ...);
};
}
}
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(myErrorHandler);
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
----
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"
For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and
the `ResponseErrorHandler` hierarchy.
@@ -1,4 +1,110 @@
[[kotlin-bean-definition-dsl]]
= Bean Definition DSL
See xref:core/beans/java/programmatic-bean-registration.adoc[Programmatic Bean Registration].
Spring Framework supports registering beans in a functional way by using lambdas
as an alternative to XML or Java configuration (`@Configuration` and `@Bean`). In a nutshell,
it lets you register beans with a lambda that acts as a `FactoryBean`.
This mechanism is very efficient, as it does not require any reflection or CGLIB proxies.
In Java, you can, for example, write the following:
[source,java,indent=0]
----
class Foo {}
class Bar {
private final Foo foo;
public Bar(Foo foo) {
this.foo = foo;
}
}
GenericApplicationContext context = new GenericApplicationContext();
context.registerBean(Foo.class);
context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class)));
----
In Kotlin, with reified type parameters and `GenericApplicationContext` Kotlin extensions,
you can instead write the following:
[source,kotlin,indent=0]
----
class Foo
class Bar(private val foo: Foo)
val context = GenericApplicationContext().apply {
registerBean<Foo>()
registerBean { Bar(it.getBean()) }
}
----
When the class `Bar` has a single constructor, you can even just specify the bean class,
the constructor parameters will be autowired by type:
[source,kotlin,indent=0]
----
val context = GenericApplicationContext().apply {
registerBean<Foo>()
registerBean<Bar>()
}
----
In order to allow a more declarative approach and cleaner syntax, Spring Framework provides
a {spring-framework-api-kdoc}/spring-context/org.springframework.context.support/-bean-definition-dsl/index.html[Kotlin bean definition DSL]
It declares an `ApplicationContextInitializer` through a clean declarative API,
which lets you deal with profiles and `Environment` for customizing
how beans are registered.
In the following example notice that:
* Type inference usually allows to avoid specifying the type for bean references like `ref("bazBean")`
* It is possible to use Kotlin top level functions to declare beans using callable references like `bean(::myRouter)` in this example
* When specifying `bean<Bar>()` or `bean(::myRouter)`, parameters are autowired by type
* The `FooBar` bean will be registered only if the `foobar` profile is active
[source,kotlin,indent=0]
----
class Foo
class Bar(private val foo: Foo)
class Baz(var message: String = "")
class FooBar(private val baz: Baz)
val myBeans = beans {
bean<Foo>()
bean<Bar>()
bean("bazBean") {
Baz().apply {
message = "Hello world"
}
}
profile("foobar") {
bean { FooBar(ref("bazBean")) }
}
bean(::myRouter)
}
fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router {
// ...
}
----
NOTE: This DSL is programmatic, meaning it allows custom registration logic of beans
through an `if` expression, a `for` loop, or any other Kotlin constructs.
You can then use this `beans()` function to register beans on the application context,
as the following example shows:
[source,kotlin,indent=0]
----
val context = GenericApplicationContext().apply {
myBeans.initialize(this)
refresh()
}
----
NOTE: Spring Boot is based on JavaConfig and
{spring-boot-issues}/8115[does not yet provide specific support for functional bean definition],
but you can experimentally use functional bean definitions through Spring Boot's `ApplicationContextInitializer` support.
See {stackoverflow-questions}/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685[this Stack Overflow answer]
for more details and up-to-date information. See also the experimental Kofu DSL developed in {spring-github-org}-experimental/spring-fu[Spring Fu incubator].
@@ -5,11 +5,30 @@ One of Kotlin's key features is {kotlin-docs}/null-safety.html[null-safety],
which cleanly deals with `null` values at compile time rather than bumping into the famous
`NullPointerException` at runtime. This makes applications safer through nullability
declarations and expressing "`value or no value`" semantics without paying the cost of wrappers, such as `Optional`.
Kotlin allows using functional constructs with nullable values. See this
{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety].
(Kotlin allows using functional constructs with nullable values. See this
{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety].)
Although Java does not let you express null-safety in its type-system, the Spring Framework
provides xref:core/null-safety.adoc[null-safety of the whole Spring Framework API]
via tooling-friendly https://jspecify.dev/[JSpecify] annotations.
provides xref:languages/kotlin/null-safety.adoc[null-safety of the whole Spring Framework API]
via tooling-friendly annotations declared in the `org.springframework.lang` package.
By default, types from Java APIs used in Kotlin are recognized as
{kotlin-docs}/java-interop.html#null-safety-and-platform-types[platform types],
for which null-checks are relaxed.
{kotlin-docs}/java-interop.html#jsr-305-support[Kotlin support for JSR-305 annotations]
and Spring nullability annotations provide null-safety for the whole Spring Framework API to Kotlin developers,
with the advantage of dealing with `null`-related issues at compile time.
As of Kotlin 2.1, Kotlin enforces strict handling of nullability annotations from `org.jspecify.annotations` package.
NOTE: Libraries such as Reactor or Spring Data provide null-safe APIs to leverage this feature.
You can configure JSR-305 checks by adding the `-Xjsr305` compiler flag with the following
options: `-Xjsr305={strict|warn|ignore}`.
For kotlin versions 1.1+, the default behavior is the same as `-Xjsr305=warn`.
The `strict` value is required to have Spring Framework API null-safety taken into account
in Kotlin types inferred from Spring API but should be used with the knowledge that Spring
API nullability declaration could evolve even between minor releases and that more checks may
be added in the future.
NOTE: Generic type arguments, varargs, and array elements nullability are not supported yet,
but should be in an upcoming release. See {kotlin-github-org}/KEEP/issues/79[this discussion]
for up-to-date information.
@@ -2,7 +2,7 @@
= Requirements
:page-section-summary-toc: 1
Spring Framework supports Kotlin 2.2+ and requires
Spring Framework supports Kotlin 1.7+ and requires
https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib[`kotlin-stdlib`]
and https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect[`kotlin-reflect`]
to be present on the classpath. They are provided by default if you bootstrap a Kotlin project on
@@ -319,7 +319,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] along with
The recommended testing framework is https://junit.org/junit5/[JUnit 5] along with
https://mockk.io/[Mockk] for mocking.
NOTE: If you are using Spring Boot, see
@@ -329,7 +329,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 allows constructor injection of beans which is pretty useful with Kotlin
JUnit Jupiter (JUnit 5) 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.
@@ -353,7 +353,7 @@ file with a `spring.test.constructor.autowire.mode = all` property.
=== `PER_CLASS` Lifecycle
Kotlin lets you specify meaningful test function names between backticks (```).
With JUnit Jupiter, Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
With JUnit Jupiter (JUnit 5), 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.
@@ -396,8 +396,8 @@ class IntegrationTests {
[[specification-like-tests]]
=== Specification-like Tests
You can create specification-like tests with Kotlin and JUnit Jupiter's `@Nested` test
class support. The following example shows how to do so:
You can create specification-like tests with JUnit 5 and Kotlin.
The following example shows how to do so:
[source,kotlin,indent=0]
----
@@ -72,13 +72,61 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met
----
[[kotlin-script-templates]]
== Kotlin Script Templates
Spring Framework provides a
{spring-framework-api}/web/servlet/view/script/ScriptTemplateView.html[`ScriptTemplateView`]
which supports {JSR}223[JSR-223] to render templates by using script engines.
By leveraging `scripting-jsr223` dependencies, it
is possible to use such feature to render Kotlin-based templates with
{kotlin-github-org}/kotlinx.html[kotlinx.html] DSL or Kotlin multiline interpolated `String`.
`build.gradle.kts`
[source,kotlin,indent=0]
----
dependencies {
runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}")
}
----
Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplateViewResolver` beans.
`KotlinScriptConfiguration.kt`
[source,kotlin,indent=0]
----
@Configuration
class KotlinScriptConfiguration {
@Bean
fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
engineName = "kotlin"
setScripts("scripts/render.kts")
renderFunction = "render"
isSharedEngine = false
}
@Bean
fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
setPrefix("templates/")
setSuffix(".kts")
}
}
----
See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example
project for more details.
[[kotlin-multiplatform-serialization]]
== Kotlin multiplatform serialization
{kotlin-github-org}/kotlinx.serialization[Kotlin multiplatform serialization] is
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON,
and ProtoBuf formats.
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The built-in support currently targets CBOR, JSON, and ProtoBuf formats.
To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependencies
and plugin. With Spring MVC and WebFlux, Kotlin serialization is configured by default if it is in the classpath and
other variants like Jackson are not. If needed, configure the converters or codecs manually.
To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin.
With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since
Kotlin serialization is designed to serialize only Kotlin classes annotated with `@Serializable`.
With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration,
if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually.
@@ -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):
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/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`]
@@ -174,9 +174,9 @@ the parameters of a test class constructor are autowired from components in the
If `@TestConstructor` is not present or meta-present on a test class, the default _test
constructor autowire mode_ will be used. See the tip below for details on how to change
the default mode. Note, however, that a local declaration of `@Autowired` or
`@jakarta.inject.Inject` on a constructor takes precedence over both `@TestConstructor`
and the default mode.
the default mode. Note, however, that a local declaration of `@Autowired`,
`@jakarta.inject.Inject`, or `@javax.inject.Inject` on a constructor takes precedence
over both `@TestConstructor` and the default mode.
.Changing the default test constructor autowire mode
[TIP]
@@ -1,13 +1,6 @@
[[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
@@ -140,8 +140,8 @@ Kotlin::
======
If we write tests that use JUnit Jupiter, we can reduce code duplication even further,
since annotations in JUnit Jupiter can also be used as meta-annotations. Consider the
following example:
since annotations in JUnit 5 can also be used as meta-annotations. Consider the following
example:
[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 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.
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.
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
@@ -31,10 +31,10 @@ under a key that is based solely on those locations. So, if `TestClassB` also de
`{"app-config.xml", "test-config.xml"}` for its locations (either explicitly or
implicitly through inheritance) but does not define `@WebAppConfiguration`, a different
`ContextLoader`, different active profiles, different context initializers, different
context customizers, different test or dynamic property sources, or a different parent
context, then the same `ApplicationContext` is shared by both test classes. This means
that the setup cost for loading an application context is incurred only once (per test
suite), and subsequent test execution is much faster.
test property sources, or a different parent context, then the same `ApplicationContext`
is shared by both test classes. This means that the setup cost for loading an application
context is incurred only once (per test suite), and subsequent test execution is much
faster.
.Test suites and forked processes
[NOTE]
@@ -62,15 +62,6 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize
alternative, you can set the same property via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
As of Spring Framework 7.0, an application context stored in the context cache will be
stopped when it is no longer actively in use and automatically restarted the next time
the context is retrieved from the cache. Specifically, the latter will restart all
auto-startup beans in the application context, effectively restoring the lifecycle state.
This ensures that background processes within the context are not actively running while
the context is not used by tests. For example, JMS listener containers, scheduled tasks,
and any other components in the context that implement `Lifecycle` or `SmartLifecycle`
will be in a "stopped" state until the context is used again by a test.
Since having a large number of application contexts loaded within a given test suite can
cause the suite to take an unnecessarily long time to run, it is often beneficial to
know exactly how many contexts have been loaded and cached. To view the statistics for
@@ -80,11 +71,10 @@ the underlying context cache, you can set the log level for the
In the unlikely case that a test corrupts the application context and requires reloading
(for example, by modifying a bean definition or the state of an application object), you
can annotate your test class or test method with `@DirtiesContext` (see the discussion of
`@DirtiesContext` in
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations]).
This instructs Spring to remove the context from the cache and rebuild the application
context before running the next test that requires the same application context. Note
that support for the `@DirtiesContext` annotation is provided by the
`@DirtiesContext` in xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations]
). This instructs Spring to remove the context from the cache and rebuild
the application context before running the next test that requires the same application
context. Note that support for the `@DirtiesContext` annotation is provided by the
`DirtiesContextBeforeModesTestExecutionListener` and the
`DirtiesContextTestExecutionListener`, which are enabled by default.
@@ -173,7 +173,7 @@ shows this configuration:
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
<bean id="sessionFactory" class="org.springframework.orm.jpa.hibernate.LocalSessionFactoryBean">
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<!-- configuration elided for brevity -->
</bean>
@@ -9,7 +9,7 @@ in JUnit and TestNG.
== SpringExtension for JUnit Jupiter
The Spring TestContext Framework offers full integration with the JUnit Jupiter testing
framework, originally introduced in JUnit 5. By annotating test classes with
framework, 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,
@@ -72,8 +72,8 @@ Kotlin::
----
======
Since you can also use annotations in JUnit Jupiter as meta-annotations, Spring provides
the `@SpringJUnitConfig` and `@SpringJUnitWebConfig` composed annotations to simplify the
Since you can also use annotations in JUnit 5 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
@@ -482,14 +482,6 @@ Kotlin::
[[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)`
@@ -545,14 +537,6 @@ 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):
@@ -622,14 +606,6 @@ Kotlin::
[[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):
@@ -96,7 +96,9 @@ Kotlin::
The following code snippet is similar to the one we saw earlier for a request-scoped
bean. However, this time, the `userService` bean has a dependency on a session-scoped
`userPreferences` bean. Note that the `UserPreferences` bean is instantiated by using a
SpEL expression that retrieves an attribute from the current HTTP session.
SpEL expression that retrieves the theme from the current HTTP session. In our test, we
need to configure a theme in the mock session managed by the TestContext framework. The
following example shows how to do so:
.Session-scoped bean configuration
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -253,7 +253,6 @@ Java::
client = WebTestClient.bindToController(new TestController())
.configureClient()
.baseUrl("/test")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.build();
----
@@ -264,7 +263,6 @@ Kotlin::
client = WebTestClient.bindToController(TestController())
.configureClient()
.baseUrl("/test")
.apiVersionInserter(ApiVersionInserter.fromHeader("API-Version").build())
.build()
----
======
@@ -576,10 +576,10 @@ parameter, though which additional constraints can be expressed.
=== Predicates
You can write your own `RequestPredicate`, but the `RequestPredicates` utility class
offers built-in options for common needs for matching based on the HTTP method, request
path, headers, xref:#api-version[API version], and more.
The following example uses an `Accept` header, request predicate:
offers commonly used implementations, based on the request path, HTTP method, content-type,
and so on.
The following example uses a request predicate to create a constraint based on the `Accept`
header:
[tabs]
======
@@ -771,51 +771,6 @@ Kotlin::
======
[[api-version]]
=== API Version
Router functions support matching by API version.
First, enable API versioning in the
xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config], and then you can
use the `version` xref:#webflux-fn-predicates[predicate] as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", version("1.2"),
request -> ServerResponse.ok().body("Hello World")).build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val route = coRouter {
GET("/hello-world", version("1.2")) {
ServerResponse.ok().bodyValueAndAwait("Hello World")
}
}
----
======
The `version` predicate can be:
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above, up to the highest
xref:web/webmvc/mvc-config/api-version.adoc[supported version].
See xref:web/webflux-versioning.adoc[API Versioning] for more details on underlying
infrastructure and support for API Versioning.
[[webflux-fn-serving-resources]]
== Serving Resources
@@ -837,7 +792,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
ClassPathResource index = new ClassPathResource("static/index.html");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate();
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
.resource(spaPredicate, index)
.build();
@@ -849,7 +805,9 @@ Kotlin::
----
val redirectToIndex = router {
val index = ClassPathResource("static/index.html")
val spaPredicate = !(path("/api/**") or path("/error"))
val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
val spaPredicate = !(path("/api/**") or path("/error") or
pathExtension(extensions::contains))
resource(spaPredicate, index)
}
----
@@ -1,104 +0,0 @@
[[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:
- Configure xref:web/webflux/config.adoc#webflux-config-api-version[API versioning]
in the WebFlux Config
- xref:web/webflux/controller/ann-requestmapping.adoc#webflux-ann-requestmapping-version[Map requests]
to annotated controller methods with an API version
- xref:web/webflux-functional.adoc#api-version[Route requests]
to functional endpoints with an API version
Client support for API versioning is available also in `RestClient`, `WebClient`, and
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as
for testing in `WebTestClient`.
[[webflux-versioning-strategy]]
== ApiVersionStrategy
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[See equivalent in the Servlet stack]#
This is the central strategy for API versioning that holds all configured preferences
related to versioning. It does the following:
- Resolves versions from the requests via xref:#webflux-versioning-resolver[ApiVersionResolver]
- Parses raw version values into `Comparable<?>` with xref:#webflux-versioning-parser[ApiVersionParser]
- xref:#webflux-versioning-validation[Validates] request versions
`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 `patWebFluxch` 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, but you can turn that off through a flag in the
WebFlux config, and use only the versions configured explicitly in the config.
By default, a version is required when API versioning is enabled, and
`MissingApiVersionException` is raised resulting in a 400 response if not present.
You can make it optional in which case the most recent version is used.
You can also specify a default version to use.
[[webflux-versioning-deprecation-handler]]
== ApiVersionDeprecationHandler
[.small]#xref:web/webmvc-versioning.adoc#mvc-versioning-deprecation-handler[See equivalent in the Reactive stack]#
This strategy can be configured to send hints and information about deprecated versions to
clients via response headers. The built-in `StandardApiVersionDeprecationHandler`
can set the "Deprecation" "Sunset" headers and "Link" headers as defined in
https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and
https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594]. You can also configure a custom
handler for different headers.
[[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.
@@ -448,7 +448,7 @@ Java::
----
@GetMapping
FragmentsRendering handle() {
return FragmentsRendering.fragment("posts").fragment("comments").build();
return FragmentsRendering.with("posts").fragment("comments").build();
}
----
@@ -458,7 +458,7 @@ Kotlin::
----
@GetMapping
fun handle(): FragmentsRendering {
return FragmentsRendering.fragment("posts").fragment("comments").build()
return FragmentsRendering.with("posts").fragment("comments").build()
}
----
======
@@ -1,7 +1,7 @@
[[webflux-client-builder]]
= Configuration
The simplest way to create `WebClient` is through one of the static factory methods:
The simplest way to create a `WebClient` is through one of the static factory methods:
* `WebClient.create()`
* `WebClient.create(String baseUrl)`
@@ -12,12 +12,10 @@ 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.
@@ -638,12 +638,13 @@ For https://www.webjars.org/documentation[WebJars], versioned URLs like
`/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them.
The related resource location is configured out of the box with Spring Boot (or can be configured
manually via `ResourceHandlerRegistry`) and does not require to add the
`org.webjars:webjars-locator-lite` dependency.
`org.webjars:webjars-locator-core` dependency.
Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the
`WebJarsResourceResolver` which is automatically registered when the
`org.webjars:webjars-locator-lite` library is present on the classpath. The resolver can re-write
URLs to include the version of the jar and can also match against incoming URLs without versions
`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a
classpath scanning that could slow down application startup. The resolver can re-write URLs to
include the version of the jar and can also match against incoming URLs without versions
-- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`.
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
@@ -674,72 +675,6 @@ 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, use the `ApiVersionConfigurer` callback of `WebFluxConfigurer`:
[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")
}
}
----
======
You can resolve the version through one of the built-in options listed below, or
alternatively use a custom `ApiVersionResolver`:
- Request header
- Request parameter
- Path segment
- Media type parameter
TIP: When using a path segment, consider configuring a shared path prefix externally
in xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options.
By default, the version is parsed with `SemanticVersionParser`, but you can also configure
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 turn that off through a flag in the WebFlux config, and
consider only the versions configured explicitly in the config as supported.
Requests with a version that is not supported are rejected with
`InvalidApiVersionException` resulting in a 400 response.
You can set an `ApiVersionDeprecationHandler` to send information about deprecated
versions to clients. The built-in standard handler can set "Deprecation", "Sunset", and
"Link" headers based on https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and
https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594].
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
@@ -43,7 +43,7 @@ request parameters. Argument names are determined through runtime-retained param
names in the bytecode.
By default, both constructor and property
xref:core/validation/data-binding.adoc[data binding] are applied. However,
xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However,
model object design requires careful consideration, and for security reasons it is
recommended either to use an object tailored specifically for web binding, or to apply
constructor binding only. If property binding must still be used, then _allowedFields_
@@ -234,10 +234,13 @@ Kotlin::
======
--
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
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.
URI path patterns can also have:
- Embedded `${...}` placeholders that are resolved on startup via
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expressions `#{...}`.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
Both classes are located in `spring-web` and are expressly designed for use with HTTP URL
@@ -409,88 +412,6 @@ 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 when you enable API versioning
in the xref:web/webflux/config.adoc#webflux-config-api-version[WebFlux Config] you need
to specify how to resolve the version. The WebFlux Config creates an
xref:web/webflux-versioning.adoc#webflux-versioning-strategy[ApiVersionStrategy] that in turn
is used to map requests.
Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` `version` attribute supports the following:
- No value -- matches any version
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above
If multiple controller methods have a version less than or equal to the request version,
the highest of those, and closest to the request version, is the one considered,
in effect superseding the rest.
To illustrate this, consider the following 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 allows only a strict match, and therefore does not match.
In this scenario, a `NotAcceptableApiVersionException` results in a 400 response.
NOTE: The above assumes the request version is a
xref:web/webflux/config.adoc#webflux-config-api-version["supported" version],
or otherwise it would fail.
See xref:web/webflux-versioning.adoc[API Versioning] for more details on underlying
infrastructure and support for API Versioning.
[[webflux-ann-requestmapping-head-options]]
== HTTP HEAD, OPTIONS
[.small]#xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-head-options[See equivalent in the Servlet stack]#
@@ -1,20 +0,0 @@
[[webflux-range]]
= Range Requests
:page-section-summary-toc: 1
[.small]#xref:web/webmvc/mvc-range.adoc[See equivalent in the Servlet stack]#
Spring WebFlux supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110]
range requests. For an overview, see the
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests]
Mozilla guide.
The `Range` header is parsed and handled transparently in WebFlux when an annotated
controller returns a `Resource` or `ResponseEntity<Resource>`, or a functional endpoint
xref:web/webflux-functional.adoc#webflux-fn-resources[serves a `Resource`]. `Range` header
support is also transparently handled when serving
xref:web/webflux/config.adoc#webflux-config-static-resources[static resources].
The underlying support is in the `HttpRange` class, which exposes methods to parse
`Range` headers and split a `Resource` into a `List<ResourceRegion>` that in turn can be
then written to the response via `ResourceRegionEncoder` and `ResourceHttpMessageWriter`.
@@ -80,7 +80,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]
@@ -175,16 +175,17 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpHandler handler = ...
JettyCoreHttpHandlerAdapter adapter = new JettyCoreHttpHandlerAdapter(handler);
Servlet servlet = new JettyHttpHandlerAdapter(handler);
Server server = new Server();
server.setHandler(adapter);
ServletContextHandler contextHandler = new ServletContextHandler(server, "");
contextHandler.addServlet(new ServletHolder(servlet), "/");
contextHandler.start();
ServerConnector connector = new ServerConnector(server);
connector.setHost(host);
connector.setPort(port);
server.addConnector(connector);
server.start();
----
@@ -193,27 +194,27 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val handler: HttpHandler = ...
val adapter = JettyCoreHttpHandlerAdapter(handler)
val servlet = JettyHttpHandlerAdapter(handler)
val server = Server()
server.setHandler(adapter)
val contextHandler = ServletContextHandler(server, "")
contextHandler.addServlet(ServletHolder(servlet), "/")
contextHandler.start();
val connector = ServerConnector(server)
connector.host = host
connector.port = port
server.addConnector(connector)
server.start()
----
======
TIP: In Spring Framework 6.2, `JettyHttpHandlerAdapter` was deprecated in favor of
`JettyCoreHttpHandlerAdapter`, which integrates directly with Jetty 12 APIs
without a Servlet layer.
*Servlet Container*
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`.
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`.
[[webflux-web-handler-api]]
@@ -546,10 +546,10 @@ parameter, through which additional constraints can be expressed.
=== Predicates
You can write your own `RequestPredicate`, but the `RequestPredicates` utility class
offers built-in options for common needs for matching based on the HTTP method, request
path, headers, xref:#api-version[API version], and more.
The following example uses an `Accept` header, request predicate:
offers commonly used implementations, based on the request path, HTTP method, content-type,
and so on.
The following example uses a request predicate to create a constraint based on the `Accept`
header:
[tabs]
======
@@ -749,51 +749,6 @@ Kotlin::
======
[[api-version]]
=== API Version
Router functions support matching by API version.
First, enable API versioning in the
xref:web/webmvc/mvc-config/api-version.adoc[MVC Config], and then you can use the
`version` xref:#webmvc-fn-predicates[predicate] as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", version("1.2"),
request -> ServerResponse.ok().body("Hello World")).build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val route = router {
GET("/hello-world", version("1.2")) {
ServerResponse.ok().body("Hello World")
}
}
----
======
The `version` predicate can be:
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above, up to the highest
xref:web/webmvc/mvc-config/api-version.adoc[supported version].
See xref:web/webmvc-versioning.adoc[API Versioning] for more details on underlying
infrastructure and support for API Versioning.
[[webmvc-fn-serving-resources]]
== Serving Resources
@@ -816,7 +771,7 @@ Java::
----
ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate();
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
.resource(spaPredicate, index)
.build();
@@ -828,7 +783,9 @@ Kotlin::
----
val redirectToIndex = router {
val index = ClassPathResource("static/index.html")
val spaPredicate = !(path("/api/**") or path("/error"))
val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
val spaPredicate = !(path("/api/**") or path("/error") or
pathExtension(extensions::contains))
resource(spaPredicate, index)
}
----
@@ -879,7 +836,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 WebMvc Java configuration:
The following example shows a WebFlux Java configuration:
[tabs]
======
@@ -1,104 +0,0 @@
[[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:
- Configure xref:web/webmvc/mvc-config/api-version.adoc[API versioning] in the MVC Config
- xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmapping-version[Map requests]
to annotated controller methods with an API version
- xref:web/webmvc-functional.adoc#api-version[Route requests]
to functional endpoints with an API version
Client support for API versioning is available also in `RestClient`, `WebClient`, and
xref:integration/rest-clients.adoc#rest-http-interface[HTTP Service] clients, as well as
for testing in MockMvc and `WebTestClient`.
[[mvc-versioning-strategy]]
== ApiVersionStrategy
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-strategy[See equivalent in the Reactive stack]#
This is the central strategy for API versioning that holds all configured preferences
related to versioning. It does the following:
- Resolves versions from the requests via xref:#mvc-versioning-resolver[ApiVersionResolver]
- Parses raw version values into `Comparable<?>` with an xref:#mvc-versioning-parser[ApiVersionParser]
- xref:#mvc-versioning-validation[Validates] request versions
- Sends deprecation hints in the responses
`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/webflux-versioning.adoc#webflux-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, but you can turn that off through a flag in the
MVC config, and use only the versions configured explicitly in the config.
By default, a version is required when API versioning is enabled, and
`MissingApiVersionException` is raised resulting in a 400 response if not present.
You can make it optional in which case the most recent version is used.
You can also specify a default version to use.
[[mvc-versioning-deprecation-handler]]
== ApiVersionDeprecationHandler
[.small]#xref:web/webflux-versioning.adoc#webflux-versioning-deprecation-handler[See equivalent in the Reactive stack]#
This strategy can be configured to send hints and information about deprecated versions to
clients via response headers. The built-in `StandardApiVersionDeprecationHandler`
can set the "Deprecation" "Sunset" headers and "Link" headers as defined in
https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and
https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594]. You can also configure a custom
handler for different headers.
[[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.
@@ -48,7 +48,7 @@ Java::
----
@GetMapping
FragmentsRendering handle() {
return FragmentsRendering.fragment("posts").fragment("comments").build();
return FragmentsRendering.with("posts").fragment("comments").build();
}
----
@@ -58,7 +58,7 @@ Kotlin::
----
@GetMapping
fun handle(): FragmentsRendering {
return FragmentsRendering.fragment("posts").fragment("comments").build()
return FragmentsRendering.with("posts").fragment("comments").build()
}
----
======
@@ -108,12 +108,12 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]#
In previous Spring Framework versions, Spring MVC could be configured to ignore trailing slashes in URL paths
when mapping incoming requests on controller methods. This means that sending a "GET /home/" request would be
handled by a controller method annotated with `@GetMapping("/home")`.
when mapping incoming requests on controller methods. This could be done by enabling the `setUseTrailingSlashMatch`
option on the `PathMatchConfigurer`. This means that sending a "GET /home/" request would be handled by a controller
method annotated with `@GetMapping("/home")`.
This option was deprecated in 6.0 and removed in 7.0, but applications are still expected to handle such
requests in a safe way. The `UrlHandlerFilter` Servlet filter has been designed for this purpose.
It can be configured to:
This option has been retired, but applications are still expected to handle such requests in a safe way.
The `UrlHandlerFilter` Servlet filter has been designed for this purpose. It can be configured to:
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request.
@@ -54,11 +54,6 @@ You can customize XML mapping as needed through the use of JAXB or Jackson's pro
When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `ObjectMapper` property.
By default, this converter supports `application/xml`. This requires the `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` dependency.
| `KotlinSerializationJsonHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON using `kotlinx.serialization`.
This converter is not configured by default, as this conflicts with Jackson.
Developers must configure it as an additional converter ahead of the Jackson one.
| `MappingJackson2CborHttpMessageConverter`
| `com.fasterxml.jackson.dataformat:jackson-dataformat-cbor`
@@ -1,37 +0,0 @@
[[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, use the `ApiVersionConfigurer` callback of `WebMvcConfigurer`:
include-code::./WebConfiguration[tag=snippet,indent=0]
You can resolve the version through one of the built-in options listed below, or
alternatively use a custom `ApiVersionResolver`:
- Request header
- Request parameter
- Path segment
- Media type parameter
TIP: When using a path segment, consider configuring a shared path prefix externally
in xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching] options.
By default, the version is parsed with `SemanticVersionParser`, but you can also configure
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 turn that off through a flag in the MVC config, and
consider only the versions configured explicitly in the config as supported.
Requests with a version that is not supported are rejected with
`InvalidApiVersionException` resulting in a 400 response.
You can set an `ApiVersionDeprecationHandler` to send information about deprecated
versions to clients. The built-in standard handler can set "Deprecation", "Sunset", and
"Link" headers based on https://datatracker.ietf.org/doc/html/rfc9745[RFC 9745] and
https://datatracker.ietf.org/doc/html/rfc8594[RFC 8594].
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.
@@ -7,10 +7,6 @@ You can use the `@EnableWebMvc` annotation to enable MVC configuration with prog
include-code::./WebConfiguration[tag=snippet,indent=0]
WARNING: As of 7.0, support for the XML configuration namespace for Spring MVC has been deprecated.
There are no plans yet for removing it completely but XML configuration will not be updated to follow
the Java configuration model.
NOTE: When using Spring Boot, you may want to use `@Configuration` classes of type `WebMvcConfigurer` but without `@EnableWebMvc` to keep Spring Boot MVC customizations. See more details in xref:web/webmvc/mvc-config/customize.adoc[the MVC Config API section] and in {spring-boot-docs-ref}/web/servlet.html#web.servlet.spring-mvc.auto-configuration[the dedicated Spring Boot documentation].
The preceding example registers a number of Spring MVC
@@ -3,10 +3,47 @@
[.small]#xref:web/webflux/config.adoc#webflux-config-message-codecs[See equivalent in the Reactive stack]#
You can configure the `HttpMessageConverter` instances to use by overriding
{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters(org.springframework.http.converter.HttpMessageConverters.Builder)[`configureMessageConverters()`].
You can set the `HttpMessageConverter` instances to use in Java configuration,
replacing the ones used by default, by overriding
{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#configureMessageConverters-java.util.List-[`configureMessageConverters()`].
You can also customize the list of configured message converters at the end by overriding
{spring-framework-api}/web/servlet/config/annotation/WebMvcConfigurer.html#extendMessageConverters-java.util.List-[`extendMessageConverters()`].
The following example configures custom Jackson JSON and XML converters with customized mappers instead of the default
ones:
TIP: In a Spring Boot application, the `WebMvcAutoConfiguration` adds any
`HttpMessageConverter` beans it detects, in addition to default converters. Hence, in a
Boot application, prefer to use the {spring-boot-docs-ref}/web/servlet.html#web.servlet.spring-mvc.message-converters[HttpMessageConverters]
mechanism. Or alternatively, use `extendMessageConverters` to modify message converters
at the end.
The following example adds XML and Jackson JSON converters with a customized `ObjectMapper`
instead of the default ones:
include-code::./WebConfiguration[tag=snippet,indent=0]
In the preceding example,
{spring-framework-api}/http/converter/json/Jackson2ObjectMapperBuilder.html[`Jackson2ObjectMapperBuilder`]
is used to create a common configuration for both `MappingJackson2HttpMessageConverter` and
`MappingJackson2XmlHttpMessageConverter` with indentation enabled, a customized date format,
and the registration of
{jackson-github-org}/jackson-module-parameter-names[`jackson-module-parameter-names`],
Which adds support for accessing parameter names (a feature added in Java 8).
This builder customizes Jackson's default properties as follows:
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/DeserializationFeature.html#FAIL_ON_UNKNOWN_PROPERTIES[`DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES`] is disabled.
* {jackson-docs}/jackson-databind/javadoc/2.6/com/fasterxml/jackson/databind/MapperFeature.html#DEFAULT_VIEW_INCLUSION[`MapperFeature.DEFAULT_VIEW_INCLUSION`] is disabled.
It also automatically registers the following well-known modules if they are detected on the classpath:
* {jackson-github-org}/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types.
* {jackson-github-org}/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`.
* {jackson-github-org}/jackson-module-kotlin[jackson-module-kotlin]: Support for Kotlin classes and data classes.
NOTE: Enabling indentation with Jackson XML support requires
https://search.maven.org/#search%7Cgav%7C1%7Cg%3A%22org.codehaus.woodstox%22%20AND%20a%3A%22woodstox-core-asl%22[`woodstox-core-asl`]
dependency in addition to https://search.maven.org/#search%7Cga%7C1%7Ca%3A%22jackson-dataformat-xml%22[`jackson-dataformat-xml`] one.
Other interesting Jackson modules are available:
* https://github.com/zalando/jackson-datatype-money[jackson-datatype-money]: Support for `javax.money` types (unofficial module).
* {jackson-github-org}/jackson-datatype-hibernate[jackson-datatype-hibernate]: Support for Hibernate-specific types and properties (including lazy-loading aspects).
@@ -48,12 +48,13 @@ For https://www.webjars.org/documentation[WebJars], versioned URLs like
`/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them.
The related resource location is configured out of the box with Spring Boot (or can be configured
manually via `ResourceHandlerRegistry`) and does not require to add the
`org.webjars:webjars-locator-lite` dependency.
`org.webjars:webjars-locator-core` dependency.
Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the
`WebJarsResourceResolver` which is automatically registered when the
`org.webjars:webjars-locator-lite` library is present on the classpath. The resolver can re-write
URLs to include the version of the jar and can also match against incoming URLs without versions
`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a
classpath scanning that could slow down application startup. The resolver can re-write URLs to
include the version of the jar and can also match against incoming URLs without versions
-- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`.
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
@@ -30,7 +30,8 @@ and others) and is equivalent to `required=false`.
| `jakarta.servlet.http.PushBuilder`
| Servlet 4.0 push builder API for programmatic HTTP/2 resource pushes.
Note that this API has been deprecated as of Servlet 6.1.
Note that, per the Servlet specification, the injected `PushBuilder` instance can be null if the client
does not support that HTTP/2 feature.
| `java.security.Principal`
| Currently authenticated user -- possibly a specific `Principal` implementation class if known.
@@ -157,5 +157,7 @@ Kotlin::
----
======
Note that you need to enable the use of matrix variables. In the MVC XML namespace, you can set
Note that you need to enable the use of matrix variables. In the MVC Java configuration,
you need to set a `UrlPathHelper` with `removeSemicolonContent=false` through
xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching]. In the MVC XML namespace, you can set
`<mvc:annotation-driven enable-matrix-variables="true"/>`.
@@ -78,7 +78,7 @@ Kotlin::
======
By default, both constructor and property
xref:core/validation/data-binding.adoc[data binding] are applied. However,
xref:core/validation/beans-beans.adoc#beans-binding[data binding] are applied. However,
model object design requires careful consideration, and for security reasons it is
recommended either to use an object tailored specifically for web binding, or to apply
constructor binding only. If property binding must still be used, then _allowedFields_
@@ -217,10 +217,13 @@ Kotlin::
----
======
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
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.
URI path patterns can also have:
- Embedded `${...}` placeholders that are resolved on startup via
`PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. This is useful, for example, to parameterize a base URL based on
external configuration.
- SpEL expression `#{...}`.
[[mvc-ann-requestmapping-pattern-comparison]]
@@ -246,6 +249,36 @@ specific than other pattern that do not have double wildcards.
For the full details, follow the above links to the pattern Comparators.
[[mvc-ann-requestmapping-suffix-pattern-match]]
== Suffix Match
Starting in 5.3, by default Spring MVC no longer performs `.{asterisk}` suffix pattern
matching where a controller mapped to `/person` is also implicitly mapped to
`/person.{asterisk}`. As a consequence path extensions are no longer used to interpret
the requested content type for the response -- for example, `/person.pdf`, `/person.xml`,
and so on.
Using file extensions in this way was necessary when browsers used to send `Accept` headers
that were hard to interpret consistently. At present, that is no longer a necessity and
using the `Accept` header should be the preferred choice.
Over time, the use of file name extensions has proven problematic in a variety of ways.
It can cause ambiguity when overlain with the use of URI variables, path parameters, and
URI encoding. Reasoning about URL-based authorization
and security (see next section for more details) also becomes more difficult.
To completely disable the use of path extensions in versions prior to 5.3, set the following:
* `useSuffixPatternMatching(false)`, see xref:web/webmvc/mvc-config/path-matching.adoc[PathMatchConfigurer]
* `favorPathExtension(false)`, see xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer]
Having a way to request content types other than through the `"Accept"` header can still
be useful, for example, when typing a URL in a browser. A safe alternative to path extensions is
to use the query parameter strategy. If you must use file extensions, consider restricting
them to a list of explicitly registered extensions through the `mediaTypes` property of
xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer].
[[mvc-ann-requestmapping-rfd]]
== Suffix Match and RFD
@@ -428,89 +461,6 @@ and xref:web/webmvc/mvc-controller/ann-requestmapping.adoc#mvc-ann-requestmappin
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 when you enable API versioning
in the xref:web/webmvc/mvc-config/api-version.adoc[MVC Config] you need
to specify how to resolve the version. The MVC Config creates an
xref:web/webmvc-versioning.adoc#mvc-versioning-strategy[ApiVersionStrategy] that in turn
is used to map requests.
Once API versioning is enabled, you can begin to map requests with versions.
The `@RequestMapping` `version` attribute supports the following:
- No value -- matches any version
- Fixed version ("1.2") -- matches the given version only
- Baseline version ("1.2+") -- matches the given version and above
If multiple controller methods have a version less than or equal to the request version,
the highest of those, and closest to the request version, is the one considered,
in effect superseding the rest.
To illustrate this, consider the following 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 allows only a strict match, and therefore does not match.
In this scenario, a `NotAcceptableApiVersionException` results in a 400 response.
NOTE: The above assumes the request version is a
xref:web/webmvc/mvc-config/api-version.adoc["supported" version], or otherwise it
would fail.
See xref:web/webmvc-versioning.adoc[API Versioning] for more details on underlying
infrastructure and support for API Versioning.
[[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]#
@@ -4,8 +4,13 @@
[.small]#xref:web/webflux/http2.adoc[See equivalent in the Reactive stack]#
Servlet 4 containers are required to support HTTP/2, and Spring Framework requires
Servlet API 6.1. From a programming model perspective, there is nothing specific that
Servlet 4 containers are required to support HTTP/2, and Spring Framework 5 is compatible
with Servlet API 4. From a programming model perspective, there is nothing specific that
applications need to do. However, there are considerations related to server configuration.
For more details, see the
{spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page].
The Servlet API does expose one construct related to HTTP/2. You can use the
`jakarta.servlet.http.PushBuilder` to proactively push resources to clients, and it
is supported as a xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[method argument]
to `@RequestMapping` methods.
@@ -1,20 +0,0 @@
[[mvc-range]]
= Range Requests
:page-section-summary-toc: 1
[.small]#xref:web/webflux/range.adoc[See equivalent in the Reactive stack]#
Spring MVC supports https://datatracker.ietf.org/doc/html/rfc9110#section-14[RFC 9110]
range requests. For an overview, see the
https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests[Ranger Requests]
Mozilla guide.
The `Range` header is parsed and handled transparently in Spring MVC when an annotated
controller returns a `Resource` or `ResponseEntity<Resource>`, or a functional endpoint
xref:web/webmvc-functional.adoc#webmvc-fn-resources[serves a `Resource`]. `Range` header
support is also transparently handled when serving
xref:web/webmvc/mvc-config/static-resources.adoc[static resources].
The underlying support is in the `HttpRange` class, which exposes methods to parse
`Range` headers and split a `Resource` into a `List<ResourceRegion>` that in turn can be
then written to the response via `ResourceRegionHttpMessageConverter`.
@@ -11,6 +11,8 @@ The `DispatcherServlet` processes requests as follows:
* The locale resolver is bound to the request to let elements in the process
resolve the locale to use when processing the request (rendering the view, preparing
data, and so on). If you do not need locale resolving, you do not need the locale resolver.
* The theme resolver is bound to the request to let elements such as views determine
which theme to use. If you do not use themes, you can ignore it.
* If you specify a multipart file resolver, the request is inspected for multiparts. If
multiparts are found, the request is wrapped in a `MultipartHttpServletRequest` for
further processing by other elements in the process. See xref:web/webmvc/mvc-servlet/multipart.adoc[Multipart Resolver] for further
@@ -43,6 +43,10 @@ The following table lists the special beans detected by the `DispatcherServlet`:
| Resolve the `Locale` a client is using and possibly their time zone, in order to be able
to offer internationalized views. See xref:web/webmvc/mvc-servlet/localeresolver.adoc[Locale].
| xref:web/webmvc/mvc-servlet/themeresolver.adoc[`ThemeResolver`]
| Resolve themes your web application can use -- for example, to offer personalized layouts.
See xref:web/webmvc/mvc-servlet/themeresolver.adoc[Themes].
| xref:web/webmvc/mvc-servlet/multipart.adoc[`MultipartResolver`]
| Abstraction for parsing a multi-part request (for example, browser form file upload) with
the help of some multipart parsing library. See xref:web/webmvc/mvc-servlet/multipart.adoc[Multipart Resolver].
@@ -0,0 +1,92 @@
[[mvc-themeresolver]]
= Themes
You can apply Spring Web MVC framework themes to set the overall look-and-feel of your
application, thereby enhancing user experience. A theme is a collection of static
resources, typically style sheets and images, that affect the visual style of the
application.
WARNING: as of 6.0 support for themes has been deprecated theme in favor of using CSS,
and without any special support on the server side.
[[mvc-themeresolver-defining]]
== Defining a theme
To use themes in your web application, you must set up an implementation of the
`org.springframework.ui.context.ThemeSource` interface. The `WebApplicationContext`
interface extends `ThemeSource` but delegates its responsibilities to a dedicated
implementation. By default, the delegate is an
`org.springframework.ui.context.support.ResourceBundleThemeSource` implementation that
loads properties files from the root of the classpath. To use a custom `ThemeSource`
implementation or to configure the base name prefix of the `ResourceBundleThemeSource`,
you can register a bean in the application context with the reserved name, `themeSource`.
The web application context automatically detects a bean with that name and uses it.
When you use the `ResourceBundleThemeSource`, a theme is defined in a simple properties
file. The properties file lists the resources that make up the theme, as the following example shows:
[literal,subs="verbatim,quotes"]
----
styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg
----
The keys of the properties are the names that refer to the themed elements from view
code. For a JSP, you typically do this using the `spring:theme` custom tag, which is
very similar to the `spring:message` tag. The following JSP fragment uses the theme
defined in the previous example to customize the look and feel:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
<head>
<link rel="stylesheet" href="<spring:theme code='styleSheet'/>" type="text/css"/>
</head>
<body style="background=<spring:theme code='background'/>">
...
</body>
</html>
----
By default, the `ResourceBundleThemeSource` uses an empty base name prefix. As a result,
the properties files are loaded from the root of the classpath. Thus, you would put the
`cool.properties` theme definition in a directory at the root of the classpath (for
example, in `/WEB-INF/classes`). The `ResourceBundleThemeSource` uses the standard Java
resource bundle loading mechanism, allowing for full internationalization of themes. For
example, we could have a `/WEB-INF/classes/cool_nl.properties` that references a special
background image with Dutch text on it.
[[mvc-themeresolver-resolving]]
== Resolving Themes
After you define themes, as described in the xref:web/webmvc/mvc-servlet/themeresolver.adoc#mvc-themeresolver-defining[preceding section],
you decide which theme to use. The `DispatcherServlet` looks for a bean named `themeResolver`
to find out which `ThemeResolver` implementation to use. A theme resolver works in much the same
way as a `LocaleResolver`. It detects the theme to use for a particular request and can also
alter the request's theme. The following table describes the theme resolvers provided by Spring:
[[mvc-theme-resolver-impls-tbl]]
.ThemeResolver implementations
[cols="1,4"]
|===
| Class | Description
| `FixedThemeResolver`
| Selects a fixed theme, set by using the `defaultThemeName` property.
| `SessionThemeResolver`
| The theme is maintained in the user's HTTP session. It needs to be set only once for
each session but is not persisted between sessions.
| `CookieThemeResolver`
| The selected theme is stored in a cookie on the client.
|===
Spring also provides a `ThemeChangeInterceptor` that lets theme changes on every
request with a simple request parameter.
@@ -82,7 +82,10 @@ for all HTTP processing -- including WebSocket handshake and all other HTTP
requests -- such as Spring MVC's `DispatcherServlet`.
This is a significant limitation of JSR-356 that Spring's WebSocket support addresses with
a standard `RequestUpgradeStrategy` implementation when running in a WebSocket API 2.1+ runtime.
server-specific `RequestUpgradeStrategy` implementations even when running in a JSR-356 runtime.
Such strategies currently exist for Tomcat, Jetty, GlassFish, WebLogic, WebSphere, and Undertow
(and WildFly). As of Jakarta WebSocket 2.1, a standard request upgrade strategy is available
which Spring chooses on Jakarta EE 10 based web containers such as Tomcat 10.1 and Jetty 12.
A secondary consideration is that Servlet containers with JSR-356 support are expected
to perform a `ServletContainerInitializer` (SCI) scan that can slow down application
@@ -1,4 +1,4 @@
xref:core/validation/data-binding.adoc[Data binding] for web requests involves
xref:core/validation/beans-beans.adoc#beans-binding[Data binding] for web requests involves
binding request parameters to a model object. By default, request parameters can be bound
to any public property of the model object, which means malicious clients can provide
extra values for properties that exist in the model object graph, but are not expected to
@@ -39,7 +39,7 @@ the properties required for the input:
----
Another good practice is to apply
xref:core/validation/data-binding.adoc#data-binding-constructor-binding[constructor binding],
xref:core/validation/beans-beans.adoc#beans-constructor-binding[constructor binding],
which uses only the request parameters it needs for constructor arguments, and any other
input is ignored. This is in contrast to property binding which by default binds every
request parameter for which there is a matching property.
@@ -24,6 +24,7 @@ import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent;
import org.springframework.aot.test.agent.RuntimeHintsInvocations;
import org.springframework.aot.test.agent.RuntimeHintsRecorder;
import org.springframework.core.SpringVersion;
import static org.assertj.core.api.Assertions.assertThat;
@@ -32,7 +33,6 @@ import static org.assertj.core.api.Assertions.assertThat;
// method is only enabled if the RuntimeHintsAgent is loaded on the current JVM.
// It also tags tests with the "RuntimeHints" JUnit tag.
@EnabledIfRuntimeHintsAgent
@SuppressWarnings("removal")
class SampleReflectionRuntimeHintsTests {
@Test
@@ -43,7 +43,7 @@ class SampleReflectionRuntimeHintsTests {
typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE));
// Invoke the relevant piece of code we want to test within a recording lambda
RuntimeHintsInvocations invocations = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> {
RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> {
SampleReflection sample = new SampleReflection();
sample.performReflection();
});
@@ -25,7 +25,6 @@ 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 {
@@ -1,32 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.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[]
@@ -17,35 +17,29 @@
package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigmessageconverters;
import java.text.SimpleDateFormat;
import java.util.List;
import tools.jackson.dataformat.xml.XmlMapper;
import tools.jackson.databind.SerializationFeature;
import tools.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.module.paramnames.ParameterNamesModule;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverters;
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
import org.springframework.http.converter.xml.JacksonXmlHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
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 {
@Override
public void configureMessageConverters(HttpMessageConverters.ServerBuilder builder) {
JsonMapper jsonMapper = JsonMapper.builder()
.findAndAddModules()
.enable(SerializationFeature.INDENT_OUTPUT)
.defaultDateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.build();
XmlMapper xmlMapper = XmlMapper.builder()
.findAndAddModules()
.defaultUseWrapper(false)
.build();
builder.jsonMessageConverter(new JacksonJsonHttpMessageConverter(jsonMapper))
.xmlMessageConverter(new JacksonXmlHttpMessageConverter(xmlMapper));
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder()
.indentOutput(true)
.dateFormat(new SimpleDateFormat("yyyy-MM-dd"))
.modulesToInstall(new ParameterNamesModule());
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}
// end::snippet[]
@@ -23,7 +23,6 @@ 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,7 +21,6 @@ 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,21 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes
import java.util.Date
data class TestItem(val id: Long, val description: String, val expirationDate: Date)

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