Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Clozel 3591f1e9fb Release v7.0.2 2025-12-11 16:53:45 +01:00
685 changed files with 5536 additions and 16576 deletions
@@ -1,13 +1,6 @@
changelog:
repository: spring-projects/spring-framework
sections:
- title: ":warning: Attention Required"
labels:
- "for: upgrade-attention"
summary:
mode: "member-comment"
config:
prefix: "Attention Required:"
- title: ":star: New Features"
labels:
- "type: enhancement"
@@ -24,7 +24,7 @@ runs:
using: composite
steps:
- name: Set Up Java
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }}
java-version: |
@@ -32,7 +32,7 @@ runs:
${{ inputs.java-toolchain == 'true' && '17' || '' }}
25
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
@@ -17,14 +17,14 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
uses: jfrog/setup-jfrog-cli@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts
shell: bash
run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}'
- name: Sync
uses: spring-io/central-publish-action@0c03960e9b16fdfe70e2443e1d5393cbc3a35622 # v0.3.0
uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0
with:
token: ${{ inputs.central-token-password }}
token-name: ${{ inputs.central-token-username }}
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'liberica'
java-version: 17
@@ -13,7 +13,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -21,7 +21,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Deploy
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build
id: build
uses: ./.github/actions/build
@@ -19,7 +19,7 @@ jobs:
uses: ./.github/actions/print-jvm-thread-dumps
- name: Upload Build Reports
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: build-reports
path: '**/build/reports/'
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
git config --global core.longPaths true
Stop-Service -name Docker
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build
id: build
uses: ./.github/actions/build
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: docs-build
+5 -5
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -21,7 +21,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
@@ -57,7 +57,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
@@ -73,7 +73,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
@@ -86,7 +86,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
+5 -5
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -20,7 +20,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
@@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
@@ -72,7 +72,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
@@ -85,7 +85,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
+5 -5
View File
@@ -30,23 +30,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Release Verification Tests
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: 'v0.0.2'
repository: spring-projects/spring-framework-release-verification
token: ${{ secrets.token }}
- name: Check Out Send Notification Action
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
path: send-notification
sparse-checkout: .github/actions/send-notification
- name: Set Up Java
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'liberica'
java-version: 17
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
cache-read-only: false
- name: Configure Gradle Properties
@@ -64,7 +64,7 @@ jobs:
run: ./gradlew spring-framework-release-verification-tests:test
- name: Upload Build Reports on Failure
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: build-reports
path: '**/build/reports/'
+5 -6
View File
@@ -6,7 +6,7 @@ plugins {
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
id 'com.gradleup.shadow' version "9.2.2" apply false
id 'me.champeau.jmh' version '0.7.2' apply false
id 'io.spring.nullability' version '0.0.11' apply false
id 'io.spring.nullability' version '0.0.8' apply false
}
ext {
@@ -70,14 +70,13 @@ configure([rootProject] + javaProjects) { project ->
ext.javadocLinks = [
"https://docs.oracle.com/en/java/javase/17/docs/api/",
//"https://jakarta.ee/specifications/platform/11/apidocs/",
"https://docs.hibernate.org/orm/7.2/javadocs/",
"https://docs.hibernate.org/orm/5.6/javadocs/",
"https://www.quartz-scheduler.org/api/2.3.0/",
"https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/core/release/api/",
"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/6.0.2/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
"https://docs.junit.org/6.0.1/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
"https://jspecify.dev/docs/api/"
] as String[]
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("13.2.0");
checkstyle.setToolVersion("12.2.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -49,6 +49,7 @@ import static org.springframework.build.architecture.ArchitectureRules.classesSh
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.
@@ -63,7 +64,8 @@ public abstract class ArchitectureCheck extends DefaultTask {
public ArchitectureCheck() {
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
getProhibitObjectsRequireNonNull().convention(true);
getRules().addAll(classesShouldNotImportForbiddenTypes(),
getRules().addAll(packageInfoShouldBeNullMarked(),
classesShouldNotImportForbiddenTypes(),
javaClassesShouldNotImportKotlinAnnotations(),
allPackagesShouldBeFreeOfTangles(),
noClassesShouldCallStringToLowerCaseWithoutLocale(),
@@ -46,6 +46,13 @@ abstract class ArchitectureRules {
.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()
@@ -78,7 +78,7 @@ public class ShadowSource extends DefaultTask {
}
@OutputDirectory
public DirectoryProperty getOutputDirectory() {
DirectoryProperty getOutputDirectory() {
return this.outputDirectory;
}
+1 -2
View File
@@ -20,7 +20,6 @@ dependencies {
}
}
def springAspectsOutput = project(":spring-aspects").sourceSets.main.output
javadoc {
javadocTool.set(javaToolchains.javadocToolFor({
languageVersion = JavaLanguageVersion.of(25)
@@ -50,7 +49,7 @@ javadoc {
doFirst {
classpath += files(
// ensure the javadoc process can resolve types compiled from .aj sources
springAspectsOutput
project(":spring-aspects").sourceSets.main.output
)
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
}
+3 -5
View File
@@ -70,7 +70,7 @@ dependencies {
implementation("com.github.ben-manes.caffeine:caffeine")
implementation("com.mchange:c3p0:0.9.5.5")
implementation("com.oracle.database.jdbc:ojdbc11")
implementation("io.micrometer:context-propagation")
implementation("io.micrometer:context-propagation")
implementation("io.projectreactor.netty:reactor-netty-http")
implementation("jakarta.jms:jakarta.jms-api")
implementation("jakarta.servlet:jakarta.servlet-api")
@@ -80,14 +80,12 @@ dependencies {
implementation("javax.cache:cache-api")
implementation("org.apache.activemq:activemq-ra:6.1.2")
implementation("org.apache.commons:commons-dbcp2:2.11.0")
implementation("org.apache.groovy:groovy-templates")
implementation("org.aspectj:aspectjweaver")
implementation("org.assertj:assertj-core")
implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
implementation("org.freemarker:freemarker")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
implementation("org.junit.jupiter:junit-jupiter-api")
implementation("tools.jackson.core:jackson-databind")
implementation("tools.jackson.dataformat:jackson-dataformat-xml")
Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 63 KiB

@@ -17,7 +17,6 @@
class="st5"
id="svg5499"
version="1.1"
font-family="Helvetica, Arial, sans-serif"
inkscape:version="0.91 r13725"
sodipodi:docname="mvc-splitted-contexts.svg"
style="font-size:12px;overflow:visible;color-interpolation-filters:sRGB;fill:none;fill-rule:evenodd;stroke-linecap:square;stroke-miterlimit:3"
@@ -37,7 +36,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path9164" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -48,7 +47,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path8836" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -59,7 +58,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path8520" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -70,7 +69,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path8216" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -81,7 +80,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path7924" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -92,7 +91,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path7644" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -103,7 +102,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path7375" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -114,7 +113,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path7119" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -125,7 +124,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path6875" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -136,7 +135,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path6643" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -147,7 +146,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path6423" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -158,7 +157,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path6215" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -169,7 +168,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path6019" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -180,7 +179,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path5835" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -191,7 +190,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path5663" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -202,7 +201,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path5503" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -213,7 +212,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path5355" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -224,7 +223,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path5219" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -235,7 +234,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path5095" /></marker><marker
inkscape:isstock="true"
style="overflow:visible;"
@@ -246,7 +245,7 @@
inkscape:stockid="Arrow2Mend"><path
transform="scale(0.6) rotate(180) translate(0,0)"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
id="path4983" /></marker><marker
inkscape:stockid="Arrow2Mend"
orient="auto"
@@ -257,7 +256,7 @@
inkscape:isstock="true"
inkscape:collect="always"><path
id="path7394"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(0.6) rotate(180) translate(0,0)" /></marker><marker
inkscape:stockid="Arrow2Lend"
@@ -268,7 +267,7 @@
style="overflow:visible;"
inkscape:isstock="true"><path
id="path8123"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(1.1) rotate(180) translate(1,0)" /></marker><marker
inkscape:stockid="Arrow1Mend"
@@ -280,7 +279,7 @@
inkscape:isstock="true"><path
id="path8035"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
inkscape:stockid="Arrow1Mend"
orient="auto"
@@ -291,7 +290,7 @@
inkscape:isstock="true"><path
id="path7959"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
inkscape:stockid="Arrow1Mend"
orient="auto"
@@ -302,7 +301,7 @@
inkscape:isstock="true"><path
id="path7376"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
inkscape:stockid="Arrow2Lend"
orient="auto"
@@ -312,7 +311,7 @@
style="overflow:visible;"
inkscape:isstock="true"><path
id="path7388"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1"
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1"
d="M 8.7185878,4.0337352 L -2.2072895,0.016013256 L 8.7185884,-4.0017078 C 6.9730900,-1.6296469 6.9831476,1.6157441 8.7185878,4.0337352 z "
transform="scale(1.1) rotate(180) translate(1,0)" /></marker><marker
inkscape:stockid="Arrow1Lend"
@@ -324,7 +323,7 @@
inkscape:isstock="true"><path
id="path7370"
d="M 0.0,0.0 L 5.0,-5.0 L -12.5,0.0 L 5.0,5.0 L 0.0,0.0 z "
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
style="fill-rule:evenodd;stroke:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
transform="scale(0.8) rotate(180) translate(12.5,0)" /></marker></defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
@@ -353,9 +352,9 @@
v:viewMarkup="false" /><style
type="text/css"
id="style5501"><![CDATA[
.st1 {fill:#969696;stroke:#333333;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st2 {fill:#F8F9FA;stroke:#333333;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st3 {fill:#333333;font-family:Helvetica, Arial, sans-serif;font-size:2.50001em;font-weight:bold}
.st1 {fill:#969696;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st2 {fill:#dde2cd;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.24}
.st3 {fill:#000000;font-family:Arial;font-size:2.50001em;font-weight:bold}
.st4 {font-size:0.333333em;font-weight:normal}
.st5 {fill:none;fill-rule:evenodd;font-size:12;overflow:visible;stroke-linecap:square;stroke-miterlimit:3}
]]></style><g
@@ -364,22 +363,20 @@
v:groupContext="foregroundPage"
id="g5503"
transform="matrix(0.99998201,0,0,1.0824094,-40.812382,-98.908648)"><rect
style="fill:#F8F9FA;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#dde2cd;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6599"
width="382.68423"
height="146.09897"
x="87.884865"
y="148.26482"
rx="8" /><v:userDefs><v:ud
y="148.26482" /><v:userDefs><v:ud
v:nameU="SchemeName"
v:val="VT4(Default)" /></v:userDefs><rect
style="fill:none;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect5725"
width="464.31128"
height="374.11411"
x="41.684383"
y="112.3262"
rx="8" /><title
y="112.3262" /><title
id="title5505">Page-1</title><v:pageProperties
v:drawingScale="0.0393701"
v:pageScale="0.0393701"
@@ -388,16 +385,15 @@
v:shadowOffsetY="-8.50394" /><v:layer
v:name="Connector"
v:index="0" /><rect
style="fill:#F8F9FA;fill-opacity:1;fill-rule:evenodd;stroke:#6db33f;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#dde2cd;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1.53790233;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect5715"
width="322.8194"
height="43.63184"
x="119.95335"
y="-135.66222"
transform="scale(1,-1)"
rx="8" /><text
transform="scale(1,-1)" /><text
xml:space="preserve"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:30.7580471px;line-height:125%;font-family:sans-serif;-inkscape-font-specification:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="168.843"
y="124.32391"
id="text5717"
@@ -405,12 +401,12 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan5719"
x="172.843"
x="168.843"
y="124.32391"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333">DispatcherServlet</tspan></text>
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:sans-serif;-inkscape-font-specification:sans-serif">DispatcherServlet</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="89.770851"
y="181.20923"
id="text6589"
@@ -418,12 +414,12 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6591"
x="106.770851"
x="89.770851"
y="181.20923"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333">Servlet WebApplicationContext</tspan></text>
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:sans-serif;-inkscape-font-specification:sans-serif">Servlet WebApplicationContext</tspan></text>
<text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="260.00443"
y="198.41273"
id="text6593"
@@ -433,39 +429,36 @@
id="tspan6595"
x="260.00443"
y="198.41273"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555">(containing controllers, view resolvers,</tspan><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle">(containing controllers, view resolvers,</tspan><tspan
sodipodi:role="line"
x="260.00443"
y="212.83057"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
id="tspan6597">and other web-related beans)</tspan></text>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6620"
width="82.040657"
height="36.72575"
x="114.52653"
y="-259.43161"
transform="scale(1,-1)"
rx="4" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
transform="scale(1,-1)" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.89166164;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6622"
width="87.843979"
height="36.697304"
x="223.39864"
y="-287.19809"
transform="scale(1,-1)"
rx="4" /><rect
transform="scale(1,-1)" /><rect
transform="scale(1,-1)"
y="-264.81918"
x="117.92834"
height="36.72575"
width="82.040657"
id="rect6614"
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><text
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="121.24728"
y="260.14957"
id="text6616"
@@ -473,20 +466,20 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6618"
x="124.24728"
x="121.24728"
y="260.14957"
style="font-size:11.53426838px;fill:#333333">Controllers</tspan></text>
style="font-size:11.53426838px">Controllers</tspan></text>
<text
transform="scale(1.0403984,0.96117025)"
sodipodi:linespacing="125%"
id="text6624"
y="282.70709"
x="219.61203"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:11.53426838px;fill:#333333"
style="font-size:11.53426838px"
y="282.70709"
x="221.61203"
x="219.61203"
id="tspan6626"
sodipodi:role="line">ViewResolver</tspan></text>
<rect
@@ -496,10 +489,9 @@
height="36.577778"
width="114.4539"
id="rect6628"
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><text
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.0161339;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="327.51276"
y="255.28464"
id="text6630"
@@ -507,27 +499,26 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6632"
x="332.51276"
x="327.51276"
y="255.28464"
style="font-size:11.53426838px;fill:#333333">HandlerMapping</tspan></text>
style="font-size:11.53426838px">HandlerMapping</tspan></text>
<rect
y="338.69724"
x="87.803261"
height="121.5683"
width="382.84744"
id="rect6634"
style="fill:#F8F9FA;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="8" /><text
style="fill:#dde2cd;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
transform="scale(1.0403984,0.96117025)"
sodipodi:linespacing="125%"
id="text6636"
y="376.61673"
x="108.61351"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;fill:#333333"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:23.06853676px;font-family:sans-serif;-inkscape-font-specification:sans-serif"
y="376.61673"
x="113.61351"
x="108.61351"
id="tspan6638"
sodipodi:role="line">Root WebApplicationContext</tspan></text>
<text
@@ -536,10 +527,10 @@
id="text6640"
y="395.35812"
x="260.93863"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
id="tspan6644"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
y="395.35812"
x="260.93863"
sodipodi:role="line">(containing middle-tier services, datasources, etc.)</tspan></text>
@@ -550,47 +541,43 @@
height="36.72575"
width="82.040657"
id="rect6648"
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><rect
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6650"
width="82.040657"
height="36.72575"
x="167.73116"
y="-445.45563"
transform="scale(1,-1)"
rx="4" /><text
transform="scale(1,-1)" /><text
transform="scale(1.0403984,0.96117025)"
sodipodi:linespacing="125%"
id="text6652"
y="448.55054"
x="175.87148"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
xml:space="preserve"><tspan
style="font-size:11.53426838px;fill:#333333"
style="font-size:11.53426838px"
y="448.55054"
x="178.87148"
x="175.87148"
id="tspan6654"
sodipodi:role="line">Services</tspan></text>
<rect
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.88435173;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
id="rect6656"
width="86.393044"
height="36.704323"
x="306.86328"
y="-439.60837"
transform="scale(1,-1)"
rx="4" /><rect
transform="scale(1,-1)" /><rect
transform="scale(1,-1)"
y="-444.99475"
x="310.26624"
height="36.701977"
width="86.876686"
id="rect6658"
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
rx="4" /><text
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.88679528;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="305.30771"
y="448.55054"
id="text6660"
@@ -598,20 +585,20 @@
transform="scale(1.0403984,0.96117025)"><tspan
sodipodi:role="line"
id="tspan6662"
x="308.30771"
x="305.30771"
y="448.55054"
style="font-size:11.53426838px;fill:#333333">Repositories</tspan></text>
style="font-size:11.53426838px">Repositories</tspan></text>
<path
style="fill:none;fill-rule:evenodd;stroke:#333333;stroke-width:0.76895118px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.76895118px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="m 265.33234,295.60379 c 0,42.65169 0,42.65169 0,0 z"
id="path7643"
inkscape:connector-curvature="0" /><path
style="fill:#333333;fill-opacity:1;fill-rule:evenodd;stroke:#333333;stroke-width:3.46028023;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
style="fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:3.46028023;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow2Mend)"
d="m 270.43505,294.70585 c 0,39.4721 0,39.87903 0,39.87903"
id="path7645"
inkscape:connector-curvature="0" /><text
xml:space="preserve"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Helvetica, Arial, sans-serif;letter-spacing:0px;word-spacing:0px;fill:#333333;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
style="font-style:normal;font-weight:normal;font-size:30.7580471px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
x="352.55331"
y="333.03622"
id="text4963"
@@ -620,6 +607,6 @@
sodipodi:role="line"
x="352.55331"
y="333.03622"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.53426838px;font-family:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
id="tspan4965">Delegates if no bean found</tspan></text>
</g></svg>

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 30 KiB

+1 -1
View File
@@ -261,6 +261,7 @@
**** xref:web/websocket/stomp/configuration-performance.adoc[]
**** xref:web/websocket/stomp/stats.adoc[]
**** xref:web/websocket/stomp/testing.adoc[]
** xref:web/integration.adoc[]
* xref:web-reactive.adoc[]
** xref:web/webflux.adoc[]
*** xref:web/webflux/new-framework.adoc[]
@@ -343,7 +344,6 @@
**** xref:testing/testcontext-framework/ctx-management/web.adoc[]
**** xref:testing/testcontext-framework/ctx-management/web-mocks.adoc[]
**** xref:testing/testcontext-framework/ctx-management/caching.adoc[]
**** xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[]
**** xref:testing/testcontext-framework/ctx-management/failure-threshold.adoc[]
**** xref:testing/testcontext-framework/ctx-management/hierarchies.adoc[]
*** xref:testing/testcontext-framework/fixture-di.adoc[]
@@ -124,10 +124,6 @@ on a test class. See xref:testing/annotations/integration-junit-jupiter.adoc#int
| The maximum size of the context cache in the _Spring TestContext Framework_. See
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching].
| `spring.test.context.cache.pause`
| The pause mode for the context cache in the _Spring TestContext Framework_. See
xref:testing/testcontext-framework/ctx-management/context-pausing.adoc[Context Pausing].
| `spring.test.context.failure.threshold`
| The failure threshold for errors encountered while attempting to load an `ApplicationContext`
in the _Spring TestContext Framework_. See
@@ -727,7 +727,7 @@ of determining parameter names, an exception will be thrown.
parameter names. This discoverer is only used if such APIs are present on the classpath.
`StandardReflectionParameterNameDiscoverer` :: Uses the standard `java.lang.reflect.Parameter`
API to determine parameter names. Requires that code be compiled with the `-parameters`
flag for `javac`. Recommended approach.
flag for `javac`. Recommended approach on Java 8+.
`AspectJAdviceParameterNameDiscoverer` :: Deduces parameter names from the pointcut
expression, `returning`, and `throwing` clauses. See the
{spring-framework-api}/aop/aspectj/AspectJAdviceParameterNameDiscoverer.html[javadoc]
@@ -104,7 +104,7 @@ Note that pointcut definitions are generally matched against any intercepted met
If a pointcut is strictly meant to be public-only, even in a CGLIB proxy scenario with
potential non-public interactions through proxies, it needs to be defined accordingly.
If your interception needs to include method calls or even constructors within the target
If your interception needs include method calls or even constructors within the target
class, consider the use of Spring-driven xref:core/aop/using-aspectj.adoc#aop-aj-ltw[native AspectJ weaving] instead
of Spring's proxy-based AOP framework. This constitutes a different mode of AOP usage
with different characteristics, so be sure to make yourself familiar with weaving
@@ -19,6 +19,10 @@ its behavior changes.
| Initialization of `SmartInitializingSingleton` beans.
| `beanName` the name of the bean.
| `spring.context.annotated-bean-reader.create`
| Creation of the `AnnotatedBeanDefinitionReader`.
|
| `spring.context.base-packages.scan`
| Scanning of base packages.
| `packages` array of base packages for scanning.
@@ -1,17 +1,16 @@
[[beans-standard-annotations]]
= Using JSR-330 Standard Annotations
= Using JSR 330 Standard Annotations
Spring offers support for JSR-330 standard _Dependency Injection_ annotations which are
available in the `jakarta.inject` package. These annotations may optionally be used as
alternatives to Spring annotations.
To use them, you need to have the relevant jar in your classpath. For example, the
`jakarta.inject` artifact is available in the standard Maven repository
(`https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/`),
Spring offers support for JSR-330 standard annotations (Dependency Injection). Those
annotations are scanned in the same way as the Spring annotations. To use them, you need
to have the relevant jars in your classpath.
[NOTE]
=====
If you use Maven, you can add the following dependency to your `pom.xml` file.
If you use Maven, the `jakarta.inject` artifact is available in the standard Maven
repository (
https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/[https://repo.maven.apache.org/maven2/jakarta/inject/jakarta.inject-api/2.0.0/]).
You can add the following dependency to your file pom.xml:
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -27,14 +26,13 @@ If you use Maven, you can add the following dependency to your `pom.xml` file.
[[beans-inject-named]]
== Dependency Injection with `@Inject` and `@Named`
Instead of using `@Autowired` for dependency injection, you may optionally choose to use
`@jakarta.inject.Inject` as follows.
Instead of `@Autowired`, you can use `@jakarta.inject.Inject` as follows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject;
@@ -56,7 +54,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject
@@ -74,19 +72,17 @@ Kotlin::
----
======
As with `@Autowired`, you can use `@Inject` at the field level, method level, and
constructor-argument level.
Furthermore, as an alternative to Spring's `ObjectProvider` mechanism, you may choose to
declare your injection point as a `jakarta.inject.Provider`, allowing for on-demand
access to beans of shorter scopes or lazy access to other beans through a
`Provider.get()` call. The following example offers a variant of the preceding example.
As with `@Autowired`, you can use `@Inject` at the field level, method level
and constructor-argument level. Furthermore, you may declare your injection point as a
`Provider`, allowing for on-demand access to beans of shorter scopes or lazy access to
other beans through a `Provider.get()` call. The following example offers a variant of the
preceding example:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject;
import jakarta.inject.Provider;
@@ -109,10 +105,9 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject
import jakarta.inject.Provider
class SimpleMovieLister {
@@ -128,15 +123,14 @@ Kotlin::
----
======
If you would like to use a qualified name for the dependency that should be injected, you
may choose to use the `@Named` annotation as an alternative to Spring's `@Qualifier`
support, as the following example shows.
If you would like to use a qualified name for the dependency that should be injected,
you should use the `@Named` annotation, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -156,7 +150,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject
import jakarta.inject.Named
@@ -176,15 +170,12 @@ Kotlin::
======
As with `@Autowired`, `@Inject` can also be used with `java.util.Optional` or
`@Nullable`. This is even more applicable here, since `@Inject` does not have a
`required` attribute. The following examples show how to use `@Inject` with `Optional`,
`@Nullable`, and Kotlin's built-in support for nullable types.
`@Nullable`. This is even more applicable here, since `@Inject` does not have
a `required` attribute. The following pair of examples show how to use `@Inject` and
`@Nullable`:
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject;
import java.util.Optional;
public class SimpleMovieLister {
@Inject
@@ -198,11 +189,8 @@ As with `@Autowired`, `@Inject` can also be used with `java.util.Optional` or
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject;
import org.jspecify.annotations.Nullable;
public class SimpleMovieLister {
@Inject
@@ -214,10 +202,8 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject
class SimpleMovieLister {
@Inject
@@ -228,21 +214,21 @@ Kotlin::
[[beans-named]]
== `@Named`: Standard Equivalent to the `@Component` Annotation
== `@Named` and `@ManagedBean`: Standard Equivalents to the `@Component` Annotation
Instead of `@Component` or other Spring stereotype annotations, you may optionally choose
to use `@jakarta.inject.Named`, as the following example shows.
Instead of `@Component`, you can use `@jakarta.inject.Named` or `jakarta.annotation.ManagedBean`,
as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject;
import jakarta.inject.Named;
@Named("movieListener")
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
public class SimpleMovieLister {
private MovieFinder movieFinder;
@@ -258,12 +244,12 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject
import jakarta.inject.Named
@Named("movieListener")
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
class SimpleMovieLister {
@Inject
@@ -274,15 +260,14 @@ Kotlin::
----
======
It is very common to use `@Component` or other Spring stereotype annotations without
specifying an explicit name for the component, and `@Named` can be used in a similar
fashion, as the following example shows.
It is very common to use `@Component` without specifying a name for the component.
`@Named` can be used in a similar fashion, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject;
import jakarta.inject.Named;
@@ -303,7 +288,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import jakarta.inject.Inject
import jakarta.inject.Named
@@ -319,14 +304,14 @@ Kotlin::
----
======
When you use `@Named`, you can use component scanning in the exact same way as when you
use Spring annotations, as the following example shows.
When you use `@Named` or `@ManagedBean`, you can use component scanning in the
exact same way as when you use Spring annotations, as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@ComponentScan(basePackages = "org.example")
@@ -337,7 +322,7 @@ Java::
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@ComponentScan(basePackages = ["org.example"])
@@ -347,106 +332,55 @@ Kotlin::
----
======
NOTE: In contrast to `@Component`, the JSR-330 `@Named` annotation is not composable. You
should use Spring's stereotype model for building custom component annotations.
[TIP]
====
If you work with legacy systems that still use `@javax.inject.Named` or
`@javax.annotation.ManagedBean` for components (note the `javax` package namespace), you
can explicitly configure component scanning to include those annotation types, as shown
in the following example.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
----
@Configuration
@ComponentScan(
basePackages = "org.example",
includeFilters = @Filter({
javax.inject.Named.class,
javax.annotation.ManagedBean.class
})
)
public class AppConfig {
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
----
@Configuration
@ComponentScan(
basePackages = ["org.example"],
includeFilters = [Filter([
javax.inject.Named::class,
javax.annotation.ManagedBean::class
])]
)
class AppConfig {
// ...
}
----
======
In addition, if you would like for the `value` attributes in `@javax.inject.Named` and
`@javax.annotation.ManagedBean` to be used as component names, you need to override the
`isStereotypeWithNameValue(...)` method in `AnnotationBeanNameGenerator` to add explicit
support for `javax.annotation.ManagedBean` and `javax.inject.Named` and register your
custom `AnnotationBeanNameGenerator` via the `nameGenerator` attribute in
`@ComponentScan`.
====
NOTE: In contrast to `@Component`, the JSR-330 `@Named` and the JSR-250 `@ManagedBean`
annotations are not composable. You should use Spring's stereotype model for building
custom component annotations.
[[beans-standard-annotations-limitations]]
== Limitations of JSR-330 Standard Annotations
When you work with JSR-330 standard annotations, you should know that some significant
features are not available, as the following table shows.
When you work with standard annotations, you should know that some significant
features are not available, as the following table shows:
[[annotations-comparison]]
.Spring component model versus JSR-330 variants
.Spring component model elements versus JSR-330 variants
|===
| Spring | JSR-330 | JSR-330 restrictions / comments
| Spring| jakarta.inject.*| jakarta.inject restrictions / comments
| `@Autowired`
| `@Inject`
| `@Inject` has no `required` attribute. Can be used with Java's `Optional` instead.
| @Autowired
| @Inject
| `@Inject` has no 'required' attribute. Can be used with Java 8's `Optional` instead.
| `@Component`
| `@Named`
| @Component
| @Named / @ManagedBean
| JSR-330 does not provide a composable model, only a way to identify named components.
| `@Scope("singleton")`
| `@Singleton`
| @Scope("singleton")
| @Singleton
| The JSR-330 default scope is like Spring's `prototype`. However, in order to keep it
consistent with Spring's general defaults, a JSR-330 bean declared in the Spring
container is a `singleton` by default. In order to use a scope other than `singleton`,
you should use Spring's `@Scope` annotation. `jakarta.inject` also provides a
`jakarta.inject.Scope` annotation; however, this one is only intended to be used
`jakarta.inject.Scope` annotation: however, this one is only intended to be used
for creating custom annotations.
| `@Qualifier`
| `@Qualifier` / `@Named`
| @Qualifier
| @Qualifier / @Named
| `jakarta.inject.Qualifier` is just a meta-annotation for building custom qualifiers.
Concrete `String` qualifiers (like Spring's `@Qualifier` with a value) can be associated
through `jakarta.inject.Named`.
| `@Value`
| @Value
| -
| no equivalent
| `@Lazy`
| @Lazy
| -
| no equivalent
| `ObjectFactory`
| `Provider`
| ObjectFactory
| Provider
| `jakarta.inject.Provider` is a direct alternative to Spring's `ObjectFactory`,
only with a shorter `get()` method name. It can also be used in combination with
Spring's `@Autowired` or with non-annotated constructors and setter methods.
@@ -531,7 +531,6 @@ following kinds of expressions cannot be compiled.
* Expressions relying on the conversion service
* Expressions using custom resolvers
* Expressions using overloaded operators
* Expressions using `Optional` with the null-safe or Elvis operator
* Expressions using array construction syntax
* Expressions using selection or projection
* Expressions using bean references
@@ -6,7 +6,7 @@ are set by using the `setVariable()` method in `EvaluationContext` implementatio
[NOTE]
====
Variable names must begin with a letter (as defined below), an underscore, or a dollar
Variable names must be begin with a letter (as defined below), an underscore, or a dollar
sign.
Variable names must be composed of one or more of the following supported types of
@@ -15,7 +15,7 @@ configuration options. You should instantiate the `SimpleJdbcInsert` in the data
layer's initialization method. For this example, the initializing method is the
`setDataSource` method. You do not need to subclass the `SimpleJdbcInsert` class. Instead,
you can create a new instance and set the table name by using the `withTableName` method.
Configuration methods for this class follow the `fluent` style that returns the instance
Configuration methods for this class follow the `fluid` style that returns the instance
of the `SimpleJdbcInsert`, which lets you chain all configuration methods. The following
example uses only one configuration method (we show examples of multiple methods later):
@@ -349,11 +349,11 @@ parameters return the data read from the table.
You can declare `SimpleJdbcCall` in a manner similar to declaring `SimpleJdbcInsert`. You
should instantiate and configure the class in the initialization method of your data-access
layer. In contrast to the `StoredProcedure` class, you do not need to create a subclass,
and you do not need to declare parameters that can be looked up in the database metadata.
The following `SimpleJdbcCall` configuration example uses the preceding stored procedure.
The only configuration option (other than the `DataSource`) is the name of the stored
procedure.
layer. Compared to the `StoredProcedure` class, you need not create a subclass
and you need not to declare parameters that can be looked up in the database metadata.
The following example of a `SimpleJdbcCall` configuration uses the preceding stored
procedure (the only configuration option, in addition to the `DataSource`, is the name
of the stored procedure):
[tabs]
======
@@ -14,7 +14,7 @@ As of Spring Framework 7.0, Spring requires Hibernate ORM 7.x for Spring's
`HibernateJpaVendorAdapter`.
The `org.springframework.orm.jpa.hibernate` package supersedes the former `orm.hibernate5`:
now for use with Hibernate ORM 7.x, tightly integrated with `HibernateJpaVendorAdapter`
now for use with Hibernate ORM 7.1+, tightly integrated with `HibernateJpaVendorAdapter`
as well as supporting Hibernate's native `SessionFactory.getCurrentSession()` style.
====
@@ -3,22 +3,21 @@
: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 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)].
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
latter if available in the JVM version you are using (Java 24+).
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 a hook-point
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 the AOT cache and CDS.
which is designed to fulfill the class loading requirements of both AOT cache and CDS.
== Creating the cache
@@ -30,22 +29,14 @@ invoked; but the lifecycle has not started, and the `ContextRefreshedEvent` has
been published.
To create the cache during the training run, it is possible to specify the `-Dspring.context.exit=onRefresh`
JVM flag to start and then exit your Spring application once the
JVM flag to start then exit your Spring application once the
`ApplicationContext` has refreshed:
--
[tabs]
======
AOT cache (Java 25+)::
+
[source,bash,subs="verbatim,quotes"]
----
java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar application.jar ...
----
AOT cache (Java 24)::
AOT cache::
+
[source,bash,subs="verbatim,quotes"]
----
@@ -64,12 +55,6 @@ java -XX:ArchiveClassesAtExit=app.jsa -Dspring.context.exit=onRefresh ...
======
--
NOTE: With Java 25+, AOT cache stores, among other things, the
https://openjdk.org/jeps/515[method profiling information]. Therefore, to benefit of this capability,
it is recommended to create an AOT cache for an application that experienced a portion of a
production-like workflow instead of using the `-Dspring.context.exit=onRefresh` flag which designed to
optimize only the startup of your application.
== Using the cache
Once the cache file has been created, you can use it to start your application faster:
@@ -97,7 +82,7 @@ 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 an `aot-cache.log` with
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:
@@ -110,11 +95,11 @@ a "shared objects file" source, as shown in the following example:
[0.151s][info][class,load] org.springframework.context.MessageSource source: shared objects file
----
If the AOT cache cannot be enabled or if you have a large number of classes that are not loaded from
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 as the one used to create it, in the same order.
Additional JARs or directories can be specified *at the end* (but will not be cached).
- When using the cache, the classpath must be the same than the one used to create it, in the same order.
Additional JARs or directories can be specified *at the end* (but won't be cached).
@@ -1,11 +1,16 @@
[[appendix]]
= Appendix
[[appendix.xsd-schemas]]
== XML Schemas
This part of the appendix lists XML schemas related to integration technologies.
[[appendix.xsd-schemas-jee]]
=== The `jee` Schema
@@ -167,7 +172,7 @@ different properties with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -179,7 +184,7 @@ without `jee`:
</bean>
----
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -195,7 +200,7 @@ with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
and a number of properties without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -210,7 +215,7 @@ and a number of properties without `jee`:
</bean>
----
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
and a number of properties with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -229,7 +234,7 @@ and a number of properties with `jee`:
The `<jee:remote-slsb/>` element configures a reference to a `remote` EJB Stateless Session Bean.
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -246,7 +251,7 @@ without `jee`:
</bean>
----
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -308,7 +313,7 @@ xref:integration/jmx/naming.adoc#jmx-context-mbeanexport[Configuring Annotation-
=== The `cache` Schema
You can use the `cache` elements to enable support for Spring's `@CacheEvict`, `@CachePut`,
and `@Caching` annotations. The `cache` schema also supports declarative XML-based caching. See
and `@Caching` annotations. It it also supports declarative XML-based caching. See
xref:integration/cache/annotations.adoc#cache-annotation-enable[Enabling Caching Annotations] and
xref:integration/cache/declarative-xml.adoc[Declarative XML-based Caching] for details.
@@ -35,7 +35,7 @@ xref:integration/cache/store-configuration.adoc#cache-store-configuration-jsr107
[[cache-store-configuration-caffeine]]
== Caffeine Cache
Caffeine is a rewrite of Guava's cache, and its implementation is located in the
Caffeine is a Java 8 rewrite of Guava's cache, and its implementation is located in the
`org.springframework.cache.caffeine` package and provides access to several features
of Caffeine.
@@ -26,7 +26,7 @@ behind the scenes for each annotated method, by using a `JmsListenerContainerFac
Such a container is not registered against the application context but can be easily
located for management purposes by using the `JmsListenerEndpointRegistry` bean.
TIP: `@JmsListener` is a repeatable annotation, so you can associate
TIP: `@JmsListener` is a repeatable annotation on Java 8, so you can associate
several JMS destinations with the same method by adding additional `@JmsListener`
declarations to it.
@@ -466,7 +466,7 @@ synchronous, asynchronous, and streaming scenarios.
* Non-blocking I/O
* Reactive Streams back pressure
* High concurrency with fewer hardware resources
* Functional-style, fluent API that takes advantage of lambda expressions
* Functional-style, fluent API that takes advantage of Java 8 lambdas
* Synchronous and asynchronous interactions
* Streaming up to or streaming down from a server
@@ -533,7 +533,8 @@ that returns a value:
----
TIP: `@Async` methods may not only declare a regular `java.util.concurrent.Future` return
type but also `java.util.concurrent.CompletableFuture`, for richer interaction with
type but also Spring's `org.springframework.util.concurrent.ListenableFuture` or, as of
Spring 4.2, JDK 8's `java.util.concurrent.CompletableFuture`, for richer interaction with
the asynchronous task and for immediate composition with further processing steps.
You can not use `@Async` in conjunction with lifecycle callbacks such as `@PostConstruct`.
@@ -3,16 +3,14 @@
:page-section-summary-toc: 1
The Spring Framework supports various Kotlin constructs, such as instantiating Kotlin classes
through primary constructors, data binding for immutable classes, and optional parameters
with default values for functions.
through primary constructors, immutable classes data binding, and function optional parameters
with default values.
Kotlin parameter names are recognized through a dedicated `KotlinReflectionParameterNameDiscoverer`,
which allows finding interface method parameter names without requiring the Java `-parameters`
compiler flag to be enabled during compilation.
TIP: For completeness, we nevertheless recommend running the Kotlin compiler with its
`-java-parameters` flag for standard Java parameter exposure.
which allows finding interface method parameter names without requiring the Java 8 `-parameters`
compiler flag to be enabled during compilation. (For completeness, we nevertheless recommend
running the Kotlin compiler with its `-java-parameters` flag for standard Java parameter exposure.)
You can declare configuration classes as
{kotlin-docs}/nested-classes.html[top level or nested but not inner],
since the latter requires a reference to the outer class.
since the later requires a reference to the outer class.
@@ -1,8 +1,8 @@
[[coroutines]]
= Coroutines
Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are instances of
suspendable computations allowing to write non-blocking code in an imperative way. On language side,
Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are Kotlin
lightweight threads allowing to write non-blocking code in an imperative way. On language side,
suspending functions provides an abstraction for asynchronous operations while on library side
{kotlin-github-org}/kotlinx.coroutines[kotlinx.coroutines] provides functions like
{kotlin-coroutines-api}/kotlinx-coroutines-core/kotlinx.coroutines/async.html[`async { }`]
@@ -5,7 +5,6 @@ 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/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitconfig[`@SpringJUnitConfig`]
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-junit-jupiter-springjunitwebconfig[`@SpringJUnitWebConfig`]
* xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]
@@ -15,47 +14,6 @@ and JUnit Jupiter (that is, the programming model in JUnit):
* xref:testing/annotations/integration-spring/annotation-disabledinaotmode.adoc[`@DisabledInAotMode`]
[[integration-testing-annotations-springextensionconfig]]
== `@SpringExtensionConfig`
`@SpringExtensionConfig` is a type-level annotation that can be used to configure the
behavior of the `SpringExtension`.
As of Spring Framework 7.0, the `SpringExtension` is configured to use a test-method
scoped `ExtensionContext`, which enables consistent dependency injection into fields and
constructors from the `ApplicationContext` for the current test method in a `@Nested`
test class hierarchy. However, if a third-party `TestExecutionListener` is not compatible
with the semantics associated with a test-method scoped extension context — or if a
developer wishes to switch to test-class scoped semantics — the `SpringExtension` can be
configured to use a test-class scoped `ExtensionContext` by annotating a top-level test
class with `@SpringExtensionConfig(useTestClassScopedExtensionContext = true)`.
[TIP]
====
If your top-level test class is configured to use JUnit Jupiter's
`@TestInstance(Lifecycle.PER_CLASS)` semantics, the `SpringExtension` will always use a
test-class scoped `ExtensionContext`, and there is no need to declare
`@SpringExtensionConfig(useTestClassScopedExtensionContext = true)`.
====
[NOTE]
====
This annotation is currently only applicable to `@Nested` test class hierarchies and
should be applied to the top-level enclosing class of a `@Nested` test class hierarchy.
Consequently, there is no need to declare this annotation on a test class that does not
contain `@Nested` test classes.
====
[NOTE]
====
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[`@NestedTestConfiguration`]
does not apply to this annotation.
`@SpringExtensionConfig` will always be detected within a `@Nested` test class hierarchy,
effectively disregarding any `@NestedTestConfiguration(OVERRIDE)` declarations.
====
[[integration-testing-annotations-junit-jupiter-springjunitconfig]]
== `@SpringJUnitConfig`
@@ -229,7 +187,7 @@ default mode may be set via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
The default mode may also be configured as a
https://docs.junit.org/current/running-tests/configuration-parameters.html[JUnit Platform configuration parameter].
https://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
If the `spring.test.constructor.autowire.mode` property is not set, test class
constructors will not be automatically autowired.
@@ -4,7 +4,8 @@
`@AfterTransaction` indicates that the annotated `void` method should be run after a
transaction is ended, for test methods that have been configured to run within a
transaction by using Spring's `@Transactional` annotation. `@AfterTransaction` methods
are not required to be `public` and may be declared on interface default methods.
are not required to be `public` and may be declared on Java 8-based interface default
methods.
[tabs]
======
@@ -4,7 +4,8 @@
`@BeforeTransaction` indicates that the annotated `void` method should be run before a
transaction is started, for test methods that have been configured to run within a
transaction by using Spring's `@Transactional` annotation. `@BeforeTransaction` methods
are not required to be `public` and may be declared on interface default methods.
are not required to be `public` and may be declared on Java 8-based interface default
methods.
The following example shows how to use the `@BeforeTransaction` annotation:
@@ -3,7 +3,7 @@
`@SqlGroup` is a container annotation that aggregates several `@Sql` annotations. You can
use `@SqlGroup` natively to declare several nested `@Sql` annotations, or you can use it
in conjunction with Java's support for repeatable annotations, where `@Sql` can be
in conjunction with Java 8's support for repeatable annotations, where `@Sql` can be
declared several times on the same class or method, implicitly generating this container
annotation. The following example shows how to declare an SQL group:
@@ -9,6 +9,7 @@ and can be used anywhere in the Spring Framework.
* `@Qualifier`
* `@Value`
* `@Resource` (jakarta.annotation) if JSR-250 is present
* `@ManagedBean` (jakarta.annotation) if JSR-250 is present
* `@Inject` (jakarta.inject) if JSR-330 is present
* `@Named` (jakarta.inject) if JSR-330 is present
* `@PersistenceContext` (jakarta.persistence) if JPA is present
@@ -11,7 +11,7 @@ https://testng.org/[TestNG] ::
testing, distributed testing, and other features. Supported in the
xref:testing/testcontext-framework.adoc[Spring TestContext Framework].
{assertj-docs}[AssertJ] ::
"Fluent assertions for Java", including support for lambda expressions, streams, and
"Fluent assertions for Java", including support for Java 8 lambdas, streams, and
numerous other features. Supported in Spring's
xref:testing/mockmvc/assertj.adoc[MockMvc testing support].
https://en.wikipedia.org/wiki/Mock_Object[Mock Objects] ::
@@ -10,7 +10,7 @@ applications without a running server via MockMvc.
[[resttestclient.setup]]
[[resttestclient-setup]]
== Setup
To set up a `RestTestClient` you need to choose a server setup to bind to. This can be one
@@ -18,7 +18,7 @@ of several MockMvc setup choices, or a connection to a live server.
[[resttestclient.controller-config]]
[[resttestclient-controller-config]]
=== Bind to Controller
This setup allows you to test specific controller(s) via mock request and response objects,
@@ -42,17 +42,55 @@ Kotlin::
----
======
[[resttestclient.context-config]]
[[resttestclient-context-config]]
=== Bind to `ApplicationContext`
This setup allows you to load Spring configuration with Spring MVC
infrastructure and controller declarations and use it to handle requests via mock request
and response objects, without a running server.
include-code::./RestClientContextTests[indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(WebConfig.class) // <1>
class MyTests {
RestTestClient client;
[[resttestclient.fn-config]]
@BeforeEach
void setUp(ApplicationContext context) { // <2>
client = RestTestClient.bindToApplicationContext(context).build(); // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `RestTestClient`
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@SpringJUnitConfig(WebConfig::class) // <1>
class MyTests {
lateinit var client: RestTestClient
@BeforeEach
fun setUp(context: ApplicationContext) { // <2>
client = RestTestClient.bindToApplicationContext(context).build() // <3>
}
}
----
<1> Specify the configuration to load
<2> Inject the configuration
<3> Create the `RestTestClient`
======
[[resttestclient-fn-config]]
=== Bind to Router Function
This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via
@@ -77,7 +115,7 @@ Kotlin::
----
======
[[resttestclient.server-config]]
[[resttestclient-server-config]]
=== Bind to Server
This setup connects to a running server to perform full, end-to-end HTTP tests:
@@ -101,7 +139,7 @@ Kotlin::
[[resttestclient.client-config]]
[[resttestclient-client-config]]
=== Client Config
In addition to the server setup options described earlier, you can also configure client
@@ -132,7 +170,7 @@ Kotlin::
[[resttestclient.tests]]
[[resttestclient-tests]]
== Writing Tests
xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and `RestTestClient` have
@@ -144,22 +182,70 @@ provides two alternative ways to verify the response:
[[resttestclient.workflow]]
[[resttestclient-workflow]]
=== Built-in Assertions
To use the built-in assertions, remain in the workflow after the call to `exchange()`, and
use one of the expectation methods. For example:
include-code::./RestClientWorkflowTests[tag=test,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
----
======
If you would like for all expectations to be asserted even if one of them fails, you can
use `expectAll(..)` instead of multiple chained `expect*(..)` calls. This feature is
similar to the _soft assertions_ support in AssertJ and the `assertAll()` support in
JUnit Jupiter.
include-code::./RestClientWorkflowTests[tag=soft-assertions,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
{ spec -> spec.expectStatus().isOk() },
{ spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON) }
)
----
======
You can then choose to decode the response body through one of the following:
@@ -170,34 +256,124 @@ You can then choose to decode the response body through one of the following:
If the built-in assertions are insufficient, you can consume the object instead and
perform any other assertions:
include-code::./RestClientWorkflowTests[tag=consume,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (for example, AssertJ)...
});
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody<Person>()
.consumeWith {
// custom assertions (for example, AssertJ)...
}
----
======
Or you can exit the workflow and obtain a `EntityExchangeResult`:
include-code::./RestClientWorkflowTests[tag=result,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk
.expectBody<Person>()
.returnResult()
----
======
TIP: When you need to decode to a target type with generics, look for the overloaded methods
that accept {spring-framework-api}/core/ParameterizedTypeReference.html[`ParameterizedTypeReference`]
instead of `Class<T>`.
[[resttestclient.no-content]]
[[resttestclient-no-content]]
==== No Content
If the response is not expected to have content, you can assert that as follows:
include-code::./NoContentTests[tag=emptyBody,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty()
----
======
If you want to ignore the response content, the following releases the content without any assertions:
include-code::./NoContentTests[tag=ignoreBody,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound()
.expectBody(Void.class);
----
NOTE: Consuming the response body (for example, with `expectBody`) is required if your tests are running with
leak detection for pooled buffers. Without that, the tool will report buffers being leaked.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/123")
.exchange()
.expectStatus().isNotFound
.expectBody<Unit>()
----
======
[[resttestclient.json]]
[[resttestclient-json]]
==== JSON Content
You can use `expectBody()` without a target type to perform assertions on the raw
@@ -205,25 +381,126 @@ content rather than through higher level Object(s).
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]:
include-code::./JsonTests[tag=jsonBody,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}")
----
======
To verify JSON content with https://github.com/jayway/JsonPath[JSONPath]:
include-code::./JsonTests[tag=jsonPath,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason")
----
======
[[resttestclient.assertj]]
[[resttestclient-assertj]]
=== AssertJ Integration
`RestTestClientResponse` is the main entry point for the AssertJ integration.
It is an `AssertProvider` that wraps the `ResponseSpec` of an exchange in order to enable
use of `assertThat()` statements. For example:
include-code::./AssertJTests[tag=withSpec,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
ResponseSpec spec = client.get().uri("/persons").exchange();
RestTestClientResponse response = RestTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// ...
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val spec = client.get().uri("/persons").exchange()
val response = RestTestClientResponse.from(spec)
assertThat(response).hasStatusOk()
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN)
// ...
----
======
You can also use the built-in workflow first, and then obtain an `ExchangeResult` to wrap
and continue with AssertJ. For example:
include-code::./AssertJTests[tag=withResult,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
ExchangeResult result = client.get().uri("/persons").exchange()
. // ...
.returnResult();
RestTestClientResponse response = RestTestClientResponse.from(result);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// ...
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = client.get().uri("/persons").exchange()
. // ...
.returnResult()
val response = RestTestClientResponse.from(spec)
assertThat(response).hasStatusOk()
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN)
// ...
----
======
@@ -62,6 +62,17 @@ 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
_paused_ 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. Note, however,
that `SmartLifecycle` components can opt out of pausing by returning `false` from
`SmartLifecycle#isPauseable()`.
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
@@ -1,45 +0,0 @@
[[testcontext-ctx-management-pausing]]
= Context Pausing
As of Spring Framework 7.0, an `ApplicationContext` stored in the context cache (see
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching]) may be
_paused_ 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. Note, however,
that `SmartLifecycle` components can opt out of pausing by returning `false` from
`SmartLifecycle#isPauseable()`.
You can control whether inactive application contexts should be paused by setting the
`PauseMode` to one of the following supported values.
`ALWAYS` :: Always pause inactive application contexts.
`ON_CONTEXT_SWITCH` :: Only pause inactive application contexts if the next context
retrieved from the context cache is a different context.
`NEVER` :: Never pause inactive application contexts, effectively disabling the pausing
feature of the context cache.
The `PauseMode` defaults to `ON_CONTEXT_SWITCH`, but it can be changed from the command
line or a build script by setting a JVM system property named
`spring.test.context.cache.pause` to one of the supported values (case insensitive). As
an alternative, you can set the property via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
For example, if you want inactive application contexts to always be paused, you can
switch from the default `ON_CONTEXT_SWITCH` mode to `ALWAYS` by setting the
`spring.test.context.cache.pause` system property to `always`.
```shell
-Dspring.test.context.cache.pause=always
```
Similarly, if you encounter issues with `Lifecycle` components that cannot or should not
opt out of pausing, or if you discover that your test suite runs more slowly due to the
pausing and restarting of application contexts, you can disable the pausing feature by
setting the `spring.test.context.cache.pause` system property to `never`.
```shell
-Dspring.test.context.cache.pause=never
```
@@ -25,7 +25,7 @@ TestNG:
* Dependency injection for test constructors, test methods, and test lifecycle callback
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
Injection with the `SpringExtension`] for further details.
* Powerful support for link:https://docs.junit.org/current/extensions/conditional-test-execution.html[conditional
* Powerful support for link:https://docs.junit.org/current/user-guide/#extensions-conditions[conditional
test execution] based on SpEL expressions, environment variables, system properties,
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
@@ -163,7 +163,7 @@ for further details.
=== Dependency Injection with the `SpringExtension`
The `SpringExtension` implements the
link:https://docs.junit.org/current/extensions/parameter-resolution.html[`ParameterResolver`]
link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
constructors, test methods, and test lifecycle callback methods.
@@ -392,17 +392,6 @@ any of its subclasses and nested classes. Thus, you may annotate a top-level tes
with `@NestedTestConfiguration`, and that will apply to all of its nested test classes
recursively.
[NOTE]
====
As of Spring Framework 7.0, the `SpringExtension` uses a test-method scoped
`ExtensionContext` within `@Nested` test class hierarchies by default. However, the
`SpringExtension` can be configured to use a test-class scoped `ExtensionContext`.
See the documentation for
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-springextensionconfig[`@SpringExtensionConfig`]
for details.
====
[TIP]
====
If you are developing a component that integrates with the Spring TestContext Framework
@@ -0,0 +1,186 @@
[[web-integration]]
= Other Web Frameworks
This chapter details Spring's integration with third-party web frameworks.
One of the core value propositions of the Spring Framework is that of enabling
_choice_. In a general sense, Spring does not force you to use or buy into any
particular architecture, technology, or methodology (although it certainly recommends
some over others). This freedom to pick and choose the architecture, technology, or
methodology that is most relevant to a developer and their development team is
arguably most evident in the web area, where Spring provides its own web frameworks
(xref:web/webmvc.adoc#mvc[Spring MVC] and xref:web/webflux.adoc#webflux[Spring WebFlux])
while, at the same time, supporting integration with a number of popular third-party
web frameworks.
[[web-integration-common]]
== Common Configuration
Before diving into the integration specifics of each supported web framework, let us
first take a look at common Spring configuration that is not specific to any one web
framework. (This section is equally applicable to Spring's own web framework variants.)
One of the concepts (for want of a better word) espoused by Spring's lightweight
application model is that of a layered architecture. Remember that in a "classic"
layered architecture, the web layer is but one of many layers. It serves as one of the
entry points into a server-side application, and it delegates to service objects
(facades) that are defined in a service layer to satisfy business-specific (and
presentation-technology agnostic) use cases. In Spring, these service objects, any other
business-specific objects, data-access objects, and others exist in a distinct "business
context", which contains no web or presentation layer objects (presentation objects,
such as Spring MVC controllers, are typically configured in a distinct "presentation
context"). This section details how you can configure a Spring container (a
`WebApplicationContext`) that contains all of the 'business beans' in your application.
Moving on to specifics, all you need to do is declare a
{spring-framework-api}/web/context/ContextLoaderListener.html[`ContextLoaderListener`]
in the standard Jakarta EE servlet `web.xml` file of your web application and add a
`contextConfigLocation` `<context-param/>` section (in the same file) that defines which
set of Spring XML configuration files to load.
Consider the following `<listener/>` configuration:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
----
Further consider the following `<context-param/>` configuration:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext*.xml</param-value>
</context-param>
----
If you do not specify the `contextConfigLocation` context parameter, the
`ContextLoaderListener` looks for a file called `/WEB-INF/applicationContext.xml` to
load. Once the context files are loaded, Spring creates a
{spring-framework-api}/web/context/WebApplicationContext.html[`WebApplicationContext`]
object based on the bean definitions and stores it in the `ServletContext` of the web
application.
All Java web frameworks are built on top of the Servlet API, so you can use the
following code snippet to get access to this "business context" `ApplicationContext`
created by the `ContextLoaderListener`.
The following example shows how to get the `WebApplicationContext`:
[source,java,indent=0,subs="verbatim,quotes"]
----
WebApplicationContext ctx = WebApplicationContextUtils.getWebApplicationContext(servletContext);
----
The
{spring-framework-api}/web/context/support/WebApplicationContextUtils.html[`WebApplicationContextUtils`]
class is for convenience, so you need not remember the name of the `ServletContext`
attribute. Its `getWebApplicationContext()` method returns `null` if an object
does not exist under the `WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE`
key. Rather than risk getting `NullPointerExceptions` in your application, it is better
to use the `getRequiredWebApplicationContext()` method. This method throws an exception
when the `ApplicationContext` is missing.
Once you have a reference to the `WebApplicationContext`, you can retrieve beans by their
name or type. Most developers retrieve beans by name and then cast them to one of their
implemented interfaces.
Fortunately, most of the frameworks in this section have simpler ways of looking up beans.
Not only do they make it easy to get beans from a Spring container, but they also let you
use dependency injection on their controllers. Each web framework section has more detail
on its specific integration strategies.
[[jsf]]
== JSF
JavaServer Faces (JSF) is the JCP's standard component-based, event-driven web
user interface framework. It is an official part of the Jakarta EE umbrella but also
individually usable, for example, through embedding Mojarra or MyFaces within Tomcat.
Please note that recent versions of JSF became closely tied to CDI infrastructure
in application servers, with some new JSF functionality only working in such an
environment. Spring's JSF support is not actively evolved anymore and primarily
exists for migration purposes when modernizing older JSF-based applications.
The key element in Spring's JSF integration is the JSF `ELResolver` mechanism.
[[jsf-springbeanfaceselresolver]]
=== Spring Bean Resolver
`SpringBeanFacesELResolver` is a JSF compliant `ELResolver` implementation,
integrating with the standard Unified EL as used by JSF and JSP. It delegates to
Spring's "business context" `WebApplicationContext` first and then to the
default resolver of the underlying JSF implementation.
Configuration-wise, you can define `SpringBeanFacesELResolver` in your JSF
`faces-context.xml` file, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<faces-config>
<application>
<el-resolver>org.springframework.web.jsf.el.SpringBeanFacesELResolver</el-resolver>
...
</application>
</faces-config>
----
[[jsf-facescontextutils]]
=== Using `FacesContextUtils`
A custom `ELResolver` works well when mapping your properties to beans in
`faces-config.xml`, but, at times, you may need to explicitly grab a bean.
The {spring-framework-api}/web/jsf/FacesContextUtils.html[`FacesContextUtils`]
class makes this easy. It is similar to `WebApplicationContextUtils`, except that
it takes a `FacesContext` parameter rather than a `ServletContext` parameter.
The following example shows how to use `FacesContextUtils`:
[source,java,indent=0,subs="verbatim,quotes"]
----
ApplicationContext ctx = FacesContextUtils.getWebApplicationContext(FacesContext.getCurrentInstance());
----
[[struts]]
== Apache Struts
Invented by Craig McClanahan, https://struts.apache.org[Struts] is an open-source project
hosted by the Apache Software Foundation. Struts 1.x greatly simplified the
JSP/Servlet programming paradigm and won over many developers who were using proprietary
frameworks. It simplified the programming model; it was open source; and it had a large
community, which let the project grow and become popular among Java web developers.
As a successor to the original Struts 1.x, check out Struts 2.x or more recent versions
as well as the Struts-provided
https://struts.apache.org/plugins/spring/[Spring Plugin] for built-in Spring integration.
[[tapestry]]
== Apache Tapestry
https://tapestry.apache.org/[Tapestry] is a "Component oriented framework for creating
dynamic, robust, highly scalable web applications in Java."
While Spring has its own xref:web/webmvc.adoc#mvc[powerful web layer], there are a number of unique
advantages to building an enterprise Java application by using a combination of Tapestry
for the web user interface and the Spring container for the lower layers.
For more information, see Tapestry's dedicated
https://tapestry.apache.org/integrating-with-spring-framework.html[integration module for Spring].
[[web-integration-resources]]
== Further Resources
The following links go to further resources about the various web frameworks described in
this chapter.
* The https://www.oracle.com/java/technologies/javaserverfaces.html[JSF] homepage
* The https://struts.apache.org/[Struts] homepage
* The https://tapestry.apache.org/[Tapestry] homepage
@@ -39,9 +39,9 @@ required CORS response headers set.
In order to enable cross-origin requests (that is, the `Origin` header is present and
differs from the host of the request), you need to have some explicitly declared CORS
configuration. If no matching CORS configuration is found, no CORS headers are added to
the responses to preflight, simple and actual CORS requests and, consequently, browsers
reject them.
configuration. If no matching CORS configuration is found, preflight requests are
rejected. No CORS headers are added to the responses of simple and actual CORS requests
and, consequently, browsers reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
@@ -305,6 +305,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
@@ -327,6 +328,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
@@ -13,9 +13,9 @@ the same xref:web/webflux/reactive-spring.adoc[Reactive Core] foundation.
== Overview
[.small]#xref:web/webmvc-functional.adoc#webmvc-fn-overview[See equivalent in the Servlet stack]#
In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes a
In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
`ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono<ServerResponse>`).
Both the request and the response object have immutable contracts that offer convenient
Both the request and the response object have immutable contracts that offer JDK 8-friendly
access to the HTTP request and response.
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
annotation-based programming model.
@@ -117,7 +117,7 @@ Most applications can run through the WebFlux Java configuration, see xref:web/w
== HandlerFunction
[.small]#xref:web/webmvc-functional.adoc#webmvc-fn-handler-functions[See equivalent in the Servlet stack]#
`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
access to the HTTP request and response.
Both request and response provide {reactive-streams-site}[Reactive Streams] back pressure
against the body streams.
@@ -235,14 +235,88 @@ val map = request.awaitMultipartData()
The following example shows how to access multipart data, one at a time, in streaming fashion:
include-code::./PartEventHandler[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
Flux<PartEvent> allPartEvents = request.bodyToFlux(PartEvent.class);
allPartsEvents.windowUntil(PartEvent::isLast)
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) {
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) {
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
}));
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val parts = request.bodyToFlux<PartEvent>()
allPartsEvents.windowUntil(PartEvent::isLast)
.concatMap {
it.switchOnFirst { signal, partEvents ->
if (signal.hasValue()) {
val event = signal.get()
if (event is FormPartEvent) {
val value: String = event.value();
// handle form field
} else if (event is FilePartEvent) {
val filename: String = event.filename();
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content);
// handle file upload
} else {
return Mono.error(RuntimeException("Unexpected event: " + event));
}
} else {
return partEvents; // either complete or error signal
}
}
}
}
----
======
NOTE: The body contents of the `PartEvent` objects must be completely consumed, relayed, or released to avoid memory leaks.
The following shows how to bind request parameters, URI variables, or headers via `DataBinder`,
and also shows how to customize the `DataBinder`:
include-code::./RequestHandler[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java]
----
Pet pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
----
Kotlin::
+
[source,kotlin]
----
val pet = request.bind(Pet::class.java, {dataBinder -> dataBinder.setAllowedFields("name")})
----
======
[[webflux-fn-response]]
=== ServerResponse
@@ -252,7 +326,24 @@ a `build` method to create it. You can use the builder to set the response statu
headers, or to provide a body. The following example creates a 200 (OK) response with JSON
content:
include-code::./ResponseHandler[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java]
----
Mono<Person> person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).body(person, Person.class);
----
Kotlin::
+
[source,kotlin]
----
val person: Person = ...
ServerResponse.ok().contentType(MediaType.APPLICATION_JSON).bodyValue(person)
----
======
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
@@ -263,7 +354,7 @@ Java::
[source,java]
----
URI location = ...
return ServerResponse.created(location).build();
ServerResponse.created(location).build();
----
Kotlin::
@@ -271,7 +362,7 @@ Kotlin::
[source,kotlin]
----
val location: URI = ...
return ServerResponse.created(location).build()
ServerResponse.created(location).build()
----
======
@@ -284,14 +375,14 @@ Java::
+
[source,java]
----
return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
----
Kotlin::
+
[source,kotlin]
----
return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
----
======
@@ -326,7 +417,87 @@ Therefore, it is useful to group related handler functions together into a handl
has a similar role as `@Controller` in an annotation-based application.
For example, the following class exposes a reactive `Person` repository:
include-code::./PersonHandler[tag=snippet,indent=0]
--
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> listPeople(ServerRequest request) { // <1>
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
public Mono<ServerResponse> createPerson(ServerRequest request) { // <2>
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
public Mono<ServerResponse> getPerson(ServerRequest request) { // <3>
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
----
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
JSON.
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
Note that `PersonRepository.savePerson(Person)` returns `Mono<Void>`: an empty `Mono` that emits
a completion signal when the person has been read from the request and stored. So we use the
`build(Publisher<Void>)` method to send a response when that completion signal is received (that is,
when the `Person` has been saved).
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
found. If it is not found, we use `switchIfEmpty(Mono<T>)` to return a 404 Not Found response.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class PersonHandler(private val repository: PersonRepository) {
suspend fun listPeople(request: ServerRequest): ServerResponse { // <1>
val people: Flow<Person> = repository.allPeople()
return ok().contentType(APPLICATION_JSON).bodyAndAwait(people);
}
suspend fun createPerson(request: ServerRequest): ServerResponse { // <2>
val person = request.awaitBody<Person>()
repository.savePerson(person)
return ok().buildAndAwait()
}
suspend fun getPerson(request: ServerRequest): ServerResponse { // <3>
val personId = request.pathVariable("id").toInt()
return repository.getPerson(personId)?.let { ok().contentType(APPLICATION_JSON).bodyValueAndAwait(it) }
?: ServerResponse.notFound().buildAndAwait()
}
}
----
<1> `listPeople` is a handler function that returns all `Person` objects found in the repository as
JSON.
<2> `createPerson` is a handler function that stores a new `Person` contained in the request body.
Note that `PersonRepository.savePerson(Person)` is a suspending function with no return type.
<3> `getPerson` is a handler function that returns a single person, identified by the `id` path
variable. We retrieve that `Person` from the repository and create a JSON response, if it is
found. If it is not found, we return a 404 Not Found response.
======
--
[[webflux-fn-handler-validation]]
=== Validation
@@ -335,7 +506,66 @@ A functional endpoint can use Spring's xref:web/webmvc/mvc-config/validation.ado
apply validation to the request body. For example, given a custom Spring
xref:web/webmvc/mvc-config/validation.adoc[Validator] implementation for a `Person`:
include-code::./PersonHandler[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class PersonHandler {
private final Validator validator = new PersonValidator(); // <1>
// ...
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate); // <2>
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString()); // <3>
}
}
}
----
<1> Create `Validator` instance.
<2> Apply validation.
<3> Raise exception for a 400 response.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class PersonHandler(private val repository: PersonRepository) {
private val validator = PersonValidator() // <1>
// ...
suspend fun createPerson(request: ServerRequest): ServerResponse {
val person = request.awaitBody<Person>()
validate(person) // <2>
repository.savePerson(person)
return ok().buildAndAwait()
}
private fun validate(person: Person) {
val errors: Errors = BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
throw ServerWebInputException(errors.toString()) // <3>
}
}
}
----
<1> Create `Validator` instance.
<2> Apply validation.
<3> Raise exception for a 400 response.
======
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
a global `Validator` instance based on `LocalValidatorFactoryBean`.
@@ -373,7 +603,28 @@ path, headers, xref:#api-version[API version], and more.
The following example uses an `Accept` header, request predicate:
include-code::./RouterConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val route = coRouter {
GET("/hello-world", accept(TEXT_PLAIN)) {
ServerResponse.ok().bodyValueAndAwait("Hello World")
}
}
----
======
You can compose multiple request predicates together by using:
@@ -408,7 +659,61 @@ There are also other ways to compose multiple router functions together:
The following example shows the composition of four routes:
include-code::./RouterConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
PersonRepository repository = ...
PersonHandler handler = new PersonHandler(repository);
RouterFunction<ServerResponse> otherRoute = ...
RouterFunction<ServerResponse> route = route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
.GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
.POST("/person", handler::createPerson) // <3>
.add(otherRoute) // <4>
.build();
----
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
`PersonHandler.getPerson`
<2> `GET /person` with an `Accept` header that matches JSON is routed to
`PersonHandler.listPeople`
<3> `POST /person` with no additional predicates is mapped to
`PersonHandler.createPerson`, and
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.springframework.http.MediaType.APPLICATION_JSON
val repository: PersonRepository = ...
val handler = PersonHandler(repository);
val otherRoute: RouterFunction<ServerResponse> = coRouter { }
val route = coRouter {
GET("/person/{id}", accept(APPLICATION_JSON), handler::getPerson) // <1>
GET("/person", accept(APPLICATION_JSON), handler::listPeople) // <2>
POST("/person", handler::createPerson) // <3>
}.and(otherRoute) // <4>
----
<1> pass:q[`GET /person/{id}`] with an `Accept` header that matches JSON is routed to
`PersonHandler.getPerson`
<2> `GET /person` with an `Accept` header that matches JSON is routed to
`PersonHandler.listPeople`
<3> `POST /person` with no additional predicates is mapped to
`PersonHandler.createPerson`, and
<4> `otherRoute` is a router function that is created elsewhere, and added to the route built.
======
[[nested-routes]]
=== Nested Routes
@@ -506,7 +811,7 @@ Java::
----
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", version("1.2"),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
request -> ServerResponse.ok().body("Hello World")).build();
----
Kotlin::
@@ -639,6 +944,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Bean
@@ -675,6 +981,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
@Bean
@@ -742,9 +1049,9 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
"/person".nest {
GET("/{id}", handler::getPerson)
GET(handler::listPeople)
GET("", handler::listPeople)
before { // <1>
ServerRequest.from(it)
.header("X-RequestHeader", "Value").build()
@@ -771,7 +1078,54 @@ Now we can add a simple security filter to our route, assuming that we have a `S
can determine whether a particular path is allowed.
The following example shows how to do so:
include-code::./RouterConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
SecurityManager securityManager = ...
RouterFunction<ServerResponse> route = route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
})
.build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val securityManager: SecurityManager = ...
val route = router {
("/person" and accept(APPLICATION_JSON)).nest {
GET("/{id}", handler::getPerson)
GET("", handler::listPeople)
POST(handler::createPerson)
filter { request, next ->
if (securityManager.allowAccessTo(request.path())) {
next(request)
}
else {
status(UNAUTHORIZED).build();
}
}
}
}
----
======
The preceding example demonstrates that invoking the `next.handle(ServerRequest)` is optional.
We only let the handler function be run when access is allowed.
@@ -56,7 +56,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class WebConfiguration implements WebFluxConfigurer {
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
@@ -79,7 +80,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
class WebConfiguration : WebFluxConfigurer {
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
@@ -117,7 +119,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class WebConfiguration implements WebFluxConfigurer {
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
// ...
@@ -139,7 +142,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
class WebConfiguration : WebFluxConfigurer {
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
// ...
@@ -206,8 +210,13 @@ The following table shows the templating libraries that we have tested on differ
[%header]
|===
|Scripting Library |Scripting Engine
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby]
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin]
|===
TIP: The basic rule for integrating any other script engine is that it must implement the
@@ -219,8 +228,17 @@ TIP: The basic rule for integrating any other script engine is that it must impl
You need to have the script engine on your classpath, the details of which vary by script engine:
* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with
Java 8+. Using the latest update release available is highly recommended.
* https://www.jruby.org[JRuby] should be added as a dependency for Ruby support.
* https://www.jython.org[Jython] should be added as a dependency for Python support.
* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory`
file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`
line should be added for Kotlin script support. See
https://github.com/sdeleuze/kotlin-script-templating[this example] for more detail.
You need to have the script templating library. One way to do that for JavaScript is
through https://www.webjars.org/[WebJars].
[[webflux-view-script-integrate]]
=== Script Templates
@@ -228,7 +246,7 @@ You need to have the script engine on your classpath, the details of which vary
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
the script files to load, what function to call to render templates, and so on.
The following example uses the Jython Python engine:
The following example uses Mustache templates and the Nashorn JavaScript engine:
[tabs]
======
@@ -237,7 +255,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class WebConfiguration implements WebFluxConfigurer {
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
@@ -247,8 +266,9 @@ Java::
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("jython");
configurer.setScripts("render.py");
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
@@ -260,7 +280,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
class WebConfiguration : WebFluxConfigurer {
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
@@ -268,8 +289,9 @@ Kotlin::
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "jython"
setScripts("render.py")
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
@@ -283,7 +305,94 @@ The `render` function is called with the following parameters:
* `RenderingContext renderingContext`: The
{spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
that gives access to the application context, the locale, the template loader, and the
URL
URL (since 5.0)
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
If your templating technology requires some customization, you can provide a script that
implements a custom render function. For example, https://handlebarsjs.com[Handlerbars]
needs to compile templates before using them and requires a
https://en.wikipedia.org/wiki/Polyfill[polyfill] in order to emulate some
browser facilities not available in the server-side script engine.
The following example shows how to set a custom render function:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
public class WebConfig implements WebFluxConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class WebConfig : WebFluxConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
----
======
NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe
script engines with templating libraries not designed for concurrency, such as Handlebars or
React running on Nashorn. In that case, Java SE 8 update 60 is required, due to
https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally
recommended to use a recent Java SE patch release in any case.
`polyfill.js` defines only the `window` object needed by Handlebars to run properly,
as the following snippet shows:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
var window = {};
----
This basic `render.js` implementation compiles the template before using it. A production
ready implementation should also store and reused cached templates or pre-compiled templates.
This can be done on the script side, as well as any customization you need (managing
template engine configuration for example).
The following example shows how compile a template:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
----
Check out the Spring Framework unit tests,
{spring-framework-code}/spring-webflux/src/test/java/org/springframework/web/reactive/result/view/script[Java], and
@@ -18,8 +18,8 @@ You can also use `WebClient.builder()` with further options:
* `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#observability.http-client.webclient[Observability support].
* `observationConvention`: xref:integration/observability.adoc#observability.config[an optional, custom convention to extract metadata] for recorded observations.
* `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.
For example:
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- The Jackson JSON and XML message converters use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
@@ -66,13 +66,6 @@ from an existing `ProblemDetail`. This could be done centrally, for example, fro
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
`ProblemDetail` of an exception into a subclass with the additional non-standard fields.
TIP: In Spring Boot, the `spring.webflux.problemdetails.enabled` property autoconfigures
a `ResponseEntityExceptionHandler` that handles built-in exceptions with problem details.
In that case, you may prefer to create another `@ControllerAdvice` instead of extending
`ResponseEntityExceptionHandler` if you want to take over the handling of a specific
built-in exception. You'll need to ensure your handler is ordered ahead of the one
configured by Spring Boot whose order is 0.
[[webflux-ann-rest-exceptions-i18n]]
== Customization and i18n
@@ -41,8 +41,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyForm(
private val name: String,
private val file: FilePart)
val name: String,
val file: FilePart)
@Controller
class FileUploadController {
@@ -103,8 +103,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@RequestPart("meta-data") metadata: Part, // <1>
@RequestPart("file-data") file: FilePart): String { // <2>
fun handle(@RequestPart("meta-data") Part metadata, // <1>
@RequestPart("file-data") FilePart file): String { // <2>
// ...
}
----
@@ -169,8 +169,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@Valid @RequestPart("meta-data") metadata: Mono<MetaData>): String {
// use one of the onError* operators...
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
// ...
}
----
======
@@ -202,7 +202,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@RequestBody parts: Mono<MultiValueMap<String, Part>>): String { // <1>
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { // <1>
// ...
}
----
@@ -227,7 +227,87 @@ when uploading. If the file is large enough to be split across multiple buffers,
For example:
include-code::./PartEventController[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartsEvents) { <1>
allPartsEvents.windowUntil(PartEvent::isLast) <2>
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> { <3>
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) { <4>
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) { <5>
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content); <6>
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
}));
}
----
<1> Using `@RequestBody`.
<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be
followed by additional events belonging to subsequent parts.
This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to
split events from all parts into windows that each belong to a single part.
<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or
file upload.
<4> Handling the form field.
<5> Handling the file upload.
<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@PostMapping("/")
fun handle(@RequestBody allPartsEvents: Flux<PartEvent>) = { // <1>
allPartsEvents.windowUntil(PartEvent::isLast) <2>
.concatMap {
it.switchOnFirst { signal, partEvents -> <3>
if (signal.hasValue()) {
val event = signal.get()
if (event is FormPartEvent) { <4>
val value: String = event.value();
// handle form field
} else if (event is FilePartEvent) { <5>
val filename: String = event.filename();
val contents: Flux<DataBuffer> = partEvents.map(PartEvent::content); <6>
// handle file upload
} else {
return Mono.error(RuntimeException("Unexpected event: " + event));
}
} else {
return partEvents; // either complete or error signal
}
}
}
}
----
<1> Using `@RequestBody`.
<2> The final `PartEvent` for a particular part will have `isLast()` set to `true`, and can be
followed by additional events belonging to subsequent parts.
This makes the `isLast` property suitable as a predicate for the `Flux::windowUntil` operator, to
split events from all parts into windows that each belong to a single part.
<3> The `Flux::switchOnFirst` operator allows you to see whether you are handling a form field or
file upload.
<4> Handling the form field.
<5> Handling the file upload.
<6> The body contents must be completely consumed, relayed, or released to avoid memory leaks.
======
Received part events can also be relayed to another service by using the `WebClient`.
See xref:web/webflux-webclient/client-body.adoc#webflux-client-body-multipart[Multipart Data].
@@ -393,10 +393,9 @@ 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 and
xref:web/webflux/config.adoc#webflux-config-api-version[supported versions] above
- No value -- matches any version, but is superseded by a more specific version match
- 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,
@@ -444,9 +443,6 @@ For request with version `"1.3"`:
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
- (4) is higher and does not match
NOTE: Version 1.3 must be present in the mappings, or be
xref:web/webflux/config.adoc#webflux-config-api-version[configured as supported].
For request with version `"1.5"`:
- (1) matches as it matches any version
@@ -458,10 +454,9 @@ A request with version `"1.6"` does not have a match. (1) and (3) do match, but
superseded by (4), which allows only a strict match, and therefore does not match.
In this scenario, a `NotAcceptableApiVersionException` results in a 400 response.
Controller methods without a version are intended to support clients created before a
versioned alternative was introduced. Therefore, even though an unversioned controller
method is considered a match for any version, it is in fact given the lowest priority,
and is effectively superseded by any alternative controller method with a version.
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.
@@ -58,7 +58,7 @@ If a publisher cannot slow down, it has to decide whether to buffer, drop, or fa
Reactive Streams plays an important role for interoperability. It is of interest to libraries
and infrastructure components but less useful as an application API, because it is too
low-level. Applications need a higher-level and richer, functional API to
compose async logic -- similar to the Java `Stream` API but not only for collections.
compose async logic -- similar to the Java 8 `Stream` API but not only for collections.
This is the role that reactive libraries play.
{reactor-github-org}/reactor[Reactor] is the reactive library of choice for
@@ -131,7 +131,7 @@ execution model benefits as others in this space and also provides a choice of s
(annotated controllers and functional web endpoints), and a choice of reactive libraries
(Reactor, RxJava, or other).
* If you are interested in a lightweight, functional web framework for use with Java
* If you are interested in a lightweight, functional web framework for use with Java 8 lambdas
or Kotlin, you can use the Spring WebFlux functional web endpoints. That can also be a good choice
for smaller applications or microservices with less complex requirements that can benefit
from greater transparency and control.
@@ -381,23 +381,16 @@ See the section on xref:web/webflux-cors.adoc[CORS] and the xref:web/webflux-cor
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
Adding trailing slash variants to all mapping declarations is not the best way to handle this use case.
The `UrlHandlerFilter` web filter has been designed for this purpose. It can be configured to:
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* mutate the request to remove the trailing slash.
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* mutate the request to act as if the request was sent without a trailing slash and continue the processing of the request.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
[[webflux-exception-handler]]
== Exceptions
@@ -665,6 +658,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class MyConfig implements WebFluxConfigurer {
@Override
@@ -679,6 +673,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebFlux
class MyConfig : WebFluxConfigurer {
override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
@@ -66,9 +66,9 @@ required CORS response headers set.
In order to enable cross-origin requests (that is, the `Origin` header is present and
differs from the host of the request), you need to have some explicitly declared CORS
configuration. If no matching CORS configuration is found, no CORS headers are added to
the responses to preflight, simple and actual CORS requests and, consequently, browsers
reject them.
configuration. If no matching CORS configuration is found, preflight requests are
rejected. No CORS headers are added to the responses of simple and actual CORS requests
and, consequently, browsers reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
@@ -208,7 +208,6 @@ Kotlin::
fun remove(@PathVariable id: Long) {
// ...
}
}
----
======
@@ -287,9 +286,84 @@ the `allowOriginPatterns` property may be used to match to a dynamic set of orig
`maxAge` is set to 30 minutes.
You can enable CORS in the Spring MVC configuration as the following example shows:
[[mvc-cors-global-java]]
=== Java Configuration
[.small]#xref:web/webflux-cors.adoc#webflux-cors-global[See equivalent in the Reactive stack]#
To enable CORS in the MVC Java config, you can use the `CorsRegistry` callback,
as the following example shows:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600);
// Add more mappings...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain2.com")
.allowedMethods("PUT", "DELETE")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true).maxAge(3600)
// Add more mappings...
}
}
----
======
[[mvc-cors-global-xml]]
=== XML Configuration
To enable CORS in the XML namespace, you can use the `<mvc:cors>` element,
as the following example shows:
[source,xml,indent=0,subs="verbatim"]
----
<mvc:cors>
<mvc:mapping path="/api/**"
allowed-origins="https://domain1.com, https://domain2.com"
allowed-methods="GET, PUT"
allowed-headers="header1, header2, header3"
exposed-headers="header1, header2" allow-credentials="true"
max-age="123" />
<mvc:mapping path="/resources/**"
allowed-origins="https://domain1.com" />
</mvc:cors>
----
include-code::./WebConfiguration[tag=snippet,indent=0]
[[mvc-cors-filter]]
== CORS Filter
@@ -15,7 +15,7 @@ the same xref:web/webmvc/mvc-servlet.adoc[DispatcherServlet].
In WebMvc.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes
`ServerRequest` and returns a `ServerResponse`.
Both the request and the response object have immutable contracts that offer convenient
Both the request and the response object have immutable contracts that offer JDK 8-friendly
access to the HTTP request and response.
`HandlerFunction` is the equivalent of the body of a `@RequestMapping` method in the
annotation-based programming model.
@@ -116,7 +116,7 @@ xref:web/webmvc-functional.adoc#webmvc-fn-running[Running a Server].
== HandlerFunction
[.small]#xref:web/webflux-functional.adoc#webflux-fn-handler-functions[See equivalent in the Reactive stack]#
`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient
`ServerRequest` and `ServerResponse` are immutable interfaces that offer JDK 8-friendly
access to the HTTP request and response, including headers, body, method, and status code.
[[webmvc-fn-request]]
@@ -836,6 +836,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
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();
RouterFunction<ServerResponse> redirectToIndex = route()
.resource(spaPredicate, index)
@@ -899,9 +900,81 @@ 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 related Spring MVC configuration:
The following example shows a WebMvc Java configuration:
include-code::./WebConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableMvc
public class WebConfig implements WebMvcConfigurer {
@Bean
public RouterFunction<?> routerFunctionA() {
// ...
}
@Bean
public RouterFunction<?> routerFunctionB() {
// ...
}
// ...
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// configure message conversion...
}
@Override
public void addCorsMappings(CorsRegistry registry) {
// configure CORS...
}
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
// configure view resolution for HTML rendering...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableMvc
class WebConfig : WebMvcConfigurer {
@Bean
fun routerFunctionA(): RouterFunction<*> {
// ...
}
@Bean
fun routerFunctionB(): RouterFunction<*> {
// ...
}
// ...
override fun configureMessageConverters(converters: List<HttpMessageConverter<*>>) {
// configure message conversion...
}
override fun addCorsMappings(registry: CorsRegistry) {
// configure CORS...
}
override fun configureViewResolvers(registry: ViewResolverRegistry) {
// configure view resolution for HTML rendering...
}
}
----
======
[[webmvc-fn-handler-filter-function]]
@@ -14,7 +14,82 @@ integration for using Spring MVC with FreeMarker templates.
The following example shows how to configure FreeMarker as a view technology:
include-code::./WebConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.freeMarker();
}
// Configure FreeMarker...
@Bean
public FreeMarkerConfigurer freeMarkerConfigurer() {
FreeMarkerConfigurer configurer = new FreeMarkerConfigurer();
configurer.setTemplateLoaderPath("/WEB-INF/freemarker");
configurer.setDefaultCharset(StandardCharsets.UTF_8);
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.freeMarker()
}
// Configure FreeMarker...
@Bean
fun freeMarkerConfigurer() = FreeMarkerConfigurer().apply {
setTemplateLoaderPath("/WEB-INF/freemarker")
setDefaultCharset(StandardCharsets.UTF_8)
}
}
----
======
The following example shows how to configure the same in XML:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:freemarker/>
</mvc:view-resolvers>
<!-- Configure FreeMarker... -->
<mvc:freemarker-configurer>
<mvc:template-loader-path location="/WEB-INF/freemarker"/>
</mvc:freemarker-configurer>
----
Alternatively, you can also declare the `FreeMarkerConfigurer` bean for full control over all
properties, as the following example shows:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="defaultEncoding" value="UTF-8"/>
</bean>
----
Your templates need to be stored in the directory specified by the `FreeMarkerConfigurer`
shown in the preceding example. Given the preceding configuration, if your controller
@@ -32,7 +107,19 @@ properties on the `FreeMarkerConfigurer` bean. The `freemarkerSettings` property
a `java.util.Properties` object, and the `freemarkerVariables` property requires a
`java.util.Map`. The following example shows how to use a `FreeMarkerConfigurer`:
include-code::./WebConfiguration[tag=snippet,indent=0]
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="freemarkerConfig" class="org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer">
<property name="templateLoaderPath" value="/WEB-INF/freemarker/"/>
<property name="freemarkerVariables">
<map>
<entry key="xml_escape" value-ref="fmXmlEscape"/>
</map>
</property>
</bean>
<bean id="fmXmlEscape" class="freemarker.template.utility.XmlEscape"/>
----
See the FreeMarker documentation for details of settings and variables as they apply to
the `Configuration` object.
@@ -14,7 +14,68 @@ NOTE: The Groovy Markup Template engine requires Groovy 2.3.1+.
The following example shows how to configure the Groovy Markup Template Engine:
include-code::./WebConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.groovy();
}
// Configure the Groovy Markup Template Engine...
@Bean
public GroovyMarkupConfigurer groovyMarkupConfigurer() {
GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer();
configurer.setResourceLoaderPath("/WEB-INF/");
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.groovy()
}
// Configure the Groovy Markup Template Engine...
@Bean
fun groovyMarkupConfigurer() = GroovyMarkupConfigurer().apply {
resourceLoaderPath = "/WEB-INF/"
}
}
----
======
The following example shows how to configure the same in XML:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:groovy/>
</mvc:view-resolvers>
<!-- Configure the Groovy Markup Template Engine... -->
<mvc:groovy-configurer resource-loader-path="/WEB-INF/"/>
----
[[mvc-view-groovymarkup-example]]
== Example
@@ -16,11 +16,39 @@ a directory under the `WEB-INF` directory so there can be no direct access by cl
This is what is done by the configuration below which registers a JSP view resolver using
a default view name prefix of `"/WEB-INF/"` and a default suffix of `".jsp"`.
include-code::./WebConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@EnableWebMvc
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp();
}
}
----
XML::
+
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:jsp/>
</mvc:view-resolvers>
----
======
[NOTE]
You can specify custom prefix and suffix.
[[mvc-view-jsp-jstl]]
== JSPs versus JSTL
@@ -11,8 +11,13 @@ templating libraries on different script engines:
[%header]
|===
|Scripting Library |Scripting Engine
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby]
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin]
|===
TIP: The basic rule for integrating any other script engine is that it must implement the
@@ -25,8 +30,18 @@ TIP: The basic rule for integrating any other script engine is that it must impl
You need to have the script engine on your classpath, the details of which vary by script engine:
* The https://openjdk.java.net/projects/nashorn/[Nashorn] JavaScript engine is provided with
Java 8+. Using the latest update release available is highly recommended.
* https://www.jruby.org[JRuby] should be added as a dependency for Ruby support.
* https://www.jython.org[Jython] should be added as a dependency for Python support.
* `org.jetbrains.kotlin:kotlin-script-util` dependency and a `META-INF/services/javax.script.ScriptEngineFactory`
file containing a `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory`
line should be added for Kotlin script support. See
https://github.com/sdeleuze/kotlin-script-templating[this example] for more details.
You need to have the script templating library. One way to do that for JavaScript is
through https://www.webjars.org/[WebJars].
[[mvc-view-script-integrate]]
== Script Templates
@@ -34,20 +49,74 @@ You need to have the script engine on your classpath, the details of which vary
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
the script files to load, what function to call to render templates, and so on.
The following example uses the Jython Python engine:
The following example uses Mustache templates and the Nashorn JavaScript engine:
include-code::./WebConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
The render function is called with the following parameters:
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
* `String template`: The template content
* `Map model`: The view model
* `RenderingContext renderingContext`: The
{spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
that gives access to the application context, the locale, the template loader, and the
URL
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("mustache.js");
configurer.setRenderObject("Mustache");
configurer.setRenderFunction("render");
return configurer;
}
}
----
The controller is used to populate the model attributes and specify the view name, as the following example shows:
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("mustache.js")
renderObject = "Mustache"
renderFunction = "render"
}
}
----
======
The following example shows the same arrangement in XML:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<mvc:annotation-driven/>
<mvc:view-resolvers>
<mvc:script-template/>
</mvc:view-resolvers>
<mvc:script-template-configurer engine-name="nashorn" render-object="Mustache" render-function="render">
<mvc:script location="mustache.js"/>
</mvc:script-template-configurer>
----
The controller would look no different for the Java and XML configurations, as the following example shows:
[tabs]
======
@@ -84,6 +153,115 @@ Kotlin::
----
======
The following example shows the Mustache template:
[source,html,indent=0,subs="verbatim,quotes"]
----
<html>
<head>
<title>{{title}}</title>
</head>
<body>
<p>{{body}}</p>
</body>
</html>
----
The render function is called with the following parameters:
* `String template`: The template content
* `Map model`: The view model
* `RenderingContext renderingContext`: The
{spring-framework-api}/web/servlet/view/script/RenderingContext.html[`RenderingContext`]
that gives access to the application context, the locale, the template loader, and the
URL (since 5.0)
`Mustache.render()` is natively compatible with this signature, so you can call it directly.
If your templating technology requires some customization, you can provide a script that
implements a custom render function. For example, https://handlebarsjs.com[Handlerbars]
needs to compile templates before using them and requires a
https://en.wikipedia.org/wiki/Polyfill[polyfill] to emulate some
browser facilities that are not available in the server-side script engine.
The following example shows how to do so:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.scriptTemplate();
}
@Bean
public ScriptTemplateConfigurer configurer() {
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
configurer.setEngineName("nashorn");
configurer.setScripts("polyfill.js", "handlebars.js", "render.js");
configurer.setRenderFunction("render");
configurer.setSharedEngine(false);
return configurer;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebMvc
class WebConfig : WebMvcConfigurer {
override fun configureViewResolvers(registry: ViewResolverRegistry) {
registry.scriptTemplate()
}
@Bean
fun configurer() = ScriptTemplateConfigurer().apply {
engineName = "nashorn"
setScripts("polyfill.js", "handlebars.js", "render.js")
renderFunction = "render"
isSharedEngine = false
}
}
----
======
NOTE: Setting the `sharedEngine` property to `false` is required when using non-thread-safe
script engines with templating libraries not designed for concurrency, such as Handlebars or
React running on Nashorn. In that case, Java SE 8 update 60 is required, due to
https://bugs.openjdk.java.net/browse/JDK-8076099[this bug], but it is generally
recommended to use a recent Java SE patch release in any case.
`polyfill.js` defines only the `window` object needed by Handlebars to run properly, as follows:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
var window = {};
----
This basic `render.js` implementation compiles the template before using it. A production-ready
implementation should also store any reused cached templates or pre-compiled templates.
You can do so on the script side (and handle any customization you need -- managing
template engine configuration, for example). The following example shows how to do so:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
function render(template, model) {
var compiledTemplate = Handlebars.compile(template);
return compiledTemplate(model);
}
----
Check out the Spring Framework unit tests,
{spring-framework-code}/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script[Java], and
{spring-framework-code}/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script[resources],
@@ -21,7 +21,45 @@ Configuration is standard for a simple Spring web application: The MVC configura
has to define an `XsltViewResolver` bean and regular MVC annotation configuration.
The following example shows how to do so:
include-code::./WebConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@EnableWebMvc
@ComponentScan
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public XsltViewResolver xsltViewResolver() {
XsltViewResolver viewResolver = new XsltViewResolver();
viewResolver.setPrefix("/WEB-INF/xsl/");
viewResolver.setSuffix(".xslt");
return viewResolver;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@EnableWebMvc
@ComponentScan
@Configuration
class WebConfig : WebMvcConfigurer {
@Bean
fun xsltViewResolver() = XsltViewResolver().apply {
setPrefix("/WEB-INF/xsl/")
setSuffix(".xslt")
}
}
----
======
[[mvc-view-xslt-controllercode]]
== Controller
@@ -120,26 +120,17 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
== URL Handler
[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]#
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
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")`.
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
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:
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* wrap the request to remove the trailing slash.
NOTE: Historically Spring MVC supported trailing slash matching of URL paths.
This capability was deprecated in 6.0 for security reasons and removed in 7.0 with
`UrlHandlerFilter` providing a safer alternative.
* 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.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
@@ -45,7 +45,7 @@ By default, this converter supports `text/xml` and `application/xml`.
| `JacksonJsonHttpMessageConverter`
| An `HttpMessageConverter` implementation that can read and write JSON by using Jackson's `JsonMapper`.
You can customize JSON mapping as needed through the use of Jackson's provided annotations.
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder` constructor parameters.
When you need further control (for cases where custom JSON serializers/deserializers need to be provided for specific types), you can inject a custom `JsonMapper` through the `JsonMapper` or `JsonMapper.Builder ` constructor parameters.
By default, this converter supports `application/json`. This requires the `tools.jackson.core:jackson-databind` dependency.
| `JacksonXmlHttpMessageConverter`
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- The Jackson JSON and XML codecs use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
To enable RFC 9457 responses for Spring MVC exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
@@ -66,13 +66,6 @@ from an existing `ProblemDetail`. This could be done centrally, for example, fro
`@ControllerAdvice` such as `ResponseEntityExceptionHandler` that re-creates the
`ProblemDetail` of an exception into a subclass with the additional non-standard fields.
TIP: In Spring Boot, the `spring.mvc.problemdetails.enabled` property autoconfigures
a `ResponseEntityExceptionHandler` that handles built-in exceptions with problem details.
In that case, you may prefer to create another `@ControllerAdvice` instead of extending
`ResponseEntityExceptionHandler` if you want to take over the handling of a specific
built-in exception. You'll need to ensure your handler is ordered ahead of the one
configured by Spring Boot whose order is 0.
[[mvc-ann-rest-exceptions-i18n]]
== Customization and i18n
@@ -191,7 +184,7 @@ Message codes and arguments for each error are also resolved via `MessageSource`
|===
NOTE: Unlike other exceptions, the message arguments for
`MethodArgumentNotValidException` and `HandlerMethodValidationException` are based on a list of
`MethodArgumentValidException` and `HandlerMethodValidationException` are based on a list of
`MessageSourceResolvable` errors that can also be customized through a
xref:core/beans/context-introduction.adoc#context-functionality-messagesource[MessageSource]
resource bundle. See
@@ -6,7 +6,7 @@
The next table describes the supported controller method arguments. Reactive types are not supported
for any arguments.
`java.util.Optional` is supported as a method argument in combination with
JDK 8's `java.util.Optional` is supported as a method argument in combination with
annotations that have a `required` attribute (for example, `@RequestParam`, `@RequestHeader`,
and others) and is equivalent to `required=false`.
@@ -424,9 +424,9 @@ 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 and xref:web/webmvc/mvc-config/api-version.adoc[supported versions] above
- No value -- matches any version, but is superseded by a more specific version match
- 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,
@@ -463,7 +463,7 @@ Java::
----
<1> match any version
<2> match version 1.1
<3> match version 1.2 and supported versions above
<3> match version 1.2 and above
<4> match version 1.5
======
@@ -474,9 +474,6 @@ For request with version `"1.3"`:
- (3) matches as it matches 1.2 and above, and is *chosen* as the highest match
- (4) is higher and does not match
NOTE: Version 1.3 must be present in the mappings, or be
xref:web/webmvc/mvc-config/api-version.adoc[configured as supported].
For request with version `"1.5"`:
- (1) matches as it matches any version
@@ -488,10 +485,9 @@ A request with version `"1.6"` does not have a match. (1) and (3) do match, but
superseded by (4), which allows only a strict match, and therefore does not match.
In this scenario, a `NotAcceptableApiVersionException` results in a 400 response.
Controller methods without a version are intended to support clients created before a
versioned alternative was introduced. Therefore, even though an unversioned controller
method is considered a match for any version, it is in fact given the lowest priority,
and is effectively superseded by any alternative controller method with a version.
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.
@@ -554,7 +550,53 @@ You can programmatically register handler methods, which you can use for dynamic
registrations or for advanced cases, such as different instances of the same handler
under different URLs. The following example registers a handler method:
include-code::./MyConfiguration[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class MyConfig {
@Autowired
public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) // <1>
throws NoSuchMethodException {
RequestMappingInfo info = RequestMappingInfo
.paths("/user/{id}").methods(RequestMethod.GET).build(); // <2>
Method method = UserHandler.class.getMethod("getUser", Long.class); // <3>
mapping.registerMapping(info, handler, method); // <4>
}
}
----
<1> Inject the target handler and the handler mapping for controllers.
<2> Prepare the request mapping meta data.
<3> Get the handler method.
<4> Add the registration.
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@Configuration
class MyConfig {
@Autowired
fun setHandlerMapping(mapping: RequestMappingHandlerMapping, handler: UserHandler) { // <1>
val info = RequestMappingInfo.paths("/user/{id}").methods(RequestMethod.GET).build() // <2>
val method = UserHandler::class.java.getMethod("getUser", Long::class.java) // <3>
mapping.registerMapping(info, handler, method) // <4>
}
}
----
<1> Inject the target handler and the handler mapping for controllers.
<2> Prepare the request mapping meta data.
<3> Get the handler method.
<4> Add the registration.
======
@@ -14,12 +14,55 @@ In turn, the `DispatcherServlet` uses Spring configuration to discover
the delegate components it needs for request mapping, view resolution, exception
handling, xref:web/webmvc/mvc-servlet/special-bean-types.adoc[and more].
The following example shows the programmatic registration and initialization of
The following example of the Java configuration registers and initializes
the `DispatcherServlet`, which is auto-detected by the Servlet container
(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]), and the
equivalent `web.xml`:
(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]):
include-code::./MyWebApplicationInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(servletContext: ServletContext) {
// Load Spring web application configuration
val context = AnnotationConfigWebApplicationContext()
context.register(AppConfig::class.java)
// Create and register the DispatcherServlet
val servlet = DispatcherServlet(context)
val registration = servletContext.addServlet("app", servlet)
registration.setLoadOnStartup(1)
registration.addMapping("/app/*")
}
}
----
======
NOTE: In addition to using the ServletContext API directly, you can also extend
`AbstractAnnotationConfigDispatcherServletInitializer` and override specific methods
@@ -30,6 +73,39 @@ alternative to `AnnotationConfigWebApplicationContext`. See the
{spring-framework-api}/web/context/support/GenericWebApplicationContext.html[`GenericWebApplicationContext`]
javadoc for details.
The following example of `web.xml` configuration registers and initializes the `DispatcherServlet`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
</web-app>
----
NOTE: Spring Boot follows a different initialization sequence. Rather than hooking into
the lifecycle of the Servlet container, Spring Boot uses Spring configuration to
bootstrap itself and the embedded Servlet container. `Filter` and `Servlet` declarations
@@ -5,7 +5,48 @@ In a Servlet environment, you have the option of configuring the Servlet contain
programmatically as an alternative or in combination with a `web.xml` file.
The following example registers a `DispatcherServlet`:
include-code::./MyWebApplicationInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.web.WebApplicationInitializer;
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext container) {
XmlWebApplicationContext appContext = new XmlWebApplicationContext();
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext));
registration.setLoadOnStartup(1);
registration.addMapping("/");
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.springframework.web.WebApplicationInitializer
class MyWebApplicationInitializer : WebApplicationInitializer {
override fun onStartup(container: ServletContext) {
val appContext = XmlWebApplicationContext()
appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
val registration = container.addServlet("dispatcher", DispatcherServlet(appContext))
registration.setLoadOnStartup(1)
registration.addMapping("/")
}
}
----
======
`WebApplicationInitializer` is an interface provided by Spring MVC that ensures your
implementation is detected and automatically used to initialize any Servlet 3 container.
@@ -14,21 +55,144 @@ An abstract base class implementation of `WebApplicationInitializer` named
`DispatcherServlet` by overriding methods to specify the servlet mapping and the
location of the `DispatcherServlet` configuration.
This is recommended for applications that use programmatic Spring configuration, as the
This is recommended for applications that use Java-based Spring configuration, as the
following example shows:
include-code::./MyWebAppInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { MyWebConfig.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return null
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return arrayOf(MyWebConfig::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
----
======
If you use XML-based Spring configuration, you should extend directly from
`AbstractDispatcherServletInitializer`, as the following example shows:
include-code::./MyXmlDispatcherServletInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
protected WebApplicationContext createServletApplicationContext() {
XmlWebApplicationContext cxt = new XmlWebApplicationContext();
cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");
return cxt;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
override fun createRootApplicationContext(): WebApplicationContext? {
return null
}
override fun createServletApplicationContext(): WebApplicationContext {
return XmlWebApplicationContext().apply {
setConfigLocation("/WEB-INF/spring/dispatcher-config.xml")
}
}
override fun getServletMappings(): Array<String> {
return arrayOf("/")
}
}
----
======
`AbstractDispatcherServletInitializer` also provides a convenient way to add `Filter`
instances and have them be automatically mapped to the `DispatcherServlet`, as the
following example shows:
include-code::./MyFilterDispatcherServletInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractDispatcherServletInitializer {
// ...
@Override
protected Filter[] getServletFilters() {
return new Filter[] {
new HiddenHttpMethodFilter(), new CharacterEncodingFilter() };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractDispatcherServletInitializer() {
// ...
override fun getServletFilters(): Array<Filter> {
return arrayOf(HiddenHttpMethodFilter(), CharacterEncodingFilter())
}
}
----
======
Each filter is added with a default name based on its concrete type and automatically
mapped to the `DispatcherServlet`.
@@ -20,15 +20,91 @@ are effectively inherited and can be overridden (that is, re-declared) in the Se
child `WebApplicationContext`, which typically contains beans local to the given `Servlet`.
The following image shows this relationship:
image::mvc-context-hierarchy.png[width=60%,align="center"]
image::mvc-context-hierarchy.png[]
The following example configures a `WebApplicationContext` hierarchy, and the equivalent `web.xml`:
The following example configures a `WebApplicationContext` hierarchy:
include-code::./MyWebAppInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class<?>[] { RootConfig.class };
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[] { App1Config.class };
}
@Override
protected String[] getServletMappings() {
return new String[] { "/app1/*" };
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyWebAppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>> {
return arrayOf(RootConfig::class.java)
}
override fun getServletConfigClasses(): Array<Class<*>> {
return arrayOf(App1Config::class.java)
}
override fun getServletMappings(): Array<String> {
return arrayOf("/app1/*")
}
}
----
======
TIP: If an application context hierarchy is not required, applications can return all
configuration through `getRootConfigClasses()` and `null` from `getServletConfigClasses()`.
The following example shows the `web.xml` equivalent:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<web-app>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/root-context.xml</param-value>
</context-param>
<servlet>
<servlet-name>app1</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/app1-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>app1</servlet-name>
<url-pattern>/app1/*</url-pattern>
</servlet-mapping>
</web-app>
----
TIP: If an application context hierarchy is not required, applications may configure a
"`root`" context only and leave the `contextConfigLocation` Servlet parameter empty.
@@ -74,7 +74,42 @@ Servlet container makes an ERROR dispatch within the container to the configured
to a `@Controller`, which could be implemented to return an error view name with a model
or to render a JSON response, as the following example shows:
include-code::./ErrorController[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RestController
public class ErrorController {
@RequestMapping(path = "/error")
public Map<String, Object> handle(HttpServletRequest request) {
Map<String, Object> map = new HashMap<>();
map.put("status", request.getAttribute("jakarta.servlet.error.status_code"));
map.put("reason", request.getAttribute("jakarta.servlet.error.message"));
return map;
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RestController
class ErrorController {
@RequestMapping(path = ["/error"])
fun handle(request: HttpServletRequest): Map<String, Any> {
val map = HashMap<String, Any>()
map["status"] = request.getAttribute("jakarta.servlet.error.status_code")
map["reason"] = request.getAttribute("jakarta.servlet.error.message")
return map
}
}
----
======
TIP: The Servlet API does not provide a way to create error page mappings in Java. You can,
however, use both a `WebApplicationInitializer` and a minimal `web.xml`.
@@ -54,9 +54,44 @@ information.
This locale resolver inspects a `Cookie` that might exist on the client to see if a
`Locale` or `TimeZone` is specified. If so, it uses the specified details. By using the
properties of this locale resolver, you can specify the name of the cookie as well as the
maximum age. The following example defines a `CookieLocaleResolver` bean:
maximum age. The following example defines a `CookieLocaleResolver`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
<bean id="localeResolver" class="org.springframework.web.servlet.i18n.CookieLocaleResolver">
<property name="cookieName" value="clientlanguage"/>
<!-- in seconds. If set to -1, the cookie is not persisted (deleted when browser shuts down) -->
<property name="cookieMaxAge" value="100000"/>
</bean>
----
The following table describes the properties `CookieLocaleResolver`:
[[mvc-cookie-locale-resolver-props-tbl]]
.CookieLocaleResolver properties
[cols="1,1,4"]
|===
| Property | Default | Description
| `cookieName`
| class name + LOCALE
| The name of the cookie
| `cookieMaxAge`
| Servlet container default
| The maximum time a cookie persists on the client. If `-1` is specified, the
cookie will not be persisted. It is available only until the client shuts down
the browser.
| `cookiePath`
| /
| Limits the visibility of the cookie to a certain part of your site. When `cookiePath` is
specified, the cookie is visible only to that path and the paths below it.
|===
include-code::./WebConfiguration[tag=snippet,indent=0]
[[mvc-localeresolver-session]]
== Session Resolver
@@ -80,7 +115,31 @@ You can enable changing of locales by adding the `LocaleChangeInterceptor` to on
accordingly, calling the `setLocale` method on the `LocaleResolver` in the dispatcher's
application context. The next example shows that calls to all `{asterisk}.view` resources
that contain a parameter named `siteLanguage` now changes the locale. So, for example,
a request for the URL `https://domain.com/home.view?siteLanguage=nl` changes the site
a request for the URL, `https://www.sf.net/home.view?siteLanguage=nl`, changes the site
language to Dutch. The following example shows how to intercept the locale:
include-code::./WebConfiguration[tag=snippet,indent=0,chomp=-tags]
[source,xml,indent=0,subs="verbatim"]
----
<bean id="localeChangeInterceptor"
class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
<property name="paramName" value="siteLanguage"/>
</bean>
<bean id="localeResolver"
class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="interceptors">
<list>
<ref bean="localeChangeInterceptor"/>
</list>
</property>
<property name="mappings">
<value>/**/*.view=someController</value>
</property>
</bean>
----
@@ -25,7 +25,62 @@ through the `enableLoggingRequestDetails` property on `DispatcherServlet`.
The following example shows how to do so by using Java configuration:
include-code::./MyInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MyInitializer
extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return ... ;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return ... ;
}
@Override
protected String[] getServletMappings() {
return ... ;
}
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
registration.setInitParameter("enableLoggingRequestDetails", "true");
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
override fun getRootConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletConfigClasses(): Array<Class<*>>? {
return ...
}
override fun getServletMappings(): Array<String> {
return ...
}
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
registration.setInitParameter("enableLoggingRequestDetails", "true")
}
}
----
======
@@ -26,7 +26,43 @@ To do so:
The following example shows how to set a `MultipartConfigElement` on the Servlet registration:
include-code::./AppInitializer[tag=snippet,indent=0]
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
// ...
@Override
protected void customizeRegistration(ServletRegistration.Dynamic registration) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(new MultipartConfigElement("/tmp"));
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class AppInitializer : AbstractAnnotationConfigDispatcherServletInitializer() {
// ...
override fun customizeRegistration(registration: ServletRegistration.Dynamic) {
// Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold
registration.setMultipartConfig(MultipartConfigElement("/tmp"))
}
}
----
======
Once the Servlet multipart configuration is in place, you can add a bean of type
`StandardServletMultipartResolver` with a name of `multipartResolver`.
@@ -152,9 +152,26 @@ from the iframe. By default, the iframe is set to download the SockJS client
from a CDN location. It is a good idea to configure this option to use
a URL from the same origin as the application.
The following example shows how to configure it:
The following example shows how to do so in Java configuration:
include-code::./WebSocketConfiguration[tag=snippet,indent=0]
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/portfolio").withSockJS()
.setClientLibraryUrl("http://localhost:8080/myapp/js/sockjs-client.js");
}
// ...
}
----
The XML namespace provides a similar option through the `<websocket:sockjs>` element.
NOTE: During initial development, do enable the SockJS client `devel` mode that prevents
the browser from caching SockJS requests (like the iframe) that would otherwise
@@ -290,4 +307,23 @@ jettyHttpClient.setExecutor(new QueuedThreadPool(1000));
The following example shows the server-side SockJS-related properties (see javadoc for details)
that you should also consider customizing:
include-code::./WebSocketConfiguration[tag=snippet,indent=0]
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class WebSocketConfig extends WebSocketMessageBrokerConfigurationSupport {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/sockjs").withSockJS()
.setStreamBytesLimit(512 * 1024) <1>
.setHttpMessageCacheSize(1000) <2>
.setDisconnectDelay(30 * 1000); <3>
}
// ...
}
----
<1> Set the `streamBytesLimit` property to 512KB (the default is 128KB -- `128 * 1024`).
<2> Set the `httpMessageCacheSize` property to 1,000 (the default is `100`).
<3> Set the `disconnectDelay` property to 30 property seconds (the default is five seconds
-- `5 * 1000`).
@@ -59,7 +59,7 @@ destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares":44}^@
{"action":"BUY","ticker":"MMM","shares",44}^@
----
After the execution, the server can
@@ -268,9 +268,9 @@ response to a browser.
`UriComponentsBuilder` exposes encoding options at two levels:
* {spring-framework-api}/web/util/UriComponentsBuilder.html#encode()[UriComponentsBuilder#encode()]:
* {spring-framework-api}/web/util/UriComponentsBuilder.html#encode--[UriComponentsBuilder#encode()]:
Pre-encodes the URI template first and then strictly encodes URI variables when expanded.
* {spring-framework-api}/web/util/UriComponents.html#encode()[UriComponents#encode()]:
* {spring-framework-api}/web/util/UriComponents.html#encode--[UriComponents#encode()]:
Encodes URI components _after_ URI variables are expanded.
Both options replace non-ASCII and illegal characters with escaped octets. However, the first option
+5 -5
View File
@@ -1,11 +1,11 @@
{
"dependencies": {
"antora": "3.2.0-alpha.11",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.2",
"antora": "3.2.0-alpha.4",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-alpha.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "5.3.4",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
"fast-xml-parser": "4.5.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
}
}
+15 -12
View File
@@ -200,7 +200,6 @@
See the License for the specific language governing permissions and
limitations under the License.
=======================================================================
SPRING FRAMEWORK ${version} SUBCOMPONENTS:
@@ -213,7 +212,7 @@ code for these subcomponents is subject to the terms and
conditions of the following licenses.
>>> ASM 9.9.1 (org.ow2.asm:asm:9.9.1):
>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1):
Copyright (c) 2000-2011 INRIA, France Telecom
All rights reserved.
@@ -250,28 +249,32 @@ Copyright (c) 1999-2009, OW2 Consortium <https://www.ow2.org/>
>>> CGLIB 3.3 (cglib:cglib:3.3):
Per the LICENSE file in the CGLIB distribution, CGLIB 3.3 is licensed
under the Apache License, version 2.0, the text of which is included above.
Per the LICENSE file in the CGLIB JAR distribution downloaded from
https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar,
CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which
is included above.
>>> JavaPoet 0.10.0 (com.palantir.javapoet:javapoet:0.10.0):
>>> JavaPoet 1.13.0 (com.squareup:javapoet:1.13.0):
Per the LICENSE file in the JavaPoet distribution, JavaPoet 0.10.0 is licensed
under the Apache License, version 2.0, the text of which is included above.
Per the LICENSE file in the JavaPoet JAR distribution downloaded from
https://github.com/square/javapoet/archive/refs/tags/javapoet-1.13.0.zip,
JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of
which is included above.
>>> Objenesis 3.5 (org.objenesis:objenesis:3.5):
>>> Objenesis 3.4 (org.objenesis:objenesis:3.4):
Per the LICENSE file in the Objenesis distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.5 is licensed under the
Per the LICENSE file in the Objenesis ZIP distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.4 is licensed under the
Apache License, version 2.0, the text of which is included above.
Per the NOTICE file in the Objenesis distribution downloaded from
Per the NOTICE file in the Objenesis ZIP distribution downloaded from
http://objenesis.org/download.html and corresponding to section 4d of the
Apache License, Version 2.0, in this case for Objenesis:
Objenesis
Copyright 2006-2026 Joe Walnes, Henri Tremblay, Leonardo Mesquita
Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita
===============================================================================
@@ -1,54 +0,0 @@
/*
* Copyright 2025-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.testing.resttestclient.assertj;
import org.junit.jupiter.api.Test;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.client.ExchangeResult;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.test.web.servlet.client.assertj.RestTestClientResponse;
import static org.assertj.core.api.Assertions.assertThat;
public class AssertJTests {
RestTestClient client;
@Test
void withSpec() {
// tag::withSpec[]
RestTestClient.ResponseSpec spec = client.get().uri("/persons").exchange();
RestTestClientResponse response = RestTestClientResponse.from(spec);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// end::withSpec[]
}
@Test
void withResult() {
// tag::withResult[]
ExchangeResult result = client.get().uri("/persons").exchange().returnResult();
RestTestClientResponse response = RestTestClientResponse.from(result);
assertThat(response).hasStatusOk();
assertThat(response).hasContentTypeCompatibleWith(MediaType.TEXT_PLAIN);
// end::withResult[]
}
}
@@ -1,37 +0,0 @@
/*
* Copyright 2025-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.testing.resttestclient.contextconfig;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.springframework.web.context.WebApplicationContext;
@SpringJUnitConfig(WebConfig.class) // Specify the configuration to load
public class RestClientContextTests {
RestTestClient client;
@BeforeEach
void setUp(WebApplicationContext context) { // Inject the configuration
// Create the `RestTestClient`
client = RestTestClient.bindToApplicationContext(context).build();
}
}
@@ -1,20 +0,0 @@
/*
* Copyright 2025-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.testing.resttestclient.contextconfig;
public class WebConfig {
}
@@ -1,50 +0,0 @@
/*
* Copyright 2025-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.testing.resttestclient.json;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.client.RestTestClient;
public class JsonTests {
RestTestClient client;
@Test
void jsonBody() {
// tag::jsonBody[]
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.json("{\"name\":\"Jane\"}");
// end::jsonBody[]
}
@Test
void jsonPath() {
// tag::jsonPath[]
client.get().uri("/persons")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$[0].name").isEqualTo("Jane")
.jsonPath("$[1].name").isEqualTo("Jason");
// end::jsonPath[]
}
}
@@ -1,56 +0,0 @@
/*
* Copyright 2025-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.testing.resttestclient.nocontent;
import org.junit.jupiter.api.Test;
import org.springframework.test.web.servlet.client.RestTestClient;
public class NoContentTests {
RestTestClient client;
@Test
void emptyBody() {
Person person = new Person("Jane");
// tag::emptyBody[]
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody().isEmpty();
// end::emptyBody[]
}
@Test
void ignoreBody() {
Person person = new Person("Jane");
// tag::ignoreBody[]
client.post().uri("/persons")
.body(person)
.exchange()
.expectStatus().isCreated()
.expectBody(Void.class);
// end::ignoreBody[]
}
record Person(String name) {
}
}
@@ -1,85 +0,0 @@
/*
* Copyright 2025-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.testing.resttestclient.workflow;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.client.EntityExchangeResult;
import org.springframework.test.web.servlet.client.RestTestClient;
public class RestClientWorkflowTests {
RestTestClient client;
@Test
void workflowTest() {
// tag::test[]
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaType.APPLICATION_JSON)
.expectBody();
// end::test[]
}
@Test
void softAssertions() {
// tag::soft-assertions[]
client.get().uri("/persons/1")
.accept(MediaType.APPLICATION_JSON)
.exchange()
.expectAll(
spec -> spec.expectStatus().isOk(),
spec -> spec.expectHeader().contentType(MediaType.APPLICATION_JSON)
);
// end::soft-assertions[]
}
@Test
void consumeWith() {
// tag::consume[]
client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.consumeWith(result -> {
// custom assertions (for example, AssertJ)...
});
// end::consume[]
}
@Test
void returnResult() {
// tag::result[]
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
.exchange()
.expectStatus().isOk()
.expectBody(Person.class)
.returnResult();
Person person = result.getResponseBody();
HttpHeaders requestHeaders = result.getRequestHeaders();
// end::result[]
}
record Person(String name) {
}
}
@@ -1,41 +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.mvccorsglobal;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
// tag::snippet[]
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://domain1.com", "https://domain2.com")
.allowedMethods("GET", "PUT")
.allowedHeaders("header1", "header2", "header3")
.exposedHeaders("header1", "header2")
.allowCredentials(true)
.maxAge(3600);
// Add more mappings...
}
}
// end::snippet[]
@@ -1,69 +0,0 @@
/*
* Copyright 2026-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.webflux.controller.annmethods.partevent;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.multipart.FilePartEvent;
import org.springframework.http.codec.multipart.FormPartEvent;
import org.springframework.http.codec.multipart.PartEvent;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@RestController
public class PartEventController {
// tag::snippet[]
@PostMapping("/")
public void handle(@RequestBody Flux<PartEvent> allPartEvents) {
// The final PartEvent for a particular part will have isLast() set to true, and can be
// followed by additional events belonging to subsequent parts.
// This makes the isLast property suitable as a predicate for the Flux::windowUntil operator, to
// split events from all parts into windows that each belong to a single part.
allPartEvents.windowUntil(PartEvent::isLast)
// The Flux::switchOnFirst operator allows you to see whether you are handling
// a form field or file upload
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) {
String value = formEvent.value();
// Handling of the form field
}
else if (event instanceof FilePartEvent fileEvent) {
String filename = fileEvent.filename();
// The body contents must be completely consumed, relayed, or released to avoid memory leaks
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
// Handling of the file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
return Mono.empty();
}));
}
// end::snippet[]
}
@@ -1,19 +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.webfluxfnhandlerclasses;
public record Person(String name) { }
@@ -1,67 +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.webfluxfnhandlerclasses;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
// tag::snippet[]
public class PersonHandler {
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
// listPeople is a handler function that returns all Person objects found
// in the repository as JSON
public Mono<ServerResponse> listPeople(ServerRequest request) {
Flux<Person> people = repository.allPeople();
return ok().contentType(APPLICATION_JSON).body(people, Person.class);
}
// createPerson is a handler function that stores a new Person contained
// in the request body.
// Note that PersonRepository.savePerson(Person) returns Mono<Void>: an empty
// Mono that emits a completion signal when the person has been read from the
// request and stored. So we use the build(Publisher<Void>) method to send a
// response when that completion signal is received (that is, when the Person
// has been saved)
public Mono<ServerResponse> createPerson(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class);
return ok().build(repository.savePerson(person));
}
// getPerson is a handler function that returns a single person, identified by
// the id path variable. We retrieve that Person from the repository and create
// a JSON response, if it is found. If it is not found, we use switchIfEmpty(Mono<T>)
// to return a 404 Not Found response.
public Mono<ServerResponse> getPerson(ServerRequest request) {
int personId = Integer.valueOf(request.pathVariable("id"));
return repository.getPerson(personId)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
}
// end::snippet[]
@@ -1,29 +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.webfluxfnhandlerclasses;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface PersonRepository {
Flux<Person> allPeople();
Mono<Void> savePerson(Mono<Person> person);
Mono<Person> getPerson(int id);
}
@@ -1,55 +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.webfluxfnhandlerfilterfunction;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonHandler;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.http.HttpStatus.UNAUTHORIZED;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
public class RouterConfiguration {
public RouterFunction<ServerResponse> route(PersonHandler handler) {
// tag::snippet[]
SecurityManager securityManager = getSecurityManager();
RouterFunction<ServerResponse> route = RouterFunctions.route()
.path("/person", b1 -> b1
.nest(accept(APPLICATION_JSON), b2 -> b2
.GET("/{id}", handler::getPerson)
.GET(handler::listPeople))
.POST(handler::createPerson))
.filter((request, next) -> {
if (securityManager.allowAccessTo(request.path())) {
return next.handle(request);
}
else {
return ServerResponse.status(UNAUTHORIZED).build();
}
}).build();
// end::snippet[]
return route;
}
SecurityManager getSecurityManager() {
return path -> false;
}
}
@@ -1,22 +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.webfluxfnhandlerfilterfunction;
public interface SecurityManager {
boolean allowAccessTo(String path);
}
@@ -1,58 +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.webfluxfnhandlervalidation;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.docs.web.webfluxfnhandlerclasses.PersonRepository;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebInputException;
import reactor.core.publisher.Mono;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;
// tag::snippet[]
public class PersonHandler {
// Create Validator instance
private final Validator validator = new PersonValidator();
private final PersonRepository repository;
public PersonHandler(PersonRepository repository) {
this.repository = repository;
}
public Mono<ServerResponse> createPerson(ServerRequest request) {
// Apply validation
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
return ok().build(repository.savePerson(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, "person");
validator.validate(person, errors);
if (errors.hasErrors()) {
// Raise exception for a 400 response
throw new ServerWebInputException(errors.toString());
}
}
}
// end::snippet[]
@@ -1,34 +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.webfluxfnhandlervalidation;
import org.springframework.docs.web.webfluxfnhandlerclasses.Person;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
public class PersonValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return Person.class.isAssignableFrom(clazz);
}
@Override
public void validate(Object target, Errors errors) {
// Validation logic
}
}
@@ -1,36 +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.webfluxfnpredicates;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
public class RouterConfiguration {
public RouterFunction<ServerResponse> route() {
// tag::snippet[]
RouterFunction<ServerResponse> route = RouterFunctions.route()
.GET("/hello-world", accept(MediaType.TEXT_PLAIN),
request -> ServerResponse.ok().bodyValue("Hello World")).build();
// end::snippet[]
return route;
}
}
@@ -1,55 +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.webfluxfnrequest;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.codec.multipart.FilePartEvent;
import org.springframework.http.codec.multipart.FormPartEvent;
import org.springframework.http.codec.multipart.PartEvent;
import org.springframework.web.reactive.function.server.ServerRequest;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class PartEventHandler {
public void handle(ServerRequest request) {
// tag::snippet[]
request.bodyToFlux(PartEvent.class).windowUntil(PartEvent::isLast)
.concatMap(p -> p.switchOnFirst((signal, partEvents) -> {
if (signal.hasValue()) {
PartEvent event = signal.get();
if (event instanceof FormPartEvent formEvent) {
String value = formEvent.value();
// handle form field
}
else if (event instanceof FilePartEvent fileEvent) {
String filename = fileEvent.filename();
Flux<DataBuffer> contents = partEvents.map(PartEvent::content);
// handle file upload
}
else {
return Mono.error(new RuntimeException("Unexpected event: " + event));
}
}
else {
return partEvents; // either complete or error signal
}
return Mono.empty();
}));
// end::snippet[]
}
}
@@ -1,33 +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.webfluxfnrequest;
import reactor.core.publisher.Mono;
import org.springframework.web.reactive.function.server.ServerRequest;
public class RequestHandler {
public void bind(ServerRequest request) {
// tag::snippet[]
Mono<Pet> pet = request.bind(Pet.class, dataBinder -> dataBinder.setAllowedFields("name"));
// end::snippet[]
}
record Pet(String name) { }
}

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