mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
457 Commits
v6.2.0-RC3
...
v6.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
| debd312d50 | |||
| a8be11ebaf | |||
| dfc10c1a8d | |||
| bbb593db48 | |||
| b2134ee71f | |||
| 440a259b71 | |||
| 6174055910 | |||
| e31ce359a1 | |||
| b336bbe539 | |||
| 056757b493 | |||
| 79c5fec1be | |||
| 689782cbad | |||
| 2df90e32c0 | |||
| f53da04717 | |||
| 124b38450b | |||
| b07217ab67 | |||
| aa7e84c89f | |||
| bd1c7b379f | |||
| 6af19244a5 | |||
| 328dd71f6e | |||
| d04883f839 | |||
| 9f55296049 | |||
| 4591a67641 | |||
| 9797bc0acd | |||
| 9d3374b28d | |||
| 09086fc648 | |||
| 128e90064e | |||
| 16e3973118 | |||
| 5c37d07ad3 | |||
| 12891d1975 | |||
| 1ca941ba9a | |||
| 17a94fb110 | |||
| 2fcae65853 | |||
| 7a0fe7d14f | |||
| ccdaed594e | |||
| c41b0140cd | |||
| ceffda7874 | |||
| 49f9b40fba | |||
| d59991fcc9 | |||
| cf46f391d7 | |||
| 2b4c7d09b0 | |||
| 61138698c6 | |||
| 3548e872b9 | |||
| 305686dbf7 | |||
| ace2f0a3e5 | |||
| 1a573d6e3c | |||
| bb7a8006c5 | |||
| ba56c1a8f4 | |||
| 9107f7b592 | |||
| 1d7cb4fc13 | |||
| 84992536c5 | |||
| ff49b0b683 | |||
| 56c4d2d4ca | |||
| 174d0e4576 | |||
| dba4881318 | |||
| 16ec08aec8 | |||
| 462c2bd538 | |||
| 7f29f0e663 | |||
| 3898482d3f | |||
| bb7ce21076 | |||
| fd4dee7ce3 | |||
| ec037b1ec0 | |||
| 68c1e2ac92 | |||
| 819a7c86c1 | |||
| 9c1346d240 | |||
| fb6e86551a | |||
| 13ba770428 | |||
| a4d99d6201 | |||
| 3923150dad | |||
| 8eb24458f8 | |||
| cda7e98643 | |||
| e92809d470 | |||
| f477c1653d | |||
| 1cea1fe962 | |||
| 8b07f93620 | |||
| a9ecf90524 | |||
| 7536777a18 | |||
| 1b18928bf0 | |||
| 8c2b44bd6c | |||
| 2df8ea94e0 | |||
| 323e52b5a9 | |||
| ed994dcd97 | |||
| 4c3b435d23 | |||
| cfe2db0581 | |||
| 53afe27109 | |||
| b9e43d05bd | |||
| d80de043ce | |||
| 7c5b6f1e1c | |||
| a6b6d19545 | |||
| 9b58df8857 | |||
| 1cc767e90b | |||
| 34d6dd9b62 | |||
| 5499878de0 | |||
| a970fc16aa | |||
| 683733a682 | |||
| 11fe2987d0 | |||
| 90423a98b0 | |||
| e9dc6be51c | |||
| 6863b904b5 | |||
| 016a4c4260 | |||
| 233f755416 | |||
| b026680e1c | |||
| e47cc19964 | |||
| 15b1310470 | |||
| ee60eb7207 | |||
| cecebd0ef1 | |||
| aac4dbf420 | |||
| 82bc4ff71d | |||
| 356d5c2cf2 | |||
| 46c13adfa8 | |||
| 886ca7f2db | |||
| c463b937b8 | |||
| 4783c321d9 | |||
| 0dfcb44821 | |||
| 8c3d5621db | |||
| 12a6a84829 | |||
| 6662ecc8cc | |||
| 4b0446fbef | |||
| 384d2749c6 | |||
| 5150a9a6ad | |||
| a8c5885aff | |||
| 7d844274dc | |||
| 70ceb3abdf | |||
| 9181cce65f | |||
| 8b6523a35b | |||
| e7f349328d | |||
| fe5f5d53b2 | |||
| 033d277bcf | |||
| 227385083d | |||
| d280358e98 | |||
| e08a7ece61 | |||
| 24fa9ea394 | |||
| 4acd9ee69b | |||
| cd1aefa5e0 | |||
| 3c3b8c7aac | |||
| 5a68d1f029 | |||
| ff9b7678a7 | |||
| 799566573a | |||
| 467d5f3ca3 | |||
| 172c25fb0e | |||
| 8771b9ea21 | |||
| ff72652890 | |||
| a1503a59ee | |||
| 5f29df3ac3 | |||
| 25287205ba | |||
| 2bb4df79c3 | |||
| 36fd82f32f | |||
| 0f26f42da7 | |||
| c48fec885c | |||
| 7c4351ab0a | |||
| 07455b10f3 | |||
| a72855b2b3 | |||
| cdddf09c20 | |||
| 53b3b934fc | |||
| 9173e13f74 | |||
| 84be0d85b4 | |||
| c971276f34 | |||
| cd2fbb1ec5 | |||
| e8745522fc | |||
| 2bfb283c91 | |||
| 50b1fb0b15 | |||
| a985b73939 | |||
| 6ec7dcf2c1 | |||
| d94e04d97a | |||
| 5a44897c55 | |||
| 181db1db75 | |||
| 138bdc597d | |||
| ef4f1f0a71 | |||
| 51b89743e1 | |||
| 019f76468b | |||
| f802c0cf24 | |||
| 0da4ae96b4 | |||
| 2ba0022704 | |||
| c59ca087b4 | |||
| 9de2b947cb | |||
| e73e7564d0 | |||
| d927d64c40 | |||
| 8544435833 | |||
| 80ea1f62b6 | |||
| bc287918f2 | |||
| c9436992c9 | |||
| 4350fc21b3 | |||
| 59ed4686c5 | |||
| d5bebd5ced | |||
| 6d86b23fbe | |||
| 0f38c28e91 | |||
| 3505c4bcad | |||
| 58670db46e | |||
| 0cdb8a1657 | |||
| fe8c70dddb | |||
| 1ed74efa6b | |||
| eabd574038 | |||
| ea386c2398 | |||
| 6c6e8ad831 | |||
| 69b74d7645 | |||
| 5ce5647d09 | |||
| ebae02a92b | |||
| 99cb6ba0a7 | |||
| 2baf70768d | |||
| 724700d708 | |||
| 83ba7e69ec | |||
| c1236a3340 | |||
| a942362221 | |||
| 1a34b0dd87 | |||
| b0fcde9aa3 | |||
| a89db89fc0 | |||
| 3d0fffa8e4 | |||
| 71f872e8bb | |||
| 54a90b20ed | |||
| d7a3971403 | |||
| cfa3463cec | |||
| d2264b221a | |||
| 2c32601553 | |||
| ea8b18fbc7 | |||
| 3ab4ee2bba | |||
| 0aa721cad0 | |||
| 63af572ce8 | |||
| 72c2343f63 | |||
| 8aeced9f80 | |||
| 70c326ed30 | |||
| 7b4e19c69b | |||
| 3b95d2c449 | |||
| 640e570583 | |||
| 66f33a8265 | |||
| 68997d8416 | |||
| 52006b71bc | |||
| 41d9f21ab9 | |||
| 7206b28272 | |||
| 0c688742e1 | |||
| 7de1dc826a | |||
| 3e3ca74020 | |||
| 66da5d7ab9 | |||
| 68d6cb9d35 | |||
| 6649a6e0f7 | |||
| 54948a4e88 | |||
| a00ba8dbcf | |||
| 4f815b0055 | |||
| 5494d78018 | |||
| cd60a0013b | |||
| 2137750591 | |||
| 13df9058a4 | |||
| aee52b53a1 | |||
| 92bbaa21e0 | |||
| 837579c2e5 | |||
| 03a75cf03a | |||
| aa7b459803 | |||
| 03fe1f0df3 | |||
| 0d72477742 | |||
| d26ee8f852 | |||
| 47c66f4352 | |||
| b5dd0a60f8 | |||
| e618f922c2 | |||
| 81a9f3d50b | |||
| c807fa597f | |||
| 576f109987 | |||
| 384dc2a9b8 | |||
| edf7f3cd43 | |||
| 58c64cba2c | |||
| 307411631d | |||
| 8d69370b95 | |||
| d990449b0d | |||
| 15dcc449a2 | |||
| 320831b18a | |||
| 172c8b2c35 | |||
| 51956fad89 | |||
| ddec8d2653 | |||
| 1aede291bb | |||
| ea3bd7ae0c | |||
| 9b0253e117 | |||
| f3753e6d64 | |||
| 41421d106b | |||
| 8c0ac8e062 | |||
| 0088b9c7f8 | |||
| 2b840ee7ef | |||
| 051f1dac24 | |||
| 5e7b3a3bed | |||
| 1910d32405 | |||
| 986ffc2072 | |||
| b9cf03f8f0 | |||
| 08a789cee9 | |||
| 7a6e401d17 | |||
| 3569cfe990 | |||
| 8b66d3c79a | |||
| a3c132c442 | |||
| 91791c1756 | |||
| dd92eac3ad | |||
| 874f056984 | |||
| b77db64459 | |||
| d597d2e159 | |||
| f5c3f3522e | |||
| d421f61a4a | |||
| 883254e1d0 | |||
| e919d0adcc | |||
| 4aafae1c33 | |||
| afef439c1f | |||
| 807e1e6126 | |||
| 5ac56bda87 | |||
| 7196f3f554 | |||
| a12d40e10b | |||
| 7f7819329c | |||
| 2494ecb47b | |||
| 241b8b48f2 | |||
| 173084f81a | |||
| 6544698078 | |||
| e0e96c487f | |||
| bb32df0a06 | |||
| 66c3aeee84 | |||
| 828980bd44 | |||
| fe8573c0ab | |||
| 130627f630 | |||
| 35b452b458 | |||
| 56525da43a | |||
| 5145cf476f | |||
| d4f8b5d820 | |||
| 6cb41dc5e3 | |||
| cca245020d | |||
| de8a94f30d | |||
| 973d815763 | |||
| 1e95332f62 | |||
| 1c2dbec971 | |||
| fec6ba4dfe | |||
| 37b110a181 | |||
| 18a3b52d5e | |||
| b523f3caff | |||
| 7f9901d35d | |||
| bfde33a514 | |||
| 25e2c1b7ce | |||
| 14b9865de7 | |||
| 62eb21f938 | |||
| 01c85b1afb | |||
| df376d9343 | |||
| d92c57f7a5 | |||
| 4d792d0e45 | |||
| 4ca596b7bc | |||
| 90ef7ac514 | |||
| c3991392df | |||
| 0a5bd89129 | |||
| 357dbc0354 | |||
| 525407e4b1 | |||
| 53b9a2cb78 | |||
| cbe2f36106 | |||
| 9dabfdf0bf | |||
| 942d2c5541 | |||
| e78179b96e | |||
| 5b5a072351 | |||
| 85cf563741 | |||
| e3ba957431 | |||
| caec8f4f36 | |||
| ae16a7fc08 | |||
| 43e7921bcc | |||
| 49a63e2c37 | |||
| 5666e363d1 | |||
| f06853a339 | |||
| 8afd01ba2c | |||
| 9724f9b9c8 | |||
| 8202282975 | |||
| d4719493bd | |||
| 59ec871e76 | |||
| 37243f44e8 | |||
| e67f892e44 | |||
| 1ced8c3856 | |||
| 0d9033592b | |||
| fc7b8ae966 | |||
| c457131f1c | |||
| 39cfe136da | |||
| fd676ed932 | |||
| 05a880e3b5 | |||
| 715e38c44f | |||
| c93af1f76d | |||
| 2e6c8daec6 | |||
| c5f8dcc245 | |||
| 541866fd70 | |||
| 0219ee656f | |||
| f6e1a5de09 | |||
| 72e7f1552b | |||
| bbe90a57ba | |||
| 9b3cb15389 | |||
| 4697ae10ee | |||
| fb98d74a47 | |||
| 4ef2b429e0 | |||
| 7e1d6fe934 | |||
| 0beb56a58c | |||
| c6126b2033 | |||
| f1e6482f92 | |||
| e3301dd1c4 | |||
| 6bd4687706 | |||
| 07b12666b4 | |||
| 2b601f9e51 | |||
| 438d6de3c1 | |||
| d43126705f | |||
| 57bbf0ca0f | |||
| a3b979c5ec | |||
| a8f5848a5d | |||
| 97f23dca22 | |||
| 86784b61cc | |||
| fdb763eeaf | |||
| 27912b1ed1 | |||
| ba692aa3ef | |||
| 0846706688 | |||
| bc65f93467 | |||
| 38c88e769d | |||
| 4a0edc59cc | |||
| c979eddab1 | |||
| 864cb25eec | |||
| e23c8bfbb6 | |||
| 08e0baac94 | |||
| 9166688b6f | |||
| da8925131e | |||
| 09d87fac57 | |||
| a005c325f7 | |||
| eaafdea1fb | |||
| f4b3520ab3 | |||
| f427ac383d | |||
| 79cf554850 | |||
| 8ffbafd384 | |||
| c0c41ddda5 | |||
| 7148b28b2b | |||
| 1255bd1fc9 | |||
| a1f6098158 | |||
| 022fdcd67e | |||
| 11ebceee38 | |||
| fa21dffcf8 | |||
| 9e3371ef07 | |||
| c2c6bb25c6 | |||
| 40960fa85a | |||
| 578928de39 | |||
| 015beb0058 | |||
| 3732c71168 | |||
| 323de1208a | |||
| 94d46eba3c | |||
| 4749d810db | |||
| 044da794f4 | |||
| 89d56b1fa6 | |||
| f1cfe7a3d4 | |||
| dfaf7a0c6a | |||
| 86a42db002 | |||
| d4b31fd4b2 | |||
| a366ea0e15 | |||
| 37622a7f90 | |||
| de2c10abcd | |||
| 81d89f478a | |||
| 52e813d0ad | |||
| f19a1b50e5 | |||
| 5532574f56 | |||
| 1b3e96b9bf | |||
| e340e45f5a | |||
| 9df4fcdc9e | |||
| a06bbccf9e | |||
| d1d6ff8737 | |||
| 9f0dbc4051 | |||
| acccbbec3f | |||
| 3cc76ef87c | |||
| 0221471265 | |||
| 3b82733e1f | |||
| ba8024d077 | |||
| efda3f0f99 | |||
| 761850816c |
@@ -31,10 +31,11 @@ runs:
|
||||
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
|
||||
${{ inputs.java-toolchain == 'true' && '17' || '' }}
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
with:
|
||||
cache-read-only: false
|
||||
develocity-access-key: ${{ inputs.develocity-access-key }}
|
||||
develocity-token-expiry: 4
|
||||
- name: Configure Gradle Properties
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -20,7 +20,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set Up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
|
||||
uses: jfrog/setup-jfrog-cli@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
|
||||
- name: Download Release Artifacts
|
||||
|
||||
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 6.2.x
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Deploy
|
||||
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
|
||||
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
|
||||
@@ -9,22 +9,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Set Up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: '17'
|
||||
- name: Check Out
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
- name: Build
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
run: ./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --no-parallel --continue build
|
||||
id: build
|
||||
uses: ./.github/actions/build
|
||||
- name: Print JVM Thread Dumps When Cancelled
|
||||
if: cancelled()
|
||||
uses: ./.github/actions/print-jvm-thread-dumps
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: CI
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 9 * * *'
|
||||
push:
|
||||
branches:
|
||||
- 6.2.x
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
@@ -20,8 +21,6 @@ jobs:
|
||||
toolchain: false
|
||||
- version: 21
|
||||
toolchain: true
|
||||
- version: 22
|
||||
toolchain: true
|
||||
- version: 23
|
||||
toolchain: true
|
||||
exclude:
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
name: Release Milestone
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v6.2.0-M[1-9]
|
||||
- v6.2.0-RC[1-9]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-stage-release:
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
name: Build and Stage Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Stage Release
|
||||
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-staging-local'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-stage-release
|
||||
uses: ./.github/workflows/verify.yml
|
||||
with:
|
||||
staging: true
|
||||
version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
secrets:
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
promote-release:
|
||||
name: Promote Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- promote-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
milestone: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
pre-release: true
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -20,7 +20,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@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
@@ -73,7 +73,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
|
||||
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
|
||||
@@ -12,8 +12,9 @@ permissions:
|
||||
|
||||
jobs:
|
||||
update-antora-ui-spring:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on Supported Branches
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
branch: [ '6.1.x' ]
|
||||
@@ -25,8 +26,9 @@ jobs:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
antora-file-path: 'framework-docs/antora-playbook.yml'
|
||||
update-antora-ui-spring-docs-build:
|
||||
runs-on: ubuntu-latest
|
||||
name: Update on docs-build
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: spring-io/spring-doc-actions/update-antora-spring-ui@5a57bcc6a0da2a1474136cf29571b277850432bc
|
||||
name: Update
|
||||
|
||||
@@ -46,7 +46,7 @@ jobs:
|
||||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@d156388eb19639ec20ade50009f3d199ce1e2808 # v4.1.0
|
||||
uses: gradle/actions/setup-gradle@0bdd871935719febd78681f197cd39af5b6e16a6 # v4.2.2
|
||||
with:
|
||||
cache-read-only: false
|
||||
- name: Configure Gradle Properties
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=17.0.12-librca
|
||||
java=17.0.13-librca
|
||||
|
||||
+6
-5
@@ -16,7 +16,7 @@ First off, thank you for taking the time to contribute! :+1: :tada:
|
||||
|
||||
### Code of Conduct
|
||||
|
||||
This project is governed by the [Spring Code of Conduct](CODE_OF_CONDUCT.adoc).
|
||||
This project is governed by the [Spring Code of Conduct](https://github.com/spring-projects/spring-framework#coc-ov-file).
|
||||
By participating you are expected to uphold this code.
|
||||
Please report unacceptable behavior to spring-code-of-conduct@spring.io.
|
||||
|
||||
@@ -65,10 +65,6 @@ follow-up reports will need to be created as new issues with a fresh description
|
||||
|
||||
#### Submit a Pull Request
|
||||
|
||||
1. If you have not previously done so, please sign the
|
||||
[Contributor License Agreement](https://cla.spring.io/sign/spring). You will be reminded
|
||||
automatically when you submit the PR.
|
||||
|
||||
1. Should you create an issue first? No, just create the pull request and use the
|
||||
description to provide context and motivation, as you would for an issue. If you want
|
||||
to start a discussion first or have already created an issue, once a pull request is
|
||||
@@ -85,6 +81,11 @@ multiple edits or corrections of the same logical change. See
|
||||
[Rewriting History section of Pro Git](https://git-scm.com/book/en/Git-Tools-Rewriting-History)
|
||||
for an overview of streamlining the commit history.
|
||||
|
||||
1. All commits must include a _Signed-off-by_ trailer at the end of each commit message
|
||||
to indicate that the contributor agrees to the Developer Certificate of Origin.
|
||||
For additional details, please refer to the blog post
|
||||
[Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring).
|
||||
|
||||
1. Format commit messages using 55 characters for the subject line, 72 characters per line
|
||||
for the description, followed by the issue fixed, for example, `Closes gh-22276`. See the
|
||||
[Commit Guidelines section of Pro Git](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines)
|
||||
|
||||
+5
-5
@@ -25,17 +25,17 @@ configure(allprojects) { project ->
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://repo.spring.io/milestone"
|
||||
url = "https://repo.spring.io/milestone"
|
||||
content {
|
||||
// Netty 5 optional support
|
||||
includeGroup 'io.projectreactor.netty'
|
||||
}
|
||||
}
|
||||
if (version.contains('-')) {
|
||||
maven { url "https://repo.spring.io/milestone" }
|
||||
maven { url = "https://repo.spring.io/milestone" }
|
||||
}
|
||||
if (version.endsWith('-SNAPSHOT')) {
|
||||
maven { url "https://repo.spring.io/snapshot" }
|
||||
maven { url = "https://repo.spring.io/snapshot" }
|
||||
}
|
||||
}
|
||||
configurations.all {
|
||||
@@ -91,7 +91,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
"https://docs.oracle.com/en/java/javase/17/docs/api/",
|
||||
"https://jakarta.ee/specifications/platform/9/apidocs/",
|
||||
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
|
||||
"https://eclipse.dev/aspectj/doc/released/aspectj5rt-api",
|
||||
"https://eclipse.dev/aspectj/doc/latest/runtime-api/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
|
||||
@@ -102,7 +102,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
|
||||
// See https://github.com/spring-projects/spring-framework/issues/27497
|
||||
//
|
||||
// "https://junit.org/junit5/docs/5.11.3/api/",
|
||||
// "https://junit.org/junit5/docs/5.11.4/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.18.1");
|
||||
checkstyle.setToolVersion("10.21.2");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.gradle.api.attributes.Usage;
|
||||
import org.gradle.api.attributes.java.TargetJvmVersion;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.jvm.JvmTestSuite;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.testing.base.TestingExtension;
|
||||
|
||||
@@ -52,7 +53,7 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
TestingExtension testing = project.getExtensions().getByType(TestingExtension.class);
|
||||
JvmTestSuite jvmTestSuite = (JvmTestSuite) testing.getSuites().getByName("test");
|
||||
RuntimeHintsAgentExtension agentExtension = createRuntimeHintsAgentExtension(project);
|
||||
Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
|
||||
TaskProvider<Test> agentTest = project.getTasks().register(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
|
||||
test.useJUnitPlatform(options -> {
|
||||
options.includeTags("RuntimeHintsTests");
|
||||
});
|
||||
@@ -63,7 +64,7 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
test.setClasspath(jvmTestSuite.getSources().getRuntimeClasspath());
|
||||
test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension));
|
||||
});
|
||||
project.getTasks().getByName("check", task -> task.dependsOn(agentTest));
|
||||
project.getTasks().named("check", task -> task.dependsOn(agentTest));
|
||||
project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
url = "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,7 +87,7 @@ tasks.register('schemaZip', Zip) {
|
||||
archiveClassifier.set("schema")
|
||||
description = "Builds -${archiveClassifier} archive containing all " +
|
||||
"XSDs for deployment at https://springframework.org/schema."
|
||||
duplicatesStrategy DuplicatesStrategy.EXCLUDE
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
moduleProjects.each { module ->
|
||||
def Properties schemas = new Properties();
|
||||
|
||||
|
||||
@@ -36,4 +36,4 @@ runtime:
|
||||
failure_level: warn
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
|
||||
|
||||
@@ -37,7 +37,7 @@ javadoc {
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
url = "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
**** xref:core/expressions/language-ref/constructors.adoc[]
|
||||
**** xref:core/expressions/language-ref/variables.adoc[]
|
||||
**** xref:core/expressions/language-ref/functions.adoc[]
|
||||
**** xref:core/expressions/language-ref/varargs.adoc[]
|
||||
**** xref:core/expressions/language-ref/bean-references.adoc[]
|
||||
**** xref:core/expressions/language-ref/operator-ternary.adoc[]
|
||||
**** xref:core/expressions/language-ref/operator-elvis.adoc[]
|
||||
|
||||
@@ -23,6 +23,12 @@ The following table lists all currently supported Spring properties.
|
||||
|===
|
||||
| Name | Description
|
||||
|
||||
| `spring.aop.ajc.ignore`
|
||||
| Instructs Spring to ignore ajc-compiled aspects for Spring AOP proxying, restoring traditional
|
||||
Spring behavior for scenarios where both weaving and AspectJ auto-proxying are enabled. See
|
||||
{spring-framework-api}++/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.html#IGNORE_AJC_PROPERTY_NAME++[`AbstractAspectJAdvisorFactory`]
|
||||
for details.
|
||||
|
||||
| `spring.aot.enabled`
|
||||
| Indicates the application should run with AOT generated artifacts. See
|
||||
xref:core/aot.adoc[Ahead of Time Optimizations] and
|
||||
@@ -32,7 +38,7 @@ for details.
|
||||
| `spring.beaninfo.ignore`
|
||||
| Instructs Spring to use the `Introspector.IGNORE_ALL_BEANINFO` mode when calling the
|
||||
JavaBeans `Introspector`. See
|
||||
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`CachedIntrospectionResults`]
|
||||
{spring-framework-api}++/beans/StandardBeanInfoFactory.html#IGNORE_BEANINFO_PROPERTY_NAME++[`StandardBeanInfoFactory`]
|
||||
for details.
|
||||
|
||||
| `spring.cache.reactivestreams.ignore`
|
||||
@@ -49,15 +55,13 @@ for details.
|
||||
|
||||
| `spring.context.checkpoint`
|
||||
| Property that specifies a common context checkpoint. See
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
|
||||
checkpoint/restore at startup] and
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
|
||||
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#CHECKPOINT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
|
||||
for details.
|
||||
|
||||
| `spring.context.exit`
|
||||
| Property for terminating the JVM when the context reaches a specific phase. See
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic
|
||||
checkpoint/restore at startup] and
|
||||
xref:integration/checkpoint-restore.adoc#_automatic_checkpointrestore_at_startup[Automatic checkpoint/restore at startup] and
|
||||
{spring-framework-api}++/context/support/DefaultLifecycleProcessor.html#EXIT_PROPERTY_NAME++[`DefaultLifecycleProcessor`]
|
||||
for details.
|
||||
|
||||
|
||||
@@ -33,11 +33,11 @@ arbitrary advice types. This section describes the basic concepts and standard a
|
||||
[[aop-api-advice-around]]
|
||||
=== Interception Around Advice
|
||||
|
||||
The most fundamental advice type in Spring is interception around advice.
|
||||
The most fundamental advice type in Spring is _interception around advice_.
|
||||
|
||||
Spring is compliant with the AOP `Alliance` interface for around advice that uses method
|
||||
interception. Classes that implement `MethodInterceptor` and that implement around advice should also implement the
|
||||
following interface:
|
||||
Spring is compliant with the AOP Alliance interface for around advice that uses method
|
||||
interception. Classes that implement around advice should therefore implement the
|
||||
following `MethodInterceptor` interface from the `org.aopalliance.intercept` package:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -49,8 +49,8 @@ following interface:
|
||||
|
||||
The `MethodInvocation` argument to the `invoke()` method exposes the method being
|
||||
invoked, the target join point, the AOP proxy, and the arguments to the method. The
|
||||
`invoke()` method should return the invocation's result: the return value of the join
|
||||
point.
|
||||
`invoke()` method should return the invocation's result: typically the return value of
|
||||
the join point.
|
||||
|
||||
The following example shows a simple `MethodInterceptor` implementation:
|
||||
|
||||
@@ -64,9 +64,9 @@ Java::
|
||||
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
System.out.println("Before: invocation=[" + invocation + "]");
|
||||
Object rval = invocation.proceed();
|
||||
Object result = invocation.proceed();
|
||||
System.out.println("Invocation returned");
|
||||
return rval;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -79,9 +79,9 @@ Kotlin::
|
||||
|
||||
override fun invoke(invocation: MethodInvocation): Any {
|
||||
println("Before: invocation=[$invocation]")
|
||||
val rval = invocation.proceed()
|
||||
val result = invocation.proceed()
|
||||
println("Invocation returned")
|
||||
return rval
|
||||
return result
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -105,7 +105,7 @@ currently define pointcut interfaces.
|
||||
[[aop-api-advice-before]]
|
||||
=== Before Advice
|
||||
|
||||
A simpler advice type is a before advice. This does not need a `MethodInvocation`
|
||||
A simpler advice type is a _before advice_. This does not need a `MethodInvocation`
|
||||
object, since it is called only before entering the method.
|
||||
|
||||
The main advantage of a before advice is that there is no need to invoke the `proceed()`
|
||||
@@ -122,10 +122,6 @@ The following listing shows the `MethodBeforeAdvice` interface:
|
||||
}
|
||||
----
|
||||
|
||||
(Spring's API design would allow for
|
||||
field before advice, although the usual objects apply to field interception and it is
|
||||
unlikely for Spring to ever implement it.)
|
||||
|
||||
Note that the return type is `void`. Before advice can insert custom behavior before the join
|
||||
point runs but cannot change the return value. If a before advice throws an
|
||||
exception, it stops further execution of the interceptor chain. The exception
|
||||
@@ -176,10 +172,10 @@ TIP: Before advice can be used with any pointcut.
|
||||
[[aop-api-advice-throws]]
|
||||
=== Throws Advice
|
||||
|
||||
Throws advice is invoked after the return of the join point if the join point threw
|
||||
_Throws advice_ is invoked after the return of the join point if the join point threw
|
||||
an exception. Spring offers typed throws advice. Note that this means that the
|
||||
`org.springframework.aop.ThrowsAdvice` interface does not contain any methods. It is a
|
||||
tag interface identifying that the given object implements one or more typed throws
|
||||
marker interface identifying that the given object implements one or more typed throws
|
||||
advice methods. These should be in the following form:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
@@ -189,9 +185,10 @@ advice methods. These should be in the following form:
|
||||
|
||||
Only the last argument is required. The method signatures may have either one or four
|
||||
arguments, depending on whether the advice method is interested in the method and
|
||||
arguments. The next two listing show classes that are examples of throws advice.
|
||||
arguments. The next two listings show classes that are examples of throws advice.
|
||||
|
||||
The following advice is invoked if a `RemoteException` is thrown (including from subclasses):
|
||||
The following advice is invoked if a `RemoteException` is thrown (including subclasses of
|
||||
`RemoteException`):
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -220,9 +217,9 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Unlike the preceding
|
||||
advice, the next example declares four arguments, so that it has access to the invoked method, method
|
||||
arguments, and target object. The following advice is invoked if a `ServletException` is thrown:
|
||||
Unlike the preceding advice, the next example declares four arguments, so that it has
|
||||
access to the invoked method, method arguments, and target object. The following advice
|
||||
is invoked if a `ServletException` is thrown:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -304,7 +301,7 @@ TIP: Throws advice can be used with any pointcut.
|
||||
[[aop-api-advice-after-returning]]
|
||||
=== After Returning Advice
|
||||
|
||||
An after returning advice in Spring must implement the
|
||||
An _after returning advice_ in Spring must implement the
|
||||
`org.springframework.aop.AfterReturningAdvice` interface, which the following listing shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
@@ -368,7 +365,7 @@ TIP: After returning advice can be used with any pointcut.
|
||||
[[aop-api-advice-introduction]]
|
||||
=== Introduction Advice
|
||||
|
||||
Spring treats introduction advice as a special kind of interception advice.
|
||||
Spring treats _introduction advice_ as a special kind of interception advice.
|
||||
|
||||
Introduction requires an `IntroductionAdvisor` and an `IntroductionInterceptor` that
|
||||
implement the following interface:
|
||||
|
||||
@@ -36,7 +36,7 @@ NOTE: At the moment, AOT is focused on allowing Spring applications to be deploy
|
||||
We intend to support more JVM-based use cases in future generations.
|
||||
|
||||
[[aot.basics]]
|
||||
== AOT engine overview
|
||||
== AOT Engine Overview
|
||||
|
||||
The entry point of the AOT engine for processing an `ApplicationContext` is `ApplicationContextAotGenerator`. It takes care of the following steps, based on a `GenericApplicationContext` that represents the application to optimize and a {spring-framework-api}/aot/generate/GenerationContext.html[`GenerationContext`]:
|
||||
|
||||
@@ -225,7 +225,7 @@ When a `datasource` instance is required, a `BeanInstanceSupplier` is called.
|
||||
This supplier invokes the `dataSource()` method on the `dataSourceConfiguration` bean.
|
||||
|
||||
[[aot.running]]
|
||||
== Running with AOT optimizations
|
||||
== Running with AOT Optimizations
|
||||
|
||||
AOT is a mandatory step to transform a Spring application to a native executable, so it
|
||||
is automatically enabled when running in this mode. It is possible to use those optimizations
|
||||
@@ -244,7 +244,7 @@ However, keep in mind that some optimizations are made at build time based on a
|
||||
This section lists the best practices that make sure your application is ready for AOT.
|
||||
|
||||
[[aot.bestpractices.bean-registration]]
|
||||
== Programmatic bean registration
|
||||
=== Programmatic Bean Registration
|
||||
|
||||
The AOT engine takes care of the `@Configuration` model and any callback that might be
|
||||
invoked as part of processing your configuration. If you need to register additional
|
||||
@@ -266,7 +266,7 @@ notion of a classpath. For cases like this, it is crucial that the scanning happ
|
||||
build time.
|
||||
|
||||
[[aot.bestpractices.bean-type]]
|
||||
=== Expose The Most Precise Bean Type
|
||||
=== Expose the Most Precise Bean Type
|
||||
|
||||
While your application may interact with an interface that a bean implements, it is still very important to declare the most precise type.
|
||||
The AOT engine performs additional checks on the bean type, such as detecting the presence of `@Autowired` members or lifecycle callback methods.
|
||||
@@ -352,21 +352,21 @@ However, this is not a best practice and flagging the preferred constructor with
|
||||
|
||||
In case you are working on a code base that you cannot modify, you can set the {spring-framework-api}/beans/factory/support/AbstractBeanDefinition.html#PREFERRED_CONSTRUCTORS_ATTRIBUTE[`preferredConstructors` attribute] on the related bean definition to indicate which constructor should be used.
|
||||
|
||||
[[aot.bestpractices.comlext-data-structure]]
|
||||
=== Avoid Complex Data Structure for Constructor Parameters and Properties
|
||||
[[aot.bestpractices.complex-data-structures]]
|
||||
=== Avoid Complex Data Structures for Constructor Parameters and Properties
|
||||
|
||||
When crafting a `RootBeanDefinition` programmatically, you are not constrained in terms of types that you can use.
|
||||
For instance, you may have a custom `record` with several properties that your bean takes as a constructor argument.
|
||||
|
||||
While this works fine with the regular runtime, AOT does not know how to generate the code of your custom data structure.
|
||||
A good rule of thumb is to keep in mind that bean definitions are an abstraction on top of several models.
|
||||
Rather than using such structure, decomposing to simple types or referring to a bean that is built as such is recommended.
|
||||
Rather than using such structures, decomposing to simple types or referring to a bean that is built as such is recommended.
|
||||
|
||||
As a last resort, you can implement your own `org.springframework.aot.generate.ValueCodeGenerator$Delegate`.
|
||||
To use it, register its fully qualified name in `META-INF/spring/aot.factories` using the `Delegate` as the key.
|
||||
|
||||
[[aot.bestpractices.custom-arguments]]
|
||||
=== Avoid Creating Bean with Custom Arguments
|
||||
=== Avoid Creating Beans with Custom Arguments
|
||||
|
||||
Spring AOT detects what needs to be done to create a bean and translates that in generated code using an instance supplier.
|
||||
The container also supports creating a bean with {spring-framework-api}++/beans/factory/BeanFactory.html#getBean(java.lang.String,java.lang.Object...)++[custom arguments] that leads to several issues with AOT:
|
||||
@@ -378,6 +378,20 @@ For instance, autowiring on fields and methods will be skipped as they are handl
|
||||
|
||||
Rather than having prototype-scoped beans created with custom arguments, we recommend a manual factory pattern where a bean is responsible for the creation of the instance.
|
||||
|
||||
[[aot.bestpractices.circular-dependencies]]
|
||||
=== Avoid Circular Dependencies
|
||||
|
||||
Certain use cases can result in circular dependencies between one or more beans. With the
|
||||
regular runtime, it may be possible to wire those circular dependencies via `@Autowired`
|
||||
on setter methods or fields. However, an AOT-optimized context will fail to start with
|
||||
explicit circular dependencies.
|
||||
|
||||
In an AOT-optimized application, you should therefore strive to avoid circular
|
||||
dependencies. If that is not possible, you can use `@Lazy` injection points or
|
||||
`ObjectProvider` to lazily access or retrieve the necessary collaborating beans. See
|
||||
xref:core/beans/classpath-scanning.adoc#beans-factorybeans-annotations-lazy-injection-points[this tip]
|
||||
for further information.
|
||||
|
||||
[[aot.bestpractices.factory-bean]]
|
||||
=== FactoryBean
|
||||
|
||||
|
||||
@@ -480,11 +480,15 @@ factory method and other bean definition properties, such as a qualifier value t
|
||||
the `@Qualifier` annotation. Other method-level annotations that can be specified are
|
||||
`@Scope`, `@Lazy`, and custom qualifier annotations.
|
||||
|
||||
TIP: In addition to its role for component initialization, you can also place the `@Lazy`
|
||||
[[beans-factorybeans-annotations-lazy-injection-points]]
|
||||
[TIP]
|
||||
====
|
||||
In addition to its role for component initialization, you can also place the `@Lazy`
|
||||
annotation on injection points marked with `@Autowired` or `@Inject`. In this context,
|
||||
it leads to the injection of a lazy-resolution proxy. However, such a proxy approach
|
||||
is rather limited. For sophisticated lazy interactions, in particular in combination
|
||||
with optional dependencies, we recommend `ObjectProvider<MyTargetBean>` instead.
|
||||
====
|
||||
|
||||
Autowired fields and methods are supported, as previously discussed, with additional
|
||||
support for autowiring of `@Bean` methods. The following example shows how to do so:
|
||||
|
||||
@@ -578,6 +578,8 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
NOTE: Do not define such beans to be lazy as the `ApplicationContext` will honour that and will not register the method to listen to events.
|
||||
|
||||
The method signature once again declares the event type to which it listens,
|
||||
but, this time, with a flexible name and without implementing a specific listener interface.
|
||||
The event type can also be narrowed through generics as long as the actual event type
|
||||
@@ -883,11 +885,12 @@ for example, for processing all events asynchronously and/or for handling listen
|
||||
== Convenient Access to Low-level Resources
|
||||
|
||||
For optimal usage and understanding of application contexts, you should familiarize
|
||||
yourself with Spring's `Resource` abstraction, as described in xref:web/webflux-webclient/client-builder.adoc#webflux-client-builder-reactor-resources[Resources].
|
||||
yourself with Spring's `Resource` abstraction, as described in
|
||||
xref:core/resources.adoc[Resources].
|
||||
|
||||
An application context is a `ResourceLoader`, which can be used to load `Resource` objects.
|
||||
A `Resource` is essentially a more feature rich version of the JDK `java.net.URL` class.
|
||||
In fact, the implementations of the `Resource` wrap an instance of `java.net.URL`, where
|
||||
In fact, implementations of `Resource` wrap an instance of `java.net.URL`, where
|
||||
appropriate. A `Resource` can obtain low-level resources from almost any location in a
|
||||
transparent fashion, including from the classpath, a filesystem location, anywhere
|
||||
describable with a standard URL, and some other variations. If the resource location
|
||||
|
||||
@@ -57,9 +57,9 @@ The following table describes these properties:
|
||||
In addition to bean definitions that contain information on how to create a specific
|
||||
bean, the `ApplicationContext` implementations also permit the registration of existing
|
||||
objects that are created outside the container (by users). This is done by accessing the
|
||||
ApplicationContext's `BeanFactory` through the `getBeanFactory()` method, which returns
|
||||
the `DefaultListableBeanFactory` implementation. `DefaultListableBeanFactory` supports
|
||||
this registration through the `registerSingleton(..)` and `registerBeanDefinition(..)`
|
||||
ApplicationContext's `BeanFactory` through the `getAutowireCapableBeanFactory()` method,
|
||||
which returns the `DefaultListableBeanFactory` implementation. `DefaultListableBeanFactory`
|
||||
supports this registration through the `registerSingleton(..)` and `registerBeanDefinition(..)`
|
||||
methods. However, typical applications work solely with beans defined through regular
|
||||
bean definition metadata.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
If a bean is a dependency of another bean, that usually means that one bean is set as a
|
||||
property of another. Typically you accomplish this with the
|
||||
xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-ref-element[`<ref/>` element>]
|
||||
xref:core/beans/dependencies/factory-properties-detailed.adoc#beans-ref-element[`<ref/>` element]
|
||||
in XML-based metadata or through xref:core/beans/dependencies/factory-autowire.adoc[autowiring].
|
||||
|
||||
However, sometimes dependencies between beans are less direct. An example is when a static
|
||||
|
||||
-7
@@ -120,8 +120,6 @@ dynamically generate a subclass that overrides the method.
|
||||
subclasses cannot be `final`, and the method to be overridden cannot be `final`, either.
|
||||
* Unit-testing a class that has an `abstract` method requires you to subclass the class
|
||||
yourself and to supply a stub implementation of the `abstract` method.
|
||||
* Concrete methods are also necessary for component scanning, which requires concrete
|
||||
classes to pick up.
|
||||
* A further key limitation is that lookup methods do not work with factory methods and
|
||||
in particular not with `@Bean` methods in configuration classes, since, in that case,
|
||||
the container is not in charge of creating the instance and therefore cannot create
|
||||
@@ -293,11 +291,6 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Note that you should typically declare such annotated lookup methods with a concrete
|
||||
stub implementation, in order for them to be compatible with Spring's component
|
||||
scanning rules where abstract classes get ignored by default. This limitation does not
|
||||
apply to explicitly registered or explicitly imported bean classes.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Another way of accessing differently scoped target beans is an `ObjectFactory`/
|
||||
|
||||
+4
-4
@@ -102,8 +102,8 @@ following snippet:
|
||||
----
|
||||
<bean id="theTargetBean" class="..." />
|
||||
|
||||
<bean id="client" class="...">
|
||||
<property name="targetName" value="theTargetBean"/>
|
||||
<bean id="theClientBean" class="...">
|
||||
<property name="targetName" ref="theTargetBean"/>
|
||||
</bean>
|
||||
----
|
||||
|
||||
@@ -168,8 +168,8 @@ listings shows how to use the `parent` attribute:
|
||||
|
||||
[source,xml,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
<!-- in the child (descendant) context -->
|
||||
<bean id="accountService" <!-- bean name is the same as the parent bean -->
|
||||
<!-- in the child (descendant) context, bean name is the same as the parent bean -->
|
||||
<bean id="accountService"
|
||||
class="org.springframework.aop.framework.ProxyFactoryBean">
|
||||
<property name="target">
|
||||
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
|
||||
|
||||
@@ -64,7 +64,7 @@ This prevents the same `@Bean` method from accidentally being invoked through a
|
||||
Java method call, which helps to reduce subtle bugs that can be hard to track down.
|
||||
|
||||
When `@Bean` methods are declared within classes that are not annotated with
|
||||
`@Configuration` - or when `@Configuration(proxyBeanMethods=false)` is declared -,
|
||||
`@Configuration`, or when `@Configuration(proxyBeanMethods=false)` is declared,
|
||||
they are referred to as being processed in a "lite" mode. In such scenarios,
|
||||
`@Bean` methods are effectively a general-purpose factory method mechanism without
|
||||
special runtime processing (that is, without generating a CGLIB subclass for it).
|
||||
|
||||
@@ -444,18 +444,27 @@ component. This section discusses both of these options.
|
||||
The compiler can operate in one of three modes, which are captured in the
|
||||
`org.springframework.expression.spel.SpelCompilerMode` enum. The modes are as follows.
|
||||
|
||||
* `OFF` (default): The compiler is switched off.
|
||||
* `IMMEDIATE`: In immediate mode, the expressions are compiled as soon as possible. This
|
||||
is typically after the first interpreted evaluation. If the compiled expression fails
|
||||
(typically due to a type changing, as described earlier), the caller of the expression
|
||||
evaluation receives an exception.
|
||||
* `MIXED`: In mixed mode, the expressions silently switch between interpreted and
|
||||
compiled mode over time. After some number of interpreted runs, they switch to compiled
|
||||
form and, if something goes wrong with the compiled form (such as a type changing, as
|
||||
described earlier), the expression automatically switches back to interpreted form
|
||||
again. Sometime later, it may generate another compiled form and switch to it.
|
||||
Basically, the exception that the user gets in `IMMEDIATE` mode is instead handled
|
||||
internally.
|
||||
`OFF` ::
|
||||
The compiler is switched off, and all expressions will be evaluated in _interpreted_
|
||||
mode. This is the default mode.
|
||||
`IMMEDIATE` ::
|
||||
In immediate mode, expressions are compiled as soon as possible, typically after the
|
||||
first interpreted evaluation. If evaluation of the compiled expression fails (for
|
||||
example, due to a type changing, as described earlier), the caller of the expression
|
||||
evaluation receives an exception. If the types of various expression elements change
|
||||
over time, consider switching to `MIXED` mode or turning off the compiler.
|
||||
`MIXED` ::
|
||||
In mixed mode, expression evaluation silently switches between _interpreted_ and
|
||||
_compiled_ over time. After some number of successful interpreted runs, the expression
|
||||
gets compiled. If evaluation of the compiled expression fails (for example, due to a
|
||||
type changing), that failure will be caught internally, and the system will switch back
|
||||
to interpreted mode for the given expression. Basically, the exception that the caller
|
||||
receives in `IMMEDIATE` mode is instead handled internally. Sometime later, the
|
||||
compiler may generate another compiled form and switch to it. This cycle of switching
|
||||
between interpreted and compiled mode will continue until the system determines that it
|
||||
does not make sense to continue trying — for example, when a certain failure threshold
|
||||
has been reached — at which point the system will permanently switch to interpreted
|
||||
mode for the given expression.
|
||||
|
||||
`IMMEDIATE` mode exists because `MIXED` mode could cause issues for expressions that
|
||||
have side effects. If a compiled expression blows up after partially succeeding, it
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
You can invoke constructors by using the `new` operator. You should use the fully
|
||||
qualified class name for all types except those located in the `java.lang` package
|
||||
(`Integer`, `Float`, `String`, and so on). The following example shows how to use the
|
||||
`new` operator to invoke constructors:
|
||||
(`Integer`, `Float`, `String`, and so on).
|
||||
xref:core/expressions/language-ref/varargs.adoc[Varargs] are also supported.
|
||||
|
||||
The following example shows how to use the `new` operator to invoke constructors.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -12,30 +14,29 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Inventor einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
Inventor einstein = parser.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor(
|
||||
'Albert Einstein', 'German'))").getValue(societyContext);
|
||||
parser.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
.getValue(societyContext);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
val einstein = parser.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor::class.java)
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
parser.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
.getValue(societyContext)
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
= Functions
|
||||
|
||||
You can extend SpEL by registering user-defined functions that can be called within
|
||||
expressions by using the `#functionName(...)` syntax. Functions can be registered as
|
||||
variables in `EvaluationContext` implementations via the `setVariable()` method.
|
||||
expressions by using the `#functionName(...)` syntax, and like with standard method
|
||||
invocations, xref:core/expressions/language-ref/varargs.adoc[varargs] are also supported
|
||||
for function invocations.
|
||||
|
||||
Functions can be registered as _variables_ in `EvaluationContext` implementations via the
|
||||
`setVariable()` method.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
@@ -110,8 +114,9 @@ potentially more efficient use cases if the `MethodHandle` target and parameters
|
||||
been fully bound prior to registration; however, partially bound handles are also
|
||||
supported.
|
||||
|
||||
Consider the `String#formatted(String, Object...)` instance method, which produces a
|
||||
message according to a template and a variable number of arguments.
|
||||
Consider the `String#formatted(Object...)` instance method, which produces a message
|
||||
according to a template and a variable number of arguments
|
||||
(xref:core/expressions/language-ref/varargs.adoc[varargs]).
|
||||
|
||||
You can register and use the `formatted` method as a `MethodHandle`, as the following
|
||||
example shows:
|
||||
@@ -151,10 +156,10 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also
|
||||
supported. This is likely to be more performant if both the target and all the arguments
|
||||
are bound. In that case no arguments are necessary in the SpEL expression, as the
|
||||
following example shows:
|
||||
As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is
|
||||
also supported. This is likely to be more performant if both the target and all the
|
||||
arguments are bound. In that case no arguments are necessary in the SpEL expression, as
|
||||
the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -168,9 +173,10 @@ Java::
|
||||
String template = "This is a %s message with %s words: <%s>";
|
||||
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
|
||||
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs); //here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs);
|
||||
context.setVariable("message", mh);
|
||||
|
||||
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
@@ -189,9 +195,10 @@ Kotlin::
|
||||
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
|
||||
|
||||
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs) //here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs)
|
||||
context.setVariable("message", mh)
|
||||
|
||||
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
@@ -201,4 +208,3 @@ Kotlin::
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
[[expressions-methods]]
|
||||
= Methods
|
||||
|
||||
You can invoke methods by using typical Java programming syntax. You can also invoke methods
|
||||
on literals. Variable arguments are also supported. The following examples show how to
|
||||
invoke methods:
|
||||
You can invoke methods by using the typical Java programming syntax. You can also invoke
|
||||
methods directly on literals such as strings or numbers.
|
||||
xref:core/expressions/language-ref/varargs.adoc[Varargs] are supported as well.
|
||||
|
||||
The following examples show how to invoke methods.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
[[expressions-varargs]]
|
||||
= Varargs Invocations
|
||||
|
||||
The Spring Expression Language supports
|
||||
https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html[varargs]
|
||||
invocations for xref:core/expressions/language-ref/constructors.adoc[constructors],
|
||||
xref:core/expressions/language-ref/methods.adoc[methods], and user-defined
|
||||
xref:core/expressions/language-ref/functions.adoc[functions].
|
||||
|
||||
The following example shows how to invoke the `java.lang.String#formatted(Object...)`
|
||||
_varargs_ method within an expression by supplying the variable argument list as separate
|
||||
arguments (`'blue', 1`).
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted('blue', 1)";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%d'.formatted('blue', 1)"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
A variable argument list can also be supplied as an array, as demonstrated in the
|
||||
following example (`new Object[] {'blue', 1}`).
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
As an alternative, a variable argument list can be supplied as a `java.util.List` – for
|
||||
example, as an xref:core/expressions/language-ref/inline-lists.adoc[inline list]
|
||||
(`{'blue', 1}`). The following example shows how to do that.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted({'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%d'.formatted({'blue', 1})"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
[[expressions-varargs-type-conversion]]
|
||||
== Varargs Type Conversion
|
||||
|
||||
In contrast to the standard support for varargs invocations in Java,
|
||||
xref:core/expressions/evaluation.adoc#expressions-type-conversion[type conversion] may be
|
||||
applied to the individual arguments when invoking varargs constructors, methods, or
|
||||
functions in SpEL.
|
||||
|
||||
For example, if we have registered a custom
|
||||
xref:core/expressions/language-ref/functions.adoc[function] in the `EvaluationContext`
|
||||
under the name `#reverseStrings` for a method with the signature
|
||||
`String reverseStrings(String... strings)`, we can invoke that function within a SpEL
|
||||
expression with any argument that can be converted to a `String`, as demonstrated in the
|
||||
following example.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "3.0, 2.0, 1, SpEL"
|
||||
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
|
||||
String message = parser.parseExpression(expression)
|
||||
.getValue(evaluationContext, String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "3.0, 2.0, 1, SpEL"
|
||||
val expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)"
|
||||
val message = parser.parseExpression(expression)
|
||||
.getValue(evaluationContext, String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
Similarly, any array whose component type is a subtype of the required varargs type can
|
||||
be supplied as the variable argument list for a varargs invocation. For example, a
|
||||
`String[]` array can be supplied to a varargs invocation that accepts an `Object...`
|
||||
argument list.
|
||||
|
||||
The following listing demonstrates that we can supply a `String[]` array to the
|
||||
`java.lang.String#formatted(Object...)` _varargs_ method. It also highlights that `1`
|
||||
will be automatically converted to `"1"`.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
@@ -196,7 +196,7 @@ Kotlin::
|
||||
|
||||
When used as `org.springframework.validation.Validator`, `LocalValidatorFactoryBean`
|
||||
invokes the underlying `jakarta.validation.Validator`, and then adapts
|
||||
``ContraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
``ConstraintViolation``s to ``FieldError``s, and registers them with the `Errors` object
|
||||
passed into the `validate` method.
|
||||
|
||||
|
||||
|
||||
@@ -281,11 +281,11 @@ Kotlin::
|
||||
|
||||
A portable format annotation API exists in the `org.springframework.format.annotation`
|
||||
package. You can use `@NumberFormat` to format `Number` fields such as `Double` and
|
||||
`Long`, `@DurationFormat` to format `Duration` fields in ISO8601 and simplified styles,
|
||||
and `@DateTimeFormat` to format `java.util.Date`, `java.util.Calendar`, `Long`
|
||||
(for millisecond timestamps) as well as JSR-310 `java.time`.
|
||||
`Long`, `@DurationFormat` to format `Duration` fields in ISO-8601 and simplified styles,
|
||||
and `@DateTimeFormat` to format fields such as `java.util.Date`, `java.util.Calendar`,
|
||||
and `Long` (for millisecond timestamps) as well as JSR-310 `java.time` types.
|
||||
|
||||
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO Date
|
||||
The following example uses `@DateTimeFormat` to format a `java.util.Date` as an ISO date
|
||||
(yyyy-MM-dd):
|
||||
|
||||
[tabs]
|
||||
@@ -311,6 +311,29 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
For further details, see the javadoc for
|
||||
{spring-framework-api}/format/annotation/DateTimeFormat.html[`@DateTimeFormat`],
|
||||
{spring-framework-api}/format/annotation/DurationFormat.html[`@DurationFormat`], and
|
||||
{spring-framework-api}/format/annotation/NumberFormat.html[`@NumberFormat`].
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Style-based formatting and parsing rely on locale-sensitive patterns which may change
|
||||
depending on the Java runtime. Specifically, applications that rely on date, time, or
|
||||
number parsing and formatting may encounter incompatible changes in behavior when running
|
||||
on JDK 20 or higher.
|
||||
|
||||
Using an ISO standardized format or a concrete pattern that you control allows for
|
||||
reliable system-independent and locale-independent parsing and formatting of date, time,
|
||||
and number values.
|
||||
|
||||
For `@DateTimeFormat`, the use of fallback patterns can also help to address
|
||||
compatibility issues.
|
||||
|
||||
For further details, see the
|
||||
https://github.com/spring-projects/spring-framework/wiki/Date-and-Time-Formatting-with-JDK-20-and-higher[Date and Time Formatting with JDK 20 and higher]
|
||||
page in the Spring Framework wiki.
|
||||
====
|
||||
|
||||
[[format-FormatterRegistry-SPI]]
|
||||
== The `FormatterRegistry` SPI
|
||||
|
||||
@@ -198,7 +198,7 @@ methods it offers can be found in the {spring-framework-api}/validation/Errors.h
|
||||
Validators may also get locally invoked for the immediate validation of a given object,
|
||||
not involving a binding process. As of 6.1, this has been simplified through a new
|
||||
`Validator.validateObject(Object)` method which is available by default now, returning
|
||||
a simple ´Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
a simple `Errors` representation which can be inspected: typically calling `hasErrors()`
|
||||
or the new `failOnError` method for turning the error summary message into an exception
|
||||
(for example, `validator.validateObject(myObject).failOnError(IllegalArgumentException::new)`).
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ xref:overview.adoc[Overview] :: History, Design Philosophy, Feedback,
|
||||
Getting Started.
|
||||
xref:core.adoc[Core] :: IoC Container, Events, Resources, i18n,
|
||||
Validation, Data Binding, Type Conversion, SpEL, AOP, AOT.
|
||||
<<testing.adoc#testing, Testing>> :: Mock Objects, TestContext Framework,
|
||||
xref:testing.adoc[Testing] :: Mock Objects, TestContext Framework,
|
||||
Spring MVC Test, WebTestClient.
|
||||
xref:data-access.adoc[Data Access] :: Transactions, DAO Support,
|
||||
JDBC, R2DBC, O/R Mapping, XML Marshalling.
|
||||
|
||||
@@ -6,7 +6,7 @@ the same way as Spring's integration does for the JDBC API.
|
||||
|
||||
JMS can be roughly divided into two areas of functionality, namely the production and
|
||||
consumption of messages. The `JmsTemplate` class is used for message production and
|
||||
synchronous message reception. For asynchronous reception similar to Jakarta EE's
|
||||
synchronous message receipt. For asynchronous receipt similar to Jakarta EE's
|
||||
message-driven bean style, Spring provides a number of message-listener containers that
|
||||
you can use to create Message-Driven POJOs (MDPs). Spring also provides a declarative way
|
||||
to create message listeners.
|
||||
|
||||
@@ -5,7 +5,7 @@ This describes how to receive messages with JMS in Spring.
|
||||
|
||||
|
||||
[[jms-receiving-sync]]
|
||||
== Synchronous Reception
|
||||
== Synchronous Receipt
|
||||
|
||||
While JMS is typically associated with asynchronous processing, you can
|
||||
consume messages synchronously. The overloaded `receive(..)` methods provide this
|
||||
@@ -16,11 +16,11 @@ the receiver should wait before giving up waiting for a message.
|
||||
|
||||
|
||||
[[jms-receiving-async]]
|
||||
== Asynchronous reception: Message-Driven POJOs
|
||||
== Asynchronous Receipt: Message-Driven POJOs
|
||||
|
||||
NOTE: Spring also supports annotated-listener endpoints through the use of the `@JmsListener`
|
||||
annotation and provides an open infrastructure to register endpoints programmatically.
|
||||
This is, by far, the most convenient way to setup an asynchronous receiver.
|
||||
annotation and provides open infrastructure to register endpoints programmatically.
|
||||
This is, by far, the most convenient way to set up an asynchronous receiver.
|
||||
See xref:integration/jms/annotated.adoc#jms-annotated-support[Enable Listener Endpoint Annotations] for more details.
|
||||
|
||||
In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Message-Driven
|
||||
@@ -154,7 +154,7 @@ listener container.
|
||||
|
||||
You can activate local resource transactions through the `sessionTransacted` flag
|
||||
on the listener container definition. Each message listener invocation then operates
|
||||
within an active JMS transaction, with message reception rolled back in case of listener
|
||||
within an active JMS transaction, with message receipt rolled back in case of listener
|
||||
execution failure. Sending a response message (through `SessionAwareMessageListener`) is
|
||||
part of the same local transaction, but any other resource operations (such as
|
||||
database access) operate independently. This usually requires duplicate message
|
||||
@@ -173,7 +173,7 @@ To configure a message listener container for XA transaction participation, you
|
||||
to configure a `JtaTransactionManager` (which, by default, delegates to the Jakarta EE
|
||||
server's transaction subsystem). Note that the underlying JMS `ConnectionFactory` needs to
|
||||
be XA-capable and properly registered with your JTA transaction coordinator. (Check your
|
||||
Jakarta EE server's configuration of JNDI resources.) This lets message reception as well
|
||||
Jakarta EE server's configuration of JNDI resources.) This lets message receipt as well
|
||||
as (for example) database access be part of the same transaction (with unified commit
|
||||
semantics, at the expense of XA transaction log overhead).
|
||||
|
||||
|
||||
@@ -167,13 +167,15 @@ operations that do not refer to a specific destination.
|
||||
|
||||
One of the most common uses of JMS messages in the EJB world is to drive message-driven
|
||||
beans (MDBs). Spring offers a solution to create message-driven POJOs (MDPs) in a way
|
||||
that does not tie a user to an EJB container. (See xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous reception: Message-Driven POJOs] for detailed
|
||||
coverage of Spring's MDP support.) Since Spring Framework 4.1, endpoint methods can be
|
||||
annotated with `@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints] for more details.
|
||||
that does not tie a user to an EJB container. (See
|
||||
xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous Receipt: Message-Driven POJOs]
|
||||
for detailed coverage of Spring's MDP support.) Endpoint methods can be annotated with
|
||||
`@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints]
|
||||
for more details.
|
||||
|
||||
A message listener container is used to receive messages from a JMS message queue and
|
||||
drive the `MessageListener` that is injected into it. The listener container is
|
||||
responsible for all threading of message reception and dispatches into the listener for
|
||||
responsible for all threading of message receipt and dispatches into the listener for
|
||||
processing. A message listener container is the intermediary between an MDP and a
|
||||
messaging provider and takes care of registering to receive messages, participating in
|
||||
transactions, resource acquisition and release, exception conversion, and so on. This
|
||||
@@ -227,7 +229,7 @@ the JMS provider, advanced functionality (such as participation in externally ma
|
||||
transactions), and compatibility with Jakarta EE environments.
|
||||
|
||||
You can customize the cache level of the container. Note that, when no caching is enabled,
|
||||
a new connection and a new session is created for each message reception. Combining this
|
||||
a new connection and a new session is created for each message receipt. Combining this
|
||||
with a non-durable subscription with high loads may lead to message loss. Make sure to
|
||||
use a proper cache level in such a case.
|
||||
|
||||
@@ -246,7 +248,7 @@ in the form of a business entity existence check or a protocol table check.
|
||||
Any such arrangements are significantly more efficient than the alternative:
|
||||
wrapping your entire processing with an XA transaction (through configuring your
|
||||
`DefaultMessageListenerContainer` with an `JtaTransactionManager`) to cover the
|
||||
reception of the JMS message as well as the execution of the business logic in your
|
||||
receipt of the JMS message as well as the execution of the business logic in your
|
||||
message listener (including database operations, etc.).
|
||||
|
||||
IMPORTANT: The default `AUTO_ACKNOWLEDGE` mode does not provide proper reliability guarantees.
|
||||
|
||||
@@ -37,7 +37,7 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
|
||||
|Processing time for an execution of a `@Scheduled` task
|
||||
|===
|
||||
|
||||
NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
|
||||
NOTE: Observations use Micrometer's official naming convention, but Metrics names will be automatically converted
|
||||
{micrometer-docs}/concepts/naming.html[to the format preferred by the monitoring system backend]
|
||||
(Prometheus, Atlas, Graphite, InfluxDB...).
|
||||
|
||||
@@ -97,7 +97,7 @@ This can be done by declaring a `SchedulingConfigurer` bean that sets the observ
|
||||
|
||||
include-code::./ObservationSchedulingConfigurer[]
|
||||
|
||||
It is using the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
|
||||
It uses the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
|
||||
You can configure a custom implementation on the `ObservationRegistry` directly.
|
||||
During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type).
|
||||
|
||||
@@ -107,7 +107,7 @@ By default, the following `KeyValues` are created:
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`code.function` _(required)_|Name of Java `Method` that is scheduled for execution.
|
||||
|`code.function` _(required)_|Name of the Java `Method` that is scheduled for execution.
|
||||
|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method, or `"ANONYMOUS"` for anonymous classes.
|
||||
|`error` _(required)_|Class name of the exception thrown during the execution, or `"none"` if no exception happened.
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
@@ -126,7 +126,7 @@ This instrumentation will create 2 types of observations:
|
||||
* `"jms.message.publish"` when a JMS message is sent to the broker, typically with `JmsTemplate`.
|
||||
* `"jms.message.process"` when a JMS message is processed by the application, typically with a `MessageListener` or a `@JmsListener` annotated method.
|
||||
|
||||
NOTE: currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the reception of a message.
|
||||
NOTE: Currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the receipt of a message.
|
||||
Such an integration would typically instrument `MessageConsumer#receive` method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application.
|
||||
|
||||
By default, both observations share the same set of possible `KeyValues`:
|
||||
@@ -138,7 +138,7 @@ By default, both observations share the same set of possible `KeyValues`:
|
||||
|`error` |Class name of the exception thrown during the messaging operation (or "none").
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
|`messaging.destination.temporary` _(required)_|Whether the destination is a `TemporaryQueue` or `TemporaryTopic` (values: `"true"` or `"false"`).
|
||||
|`messaging.operation` _(required)_|Name of JMS operation being performed (values: `"publish"` or `"process"`).
|
||||
|`messaging.operation` _(required)_|Name of the JMS operation being performed (values: `"publish"` or `"process"`).
|
||||
|===
|
||||
|
||||
.High cardinality Keys
|
||||
@@ -146,7 +146,7 @@ By default, both observations share the same set of possible `KeyValues`:
|
||||
|===
|
||||
|Name | Description
|
||||
|`messaging.message.conversation_id` |The correlation ID of the JMS message.
|
||||
|`messaging.destination.name` |The name of destination the current message was sent to.
|
||||
|`messaging.destination.name` |The name of the destination the current message was sent to.
|
||||
|`messaging.message.id` |Value used by the messaging system as an identifier for the message.
|
||||
|===
|
||||
|
||||
@@ -213,7 +213,7 @@ By default, the following `KeyValues` are created:
|
||||
|Name | Description
|
||||
|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened.
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|
||||
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
|
||||
@@ -235,10 +235,10 @@ This can be done on the `WebHttpHandlerBuilder`, as follows:
|
||||
|
||||
include-code::./HttpHandlerConfiguration[]
|
||||
|
||||
It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by an application Controller.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and <<web.adoc#webflux-ann-rest-exceptions,`ProblemDetail` support>> will not be recorded with the observation.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and xref:web/webflux/ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
|
||||
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
|
||||
|
||||
include-code::./UserController[]
|
||||
@@ -251,7 +251,7 @@ By default, the following `KeyValues` are created:
|
||||
|Name | Description
|
||||
|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened.
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|
||||
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
|
||||
@@ -270,6 +270,7 @@ By default, the following `KeyValues` are created:
|
||||
== HTTP Client Instrumentation
|
||||
|
||||
HTTP client exchange observations are created with the name `"http.client.requests"` for blocking and reactive clients.
|
||||
This observation measures the entire HTTP request/response exchange, from connection establishment up to body deserialization.
|
||||
Unlike their server counterparts, the instrumentation is implemented directly in the client so the only required step is to configure an `ObservationRegistry` on the client.
|
||||
|
||||
[[observability.http-client.resttemplate]]
|
||||
@@ -284,8 +285,8 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|
||||
@@ -312,8 +313,8 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if the request could not be created.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|
||||
@@ -332,7 +333,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
[[observability.http-client.webclient]]
|
||||
=== WebClient
|
||||
|
||||
Applications must configure an `ObservationRegistry` on the `WebClient` builder to enable the instrumentation; without that, observations are "no-ops".
|
||||
Applications must configure an `ObservationRegistry` on the `WebClient.Builder` to enable the instrumentation; without that, observations are "no-ops".
|
||||
Spring Boot will auto-configure `WebClient.Builder` beans with the observation registry already set.
|
||||
|
||||
Instrumentation uses the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
|
||||
@@ -341,8 +342,8 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. The protocol, host and port part of the URI are not considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|`outcome` _(required)_|Outcome of the HTTP client exchange.
|
||||
|
||||
@@ -115,11 +115,15 @@ Finally, the body can be set to a callback function that writes to an `OutputStr
|
||||
|
||||
==== Retrieving the response
|
||||
|
||||
Once the request has been set up, the HTTP response is accessed by invoking `retrieve()`.
|
||||
The response body can be accessed by using `body(Class)` or `body(ParameterizedTypeReference)` for parameterized types like lists.
|
||||
Once the request has been set up, it can be sent by chaining method calls after `retrieve()`.
|
||||
For example, the response body can be accessed by using `retrieve().body(Class)` or `retrieve().body(ParameterizedTypeReference)` for parameterized types like lists.
|
||||
The `body` method converts the response contents into various types – for instance, bytes can be converted into a `String`, JSON can be converted into objects using Jackson, and so on (see <<rest-message-conversion>>).
|
||||
|
||||
The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body.
|
||||
The response can also be converted into a `ResponseEntity`, giving access to the response headers as well as the body, with `retrieve().toEntity(Class)`
|
||||
|
||||
NOTE: Calling `retrieve()` by itself is a no-op and returns a `ResponseSpec`.
|
||||
Applications must invoke a terminal operation on the `ResponseSpec` to have any side effect.
|
||||
If consuming the response has no interest for your use case, you can use `retrieve().toBodilessEntity()`.
|
||||
|
||||
This sample shows how `RestClient` can be used to perform a simple `GET` request.
|
||||
|
||||
@@ -938,10 +942,10 @@ method parameters:
|
||||
| Dynamically set the HTTP method for the request, overriding the annotation's `method` attribute
|
||||
|
||||
| `@RequestHeader`
|
||||
| Add a request header or multiple headers. The argument may be a `Map<String, ?>` or
|
||||
`MultiValueMap<String, ?>` with multiple headers, a `Collection<?>` of values, or an
|
||||
individual value. Type conversion is supported for non-String values. This overrides
|
||||
the annotation's `headers` attribute.
|
||||
| Add a request header or multiple headers. The argument may be a single value,
|
||||
a `Collection<?>` of values, `Map<String, ?>`,`MultiValueMap<String, ?>`.
|
||||
Type conversion is supported for non-String values. Header values are added and
|
||||
do not override already added header values.
|
||||
|
||||
| `@PathVariable`
|
||||
| Add a variable for expand a placeholder in the request URL. The argument may be a
|
||||
@@ -988,6 +992,27 @@ parameter annotation) is set to `false`, or the parameter is marked optional as
|
||||
|
||||
|
||||
|
||||
[[rest-http-interface.custom-resolver]]
|
||||
=== Custom argument resolver
|
||||
|
||||
For more complex cases, HTTP interfaces do not support `RequestEntity` types as method parameters.
|
||||
This would take over the entire HTTP request and not improve the semantics of the interface.
|
||||
Instead of adding many method parameters, developers can combine them into a custom type
|
||||
and configure a dedicated `HttpServiceArgumentResolver` implementation.
|
||||
|
||||
In the following HTTP interface, we are using a custom `Search` type as a parameter:
|
||||
|
||||
include-code::./CustomHttpServiceArgumentResolver[tag=httpinterface,indent=0]
|
||||
|
||||
We can implement our own `HttpServiceArgumentResolver` that supports our custom `Search` type
|
||||
and writes its data in the outgoing HTTP request.
|
||||
|
||||
include-code::./CustomHttpServiceArgumentResolver[tag=argumentresolver,indent=0]
|
||||
|
||||
Finally, we can use this argument resolver during the setup and use our HTTP interface.
|
||||
|
||||
include-code::./CustomHttpServiceArgumentResolver[tag=usage,indent=0]
|
||||
|
||||
[[rest-http-interface-return-values]]
|
||||
=== Return Values
|
||||
|
||||
|
||||
@@ -14,16 +14,13 @@ For example, `@Autowired lateinit var thing: Thing` implies that a bean
|
||||
of type `Thing` must be registered in the application context, while `@Autowired lateinit var thing: Thing?`
|
||||
does not raise an error if such a bean does not exist.
|
||||
|
||||
Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, Car)` implies
|
||||
Following the same principle, `@Bean fun play(toy: Toy, car: Car?) = Baz(toy, car)` implies
|
||||
that a bean of type `Toy` must be registered in the application context, while a bean of
|
||||
type `Car` may or may not exist. The same behavior applies to autowired constructor parameters.
|
||||
|
||||
NOTE: If you use bean validation on classes with properties or a primary constructor
|
||||
parameters, you may need to use
|
||||
with parameters, you may need to use
|
||||
{kotlin-docs}/annotations.html#annotation-use-site-targets[annotation use-site targets],
|
||||
such as `@field:NotNull` or `@get:Size(min=5, max=15)`, as described in
|
||||
{stackoverflow-site}/a/35853200/1092077[this Stack Overflow response].
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+211
-52
@@ -1,48 +1,91 @@
|
||||
[[spring-testing-annotation-beanoverriding-mockitobean]]
|
||||
= `@MockitoBean` and `@MockitoSpyBean`
|
||||
|
||||
`@MockitoBean` and `@MockitoSpyBean` are used on fields in test classes to override beans
|
||||
in the test's `ApplicationContext` with a Mockito _mock_ or _spy_, respectively. In the
|
||||
latter case, an early instance of the original bean is captured and wrapped by the spy.
|
||||
{spring-framework-api}/test/context/bean/override/mockito/MockitoBean.html[`@MockitoBean`] and
|
||||
{spring-framework-api}/test/context/bean/override/mockito/MockitoSpyBean.html[`@MockitoSpyBean`]
|
||||
can be used in test classes to override a bean in the test's `ApplicationContext` with a
|
||||
Mockito _mock_ or _spy_, respectively. In the latter case, an early instance of the
|
||||
original bean is captured and wrapped by the spy.
|
||||
|
||||
By default, the annotated field's type is used to search for candidate beans to override.
|
||||
If multiple candidates match, `@Qualifier` can be provided to narrow the candidate to
|
||||
override. Alternatively, a candidate whose bean name matches the name of the field will
|
||||
match.
|
||||
The annotations can be applied in the following ways.
|
||||
|
||||
When using `@MockitoBean`, a new bean will be created if a corresponding bean does not
|
||||
exist. However, if you would like for the test to fail when a corresponding bean does not
|
||||
exist, you can set the `enforceOverride` attribute to `true` – for example,
|
||||
`@MockitoBean(enforceOverride = true)`.
|
||||
* On a non-static field in a test class or any of its superclasses.
|
||||
* On a non-static field in an enclosing class for a `@Nested` test class or in any class
|
||||
in the type hierarchy or enclosing class hierarchy above the `@Nested` test class.
|
||||
* At the type level on a test class or any superclass or implemented interface in the
|
||||
type hierarchy above the test class.
|
||||
* At the type level on an enclosing class for a `@Nested` test class or on any class or
|
||||
interface in the type hierarchy or enclosing class hierarchy above the `@Nested` test
|
||||
class.
|
||||
|
||||
To use a by-name override rather than a by-type override, specify the `name` attribute
|
||||
of the annotation.
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared on a field, the bean to mock or spy
|
||||
is inferred from the type of the annotated field. If multiple candidates exist in the
|
||||
`ApplicationContext`, a `@Qualifier` annotation can be declared on the field to help
|
||||
disambiguate. In the absence of a `@Qualifier` annotation, the name of the annotated
|
||||
field will be used as a _fallback qualifier_. Alternatively, you can explicitly specify a
|
||||
bean name to mock or spy by setting the `value` or `name` attribute in the annotation.
|
||||
|
||||
When `@MockitoBean` or `@MockitoSpyBean` is declared at the type level, the type of bean
|
||||
(or beans) to mock or spy must be supplied via the `types` attribute in the annotation –
|
||||
for example, `@MockitoBean(types = {OrderService.class, UserService.class})`. If multiple
|
||||
candidates exist in the `ApplicationContext`, you can explicitly specify a bean name to
|
||||
mock or spy by setting the `name` attribute. Note, however, that the `types` attribute
|
||||
must contain a single type if an explicit bean `name` is configured – for example,
|
||||
`@MockitoBean(name = "ps1", types = PrintingService.class)`.
|
||||
|
||||
To support reuse of mock configuration, `@MockitoBean` and `@MockitoSpyBean` may be used
|
||||
as meta-annotations to create custom _composed annotations_ – for example, to define
|
||||
common mock or spy configuration in a single annotation that can be reused across a test
|
||||
suite. `@MockitoBean` and `@MockitoSpyBean` can also be used as repeatable annotations at
|
||||
the type level — for example, to mock or spy several beans by name.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Qualifiers, including the name of the field, are used to determine if a separate
|
||||
Qualifiers, including the name of a field, are used to determine if a separate
|
||||
`ApplicationContext` needs to be created. If you are using this feature to mock or spy
|
||||
the same bean in several tests, make sure to name the field consistently to avoid
|
||||
the same bean in several test classes, make sure to name the fields consistently to avoid
|
||||
creating unnecessary contexts.
|
||||
====
|
||||
|
||||
Each annotation also defines Mockito-specific attributes to fine-tune the mocking details.
|
||||
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
|
||||
|
||||
By default, the `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy for test bean overriding].
|
||||
If no existing bean matches, a new bean is created on the fly. As mentioned previously,
|
||||
you can switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to
|
||||
`true`.
|
||||
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy for bean overrides].
|
||||
If a corresponding bean does not exist, a new bean will be created. However, you can
|
||||
switch to the `REPLACE` strategy by setting the `enforceOverride` attribute to `true` –
|
||||
for example, `@MockitoBean(enforceOverride = true)`.
|
||||
|
||||
The `@MockitoSpyBean` annotation uses the `WRAP`
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-custom[strategy],
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy],
|
||||
and the original instance is wrapped in a Mockito spy. This strategy requires that
|
||||
exactly one candidate bean exists.
|
||||
|
||||
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
|
||||
bean will result in an exception.
|
||||
[TIP]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
|
||||
When using `@MockitoBean` to mock a bean created by a `FactoryBean`, the `FactoryBean`
|
||||
will be replaced with a singleton mock of the type of object created by the `FactoryBean`.
|
||||
|
||||
When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be created
|
||||
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
There are no restrictions on the visibility of `@MockitoBean` and `@MockitoSpyBean`
|
||||
fields.
|
||||
|
||||
Such fields can therefore be `public`, `protected`, package-private (default visibility),
|
||||
or `private` depending on the needs or coding practices of the project.
|
||||
====
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
|
||||
== `@MockitoBean` Examples
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean`
|
||||
annotation.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -50,23 +93,26 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@MockitoBean // <1>
|
||||
private CustomService customService;
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
// test case body...
|
||||
@MockitoBean // <1>
|
||||
CustomService customService;
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Replace the bean with type `CustomService` with a Mockito `mock`.
|
||||
<1> Replace the bean with type `CustomService` with a Mockito mock.
|
||||
======
|
||||
|
||||
In the example above, we are creating a mock for `CustomService`. If more than one bean
|
||||
of that type exists, the bean named `customService` is considered. Otherwise, the test
|
||||
will fail, and you will need to provide a qualifier of some sort to identify which of the
|
||||
`CustomService` beans you want to override. If no such bean exists, a bean definition
|
||||
will be created with an auto-generated bean name.
|
||||
`CustomService` beans you want to override. If no such bean exists, a bean will be
|
||||
created with an auto-generated bean name.
|
||||
|
||||
The following example uses a by-name lookup, rather than a by-type lookup:
|
||||
The following example uses a by-name lookup, rather than a by-type lookup. If no bean
|
||||
named `service` exists, one is created.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -74,20 +120,20 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoBean("service") // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
// tests...
|
||||
|
||||
}
|
||||
----
|
||||
<1> Replace the bean named `service` with a Mockito `mock`.
|
||||
<1> Replace the bean named `service` with a Mockito mock.
|
||||
======
|
||||
|
||||
If no bean definition named `service` exists, one is created.
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoSpyBean` annotation:
|
||||
The following `@SharedMocks` annotation registers two mocks by-type and one mock by-name.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -95,14 +141,73 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@MockitoSpyBean // <1>
|
||||
private CustomService customService;
|
||||
|
||||
// test case body...
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@MockitoBean(types = {OrderService.class, UserService.class}) // <1>
|
||||
@MockitoBean(name = "ps1", types = PrintingService.class) // <2>
|
||||
public @interface SharedMocks {
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
|
||||
<1> Register `OrderService` and `UserService` mocks by-type.
|
||||
<2> Register `PrintingService` mock by-name.
|
||||
======
|
||||
|
||||
The following demonstrates how `@SharedMocks` can be used on a test class.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
@SharedMocks // <1>
|
||||
class BeanOverrideTests {
|
||||
|
||||
@Autowired OrderService orderService; // <2>
|
||||
|
||||
@Autowired UserService userService; // <2>
|
||||
|
||||
@Autowired PrintingService ps1; // <2>
|
||||
|
||||
// Inject other components that rely on the mocks.
|
||||
|
||||
@Test
|
||||
void testThatDependsOnMocks() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Register common mocks via the custom `@SharedMocks` annotation.
|
||||
<2> Optionally inject mocks to _stub_ or _verify_ them.
|
||||
======
|
||||
|
||||
TIP: The mocks can also be injected into `@Configuration` classes or other test-related
|
||||
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
|
||||
APIs.
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitospybean-examples]]
|
||||
== `@MockitoSpyBean` Examples
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoSpyBean`
|
||||
annotation.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoSpyBean // <1>
|
||||
CustomService customService;
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito spy.
|
||||
======
|
||||
|
||||
In the example above, we are wrapping the bean with type `CustomService`. If more than
|
||||
@@ -110,7 +215,7 @@ one bean of that type exists, the bean named `customService` is considered. Othe
|
||||
the test will fail, and you will need to provide a qualifier of some sort to identify
|
||||
which of the `CustomService` beans you want to spy.
|
||||
|
||||
The following example uses a by-name lookup, rather than a by-type lookup:
|
||||
The following example uses a by-name lookup, rather than a by-type lookup.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -118,13 +223,67 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoSpyBean("service") // <1>
|
||||
private CustomService customService;
|
||||
|
||||
// test case body...
|
||||
CustomService customService;
|
||||
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean named `service` with a Mockito `spy`.
|
||||
<1> Wrap the bean named `service` with a Mockito spy.
|
||||
======
|
||||
|
||||
The following `@SharedSpies` annotation registers two spies by-type and one spy by-name.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@MockitoSpyBean(types = {OrderService.class, UserService.class}) // <1>
|
||||
@MockitoSpyBean(name = "ps1", types = PrintingService.class) // <2>
|
||||
public @interface SharedSpies {
|
||||
}
|
||||
----
|
||||
<1> Register `OrderService` and `UserService` spies by-type.
|
||||
<2> Register `PrintingService` spy by-name.
|
||||
======
|
||||
|
||||
The following demonstrates how `@SharedSpies` can be used on a test class.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
@SharedSpies // <1>
|
||||
class BeanOverrideTests {
|
||||
|
||||
@Autowired OrderService orderService; // <2>
|
||||
|
||||
@Autowired UserService userService; // <2>
|
||||
|
||||
@Autowired PrintingService ps1; // <2>
|
||||
|
||||
// Inject other components that rely on the spies.
|
||||
|
||||
@Test
|
||||
void testThatDependsOnMocks() {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Register common spies via the custom `@SharedSpies` annotation.
|
||||
<2> Optionally inject spies to _stub_ or _verify_ them.
|
||||
======
|
||||
|
||||
TIP: The spies can also be injected into `@Configuration` classes or other test-related
|
||||
components in the `ApplicationContext` in order to configure them with Mockito's stubbing
|
||||
APIs.
|
||||
|
||||
+28
-10
@@ -1,7 +1,8 @@
|
||||
[[spring-testing-annotation-beanoverriding-testbean]]
|
||||
= `@TestBean`
|
||||
|
||||
`@TestBean` is used on a field in a test class to override a specific bean in the test's
|
||||
{spring-framework-api}/test/context/bean/override/convention/TestBean.html[`@TestBean`]
|
||||
is used on a non-static field in a test class to override a specific bean in the test's
|
||||
`ApplicationContext` with an instance provided by a factory method.
|
||||
|
||||
The associated factory method name is derived from the annotated field's name, or the
|
||||
@@ -30,6 +31,14 @@ same bean in several tests, make sure to name the field consistently to avoid cr
|
||||
unnecessary contexts.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
There are no restrictions on the visibility of `@TestBean` fields or factory methods.
|
||||
|
||||
Such fields and methods can therefore be `public`, `protected`, package-private (default
|
||||
visibility), or `private` depending on the needs or coding practices of the project.
|
||||
====
|
||||
|
||||
The following example shows how to use the default behavior of the `@TestBean` annotation:
|
||||
|
||||
[tabs]
|
||||
@@ -40,11 +49,11 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@TestBean // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
private static CustomService customService() { // <2>
|
||||
static CustomService customService() { // <2>
|
||||
return new MyFakeCustomService();
|
||||
}
|
||||
}
|
||||
@@ -68,11 +77,11 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@TestBean(name = "service", methodName = "createCustomService") // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
private static CustomService createCustomService() { // <2>
|
||||
static CustomService createCustomService() { // <2>
|
||||
return new MyFakeCustomService();
|
||||
}
|
||||
}
|
||||
@@ -82,15 +91,24 @@ Java::
|
||||
<2> The result of this static method will be used as the instance and injected into the field.
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
[TIP]
|
||||
====
|
||||
Spring searches for the factory method to invoke in the test class, in the test class
|
||||
hierarchy, and in the enclosing class hierarchy for a `@Nested` test class.
|
||||
To locate the factory method to invoke, Spring searches in the class in which the
|
||||
`@TestBean` field is declared, in one of its superclasses, or in any implemented
|
||||
interfaces. If the `@TestBean` field is declared in a `@Nested` test class, the enclosing
|
||||
class hierarchy will also be searched.
|
||||
|
||||
Alternatively, a factory method in an external class can be referenced via its
|
||||
fully-qualified method name following the syntax `<fully-qualified class name>#<method name>`
|
||||
– for example, `methodName = "org.example.TestUtils#createCustomService"`.
|
||||
====
|
||||
|
||||
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
|
||||
bean will result in an exception.
|
||||
[TIP]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
|
||||
When overriding a bean created by a `FactoryBean`, the `FactoryBean` will be replaced
|
||||
with a singleton bean corresponding to the value returned from the `@TestBean` factory
|
||||
method.
|
||||
====
|
||||
|
||||
+19
-6
@@ -2,7 +2,8 @@
|
||||
= Bean Overriding in Tests
|
||||
|
||||
Bean overriding in tests refers to the ability to override specific beans in the
|
||||
`ApplicationContext` for a test class, by annotating one or more fields in the test class.
|
||||
`ApplicationContext` for a test class, by annotating the test class or one or more
|
||||
non-static fields in the test class.
|
||||
|
||||
NOTE: This feature is intended as a less risky alternative to the practice of registering
|
||||
a bean via `@Bean` with the `DefaultListableBeanFactory`
|
||||
@@ -41,14 +42,16 @@ The `spring-test` module registers implementations of the latter two
|
||||
{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`
|
||||
properties file].
|
||||
|
||||
The bean overriding infrastructure searches in test classes for any field meta-annotated
|
||||
with `@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
|
||||
The bean overriding infrastructure searches for annotations on test classes as well as
|
||||
annotations on non-static fields in test classes that are meta-annotated with
|
||||
`@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
|
||||
responsible for creating an appropriate `BeanOverrideHandler`.
|
||||
|
||||
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
|
||||
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
|
||||
defined by the corresponding `BeanOverrideStrategy`:
|
||||
|
||||
[[testcontext-bean-overriding-strategy]]
|
||||
`REPLACE`::
|
||||
Replaces the bean. Throws an exception if a corresponding bean does not exist.
|
||||
`REPLACE_OR_CREATE`::
|
||||
@@ -57,6 +60,19 @@ defined by the corresponding `BeanOverrideStrategy`:
|
||||
`WRAP`::
|
||||
Retrieves the original bean and wraps it.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Only _singleton_ beans can be overridden. Any attempt to override a non-singleton bean
|
||||
will result in an exception.
|
||||
|
||||
When replacing a bean created by a `FactoryBean`, the `FactoryBean` itself will be
|
||||
replaced with a singleton bean corresponding to bean override instance created by the
|
||||
applicable `BeanOverrideHandler`.
|
||||
|
||||
When wrapping a bean created by a `FactoryBean`, the object created by the `FactoryBean`
|
||||
will be wrapped, not the `FactoryBean` itself.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
In contrast to Spring's autowiring mechanism (for example, resolution of an `@Autowired`
|
||||
@@ -71,6 +87,3 @@ Alternatively, the user can directly provide the bean name in the custom annotat
|
||||
`BeanOverrideProcessor` implementations may also internally compute a bean name based on
|
||||
a convention or some other method.
|
||||
====
|
||||
|
||||
NOTE: Only _singleton_ beans can be overridden. Any attempt to override a non-singleton
|
||||
bean will result in an exception.
|
||||
|
||||
@@ -10,6 +10,7 @@ by default, exactly in the following order:
|
||||
annotation for "`before`" modes.
|
||||
* `ApplicationEventsTestExecutionListener`: Provides support for
|
||||
xref:testing/testcontext-framework/application-events.adoc[`ApplicationEvents`].
|
||||
* `BeanOverrideTestExecutionListener`: Provides support for xref:testing/testcontext-framework/bean-overriding.adoc[] .
|
||||
* `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test
|
||||
instance.
|
||||
* `MicrometerObservationRegistryTestExecutionListener`: Provides support for
|
||||
@@ -25,7 +26,6 @@ by default, exactly in the following order:
|
||||
* `EventPublishingTestExecutionListener`: Publishes test execution events to the test's
|
||||
`ApplicationContext` (see xref:testing/testcontext-framework/test-execution-events.adoc[Test Execution Events]).
|
||||
* `MockitoResetTestExecutionListener`: Resets mocks as configured by `@MockitoBean` or `@MockitoSpyBean`.
|
||||
* `BeanOverrideTestExecutionListener`: Provides support for xref:testing/testcontext-framework/bean-overriding.adoc[] .
|
||||
|
||||
[[testcontext-tel-config-registering-tels]]
|
||||
== Registering `TestExecutionListener` Implementations
|
||||
|
||||
@@ -127,7 +127,7 @@ Kotlin::
|
||||
======
|
||||
|
||||
For Spring MVC, use the following where the Spring `ApplicationContext` is passed to
|
||||
{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup]
|
||||
{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup(org.springframework.web.context.WebApplicationContext)[MockMvcBuilders.webAppContextSetup]
|
||||
to create a xref:testing/mockmvc.adoc[MockMvc] instance to handle
|
||||
requests:
|
||||
|
||||
@@ -193,7 +193,7 @@ Kotlin::
|
||||
[[webtestclient-fn-config]]
|
||||
=== Bind to Router Function
|
||||
|
||||
This setup allows you to test <<web-reactive.adoc#webflux-fn, functional endpoints>> via
|
||||
This setup allows you to test xref:web/webflux-functional.adoc[functional endpoints] via
|
||||
mock request and response objects, without a running server.
|
||||
|
||||
For WebFlux, use the following which delegates to `RouterFunctions.toWebHandler` to
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-cors]]
|
||||
= CORS
|
||||
|
||||
[.small]#xref:web/webmvc-cors.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
@@ -364,7 +365,7 @@ Kotlin::
|
||||
|
||||
You can apply CORS support through the built-in
|
||||
{spring-framework-api}/web/cors/reactive/CorsWebFilter.html[`CorsWebFilter`], which is a
|
||||
good fit with <<webflux-fn, functional endpoints>>.
|
||||
good fit with xref:web/webflux-functional.adoc[functional endpoints].
|
||||
|
||||
NOTE: If you try to use the `CorsFilter` with Spring Security, keep in mind that Spring
|
||||
Security has {docs-spring-security}/servlet/integrations/cors.html[built-in support] for
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-fn]]
|
||||
= Functional Endpoints
|
||||
|
||||
[.small]#xref:web/webmvc-functional.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
Spring WebFlux includes WebFlux.fn, a lightweight functional programming model in which functions
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[webflux-websocket]]
|
||||
= WebSockets
|
||||
|
||||
[.small]#xref:web/websocket.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
This part of the reference documentation covers support for reactive-stack WebSocket
|
||||
|
||||
@@ -94,7 +94,8 @@ class WebConfig : WebFluxConfigurer {
|
||||
[.small]#xref:web/webmvc/mvc-config/conversion.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
By default, formatters for various number and date types are installed, along with support
|
||||
for customization via `@NumberFormat`, `@DurationFormat` and `@DateTimeFormat` on fields.
|
||||
for customization via `@NumberFormat`, `@DurationFormat`, and `@DateTimeFormat` on fields
|
||||
and parameters.
|
||||
|
||||
To register custom formatters and converters in Java config, use the following:
|
||||
|
||||
@@ -349,7 +350,6 @@ which customizes Jackson's default properties with the following ones:
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {jackson-github-org}/jackson-datatype-joda[`jackson-datatype-joda`]: Support for Joda-Time types.
|
||||
* {jackson-github-org}/jackson-datatype-jsr310[`jackson-datatype-jsr310`]: Support for Java 8 Date and Time API types.
|
||||
* {jackson-github-org}/jackson-datatype-jdk8[`jackson-datatype-jdk8`]: Support for other Java 8 types, such as `Optional`.
|
||||
* {jackson-github-org}/jackson-module-kotlin[`jackson-module-kotlin`]: Support for Kotlin classes and data classes.
|
||||
|
||||
@@ -605,9 +605,4 @@ For method parameters and returns values, generally, `@HttpExchange` supports a
|
||||
subset of the method parameters that `@RequestMapping` does. Notably, it excludes any
|
||||
server-side specific parameter types. For details, see the list for
|
||||
xref:integration/rest-clients.adoc#rest-http-interface-method-parameters[@HttpExchange] and
|
||||
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
|
||||
`@HttpExchange` also supports a `headers()` parameter which accepts `"name=value"`-like
|
||||
pairs like in `@RequestMapping(headers={})` on the client side. On the server side,
|
||||
this extends to the full syntax that
|
||||
xref:#webflux-ann-requestmapping-params-and-headers[`@RequestMapping`] supports.
|
||||
xref:web/webflux/controller/ann-methods/arguments.adoc[@RequestMapping].
|
||||
@@ -101,7 +101,7 @@ On that foundation, Spring WebFlux provides a choice of two programming models:
|
||||
from the `spring-web` module. Both Spring MVC and WebFlux controllers support reactive
|
||||
(Reactor and RxJava) return types, and, as a result, it is not easy to tell them apart. One notable
|
||||
difference is that WebFlux also supports reactive `@RequestBody` arguments.
|
||||
* <<webflux-fn>>: Lambda-based, lightweight, and functional programming model. You can think of
|
||||
* xref:web/webflux-functional.adoc[Functional Endpoints]: Lambda-based, lightweight, and functional programming model. You can think of
|
||||
this as a small library or a set of utilities that an application can use to route and
|
||||
handle requests. The big difference with annotated controllers is that the application
|
||||
is in charge of request handling from start to finish versus declaring intent through
|
||||
|
||||
@@ -419,6 +419,24 @@ controllers. However, when you use it with Spring Security, we advise relying on
|
||||
|
||||
See the section on xref:web/webflux-cors.adoc[CORS] and the xref:web/webflux-cors.adoc#webflux-cors-webfilter[CORS `WebFilter`] for more details.
|
||||
|
||||
[[filters.url-handler]]
|
||||
=== URL Handler
|
||||
[.small]#xref:web/webmvc/filters.adoc#filters.url-handler[See equivalent in the Servlet stack]#
|
||||
|
||||
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
|
||||
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
|
||||
|
||||
Adding trailing slash variants to all mapping declarations is not the best way to handle this use case.
|
||||
The `UrlHandlerFilter` web filter has been designed for this purpose. It can be configured to:
|
||||
|
||||
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
|
||||
* mutate the request to act as if the request was sent without a trailing slash and continue the processing of the request.
|
||||
|
||||
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
|
||||
|
||||
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
|
||||
|
||||
|
||||
|
||||
[[webflux-exception-handler]]
|
||||
== Exceptions
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
[[mvc-cors]]
|
||||
= CORS
|
||||
|
||||
[.small]#xref:web/webflux-cors.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring MVC lets you handle CORS (Cross-Origin Resource Sharing). This section
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[webmvc-fn]]
|
||||
= Functional Endpoints
|
||||
[.small]#<<web-reactive.adoc#webflux-fn, See equivalent in the Reactive stack>>#
|
||||
|
||||
[.small]#xref:web/webflux-functional.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring Web MVC includes WebMvc.fn, a lightweight functional programming model in which functions
|
||||
are used to route and handle requests and contracts are designed for immutability.
|
||||
|
||||
@@ -9,7 +9,11 @@ The `spring-web` module provides some useful filters:
|
||||
* xref:web/webmvc/filters.adoc#filters-forwarded-headers[Forwarded Headers]
|
||||
* xref:web/webmvc/filters.adoc#filters-shallow-etag[Shallow ETag]
|
||||
* xref:web/webmvc/filters.adoc#filters-cors[CORS]
|
||||
* xref:web/webmvc/filters.adoc#filters.url-handler[URL Handler]
|
||||
|
||||
Servlet filters can be configured in the `web.xml` configuration file or using Servlet annotations.
|
||||
If you are using Spring Boot, you can
|
||||
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare them as beans and configure them as part of your application].
|
||||
|
||||
|
||||
[[filters-http-put]]
|
||||
@@ -26,7 +30,7 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method
|
||||
|
||||
|
||||
|
||||
[[forwarded-headers]]
|
||||
[[filters-forwarded-headers]]
|
||||
== Forwarded Headers
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]#
|
||||
|
||||
@@ -109,4 +113,22 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
|
||||
|
||||
|
||||
|
||||
[[filters.url-handler]]
|
||||
== 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 could be done by enabling the `setUseTrailingSlashMatch`
|
||||
option on the `PathMatchConfigurer`. This means that sending a "GET /home/" request would be handled by a controller
|
||||
method annotated with `@GetMapping("/home")`.
|
||||
|
||||
This option has been retired, but applications are still expected to handle such requests in a safe way.
|
||||
The `UrlHandlerFilter` Servlet filter has been designed for this purpose. It can be configured to:
|
||||
|
||||
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
|
||||
* wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request.
|
||||
|
||||
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
|
||||
|
||||
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@
|
||||
[.small]#xref:web/webflux/config.adoc#webflux-config-conversion[See equivalent in the Reactive stack]#
|
||||
|
||||
By default, formatters for various number and date types are installed, along with support
|
||||
for customization via `@NumberFormat`, `@DurationFormat` and `@DateTimeFormat` on fields.
|
||||
for customization via `@NumberFormat`, `@DurationFormat`, and `@DateTimeFormat` on fields
|
||||
and parameters.
|
||||
|
||||
To register custom formatters and converters, use the following:
|
||||
|
||||
|
||||
@@ -35,7 +35,6 @@ This builder customizes Jackson's default properties as follows:
|
||||
|
||||
It also automatically registers the following well-known modules if they are detected on the classpath:
|
||||
|
||||
* {jackson-github-org}/jackson-datatype-joda[jackson-datatype-joda]: Support for Joda-Time types.
|
||||
* {jackson-github-org}/jackson-datatype-jsr310[jackson-datatype-jsr310]: Support for Java 8 Date and Time API types.
|
||||
* {jackson-github-org}/jackson-datatype-jdk8[jackson-datatype-jdk8]: Support for other Java 8 types, such as `Optional`.
|
||||
* {jackson-github-org}/jackson-module-kotlin[jackson-module-kotlin]: Support for Kotlin classes and data classes.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[websocket]]
|
||||
= WebSockets
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webflux-websocket.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
This part of the reference documentation covers support for Servlet stack, WebSocket
|
||||
|
||||
@@ -19,6 +19,6 @@ from where they are handled according to their destination prefix. As the channe
|
||||
a `ThreadPoolExecutor`, messages are processed in different threads, and the resulting sequence
|
||||
of handling may not match the exact order in which they were received.
|
||||
|
||||
To enable ordered publishing, set the `setPreserveReceiveOrder` flag as follows:
|
||||
To enable ordered receiving, set the `setPreserveReceiveOrder` flag as follows:
|
||||
|
||||
include-code::./ReceiveOrderWebSocketConfiguration[tag=snippet,indent=0]
|
||||
|
||||
@@ -45,7 +45,7 @@ input is ignored. This is in contrast to property binding which by default binds
|
||||
request parameter for which there is a matching property.
|
||||
|
||||
If neither a dedicated model object nor constructor binding is sufficient, and you must
|
||||
use property binding, we strongy recommend registering `allowedFields` patterns (case
|
||||
use property binding, we strongly recommend registering `allowedFields` patterns (case
|
||||
sensitive) on `WebDataBinder` in order to prevent unexpected properties from being set.
|
||||
For example:
|
||||
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.integration.resthttpinterface.customresolver;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.web.client.RestClient;
|
||||
import org.springframework.web.client.support.RestClientAdapter;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
import org.springframework.web.service.invoker.HttpRequestValues;
|
||||
import org.springframework.web.service.invoker.HttpServiceArgumentResolver;
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||
|
||||
public class CustomHttpServiceArgumentResolver {
|
||||
|
||||
// tag::httpinterface[]
|
||||
interface RepositoryService {
|
||||
|
||||
@GetExchange("/repos/search")
|
||||
List<Repository> searchRepository(Search search);
|
||||
|
||||
}
|
||||
// end::httpinterface[]
|
||||
|
||||
class Sample {
|
||||
|
||||
void sample() {
|
||||
// tag::usage[]
|
||||
RestClient restClient = RestClient.builder().baseUrl("https://api.github.com/").build();
|
||||
RestClientAdapter adapter = RestClientAdapter.create(restClient);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory
|
||||
.builderFor(adapter)
|
||||
.customArgumentResolver(new SearchQueryArgumentResolver())
|
||||
.build();
|
||||
RepositoryService repositoryService = factory.createClient(RepositoryService.class);
|
||||
|
||||
Search search = Search.create()
|
||||
.owner("spring-projects")
|
||||
.language("java")
|
||||
.query("rest")
|
||||
.build();
|
||||
List<Repository> repositories = repositoryService.searchRepository(search);
|
||||
// end::usage[]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// tag::argumentresolver[]
|
||||
static class SearchQueryArgumentResolver implements HttpServiceArgumentResolver {
|
||||
@Override
|
||||
public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
|
||||
if (parameter.getParameterType().equals(Search.class)) {
|
||||
Search search = (Search) argument;
|
||||
requestValues.addRequestParameter("owner", search.owner());
|
||||
requestValues.addRequestParameter("language", search.language());
|
||||
requestValues.addRequestParameter("query", search.query());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// end::argumentresolver[]
|
||||
|
||||
|
||||
record Search (String query, String owner, String language) {
|
||||
|
||||
static Builder create() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
static class Builder {
|
||||
|
||||
Builder query(String query) { return this;}
|
||||
|
||||
Builder owner(String owner) { return this;}
|
||||
|
||||
Builder language(String language) { return this;}
|
||||
|
||||
Search build() {
|
||||
return new Search(null, null, null);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record Repository(String name) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.filters.urlhandler;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.filter.reactive.UrlHandlerFilter;
|
||||
|
||||
public class UrlHandlerFilterConfiguration {
|
||||
|
||||
public void configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will mutate the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").mutateRequest()
|
||||
.build();
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.filters.urlhandler;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.filter.UrlHandlerFilter;
|
||||
|
||||
public class UrlHandlerFilterConfiguration {
|
||||
|
||||
public void configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
UrlHandlerFilter urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will wrap the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").wrapRequest()
|
||||
.build();
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.integration.resthttpinterface.customresolver
|
||||
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.web.client.RestClient
|
||||
import org.springframework.web.client.support.RestClientAdapter
|
||||
import org.springframework.web.service.annotation.GetExchange
|
||||
import org.springframework.web.service.invoker.HttpRequestValues
|
||||
import org.springframework.web.service.invoker.HttpServiceArgumentResolver
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory
|
||||
|
||||
class CustomHttpServiceArgumentResolver {
|
||||
|
||||
// tag::httpinterface[]
|
||||
interface RepositoryService {
|
||||
|
||||
@GetExchange("/repos/search")
|
||||
fun searchRepository(search: Search): List<Repository>
|
||||
|
||||
}
|
||||
// end::httpinterface[]
|
||||
|
||||
class Sample {
|
||||
fun sample() {
|
||||
// tag::usage[]
|
||||
val restClient = RestClient.builder().baseUrl("https://api.github.com/").build()
|
||||
val adapter = RestClientAdapter.create(restClient)
|
||||
val factory = HttpServiceProxyFactory
|
||||
.builderFor(adapter)
|
||||
.customArgumentResolver(SearchQueryArgumentResolver())
|
||||
.build()
|
||||
val repositoryService = factory.createClient<RepositoryService>(RepositoryService::class.java)
|
||||
|
||||
val search = Search(owner = "spring-projects", language = "java", query = "rest")
|
||||
val repositories = repositoryService.searchRepository(search)
|
||||
// end::usage[]
|
||||
repositories.size
|
||||
}
|
||||
}
|
||||
|
||||
// tag::argumentresolver[]
|
||||
class SearchQueryArgumentResolver : HttpServiceArgumentResolver {
|
||||
override fun resolve(
|
||||
argument: Any?,
|
||||
parameter: MethodParameter,
|
||||
requestValues: HttpRequestValues.Builder
|
||||
): Boolean {
|
||||
if (parameter.getParameterType() == Search::class.java) {
|
||||
val search = argument as Search
|
||||
requestValues.addRequestParameter("owner", search.owner)
|
||||
.addRequestParameter("language", search.language)
|
||||
.addRequestParameter("query", search.query)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
// end::argumentresolver[]
|
||||
|
||||
data class Search(val query: String, val owner: String, val language: String)
|
||||
|
||||
data class Repository(val name: String)
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webflux.filters.urlhandler
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.filter.reactive.UrlHandlerFilter
|
||||
|
||||
class UrlHandlerFilterConfiguration {
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
fun configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
val urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will mutate the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").mutateRequest()
|
||||
.build()
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.docs.web.webmvc.filters.urlhandler
|
||||
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.web.filter.UrlHandlerFilter
|
||||
|
||||
class UrlHandlerFilterConfiguration {
|
||||
|
||||
@Suppress("UNUSED_VARIABLE")
|
||||
fun configureUrlHandlerFilter() {
|
||||
// tag::config[]
|
||||
val urlHandlerFilter = UrlHandlerFilter
|
||||
// will HTTP 308 redirect "/blog/my-blog-post/" -> "/blog/my-blog-post"
|
||||
.trailingSlashHandler("/blog/**").redirect(HttpStatus.PERMANENT_REDIRECT)
|
||||
// will wrap the request to "/admin/user/account/" and make it as "/admin/user/account"
|
||||
.trailingSlashHandler("/admin/**").wrapRequest()
|
||||
.build()
|
||||
// end::config[]
|
||||
}
|
||||
}
|
||||
@@ -7,32 +7,32 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.0"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.0-RC1"))
|
||||
api(platform("io.netty:netty-bom:4.1.113.Final"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.4"))
|
||||
api(platform("io.netty:netty-bom:4.1.118.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.0-RC1"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.3"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.23"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.3"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.26.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.13"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.13"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.16"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.16"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
|
||||
api(platform("org.junit:junit-bom:5.11.3"))
|
||||
api(platform("org.mockito:mockito-bom:5.14.1"))
|
||||
api(platform("org.junit:junit-bom:5.11.4"))
|
||||
api(platform("org.mockito:mockito-bom:5.15.2"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.6.2")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||
api("com.github.librepdf:openpdf:1.3.43")
|
||||
api("com.google.code.findbugs:findbugs:3.0.1")
|
||||
api("com.google.code.findbugs:jsr305:3.0.2")
|
||||
api("com.google.code.gson:gson:2.10.1")
|
||||
api("com.google.protobuf:protobuf-java-util:4.27.1")
|
||||
api("com.h2database:h2:2.2.224")
|
||||
api("com.google.code.gson:gson:2.11.0")
|
||||
api("com.google.protobuf:protobuf-java-util:4.29.3")
|
||||
api("com.h2database:h2:2.3.232")
|
||||
api("com.jayway.jsonpath:json-path:2.9.0")
|
||||
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
|
||||
api("com.rometools:rome:1.19.0")
|
||||
@@ -44,7 +44,7 @@ dependencies {
|
||||
api("com.sun.xml.bind:jaxb-impl:3.0.2")
|
||||
api("com.sun.xml.bind:jaxb-xjc:3.0.2")
|
||||
api("com.thoughtworks.qdox:qdox:2.1.0")
|
||||
api("com.thoughtworks.xstream:xstream:1.4.20")
|
||||
api("com.thoughtworks.xstream:xstream:1.4.21")
|
||||
api("commons-io:commons-io:2.15.0")
|
||||
api("de.bechte.junit:junit-hierarchicalcontextrunner:4.12.2")
|
||||
api("io.micrometer:context-propagation:1.1.1")
|
||||
@@ -54,11 +54,11 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.8")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.10")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.17.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.17.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.17.Final")
|
||||
api("io.undertow:undertow-core:2.3.18.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.18.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
|
||||
api("io.vavr:vavr:0.10.4")
|
||||
api("jakarta.activation:jakarta.activation-api:2.0.1")
|
||||
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
|
||||
@@ -100,8 +100,8 @@ dependencies {
|
||||
api("org.apache.derby:derby:10.16.1.1")
|
||||
api("org.apache.derby:derbyclient:10.16.1.1")
|
||||
api("org.apache.derby:derbytools:10.16.1.1")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4.1")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.1")
|
||||
api("org.apache.poi:poi-ooxml:5.2.5")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
|
||||
@@ -116,7 +116,7 @@ dependencies {
|
||||
api("org.crac:crac:1.4.0")
|
||||
api("org.dom4j:dom4j:2.1.4")
|
||||
api("org.easymock:easymock:5.4.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.7")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.8")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4")
|
||||
api("org.eclipse:yasson:2.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
@@ -129,25 +129,25 @@ dependencies {
|
||||
api("org.hamcrest:hamcrest:2.2")
|
||||
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
|
||||
api("org.hibernate:hibernate-validator:7.0.5.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.2")
|
||||
api("org.htmlunit:htmlunit:4.2.0")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.6.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jruby:jruby:9.4.8.0")
|
||||
api("org.jruby:jruby:9.4.9.0")
|
||||
api("org.junit.support:testng-engine:1.0.5")
|
||||
api("org.mozilla:rhino:1.7.15")
|
||||
api("org.ogce:xpp3:1.1.6")
|
||||
api("org.python:jython-standalone:2.7.3")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.20.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.20.0")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.26.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.26.0")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.16")
|
||||
api("org.testng:testng:7.9.0")
|
||||
api("org.testng:testng:7.11.0")
|
||||
api("org.webjars:underscorejs:1.8.3")
|
||||
api("org.webjars:webjars-locator-core:0.55")
|
||||
api("org.webjars:webjars-locator-lite:1.0.0")
|
||||
api("org.xmlunit:xmlunit-assertj:2.9.1")
|
||||
api("org.xmlunit:xmlunit-matchers:2.9.1")
|
||||
api("org.yaml:snakeyaml:2.2")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.0")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.0")
|
||||
api("org.yaml:snakeyaml:2.3")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.2.0-SNAPSHOT
|
||||
version=6.2.3
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -55,8 +55,9 @@ plugins.withType(JavaPlugin).configureEach {
|
||||
languageVersion = testLanguageVersion
|
||||
}
|
||||
// Enable Java experimental support in Bytebuddy
|
||||
// Remove when JDK 22 is supported by Mockito
|
||||
if (testLanguageVersion == JavaLanguageVersion.of(22)) {
|
||||
// Bytebuddy 1.15.4 supports JDK <= 24
|
||||
// see https://github.com/raphw/byte-buddy/blob/master/release-notes.md
|
||||
if (testLanguageVersion.compareTo(JavaLanguageVersion.of(24)) > 0 ) {
|
||||
jvmArgs("-Dnet.bytebuddy.experimental=true")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
||||
+59
-59
@@ -56,33 +56,33 @@ import kotlin.annotation.AnnotationTarget.TYPE
|
||||
@SpringJUnitConfig(InterceptorConfig::class)
|
||||
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
|
||||
class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
@Autowired val echo: Echo,
|
||||
@Autowired val firstAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val countingAspect: CountingAspect,
|
||||
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
|
||||
@Autowired val echo: Echo,
|
||||
@Autowired val firstAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val secondAdvisor: TestPointcutAdvisor,
|
||||
@Autowired val countingAspect: CountingAspect,
|
||||
@Autowired val reactiveTransactionManager: ReactiveCallCountingTransactionManager) {
|
||||
|
||||
@Test
|
||||
fun `Multiple interceptors with regular function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
assertThat(echo.echo(value)).isEqualTo(value)
|
||||
@Test
|
||||
fun `Multiple interceptors with regular function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
assertThat(echo.echo(value)).isEqualTo(value)
|
||||
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
|
||||
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { String::class.java.isAssignableFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Multiple interceptors with suspending function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
runBlocking {
|
||||
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
|
||||
}
|
||||
@Test
|
||||
fun `Multiple interceptors with suspending function`() {
|
||||
assertThat(firstAdvisor.interceptor.invocations).isEmpty()
|
||||
assertThat(secondAdvisor.interceptor.invocations).isEmpty()
|
||||
val value = "Hello!"
|
||||
runBlocking {
|
||||
assertThat(echo.suspendingEcho(value)).isEqualTo(value)
|
||||
}
|
||||
assertThat(firstAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
|
||||
assertThat(secondAdvisor.interceptor.invocations).singleElement().matches { Mono::class.java.isAssignableFrom(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test // gh-33095
|
||||
fun `Aspect and reactive transactional with suspending function`() {
|
||||
@@ -113,17 +113,17 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
assertThat(countingAspect.counter).`as`("aspect applied once per key").isEqualTo(2)
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
@EnableTransactionManagement
|
||||
@Configuration
|
||||
@EnableAspectJAutoProxy
|
||||
@EnableTransactionManagement
|
||||
@EnableCaching
|
||||
open class InterceptorConfig {
|
||||
open class InterceptorConfig {
|
||||
|
||||
@Bean
|
||||
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
|
||||
@Bean
|
||||
open fun firstAdvisor() = TestPointcutAdvisor().apply { order = 0 }
|
||||
|
||||
@Bean
|
||||
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
|
||||
@Bean
|
||||
open fun secondAdvisor() = TestPointcutAdvisor().apply { order = 1 }
|
||||
|
||||
@Bean
|
||||
open fun countingAspect() = CountingAspect()
|
||||
@@ -138,34 +138,34 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
return ConcurrentMapCacheManager()
|
||||
}
|
||||
|
||||
@Bean
|
||||
open fun echo(): Echo {
|
||||
return Echo()
|
||||
}
|
||||
}
|
||||
@Bean
|
||||
open fun echo(): Echo {
|
||||
return Echo()
|
||||
}
|
||||
}
|
||||
|
||||
class TestMethodInterceptor: MethodInterceptor {
|
||||
class TestMethodInterceptor: MethodInterceptor {
|
||||
|
||||
var invocations: MutableList<Class<*>> = mutableListOf()
|
||||
var invocations: MutableList<Class<*>> = mutableListOf()
|
||||
|
||||
@Suppress("RedundantNullableReturnType")
|
||||
override fun invoke(invocation: MethodInvocation): Any? {
|
||||
val result = invocation.proceed()
|
||||
invocations.add(result!!.javaClass)
|
||||
return result
|
||||
}
|
||||
@Suppress("RedundantNullableReturnType")
|
||||
override fun invoke(invocation: MethodInvocation): Any? {
|
||||
val result = invocation.proceed()
|
||||
invocations.add(result!!.javaClass)
|
||||
return result
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
|
||||
class TestPointcutAdvisor : StaticMethodMatcherPointcutAdvisor(TestMethodInterceptor()) {
|
||||
|
||||
val interceptor: TestMethodInterceptor
|
||||
get() = advice as TestMethodInterceptor
|
||||
val interceptor: TestMethodInterceptor
|
||||
get() = advice as TestMethodInterceptor
|
||||
|
||||
override fun matches(method: Method, targetClass: Class<*>): Boolean {
|
||||
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
|
||||
}
|
||||
}
|
||||
override fun matches(method: Method, targetClass: Class<*>): Boolean {
|
||||
return targetClass == Echo::class.java && method.name.lowercase().endsWith("echo")
|
||||
}
|
||||
}
|
||||
|
||||
@Target(CLASS, FUNCTION, ANNOTATION_CLASS, TYPE)
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@@ -185,16 +185,16 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
}
|
||||
}
|
||||
|
||||
open class Echo {
|
||||
open class Echo {
|
||||
|
||||
open fun echo(value: String): String {
|
||||
return value
|
||||
}
|
||||
open fun echo(value: String): String {
|
||||
return value
|
||||
}
|
||||
|
||||
open suspend fun suspendingEcho(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
open suspend fun suspendingEcho(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@Counting
|
||||
@@ -212,6 +212,6 @@ class AspectJAutoProxyInterceptorKotlinIntegrationTests(
|
||||
return "$value ${cacheCounter++}"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "com.gradle.develocity" version "3.17.2"
|
||||
id "com.gradle.develocity" version "3.19"
|
||||
id "io.spring.ge.conventions" version "0.0.17"
|
||||
id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2008 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,11 +27,11 @@ package org.springframework.aop;
|
||||
* <p>Some examples of valid methods would be:
|
||||
*
|
||||
* <pre class="code">public void afterThrowing(Exception ex)</pre>
|
||||
* <pre class="code">public void afterThrowing(RemoteException)</pre>
|
||||
* <pre class="code">public void afterThrowing(RemoteException ex)</pre>
|
||||
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, Exception ex)</pre>
|
||||
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)</pre>
|
||||
*
|
||||
* The first three arguments are optional, and only useful if we want further
|
||||
* <p>The first three arguments are optional, and only useful if we want further
|
||||
* information about the joinpoint, as in AspectJ <b>after-throwing</b> advice.
|
||||
*
|
||||
* <p><b>Note:</b> If a throws-advice method throws an exception itself, it will
|
||||
|
||||
+13
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -276,14 +276,18 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
|
||||
}
|
||||
if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) {
|
||||
// May need to add implicit join point arg name...
|
||||
Class<?> firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0];
|
||||
if (firstArgType == JoinPoint.class ||
|
||||
firstArgType == ProceedingJoinPoint.class ||
|
||||
firstArgType == JoinPoint.StaticPart.class) {
|
||||
String[] oldNames = this.argumentNames;
|
||||
this.argumentNames = new String[oldNames.length + 1];
|
||||
this.argumentNames[0] = "THIS_JOIN_POINT";
|
||||
System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length);
|
||||
for (int i = 0; i < this.aspectJAdviceMethod.getParameterCount(); i++) {
|
||||
Class<?> argType = this.aspectJAdviceMethod.getParameterTypes()[i];
|
||||
if (argType == JoinPoint.class ||
|
||||
argType == ProceedingJoinPoint.class ||
|
||||
argType == JoinPoint.StaticPart.class) {
|
||||
String[] oldNames = this.argumentNames;
|
||||
this.argumentNames = new String[oldNames.length + 1];
|
||||
System.arraycopy(oldNames, 0, this.argumentNames, 0, i);
|
||||
this.argumentNames[i] = "THIS_JOIN_POINT";
|
||||
System.arraycopy(oldNames, i, this.argumentNames, i + 1, oldNames.length - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-1
@@ -18,6 +18,7 @@ package org.springframework.aop.aspectj.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
import java.util.StringTokenizer;
|
||||
@@ -37,6 +38,7 @@ import org.aspectj.lang.reflect.PerClauseKind;
|
||||
|
||||
import org.springframework.aop.framework.AopConfigException;
|
||||
import org.springframework.core.ParameterNameDiscoverer;
|
||||
import org.springframework.core.SpringProperties;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
@@ -58,6 +60,23 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[] {
|
||||
Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};
|
||||
|
||||
private static final String AJC_MAGIC = "ajc$";
|
||||
|
||||
/**
|
||||
* System property that instructs Spring to ignore ajc-compiled aspects
|
||||
* for Spring AOP proxying, restoring traditional Spring behavior for
|
||||
* scenarios where both weaving and AspectJ auto-proxying are enabled.
|
||||
* <p>The default is "false". Consider switching this to "true" if you
|
||||
* encounter double execution of your aspects in a given build setup.
|
||||
* Note that we recommend restructuring your AspectJ configuration to
|
||||
* avoid such double exposure of an AspectJ aspect to begin with.
|
||||
* @since 6.1.15
|
||||
*/
|
||||
public static final String IGNORE_AJC_PROPERTY_NAME = "spring.aop.ajc.ignore";
|
||||
|
||||
private static final boolean shouldIgnoreAjcCompiledAspects =
|
||||
SpringProperties.getFlag(IGNORE_AJC_PROPERTY_NAME);
|
||||
|
||||
|
||||
/** Logger available to subclasses. */
|
||||
protected final Log logger = LogFactory.getLog(getClass());
|
||||
@@ -67,7 +86,8 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
|
||||
@Override
|
||||
public boolean isAspect(Class<?> clazz) {
|
||||
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
|
||||
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null &&
|
||||
(!shouldIgnoreAjcCompiledAspects || !compiledByAjc(clazz)));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -114,6 +134,15 @@ public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFac
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean compiledByAjc(Class<?> clazz) {
|
||||
for (Field field : clazz.getDeclaredFields()) {
|
||||
if (field.getName().startsWith(AJC_MAGIC)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Enum for AspectJ annotation types.
|
||||
|
||||
@@ -668,7 +668,8 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof MethodCacheKey that && this.method == that.method));
|
||||
return (this == other || (other instanceof MethodCacheKey that &&
|
||||
(this.method == that.method || this.method.equals(that.method))));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-2
@@ -60,11 +60,12 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
|
||||
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
|
||||
if (config.isOptimize() || config.isProxyTargetClass() || !config.hasUserSuppliedInterfaces()) {
|
||||
Class<?> targetClass = config.getTargetClass();
|
||||
if (targetClass == null) {
|
||||
if (targetClass == null && config.getProxiedInterfaces().length == 0) {
|
||||
throw new AopConfigException("TargetSource cannot determine target class: " +
|
||||
"Either an interface or a target is required for proxy creation.");
|
||||
}
|
||||
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
|
||||
if (targetClass == null || targetClass.isInterface() ||
|
||||
Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
|
||||
return new JdkDynamicAopProxy(config);
|
||||
}
|
||||
return new ObjenesisCglibAopProxy(config);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -281,8 +281,8 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware {
|
||||
* @param returnType the declared return type (potentially a {@link Future} variant)
|
||||
* @return the execution result (potentially a corresponding {@link Future} handle)
|
||||
*/
|
||||
@SuppressWarnings("removal")
|
||||
@Nullable
|
||||
@SuppressWarnings("deprecation")
|
||||
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
|
||||
if (CompletableFuture.class.isAssignableFrom(returnType)) {
|
||||
return executor.submitCompletable(task);
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -125,7 +125,7 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport imple
|
||||
return null;
|
||||
};
|
||||
|
||||
return doSubmit(task, executor, invocation.getMethod().getReturnType());
|
||||
return doSubmit(task, executor, userMethod.getReturnType());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.aop.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractAspectJAdvice}.
|
||||
*
|
||||
* @author Joshua Chen
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class AbstractAspectJAdviceTests {
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withoutJoinPointParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithNoJoinPoint");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsFirstParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsFirstParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsLastParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsLastParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2", "THIS_JOIN_POINT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsMiddleParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsMiddleParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "THIS_JOIN_POINT", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withProceedingJoinPoint() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithProceedingJoinPoint");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withStaticPart() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithStaticPart");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
private Consumer<AbstractAspectJAdvice> hasArgumentNames(String... argumentNames) {
|
||||
return advice -> assertThat(advice).extracting("argumentNames")
|
||||
.asInstanceOf(InstanceOfAssertFactories.array(String[].class))
|
||||
.containsExactly(argumentNames);
|
||||
}
|
||||
|
||||
private AbstractAspectJAdvice getAspectJAdvice(final String methodName) {
|
||||
AbstractAspectJAdvice advice = new TestAspectJAdvice(getMethod(methodName),
|
||||
mock(AspectJExpressionPointcut.class), mock(AspectInstanceFactory.class));
|
||||
advice.setArgumentNamesFromStringArray("arg1", "arg2");
|
||||
return advice;
|
||||
}
|
||||
|
||||
private Method getMethod(final String methodName) {
|
||||
return Arrays.stream(Sample.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(methodName)).findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class TestAspectJAdvice extends AbstractAspectJAdvice {
|
||||
|
||||
public TestAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut,
|
||||
AspectInstanceFactory aspectInstanceFactory) {
|
||||
super(aspectJAdviceMethod, pointcut, aspectInstanceFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBeforeAdvice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAfterAdvice() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class Sample {
|
||||
|
||||
void methodWithNoJoinPoint(String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsFirstParameter(JoinPoint joinPoint, String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsLastParameter(String arg1, String arg2, JoinPoint joinPoint) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsMiddleParameter(String arg1, JoinPoint joinPoint, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithProceedingJoinPoint(ProceedingJoinPoint joinPoint, String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithStaticPart(JoinPoint.StaticPart staticPart, String arg1, String arg2) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+30
-52
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,30 +16,32 @@
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.assertj.core.api.WithAssertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.mockito.BDDMockito.doAnswer;
|
||||
import static org.mockito.BDDMockito.doThrow;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowable;
|
||||
import static org.mockito.BDDMockito.willAnswer;
|
||||
import static org.mockito.BDDMockito.willThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Mikaël Francoeur
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
* @see JdkProxyExceptionHandlingTests
|
||||
* @see CglibProxyExceptionHandlingTests
|
||||
*/
|
||||
abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
abstract class AbstractProxyExceptionHandlingTests {
|
||||
|
||||
private static final RuntimeException uncheckedException = new RuntimeException();
|
||||
|
||||
@@ -47,14 +49,12 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
private static final UndeclaredCheckedException undeclaredCheckedException = new UndeclaredCheckedException();
|
||||
|
||||
protected final MyClass target = mock(MyClass.class);
|
||||
protected final MyClass target = mock();
|
||||
|
||||
protected final ProxyFactory proxyFactory = new ProxyFactory(target);
|
||||
|
||||
@Nullable
|
||||
protected MyInterface proxy;
|
||||
|
||||
@Nullable
|
||||
private Throwable throwableSeenByCaller;
|
||||
|
||||
|
||||
@@ -63,46 +63,24 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
Mockito.clearInvocations(target);
|
||||
}
|
||||
|
||||
protected void assertProxyType(Object proxy) {
|
||||
}
|
||||
|
||||
protected abstract void assertProxyType(Object proxy);
|
||||
|
||||
|
||||
private void invokeProxy() {
|
||||
throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething());
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
private Answer<?> sneakyThrow(Throwable throwable) {
|
||||
private static Answer<?> sneakyThrow(Throwable throwable) {
|
||||
return invocation -> {
|
||||
throw throwable;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
static class JdkAopProxyTests extends ProxyExceptionHandlingTests {
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Proxy.isProxyClass(proxy.getClass())).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class CglibAopProxyTests extends ProxyExceptionHandlingTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
proxyFactory.setProxyTargetClass(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Enhancer.isEnhanced(proxy.getClass())).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class WhenThereIsOneInterceptor {
|
||||
class WhenThereIsOneInterceptorTests {
|
||||
|
||||
@Nullable
|
||||
private Throwable throwableSeenByInterceptor;
|
||||
@@ -110,23 +88,23 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
proxyFactory.addAdvice(captureThrowable());
|
||||
proxy = (MyInterface) proxyFactory.getProxy(ProxyExceptionHandlingTests.class.getClassLoader());
|
||||
proxy = (MyInterface) proxyFactory.getProxy(getClass().getClassLoader());
|
||||
assertProxyType(proxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doAnswer(sneakyThrow(undeclaredCheckedException)).when(target).doSomething();
|
||||
willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByInterceptor).isSameAs(undeclaredCheckedException);
|
||||
assertThat(throwableSeenByCaller)
|
||||
.isInstanceOf(UndeclaredThrowableException.class)
|
||||
.hasCauseReference(undeclaredCheckedException);
|
||||
.cause().isSameAs(undeclaredCheckedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doThrow(declaredCheckedException).when(target).doSomething();
|
||||
willThrow(declaredCheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByInterceptor).isSameAs(declaredCheckedException);
|
||||
assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException);
|
||||
@@ -134,7 +112,7 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
@Test
|
||||
void targetThrowsUncheckedException() throws DeclaredCheckedException {
|
||||
doThrow(uncheckedException).when(target).doSomething();
|
||||
willThrow(uncheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByInterceptor).isSameAs(uncheckedException);
|
||||
assertThat(throwableSeenByCaller).isSameAs(uncheckedException);
|
||||
@@ -155,40 +133,40 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
|
||||
|
||||
@Nested
|
||||
class WhenThereAreNoInterceptors {
|
||||
class WhenThereAreNoInterceptorsTests {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
proxy = (MyInterface) proxyFactory.getProxy(ProxyExceptionHandlingTests.class.getClassLoader());
|
||||
proxy = (MyInterface) proxyFactory.getProxy(getClass().getClassLoader());
|
||||
assertProxyType(proxy);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doAnswer(sneakyThrow(undeclaredCheckedException)).when(target).doSomething();
|
||||
willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByCaller)
|
||||
.isInstanceOf(UndeclaredThrowableException.class)
|
||||
.hasCauseReference(undeclaredCheckedException);
|
||||
.cause().isSameAs(undeclaredCheckedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException {
|
||||
doThrow(declaredCheckedException).when(target).doSomething();
|
||||
willThrow(declaredCheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByCaller).isSameAs(declaredCheckedException);
|
||||
}
|
||||
|
||||
@Test
|
||||
void targetThrowsUncheckedException() throws DeclaredCheckedException {
|
||||
doThrow(uncheckedException).when(target).doSomething();
|
||||
willThrow(uncheckedException).given(target).doSomething();
|
||||
invokeProxy();
|
||||
assertThat(throwableSeenByCaller).isSameAs(uncheckedException);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected interface MyInterface {
|
||||
interface MyInterface {
|
||||
|
||||
void doSomething() throws DeclaredCheckedException;
|
||||
}
|
||||
@@ -202,11 +180,11 @@ abstract class ProxyExceptionHandlingTests implements WithAssertions {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected static class UndeclaredCheckedException extends Exception {
|
||||
private static class UndeclaredCheckedException extends Exception {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
protected static class DeclaredCheckedException extends Exception {
|
||||
private static class DeclaredCheckedException extends Exception {
|
||||
}
|
||||
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Mikaël Francoeur
|
||||
* @since 6.2
|
||||
* @see JdkProxyExceptionHandlingTests
|
||||
*/
|
||||
class CglibProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests {
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
proxyFactory.setProxyTargetClass(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Enhancer.isEnhanced(proxy.getClass())).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.framework;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Mikaël Francoeur
|
||||
* @since 6.2
|
||||
* @see CglibProxyExceptionHandlingTests
|
||||
*/
|
||||
class JdkProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests {
|
||||
|
||||
@Override
|
||||
protected void assertProxyType(Object proxy) {
|
||||
assertThat(Proxy.isProxyClass(proxy.getClass())).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -340,6 +340,18 @@ class ProxyFactoryTests {
|
||||
assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(MyDate.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyInterfaceInCaseOfIntroducedInterfaceOnly() {
|
||||
ProxyFactory pf = new ProxyFactory();
|
||||
pf.addInterface(TimeStamped.class);
|
||||
TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(0L);
|
||||
pf.addAdvisor(new DefaultIntroductionAdvisor(ti, TimeStamped.class));
|
||||
Object proxy = pf.getProxy();
|
||||
assertThat(AopUtils.isJdkDynamicProxy(proxy)).as("Proxy is a JDK proxy").isTrue();
|
||||
assertThat(proxy).isInstanceOf(TimeStamped.class);
|
||||
assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(proxy.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyInterfaceInCaseOfNonTargetInterface() {
|
||||
ProxyFactory pf = new ProxyFactory();
|
||||
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.aop.interceptor;
|
||||
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.Future;
|
||||
|
||||
import org.aopalliance.intercept.MethodInvocation;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
|
||||
import org.springframework.core.task.AsyncTaskExecutor;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.spy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
|
||||
/**
|
||||
* Tests for {@link AsyncExecutionInterceptor}.
|
||||
*
|
||||
* @author Bao Ngo
|
||||
* @since 7.0
|
||||
*/
|
||||
class AsyncExecutionInterceptorTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void invokeOnInterfaceWithGeneric() throws Throwable {
|
||||
AsyncExecutionInterceptor interceptor = spy(new AsyncExecutionInterceptor(null));
|
||||
FutureRunner impl = new FutureRunner();
|
||||
MethodInvocation mi = mock();
|
||||
given(mi.getThis()).willReturn(impl);
|
||||
given(mi.getMethod()).willReturn(GenericRunner.class.getMethod("run"));
|
||||
|
||||
interceptor.invoke(mi);
|
||||
ArgumentCaptor<Class<?>> classArgumentCaptor = ArgumentCaptor.forClass(Class.class);
|
||||
verify(interceptor).doSubmit(any(Callable.class), any(AsyncTaskExecutor.class), classArgumentCaptor.capture());
|
||||
assertThat(classArgumentCaptor.getValue()).isEqualTo(Future.class);
|
||||
}
|
||||
|
||||
|
||||
interface GenericRunner<O> {
|
||||
|
||||
O run();
|
||||
}
|
||||
|
||||
static class FutureRunner implements GenericRunner<Future<Void>> {
|
||||
@Override
|
||||
public Future<Void> run() {
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,17 +31,17 @@ import kotlin.coroutines.Continuation
|
||||
*/
|
||||
class AopUtilsKotlinTests {
|
||||
|
||||
@Test
|
||||
fun `Invoking suspending function should return Mono`() {
|
||||
val value = "foo"
|
||||
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
|
||||
@Test
|
||||
fun `Invoking suspending function should return Mono`() {
|
||||
val value = "foo"
|
||||
val method = ReflectionUtils.findMethod(WithoutInterface::class.java, "handle",
|
||||
String::class. java, Continuation::class.java)!!
|
||||
val continuation = Continuation<Any>(CoroutineName("test")) { }
|
||||
val continuation = Continuation<Any>(CoroutineName("test")) { }
|
||||
val result = AopUtils.invokeJoinpointUsingReflection(WithoutInterface(), method, arrayOf(value, continuation))
|
||||
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
|
||||
assertThat(it.block()).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
assertThat(result).isInstanceOfSatisfying(Mono::class.java) {
|
||||
assertThat(it.block()).isEqualTo(value)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Invoking suspending function on bridged method should return Mono`() {
|
||||
@@ -54,11 +54,11 @@ class AopUtilsKotlinTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
suspend fun suspendingFunction(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
@Suppress("unused")
|
||||
suspend fun suspendingFunction(value: String): String {
|
||||
delay(1)
|
||||
return value
|
||||
}
|
||||
|
||||
class WithoutInterface {
|
||||
suspend fun handle(value: String): String {
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,10 +28,10 @@ import org.springframework.scheduling.annotation.Async;
|
||||
* <p>This aspect routes methods marked with the {@link Async} annotation as well as methods
|
||||
* in classes marked with the same. Any method expected to be routed asynchronously must
|
||||
* return either {@code void}, {@link Future}, or a subtype of {@link Future} (in particular,
|
||||
* Spring's {@link org.springframework.util.concurrent.ListenableFuture}). This aspect,
|
||||
* therefore, will produce a compile-time error for methods that violate this constraint
|
||||
* on the return type. If, however, a class marked with {@code @Async} contains a method
|
||||
* that violates this constraint, it produces only a warning.
|
||||
* {@link java.util.concurrent.CompletableFuture}). This aspect, therefore, will produce a
|
||||
* compile-time error for methods that violate this constraint on the return type. If,
|
||||
* however, a class marked with {@code @Async} contains a method that violates this
|
||||
* constraint, it produces only a warning.
|
||||
*
|
||||
* <p>This aspect needs to be injected with an implementation of a task-oriented
|
||||
* {@link java.util.concurrent.Executor} to activate it for a specific thread pool,
|
||||
|
||||
+28
-19
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -658,6 +658,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
|
||||
value = list.get(index);
|
||||
}
|
||||
else if (value instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
|
||||
// IMPORTANT: Do not pass full property name in here - property editors
|
||||
// must not kick in for map keys but rather only for map values.
|
||||
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
|
||||
value = map.get(convertedMapKey);
|
||||
}
|
||||
else if (value instanceof Iterable iterable) {
|
||||
// Apply index to Iterator in case of a Set/Collection/Iterable.
|
||||
int index = Integer.parseInt(key);
|
||||
@@ -685,14 +693,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
currIndex + ", accessed using property path '" + propertyName + "'");
|
||||
}
|
||||
}
|
||||
else if (value instanceof Map map) {
|
||||
Class<?> mapKeyType = ph.getResolvableType().getNested(i + 1).asMap().resolveGeneric(0);
|
||||
// IMPORTANT: Do not pass full property name in here - property editors
|
||||
// must not kick in for map keys but rather only for map values.
|
||||
TypeDescriptor typeDescriptor = TypeDescriptor.valueOf(mapKeyType);
|
||||
Object convertedMapKey = convertIfNecessary(null, null, key, mapKeyType, typeDescriptor);
|
||||
value = map.get(convertedMapKey);
|
||||
}
|
||||
else {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Property referenced in indexed property path '" + propertyName +
|
||||
@@ -904,16 +904,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
private Object newValue(Class<?> type, @Nullable TypeDescriptor desc, String name) {
|
||||
try {
|
||||
if (type.isArray()) {
|
||||
Class<?> componentType = type.componentType();
|
||||
// TODO - only handles 2-dimensional arrays
|
||||
if (componentType.isArray()) {
|
||||
Object array = Array.newInstance(componentType, 1);
|
||||
Array.set(array, 0, Array.newInstance(componentType.componentType(), 0));
|
||||
return array;
|
||||
}
|
||||
else {
|
||||
return Array.newInstance(componentType, 0);
|
||||
}
|
||||
return createArray(type);
|
||||
}
|
||||
else if (Collection.class.isAssignableFrom(type)) {
|
||||
TypeDescriptor elementDesc = (desc != null ? desc.getElementTypeDescriptor() : null);
|
||||
@@ -937,6 +928,24 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the array for the given array type.
|
||||
* @param arrayType the desired type of the target array
|
||||
* @return a new array instance
|
||||
*/
|
||||
private static Object createArray(Class<?> arrayType) {
|
||||
Assert.notNull(arrayType, "Array type must not be null");
|
||||
Class<?> componentType = arrayType.componentType();
|
||||
if (componentType.isArray()) {
|
||||
Object array = Array.newInstance(componentType, 1);
|
||||
Array.set(array, 0, createArray(componentType));
|
||||
return array;
|
||||
}
|
||||
else {
|
||||
return Array.newInstance(componentType, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the given property name into the corresponding property name tokens.
|
||||
* @param propertyName the property name to parse
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,4 +45,18 @@ public interface PropertyEditorRegistrar {
|
||||
*/
|
||||
void registerCustomEditors(PropertyEditorRegistry registry);
|
||||
|
||||
/**
|
||||
* Indicate whether this registrar exclusively overrides default editors
|
||||
* rather than registering custom editors, intended to be applied lazily.
|
||||
* <p>This has an impact on registrar handling in a bean factory: see
|
||||
* {@link org.springframework.beans.factory.config.ConfigurableBeanFactory#addPropertyEditorRegistrar}.
|
||||
* @since 6.2.3
|
||||
* @see PropertyEditorRegistry#registerCustomEditor
|
||||
* @see PropertyEditorRegistrySupport#overrideDefaultEditor
|
||||
* @see PropertyEditorRegistrySupport#setDefaultEditorRegistrar
|
||||
*/
|
||||
default boolean overridesDefaultEditors() {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -99,6 +99,9 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
|
||||
private boolean configValueEditorsActive = false;
|
||||
|
||||
@Nullable
|
||||
private PropertyEditorRegistrar defaultEditorRegistrar;
|
||||
|
||||
@Nullable
|
||||
private Map<Class<?>, PropertyEditor> defaultEditors;
|
||||
|
||||
@@ -155,6 +158,19 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
this.configValueEditorsActive = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a registrar for default editors, as a lazy way of overriding default editors.
|
||||
* <p>This is expected to be a collaborator with {@link PropertyEditorRegistrySupport},
|
||||
* downcasting the given {@link PropertyEditorRegistry} accordingly and calling
|
||||
* {@link #overrideDefaultEditor} for registering additional default editors on it.
|
||||
* @param registrar the registrar to call when default editors are actually needed
|
||||
* @since 6.2.3
|
||||
* @see #overrideDefaultEditor
|
||||
*/
|
||||
public void setDefaultEditorRegistrar(PropertyEditorRegistrar registrar) {
|
||||
this.defaultEditorRegistrar = registrar;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the default editor for the specified type with the given property editor.
|
||||
* <p>Note that this is different from registering a custom editor in that the editor
|
||||
@@ -184,6 +200,9 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
|
||||
if (!this.defaultEditorsActive) {
|
||||
return null;
|
||||
}
|
||||
if (this.overriddenDefaultEditors == null && this.defaultEditorRegistrar != null) {
|
||||
this.defaultEditorRegistrar.registerCustomEditors(this);
|
||||
}
|
||||
if (this.overriddenDefaultEditors != null) {
|
||||
PropertyEditor editor = this.overriddenDefaultEditors.get(requiredType);
|
||||
if (editor != null) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,10 +39,14 @@ import org.springframework.util.StringUtils;
|
||||
* (which the methods defined on the ListableBeanFactory interface don't,
|
||||
* in contrast to the methods defined on the BeanFactory interface).
|
||||
*
|
||||
* <p><b>NOTE:</b> It is generally preferable to use {@link ObjectProvider#stream()}
|
||||
* via {@link BeanFactory#getBeanProvider} instead of this utility class.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @since 04.07.2003
|
||||
* @see BeanFactory#getBeanProvider
|
||||
*/
|
||||
public abstract class BeanFactoryUtils {
|
||||
|
||||
@@ -308,7 +312,7 @@ public abstract class BeanFactoryUtils {
|
||||
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
|
||||
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
|
||||
* @param lbf the bean factory
|
||||
* @param type type of bean to match
|
||||
* @param type the type of bean to match
|
||||
* @return the Map of matching bean instances, or an empty Map if none
|
||||
* @throws BeansException if a bean could not be created
|
||||
* @see ListableBeanFactory#getBeansOfType(Class)
|
||||
@@ -347,7 +351,7 @@ public abstract class BeanFactoryUtils {
|
||||
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
|
||||
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
|
||||
* @param lbf the bean factory
|
||||
* @param type type of bean to match
|
||||
* @param type the type of bean to match
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
@@ -395,7 +399,7 @@ public abstract class BeanFactoryUtils {
|
||||
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
|
||||
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
|
||||
* @param lbf the bean factory
|
||||
* @param type type of bean to match
|
||||
* @param type the type of bean to match
|
||||
* @return the matching bean instance
|
||||
* @throws NoSuchBeanDefinitionException if no bean of the given type was found
|
||||
* @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
|
||||
@@ -425,7 +429,7 @@ public abstract class BeanFactoryUtils {
|
||||
* 'replacing' beans by explicitly choosing the same bean name in a child factory;
|
||||
* the bean in the ancestor factory won't be visible then, not even for by-type lookups.
|
||||
* @param lbf the bean factory
|
||||
* @param type type of bean to match
|
||||
* @param type the type of bean to match
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
@@ -457,7 +461,7 @@ public abstract class BeanFactoryUtils {
|
||||
* <p>This version of {@code beanOfType} automatically includes
|
||||
* prototypes and FactoryBeans.
|
||||
* @param lbf the bean factory
|
||||
* @param type type of bean to match
|
||||
* @param type the type of bean to match
|
||||
* @return the matching bean instance
|
||||
* @throws NoSuchBeanDefinitionException if no bean of the given type was found
|
||||
* @throws NoUniqueBeanDefinitionException if more than one bean of the given type was found
|
||||
@@ -481,7 +485,7 @@ public abstract class BeanFactoryUtils {
|
||||
* only raw FactoryBeans will be checked (which doesn't require initialization
|
||||
* of each FactoryBean).
|
||||
* @param lbf the bean factory
|
||||
* @param type type of bean to match
|
||||
* @param type the type of bean to match
|
||||
* @param includeNonSingletons whether to include prototype or scoped beans too
|
||||
* or just singletons (also applies to FactoryBeans)
|
||||
* @param allowEagerInit whether to initialize <i>lazy-init singletons</i> and
|
||||
@@ -529,7 +533,7 @@ public abstract class BeanFactoryUtils {
|
||||
|
||||
/**
|
||||
* Extract a unique bean for the given type from the given Map of matching beans.
|
||||
* @param type type of bean to match
|
||||
* @param type the type of bean to match
|
||||
* @param matchingBeans all matching beans found
|
||||
* @return the unique bean instance
|
||||
* @throws NoSuchBeanDefinitionException if no bean of the given type was found
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,6 +18,7 @@ package org.springframework.beans.factory;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@@ -32,6 +33,8 @@ import org.springframework.lang.Nullable;
|
||||
* <p>In a {@link BeanFactory} environment, every {@code ObjectProvider} obtained
|
||||
* from the factory will be bound to its {@code BeanFactory} for a specific bean
|
||||
* type, matching all provider calls against factory-registered bean definitions.
|
||||
* Note that all such calls dynamically operate on the underlying factory state,
|
||||
* freshly resolving the requested target object on every call.
|
||||
*
|
||||
* <p>As of 5.1, this interface extends {@link Iterable} and provides {@link Stream}
|
||||
* support. It can be therefore be used in {@code for} loops, provides {@link #forEach}
|
||||
@@ -51,6 +54,18 @@ import org.springframework.lang.Nullable;
|
||||
*/
|
||||
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
|
||||
|
||||
/**
|
||||
* A predicate for unfiltered type matches, including non-default candidates
|
||||
* but still excluding non-autowire candidates when used on injection points.
|
||||
* @since 6.2.3
|
||||
* @see #stream(Predicate)
|
||||
* @see #orderedStream(Predicate)
|
||||
* @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate()
|
||||
* @see org.springframework.beans.factory.support.AbstractBeanDefinition#isDefaultCandidate()
|
||||
*/
|
||||
Predicate<Class<?>> UNFILTERED = (clazz -> true);
|
||||
|
||||
|
||||
@Override
|
||||
default T getObject() throws BeansException {
|
||||
Iterator<T> it = iterator();
|
||||
@@ -196,6 +211,10 @@ public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
|
||||
/**
|
||||
* Return a sequential {@link Stream} over all matching object instances,
|
||||
* without specific ordering guarantees (but typically in registration order).
|
||||
* <p>Note: The result may be filtered by default according to qualifiers on the
|
||||
* injection point versus target beans and the general autowire candidate status
|
||||
* of matching beans. For custom filtering against type-matching candidates, use
|
||||
* {@link #stream(Predicate)} instead (potentially with {@link #UNFILTERED}).
|
||||
* @since 5.1
|
||||
* @see #iterator()
|
||||
* @see #orderedStream()
|
||||
@@ -217,6 +236,10 @@ public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
|
||||
* {@link #stream()} method. You may override this to apply an
|
||||
* {@link org.springframework.core.annotation.AnnotationAwareOrderComparator}
|
||||
* if necessary.
|
||||
* <p>Note: The result may be filtered by default according to qualifiers on the
|
||||
* injection point versus target beans and the general autowire candidate status
|
||||
* of matching beans. For custom filtering against type-matching candidates, use
|
||||
* {@link #stream(Predicate)} instead (potentially with {@link #UNFILTERED}).
|
||||
* @since 5.1
|
||||
* @see #stream()
|
||||
* @see org.springframework.core.OrderComparator
|
||||
@@ -225,4 +248,32 @@ public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> {
|
||||
return stream().sorted(OrderComparator.INSTANCE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a custom-filtered {@link Stream} over all matching object instances,
|
||||
* without specific ordering guarantees (but typically in registration order).
|
||||
* @param customFilter a custom type filter for selecting beans among the raw
|
||||
* bean type matches (or {@link #UNFILTERED} for all raw type matches without
|
||||
* any default filtering)
|
||||
* @since 6.2.3
|
||||
* @see #stream()
|
||||
* @see #orderedStream(Predicate)
|
||||
*/
|
||||
default Stream<T> stream(Predicate<Class<?>> customFilter) {
|
||||
return stream().filter(obj -> customFilter.test(obj.getClass()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a custom-filtered {@link Stream} over all matching object instances,
|
||||
* pre-ordered according to the factory's common order comparator.
|
||||
* @param customFilter a custom type filter for selecting beans among the raw
|
||||
* bean type matches (or {@link #UNFILTERED} for all raw type matches without
|
||||
* any default filtering)
|
||||
* @since 6.2.3
|
||||
* @see #orderedStream()
|
||||
* @see #stream(Predicate)
|
||||
*/
|
||||
default Stream<T> orderedStream(Predicate<Class<?>> customFilter) {
|
||||
return orderedStream().filter(obj -> customFilter.test(obj.getClass()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -95,7 +95,7 @@ public abstract class BeanFactoryAnnotationUtils {
|
||||
// Full qualifier matching supported.
|
||||
return qualifiedBeanOfType(lbf, beanType, qualifier);
|
||||
}
|
||||
else if (beanFactory.containsBean(qualifier)) {
|
||||
else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) {
|
||||
// Fallback: target bean at least found by bean name.
|
||||
return beanFactory.getBean(qualifier, beanType);
|
||||
}
|
||||
@@ -110,16 +110,16 @@ public abstract class BeanFactoryAnnotationUtils {
|
||||
/**
|
||||
* Obtain a bean of type {@code T} from the given {@code BeanFactory} declaring a qualifier
|
||||
* (for example, {@code <qualifier>} or {@code @Qualifier}) matching the given qualifier).
|
||||
* @param bf the factory to get the target bean from
|
||||
* @param beanFactory the factory to get the target bean from
|
||||
* @param beanType the type of bean to retrieve
|
||||
* @param qualifier the qualifier for selecting between multiple bean matches
|
||||
* @return the matching bean of type {@code T} (never {@code null})
|
||||
*/
|
||||
private static <T> T qualifiedBeanOfType(ListableBeanFactory bf, Class<T> beanType, String qualifier) {
|
||||
String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(bf, beanType);
|
||||
private static <T> T qualifiedBeanOfType(ListableBeanFactory beanFactory, Class<T> beanType, String qualifier) {
|
||||
String[] candidateBeans = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(beanFactory, beanType);
|
||||
String matchingBean = null;
|
||||
for (String beanName : candidateBeans) {
|
||||
if (isQualifierMatch(qualifier::equals, beanName, bf)) {
|
||||
if (isQualifierMatch(qualifier::equals, beanName, beanFactory)) {
|
||||
if (matchingBean != null) {
|
||||
throw new NoUniqueBeanDefinitionException(beanType, matchingBean, beanName);
|
||||
}
|
||||
@@ -127,11 +127,11 @@ public abstract class BeanFactoryAnnotationUtils {
|
||||
}
|
||||
}
|
||||
if (matchingBean != null) {
|
||||
return bf.getBean(matchingBean, beanType);
|
||||
return beanFactory.getBean(matchingBean, beanType);
|
||||
}
|
||||
else if (bf.containsBean(qualifier)) {
|
||||
else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifier, beanType)) {
|
||||
// Fallback: target bean at least found by bean name - probably a manually registered singleton.
|
||||
return bf.getBean(qualifier, beanType);
|
||||
return beanFactory.getBean(qualifier, beanType);
|
||||
}
|
||||
else {
|
||||
throw new NoSuchBeanDefinitionException(qualifier, "No matching " + beanType.getSimpleName() +
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
RootBeanDefinition mbd = this.registeredBean.getMergedBeanDefinition();
|
||||
Class<?> beanClass = (mbd.hasBeanClass() ? mbd.getBeanClass() : null);
|
||||
Class<?> beanClass = (mbd.hasBeanClass() ? ClassUtils.getUserClass(mbd.getBeanClass()) : null);
|
||||
CodeBlock beanClassCode = generateBeanClassCode(
|
||||
beanRegistrationCode.getClassName().packageName(),
|
||||
(beanClass != null ? beanClass : beanType.toClass()));
|
||||
|
||||
+42
-30
@@ -156,91 +156,96 @@ public class InstanceSupplierCodeGenerator {
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Constructor<?> constructor) {
|
||||
String beanName = registeredBean.getBeanName();
|
||||
Class<?> beanClass = registeredBean.getBeanClass();
|
||||
ConstructorDescriptor descriptor = new ConstructorDescriptor(
|
||||
registeredBean.getBeanName(), constructor, registeredBean.getBeanClass());
|
||||
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) {
|
||||
return generateCodeForInaccessibleConstructor(beanName, constructor,
|
||||
hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
|
||||
Class<?> publicType = descriptor.publicType();
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) {
|
||||
return generateCodeForInaccessibleConstructor(descriptor,
|
||||
hints -> hints.registerType(publicType, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
|
||||
}
|
||||
|
||||
if (!isVisible(constructor, constructor.getDeclaringClass())) {
|
||||
return generateCodeForInaccessibleConstructor(beanName, constructor,
|
||||
return generateCodeForInaccessibleConstructor(descriptor,
|
||||
hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE));
|
||||
}
|
||||
return generateCodeForAccessibleConstructor(beanName, constructor);
|
||||
return generateCodeForAccessibleConstructor(descriptor);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForAccessibleConstructor(String beanName, Constructor<?> constructor) {
|
||||
private CodeBlock generateCodeForAccessibleConstructor(ConstructorDescriptor descriptor) {
|
||||
Constructor<?> constructor = descriptor.constructor();
|
||||
this.generationContext.getRuntimeHints().reflection().registerConstructor(
|
||||
constructor, ExecutableMode.INTROSPECT);
|
||||
|
||||
if (constructor.getParameterCount() == 0) {
|
||||
if (!this.allowDirectSupplierShortcut) {
|
||||
return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, constructor.getDeclaringClass());
|
||||
return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, descriptor.actualType());
|
||||
}
|
||||
if (!isThrowingCheckedException(constructor)) {
|
||||
return CodeBlock.of("$T::new", constructor.getDeclaringClass());
|
||||
return CodeBlock.of("$T::new", descriptor.actualType());
|
||||
}
|
||||
return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, constructor.getDeclaringClass());
|
||||
return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, descriptor.actualType());
|
||||
}
|
||||
|
||||
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method ->
|
||||
buildGetInstanceMethodForConstructor(method, beanName, constructor, PRIVATE_STATIC));
|
||||
buildGetInstanceMethodForConstructor(method, descriptor, PRIVATE_STATIC));
|
||||
return generateReturnStatement(generatedMethod);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName,
|
||||
Constructor<?> constructor, Consumer<ReflectionHints> hints) {
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(ConstructorDescriptor descriptor,
|
||||
Consumer<ReflectionHints> hints) {
|
||||
|
||||
Constructor<?> constructor = descriptor.constructor();
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
codeWarnings.detectDeprecation(constructor.getDeclaringClass(), constructor)
|
||||
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
|
||||
hints.accept(this.generationContext.getRuntimeHints().reflection());
|
||||
|
||||
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName());
|
||||
method.addModifiers(PRIVATE_STATIC);
|
||||
codeWarnings.suppress(method);
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, constructor.getDeclaringClass()));
|
||||
method.addStatement(generateResolverForConstructor(constructor));
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, descriptor.publicType()));
|
||||
method.addStatement(generateResolverForConstructor(descriptor));
|
||||
});
|
||||
|
||||
return generateReturnStatement(generatedMethod);
|
||||
}
|
||||
|
||||
private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, String beanName,
|
||||
Constructor<?> constructor, javax.lang.model.element.Modifier... modifiers) {
|
||||
private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, ConstructorDescriptor descriptor,
|
||||
javax.lang.model.element.Modifier... modifiers) {
|
||||
|
||||
Class<?> declaringClass = constructor.getDeclaringClass();
|
||||
Constructor<?> constructor = descriptor.constructor();
|
||||
Class<?> publicType = descriptor.publicType();
|
||||
Class<?> actualType = descriptor.actualType();
|
||||
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
codeWarnings.detectDeprecation(declaringClass, constructor)
|
||||
codeWarnings.detectDeprecation(actualType, constructor)
|
||||
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName());
|
||||
method.addModifiers(modifiers);
|
||||
codeWarnings.suppress(method);
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, declaringClass));
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, publicType));
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.add(generateResolverForConstructor(constructor));
|
||||
code.add(generateResolverForConstructor(descriptor));
|
||||
boolean hasArguments = constructor.getParameterCount() > 0;
|
||||
boolean onInnerClass = ClassUtils.isInnerClass(declaringClass);
|
||||
boolean onInnerClass = ClassUtils.isInnerClass(actualType);
|
||||
|
||||
CodeBlock arguments = hasArguments ?
|
||||
new AutowiredArgumentsCodeGenerator(declaringClass, constructor)
|
||||
new AutowiredArgumentsCodeGenerator(actualType, constructor)
|
||||
.generateCode(constructor.getParameterTypes(), (onInnerClass ? 1 : 0))
|
||||
: NO_ARGS;
|
||||
|
||||
CodeBlock newInstance = generateNewInstanceCodeForConstructor(declaringClass, arguments);
|
||||
CodeBlock newInstance = generateNewInstanceCodeForConstructor(actualType, arguments);
|
||||
code.add(generateWithGeneratorCode(hasArguments, newInstance));
|
||||
method.addStatement(code.build());
|
||||
}
|
||||
|
||||
private CodeBlock generateResolverForConstructor(Constructor<?> constructor) {
|
||||
CodeBlock parameterTypes = generateParameterTypesCode(constructor.getParameterTypes());
|
||||
private CodeBlock generateResolverForConstructor(ConstructorDescriptor descriptor) {
|
||||
CodeBlock parameterTypes = generateParameterTypesCode(descriptor.constructor().getParameterTypes());
|
||||
return CodeBlock.of("return $T.<$T>forConstructor($L)", BeanInstanceSupplier.class,
|
||||
constructor.getDeclaringClass(), parameterTypes);
|
||||
descriptor.publicType(), parameterTypes);
|
||||
}
|
||||
|
||||
private CodeBlock generateNewInstanceCodeForConstructor(Class<?> declaringClass, CodeBlock args) {
|
||||
@@ -438,4 +443,11 @@ public class InstanceSupplierCodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
record ConstructorDescriptor(String beanName, Constructor<?> constructor, Class<?> publicType) {
|
||||
|
||||
Class<?> actualType() {
|
||||
return this.constructor.getDeclaringClass();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -183,7 +183,11 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single
|
||||
* on the given registry, fresh for each bean creation attempt. This avoids
|
||||
* the need for synchronization on custom editors; hence, it is generally
|
||||
* preferable to use this method instead of {@link #registerCustomEditor}.
|
||||
* <p>If the given registrar implements
|
||||
* {@link PropertyEditorRegistrar#overridesDefaultEditors()} to return {@code true},
|
||||
* it will be applied lazily (only when default editors are actually needed).
|
||||
* @param registrar the PropertyEditorRegistrar to register
|
||||
* @see PropertyEditorRegistrar#overridesDefaultEditors()
|
||||
*/
|
||||
void addPropertyEditorRegistrar(PropertyEditorRegistrar registrar);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user