mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
558 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ba590ac9e4 | |||
| ee62701f56 | |||
| fa168ca78a | |||
| 3c228a5c1d | |||
| 9bf6b8cddf | |||
| 37ecdd1437 | |||
| 73f1c5a189 | |||
| 4d296fb4ca | |||
| 6a9444473f | |||
| 03ae97b2eb | |||
| e34cdc2a55 | |||
| 8b1b9ef9e5 | |||
| efdaae02e0 | |||
| 49ffb833a3 | |||
| 065e50a444 | |||
| 7135527765 | |||
| 65e30132f3 | |||
| ebb44a8368 | |||
| 3096bb6d0c | |||
| 2b2a57b4a3 | |||
| 3295289e17 | |||
| 457e876303 | |||
| 021bf6e77d | |||
| 90453643cc | |||
| 5a2cbc1ab3 | |||
| 348b4cd067 | |||
| 9b52cfd7d5 | |||
| dd6eede243 | |||
| de97e35189 | |||
| 45a2c51fe1 | |||
| b98c3257af | |||
| ea8ae09cb7 | |||
| 0abfad870c | |||
| 5a9af9e024 | |||
| 0867dfca33 | |||
| b4355dc955 | |||
| bc91e0ea96 | |||
| 8599ee6c2b | |||
| bc466022b1 | |||
| 6f11711e27 | |||
| c067919173 | |||
| ac773d97e9 | |||
| bd7007227c | |||
| e8f873a349 | |||
| b943817f3e | |||
| 4466548f53 | |||
| 9c183f9e77 | |||
| 03620fc530 | |||
| 1c108054ee | |||
| d0b186a1c7 | |||
| 4172581f1b | |||
| c88ba6c90e | |||
| d7c13d6518 | |||
| 5013d6d771 | |||
| 9cd7b6a91e | |||
| e9dcd64068 | |||
| ce7f47c962 | |||
| ef34464c94 | |||
| ef11a00c0b | |||
| 176b0b09bf | |||
| c48ff357dc | |||
| 190dabb8e1 | |||
| d15abd58b4 | |||
| 44500cf868 | |||
| e384389790 | |||
| 49e5c84928 | |||
| 56eb135608 | |||
| 5c5cf73e11 | |||
| 858c2bd270 | |||
| 124582d910 | |||
| 253f321e8b | |||
| 0252e39409 | |||
| 2f60083cd5 | |||
| b83e07ff8c | |||
| 1841ad3472 | |||
| 0477ba4de4 | |||
| 8c376e9cc5 | |||
| 018d3c9ef2 | |||
| c70741f60d | |||
| f40d98668d | |||
| 9c13c6b695 | |||
| b49924ba37 | |||
| 7b8c104077 | |||
| 8f62a8f579 | |||
| d0966dfb58 | |||
| bb45a3ae69 | |||
| 7095f4cb66 | |||
| a22d204681 | |||
| f27382cfb6 | |||
| 6bc196883a | |||
| c4f66b776f | |||
| 87e04df983 | |||
| 4d648f8b5d | |||
| 6ea9f66fd7 | |||
| eea6addd26 | |||
| 7f2c1f447f | |||
| 26869b0e4c | |||
| cd987fc104 | |||
| 3f9402a56b | |||
| c168e1c297 | |||
| 3afd551174 | |||
| ffd15155ee | |||
| 74ab5e4e25 | |||
| 463541967a | |||
| 4510b78dfd | |||
| 63f4ba4b2a | |||
| 0c7bc232d6 | |||
| 470bf3b0bb | |||
| 2ca9f6f064 | |||
| ecd8cd797e | |||
| cc5ae23915 | |||
| dbd47ff4f9 | |||
| 381bc4c405 | |||
| 0f2308e85f | |||
| ee804ee8fb | |||
| 4e5979c75a | |||
| e7db15b325 | |||
| 8f9cbcd86d | |||
| 6bb964e2d0 | |||
| a946fe2bf8 | |||
| 671d972454 | |||
| d9047d39e6 | |||
| 4db12806d1 | |||
| 290c9c4a19 | |||
| e01ad5a08d | |||
| b8158df3d6 | |||
| c4e25a1162 | |||
| 48009c8534 | |||
| 203ca30a64 | |||
| 34ea0461c7 | |||
| fbaeaf12bd | |||
| 7b08feeb6d | |||
| 743f32675d | |||
| 3ddc607b3e | |||
| f68fb97e7e | |||
| 044258f085 | |||
| 36d9357f94 | |||
| dcb9383ba1 | |||
| f8a3077da9 | |||
| 9fd1d0c6a3 | |||
| 30fcaef813 | |||
| 75e5a75da5 | |||
| 9bf01df230 | |||
| 8d2166139f | |||
| 374c3b4545 | |||
| d7e470d3e0 | |||
| 2862c87601 | |||
| aa56b5001a | |||
| 84430a8db2 | |||
| 6905dff660 | |||
| 20736bd06f | |||
| 37fb79e8ff | |||
| d8f8e76791 | |||
| dc41ff569e | |||
| 47651350f3 | |||
| 15c20c3e65 | |||
| 5455c645f0 | |||
| adb4b675fc | |||
| 208d52d852 | |||
| 18c3b637e4 | |||
| 34c69bfc67 | |||
| 37d7af42ac | |||
| cc986cd2e8 | |||
| 0141725638 | |||
| 0c2ba4e38e | |||
| c6a9aa59a3 | |||
| ad949a7450 | |||
| 86b2617c7f | |||
| 760376c318 | |||
| 5b6abe4c13 | |||
| 7a839e988a | |||
| 46859d6391 | |||
| 573e74b8bd | |||
| 6c231804a0 | |||
| 7c3913050a | |||
| ec488282a8 | |||
| 911cdb2ad0 | |||
| 0f83c483bb | |||
| c9050607bc | |||
| 5e82ee6bd7 | |||
| 387677eae8 | |||
| 4a314867d7 | |||
| 020f556841 | |||
| 6ea3b5a0e8 | |||
| d764087dbf | |||
| 9ab43b138a | |||
| 09ae080b99 | |||
| 70a1b2fae3 | |||
| 90ddb40d7a | |||
| 143985e862 | |||
| 4bd280b87e | |||
| 5877a38fa1 | |||
| 94d29bac9f | |||
| a6f3f18d89 | |||
| 8334cb1abb | |||
| 2d88f18bf1 | |||
| 15a6641677 | |||
| 63c8e7cb5d | |||
| c5ecc50bfe | |||
| 0fd94f1b9f | |||
| 2472126475 | |||
| f3b50c9a58 | |||
| 0a6e666857 | |||
| 403362fe3c | |||
| 534f123fe8 | |||
| e421104cf3 | |||
| adfeba23e0 | |||
| 7789e12575 | |||
| 30d793cefe | |||
| 78cc5df748 | |||
| 5ffaea7a43 | |||
| 1633ad24f3 | |||
| 5faf2ed7f4 | |||
| d2733cea36 | |||
| 108caea385 | |||
| 7e9ac120ac | |||
| c64dae3623 | |||
| 559ea6c480 | |||
| 34315fc20f | |||
| 3bb4795d43 | |||
| aae2952a32 | |||
| f62251aebd | |||
| f92f9c1d5b | |||
| 2b38c00656 | |||
| 06721ba6c0 | |||
| b6a5402d88 | |||
| aff9ac72ec | |||
| f895d762cd | |||
| 634d1dd20b | |||
| 725b02a66d | |||
| 5a0bd9e5d4 | |||
| 6dd73ae3e4 | |||
| 4606337180 | |||
| f45a08d4f5 | |||
| d62ce2969a | |||
| ed3fd12210 | |||
| 6f596e2e84 | |||
| 6786e1c3e5 | |||
| 93134fd4d1 | |||
| d0ceefedc6 | |||
| e230ea537c | |||
| 94eb6006e8 | |||
| 2576702cda | |||
| 2099e046d3 | |||
| 2ee7a8e77a | |||
| 3c40e5e501 | |||
| 9e45178202 | |||
| d82e70e345 | |||
| 1109892134 | |||
| 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 |
@@ -15,7 +15,7 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Generate Changelog
|
||||
uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11
|
||||
uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12
|
||||
with:
|
||||
config-file: .github/actions/create-github-release/changelog-generator.yml
|
||||
milestone: ${{ inputs.milestone }}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
-1
@@ -42,7 +42,8 @@ spring-*/src/main/java/META-INF/MANIFEST.MF
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea
|
||||
.idea/*
|
||||
!.idea/icon.svg
|
||||
out
|
||||
test-output
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
Generated
+52
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 510 510" style="enable-background:new 0 0 510 510;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#6DB33F;}
|
||||
</style>
|
||||
<title>icon-framework</title>
|
||||
<g id="Layer_2_1_">
|
||||
<g id="Layer_1-2">
|
||||
<path class="st0" d="M479.2,162.5c-5.6-34.3-20.5-64.4-43.9-87.8S382,36.5,347.6,30.9C320.1,11.2,288.5,0,255,0
|
||||
s-65.1,11.2-92.6,30.9C128,36.5,98,51.3,74.7,74.7C50.9,98.4,36.5,128.7,31,162.2C11.2,189.8,0,221.4,0,255s11.2,65.2,31,92.8
|
||||
c5.5,33.5,19.9,63.8,43.7,87.5c23.3,23.3,53.3,38.2,87.7,43.8C190,498.8,221.5,510,255,510s65-11.2,92.6-30.9
|
||||
c34.4-5.6,64.4-20.5,87.7-43.8c23.8-23.8,38.1-54.1,43.7-87.5c19.8-27.6,31-59.2,31-92.8S498.8,190,479.2,162.5z M473,224.2
|
||||
c-0.8,6-1.9,11.9-3.2,17.9c-7.2-22.4-17.1-43.8-29.4-63.8c-5.5-22.8-13.6-44.9-24.4-65.8c22.9,14.8,42.2,32.6,56.7,52.6
|
||||
C475.7,184.6,475.8,204.6,473,224.2z M444.7,199.6c9.3,17.7,16.6,36.3,21.9,55.6c-5.3,19.3-12.7,37.9-22,55.6
|
||||
C450.9,274,451,236.4,444.7,199.6L444.7,199.6z M175.4,341.1c6.5-4.5,13.7-7.8,21.3-9.8c32.2-7.5,46.7-9.9,66.3-18.4
|
||||
c36.8-15.9,74.8-53.3,84.6-94c-16.1,38.8-59.5,70.2-97.9,81.6c-26.2,7.8-79.8,13.6-80.1,13.6l-1.4-1.2
|
||||
c-30.6-17.4-27-85.8,31.8-104.1c25.7-8,55.9-0.7,83.5-5.4c29.4-5,64.4-24.5,79.9-52c12.3,46.4,26.1,118.5-9.7,160.2
|
||||
C309.4,363,211.8,318,175.4,341.1z M310.8,444.6c-17.7,9.4-36.5,16.8-55.8,22.1c-19.4-5.3-38.1-12.7-55.8-22.1
|
||||
C236.1,450.9,273.9,450.9,310.8,444.6z M117.6,392.4c13.1,13.1,27.3,25.1,42.3,35.8c-19-5.9-37.2-13.8-54.5-23.7
|
||||
c-9.9-17.3-17.8-35.7-23.7-54.7C92.4,365.1,104.5,379.3,117.6,392.4z M81.7,160.1c5.9-19,13.9-37.4,23.7-54.7
|
||||
c17.3-9.8,35.5-17.8,54.5-23.7c-15.1,10.8-29.2,22.7-42.3,35.8C104.5,130.7,92.4,144.9,81.7,160.1z M149.1,346.4
|
||||
c-8.5,0.2-15.5-6.5-15.7-15c-0.2-8.5,6.5-15.5,15-15.7c8.5-0.2,15.5,6.5,15.7,15c0,0,0,0.1,0,0.1
|
||||
C164.2,339.3,157.5,346.2,149.1,346.4L149.1,346.4z M199.2,65.4c17.7-9.4,36.5-16.8,55.8-22.1c19.4,5.3,38.1,12.7,55.8,22.1
|
||||
C273.9,59.1,236.1,59.1,199.2,65.4L199.2,65.4z M392.4,392.4c13.1-13.1,25.1-27.3,35.9-42.4c-5.9,19-13.9,37.3-23.7,54.5
|
||||
c-17.3,9.8-35.5,17.7-54.5,23.7C365.2,417.5,379.3,405.5,392.4,392.4L392.4,392.4z M392.4,117.6c-13.1-13.1-27.2-25.1-42.3-35.8
|
||||
c19,5.9,37.2,13.9,54.5,23.7c9.8,17.3,17.8,35.6,23.7,54.7C417.6,144.9,405.5,130.7,392.4,117.6L392.4,117.6z M430.5,79.5
|
||||
c19.4,19.3,32.6,43.6,39.3,71.1c-16.2-19.2-36.6-36.2-60.4-50c-13.8-23.8-30.8-44.2-50-60.4C387.1,46.9,411.2,60.1,430.5,79.5
|
||||
L430.5,79.5z M397.6,94c-20.8-10.7-42.8-18.8-65.6-24.2c-20-12.4-41.5-22.4-63.9-29.6c15.5-3.4,31.3-5.1,47.2-5.2
|
||||
c10,0,19.9,0.8,29.7,2.3C365.1,51.8,382.8,71.1,397.6,94z M255,6.8c27.6,0,53.8,8,77.4,22.2c-5.6-0.5-11.3-0.7-17.1-0.7
|
||||
c-20.4,0.1-40.6,2.8-60.3,8.1c-19.7-5.3-39.9-8-60.3-8.1c-5.8,0-11.5,0.2-17.1,0.7C201.2,14.7,227.4,6.8,255,6.8z M165,37.3
|
||||
c9.8-1.5,19.8-2.3,29.7-2.3c15.9,0.1,31.7,1.8,47.2,5.2C219.5,47.4,198,57.3,178,69.8c-22.7,5.4-44.8,13.6-65.6,24.2
|
||||
C127.2,71.1,144.9,51.8,165,37.3z M79.5,79.5c19.3-19.3,43.5-32.6,71.1-39.3c-19.2,16.2-36.2,36.6-50,60.4
|
||||
c-23.7,13.8-44,30.6-60.2,49.8C47,123.4,59.8,99.1,79.5,79.5z M37.4,164.9c14.6-20,33.8-37.7,56.6-52.4
|
||||
c-10.7,20.9-18.8,43-24.3,65.8c-12.3,20-22.2,41.4-29.4,63.8C34.6,215.4,33.5,189.3,37.4,164.9z M65.4,310.4
|
||||
c-9.3-17.6-16.6-36.2-21.9-55.4c5.3-19.2,12.6-37.8,21.9-55.4C59.1,236.3,59.1,273.7,65.4,310.4L65.4,310.4z M29.2,332.8
|
||||
c-14.4-23.7-22.4-50-22.4-77.8s8.1-54.1,22.4-77.8c-2.1,25,0.4,51.3,7.4,77.8C29.5,281.5,27,307.8,29.2,332.8z M40.3,268
|
||||
c7.2,22.3,17.1,43.7,29.4,63.7c5.5,22.8,13.6,44.9,24.3,65.8c-22.8-14.7-42-32.5-56.6-52.4C33.5,320.7,34.6,294.6,40.3,268z
|
||||
M79.5,430.5c-19.6-19.6-32.5-43.9-39.1-70.9c16.1,19.1,36.5,36,60.2,49.8c13.8,23.7,30.8,44.2,50,60.4
|
||||
C122.9,463.1,98.8,449.9,79.5,430.5z M112.5,416c20.8,10.7,42.8,18.8,65.5,24.2c20,12.4,41.5,22.4,63.9,29.6
|
||||
c-15.5,3.4-31.3,5.1-47.2,5.1c-10,0-19.9-0.8-29.7-2.3C144.9,458.1,127.2,438.9,112.5,416L112.5,416z M255,503.2
|
||||
c-27.6,0-53.8-8-77.4-22.2c5.6,0.5,11.3,0.7,17.1,0.7c20.4-0.1,40.6-2.8,60.3-8.1c19.7,5.3,39.9,8,60.3,8.1
|
||||
c5.8,0,11.5-0.2,17.1-0.7C308.8,495.3,282.6,503.2,255,503.2z M345,472.7c-9.8,1.5-19.8,2.3-29.7,2.3c-15.9-0.1-31.7-1.8-47.2-5.1
|
||||
c22.4-7.2,43.9-17.2,63.9-29.6c22.7-5.4,44.8-13.6,65.6-24.2C382.8,438.9,365.1,458.1,345,472.7z M430.5,430.5
|
||||
c-19.3,19.3-43.5,32.6-71.1,39.3c19.2-16.2,36.2-36.6,50-60.4c23.7-13.8,44-30.6,60.2-49.8C463.1,386.6,450.1,410.9,430.5,430.5
|
||||
L430.5,430.5z M472.6,345.1c-14.6,20-33.8,37.7-56.6,52.4c10.7-20.8,18.8-42.8,24.3-65.6c12.3-20,22.2-41.3,29.5-63.7
|
||||
C475.4,294.7,476.5,320.8,472.6,345.1L472.6,345.1z M473.5,255.3c2.6-9.9,4.7-20,6.1-30.1c2.2-15.7,2.7-31.7,1.4-47.5
|
||||
c14.2,23.6,22.2,49.8,22.2,77.4s-8.1,54.1-22.4,77.8C482.9,307.9,480.5,281.6,473.5,255.3L473.5,255.3z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -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)
|
||||
|
||||
+6
-6
@@ -9,7 +9,7 @@ plugins {
|
||||
id 'io.github.goooler.shadow' version '8.1.8' apply false
|
||||
id 'me.champeau.jmh' version '0.7.2' apply false
|
||||
id 'me.champeau.mrjar' version '0.1.1'
|
||||
id "net.ltgt.errorprone" version "3.1.0" apply false
|
||||
id "net.ltgt.errorprone" version "4.1.0" apply false
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -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.12.2/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,2 +1,2 @@
|
||||
org.gradle.caching=true
|
||||
javaFormatVersion=0.0.42
|
||||
javaFormatVersion=0.0.43
|
||||
|
||||
@@ -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.20.1");
|
||||
checkstyle.setToolVersion("10.23.1");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
@@ -64,7 +64,7 @@ public class CheckstyleConventions {
|
||||
NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class);
|
||||
noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines"));
|
||||
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**",
|
||||
"**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**");
|
||||
"**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "buildSrc/build/**");
|
||||
List<String> buildFolders = List.of("bin", "build", "out");
|
||||
project.allprojects(subproject -> {
|
||||
Path rootPath = project.getRootDir().toPath();
|
||||
|
||||
@@ -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.
|
||||
@@ -21,6 +21,8 @@ import java.util.Map;
|
||||
import org.gradle.api.Project;
|
||||
import org.gradle.api.plugins.JavaBasePlugin;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.api.tasks.testing.TestFrameworkOptions;
|
||||
import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions;
|
||||
import org.gradle.testretry.TestRetryPlugin;
|
||||
import org.gradle.testretry.TestRetryTaskExtension;
|
||||
|
||||
@@ -34,6 +36,7 @@ import org.gradle.testretry.TestRetryTaskExtension;
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @author Andy Wilkinson
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class TestConventions {
|
||||
|
||||
@@ -50,7 +53,12 @@ class TestConventions {
|
||||
}
|
||||
|
||||
private void configureTests(Project project, Test test) {
|
||||
test.useJUnitPlatform();
|
||||
TestFrameworkOptions existingOptions = test.getOptions();
|
||||
test.useJUnitPlatform(options -> {
|
||||
if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) {
|
||||
options.copyFrom(junitPlatformOptions);
|
||||
}
|
||||
});
|
||||
test.include("**/*Tests.class", "**/*Test.class");
|
||||
test.setSystemProperties(Map.of(
|
||||
"java.awt.headless", "true",
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
id 'java-platform'
|
||||
id 'io.freefair.aggregate-javadoc' version '8.3'
|
||||
id 'io.freefair.aggregate-javadoc' version '8.13.1'
|
||||
}
|
||||
|
||||
description = "Spring Framework API Docs"
|
||||
@@ -9,7 +9,7 @@ apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
url = "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ dependencies {
|
||||
|
||||
javadoc {
|
||||
title = "${rootProject.description} ${version} API"
|
||||
failOnError = true
|
||||
options {
|
||||
encoding = "UTF-8"
|
||||
memberLevel = JavadocMemberLevel.PROTECTED
|
||||
@@ -31,8 +32,9 @@ javadoc {
|
||||
destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile
|
||||
splitIndex = true
|
||||
links(rootProject.ext.javadocLinks)
|
||||
addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
// Check for 'syntax' and 'reference' during linting.
|
||||
addBooleanOption('Xdoclint:syntax,reference', true)
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
}
|
||||
maxMemory = "1024m"
|
||||
doFirst {
|
||||
@@ -87,7 +89,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,38 +37,39 @@ javadoc {
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
url = "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":spring-aspects"))
|
||||
api(project(":spring-context"))
|
||||
api(project(":spring-context-support"))
|
||||
api(project(":spring-jdbc"))
|
||||
api(project(":spring-jms"))
|
||||
api(project(":spring-test"))
|
||||
api(project(":spring-web"))
|
||||
api(project(":spring-webflux"))
|
||||
api(project(":spring-webmvc"))
|
||||
api(project(":spring-websocket"))
|
||||
|
||||
api("com.fasterxml.jackson.core:jackson-databind")
|
||||
api("com.fasterxml.jackson.module:jackson-module-parameter-names")
|
||||
api("com.mchange:c3p0:0.9.5.5")
|
||||
api("com.oracle.database.jdbc:ojdbc11")
|
||||
api("io.projectreactor.netty:reactor-netty-http")
|
||||
api("jakarta.jms:jakarta.jms-api")
|
||||
api("jakarta.servlet:jakarta.servlet-api")
|
||||
api("jakarta.resource:jakarta.resource-api")
|
||||
api("jakarta.validation:jakarta.validation-api")
|
||||
api("javax.cache:cache-api")
|
||||
api("org.apache.activemq:activemq-ra:6.1.2")
|
||||
api("org.apache.commons:commons-dbcp2:2.11.0")
|
||||
api("org.aspectj:aspectjweaver")
|
||||
api("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
|
||||
api("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
|
||||
implementation(project(":spring-aspects"))
|
||||
implementation(project(":spring-context"))
|
||||
implementation(project(":spring-context-support"))
|
||||
implementation(project(":spring-core-test"))
|
||||
implementation(project(":spring-jdbc"))
|
||||
implementation(project(":spring-jms"))
|
||||
implementation(project(":spring-test"))
|
||||
implementation(project(":spring-web"))
|
||||
implementation(project(":spring-webflux"))
|
||||
implementation(project(":spring-webmvc"))
|
||||
implementation(project(":spring-websocket"))
|
||||
|
||||
implementation("com.fasterxml.jackson.core:jackson-databind")
|
||||
implementation("com.fasterxml.jackson.module:jackson-module-parameter-names")
|
||||
implementation("com.mchange:c3p0:0.9.5.5")
|
||||
implementation("com.oracle.database.jdbc:ojdbc11")
|
||||
implementation("io.projectreactor.netty:reactor-netty-http")
|
||||
implementation("jakarta.jms:jakarta.jms-api")
|
||||
implementation("jakarta.servlet:jakarta.servlet-api")
|
||||
implementation("jakarta.resource:jakarta.resource-api")
|
||||
implementation("jakarta.validation:jakarta.validation-api")
|
||||
implementation("jakarta.websocket:jakarta.websocket-client-api")
|
||||
implementation("javax.cache:cache-api")
|
||||
implementation("org.apache.activemq:activemq-ra:6.1.2")
|
||||
implementation("org.apache.commons:commons-dbcp2:2.11.0")
|
||||
implementation("org.aspectj:aspectjweaver")
|
||||
implementation("org.assertj:assertj-core")
|
||||
implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib")
|
||||
implementation("org.junit.jupiter:junit-jupiter-api")
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
|
||||
@@ -92,11 +92,25 @@ the repeated JNDI lookup overhead. See
|
||||
{spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`]
|
||||
for details.
|
||||
|
||||
| `spring.locking.strict`
|
||||
| Instructs Spring to enforce strict locking during bean creation, rather than the mix of
|
||||
strict and lenient locking that 6.2 applies by default. See
|
||||
{spring-framework-api}++/beans/factory/support/DefaultListableBeanFactory.html#STRICT_LOCKING_PROPERTY_NAME++[`DefaultListableBeanFactory`]
|
||||
for details.
|
||||
|
||||
| `spring.objenesis.ignore`
|
||||
| Instructs Spring to ignore Objenesis, not even attempting to use it. See
|
||||
{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
|
||||
for details.
|
||||
|
||||
| `spring.placeholder.escapeCharacter.default`
|
||||
| The default escape character for property placeholder support. If not set, `'\'` will
|
||||
be used. Can be set to a custom escape character or an empty string to disable support
|
||||
for an escape character. The default escape character be explicitly overridden in
|
||||
`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See
|
||||
{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`]
|
||||
for details.
|
||||
|
||||
| `spring.test.aot.processing.failOnError`
|
||||
| A boolean flag that controls whether errors encountered during AOT processing in the
|
||||
_Spring TestContext Framework_ should result in an exception that fails the overall process.
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -469,20 +469,20 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
|
||||
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
|
||||
// ...
|
||||
registry.registerBeanDefinition("myClient", beanDefinition);
|
||||
RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
|
||||
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
|
||||
// ...
|
||||
registry.registerBeanDefinition("myClient", beanDefinition);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java)
|
||||
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java));
|
||||
// ...
|
||||
registry.registerBeanDefinition("myClient", beanDefinition)
|
||||
val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java)
|
||||
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java));
|
||||
// ...
|
||||
registry.registerBeanDefinition("myClient", beanDefinition)
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -444,9 +444,9 @@ through Java 8's `java.util.Optional`, as the following example shows:
|
||||
}
|
||||
----
|
||||
|
||||
You can also use a `@Nullable` annotation (of any kind in any package -- for example,
|
||||
`javax.annotation.Nullable` from JSR-305) or just leverage Kotlin built-in null-safety
|
||||
support:
|
||||
You can also use a parameter-level `@Nullable` annotation (of any kind in any package --
|
||||
for example, `javax.annotation.Nullable` from JSR-305) or just leverage Kotlin built-in
|
||||
null-safety support:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -477,6 +477,13 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
A type-level `@Nullable` annotation such as from JSpecify is not supported in Spring
|
||||
Framework 6.2 yet. You need to upgrade to Spring Framework 7.0 where the framework
|
||||
detects type-level annotations and consistently declares JSpecify in its own codebase.
|
||||
====
|
||||
|
||||
You can also use `@Autowired` for interfaces that are well-known resolvable
|
||||
dependencies: `BeanFactory`, `ApplicationContext`, `Environment`, `ResourceLoader`,
|
||||
`ApplicationEventPublisher`, and `MessageSource`. These interfaces and their extended
|
||||
|
||||
+47
-44
@@ -9,15 +9,15 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
|
||||
private final String catalog;
|
||||
private final String catalog;
|
||||
|
||||
public MovieRecommender(@Value("${catalog.name}") String catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
}
|
||||
public MovieRecommender(@Value("${catalog.name}") String catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -37,9 +37,9 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
@PropertySource("classpath:application.properties")
|
||||
public class AppConfig { }
|
||||
@Configuration
|
||||
@PropertySource("classpath:application.properties")
|
||||
public class AppConfig { }
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -56,7 +56,7 @@ And the following `application.properties` file:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
catalog.name=MovieCatalog
|
||||
catalog.name=MovieCatalog
|
||||
----
|
||||
|
||||
In that case, the `catalog` parameter and field will be equal to the `MovieCatalog` value.
|
||||
@@ -101,8 +101,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig
|
||||
|
||||
Using the above configuration ensures Spring initialization failure if any `${}`
|
||||
placeholder could not be resolved. It is also possible to use methods like
|
||||
`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or
|
||||
`setEscapeCharacter` to customize placeholders.
|
||||
`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or
|
||||
`setEscapeCharacter()` to customize the placeholder syntax. In addition, the default
|
||||
escape character can be changed or disabled globally by setting the
|
||||
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
|
||||
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
|
||||
|
||||
NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that
|
||||
will get properties from `application.properties` and `application.yml` files.
|
||||
@@ -119,15 +122,15 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
|
||||
private final String catalog;
|
||||
private final String catalog;
|
||||
|
||||
public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
}
|
||||
public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -150,16 +153,16 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
|
||||
@Bean
|
||||
public ConversionService conversionService() {
|
||||
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
|
||||
conversionService.addConverter(new MyCustomConverter());
|
||||
return conversionService;
|
||||
}
|
||||
}
|
||||
@Bean
|
||||
public ConversionService conversionService() {
|
||||
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
|
||||
conversionService.addConverter(new MyCustomConverter());
|
||||
return conversionService;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -188,15 +191,15 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
|
||||
private final String catalog;
|
||||
private final String catalog;
|
||||
|
||||
public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
}
|
||||
public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
|
||||
this.catalog = catalog;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -217,16 +220,16 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
@Component
|
||||
public class MovieRecommender {
|
||||
|
||||
private final Map<String, Integer> countOfMoviesPerCatalog;
|
||||
private final Map<String, Integer> countOfMoviesPerCatalog;
|
||||
|
||||
public MovieRecommender(
|
||||
@Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
|
||||
this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
|
||||
}
|
||||
}
|
||||
public MovieRecommender(
|
||||
@Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
|
||||
this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
|
||||
@@ -119,7 +119,7 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
|
||||
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
|
||||
----
|
||||
======
|
||||
|
||||
@@ -310,16 +310,16 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.beans.factory.getBean
|
||||
|
||||
// create and configure beans
|
||||
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
|
||||
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
|
||||
|
||||
// retrieve configured instance
|
||||
val service = context.getBean<PetStoreService>("petStore")
|
||||
// retrieve configured instance
|
||||
val service = context.getBean<PetStoreService>("petStore")
|
||||
|
||||
// use configured instance
|
||||
var userList = service.getUsernameList()
|
||||
// use configured instance
|
||||
var userList = service.getUsernameList()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -513,7 +513,7 @@ the classes above:
|
||||
<property name="notificationAddress" value="blockedlist@example.org"/>
|
||||
</bean>
|
||||
|
||||
<!-- optional: a custom ApplicationEventMulticaster definition -->
|
||||
<!-- optional: a custom ApplicationEventMulticaster definition -->
|
||||
<bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">
|
||||
<property name="taskExecutor" ref="..."/>
|
||||
<property name="errorHandler" ref="..."/>
|
||||
@@ -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
|
||||
|
||||
@@ -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`/
|
||||
|
||||
+2
-2
@@ -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 -->
|
||||
|
||||
@@ -226,7 +226,7 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
import org.springframework.beans.factory.getBean
|
||||
import org.springframework.beans.factory.getBean
|
||||
|
||||
fun main() {
|
||||
val ctx = ClassPathXmlApplicationContext("scripting/beans.xml")
|
||||
@@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the
|
||||
|
||||
|
||||
[[beans-factory-placeholderconfigurer]]
|
||||
=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer`
|
||||
=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer`
|
||||
|
||||
You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values
|
||||
from a bean definition in a separate file by using the standard Java `Properties` format.
|
||||
@@ -341,8 +341,8 @@ with placeholder values is defined:
|
||||
|
||||
The example shows properties configured from an external `Properties` file. At runtime,
|
||||
a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some
|
||||
properties of the DataSource. The values to replace are specified as placeholders of the
|
||||
form pass:q[`${property-name}`], which follows the Ant and log4j and JSP EL style.
|
||||
properties of the `DataSource`. The values to replace are specified as placeholders of the
|
||||
form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style.
|
||||
|
||||
The actual values come from another file in the standard Java `Properties` format:
|
||||
|
||||
@@ -355,11 +355,15 @@ jdbc.password=root
|
||||
----
|
||||
|
||||
Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and
|
||||
the same applies for other placeholder values that match keys in the properties file.
|
||||
The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
|
||||
attributes of a bean definition. Furthermore, you can customize the placeholder prefix and suffix.
|
||||
the same applies for other placeholder values that match keys in the properties file. The
|
||||
`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
|
||||
attributes of a bean definition. Furthermore, you can customize the placeholder prefix,
|
||||
suffix, default value separator, and escape character. In addition, the default escape
|
||||
character can be changed or disabled globally by setting the
|
||||
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
|
||||
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
|
||||
|
||||
With the `context` namespace introduced in Spring 2.5, you can configure property placeholders
|
||||
With the `context` namespace, you can configure property placeholders
|
||||
with a dedicated configuration element. You can provide one or more locations as a
|
||||
comma-separated list in the `location` attribute, as the following example shows:
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
----
|
||||
======
|
||||
|
||||
@@ -31,7 +31,7 @@ Java::
|
||||
----
|
||||
public class MyBean {
|
||||
private final Log log = LogFactory.getLog(getClass());
|
||||
// ...
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
@@ -40,8 +40,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class MyBean {
|
||||
private val log = LogFactory.getLog(javaClass)
|
||||
// ...
|
||||
private val log = LogFactory.getLog(javaClass)
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
@@ -399,7 +399,7 @@ A `ConstraintViolation` on the `degrees` method parameter is adapted to a
|
||||
`MessageSourceResolvable` with the following:
|
||||
|
||||
- Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"`
|
||||
- Message arguments "degrees2 and 2 (the field name and the constraint attribute)
|
||||
- Message arguments "degrees" and 2 (the field name and the constraint attribute)
|
||||
- Default message "must be less than or equal to 2"
|
||||
|
||||
To customize the above default message, you can add a property such as:
|
||||
|
||||
@@ -78,27 +78,27 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.setDatabaseConfigurer(EmbeddedDatabaseConfigurers
|
||||
.customizeConfigurer(H2, this::customize))
|
||||
.addScript("schema.sql")
|
||||
.build();
|
||||
}
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.setDatabaseConfigurer(EmbeddedDatabaseConfigurers
|
||||
.customizeConfigurer(H2, this::customize))
|
||||
.addScript("schema.sql")
|
||||
.build();
|
||||
}
|
||||
|
||||
private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) {
|
||||
return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) {
|
||||
@Override
|
||||
public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {
|
||||
super.configureConnectionProperties(properties, databaseName);
|
||||
properties.setDriverClass(CustomDriver.class);
|
||||
}
|
||||
};
|
||||
}
|
||||
private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) {
|
||||
return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) {
|
||||
@Override
|
||||
public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {
|
||||
super.configureConnectionProperties(properties, databaseName);
|
||||
properties.setDriverClass(CustomDriver.class);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
|
||||
.then();
|
||||
.then();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -144,7 +144,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
|
||||
.await()
|
||||
.await()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -173,7 +173,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
|
||||
.fetch().first();
|
||||
.fetch().first();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -181,7 +181,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val first = client.sql("SELECT id, name FROM person")
|
||||
.fetch().awaitSingle()
|
||||
.fetch().awaitSingle()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -194,8 +194,8 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
|
||||
.bind("fn", "Joe")
|
||||
.fetch().first();
|
||||
.bind("fn", "Joe")
|
||||
.fetch().first();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -203,8 +203,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
|
||||
.bind("fn", "Joe")
|
||||
.fetch().awaitSingle()
|
||||
.bind("fn", "Joe")
|
||||
.fetch().awaitSingle()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -240,8 +240,8 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Flux<String> names = client.sql("SELECT name FROM person")
|
||||
.map(row -> row.get("name", String.class))
|
||||
.all();
|
||||
.map(row -> row.get("name", String.class))
|
||||
.all();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -249,8 +249,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val names = client.sql("SELECT name FROM person")
|
||||
.map{ row: Row -> row.get("name", String.class) }
|
||||
.flow()
|
||||
.map{ row: Row -> row.get("name", String.class) }
|
||||
.flow()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -301,8 +301,8 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
|
||||
.bind("fn", "Joe")
|
||||
.fetch().rowsUpdated();
|
||||
.bind("fn", "Joe")
|
||||
.fetch().rowsUpdated();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -310,8 +310,8 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val affectedRows = client.sql("UPDATE person SET first_name = :fn")
|
||||
.bind("fn", "Joe")
|
||||
.fetch().awaitRowsUpdated()
|
||||
.bind("fn", "Joe")
|
||||
.fetch().awaitRowsUpdated()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -337,9 +337,9 @@ The following example shows parameter binding for a query:
|
||||
|
||||
[source,java]
|
||||
----
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind("id", "joe")
|
||||
.bind("name", "Joe")
|
||||
.bind("age", 34);
|
||||
----
|
||||
|
||||
@@ -369,9 +369,9 @@ Indices are zero based.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind(0, "joe")
|
||||
.bind(1, "Joe")
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bind(0, "joe")
|
||||
.bind(1, "Joe")
|
||||
.bind(2, 34);
|
||||
----
|
||||
|
||||
@@ -379,9 +379,9 @@ In case your application is binding to many parameters, the same can be achieved
|
||||
|
||||
[source,java]
|
||||
----
|
||||
List<?> values = List.of("joe", "Joe", 34);
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bindValues(values);
|
||||
List<?> values = List.of("joe", "Joe", 34);
|
||||
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
|
||||
.bindValues(values);
|
||||
----
|
||||
|
||||
|
||||
@@ -428,7 +428,7 @@ Java::
|
||||
tuples.add(new Object[] {"Ann", 50});
|
||||
|
||||
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
|
||||
.bind("tuples", tuples);
|
||||
.bind("tuples", tuples);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -440,7 +440,7 @@ Kotlin::
|
||||
tuples.add(arrayOf("Ann", 50))
|
||||
|
||||
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
|
||||
.bind("tuples", tuples)
|
||||
.bind("tuples", tuples)
|
||||
----
|
||||
======
|
||||
|
||||
@@ -455,7 +455,7 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
|
||||
.bind("ages", Arrays.asList(35, 50));
|
||||
.bind("ages", Arrays.asList(35, 50));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -463,7 +463,7 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
|
||||
.bind("ages", arrayOf(35, 50))
|
||||
.bind("ages", arrayOf(35, 50))
|
||||
----
|
||||
======
|
||||
|
||||
@@ -490,9 +490,9 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
|
||||
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
|
||||
.bind("name", …)
|
||||
.bind("state", …);
|
||||
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
|
||||
.bind("name", …)
|
||||
.bind("state", …);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -516,10 +516,10 @@ Java::
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
|
||||
.filter(statement -> s.returnGeneratedValues("id"));
|
||||
.filter(statement -> s.returnGeneratedValues("id"));
|
||||
|
||||
client.sql("SELECT id, name, state FROM table")
|
||||
.filter(statement -> s.fetchSize(25));
|
||||
.filter(statement -> s.fetchSize(25));
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
@@ -527,10 +527,10 @@ Kotlin::
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
|
||||
.filter { statement -> s.returnGeneratedValues("id") }
|
||||
.filter { statement -> s.returnGeneratedValues("id") }
|
||||
|
||||
client.sql("SELECT id, name, state FROM table")
|
||||
.filter { statement -> s.fetchSize(25) }
|
||||
.filter { statement -> s.fetchSize(25) }
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -55,11 +55,11 @@ a "shared objects file" source, as shown in the following example:
|
||||
|
||||
[source,shell,indent=0,subs="verbatim"]
|
||||
----
|
||||
[0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top)
|
||||
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top)
|
||||
[0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top)
|
||||
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
|
||||
----
|
||||
|
||||
If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -30,36 +30,36 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
RestClient defaultClient = RestClient.create();
|
||||
|
||||
RestClient customClient = RestClient.builder()
|
||||
.requestFactory(new HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build();
|
||||
RestClient defaultClient = RestClient.create();
|
||||
|
||||
RestClient customClient = RestClient.builder()
|
||||
.requestFactory(new HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(Map.of("variable", "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim"]
|
||||
----
|
||||
val defaultClient = RestClient.create()
|
||||
|
||||
val customClient = RestClient.builder()
|
||||
.requestFactory(HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(mapOf("variable" to "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build()
|
||||
val defaultClient = RestClient.create()
|
||||
|
||||
val customClient = RestClient.builder()
|
||||
.requestFactory(HttpComponentsClientHttpRequestFactory())
|
||||
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
|
||||
.baseUrl("https://example.com")
|
||||
.defaultUriVariables(mapOf("variable" to "foo"))
|
||||
.defaultHeader("My-Header", "Foo")
|
||||
.defaultCookie("My-Cookie", "Bar")
|
||||
.requestInterceptor(myCustomInterceptor)
|
||||
.requestInitializer(myCustomInitializer)
|
||||
.build()
|
||||
----
|
||||
======
|
||||
|
||||
@@ -81,20 +81,20 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
int id = 42;
|
||||
restClient.get()
|
||||
.uri("https://example.com/orders/{id}", id)
|
||||
....
|
||||
int id = 42;
|
||||
restClient.get()
|
||||
.uri("https://example.com/orders/{id}", id)
|
||||
// ...
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val id = 42
|
||||
restClient.get()
|
||||
.uri("https://example.com/orders/{id}", id)
|
||||
...
|
||||
val id = 42
|
||||
restClient.get()
|
||||
.uri("https://example.com/orders/{id}", id)
|
||||
// ...
|
||||
----
|
||||
======
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -129,12 +133,12 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
String result = restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body(String.class); <4>
|
||||
|
||||
System.out.println(result); <5>
|
||||
String result = restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body(String.class); <4>
|
||||
|
||||
System.out.println(result); <5>
|
||||
----
|
||||
<1> Set up a GET request
|
||||
<2> Specify the URL to connect to
|
||||
@@ -146,12 +150,12 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result= restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body<String>() <4>
|
||||
|
||||
println(result) <5>
|
||||
val result= restClient.get() <1>
|
||||
.uri("https://example.com") <2>
|
||||
.retrieve() <3>
|
||||
.body<String>() <4>
|
||||
|
||||
println(result) <5>
|
||||
----
|
||||
<1> Set up a GET request
|
||||
<2> Specify the URL to connect to
|
||||
@@ -168,14 +172,14 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ResponseEntity<String> result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity(String.class); <2>
|
||||
|
||||
System.out.println("Response status: " + result.getStatusCode()); <3>
|
||||
System.out.println("Response headers: " + result.getHeaders()); <3>
|
||||
System.out.println("Contents: " + result.getBody()); <3>
|
||||
ResponseEntity<String> result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity(String.class); <2>
|
||||
|
||||
System.out.println("Response status: " + result.getStatusCode()); <3>
|
||||
System.out.println("Response headers: " + result.getHeaders()); <3>
|
||||
System.out.println("Contents: " + result.getBody()); <3>
|
||||
----
|
||||
<1> Set up a GET request for the specified URL
|
||||
<2> Convert the response into a `ResponseEntity`
|
||||
@@ -185,14 +189,14 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity<String>() <2>
|
||||
|
||||
println("Response status: " + result.statusCode) <3>
|
||||
println("Response headers: " + result.headers) <3>
|
||||
println("Contents: " + result.body) <3>
|
||||
val result = restClient.get() <1>
|
||||
.uri("https://example.com") <1>
|
||||
.retrieve()
|
||||
.toEntity<String>() <2>
|
||||
|
||||
println("Response status: " + result.statusCode) <3>
|
||||
println("Response headers: " + result.headers) <3>
|
||||
println("Contents: " + result.body) <3>
|
||||
----
|
||||
<1> Set up a GET request for the specified URL
|
||||
<2> Convert the response into a `ResponseEntity`
|
||||
@@ -208,12 +212,12 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
int id = ...;
|
||||
Pet pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
.body(Pet.class); <3>
|
||||
int id = ...;
|
||||
Pet pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
.body(Pet.class); <3>
|
||||
----
|
||||
<1> Using URI variables
|
||||
<2> Set the `Accept` header to `application/json`
|
||||
@@ -223,12 +227,12 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val id = ...
|
||||
val pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
.body<Pet>() <3>
|
||||
val id = ...
|
||||
val pet = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id) <1>
|
||||
.accept(APPLICATION_JSON) <2>
|
||||
.retrieve()
|
||||
.body<Pet>() <3>
|
||||
----
|
||||
<1> Using URI variables
|
||||
<2> Set the `Accept` header to `application/json`
|
||||
@@ -243,13 +247,13 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Pet pet = ... <1>
|
||||
ResponseEntity<Void> response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
.retrieve()
|
||||
.toBodilessEntity(); <5>
|
||||
Pet pet = ... <1>
|
||||
ResponseEntity<Void> response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
.retrieve()
|
||||
.toBodilessEntity(); <5>
|
||||
----
|
||||
<1> Create a `Pet` domain object
|
||||
<2> Set up a POST request, and the URL to connect to
|
||||
@@ -261,13 +265,13 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val pet: Pet = ... <1>
|
||||
val response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
.retrieve()
|
||||
.toBodilessEntity() <5>
|
||||
val pet: Pet = ... <1>
|
||||
val response = restClient.post() <2>
|
||||
.uri("https://petclinic.example.com/pets/new") <2>
|
||||
.contentType(APPLICATION_JSON) <3>
|
||||
.body(pet) <4>
|
||||
.retrieve()
|
||||
.toBodilessEntity() <5>
|
||||
----
|
||||
<1> Create a `Pet` domain object
|
||||
<2> Set up a POST request, and the URL to connect to
|
||||
@@ -287,13 +291,13 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
String result = restClient.get() <1>
|
||||
.uri("https://example.com/this-url-does-not-exist") <1>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2>
|
||||
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3>
|
||||
})
|
||||
.body(String.class);
|
||||
String result = restClient.get() <1>
|
||||
.uri("https://example.com/this-url-does-not-exist") <1>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2>
|
||||
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3>
|
||||
})
|
||||
.body(String.class);
|
||||
----
|
||||
<1> Create a GET request for a URL that returns a 404 status code
|
||||
<2> Set up a status handler for all 4xx status codes
|
||||
@@ -303,12 +307,12 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result = restClient.get() <1>
|
||||
.uri("https://example.com/this-url-does-not-exist") <1>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2>
|
||||
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3>
|
||||
.body<String>()
|
||||
val result = restClient.get() <1>
|
||||
.uri("https://example.com/this-url-does-not-exist") <1>
|
||||
.retrieve()
|
||||
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2>
|
||||
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3>
|
||||
.body<String>()
|
||||
----
|
||||
<1> Create a GET request for a URL that returns a 404 status code
|
||||
<2> Set up a status handler for all 4xx status codes
|
||||
@@ -326,18 +330,18 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Pet result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(APPLICATION_JSON)
|
||||
.exchange((request, response) -> { <1>
|
||||
if (response.getStatusCode().is4xxClientError()) { <2>
|
||||
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2>
|
||||
}
|
||||
else {
|
||||
Pet pet = convertResponse(response); <3>
|
||||
return pet;
|
||||
}
|
||||
});
|
||||
Pet result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(APPLICATION_JSON)
|
||||
.exchange((request, response) -> { <1>
|
||||
if (response.getStatusCode().is4xxClientError()) { <2>
|
||||
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2>
|
||||
}
|
||||
else {
|
||||
Pet pet = convertResponse(response); <3>
|
||||
return pet;
|
||||
}
|
||||
});
|
||||
----
|
||||
<1> `exchange` provides the request and response
|
||||
<2> Throw an exception when the response has a 4xx status code
|
||||
@@ -347,17 +351,17 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange { request, response -> <1>
|
||||
if (response.getStatusCode().is4xxClientError()) { <2>
|
||||
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2>
|
||||
} else {
|
||||
val pet: Pet = convertResponse(response) <3>
|
||||
pet
|
||||
}
|
||||
}
|
||||
val result = restClient.get()
|
||||
.uri("https://petclinic.example.com/pets/{id}", id)
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.exchange { request, response -> <1>
|
||||
if (response.getStatusCode().is4xxClientError()) { <2>
|
||||
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2>
|
||||
} else {
|
||||
val pet: Pet = convertResponse(response) <3>
|
||||
pet
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> `exchange` provides the request and response
|
||||
<2> Throw an exception when the response has a 4xx status code
|
||||
@@ -376,15 +380,14 @@ To serialize only a subset of the object properties, you can specify a {baeldung
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
|
||||
value.setSerializationView(User.WithoutPasswordView.class);
|
||||
|
||||
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
|
||||
.contentType(APPLICATION_JSON)
|
||||
.body(value)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
|
||||
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
|
||||
value.setSerializationView(User.WithoutPasswordView.class);
|
||||
|
||||
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
|
||||
.contentType(APPLICATION_JSON)
|
||||
.body(value)
|
||||
.retrieve()
|
||||
.toBodilessEntity();
|
||||
----
|
||||
|
||||
==== Multipart
|
||||
@@ -394,24 +397,24 @@ For example:
|
||||
|
||||
[source,java,indent=0,subs="verbatim"]
|
||||
----
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
|
||||
// send using RestClient.post or RestTemplate.postForEntity
|
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
|
||||
|
||||
parts.add("fieldPart", "fieldValue");
|
||||
parts.add("filePart", new FileSystemResource("...logo.png"));
|
||||
parts.add("jsonPart", new Person("Jason"));
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_XML);
|
||||
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
|
||||
|
||||
// send using RestClient.post or RestTemplate.postForEntity
|
||||
----
|
||||
|
||||
In most cases, you do not have to specify the `Content-Type` for each part.
|
||||
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension.
|
||||
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
|
||||
|
||||
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
|
||||
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
|
||||
|
||||
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`.
|
||||
If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`.
|
||||
@@ -853,7 +856,7 @@ Start by creating the interface with `@HttpExchange` methods:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
interface RepositoryService {
|
||||
public interface RepositoryService {
|
||||
|
||||
@GetExchange("/repos/{owner}/{repo}")
|
||||
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
|
||||
@@ -904,7 +907,7 @@ For `RestTemplate`:
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json")
|
||||
interface RepositoryService {
|
||||
public interface RepositoryService {
|
||||
|
||||
@GetExchange
|
||||
Repository getRepository(@PathVariable String owner, @PathVariable String repo);
|
||||
@@ -938,10 +941,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 +991,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
|
||||
|
||||
@@ -1112,11 +1136,11 @@ performed through the client:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setErrorHandler(myErrorHandler);
|
||||
|
||||
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setErrorHandler(myErrorHandler);
|
||||
|
||||
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
|
||||
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
|
||||
----
|
||||
|
||||
For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and
|
||||
|
||||
@@ -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].
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -215,45 +215,43 @@ For suspending functions, a `TransactionalOperator.executeAndAwait` extension is
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
import org.springframework.transaction.reactive.executeAndAwait
|
||||
import org.springframework.transaction.reactive.executeAndAwait
|
||||
|
||||
class PersonRepository(private val operator: TransactionalOperator) {
|
||||
class PersonRepository(private val operator: TransactionalOperator) {
|
||||
|
||||
suspend fun initDatabase() = operator.executeAndAwait {
|
||||
insertPerson1()
|
||||
insertPerson2()
|
||||
}
|
||||
suspend fun initDatabase() = operator.executeAndAwait {
|
||||
insertPerson1()
|
||||
insertPerson2()
|
||||
}
|
||||
|
||||
private suspend fun insertPerson1() {
|
||||
// INSERT SQL statement
|
||||
}
|
||||
private suspend fun insertPerson1() {
|
||||
// INSERT SQL statement
|
||||
}
|
||||
|
||||
private suspend fun insertPerson2() {
|
||||
// INSERT SQL statement
|
||||
}
|
||||
}
|
||||
private suspend fun insertPerson2() {
|
||||
// INSERT SQL statement
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
For Kotlin `Flow`, a `Flow<T>.transactional` extension is provided.
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
import org.springframework.transaction.reactive.transactional
|
||||
import org.springframework.transaction.reactive.transactional
|
||||
|
||||
class PersonRepository(private val operator: TransactionalOperator) {
|
||||
class PersonRepository(private val operator: TransactionalOperator) {
|
||||
|
||||
fun updatePeople() = findPeople().map(::updatePerson).transactional(operator)
|
||||
fun updatePeople() = findPeople().map(::updatePerson).transactional(operator)
|
||||
|
||||
private fun findPeople(): Flow<Person> {
|
||||
// SELECT SQL statement
|
||||
}
|
||||
private fun findPeople(): Flow<Person> {
|
||||
// SELECT SQL statement
|
||||
}
|
||||
|
||||
private suspend fun updatePerson(person: Person): Person {
|
||||
// UPDATE SQL statement
|
||||
}
|
||||
}
|
||||
private suspend fun updatePerson(person: Person): Person {
|
||||
// UPDATE SQL statement
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@ NOTE: If you use Spring Boot, you should probably use
|
||||
instead of `@Value` annotations.
|
||||
|
||||
As an alternative, you can customize the property placeholder prefix by declaring the
|
||||
following configuration beans:
|
||||
following `PropertySourcesPlaceholderConfigurer` bean:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@@ -200,8 +200,10 @@ following configuration beans:
|
||||
}
|
||||
----
|
||||
|
||||
You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`)
|
||||
that uses the `${...}` syntax, with configuration beans, as the following example shows:
|
||||
You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use
|
||||
the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by
|
||||
declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example
|
||||
shows:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl
|
||||
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
|
||||
----
|
||||
|
||||
In addition, the default escape character can be changed or disabled globally by setting
|
||||
the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or
|
||||
via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
|
||||
|
||||
|
||||
[[checked-exceptions]]
|
||||
@@ -296,17 +301,17 @@ for example when writing a `org.springframework.core.convert.converter.Converter
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
|
||||
// ...
|
||||
}
|
||||
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
When converting any kind of objects, star projection with `*` can be used instead of `out Any`.
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
|
||||
// ...
|
||||
}
|
||||
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans,
|
||||
@@ -340,13 +345,14 @@ file with a `spring.test.constructor.autowire.mode = all` property.
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
@TestConstructor(autowireMode = AutowireMode.ALL)
|
||||
class OrderServiceIntegrationTests(val orderService: OrderService,
|
||||
val customerService: CustomerService) {
|
||||
|
||||
// tests that use the injected OrderService and CustomerService
|
||||
}
|
||||
@SpringJUnitConfig(TestConfig::class)
|
||||
@TestConstructor(autowireMode = AutowireMode.ALL)
|
||||
class OrderServiceIntegrationTests(
|
||||
val orderService: OrderService,
|
||||
val customerService: CustomerService) {
|
||||
|
||||
// tests that use the injected OrderService and CustomerService
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -368,29 +374,29 @@ The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on n
|
||||
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
|
||||
class IntegrationTests {
|
||||
|
||||
val application = Application(8181)
|
||||
val client = WebClient.create("http://localhost:8181")
|
||||
val application = Application(8181)
|
||||
val client = WebClient.create("http://localhost:8181")
|
||||
|
||||
@BeforeAll
|
||||
fun beforeAll() {
|
||||
application.start()
|
||||
}
|
||||
@BeforeAll
|
||||
fun beforeAll() {
|
||||
application.start()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Find all users on HTML page`() {
|
||||
client.get().uri("/users")
|
||||
.accept(TEXT_HTML)
|
||||
.retrieve()
|
||||
.bodyToMono<String>()
|
||||
.test()
|
||||
.expectNextMatches { it.contains("Foo") }
|
||||
.verifyComplete()
|
||||
}
|
||||
@Test
|
||||
fun `Find all users on HTML page`() {
|
||||
client.get().uri("/users")
|
||||
.accept(TEXT_HTML)
|
||||
.retrieve()
|
||||
.bodyToMono<String>()
|
||||
.test()
|
||||
.expectNextMatches { it.contains("Foo") }
|
||||
.verifyComplete()
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun afterAll() {
|
||||
application.stop()
|
||||
}
|
||||
@AfterAll
|
||||
fun afterAll() {
|
||||
application.stop()
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -403,26 +409,27 @@ The following example shows how to do so:
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
class SpecificationLikeTests {
|
||||
class SpecificationLikeTests {
|
||||
|
||||
@Nested
|
||||
@DisplayName("a calculator")
|
||||
inner class Calculator {
|
||||
|
||||
@Nested
|
||||
@DisplayName("a calculator")
|
||||
inner class Calculator {
|
||||
val calculator = SampleCalculator()
|
||||
|
||||
@Test
|
||||
fun `should return the result of adding the first number to the second number`() {
|
||||
val sum = calculator.sum(2, 4)
|
||||
assertEquals(6, sum)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return the result of subtracting the second number from the first number`() {
|
||||
val subtract = calculator.subtract(4, 2)
|
||||
assertEquals(2, subtract)
|
||||
}
|
||||
}
|
||||
}
|
||||
val calculator = SampleCalculator()
|
||||
|
||||
@Test
|
||||
fun `should return the result of adding the first number to the second number`() {
|
||||
val sum = calculator.sum(2, 4)
|
||||
assertEquals(6, sum)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return the result of subtracting the second number from the first number`() {
|
||||
val subtract = calculator.subtract(4, 2)
|
||||
assertEquals(2, subtract)
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
[[kotlin-web]]
|
||||
= Web
|
||||
|
||||
|
||||
|
||||
[[router-dsl]]
|
||||
== Router DSL
|
||||
|
||||
@@ -16,27 +14,27 @@ These DSL let you write clean and idiomatic Kotlin code to build a `RouterFuncti
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
class RouterRouterConfiguration {
|
||||
|
||||
@Bean
|
||||
fun mainRouter(userHandler: UserHandler) = router {
|
||||
accept(TEXT_HTML).nest {
|
||||
GET("/") { ok().render("index") }
|
||||
GET("/sse") { ok().render("sse") }
|
||||
GET("/users", userHandler::findAllView)
|
||||
}
|
||||
"/api".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/users", userHandler::findAll)
|
||||
@Configuration
|
||||
class RouterRouterConfiguration {
|
||||
|
||||
@Bean
|
||||
fun mainRouter(userHandler: UserHandler) = router {
|
||||
accept(TEXT_HTML).nest {
|
||||
GET("/") { ok().render("index") }
|
||||
GET("/sse") { ok().render("sse") }
|
||||
GET("/users", userHandler::findAllView)
|
||||
}
|
||||
accept(TEXT_EVENT_STREAM).nest {
|
||||
GET("/users", userHandler::stream)
|
||||
"/api".nest {
|
||||
accept(APPLICATION_JSON).nest {
|
||||
GET("/users", userHandler::findAll)
|
||||
}
|
||||
accept(TEXT_EVENT_STREAM).nest {
|
||||
GET("/users", userHandler::stream)
|
||||
}
|
||||
}
|
||||
resources("/**", ClassPathResource("static/"))
|
||||
}
|
||||
resources("/**", ClassPathResource("static/"))
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: This DSL is programmatic, meaning that it allows custom registration logic of beans
|
||||
@@ -55,22 +53,22 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met
|
||||
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
val mockMvc: MockMvc = ...
|
||||
mockMvc.get("/person/{name}", "Lee") {
|
||||
secure = true
|
||||
accept = APPLICATION_JSON
|
||||
headers {
|
||||
contentLanguage = Locale.FRANCE
|
||||
val mockMvc: MockMvc = ...
|
||||
mockMvc.get("/person/{name}", "Lee") {
|
||||
secure = true
|
||||
accept = APPLICATION_JSON
|
||||
headers {
|
||||
contentLanguage = Locale.FRANCE
|
||||
}
|
||||
principal = Principal { "foo" }
|
||||
}.andExpect {
|
||||
status { isOk }
|
||||
content { contentType(APPLICATION_JSON) }
|
||||
jsonPath("$.name") { value("Lee") }
|
||||
content { json("""{"someBoolean": false}""", false) }
|
||||
}.andDo {
|
||||
print()
|
||||
}
|
||||
principal = Principal { "foo" }
|
||||
}.andExpect {
|
||||
status { isOk }
|
||||
content { contentType(APPLICATION_JSON) }
|
||||
jsonPath("$.name") { value("Lee") }
|
||||
content { json("""{"someBoolean": false}""", false) }
|
||||
}.andDo {
|
||||
print()
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
@@ -89,9 +87,9 @@ is possible to use such feature to render Kotlin-based templates with
|
||||
`build.gradle.kts`
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
dependencies {
|
||||
runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}")
|
||||
}
|
||||
dependencies {
|
||||
runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}")
|
||||
}
|
||||
----
|
||||
|
||||
Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplateViewResolver` beans.
|
||||
@@ -99,23 +97,23 @@ Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplat
|
||||
`KotlinScriptConfiguration.kt`
|
||||
[source,kotlin,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
class KotlinScriptConfiguration {
|
||||
|
||||
@Bean
|
||||
fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
|
||||
engineName = "kotlin"
|
||||
setScripts("scripts/render.kts")
|
||||
renderFunction = "render"
|
||||
isSharedEngine = false
|
||||
@Configuration
|
||||
class KotlinScriptConfiguration {
|
||||
|
||||
@Bean
|
||||
fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
|
||||
engineName = "kotlin"
|
||||
setScripts("scripts/render.kts")
|
||||
renderFunction = "render"
|
||||
isSharedEngine = false
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
|
||||
setPrefix("templates/")
|
||||
setSuffix(".kts")
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
|
||||
setPrefix("templates/")
|
||||
setSuffix(".kts")
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example
|
||||
@@ -127,7 +125,7 @@ project for more details.
|
||||
== Kotlin multiplatform serialization
|
||||
|
||||
{kotlin-github-org}/kotlinx.serialization[Kotlin multiplatform serialization] is
|
||||
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, and ProtoBuf formats.
|
||||
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The built-in support currently targets CBOR, JSON, and ProtoBuf formats.
|
||||
|
||||
To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin.
|
||||
With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since
|
||||
@@ -135,6 +133,3 @@ Kotlin serialization is designed to serialize only Kotlin classes annotated with
|
||||
With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration,
|
||||
if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
= Spring JUnit 4 Testing Annotations
|
||||
|
||||
The following annotations are supported only when used in conjunction with the
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules]
|
||||
, or xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]:
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner],
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules], or
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]:
|
||||
|
||||
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-ifprofilevalue[`@IfProfileValue`]
|
||||
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`]
|
||||
|
||||
+215
-50
@@ -1,41 +1,77 @@
|
||||
[[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.
|
||||
[WARNING]
|
||||
====
|
||||
Using `@MockitoBean` or `@MockitoSpyBean` in conjunction with `@ContextHierarchy` can
|
||||
lead to undesirable results since each `@MockitoBean` or `@MockitoSpyBean` will be
|
||||
applied to all context hierarchy levels by default. To ensure that a particular
|
||||
`@MockitoBean` or `@MockitoSpyBean` is applied to a single context hierarchy level, set
|
||||
the `contextName` attribute to match a configured `@ContextConfiguration` name – for
|
||||
example, `@MockitoBean(contextName = "app-config")` or
|
||||
`@MockitoSpyBean(contextName = "app-config")`.
|
||||
|
||||
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`.
|
||||
See
|
||||
xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
|
||||
hierarchies with bean overrides] for further details and examples.
|
||||
====
|
||||
|
||||
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
|
||||
|
||||
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.
|
||||
|
||||
@@ -51,7 +87,20 @@ When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be
|
||||
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
|
||||
====
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
|
||||
[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]
|
||||
======
|
||||
@@ -59,23 +108,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]
|
||||
======
|
||||
@@ -83,20 +135,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]
|
||||
======
|
||||
@@ -104,14 +156,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
|
||||
@@ -119,7 +230,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]
|
||||
======
|
||||
@@ -127,13 +238,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.
|
||||
|
||||
+31
-7
@@ -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,27 @@ same bean in several tests, make sure to name the field consistently to avoid cr
|
||||
unnecessary contexts.
|
||||
====
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Using `@TestBean` in conjunction with `@ContextHierarchy` can lead to undesirable results
|
||||
since each `@TestBean` will be applied to all context hierarchy levels by default. To
|
||||
ensure that a particular `@TestBean` is applied to a single context hierarchy level, set
|
||||
the `contextName` attribute to match a configured `@ContextConfiguration` name – for
|
||||
example, `@TestBean(contextName = "app-config")`.
|
||||
|
||||
See
|
||||
xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
|
||||
hierarchies with bean overrides] for further details and examples.
|
||||
====
|
||||
|
||||
[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 +62,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 +90,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();
|
||||
}
|
||||
}
|
||||
@@ -84,8 +106,10 @@ Java::
|
||||
|
||||
[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>`
|
||||
|
||||
@@ -26,16 +26,16 @@ Java::
|
||||
|
||||
@Test
|
||||
void test() throws Exception {
|
||||
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
|
||||
.andExpect(status().isOk()) <1>
|
||||
.andExpect(request().asyncStarted()) <2>
|
||||
.andExpect(request().asyncResult("body")) <3>
|
||||
.andReturn();
|
||||
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
|
||||
.andExpect(status().isOk()) <1>
|
||||
.andExpect(request().asyncStarted()) <2>
|
||||
.andExpect(request().asyncResult("body")) <3>
|
||||
.andReturn();
|
||||
|
||||
this.mockMvc.perform(asyncDispatch(mvcResult)) <4>
|
||||
.andExpect(status().isOk()) <5>
|
||||
.andExpect(content().string("body"));
|
||||
}
|
||||
this.mockMvc.perform(asyncDispatch(mvcResult)) <4>
|
||||
.andExpect(status().isOk()) <5>
|
||||
.andExpect(content().string("body"));
|
||||
}
|
||||
----
|
||||
<1> Check response status is still unchanged
|
||||
<2> Async processing must have started
|
||||
|
||||
+6
-3
@@ -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`::
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
Once the TestContext framework loads an `ApplicationContext` (or `WebApplicationContext`)
|
||||
for a test, that context is cached and reused for all subsequent tests that declare the
|
||||
same unique context configuration within the same test suite. To understand how caching
|
||||
works, it is important to understand what is meant by "`unique`" and "`test suite.`"
|
||||
works, it is important to understand what is meant by "unique" and "test suite."
|
||||
|
||||
An `ApplicationContext` can be uniquely identified by the combination of configuration
|
||||
parameters that is used to load it. Consequently, the unique combination of configuration
|
||||
@@ -15,8 +15,8 @@ framework uses the following configuration parameters to build the context cache
|
||||
* `classes` (from `@ContextConfiguration`)
|
||||
* `contextInitializerClasses` (from `@ContextConfiguration`)
|
||||
* `contextCustomizers` (from `ContextCustomizerFactory`) – this includes
|
||||
`@DynamicPropertySource` methods as well as various features from Spring Boot's
|
||||
testing support such as `@MockBean` and `@SpyBean`.
|
||||
`@DynamicPropertySource` methods, bean overrides (such as `@TestBean`, `@MockitoBean`,
|
||||
`@MockitoSpyBean` etc.), as well as various features from Spring Boot's testing support.
|
||||
* `contextLoader` (from `@ContextConfiguration`)
|
||||
* `parent` (from `@ContextHierarchy`)
|
||||
* `activeProfiles` (from `@ActiveProfiles`)
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
[[testcontext-context-customizers]]
|
||||
= Configuration Configuration with Context Customizers
|
||||
= Context Configuration with Context Customizers
|
||||
|
||||
A `ContextCustomizer` is responsible for customizing the supplied
|
||||
`ConfigurableApplicationContext` after bean definitions have been loaded into the context
|
||||
|
||||
+126
-9
@@ -22,8 +22,19 @@ given level in the hierarchy, the configuration resource type (that is, XML conf
|
||||
files or component classes) must be consistent. Otherwise, it is perfectly acceptable to
|
||||
have different levels in a context hierarchy configured using different resource types.
|
||||
|
||||
The remaining JUnit Jupiter based examples in this section show common configuration
|
||||
scenarios for integration tests that require the use of context hierarchies.
|
||||
[NOTE]
|
||||
====
|
||||
If you use `@DirtiesContext` in a test whose context is configured as part of a context
|
||||
hierarchy, you can use the `hierarchyMode` flag to control how the context cache is
|
||||
cleared.
|
||||
|
||||
For further details, see the discussion of `@DirtiesContext` in
|
||||
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations]
|
||||
and the {spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
|
||||
====
|
||||
|
||||
The JUnit Jupiter based examples in this section show common configuration scenarios for
|
||||
integration tests that require the use of context hierarchies.
|
||||
|
||||
**Single test class with context hierarchy**
|
||||
--
|
||||
@@ -229,12 +240,118 @@ Kotlin::
|
||||
class ExtendedTests : BaseTests() {}
|
||||
----
|
||||
======
|
||||
|
||||
.Dirtying a context within a context hierarchy
|
||||
NOTE: If you use `@DirtiesContext` in a test whose context is configured as part of a
|
||||
context hierarchy, you can use the `hierarchyMode` flag to control how the context cache
|
||||
is cleared. For further details, see the discussion of `@DirtiesContext` in
|
||||
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] and the
|
||||
{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
|
||||
--
|
||||
|
||||
[[testcontext-ctx-management-ctx-hierarchies-with-bean-overrides]]
|
||||
**Context hierarchies with bean overrides**
|
||||
--
|
||||
When `@ContextHierarchy` is used in conjunction with
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc[bean overrides] such as
|
||||
`@TestBean`, `@MockitoBean`, or `@MockitoSpyBean`, it may be desirable or necessary to
|
||||
have the override applied to a single level in the context hierarchy. To achieve that,
|
||||
the bean override must specify a context name that matches a name configured via the
|
||||
`name` attribute in `@ContextConfiguration`.
|
||||
|
||||
The following test class configures the name of the second hierarchy level to be
|
||||
`"user-config"` and simultaneously specifies that the `UserService` should be wrapped in
|
||||
a Mockito spy in the context named `"user-config"`. Consequently, Spring will only
|
||||
attempt to create the spy in the `"user-config"` context and will not attempt to create
|
||||
the spy in the parent context.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextHierarchy({
|
||||
@ContextConfiguration(classes = AppConfig.class),
|
||||
@ContextConfiguration(classes = UserConfig.class, name = "user-config")
|
||||
})
|
||||
class IntegrationTests {
|
||||
|
||||
@MockitoSpyBean(contextName = "user-config")
|
||||
UserService userService;
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextHierarchy(
|
||||
ContextConfiguration(classes = [AppConfig::class]),
|
||||
ContextConfiguration(classes = [UserConfig::class], name = "user-config"))
|
||||
class IntegrationTests {
|
||||
|
||||
@MockitoSpyBean(contextName = "user-config")
|
||||
lateinit var userService: UserService
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
When applying bean overrides in different levels of the context hierarchy, you may need
|
||||
to have all of the bean override instances injected into the test class in order to
|
||||
interact with them — for example, to configure stubbing for mocks. However, `@Autowired`
|
||||
will always inject a matching bean found in the lowest level of the context hierarchy.
|
||||
Thus, to inject bean override instances from specific levels in the context hierarchy,
|
||||
you need to annotate fields with appropriate bean override annotations and configure the
|
||||
name of the context level.
|
||||
|
||||
The following test class configures the names of the hierarchy levels to be `"parent"`
|
||||
and `"child"`. It also declares two `PropertyService` fields that are configured to
|
||||
create or replace `PropertyService` beans with Mockito mocks in the respective contexts,
|
||||
named `"parent"` and `"child"`. Consequently, the mock from the `"parent"` context will
|
||||
be injected into the `propertyServiceInParent` field, and the mock from the `"child"`
|
||||
context will be injected into the `propertyServiceInChild` field.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension.class)
|
||||
@ContextHierarchy({
|
||||
@ContextConfiguration(classes = ParentConfig.class, name = "parent"),
|
||||
@ContextConfiguration(classes = ChildConfig.class, name = "child")
|
||||
})
|
||||
class IntegrationTests {
|
||||
|
||||
@MockitoBean(contextName = "parent")
|
||||
PropertyService propertyServiceInParent;
|
||||
|
||||
@MockitoBean(contextName = "child")
|
||||
PropertyService propertyServiceInChild;
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@ContextHierarchy(
|
||||
ContextConfiguration(classes = [ParentConfig::class], name = "parent"),
|
||||
ContextConfiguration(classes = [ChildConfig::class], name = "child"))
|
||||
class IntegrationTests {
|
||||
|
||||
@MockitoBean(contextName = "parent")
|
||||
lateinit var propertyServiceInParent: PropertyService
|
||||
|
||||
@MockitoBean(contextName = "child")
|
||||
lateinit var propertyServiceInChild: PropertyService
|
||||
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
--
|
||||
|
||||
+3
-3
@@ -18,9 +18,9 @@ Do not run tests in parallel if the tests:
|
||||
* Use Spring Framework's `@DirtiesContext` support.
|
||||
* Use Spring Framework's `@MockitoBean` or `@MockitoSpyBean` support.
|
||||
* Use Spring Boot's `@MockBean` or `@SpyBean` support.
|
||||
* Use JUnit 4's `@FixMethodOrder` support or any testing framework feature
|
||||
that is designed to ensure that test methods run in a particular order. Note,
|
||||
however, that this does not apply if entire test classes are run in parallel.
|
||||
* Use JUnit Jupiter's `@TestMethodOrder` support or any testing framework feature that is
|
||||
designed to ensure that test methods run in a particular order. Note, however, that
|
||||
this does not apply if entire test classes are run in parallel.
|
||||
* Change the state of shared services or systems such as a database, message broker,
|
||||
filesystem, and others. This applies to both embedded and external systems.
|
||||
|
||||
|
||||
+195
-178
@@ -1,166 +1,9 @@
|
||||
[[testcontext-support-classes]]
|
||||
= TestContext Framework Support Classes
|
||||
|
||||
This section describes the various classes that support the Spring TestContext Framework.
|
||||
This section describes the various classes that support the Spring TestContext Framework
|
||||
in JUnit and TestNG.
|
||||
|
||||
[[testcontext-junit4-runner]]
|
||||
== Spring JUnit 4 Runner
|
||||
|
||||
The Spring TestContext Framework offers full integration with JUnit 4 through a custom
|
||||
runner (supported on JUnit 4.12 or higher). By annotating test classes with
|
||||
`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
|
||||
variant, developers can implement standard JUnit 4-based unit and integration tests and
|
||||
simultaneously reap the benefits of the TestContext framework, such as support for
|
||||
loading application contexts, dependency injection of test instances, transactional test
|
||||
method execution, and so on. If you want to use the Spring TestContext Framework with an
|
||||
alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
|
||||
(such as the `MockitoJUnitRunner`), you can, optionally, use
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] instead.
|
||||
|
||||
The following code listing shows the minimal requirements for configuring a test class to
|
||||
run with the custom Spring `Runner`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@TestExecutionListeners({})
|
||||
public class SimpleTest {
|
||||
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner::class)
|
||||
@TestExecutionListeners
|
||||
class SimpleTest {
|
||||
|
||||
@Test
|
||||
fun testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
|
||||
disable the default listeners, which otherwise would require an `ApplicationContext` to
|
||||
be configured through `@ContextConfiguration`.
|
||||
|
||||
[[testcontext-junit4-rules]]
|
||||
== Spring JUnit 4 Rules
|
||||
|
||||
The `org.springframework.test.context.junit4.rules` package provides the following JUnit
|
||||
4 rules (supported on JUnit 4.12 or higher):
|
||||
|
||||
* `SpringClassRule`
|
||||
* `SpringMethodRule`
|
||||
|
||||
`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
|
||||
TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
|
||||
instance-level and method-level features of the Spring TestContext Framework.
|
||||
|
||||
In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
|
||||
being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
|
||||
combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
|
||||
third-party runners (such as the `MockitoJUnitRunner`).
|
||||
|
||||
To support the full functionality of the TestContext framework, you must combine a
|
||||
`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
|
||||
to declare these rules in an integration test:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Optionally specify a non-Spring Runner via @RunWith(...)
|
||||
@ContextConfiguration
|
||||
public class IntegrationTest {
|
||||
|
||||
@ClassRule
|
||||
public static final SpringClassRule springClassRule = new SpringClassRule();
|
||||
|
||||
@Rule
|
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule();
|
||||
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Optionally specify a non-Spring Runner via @RunWith(...)
|
||||
@ContextConfiguration
|
||||
class IntegrationTest {
|
||||
|
||||
@Rule
|
||||
val springMethodRule = SpringMethodRule()
|
||||
|
||||
@Test
|
||||
fun testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ClassRule
|
||||
val springClassRule = SpringClassRule()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[testcontext-support-classes-junit4]]
|
||||
== JUnit 4 Support Classes
|
||||
|
||||
The `org.springframework.test.context.junit4` package provides the following support
|
||||
classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
|
||||
|
||||
* `AbstractJUnit4SpringContextTests`
|
||||
* `AbstractTransactionalJUnit4SpringContextTests`
|
||||
|
||||
`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
|
||||
Spring TestContext Framework with explicit `ApplicationContext` testing support in a
|
||||
JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
|
||||
`protected` `applicationContext` instance variable that you can use to perform explicit
|
||||
bean lookups or to test the state of the context as a whole.
|
||||
|
||||
`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
|
||||
`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
|
||||
access. This class expects a `javax.sql.DataSource` bean and a
|
||||
`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
|
||||
extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
|
||||
`jdbcTemplate` instance variable that you can use to run SQL statements to query the
|
||||
database. You can use such queries to confirm database state both before and after
|
||||
running database-related application code, and Spring ensures that such queries run in
|
||||
the scope of the same transaction as the application code. When used in conjunction with
|
||||
an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
|
||||
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
|
||||
`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
|
||||
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
|
||||
Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
|
||||
`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
|
||||
|
||||
TIP: These classes are a convenience for extension. If you do not want your test classes
|
||||
to be tied to a Spring-specific class hierarchy, you can configure your own custom test
|
||||
classes by using `@RunWith(SpringRunner.class)` or xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules]
|
||||
.
|
||||
|
||||
[[testcontext-junit-jupiter-extension]]
|
||||
== SpringExtension for JUnit Jupiter
|
||||
@@ -177,14 +20,17 @@ following features above and beyond the feature set that Spring supports for JUn
|
||||
TestNG:
|
||||
|
||||
* Dependency injection for test constructors, test methods, and test lifecycle callback
|
||||
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details.
|
||||
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
|
||||
Injection with the `SpringExtension`] for further details.
|
||||
* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
|
||||
test execution] based on SpEL expressions, environment variables, system properties,
|
||||
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details and examples.
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
|
||||
for further details and examples.
|
||||
* Custom composed annotations that combine annotations from Spring and JUnit Jupiter. See
|
||||
the `@TransactionalDevTestConfig` and `@TransactionalIntegrationTest` examples in
|
||||
xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for further details.
|
||||
xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for
|
||||
further details.
|
||||
|
||||
The following code listing shows how to configure a test class to use the
|
||||
`SpringExtension` in conjunction with `@ContextConfiguration`:
|
||||
@@ -307,7 +153,8 @@ Kotlin::
|
||||
======
|
||||
|
||||
See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details.
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
|
||||
for further details.
|
||||
|
||||
[[testcontext-junit-jupiter-di]]
|
||||
=== Dependency Injection with the `SpringExtension`
|
||||
@@ -318,10 +165,9 @@ extension API from JUnit Jupiter, which lets Spring provide dependency injection
|
||||
constructors, test methods, and test lifecycle callback methods.
|
||||
|
||||
Specifically, the `SpringExtension` can inject dependencies from the test's
|
||||
`ApplicationContext` into test constructors and methods that are annotated with
|
||||
Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`,
|
||||
`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`,
|
||||
and others.
|
||||
`ApplicationContext` into test constructors and methods that are annotated with Spring's
|
||||
`@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@AfterAll`,
|
||||
`@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and others.
|
||||
|
||||
|
||||
[[testcontext-junit-jupiter-di-constructor]]
|
||||
@@ -341,8 +187,9 @@ autowirable if one of the following conditions is met (in order of precedence).
|
||||
attribute set to `ALL`.
|
||||
* The default _test constructor autowire mode_ has been changed to `ALL`.
|
||||
|
||||
See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] for details on the use of
|
||||
`@TestConstructor` and how to change the global _test constructor autowire mode_.
|
||||
See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]
|
||||
for details on the use of `@TestConstructor` and how to change the global _test
|
||||
constructor autowire mode_.
|
||||
|
||||
WARNING: If the constructor for a test class is considered to be _autowirable_, Spring
|
||||
assumes the responsibility for resolving arguments for all parameters in the constructor.
|
||||
@@ -407,8 +254,9 @@ Kotlin::
|
||||
Note that this feature lets test dependencies be `final` and therefore immutable.
|
||||
|
||||
If the `spring.test.constructor.autowire.mode` property is to `all` (see
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), we can omit the declaration of
|
||||
`@Autowired` on the constructor in the previous example, resulting in the following.
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]),
|
||||
we can omit the declaration of `@Autowired` on the constructor in the previous example,
|
||||
resulting in the following.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -553,17 +401,19 @@ honor `@NestedTestConfiguration` semantics.
|
||||
In order to allow development teams to change the default to `OVERRIDE` – for example,
|
||||
for compatibility with Spring Framework 5.0 through 5.2 – the default mode can be changed
|
||||
globally via a JVM system property or a `spring.properties` file in the root of the
|
||||
classpath. See the xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"]
|
||||
note for details.
|
||||
classpath. See the
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"]
|
||||
note for details.
|
||||
|
||||
Although the following "Hello World" example is very simplistic, it shows how to declare
|
||||
common configuration on a top-level class that is inherited by its `@Nested` test
|
||||
classes. In this particular example, only the `TestConfig` configuration class is
|
||||
inherited. Each nested test class provides its own set of active profiles, resulting in a
|
||||
distinct `ApplicationContext` for each nested test class (see
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). Consult the list of
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] to see
|
||||
which annotations can be inherited in `@Nested` test classes.
|
||||
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details).
|
||||
Consult the list of
|
||||
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations]
|
||||
to see which annotations can be inherited in `@Nested` test classes.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -626,8 +476,174 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
[[testcontext-junit4-support]]
|
||||
== JUnit 4 Support
|
||||
|
||||
[[testcontext-junit4-runner]]
|
||||
=== Spring JUnit 4 Runner
|
||||
|
||||
The Spring TestContext Framework offers full integration with JUnit 4 through a custom
|
||||
runner (supported on JUnit 4.12 or higher). By annotating test classes with
|
||||
`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
|
||||
variant, developers can implement standard JUnit 4-based unit and integration tests and
|
||||
simultaneously reap the benefits of the TestContext framework, such as support for
|
||||
loading application contexts, dependency injection of test instances, transactional test
|
||||
method execution, and so on. If you want to use the Spring TestContext Framework with an
|
||||
alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
|
||||
(such as the `MockitoJUnitRunner`), you can, optionally, use
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules]
|
||||
instead.
|
||||
|
||||
The following code listing shows the minimal requirements for configuring a test class to
|
||||
run with the custom Spring `Runner`:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner.class)
|
||||
@TestExecutionListeners({})
|
||||
public class SimpleTest {
|
||||
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@RunWith(SpringRunner::class)
|
||||
@TestExecutionListeners
|
||||
class SimpleTest {
|
||||
|
||||
@Test
|
||||
fun testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
|
||||
disable the default listeners, which otherwise would require an `ApplicationContext` to
|
||||
be configured through `@ContextConfiguration`.
|
||||
|
||||
[[testcontext-junit4-rules]]
|
||||
=== Spring JUnit 4 Rules
|
||||
|
||||
The `org.springframework.test.context.junit4.rules` package provides the following JUnit
|
||||
4 rules (supported on JUnit 4.12 or higher):
|
||||
|
||||
* `SpringClassRule`
|
||||
* `SpringMethodRule`
|
||||
|
||||
`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
|
||||
TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
|
||||
instance-level and method-level features of the Spring TestContext Framework.
|
||||
|
||||
In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
|
||||
being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
|
||||
combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
|
||||
third-party runners (such as the `MockitoJUnitRunner`).
|
||||
|
||||
To support the full functionality of the TestContext framework, you must combine a
|
||||
`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
|
||||
to declare these rules in an integration test:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Optionally specify a non-Spring Runner via @RunWith(...)
|
||||
@ContextConfiguration
|
||||
public class IntegrationTest {
|
||||
|
||||
@ClassRule
|
||||
public static final SpringClassRule springClassRule = new SpringClassRule();
|
||||
|
||||
@Rule
|
||||
public final SpringMethodRule springMethodRule = new SpringMethodRule();
|
||||
|
||||
@Test
|
||||
public void testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// Optionally specify a non-Spring Runner via @RunWith(...)
|
||||
@ContextConfiguration
|
||||
class IntegrationTest {
|
||||
|
||||
@Rule
|
||||
val springMethodRule = SpringMethodRule()
|
||||
|
||||
@Test
|
||||
fun testMethod() {
|
||||
// test logic...
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ClassRule
|
||||
val springClassRule = SpringClassRule()
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[[testcontext-support-classes-junit4]]
|
||||
=== JUnit 4 Base Classes
|
||||
|
||||
The `org.springframework.test.context.junit4` package provides the following support
|
||||
classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
|
||||
|
||||
* `AbstractJUnit4SpringContextTests`
|
||||
* `AbstractTransactionalJUnit4SpringContextTests`
|
||||
|
||||
`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
|
||||
Spring TestContext Framework with explicit `ApplicationContext` testing support in a
|
||||
JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
|
||||
`protected` `applicationContext` instance variable that you can use to perform explicit
|
||||
bean lookups or to test the state of the context as a whole.
|
||||
|
||||
`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
|
||||
`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
|
||||
access. This class expects a `javax.sql.DataSource` bean and a
|
||||
`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
|
||||
extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
|
||||
`jdbcTemplate` instance variable that you can use to run SQL statements to query the
|
||||
database. You can use such queries to confirm database state both before and after
|
||||
running database-related application code, and Spring ensures that such queries run in
|
||||
the scope of the same transaction as the application code. When used in conjunction with
|
||||
an ORM tool, be sure to avoid
|
||||
xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
|
||||
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
|
||||
`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
|
||||
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
|
||||
Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
|
||||
`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
|
||||
|
||||
TIP: These classes are a convenience for extension. If you do not want your test classes
|
||||
to be tied to a Spring-specific class hierarchy, you can configure your own custom test
|
||||
classes by using `@RunWith(SpringRunner.class)` or
|
||||
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules].
|
||||
|
||||
|
||||
[[testcontext-support-classes-testng]]
|
||||
== TestNG Support Classes
|
||||
== TestNG Support
|
||||
|
||||
The `org.springframework.test.context.testng` package provides the following support
|
||||
classes for TestNG based test cases:
|
||||
@@ -650,7 +666,8 @@ extend `AbstractTransactionalTestNGSpringContextTests`, you can access a `protec
|
||||
database. You can use such queries to confirm database state both before and after
|
||||
running database-related application code, and Spring ensures that such queries run in
|
||||
the scope of the same transaction as the application code. When used in conjunction with
|
||||
an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
|
||||
an ORM tool, be sure to avoid
|
||||
xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
|
||||
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
|
||||
`AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that
|
||||
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
|
||||
|
||||
@@ -7,16 +7,17 @@ by default, exactly in the following order:
|
||||
* `ServletTestExecutionListener`: Configures Servlet API mocks for a
|
||||
`WebApplicationContext`.
|
||||
* `DirtiesContextBeforeModesTestExecutionListener`: Handles the `@DirtiesContext`
|
||||
annotation for "`before`" modes.
|
||||
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[] .
|
||||
* `BeanOverrideTestExecutionListener`: Provides support for
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc[].
|
||||
* `DependencyInjectionTestExecutionListener`: Provides dependency injection for the test
|
||||
instance.
|
||||
* `MicrometerObservationRegistryTestExecutionListener`: Provides support for
|
||||
Micrometer's `ObservationRegistry`.
|
||||
* `DirtiesContextTestExecutionListener`: Handles the `@DirtiesContext` annotation for
|
||||
"`after`" modes.
|
||||
"after" modes.
|
||||
* `CommonCachesTestExecutionListener`: Clears resource caches in the test's
|
||||
`ApplicationContext` if necessary.
|
||||
* `TransactionalTestExecutionListener`: Provides transactional test execution with
|
||||
@@ -161,15 +162,16 @@ change from release to release -- for example, `SqlScriptsTestExecutionListener`
|
||||
introduced in Spring Framework 4.1, and `DirtiesContextBeforeModesTestExecutionListener`
|
||||
was introduced in Spring Framework 4.2. Furthermore, third-party frameworks like Spring
|
||||
Boot and Spring Security register their own default `TestExecutionListener`
|
||||
implementations by using the aforementioned xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-automatic-discovery[automatic discovery mechanism]
|
||||
.
|
||||
implementations by using the aforementioned
|
||||
xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-automatic-discovery[automatic discovery mechanism].
|
||||
|
||||
To avoid having to be aware of and re-declare all default listeners, you can set the
|
||||
`mergeMode` attribute of `@TestExecutionListeners` to `MergeMode.MERGE_WITH_DEFAULTS`.
|
||||
`MERGE_WITH_DEFAULTS` indicates that locally declared listeners should be merged with the
|
||||
default listeners. The merging algorithm ensures that duplicates are removed from the
|
||||
list and that the resulting set of merged listeners is sorted according to the semantics
|
||||
of `AnnotationAwareOrderComparator`, as described in xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-ordering[Ordering `TestExecutionListener` Implementations].
|
||||
of `AnnotationAwareOrderComparator`, as described in
|
||||
xref:testing/testcontext-framework/tel-config.adoc#testcontext-tel-config-ordering[Ordering `TestExecutionListener` Implementations].
|
||||
If a listener implements `Ordered` or is annotated with `@Order`, it can influence the
|
||||
position in which it is merged with the defaults. Otherwise, locally declared listeners
|
||||
are appended to the list of default listeners when merged.
|
||||
|
||||
@@ -47,8 +47,7 @@ out-of-container tests for code that depends on environment-specific properties.
|
||||
The `org.springframework.mock.web` package contains a comprehensive set of Servlet API
|
||||
mock objects that are useful for testing web contexts, controllers, and filters. These
|
||||
mock objects are targeted at usage with Spring's Web MVC framework and are generally more
|
||||
convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock])
|
||||
or alternative Servlet API mock objects (such as http://www.mockobjects.com[MockObjects]).
|
||||
convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock]).
|
||||
|
||||
TIP: Since Spring Framework 6.0, the mock objects in `org.springframework.mock.web` are
|
||||
based on the Servlet 6.0 API.
|
||||
|
||||
@@ -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
|
||||
@@ -396,7 +396,7 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
import org.springframework.test.web.reactive.server.expectBody
|
||||
|
||||
client.get().uri("/persons/1")
|
||||
.exchange()
|
||||
|
||||
@@ -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-view]]
|
||||
= View Technologies
|
||||
|
||||
[.small]#xref:web/webmvc-view.adoc[See equivalent in the Servlet stack]#
|
||||
|
||||
The rendering of views in Spring WebFlux is pluggable. Whether you decide to
|
||||
@@ -224,9 +225,9 @@ The following table shows the templating libraries that we have tested on differ
|
||||
|Scripting Library |Scripting Engine
|
||||
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://facebook.github.io/react/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.embeddedjs.com/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.stuartellis.name/articles/erb/[ERB] |https://www.jruby.org[JRuby]
|
||||
|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby]
|
||||
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|
||||
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin]
|
||||
|===
|
||||
|
||||
@@ -306,34 +306,34 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Resource resource = ...
|
||||
Mono<String> result = webClient
|
||||
.post()
|
||||
.uri("https://example.com")
|
||||
.body(Flux.concat(
|
||||
FormPartEvent.create("field", "field value"),
|
||||
FilePartEvent.create("file", resource)
|
||||
), PartEvent.class)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class);
|
||||
Resource resource = ...
|
||||
Mono<String> result = webClient
|
||||
.post()
|
||||
.uri("https://example.com")
|
||||
.body(Flux.concat(
|
||||
FormPartEvent.create("field", "field value"),
|
||||
FilePartEvent.create("file", resource)
|
||||
), PartEvent.class)
|
||||
.retrieve()
|
||||
.bodyToMono(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
var resource: Resource = ...
|
||||
var result: Mono<String> = webClient
|
||||
.post()
|
||||
.uri("https://example.com")
|
||||
.body(
|
||||
Flux.concat(
|
||||
FormPartEvent.create("field", "field value"),
|
||||
FilePartEvent.create("file", resource)
|
||||
var resource: Resource = ...
|
||||
var result: Mono<String> = webClient
|
||||
.post()
|
||||
.uri("https://example.com")
|
||||
.body(
|
||||
Flux.concat(
|
||||
FormPartEvent.create("field", "field value"),
|
||||
FilePartEvent.create("file", resource)
|
||||
)
|
||||
)
|
||||
)
|
||||
.retrieve()
|
||||
.bodyToMono()
|
||||
.retrieve()
|
||||
.bodyToMono()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -390,29 +390,29 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build();
|
||||
|
||||
ClientHttpConnector connector =
|
||||
new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory());
|
||||
ClientHttpConnector connector =
|
||||
new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory());
|
||||
|
||||
WebClient webClient = WebClient.builder().clientConnector(connector).build();
|
||||
WebClient webClient = WebClient.builder().clientConnector(connector).build();
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build()
|
||||
val httpClient = HttpClient.newBuilder()
|
||||
.followRedirects(Redirect.NORMAL)
|
||||
.connectTimeout(Duration.ofSeconds(20))
|
||||
.build()
|
||||
|
||||
val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory())
|
||||
val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory())
|
||||
|
||||
val webClient = WebClient.builder().clientConnector(connector).build()
|
||||
val webClient = WebClient.builder().clientConnector(connector).build()
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -158,65 +158,65 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
||||
if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
|
||||
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
|
||||
return next.exchange(ClientRequest.from(request).body((outputMessage, context) ->
|
||||
request.body().insert(new BufferingDecorator(outputMessage), context)).build()
|
||||
);
|
||||
} else {
|
||||
return next.exchange(request);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BufferingDecorator extends ClientHttpRequestDecorator {
|
||||
|
||||
private BufferingDecorator(ClientHttpRequest delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
return DataBufferUtils.join(body).flatMap(buffer -> {
|
||||
getHeaders().setContentLength(buffer.readableByteCount());
|
||||
return super.writeWith(Mono.just(buffer));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
|
||||
|
||||
@Override
|
||||
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
|
||||
if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
|
||||
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
|
||||
return next.exchange(ClientRequest.from(request).body((outputMessage, context) ->
|
||||
request.body().insert(new BufferingDecorator(outputMessage), context)).build()
|
||||
);
|
||||
} else {
|
||||
return next.exchange(request);
|
||||
}
|
||||
}
|
||||
|
||||
private static final class BufferingDecorator extends ClientHttpRequestDecorator {
|
||||
|
||||
private BufferingDecorator(ClientHttpRequest delegate) {
|
||||
super(delegate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
|
||||
return DataBufferUtils.join(body).flatMap(buffer -> {
|
||||
getHeaders().setContentLength(buffer.readableByteCount());
|
||||
return super.writeWith(Mono.just(buffer));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class MultipartExchangeFilterFunction : ExchangeFilterFunction {
|
||||
|
||||
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
|
||||
return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
|
||||
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
|
||||
next.exchange(ClientRequest.from(request)
|
||||
.body { message, context -> request.body().insert(BufferingDecorator(message), context) }
|
||||
.build())
|
||||
}
|
||||
else {
|
||||
next.exchange(request)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) {
|
||||
override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
|
||||
return DataBufferUtils.join(body)
|
||||
.flatMap {
|
||||
headers.contentLength = it.readableByteCount().toLong()
|
||||
super.writeWith(Mono.just(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class MultipartExchangeFilterFunction : ExchangeFilterFunction {
|
||||
|
||||
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
|
||||
return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
|
||||
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
|
||||
next.exchange(ClientRequest.from(request)
|
||||
.body { message, context -> request.body().insert(BufferingDecorator(message), context) }
|
||||
.build())
|
||||
}
|
||||
else {
|
||||
next.exchange(request)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) {
|
||||
override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
|
||||
return DataBufferUtils.join(body)
|
||||
.flatMap {
|
||||
headers.contentLength = it.readableByteCount().toLong()
|
||||
super.writeWith(Mono.just(it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
======
|
||||
======
|
||||
|
||||
@@ -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
|
||||
@@ -206,14 +207,14 @@ Kotlin::
|
||||
class ExampleHandler : WebSocketHandler {
|
||||
|
||||
override fun handle(session: WebSocketSession): Mono<Void> {
|
||||
return session.receive() // <1>
|
||||
return session.receive() // <1>
|
||||
.doOnNext {
|
||||
// ... // <2>
|
||||
}
|
||||
.concatMap {
|
||||
// ... // <3>
|
||||
}
|
||||
.then() // <4>
|
||||
.then() // <4>
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -267,16 +268,16 @@ Kotlin::
|
||||
|
||||
override fun handle(session: WebSocketSession): Mono<Void> {
|
||||
|
||||
val output = session.receive() // <1>
|
||||
val output = session.receive() // <1>
|
||||
.doOnNext {
|
||||
// ...
|
||||
}
|
||||
.concatMap {
|
||||
// ...
|
||||
}
|
||||
.map { session.textMessage("Echo $it") } // <2>
|
||||
.map { session.textMessage("Echo $it") } // <2>
|
||||
|
||||
return session.send(output) // <3>
|
||||
return session.send(output) // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -149,7 +149,7 @@ Java::
|
||||
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
|
||||
registrar.setUseIsoFormat(true);
|
||||
registrar.registerFormatters(registry);
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
+1
-1
@@ -62,7 +62,7 @@ Java::
|
||||
----
|
||||
class Account {
|
||||
|
||||
private final String firstName;
|
||||
private final String firstName;
|
||||
|
||||
public Account(@BindParam("first-name") String firstName) {
|
||||
this.firstName = firstName;
|
||||
|
||||
+2
-2
@@ -93,8 +93,8 @@ Java::
|
||||
----
|
||||
@ModelAttribute
|
||||
public void addAccount(@RequestParam String number) {
|
||||
Mono<Account> accountMono = accountRepository.findAccount(number);
|
||||
model.addAttribute("account", accountMono);
|
||||
Mono<Account> accountMono = accountRepository.findAccount(number);
|
||||
model.addAttribute("account", accountMono);
|
||||
}
|
||||
|
||||
@PostMapping("/accounts")
|
||||
|
||||
@@ -234,8 +234,8 @@ Kotlin::
|
||||
--
|
||||
|
||||
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
|
||||
through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. You can use this to, for example, parameterize a base URL based on
|
||||
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
|
||||
other property sources. You can use this, for example, to parameterize a base URL based on
|
||||
some external configuration.
|
||||
|
||||
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
|
||||
@@ -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].
|
||||
@@ -57,7 +57,7 @@ locale and language specific resource bundles.
|
||||
For further custom handling of method validation errors, you can extend
|
||||
`ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller
|
||||
or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly.
|
||||
The exception contains a list of``ParameterValidationResult``s that group validation errors
|
||||
The exception contains a list of ``ParameterValidationResult``s that group validation errors
|
||||
by method parameter. You can either iterate over those, or provide a visitor with callback
|
||||
methods by controller method parameter type:
|
||||
|
||||
@@ -104,21 +104,21 @@ Kotlin::
|
||||
|
||||
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -782,8 +782,8 @@ Java::
|
||||
----
|
||||
WebClient webClient = WebClient.builder()
|
||||
.codecs(configurer -> {
|
||||
CustomDecoder decoder = new CustomDecoder();
|
||||
configurer.customCodecs().registerWithDefaultConfig(decoder);
|
||||
CustomDecoder decoder = new CustomDecoder();
|
||||
configurer.customCodecs().registerWithDefaultConfig(decoder);
|
||||
})
|
||||
.build();
|
||||
----
|
||||
@@ -794,8 +794,8 @@ Kotlin::
|
||||
----
|
||||
val webClient = WebClient.builder()
|
||||
.codecs({ configurer ->
|
||||
val decoder = CustomDecoder()
|
||||
configurer.customCodecs().registerWithDefaultConfig(decoder)
|
||||
val decoder = CustomDecoder()
|
||||
configurer.customCodecs().registerWithDefaultConfig(decoder)
|
||||
})
|
||||
.build()
|
||||
----
|
||||
|
||||
@@ -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.
|
||||
@@ -275,7 +276,7 @@ ServerResponse.async(asyncResponse);
|
||||
----
|
||||
======
|
||||
|
||||
https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the
|
||||
https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events] can be provided via the
|
||||
static `sse` method on `ServerResponse`. The builder provided by that method
|
||||
allows you to send Strings, or other objects as JSON. For example:
|
||||
|
||||
@@ -780,7 +781,7 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
ClassPathResource index = new ClassPathResource("static/index.html");
|
||||
ClassPathResource index = new ClassPathResource("static/index.html");
|
||||
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
|
||||
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
|
||||
RouterFunction<ServerResponse> redirectToIndex = route()
|
||||
@@ -792,7 +793,7 @@ Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val redirectToIndex = router {
|
||||
val redirectToIndex = router {
|
||||
val index = ClassPathResource("static/index.html")
|
||||
val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
|
||||
val spaPredicate = !(path("/api/**") or path("/error") or
|
||||
@@ -813,16 +814,16 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Resource location = new FileUrlResource("public-resources/");
|
||||
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
|
||||
Resource location = new FileUrlResource("public-resources/");
|
||||
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val location = FileUrlResource("public-resources/")
|
||||
val resources = router { resources("/resources/**", location) }
|
||||
val location = FileUrlResource("public-resources/")
|
||||
val resources = router { resources("/resources/**", location) }
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
[[mvc-view]]
|
||||
= View Technologies
|
||||
:page-section-summary-toc: 1
|
||||
|
||||
[.small]#xref:web/webflux-view.adoc[See equivalent in the Reactive stack]#
|
||||
|
||||
The rendering of views in Spring MVC is pluggable. Whether you decide to use
|
||||
|
||||
@@ -130,6 +130,7 @@ the `Configuration` object.
|
||||
|
||||
[[mvc-view-freemarker-forms]]
|
||||
== Form Handling
|
||||
[.small]#xref:web/webflux-view.adoc#webflux-view-freemarker-forms[See equivalent in the Reactive stack]#
|
||||
|
||||
Spring provides a tag library for use in JSPs that contains, among others, a
|
||||
`<spring:bind/>` element. This element primarily lets forms display values from
|
||||
|
||||
@@ -13,9 +13,9 @@ templating libraries on different script engines:
|
||||
|Scripting Library |Scripting Engine
|
||||
|https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://facebook.github.io/react/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.embeddedjs.com/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://www.stuartellis.name/articles/erb/[ERB] |https://www.jruby.org[JRuby]
|
||||
|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn]
|
||||
|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby]
|
||||
|https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython]
|
||||
|https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin]
|
||||
|===
|
||||
@@ -47,7 +47,7 @@ through https://www.webjars.org/[WebJars].
|
||||
|
||||
[[mvc-view-script-integrate]]
|
||||
== Script Templates
|
||||
[.small]#xref:web/webflux-view.adoc#webflux-view-script[See equivalent in the Reactive stack]#
|
||||
[.small]#xref:web/webflux-view.adoc#webflux-view-script-integrate[See equivalent in the Reactive stack]#
|
||||
|
||||
You can declare a `ScriptTemplateConfigurer` bean to specify the script engine to use,
|
||||
the script files to load, what function to call to render templates, and so on.
|
||||
|
||||
@@ -30,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]#
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ invokes the configured exception resolvers and completes the request.
|
||||
=== SSE
|
||||
|
||||
`SseEmitter` (a subclass of `ResponseBodyEmitter`) provides support for
|
||||
https://www.w3.org/TR/eventsource/[Server-Sent Events], where events sent from the server
|
||||
https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events], where events sent from the server
|
||||
are formatted according to the W3C SSE specification. To produce an SSE
|
||||
stream from a controller, return `SseEmitter`, as the following example shows:
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ already set.
|
||||
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
|
||||
and also falls back on it if no compatible media type is found.
|
||||
|
||||
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
|
||||
To enable RFC 9457 responses for Spring MVC exceptions and for any
|
||||
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
|
||||
xref:web/webmvc/mvc-controller/ann-advice.adoc[@ControllerAdvice] in Spring configuration. The handler
|
||||
has an `@ExceptionHandler` method that handles any `ErrorResponse` exception, which
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ Java::
|
||||
----
|
||||
class Account {
|
||||
|
||||
private final String firstName;
|
||||
private final String firstName;
|
||||
|
||||
public Account(@BindParam("first-name") String firstName) {
|
||||
this.firstName = firstName;
|
||||
|
||||
@@ -57,7 +57,7 @@ locale and language specific resource bundles.
|
||||
For further custom handling of method validation errors, you can extend
|
||||
`ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller
|
||||
or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly.
|
||||
The exception contains a list of``ParameterValidationResult``s that group validation errors
|
||||
The exception contains a list of ``ParameterValidationResult``s that group validation errors
|
||||
by method parameter. You can either iterate over those, or provide a visitor with callback
|
||||
methods by controller method parameter type:
|
||||
|
||||
@@ -73,12 +73,12 @@ Java::
|
||||
|
||||
@Override
|
||||
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
|
||||
// ...
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
|
||||
// ...
|
||||
// ...
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,7 +88,7 @@ Java::
|
||||
|
||||
@Override
|
||||
public void other(ParameterValidationResult result) {
|
||||
// ...
|
||||
// ...
|
||||
}
|
||||
});
|
||||
----
|
||||
@@ -103,22 +103,22 @@ Kotlin::
|
||||
ex.visitResults(object : HandlerMethodValidationException.Visitor {
|
||||
|
||||
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
override fun other(result: ParameterValidationResult) {
|
||||
// ...
|
||||
}
|
||||
// ...
|
||||
}
|
||||
})
|
||||
----
|
||||
======
|
||||
|
||||
@@ -13,7 +13,7 @@ reference documentation, including:
|
||||
* {docs-spring-security}/features/exploits/csrf.html#csrf-protection[CSRF protection]
|
||||
* {docs-spring-security}/features/exploits/headers.html[Security Response Headers]
|
||||
|
||||
https://hdiv.org/[HDIV] is another web security framework that integrates with Spring MVC.
|
||||
https://github.com/hdiv/hdiv[HDIV] is another web security framework that integrates with Spring MVC.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -22,11 +22,11 @@ The following example code is based on it:
|
||||
[source,javascript,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
const stompClient = new StompJs.Client({
|
||||
brokerURL: 'ws://domain.com/portfolio',
|
||||
onConnect: () => {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
brokerURL: 'ws://domain.com/portfolio',
|
||||
onConnect: () => {
|
||||
// ...
|
||||
}
|
||||
});
|
||||
----
|
||||
|
||||
Alternatively, if you connect through SockJS, you can enable the
|
||||
@@ -47,5 +47,3 @@ interactive web application] -- a getting started guide.
|
||||
* https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample
|
||||
application.
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"@antora/collector-extension": "1.0.0-alpha.3",
|
||||
"@asciidoctor/tabs": "1.0.0-beta.6",
|
||||
"@springio/antora-extensions": "1.14.2",
|
||||
"fast-xml-parser": "4.5.2",
|
||||
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
|
||||
}
|
||||
}
|
||||
|
||||
+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[]
|
||||
public 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) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+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)
|
||||
}
|
||||
@@ -7,31 +7,31 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.0"))
|
||||
api(platform("io.netty:netty-bom:4.1.115.Final"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.7"))
|
||||
api(platform("io.netty:netty-bom:4.1.121.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.0"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.4"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.6"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.26"))
|
||||
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.15"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.15"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.3"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.0.21"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21"))
|
||||
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.2"))
|
||||
api(platform("org.junit:junit-bom:5.12.2"))
|
||||
api(platform("org.mockito:mockito-bom:5.17.0"))
|
||||
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.2")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.0")
|
||||
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.11.0")
|
||||
api("com.google.protobuf:protobuf-java-util:4.28.3")
|
||||
api("com.google.code.gson:gson:2.12.1")
|
||||
api("com.google.protobuf:protobuf-java-util:4.30.2")
|
||||
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")
|
||||
@@ -43,7 +43,7 @@ dependencies {
|
||||
api("com.sun.xml.bind:jaxb-core:3.0.2")
|
||||
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.qdox:qdox:2.2.0")
|
||||
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")
|
||||
@@ -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.9")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.10")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.18.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.17.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.17.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.1")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.1")
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.4.4")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
|
||||
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")
|
||||
@@ -110,18 +110,18 @@ dependencies {
|
||||
api("org.aspectj:aspectjrt:1.9.22.1")
|
||||
api("org.aspectj:aspectjtools:1.9.22.1")
|
||||
api("org.aspectj:aspectjweaver:1.9.22.1")
|
||||
api("org.awaitility:awaitility:4.2.2")
|
||||
api("org.awaitility:awaitility:4.3.0")
|
||||
api("org.bouncycastle:bcpkix-jdk18on:1.72")
|
||||
api("org.codehaus.jettison:jettison:1.5.4")
|
||||
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.8")
|
||||
api("org.easymock:easymock:5.5.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.9")
|
||||
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")
|
||||
api("org.ehcache:jcache:1.0.1")
|
||||
api("org.freemarker:freemarker:2.3.33")
|
||||
api("org.freemarker:freemarker:2.3.34")
|
||||
api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea")
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3")
|
||||
@@ -130,24 +130,24 @@ dependencies {
|
||||
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.4")
|
||||
api("org.htmlunit:htmlunit:4.6.0")
|
||||
api("org.htmlunit:htmlunit:4.10.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
api("org.jruby:jruby:9.4.9.0")
|
||||
api("org.jruby:jruby:9.4.12.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.python:jython-standalone:2.7.4")
|
||||
api("org.quartz-scheduler:quartz:2.3.2")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.26.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.26.0")
|
||||
api("org.seleniumhq.selenium:htmlunit3-driver:4.29.0")
|
||||
api("org.seleniumhq.selenium:selenium-java:4.29.0")
|
||||
api("org.skyscreamer:jsonassert:1.5.3")
|
||||
api("org.slf4j:slf4j-api:2.0.16")
|
||||
api("org.testng:testng:7.10.2")
|
||||
api("org.slf4j:slf4j-api:2.0.17")
|
||||
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.webjars:webjars-locator-core:0.59")
|
||||
api("org.webjars:webjars-locator-lite:1.1.0")
|
||||
api("org.xmlunit:xmlunit-assertj:2.10.0")
|
||||
api("org.xmlunit:xmlunit-matchers:2.10.0")
|
||||
api("org.yaml:snakeyaml:2.3")
|
||||
api("org.yaml:snakeyaml:2.4")
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user