mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f64480c9f | |||
| edda4731e1 | |||
| 9fa2d7d190 | |||
| c30427fd4e | |||
| 1d908f1847 | |||
| 37b076be51 | |||
| a9453a5959 | |||
| 3781ba223e | |||
| f11a1e6f82 | |||
| ffc785471b | |||
| 6e2fbfe108 | |||
| 968e037503 | |||
| f0a9f649c1 | |||
| 600d6c6fc0 | |||
| 7a55ce48a9 | |||
| 5df9fd4eff | |||
| da13a24604 | |||
| 67e88f3c20 | |||
| 24e66b63d1 | |||
| f3832c7262 | |||
| 16e99f289c | |||
| 642e554c52 | |||
| 4f6304707d | |||
| 3c112703d9 | |||
| 8c44a61033 | |||
| 5e338ef1b8 | |||
| 445da24631 |
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: CI
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 9 * * *'
|
||||
push:
|
||||
branches:
|
||||
- 6.2.x
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -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 }}
|
||||
@@ -2,7 +2,7 @@ name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.[0-9]+
|
||||
- v6.2.[0-9]+
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -54,4 +54,3 @@ atlassian-ide-plugin.xml
|
||||
cached-antora-playbook.yml
|
||||
|
||||
node_modules
|
||||
/.kotlin/
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
-139
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
-61
@@ -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);
|
||||
}
|
||||
}
|
||||
-137
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
+19
-33
@@ -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.
|
||||
=====
|
||||
|
||||
|
||||
|
||||
+1
-44
@@ -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: ⁠ is the Unicode Character 'WORD JOINER', which prevents undesired line wraps.
|
||||
|
||||
Similarly, if `names` is of type `Optional<List<String>>`, the expression
|
||||
`names?.?⁠[#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)
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
+24
-20
@@ -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
|
||||
+3
-3
@@ -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.
|
||||
|
||||
+5
-5
@@ -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
|
||||
|
||||
+8
-18
@@ -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>
|
||||
|
||||
|
||||
+3
-27
@@ -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):
|
||||
|
||||
|
||||
+3
-1
@@ -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
|
||||
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
+2
-1
@@ -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.
|
||||
|
||||
+3
-1
@@ -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"/>`.
|
||||
|
||||
+1
-1
@@ -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_
|
||||
|
||||
+37
-87
@@ -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.
|
||||
|
||||
+2
-2
@@ -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();
|
||||
});
|
||||
|
||||
-1
@@ -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 {
|
||||
|
||||
-32
@@ -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[]
|
||||
+13
-19
@@ -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[]
|
||||
|
||||
-1
@@ -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 {
|
||||
|
||||
-1
@@ -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 {
|
||||
|
||||
-21
@@ -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
Reference in New Issue
Block a user