mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
313 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 16ecbfc9fe | |||
| 85c18caf25 | |||
| 22bd8bd704 | |||
| 9b10bb5e08 | |||
| 727ccd04ef | |||
| a9f447e8d7 | |||
| 0841e79e32 | |||
| a1868d3e9e | |||
| e5aac66157 | |||
| 8bf85d2596 | |||
| 508b31da4f | |||
| d291272736 | |||
| db01f07037 | |||
| b9e190e313 | |||
| d712ec3d49 | |||
| 5baa4fdd69 | |||
| 9273a11a2c | |||
| 7ea11baff9 | |||
| 655fd2e2bf | |||
| a31574a0a2 | |||
| 44f3e7b427 | |||
| e8e24e65d2 | |||
| 9a002a77b2 | |||
| 97e96895db | |||
| e98c2fe488 | |||
| 75705bcbb1 | |||
| 47a2e7059e | |||
| 14b6339351 | |||
| bc2e89a786 | |||
| 4abbddf601 | |||
| 926bcbd9a6 | |||
| cb8ed43be1 | |||
| 1703388074 | |||
| d84c4a39e2 | |||
| 6f03c186b9 | |||
| e10d37ad54 | |||
| 4734837fe4 | |||
| 4a13655aa5 | |||
| 5ef9972528 | |||
| a9b1d6335e | |||
| 98d4046e4b | |||
| 282fee5c95 | |||
| 0d4737aeec | |||
| 1ef8734881 | |||
| 6dfa54b8ea | |||
| 2d3a2c5cbd | |||
| 62e590a437 | |||
| 7f29df4d79 | |||
| 8fb119df1e | |||
| 1bb0fdda6b | |||
| 6507367d10 | |||
| fee7e6b7e2 | |||
| 35c4973504 | |||
| 77b54c7409 | |||
| ff9ba15762 | |||
| c33b82f2a1 | |||
| ec459aefbf | |||
| b227701473 | |||
| 7a85ce959b | |||
| e93e55b456 | |||
| e22b1648bf | |||
| 78967d4bd6 | |||
| 1d2351c87b | |||
| 02dc4b6342 | |||
| 143828e27c | |||
| 7998bc9de2 | |||
| 8a1a9d2ab2 | |||
| 394a97d7e0 | |||
| 05ffe8249f | |||
| 20970a4a37 | |||
| cb4b87cce2 | |||
| 1d470fc375 | |||
| 7fc619acdc | |||
| e5f8c5b7ae | |||
| 3666ad9d7f | |||
| 652d509cfc | |||
| 153c378bb2 | |||
| cdbaa7f3a7 | |||
| d1b0bdb3d5 | |||
| 59a9d0fcb9 | |||
| db9045ae28 | |||
| 7c2159fbe9 | |||
| f2d3da3f32 | |||
| f1c86b1bbc | |||
| 7fde8a6fa3 | |||
| 033c309d39 | |||
| ac243fae65 | |||
| ad19c1ac82 | |||
| 29d125e659 | |||
| d31bd8139d | |||
| fb10455d3c | |||
| 40350653e1 | |||
| a08a4d6068 | |||
| 6fd84e4c2f | |||
| 516bf1c606 | |||
| 32b55e68fc | |||
| b1f427f137 | |||
| f52481493c | |||
| 149397ed10 | |||
| e1d3076570 | |||
| 16f4b23c32 | |||
| 54c3d01270 | |||
| 799a520c35 | |||
| bf5386712c | |||
| 5d33de943b | |||
| 7da65fe4fc | |||
| 0d612b3b1d | |||
| 4dacc64a30 | |||
| ba47dd0714 | |||
| 79386b0e83 | |||
| d1bfe03358 | |||
| 4bc9f68451 | |||
| ce0e37536b | |||
| b0ac8df13e | |||
| 80a57c2468 | |||
| 02e05b40b0 | |||
| c8c99d0464 | |||
| 867dd0e972 | |||
| 2c96f18439 | |||
| 96fb5d72eb | |||
| bc01eeb433 | |||
| 77c24b28b6 | |||
| e809395635 | |||
| 801035bea7 | |||
| 9e4a54bb99 | |||
| e56f7c1e53 | |||
| 65bdc78a1e | |||
| fd3bbb057e | |||
| c47da1247a | |||
| 22cf7958a5 | |||
| ab314aadd9 | |||
| 611811d8fc | |||
| 0c9f40b171 | |||
| 5b4eda3d20 | |||
| af8fd8f07d | |||
| 345fa51e77 | |||
| c05ea841bc | |||
| dab28b5dce | |||
| 1c4b20287f | |||
| 8fbd147f0b | |||
| 1c56ec3f7e | |||
| 65565de1a8 | |||
| b3c26255b1 | |||
| 66962875f9 | |||
| 5490ba94ae | |||
| 17e02804ba | |||
| 357205723f | |||
| c4c167c0e1 | |||
| 4119ecc25b | |||
| 9ebfdb8b6a | |||
| c4cb55fbde | |||
| 7917ae53e8 | |||
| 693ec8219f | |||
| 9b95482557 | |||
| a029dfc6a8 | |||
| a921c487b4 | |||
| 3f01830d11 | |||
| 64d95d7f3e | |||
| 5a4b1bd1d1 | |||
| a91402dcc6 | |||
| 0014e0b3c9 | |||
| 49da766551 | |||
| 50bbe6191a | |||
| a8c33c13e2 | |||
| b164db35c1 | |||
| 385e62dbf0 | |||
| 4d4adeb2c6 | |||
| 7aa862f5ec | |||
| 2d2809f45c | |||
| 3f390d55f0 | |||
| 2b96a61063 | |||
| c55351aa4a | |||
| 48bedd0af1 | |||
| c1b0e68d6c | |||
| fe2113f5a3 | |||
| 2ff2a89f22 | |||
| 14641c4299 | |||
| 62fd09dfa5 | |||
| 9df19dedaf | |||
| 01a57a7219 | |||
| b5c2003c51 | |||
| 5f5da06467 | |||
| 9f19b4074d | |||
| a784eb0ab4 | |||
| fa404063ab | |||
| 948af8bcc6 | |||
| e3997e3060 | |||
| 0323218129 | |||
| a62699ccfc | |||
| dd6faf62bb | |||
| cbc2c63abc | |||
| 2f83e6b4b5 | |||
| 19c2506476 | |||
| ee4780fab6 | |||
| 8e6d44349d | |||
| ceb6ab3c74 | |||
| 7140ab5bca | |||
| 615c25d4fa | |||
| 3df66580f3 | |||
| 9711db787e | |||
| d3a385d222 | |||
| 38a397873e | |||
| 73fa708905 | |||
| 23a532ed82 | |||
| e0db67aa3b | |||
| 1370a833ed | |||
| 8d4320e9ef | |||
| c53a00a6c4 | |||
| 00606d2171 | |||
| 3e5d8d184a | |||
| b69cbad38d | |||
| 39106b5811 | |||
| 2f91baa71c | |||
| d8e303f95b | |||
| 8ff89ffda2 | |||
| dcf5d69d94 | |||
| 7d33a87278 | |||
| b878771dca | |||
| 06f0f2f317 | |||
| f16474d585 | |||
| dfb17d84d6 | |||
| eea9130ea6 | |||
| d85ac300bb | |||
| d4d10dc75d | |||
| 169465cce1 | |||
| c4d5e3c57d | |||
| 38f5f4de8e | |||
| 644072f7ea | |||
| 859940b824 | |||
| 992ae2ea6b | |||
| 589a10e6e6 | |||
| 79bd2b11da | |||
| 0624c3a597 | |||
| bf695c058d | |||
| a1204a405a | |||
| e0aa116217 | |||
| 7fdda1cf0f | |||
| 05b98b0a32 | |||
| 46efd930f0 | |||
| a9027ddf76 | |||
| 5551292639 | |||
| 1240962d68 | |||
| 9ef4ceb047 | |||
| c7413cbdfc | |||
| e94aaab288 | |||
| c5044bfdbc | |||
| 9abe4b46ca | |||
| 192da6229c | |||
| e1402fd252 | |||
| 253eb28458 | |||
| 97cd9dd03b | |||
| 3027d78f40 | |||
| d752e80356 | |||
| e7f019bd3f | |||
| 6939cb90df | |||
| 3ddaf51f5d | |||
| 6a28f06b2e | |||
| 5ecece91dc | |||
| 13d7a6c480 | |||
| 6e66af15a6 | |||
| cd8e38448c | |||
| 93ce99ea06 | |||
| bf8a1600e5 | |||
| 96eee8c8b1 | |||
| b8ed4f2ba8 | |||
| 0048f01074 | |||
| 0b904a878d | |||
| 9c78f84915 | |||
| a55309d478 | |||
| 46fb7c026c | |||
| d36244ee56 | |||
| 4b07edbaeb | |||
| eb3bfc0d65 | |||
| d077e043d6 | |||
| 7212fbe36a | |||
| 5b6c231788 | |||
| aeb0115605 | |||
| 08104100d3 | |||
| 214cb2c1e1 | |||
| 26e0f550b0 | |||
| 012fb29097 | |||
| 92a43c007a | |||
| fc1ff88ede | |||
| fc2e1dd2e5 | |||
| ffd2e03a9f | |||
| fd6e15bb4d | |||
| 85c6fb0fd0 | |||
| 0b2bb7e751 | |||
| faa17abdae | |||
| 89ca8e6976 | |||
| 7353ab41d2 | |||
| f1db0ef036 | |||
| e9a4b93477 | |||
| 86e89d53a9 | |||
| c813577908 | |||
| ec6b7730a2 | |||
| fc29d88778 | |||
| 033df68da6 | |||
| df5b06dabc | |||
| 3d8d7ffa1e | |||
| 6d5a512d90 | |||
| d4e4250bb1 | |||
| 93bc022a46 | |||
| a4c72c8fcf | |||
| 3d7862abb6 | |||
| 7fd0c5041d | |||
| d99ace81c1 | |||
| ed451c107f | |||
| 452257eb96 | |||
| a7863a0877 | |||
| 1da0e13a2b | |||
| ee6a156275 | |||
| 0b6fe8dcc5 |
@@ -1,6 +1,13 @@
|
||||
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@v4
|
||||
uses: actions/setup-java@v5
|
||||
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@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
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@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6
|
||||
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
|
||||
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@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0
|
||||
uses: spring-io/central-publish-action@0c03960e9b16fdfe70e2443e1d5393cbc3a35622 # v0.3.0
|
||||
with:
|
||||
token: ${{ inputs.central-token-password }}
|
||||
token-name: ${{ inputs.central-token-username }}
|
||||
|
||||
@@ -18,9 +18,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- 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@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
|
||||
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- 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@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-reports
|
||||
path: '**/build/reports/'
|
||||
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
git config --global core.longPaths true
|
||||
Stop-Service -name Docker
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- name: Build
|
||||
id: build
|
||||
uses: ./.github/actions/build
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: docs-build
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- 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@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
|
||||
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
|
||||
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@v6
|
||||
- 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@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
|
||||
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
|
||||
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@v6
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
|
||||
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
- 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@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
|
||||
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
|
||||
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@v6
|
||||
- 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@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
|
||||
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
|
||||
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@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
uses: actions/checkout@v6
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
|
||||
@@ -30,23 +30,23 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Release Verification Tests
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: 'v0.0.2'
|
||||
repository: spring-projects/spring-framework-release-verification
|
||||
token: ${{ secrets.token }}
|
||||
- name: Check Out Send Notification Action
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: send-notification
|
||||
sparse-checkout: .github/actions/send-notification
|
||||
- name: Set Up Java
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
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@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: build-reports
|
||||
path: '**/build/reports/'
|
||||
|
||||
+6
-5
@@ -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.8' apply false
|
||||
id 'io.spring.nullability' version '0.0.11' apply false
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -70,13 +70,14 @@ 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/5.6/javadocs/",
|
||||
"https://docs.hibernate.org/orm/7.2/javadocs/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
|
||||
"https://hc.apache.org/httpcomponents-client-5.6.x/current/httpclient5/apidocs/",
|
||||
"https://projectreactor.io/docs/core/release/api/",
|
||||
"https://projectreactor.io/docs/test/release/api/",
|
||||
"https://junit.org/junit4/javadoc/4.13.2/",
|
||||
"https://docs.junit.org/6.0.1/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
"https://docs.junit.org/6.0.2/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.4-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("12.2.0");
|
||||
checkstyle.setToolVersion("13.2.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
+1
-3
@@ -49,7 +49,6 @@ 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.
|
||||
@@ -64,8 +63,7 @@ public abstract class ArchitectureCheck extends DefaultTask {
|
||||
public ArchitectureCheck() {
|
||||
getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName()));
|
||||
getProhibitObjectsRequireNonNull().convention(true);
|
||||
getRules().addAll(packageInfoShouldBeNullMarked(),
|
||||
classesShouldNotImportForbiddenTypes(),
|
||||
getRules().addAll(classesShouldNotImportForbiddenTypes(),
|
||||
javaClassesShouldNotImportKotlinAnnotations(),
|
||||
allPackagesShouldBeFreeOfTangles(),
|
||||
noClassesShouldCallStringToLowerCaseWithoutLocale(),
|
||||
|
||||
@@ -46,13 +46,6 @@ 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
|
||||
DirectoryProperty getOutputDirectory() {
|
||||
public DirectoryProperty getOutputDirectory() {
|
||||
return this.outputDirectory;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ dependencies {
|
||||
}
|
||||
}
|
||||
|
||||
def springAspectsOutput = project(":spring-aspects").sourceSets.main.output
|
||||
javadoc {
|
||||
javadocTool.set(javaToolchains.javadocToolFor({
|
||||
languageVersion = JavaLanguageVersion.of(25)
|
||||
@@ -49,7 +50,7 @@ javadoc {
|
||||
doFirst {
|
||||
classpath += files(
|
||||
// ensure the javadoc process can resolve types compiled from .aj sources
|
||||
project(":spring-aspects").sourceSets.main.output
|
||||
springAspectsOutput
|
||||
)
|
||||
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
|
||||
}
|
||||
|
||||
@@ -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,12 +80,14 @@ 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: 63 KiB After Width: | Height: | Size: 158 KiB |
@@ -17,6 +17,7 @@
|
||||
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"
|
||||
@@ -36,7 +37,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path9164" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -47,7 +48,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path8836" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -58,7 +59,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path8520" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -69,7 +70,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path8216" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -80,7 +81,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path7924" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -91,7 +92,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path7644" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -102,7 +103,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path7375" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -113,7 +114,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path7119" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -124,7 +125,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path6875" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -135,7 +136,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path6643" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -146,7 +147,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path6423" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -157,7 +158,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path6215" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -168,7 +169,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path6019" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -179,7 +180,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path5835" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -190,7 +191,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path5663" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -201,7 +202,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path5503" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -212,7 +213,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path5355" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -223,7 +224,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path5219" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -234,7 +235,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path5095" /></marker><marker
|
||||
inkscape:isstock="true"
|
||||
style="overflow:visible;"
|
||||
@@ -245,7 +246,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:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
id="path4983" /></marker><marker
|
||||
inkscape:stockid="Arrow2Mend"
|
||||
orient="auto"
|
||||
@@ -256,7 +257,7 @@
|
||||
inkscape:isstock="true"
|
||||
inkscape:collect="always"><path
|
||||
id="path7394"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;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"
|
||||
@@ -267,7 +268,7 @@
|
||||
style="overflow:visible;"
|
||||
inkscape:isstock="true"><path
|
||||
id="path8123"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;stroke-opacity:1;fill:#333333;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"
|
||||
@@ -279,7 +280,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:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
@@ -290,7 +291,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:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
|
||||
inkscape:stockid="Arrow1Mend"
|
||||
orient="auto"
|
||||
@@ -301,7 +302,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:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
transform="scale(0.4) rotate(180) translate(10,0)" /></marker><marker
|
||||
inkscape:stockid="Arrow2Lend"
|
||||
orient="auto"
|
||||
@@ -311,7 +312,7 @@
|
||||
style="overflow:visible;"
|
||||
inkscape:isstock="true"><path
|
||||
id="path7388"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#000000;stroke-opacity:1"
|
||||
style="fill-rule:evenodd;stroke-width:0.625;stroke-linejoin:round;stroke:#333333;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"
|
||||
@@ -323,7 +324,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:#000000;stroke-width:1pt;stroke-opacity:1;fill:#000000;fill-opacity:1"
|
||||
style="fill-rule:evenodd;stroke:#333333;stroke-width:1pt;stroke-opacity:1;fill:#333333;fill-opacity:1"
|
||||
transform="scale(0.8) rotate(180) translate(12.5,0)" /></marker></defs><sodipodi:namedview
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
@@ -352,9 +353,9 @@
|
||||
v:viewMarkup="false" /><style
|
||||
type="text/css"
|
||||
id="style5501"><![CDATA[
|
||||
.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}
|
||||
.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}
|
||||
.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
|
||||
@@ -363,20 +364,22 @@
|
||||
v:groupContext="foregroundPage"
|
||||
id="g5503"
|
||||
transform="matrix(0.99998201,0,0,1.0824094,-40.812382,-98.908648)"><rect
|
||||
style="fill:#dde2cd;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:#F8F9FA;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6599"
|
||||
width="382.68423"
|
||||
height="146.09897"
|
||||
x="87.884865"
|
||||
y="148.26482" /><v:userDefs><v:ud
|
||||
y="148.26482"
|
||||
rx="8" /><v:userDefs><v:ud
|
||||
v:nameU="SchemeName"
|
||||
v:val="VT4(Default)" /></v:userDefs><rect
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:none;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect5725"
|
||||
width="464.31128"
|
||||
height="374.11411"
|
||||
x="41.684383"
|
||||
y="112.3262" /><title
|
||||
y="112.3262"
|
||||
rx="8" /><title
|
||||
id="title5505">Page-1</title><v:pageProperties
|
||||
v:drawingScale="0.0393701"
|
||||
v:pageScale="0.0393701"
|
||||
@@ -385,15 +388,16 @@
|
||||
v:shadowOffsetY="-8.50394" /><v:layer
|
||||
v:name="Connector"
|
||||
v:index="0" /><rect
|
||||
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"
|
||||
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"
|
||||
id="rect5715"
|
||||
width="322.8194"
|
||||
height="43.63184"
|
||||
x="119.95335"
|
||||
y="-135.66222"
|
||||
transform="scale(1,-1)" /><text
|
||||
transform="scale(1,-1)"
|
||||
rx="8" /><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: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"
|
||||
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"
|
||||
x="168.843"
|
||||
y="124.32391"
|
||||
id="text5717"
|
||||
@@ -401,12 +405,12 @@
|
||||
transform="scale(1.0403984,0.96117025)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan5719"
|
||||
x="168.843"
|
||||
x="172.843"
|
||||
y="124.32391"
|
||||
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>
|
||||
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>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
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"
|
||||
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"
|
||||
x="89.770851"
|
||||
y="181.20923"
|
||||
id="text6589"
|
||||
@@ -414,12 +418,12 @@
|
||||
transform="scale(1.0403984,0.96117025)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan6591"
|
||||
x="89.770851"
|
||||
x="106.770851"
|
||||
y="181.20923"
|
||||
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>
|
||||
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>
|
||||
<text
|
||||
xml:space="preserve"
|
||||
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"
|
||||
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"
|
||||
x="260.00443"
|
||||
y="198.41273"
|
||||
id="text6593"
|
||||
@@ -429,36 +433,39 @@
|
||||
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:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle">(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:Helvetica, Arial, sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle;fill:#555555">(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:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
|
||||
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"
|
||||
id="tspan6597">and other web-related beans)</tspan></text>
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;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)" /><rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.89166164;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
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"
|
||||
id="rect6622"
|
||||
width="87.843979"
|
||||
height="36.697304"
|
||||
x="223.39864"
|
||||
y="-287.19809"
|
||||
transform="scale(1,-1)" /><rect
|
||||
transform="scale(1,-1)"
|
||||
rx="4" /><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:#000000;stroke-width:0.86203903;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
rx="4" /><text
|
||||
xml:space="preserve"
|
||||
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"
|
||||
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"
|
||||
x="121.24728"
|
||||
y="260.14957"
|
||||
id="text6616"
|
||||
@@ -466,20 +473,20 @@
|
||||
transform="scale(1.0403984,0.96117025)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan6618"
|
||||
x="121.24728"
|
||||
x="124.24728"
|
||||
y="260.14957"
|
||||
style="font-size:11.53426838px">Controllers</tspan></text>
|
||||
style="font-size:11.53426838px;fill:#333333">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: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"
|
||||
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"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-size:11.53426838px"
|
||||
style="font-size:11.53426838px;fill:#333333"
|
||||
y="282.70709"
|
||||
x="219.61203"
|
||||
x="221.61203"
|
||||
id="tspan6626"
|
||||
sodipodi:role="line">ViewResolver</tspan></text>
|
||||
<rect
|
||||
@@ -489,9 +496,10 @@
|
||||
height="36.577778"
|
||||
width="114.4539"
|
||||
id="rect6628"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.0161339;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
rx="4" /><text
|
||||
xml:space="preserve"
|
||||
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"
|
||||
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"
|
||||
x="327.51276"
|
||||
y="255.28464"
|
||||
id="text6630"
|
||||
@@ -499,26 +507,27 @@
|
||||
transform="scale(1.0403984,0.96117025)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan6632"
|
||||
x="327.51276"
|
||||
x="332.51276"
|
||||
y="255.28464"
|
||||
style="font-size:11.53426838px">HandlerMapping</tspan></text>
|
||||
style="font-size:11.53426838px;fill:#333333">HandlerMapping</tspan></text>
|
||||
<rect
|
||||
y="338.69724"
|
||||
x="87.803261"
|
||||
height="121.5683"
|
||||
width="382.84744"
|
||||
id="rect6634"
|
||||
style="fill:#dde2cd;fill-opacity:1;stroke:#000000;stroke-width:1.53790233;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
|
||||
style="fill:#F8F9FA;fill-opacity:1;stroke:#6db33f;stroke-width:2;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
rx="8" /><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: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"
|
||||
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"
|
||||
xml:space="preserve"><tspan
|
||||
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"
|
||||
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"
|
||||
y="376.61673"
|
||||
x="108.61351"
|
||||
x="113.61351"
|
||||
id="tspan6638"
|
||||
sodipodi:role="line">Root WebApplicationContext</tspan></text>
|
||||
<text
|
||||
@@ -527,10 +536,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: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"
|
||||
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"
|
||||
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:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
|
||||
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"
|
||||
y="395.35812"
|
||||
x="260.93863"
|
||||
sodipodi:role="line">(containing middle-tier services, datasources, etc.)</tspan></text>
|
||||
@@ -541,43 +550,47 @@
|
||||
height="36.72575"
|
||||
width="82.040657"
|
||||
id="rect6648"
|
||||
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"
|
||||
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"
|
||||
id="rect6650"
|
||||
width="82.040657"
|
||||
height="36.72575"
|
||||
x="167.73116"
|
||||
y="-445.45563"
|
||||
transform="scale(1,-1)" /><text
|
||||
transform="scale(1,-1)"
|
||||
rx="4" /><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: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"
|
||||
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"
|
||||
xml:space="preserve"><tspan
|
||||
style="font-size:11.53426838px"
|
||||
style="font-size:11.53426838px;fill:#333333"
|
||||
y="448.55054"
|
||||
x="175.87148"
|
||||
x="178.87148"
|
||||
id="tspan6654"
|
||||
sodipodi:role="line">Services</tspan></text>
|
||||
<rect
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.88435173;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;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)" /><rect
|
||||
transform="scale(1,-1)"
|
||||
rx="4" /><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:#000000;stroke-width:0.88679528;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1" /><text
|
||||
style="fill:#ffffff;fill-opacity:1;stroke:#6db33f;stroke-width:1.5;stroke-miterlimit:3;stroke-dasharray:none;stroke-opacity:1"
|
||||
rx="4" /><text
|
||||
xml:space="preserve"
|
||||
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"
|
||||
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"
|
||||
x="305.30771"
|
||||
y="448.55054"
|
||||
id="text6660"
|
||||
@@ -585,20 +598,20 @@
|
||||
transform="scale(1.0403984,0.96117025)"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan6662"
|
||||
x="305.30771"
|
||||
x="308.30771"
|
||||
y="448.55054"
|
||||
style="font-size:11.53426838px">Repositories</tspan></text>
|
||||
style="font-size:11.53426838px;fill:#333333">Repositories</tspan></text>
|
||||
<path
|
||||
style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.76895118px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#333333;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:#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)"
|
||||
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)"
|
||||
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: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"
|
||||
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"
|
||||
x="352.55331"
|
||||
y="333.03622"
|
||||
id="text4963"
|
||||
@@ -607,6 +620,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:sans-serif;-inkscape-font-specification:sans-serif;text-align:center;text-anchor:middle"
|
||||
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"
|
||||
id="tspan4965">Delegates if no bean found</tspan></text>
|
||||
</g></svg>
|
||||
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 31 KiB |
@@ -261,7 +261,6 @@
|
||||
**** 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[]
|
||||
@@ -344,6 +343,7 @@
|
||||
**** 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,6 +124,10 @@ 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 on Java 8+.
|
||||
flag for `javac`. Recommended approach.
|
||||
`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 include method calls or even constructors within the target
|
||||
If your interception needs to 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,10 +19,6 @@ 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,16 +1,17 @@
|
||||
[[beans-standard-annotations]]
|
||||
= Using JSR 330 Standard Annotations
|
||||
= Using JSR-330 Standard Annotations
|
||||
|
||||
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.
|
||||
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/`),
|
||||
|
||||
[NOTE]
|
||||
=====
|
||||
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:
|
||||
If you use Maven, you can add the following dependency to your `pom.xml` file.
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -26,13 +27,14 @@ You can add the following dependency to your file pom.xml:
|
||||
[[beans-inject-named]]
|
||||
== Dependency Injection with `@Inject` and `@Named`
|
||||
|
||||
Instead of `@Autowired`, you can use `@jakarta.inject.Inject` as follows:
|
||||
Instead of using `@Autowired` for dependency injection, you may optionally choose to use
|
||||
`@jakarta.inject.Inject` as follows.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject;
|
||||
|
||||
@@ -54,7 +56,7 @@ Java::
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject
|
||||
|
||||
@@ -72,17 +74,19 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Provider;
|
||||
@@ -105,9 +109,10 @@ Java::
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.inject.Provider
|
||||
|
||||
class SimpleMovieLister {
|
||||
|
||||
@@ -123,14 +128,15 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
@@ -150,7 +156,7 @@ Java::
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.inject.Named
|
||||
@@ -170,12 +176,15 @@ 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 pair of examples show how to use `@Inject` and
|
||||
`@Nullable`:
|
||||
`@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.
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject;
|
||||
import java.util.Optional;
|
||||
|
||||
public class SimpleMovieLister {
|
||||
|
||||
@Inject
|
||||
@@ -189,8 +198,11 @@ a `required` attribute. The following pair of examples show how to use `@Inject`
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public class SimpleMovieLister {
|
||||
|
||||
@Inject
|
||||
@@ -202,8 +214,10 @@ Java::
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject
|
||||
|
||||
class SimpleMovieLister {
|
||||
|
||||
@Inject
|
||||
@@ -214,21 +228,21 @@ Kotlin::
|
||||
|
||||
|
||||
[[beans-named]]
|
||||
== `@Named` and `@ManagedBean`: Standard Equivalents to the `@Component` Annotation
|
||||
== `@Named`: Standard Equivalent to the `@Component` Annotation
|
||||
|
||||
Instead of `@Component`, you can use `@jakarta.inject.Named` or `jakarta.annotation.ManagedBean`,
|
||||
as the following example shows:
|
||||
Instead of `@Component` or other Spring stereotype annotations, you may optionally choose
|
||||
to use `@jakarta.inject.Named`, as the following example shows.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
|
||||
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
|
||||
@Named("movieListener")
|
||||
public class SimpleMovieLister {
|
||||
|
||||
private MovieFinder movieFinder;
|
||||
@@ -244,12 +258,12 @@ Java::
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.inject.Named
|
||||
|
||||
@Named("movieListener") // @ManagedBean("movieListener") could be used as well
|
||||
@Named("movieListener")
|
||||
class SimpleMovieLister {
|
||||
|
||||
@Inject
|
||||
@@ -260,14 +274,15 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.inject.Named;
|
||||
@@ -288,7 +303,7 @@ Java::
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
import jakarta.inject.Inject
|
||||
import jakarta.inject.Named
|
||||
@@ -304,14 +319,14 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
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:
|
||||
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.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
[source,java,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = "org.example")
|
||||
@@ -322,7 +337,7 @@ Java::
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes",fold="-imports"]
|
||||
----
|
||||
@Configuration
|
||||
@ComponentScan(basePackages = ["org.example"])
|
||||
@@ -332,55 +347,106 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
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.
|
||||
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`.
|
||||
====
|
||||
|
||||
|
||||
[[beans-standard-annotations-limitations]]
|
||||
== Limitations of JSR-330 Standard Annotations
|
||||
|
||||
When you work with standard annotations, you should know that some significant
|
||||
features are not available, as the following table shows:
|
||||
When you work with JSR-330 standard annotations, you should know that some significant
|
||||
features are not available, as the following table shows.
|
||||
|
||||
[[annotations-comparison]]
|
||||
.Spring component model elements versus JSR-330 variants
|
||||
.Spring component model versus JSR-330 variants
|
||||
|===
|
||||
| Spring| jakarta.inject.*| jakarta.inject restrictions / comments
|
||||
| Spring | JSR-330 | JSR-330 restrictions / comments
|
||||
|
||||
| @Autowired
|
||||
| @Inject
|
||||
| `@Inject` has no 'required' attribute. Can be used with Java 8's `Optional` instead.
|
||||
| `@Autowired`
|
||||
| `@Inject`
|
||||
| `@Inject` has no `required` attribute. Can be used with Java's `Optional` instead.
|
||||
|
||||
| @Component
|
||||
| @Named / @ManagedBean
|
||||
| `@Component`
|
||||
| `@Named`
|
||||
| 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,6 +531,7 @@ 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 be begin with a letter (as defined below), an underscore, or a dollar
|
||||
Variable names must 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 `fluid` style that returns the instance
|
||||
Configuration methods for this class follow the `fluent` 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. 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):
|
||||
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.
|
||||
|
||||
[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.1+, tightly integrated with `HibernateJpaVendorAdapter`
|
||||
now for use with Hibernate ORM 7.x, tightly integrated with `HibernateJpaVendorAdapter`
|
||||
as well as supporting Hibernate's native `SessionFactory.getCurrentSession()` style.
|
||||
====
|
||||
|
||||
|
||||
@@ -3,21 +3,22 @@
|
||||
: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
|
||||
The ahead-of-time cache is a JVM feature introduced in Java 24 via
|
||||
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
|
||||
later if available in the JVM version your are using (Java 24+).
|
||||
latter if available in the JVM version you are using (Java 24+).
|
||||
|
||||
To use this feature, an AOT cache should be created for the particular classpath of the
|
||||
application. It is possible to create this cache on the deployed instance, or during a
|
||||
training run performed for example when packaging the application thanks to an hook-point
|
||||
training run performed for example when packaging the application thanks to a hook-point
|
||||
provided by the Spring Framework to ease such use case. Once the cache is available, users
|
||||
should opt in to use it via a JVM flag.
|
||||
|
||||
NOTE: If you are using Spring Boot, it is highly recommended to leverage its
|
||||
{spring-boot-docs-ref}/packaging/efficient.html#packaging.efficient.unpacking[executable JAR unpacking support]
|
||||
which is designed to fulfill the class loading requirements of both AOT cache and CDS.
|
||||
which is designed to fulfill the class loading requirements of both the AOT cache and CDS.
|
||||
|
||||
== Creating the cache
|
||||
|
||||
@@ -29,14 +30,22 @@ 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 then exit your Spring application once the
|
||||
JVM flag to start and then exit your Spring application once the
|
||||
`ApplicationContext` has refreshed:
|
||||
|
||||
|
||||
--
|
||||
[tabs]
|
||||
======
|
||||
AOT cache::
|
||||
|
||||
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)::
|
||||
+
|
||||
[source,bash,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -55,6 +64,12 @@ 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:
|
||||
@@ -82,7 +97,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 a `aot-cache.log` with
|
||||
an extra attribute: `-Xlog:class+load:file=aot-cache.log`. This creates an `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:
|
||||
|
||||
@@ -95,11 +110,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 can't be enabled or if you have a large number of classes that are not loaded from
|
||||
If the AOT cache cannot be enabled or if you have a large number of classes that are not loaded from
|
||||
the cache, make sure that the following conditions are fulfilled when creating and using the cache:
|
||||
|
||||
- The very same JVM must be used.
|
||||
- The classpath must be specified as a JAR or a list of JARs, and avoid the usage of directories and `*` wildcard characters.
|
||||
- The timestamps of the JARs must be preserved.
|
||||
- When using the cache, the classpath must be the same than the one used to create it, in the same order.
|
||||
Additional JARs or directories can be specified *at the end* (but won't be cached).
|
||||
- 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).
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
[[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
|
||||
|
||||
@@ -172,7 +167,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 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
|
||||
without `jee`:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
@@ -184,7 +179,7 @@ without `jee`:
|
||||
</bean>
|
||||
----
|
||||
|
||||
The following example shows how to 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
|
||||
with `jee`:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
@@ -200,7 +195,7 @@ with `jee`:
|
||||
|
||||
The `<jee:local-slsb/>` element configures 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
|
||||
The following example shows how to configure a reference to a local EJB Stateless Session Bean
|
||||
and a number of properties without `jee`:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
@@ -215,7 +210,7 @@ and a number of properties without `jee`:
|
||||
</bean>
|
||||
----
|
||||
|
||||
The following example shows how to 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
|
||||
and a number of properties with `jee`:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
@@ -234,7 +229,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 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
|
||||
without `jee`:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
@@ -251,7 +246,7 @@ without `jee`:
|
||||
</bean>
|
||||
----
|
||||
|
||||
The following example shows how to 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
|
||||
with `jee`:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
@@ -313,7 +308,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. It it also supports declarative XML-based caching. See
|
||||
and `@Caching` annotations. The `cache` schema 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.
|
||||
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ xref:integration/cache/store-configuration.adoc#cache-store-configuration-jsr107
|
||||
[[cache-store-configuration-caffeine]]
|
||||
== Caffeine Cache
|
||||
|
||||
Caffeine is a Java 8 rewrite of Guava's cache, and its implementation is located in the
|
||||
Caffeine is a 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 on Java 8, so you can associate
|
||||
TIP: `@JmsListener` is a repeatable annotation, 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 Java 8 lambdas
|
||||
* Functional-style, fluent API that takes advantage of lambda expressions
|
||||
* Synchronous and asynchronous interactions
|
||||
* Streaming up to or streaming down from a server
|
||||
|
||||
|
||||
@@ -533,8 +533,7 @@ that returns a value:
|
||||
----
|
||||
|
||||
TIP: `@Async` methods may not only declare a regular `java.util.concurrent.Future` return
|
||||
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
|
||||
type but also `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,14 +3,16 @@
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
The Spring Framework supports various Kotlin constructs, such as instantiating Kotlin classes
|
||||
through primary constructors, immutable classes data binding, and function optional parameters
|
||||
with default values.
|
||||
through primary constructors, data binding for immutable classes, and optional parameters
|
||||
with default values for functions.
|
||||
|
||||
Kotlin parameter names are recognized through a dedicated `KotlinReflectionParameterNameDiscoverer`,
|
||||
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.)
|
||||
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.
|
||||
|
||||
You can declare configuration classes as
|
||||
{kotlin-docs}/nested-classes.html[top level or nested but not inner],
|
||||
since the later requires a reference to the outer class.
|
||||
since the latter requires a reference to the outer class.
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
[[coroutines]]
|
||||
= Coroutines
|
||||
|
||||
Kotlin {kotlin-docs}/coroutines-overview.html[Coroutines] are Kotlin
|
||||
lightweight threads allowing to write non-blocking code in an imperative way. On language side,
|
||||
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,
|
||||
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 { }`]
|
||||
|
||||
+43
-1
@@ -5,6 +5,7 @@ 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`]
|
||||
@@ -14,6 +15,47 @@ 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`
|
||||
|
||||
@@ -187,7 +229,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/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
|
||||
https://docs.junit.org/current/running-tests/configuration-parameters.html[JUnit Platform configuration parameter].
|
||||
|
||||
If the `spring.test.constructor.autowire.mode` property is not set, test class
|
||||
constructors will not be automatically autowired.
|
||||
|
||||
+1
-2
@@ -4,8 +4,7 @@
|
||||
`@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 Java 8-based interface default
|
||||
methods.
|
||||
are not required to be `public` and may be declared on interface default methods.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
+1
-2
@@ -4,8 +4,7 @@
|
||||
`@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 Java 8-based interface default
|
||||
methods.
|
||||
are not required to be `public` and may be declared on interface default methods.
|
||||
|
||||
The following example shows how to use the `@BeforeTransaction` annotation:
|
||||
|
||||
|
||||
+1
-1
@@ -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 8's support for repeatable annotations, where `@Sql` can be
|
||||
in conjunction with Java'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,7 +9,6 @@ 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 Java 8 lambdas, streams, and
|
||||
"Fluent assertions for Java", including support for lambda expressions, 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,55 +42,17 @@ 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.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(WebConfig.class) // <1>
|
||||
class MyTests {
|
||||
include-code::./RestClientContextTests[indent=0]
|
||||
|
||||
RestTestClient client;
|
||||
|
||||
@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]]
|
||||
[[resttestclient.fn-config]]
|
||||
=== Bind to Router Function
|
||||
|
||||
This setup allows you to test xref:web/webmvc-functional.adoc[functional endpoints] via
|
||||
@@ -115,7 +77,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:
|
||||
@@ -139,7 +101,7 @@ Kotlin::
|
||||
|
||||
|
||||
|
||||
[[resttestclient-client-config]]
|
||||
[[resttestclient.client-config]]
|
||||
=== Client Config
|
||||
|
||||
In addition to the server setup options described earlier, you can also configure client
|
||||
@@ -170,7 +132,7 @@ Kotlin::
|
||||
|
||||
|
||||
|
||||
[[resttestclient-tests]]
|
||||
[[resttestclient.tests]]
|
||||
== Writing Tests
|
||||
|
||||
xref:integration/rest-clients.adoc#rest-restclient[`RestClient`] and `RestTestClient` have
|
||||
@@ -182,70 +144,22 @@ 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:
|
||||
|
||||
[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);
|
||||
----
|
||||
include-code::./RestClientWorkflowTests[tag=test,indent=0]
|
||||
|
||||
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.
|
||||
|
||||
[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)
|
||||
);
|
||||
----
|
||||
include-code::./RestClientWorkflowTests[tag=soft-assertions,indent=0]
|
||||
|
||||
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:
|
||||
|
||||
@@ -256,124 +170,34 @@ 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:
|
||||
|
||||
[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)...
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./RestClientWorkflowTests[tag=consume,indent=0]
|
||||
|
||||
Or you can exit the workflow and obtain a `EntityExchangeResult`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
EntityExchangeResult<Person> result = client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(Person.class)
|
||||
.returnResult();
|
||||
----
|
||||
include-code::./RestClientWorkflowTests[tag=result,indent=0]
|
||||
|
||||
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:
|
||||
|
||||
[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()
|
||||
----
|
||||
======
|
||||
include-code::./NoContentTests[tag=emptyBody,indent=0]
|
||||
|
||||
If you want to ignore the response content, the following releases the content without any assertions:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound()
|
||||
.expectBody(Void.class);
|
||||
----
|
||||
include-code::./NoContentTests[tag=ignoreBody,indent=0]
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/123")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound
|
||||
.expectBody<Unit>()
|
||||
----
|
||||
======
|
||||
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.
|
||||
|
||||
|
||||
[[resttestclient-json]]
|
||||
[[resttestclient.json]]
|
||||
==== JSON Content
|
||||
|
||||
You can use `expectBody()` without a target type to perform assertions on the raw
|
||||
@@ -381,126 +205,25 @@ content rather than through higher level Object(s).
|
||||
|
||||
To verify the full JSON content with https://jsonassert.skyscreamer.org[JSONAssert]:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody()
|
||||
.json("{\"name\":\"Jane\"}")
|
||||
----
|
||||
include-code::./JsonTests[tag=jsonBody,indent=0]
|
||||
|
||||
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]:
|
||||
|
||||
[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")
|
||||
----
|
||||
======
|
||||
include-code::./JsonTests[tag=jsonPath,indent=0]
|
||||
|
||||
|
||||
|
||||
[[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:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ResponseSpec spec = client.get().uri("/persons").exchange();
|
||||
include-code::./AssertJTests[tag=withSpec,indent=0]
|
||||
|
||||
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:
|
||||
|
||||
[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)
|
||||
// ...
|
||||
----
|
||||
======
|
||||
include-code::./AssertJTests[tag=withResult,indent=0]
|
||||
|
||||
-11
@@ -62,17 +62,6 @@ script by setting a JVM system property named `spring.test.context.cache.maxSize
|
||||
alternative, you can set the same property via the
|
||||
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
|
||||
|
||||
As of Spring Framework 7.0, an application context stored in the context cache will be
|
||||
_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
|
||||
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
[[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
|
||||
```
|
||||
+13
-2
@@ -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/user-guide/#extensions-conditions[conditional
|
||||
* Powerful support for link:https://docs.junit.org/current/extensions/conditional-test-execution.html[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/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
|
||||
link:https://docs.junit.org/current/extensions/parameter-resolution.html[`ParameterResolver`]
|
||||
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
|
||||
constructors, test methods, and test lifecycle callback methods.
|
||||
|
||||
@@ -392,6 +392,17 @@ 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
|
||||
|
||||
@@ -1,186 +0,0 @@
|
||||
[[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, preflight requests are
|
||||
rejected. No CORS headers are added to the responses of simple and actual CORS requests
|
||||
and, consequently, browsers reject them.
|
||||
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.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
|
||||
@@ -305,7 +305,6 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
@@ -328,7 +327,6 @@ 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
|
||||
In WebFlux.fn, an HTTP request is handled with a `HandlerFunction`: a function that takes a
|
||||
`ServerRequest` and returns a delayed `ServerResponse` (i.e. `Mono<ServerResponse>`).
|
||||
Both the request and the response object have immutable contracts that offer JDK 8-friendly
|
||||
Both the request and the response object have immutable contracts that offer convenient
|
||||
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 JDK 8-friendly
|
||||
`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient
|
||||
access to the HTTP request and response.
|
||||
Both request and response provide {reactive-streams-site}[Reactive Streams] back pressure
|
||||
against the body streams.
|
||||
@@ -235,88 +235,14 @@ val map = request.awaitMultipartData()
|
||||
|
||||
The following example shows how to access multipart data, one at a time, in streaming fashion:
|
||||
|
||||
[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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./PartEventHandler[tag=snippet,indent=0]
|
||||
|
||||
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`:
|
||||
|
||||
[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")})
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
include-code::./RequestHandler[tag=snippet,indent=0]
|
||||
|
||||
[[webflux-fn-response]]
|
||||
=== ServerResponse
|
||||
@@ -326,24 +252,7 @@ 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:
|
||||
|
||||
[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)
|
||||
----
|
||||
======
|
||||
include-code::./ResponseHandler[tag=snippet,indent=0]
|
||||
|
||||
The following example shows how to build a 201 (CREATED) response with a `Location` header and no body:
|
||||
|
||||
@@ -354,7 +263,7 @@ Java::
|
||||
[source,java]
|
||||
----
|
||||
URI location = ...
|
||||
ServerResponse.created(location).build();
|
||||
return ServerResponse.created(location).build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -362,7 +271,7 @@ Kotlin::
|
||||
[source,kotlin]
|
||||
----
|
||||
val location: URI = ...
|
||||
ServerResponse.created(location).build()
|
||||
return ServerResponse.created(location).build()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -375,14 +284,14 @@ Java::
|
||||
+
|
||||
[source,java]
|
||||
----
|
||||
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
|
||||
return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView.class).body(...);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin]
|
||||
----
|
||||
ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
|
||||
return ServerResponse.ok().hint(JacksonCodecSupport.JSON_VIEW_HINT, MyJacksonView::class.java).body(...)
|
||||
----
|
||||
======
|
||||
|
||||
@@ -417,87 +326,7 @@ 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:
|
||||
|
||||
--
|
||||
[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.
|
||||
======
|
||||
--
|
||||
include-code::./PersonHandler[tag=snippet,indent=0]
|
||||
|
||||
[[webflux-fn-handler-validation]]
|
||||
=== Validation
|
||||
@@ -506,66 +335,7 @@ 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`:
|
||||
|
||||
[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.
|
||||
======
|
||||
include-code::./PersonHandler[tag=snippet,indent=0]
|
||||
|
||||
Handlers can also use the standard bean validation API (JSR-303) by creating and injecting
|
||||
a global `Validator` instance based on `LocalValidatorFactoryBean`.
|
||||
@@ -603,28 +373,7 @@ path, headers, xref:#api-version[API version], and more.
|
||||
|
||||
The following example uses an `Accept` header, request predicate:
|
||||
|
||||
[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")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./RouterConfiguration[tag=snippet,indent=0]
|
||||
|
||||
You can compose multiple request predicates together by using:
|
||||
|
||||
@@ -659,61 +408,7 @@ There are also other ways to compose multiple router functions together:
|
||||
|
||||
The following example shows the composition of four routes:
|
||||
|
||||
|
||||
[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.
|
||||
======
|
||||
include-code::./RouterConfiguration[tag=snippet,indent=0]
|
||||
|
||||
[[nested-routes]]
|
||||
=== Nested Routes
|
||||
@@ -811,7 +506,7 @@ Java::
|
||||
----
|
||||
RouterFunction<ServerResponse> route = RouterFunctions.route()
|
||||
.GET("/hello-world", version("1.2"),
|
||||
request -> ServerResponse.ok().body("Hello World")).build();
|
||||
request -> ServerResponse.ok().bodyValue("Hello World")).build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -944,7 +639,6 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
|
||||
@Bean
|
||||
@@ -981,7 +675,6 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
|
||||
@Bean
|
||||
@@ -1049,9 +742,9 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val route = router {
|
||||
"/person".nest {
|
||||
("/person" and accept(APPLICATION_JSON)).nest {
|
||||
GET("/{id}", handler::getPerson)
|
||||
GET("", handler::listPeople)
|
||||
GET(handler::listPeople)
|
||||
before { // <1>
|
||||
ServerRequest.from(it)
|
||||
.header("X-RequestHeader", "Value").build()
|
||||
@@ -1078,54 +771,7 @@ 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:
|
||||
|
||||
[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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./RouterConfiguration[tag=snippet,indent=0]
|
||||
|
||||
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,8 +56,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
public class WebConfiguration implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
@@ -80,8 +79,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
class WebConfiguration : WebFluxConfigurer {
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.freeMarker()
|
||||
@@ -119,8 +117,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
public class WebConfiguration implements WebFluxConfigurer {
|
||||
|
||||
// ...
|
||||
|
||||
@@ -142,8 +139,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
class WebConfiguration : WebFluxConfigurer {
|
||||
|
||||
// ...
|
||||
|
||||
@@ -210,13 +206,8 @@ 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
|
||||
@@ -228,17 +219,8 @@ 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
|
||||
@@ -246,7 +228,7 @@ through https://www.webjars.org/[WebJars].
|
||||
|
||||
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 Mustache templates and the Nashorn JavaScript engine:
|
||||
The following example uses the Jython Python engine:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -255,8 +237,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
public class WebConfig implements WebFluxConfigurer {
|
||||
public class WebConfiguration implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
@@ -266,9 +247,8 @@ Java::
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer configurer() {
|
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
||||
configurer.setEngineName("nashorn");
|
||||
configurer.setScripts("mustache.js");
|
||||
configurer.setRenderObject("Mustache");
|
||||
configurer.setEngineName("jython");
|
||||
configurer.setScripts("render.py");
|
||||
configurer.setRenderFunction("render");
|
||||
return configurer;
|
||||
}
|
||||
@@ -280,8 +260,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class WebConfig : WebFluxConfigurer {
|
||||
class WebConfiguration : WebFluxConfigurer {
|
||||
|
||||
override fun configureViewResolvers(registry: ViewResolverRegistry) {
|
||||
registry.scriptTemplate()
|
||||
@@ -289,9 +268,8 @@ Kotlin::
|
||||
|
||||
@Bean
|
||||
fun configurer() = ScriptTemplateConfigurer().apply {
|
||||
engineName = "nashorn"
|
||||
setScripts("mustache.js")
|
||||
renderObject = "Mustache"
|
||||
engineName = "jython"
|
||||
setScripts("render.py")
|
||||
renderFunction = "render"
|
||||
}
|
||||
}
|
||||
@@ -305,94 +283,7 @@ 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 (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);
|
||||
}
|
||||
----
|
||||
URL
|
||||
|
||||
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#http-client.webclient[Observability support].
|
||||
* `observationConvention`: xref:integration/observability.adoc#config[an optional, custom convention to extract metadata] for recorded observations.
|
||||
* `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.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
|
||||
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
||||
@@ -66,6 +66,13 @@ 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
|
||||
|
||||
+8
-88
@@ -41,8 +41,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class MyForm(
|
||||
val name: String,
|
||||
val file: FilePart)
|
||||
private val name: String,
|
||||
private val file: FilePart)
|
||||
|
||||
@Controller
|
||||
class FileUploadController {
|
||||
@@ -103,8 +103,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@PostMapping("/")
|
||||
fun handle(@RequestPart("meta-data") Part metadata, // <1>
|
||||
@RequestPart("file-data") FilePart file): String { // <2>
|
||||
fun handle(@RequestPart("meta-data") metadata: Part, // <1>
|
||||
@RequestPart("file-data") file: FilePart): String { // <2>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
@@ -169,8 +169,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@PostMapping("/")
|
||||
fun handle(@Valid @RequestPart("meta-data") metadata: MetaData): String {
|
||||
// ...
|
||||
fun handle(@Valid @RequestPart("meta-data") metadata: Mono<MetaData>): String {
|
||||
// use one of the onError* operators...
|
||||
}
|
||||
----
|
||||
======
|
||||
@@ -202,7 +202,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@PostMapping("/")
|
||||
fun handle(@RequestBody parts: MultiValueMap<String, Part>): String { // <1>
|
||||
fun handle(@RequestBody parts: Mono<MultiValueMap<String, Part>>): String { // <1>
|
||||
// ...
|
||||
}
|
||||
----
|
||||
@@ -227,87 +227,7 @@ when uploading. If the file is large enough to be split across multiple buffers,
|
||||
|
||||
For example:
|
||||
|
||||
[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.
|
||||
======
|
||||
include-code::./PartEventController[tag=snippet,indent=0]
|
||||
|
||||
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,9 +393,10 @@ is used to map requests.
|
||||
Once API versioning is enabled, you can begin to map requests with versions.
|
||||
The `@RequestMapping` `version` attribute supports the following:
|
||||
|
||||
- No value -- matches any version
|
||||
- Fixed version ("1.2") -- matches the given version only
|
||||
- Baseline version ("1.2+") -- matches the given version and above
|
||||
- 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
|
||||
|
||||
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,
|
||||
@@ -443,6 +444,9 @@ 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
|
||||
@@ -454,9 +458,10 @@ 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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 8 `Stream` API but not only for collections.
|
||||
compose async logic -- similar to the Java `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 8 lambdas
|
||||
* If you are interested in a lightweight, functional web framework for use with Java
|
||||
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,16 +381,23 @@ 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")`.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
* 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.
|
||||
* 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.
|
||||
|
||||
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
|
||||
@@ -658,7 +665,6 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebFlux
|
||||
class MyConfig implements WebFluxConfigurer {
|
||||
|
||||
@Override
|
||||
@@ -673,7 +679,6 @@ 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, preflight requests are
|
||||
rejected. No CORS headers are added to the responses of simple and actual CORS requests
|
||||
and, consequently, browsers reject them.
|
||||
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.
|
||||
|
||||
Each `HandlerMapping` can be
|
||||
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
|
||||
@@ -208,6 +208,7 @@ Kotlin::
|
||||
fun remove(@PathVariable id: Long) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -286,84 +287,9 @@ the `allowOriginPatterns` property may be used to match to a dynamic set of orig
|
||||
|
||||
`maxAge` is set to 30 minutes.
|
||||
|
||||
[[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>
|
||||
----
|
||||
You can enable CORS in the Spring MVC configuration as the following example shows:
|
||||
|
||||
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 JDK 8-friendly
|
||||
Both the request and the response object have immutable contracts that offer convenient
|
||||
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 JDK 8-friendly
|
||||
`ServerRequest` and `ServerResponse` are immutable interfaces that offer convenient
|
||||
access to the HTTP request and response, including headers, body, method, and status code.
|
||||
|
||||
[[webmvc-fn-request]]
|
||||
@@ -836,7 +836,6 @@ 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)
|
||||
@@ -900,81 +899,9 @@ processing lifecycle and also (potentially) run side by side with annotated cont
|
||||
any are declared. It is also how functional endpoints are enabled by the Spring Boot Web
|
||||
starter.
|
||||
|
||||
The following example shows a WebMvc Java configuration:
|
||||
The following example shows a related Spring MVC configuration:
|
||||
|
||||
[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...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
|
||||
[[webmvc-fn-handler-filter-function]]
|
||||
|
||||
@@ -14,82 +14,7 @@ integration for using Spring MVC with FreeMarker templates.
|
||||
|
||||
The following example shows how to configure FreeMarker as a view technology:
|
||||
|
||||
[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>
|
||||
----
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
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
|
||||
@@ -107,19 +32,7 @@ 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`:
|
||||
|
||||
[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"/>
|
||||
----
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
See the FreeMarker documentation for details of settings and variables as they apply to
|
||||
the `Configuration` object.
|
||||
|
||||
@@ -14,68 +14,7 @@ NOTE: The Groovy Markup Template engine requires Groovy 2.3.1+.
|
||||
|
||||
The following example shows how to configure the Groovy Markup Template Engine:
|
||||
|
||||
[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/"/>
|
||||
----
|
||||
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
[[mvc-view-groovymarkup-example]]
|
||||
== Example
|
||||
|
||||
@@ -16,39 +16,11 @@ 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"`.
|
||||
|
||||
[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>
|
||||
----
|
||||
======
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
[NOTE]
|
||||
You can specify custom prefix and suffix.
|
||||
|
||||
|
||||
[[mvc-view-jsp-jstl]]
|
||||
== JSPs versus JSTL
|
||||
|
||||
|
||||
@@ -11,13 +11,8 @@ 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
|
||||
@@ -30,18 +25,8 @@ 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
|
||||
@@ -49,74 +34,20 @@ through https://www.webjars.org/[WebJars].
|
||||
|
||||
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 Mustache templates and the Nashorn JavaScript engine:
|
||||
The following example uses the Jython Python engine:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@EnableWebMvc
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
@Override
|
||||
public void configureViewResolvers(ViewResolverRegistry registry) {
|
||||
registry.scriptTemplate();
|
||||
}
|
||||
The render function is called with the following parameters:
|
||||
|
||||
@Bean
|
||||
public ScriptTemplateConfigurer configurer() {
|
||||
ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer();
|
||||
configurer.setEngineName("nashorn");
|
||||
configurer.setScripts("mustache.js");
|
||||
configurer.setRenderObject("Mustache");
|
||||
configurer.setRenderFunction("render");
|
||||
return configurer;
|
||||
}
|
||||
}
|
||||
----
|
||||
* `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
|
||||
|
||||
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:
|
||||
The controller is used to populate the model attributes and specify the view name, as the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -153,115 +84,6 @@ 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,45 +21,7 @@ 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:
|
||||
|
||||
[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")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
[[mvc-view-xslt-controllercode]]
|
||||
== Controller
|
||||
|
||||
@@ -120,17 +120,26 @@ 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]#
|
||||
|
||||
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")`.
|
||||
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")`.
|
||||
|
||||
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:
|
||||
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:
|
||||
|
||||
* 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.
|
||||
* 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.
|
||||
|
||||
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.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
To enable RFC 9457 responses for Spring MVC exceptions and for any
|
||||
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
||||
@@ -66,6 +66,13 @@ 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
|
||||
@@ -184,7 +191,7 @@ Message codes and arguments for each error are also resolved via `MessageSource`
|
||||
|===
|
||||
|
||||
NOTE: Unlike other exceptions, the message arguments for
|
||||
`MethodArgumentValidException` and `HandlerMethodValidationException` are based on a list of
|
||||
`MethodArgumentNotValidException` 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
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
The next table describes the supported controller method arguments. Reactive types are not supported
|
||||
for any arguments.
|
||||
|
||||
JDK 8's `java.util.Optional` is supported as a method argument in combination with
|
||||
`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`.
|
||||
|
||||
|
||||
+11
-53
@@ -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 version and above
|
||||
- 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
|
||||
|
||||
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 above
|
||||
<3> match version 1.2 and supported versions above
|
||||
<4> match version 1.5
|
||||
======
|
||||
|
||||
@@ -474,6 +474,9 @@ 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
|
||||
@@ -485,9 +488,10 @@ 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.
|
||||
|
||||
NOTE: The above assumes the request version is a
|
||||
xref:web/webmvc/mvc-config/api-version.adoc["supported" version], or otherwise it
|
||||
would fail.
|
||||
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.
|
||||
|
||||
See xref:web/webmvc-versioning.adoc[API Versioning] for more details on underlying
|
||||
infrastructure and support for API Versioning.
|
||||
@@ -550,53 +554,7 @@ 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:
|
||||
|
||||
[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.
|
||||
======
|
||||
include-code::./MyConfiguration[tag=snippet,indent=0]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -14,55 +14,12 @@ 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 of the Java configuration registers and initializes
|
||||
The following example shows the programmatic registration and initialization of
|
||||
the `DispatcherServlet`, which is auto-detected by the Servlet container
|
||||
(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]):
|
||||
(see xref:web/webmvc/mvc-servlet/container-config.adoc[Servlet Config]), and the
|
||||
equivalent `web.xml`:
|
||||
|
||||
[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/*")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./MyWebApplicationInitializer[tag=snippet,indent=0]
|
||||
|
||||
NOTE: In addition to using the ServletContext API directly, you can also extend
|
||||
`AbstractAnnotationConfigDispatcherServletInitializer` and override specific methods
|
||||
@@ -73,39 +30,6 @@ 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,48 +5,7 @@ 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`:
|
||||
|
||||
[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("/")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
include-code::./MyWebApplicationInitializer[tag=snippet,indent=0]
|
||||
|
||||
`WebApplicationInitializer` is an interface provided by Spring MVC that ensures your
|
||||
implementation is detected and automatically used to initialize any Servlet 3 container.
|
||||
@@ -55,144 +14,21 @@ 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 Java-based Spring configuration, as the
|
||||
This is recommended for applications that use programmatic Spring configuration, as the
|
||||
following example shows:
|
||||
|
||||
[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("/")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./MyWebAppInitializer[tag=snippet,indent=0]
|
||||
|
||||
If you use XML-based Spring configuration, you should extend directly from
|
||||
`AbstractDispatcherServletInitializer`, as the following example shows:
|
||||
|
||||
[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("/")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./MyXmlDispatcherServletInitializer[tag=snippet,indent=0]
|
||||
|
||||
`AbstractDispatcherServletInitializer` also provides a convenient way to add `Filter`
|
||||
instances and have them be automatically mapped to the `DispatcherServlet`, as the
|
||||
following example shows:
|
||||
|
||||
[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())
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./MyFilterDispatcherServletInitializer[tag=snippet,indent=0]
|
||||
|
||||
Each filter is added with a default name based on its concrete type and automatically
|
||||
mapped to the `DispatcherServlet`.
|
||||
|
||||
@@ -20,91 +20,15 @@ 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[]
|
||||
image::mvc-context-hierarchy.png[width=60%,align="center"]
|
||||
|
||||
The following example configures a `WebApplicationContext` hierarchy:
|
||||
The following example configures a `WebApplicationContext` hierarchy, and the equivalent `web.xml`:
|
||||
|
||||
[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/*")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./MyWebAppInitializer[tag=snippet,indent=0]
|
||||
|
||||
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,42 +74,7 @@ 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:
|
||||
|
||||
[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
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./ErrorController[tag=snippet,indent=0]
|
||||
|
||||
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,44 +54,9 @@ 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`:
|
||||
|
||||
[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.
|
||||
|===
|
||||
maximum age. The following example defines a `CookieLocaleResolver` bean:
|
||||
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0]
|
||||
|
||||
[[mvc-localeresolver-session]]
|
||||
== Session Resolver
|
||||
@@ -115,31 +80,7 @@ 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://www.sf.net/home.view?siteLanguage=nl`, changes the site
|
||||
a request for the URL `https://domain.com/home.view?siteLanguage=nl` changes the site
|
||||
language to Dutch. The following example shows how to intercept the locale:
|
||||
|
||||
[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>
|
||||
----
|
||||
|
||||
|
||||
|
||||
include-code::./WebConfiguration[tag=snippet,indent=0,chomp=-tags]
|
||||
|
||||
@@ -25,62 +25,7 @@ through the `enableLoggingRequestDetails` property on `DispatcherServlet`.
|
||||
|
||||
The following example shows how to do so by using Java configuration:
|
||||
|
||||
[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")
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./MyInitializer[tag=snippet,indent=0]
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -26,43 +26,7 @@ To do so:
|
||||
|
||||
The following example shows how to set a `MultipartConfigElement` on the Servlet registration:
|
||||
|
||||
[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"))
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
======
|
||||
include-code::./AppInitializer[tag=snippet,indent=0]
|
||||
|
||||
Once the Servlet multipart configuration is in place, you can add a bean of type
|
||||
`StandardServletMultipartResolver` with a name of `multipartResolver`.
|
||||
|
||||
@@ -152,26 +152,9 @@ 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 do so in Java configuration:
|
||||
The following example shows how to configure it:
|
||||
|
||||
[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.
|
||||
include-code::./WebSocketConfiguration[tag=snippet,indent=0]
|
||||
|
||||
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
|
||||
@@ -307,23 +290,4 @@ 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:
|
||||
|
||||
[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`).
|
||||
include-code::./WebSocketConfiguration[tag=snippet,indent=0]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"antora": "3.2.0-alpha.4",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.2",
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"antora": "3.2.0-alpha.11",
|
||||
"@antora/atlas-extension": "1.0.0-alpha.5",
|
||||
"@antora/collector-extension": "1.0.2",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.7",
|
||||
"fast-xml-parser": "4.5.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
"fast-xml-parser": "5.3.4",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
|
||||
}
|
||||
}
|
||||
|
||||
+12
-15
@@ -200,6 +200,7 @@
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
|
||||
=======================================================================
|
||||
|
||||
SPRING FRAMEWORK ${version} SUBCOMPONENTS:
|
||||
@@ -212,7 +213,7 @@ code for these subcomponents is subject to the terms and
|
||||
conditions of the following licenses.
|
||||
|
||||
|
||||
>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1):
|
||||
>>> ASM 9.9.1 (org.ow2.asm:asm:9.9.1):
|
||||
|
||||
Copyright (c) 2000-2011 INRIA, France Telecom
|
||||
All rights reserved.
|
||||
@@ -249,32 +250,28 @@ Copyright (c) 1999-2009, OW2 Consortium <https://www.ow2.org/>
|
||||
|
||||
>>> CGLIB 3.3 (cglib:cglib:3.3):
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
>>> JavaPoet 1.13.0 (com.squareup:javapoet:1.13.0):
|
||||
>>> JavaPoet 0.10.0 (com.palantir.javapoet:javapoet:0.10.0):
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
|
||||
>>> Objenesis 3.4 (org.objenesis:objenesis:3.4):
|
||||
>>> Objenesis 3.5 (org.objenesis:objenesis:3.5):
|
||||
|
||||
Per the LICENSE file in the Objenesis ZIP distribution downloaded from
|
||||
http://objenesis.org/download.html, Objenesis 3.4 is licensed under the
|
||||
Per the LICENSE file in the Objenesis distribution downloaded from
|
||||
http://objenesis.org/download.html, Objenesis 3.5 is licensed under the
|
||||
Apache License, version 2.0, the text of which is included above.
|
||||
|
||||
Per the NOTICE file in the Objenesis ZIP distribution downloaded from
|
||||
Per the NOTICE file in the Objenesis 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-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita
|
||||
Copyright 2006-2026 Joe Walnes, Henri Tremblay, Leonardo Mesquita
|
||||
|
||||
|
||||
===============================================================================
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 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[]
|
||||
}
|
||||
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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();
|
||||
}
|
||||
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 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 {
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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[]
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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) {
|
||||
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* 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[]
|
||||
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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[]
|
||||
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* 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) { }
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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[]
|
||||
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* 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[]
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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[]
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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
Reference in New Issue
Block a user