mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
385 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 90f9c0929b | |||
| 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 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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.2");
|
||||
checkstyle.setToolVersion("10.23.0");
|
||||
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();
|
||||
|
||||
@@ -27,6 +27,7 @@ import org.gradle.api.attributes.Usage;
|
||||
import org.gradle.api.attributes.java.TargetJvmVersion;
|
||||
import org.gradle.api.plugins.JavaPlugin;
|
||||
import org.gradle.api.plugins.jvm.JvmTestSuite;
|
||||
import org.gradle.api.tasks.TaskProvider;
|
||||
import org.gradle.api.tasks.testing.Test;
|
||||
import org.gradle.testing.base.TestingExtension;
|
||||
|
||||
@@ -52,7 +53,7 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
TestingExtension testing = project.getExtensions().getByType(TestingExtension.class);
|
||||
JvmTestSuite jvmTestSuite = (JvmTestSuite) testing.getSuites().getByName("test");
|
||||
RuntimeHintsAgentExtension agentExtension = createRuntimeHintsAgentExtension(project);
|
||||
Test agentTest = project.getTasks().create(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
|
||||
TaskProvider<Test> agentTest = project.getTasks().register(RUNTIMEHINTS_TEST_TASK, Test.class, test -> {
|
||||
test.useJUnitPlatform(options -> {
|
||||
options.includeTags("RuntimeHintsTests");
|
||||
});
|
||||
@@ -63,7 +64,7 @@ public class RuntimeHintsAgentPlugin implements Plugin<Project> {
|
||||
test.setClasspath(jvmTestSuite.getSources().getRuntimeClasspath());
|
||||
test.getJvmArgumentProviders().add(createRuntimeHintsAgentArgumentProvider(project, agentExtension));
|
||||
});
|
||||
project.getTasks().getByName("check", task -> task.dependsOn(agentTest));
|
||||
project.getTasks().named("check", task -> task.dependsOn(agentTest));
|
||||
project.getDependencies().add(CONFIGURATION_NAME, project.project(":spring-core-test"));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ apply from: "${rootDir}/gradle/publications.gradle"
|
||||
|
||||
repositories {
|
||||
maven {
|
||||
url "https://repo.spring.io/release"
|
||||
url = "https://repo.spring.io/release"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,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 +88,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();
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -92,6 +92,12 @@ 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`]
|
||||
|
||||
@@ -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
|
||||
|
||||
+42
-42
@@ -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.
|
||||
@@ -119,15 +119,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 +150,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 +188,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 +217,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")
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]]
|
||||
@@ -285,7 +286,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
|===
|
||||
|Name | Description
|
||||
|`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. Only the path part of the URI is considered.
|
||||
|`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.
|
||||
@@ -313,7 +314,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
|===
|
||||
|Name | Description
|
||||
|`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. Only the path part of the URI is considered.
|
||||
|`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.
|
||||
@@ -342,7 +343,7 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien
|
||||
|===
|
||||
|Name | Description
|
||||
|`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. Only the path part of the URI is considered.
|
||||
|`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
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -296,17 +296,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 +340,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 +369,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 +404,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.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
+199
-44
@@ -1,42 +1,77 @@
|
||||
[[spring-testing-annotation-beanoverriding-mockitobean]]
|
||||
= `@MockitoBean` and `@MockitoSpyBean`
|
||||
|
||||
`@MockitoBean` and `@MockitoSpyBean` are used on non-static 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.
|
||||
|
||||
@@ -61,7 +96,11 @@ Such fields can therefore be `public`, `protected`, package-private (default vis
|
||||
or `private` depending on the needs or coding practices of the project.
|
||||
====
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
|
||||
[[spring-testing-annotation-beanoverriding-mockitobean-examples]]
|
||||
== `@MockitoBean` Examples
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean`
|
||||
annotation.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -69,23 +108,26 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoBean // <1>
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
// 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]
|
||||
======
|
||||
@@ -93,20 +135,20 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoBean("service") // <1>
|
||||
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]
|
||||
======
|
||||
@@ -114,14 +156,73 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@MockitoBean(types = {OrderService.class, UserService.class}) // <1>
|
||||
@MockitoBean(name = "ps1", types = PrintingService.class) // <2>
|
||||
public @interface SharedMocks {
|
||||
}
|
||||
----
|
||||
<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;
|
||||
|
||||
// test case body...
|
||||
// tests...
|
||||
}
|
||||
----
|
||||
<1> Wrap the bean with type `CustomService` with a Mockito `spy`.
|
||||
<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
|
||||
@@ -129,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]
|
||||
======
|
||||
@@ -137,13 +238,67 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@SpringJUnitConfig(TestConfig.class)
|
||||
class BeanOverrideTests {
|
||||
|
||||
@MockitoSpyBean("service") // <1>
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
// 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.
|
||||
|
||||
+20
-4
@@ -1,8 +1,9 @@
|
||||
[[spring-testing-annotation-beanoverriding-testbean]]
|
||||
= `@TestBean`
|
||||
|
||||
`@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.
|
||||
{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
|
||||
bean name if specified. The factory method must be `static`, accept no arguments, and
|
||||
@@ -30,6 +31,19 @@ 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.
|
||||
@@ -92,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
|
||||
|
||||
+7
-6
@@ -2,8 +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 non-static 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`
|
||||
@@ -42,15 +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 non-static field that
|
||||
is meta-annotated with `@BeanOverride` and instantiates the corresponding
|
||||
`BeanOverrideProcessor` which is responsible for creating an appropriate
|
||||
`BeanOverrideHandler`.
|
||||
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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.2"))
|
||||
api(platform("io.netty:netty-bom:4.1.115.Final"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.3"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.5"))
|
||||
api(platform("io.netty:netty-bom:4.1.119.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.1"))
|
||||
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.4"))
|
||||
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.18"))
|
||||
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18"))
|
||||
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")
|
||||
@@ -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.3")
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.2.1
|
||||
version=6.2.6
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
+2
-2
@@ -98,7 +98,7 @@ if (project.name == "spring-oxm") {
|
||||
}
|
||||
|
||||
// Include project specific settings
|
||||
task eclipseSettings(type: Copy) {
|
||||
tasks.register('eclipseSettings', Copy) {
|
||||
from rootProject.files(
|
||||
'src/eclipse/org.eclipse.core.resources.prefs',
|
||||
'src/eclipse/org.eclipse.jdt.core.prefs',
|
||||
@@ -107,7 +107,7 @@ task eclipseSettings(type: Copy) {
|
||||
outputs.upToDateWhen { false }
|
||||
}
|
||||
|
||||
task cleanEclipseSettings(type: Delete) {
|
||||
tasks.register('cleanEclipseSettings', Delete) {
|
||||
delete project.file('.settings/org.eclipse.core.resources.prefs')
|
||||
delete project.file('.settings/org.eclipse.jdt.core.prefs')
|
||||
delete project.file('.settings/org.eclipse.jdt.ui.prefs')
|
||||
|
||||
+25
-26
@@ -70,31 +70,30 @@ normalization {
|
||||
javadoc {
|
||||
description = "Generates project-level javadoc for use in -javadoc jar"
|
||||
|
||||
options.encoding = "UTF-8"
|
||||
options.memberLevel = JavadocMemberLevel.PROTECTED
|
||||
options.author = true
|
||||
options.header = project.name
|
||||
options.use = true
|
||||
options.links(project.ext.javadocLinks)
|
||||
// Check for syntax during linting. 'none' doesn't seem to work in suppressing
|
||||
// all linting warnings all the time (see/link references most notably).
|
||||
options.addStringOption("Xdoclint:syntax", "-quiet")
|
||||
|
||||
// Suppress warnings due to cross-module @see and @link references.
|
||||
// Note that global 'api' task does display all warnings, and
|
||||
// checks for 'reference' on top of 'syntax'.
|
||||
logging.captureStandardError LogLevel.INFO
|
||||
logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message
|
||||
options {
|
||||
encoding = "UTF-8"
|
||||
memberLevel = JavadocMemberLevel.PROTECTED
|
||||
author = true
|
||||
header = project.name
|
||||
use = true
|
||||
links(project.ext.javadocLinks)
|
||||
// Check for 'syntax' during linting. Note that the global
|
||||
// 'framework-api:javadoc' task checks for 'reference' in addition
|
||||
// to 'syntax'.
|
||||
addBooleanOption("Xdoclint:syntax,-reference", true)
|
||||
addBooleanOption('Werror', true) // fail build on Javadoc warnings
|
||||
}
|
||||
}
|
||||
|
||||
task sourcesJar(type: Jar, dependsOn: classes) {
|
||||
tasks.register('sourcesJar', Jar) {
|
||||
dependsOn classes
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
archiveClassifier.set("sources")
|
||||
from sourceSets.main.allSource
|
||||
// Don't include or exclude anything explicitly by default. See SPR-12085.
|
||||
}
|
||||
|
||||
task javadocJar(type: Jar) {
|
||||
tasks.register('javadocJar', Jar) {
|
||||
archiveClassifier.set("javadoc")
|
||||
from javadoc
|
||||
}
|
||||
@@ -114,16 +113,16 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesApiElem
|
||||
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
|
||||
|
||||
tasks.withType(JavaCompile).configureEach {
|
||||
options.errorprone {
|
||||
disableAllChecks = true
|
||||
option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract")
|
||||
option("NullAway:AnnotatedPackages", "org.springframework")
|
||||
option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," +
|
||||
options.errorprone {
|
||||
disableAllChecks = true
|
||||
option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract")
|
||||
option("NullAway:AnnotatedPackages", "org.springframework")
|
||||
option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," +
|
||||
"org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," +
|
||||
"org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature")
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.compileJava {
|
||||
// The check defaults to a warning, bump it up to an error for the main sources
|
||||
options.errorprone.error("NullAway")
|
||||
}
|
||||
// The check defaults to a warning, bump it up to an error for the main sources
|
||||
options.errorprone.error("NullAway")
|
||||
}
|
||||
|
||||
Vendored
BIN
Binary file not shown.
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -86,8 +86,7 @@ done
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||
' "$PWD" ) || exit
|
||||
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
@@ -206,7 +205,7 @@ fi
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "com.gradle.develocity" version "3.17.2"
|
||||
id "com.gradle.develocity" version "3.19"
|
||||
id "io.spring.ge.conventions" version "0.0.17"
|
||||
id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0"
|
||||
}
|
||||
|
||||
+13
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -276,14 +276,18 @@ public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedence
|
||||
}
|
||||
if (this.aspectJAdviceMethod.getParameterCount() == this.argumentNames.length + 1) {
|
||||
// May need to add implicit join point arg name...
|
||||
Class<?> firstArgType = this.aspectJAdviceMethod.getParameterTypes()[0];
|
||||
if (firstArgType == JoinPoint.class ||
|
||||
firstArgType == ProceedingJoinPoint.class ||
|
||||
firstArgType == JoinPoint.StaticPart.class) {
|
||||
String[] oldNames = this.argumentNames;
|
||||
this.argumentNames = new String[oldNames.length + 1];
|
||||
this.argumentNames[0] = "THIS_JOIN_POINT";
|
||||
System.arraycopy(oldNames, 0, this.argumentNames, 1, oldNames.length);
|
||||
for (int i = 0; i < this.aspectJAdviceMethod.getParameterCount(); i++) {
|
||||
Class<?> argType = this.aspectJAdviceMethod.getParameterTypes()[i];
|
||||
if (argType == JoinPoint.class ||
|
||||
argType == ProceedingJoinPoint.class ||
|
||||
argType == JoinPoint.StaticPart.class) {
|
||||
String[] oldNames = this.argumentNames;
|
||||
this.argumentNames = new String[oldNames.length + 1];
|
||||
System.arraycopy(oldNames, 0, this.argumentNames, 0, i);
|
||||
this.argumentNames[i] = "THIS_JOIN_POINT";
|
||||
System.arraycopy(oldNames, i, this.argumentNames, i + 1, oldNames.length - i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -241,7 +241,7 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
|
||||
|
||||
try {
|
||||
int algorithmicStep = STEP_JOIN_POINT_BINDING;
|
||||
while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) {
|
||||
while (this.numberOfRemainingUnboundArguments > 0 && algorithmicStep < STEP_FINISHED) {
|
||||
switch (algorithmicStep++) {
|
||||
case STEP_JOIN_POINT_BINDING -> {
|
||||
if (!maybeBindThisJoinPoint()) {
|
||||
@@ -373,7 +373,8 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
|
||||
if (this.returningName != null) {
|
||||
if (this.numberOfRemainingUnboundArguments > 1) {
|
||||
throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName +
|
||||
"' is ambiguous: there are " + this.numberOfRemainingUnboundArguments + " candidates.");
|
||||
"' is ambiguous: there are " + this.numberOfRemainingUnboundArguments + " candidates. " +
|
||||
"Consider compiling with -parameters in order to make declared parameter names available.");
|
||||
}
|
||||
|
||||
// We're all set... find the unbound parameter, and bind it.
|
||||
@@ -485,8 +486,8 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
|
||||
*/
|
||||
private void maybeBindThisOrTargetOrArgsFromPointcutExpression() {
|
||||
if (this.numberOfRemainingUnboundArguments > 1) {
|
||||
throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments
|
||||
+ " unbound args at this()/target()/args() binding stage, with no way to determine between them");
|
||||
throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments +
|
||||
" unbound args at this()/target()/args() binding stage, with no way to determine between them");
|
||||
}
|
||||
|
||||
List<String> varNames = new ArrayList<>();
|
||||
@@ -535,8 +536,8 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
|
||||
|
||||
private void maybeBindReferencePointcutParameter() {
|
||||
if (this.numberOfRemainingUnboundArguments > 1) {
|
||||
throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments
|
||||
+ " unbound args at reference pointcut binding stage, with no way to determine between them");
|
||||
throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments +
|
||||
" unbound args at reference pointcut binding stage, with no way to determine between them");
|
||||
}
|
||||
|
||||
List<String> varNames = new ArrayList<>();
|
||||
@@ -741,7 +742,9 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov
|
||||
* Simple record to hold the extracted text from a pointcut body, together
|
||||
* with the number of tokens consumed in extracting it.
|
||||
*/
|
||||
private record PointcutBody(int numTokensConsumed, @Nullable String text) {}
|
||||
private record PointcutBody(int numTokensConsumed, @Nullable String text) {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Thrown in response to an ambiguous binding being detected when
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -111,7 +111,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
|
||||
* Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given
|
||||
* {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances,
|
||||
* for bean pointcut handling as well as consistent {@link ClassLoader} resolution.
|
||||
* @param beanFactory the BeanFactory to propagate (may be {@code null}}
|
||||
* @param beanFactory the BeanFactory to propagate (may be {@code null})
|
||||
* @since 4.3.6
|
||||
* @see AspectJExpressionPointcut#setBeanFactory
|
||||
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader()
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,8 +46,8 @@ public class AspectEntry implements ParseState.Entry {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Aspect: " + (StringUtils.hasLength(this.id) ? "id='" + this.id + "'"
|
||||
: "ref='" + this.ref + "'");
|
||||
return "Aspect: " + (StringUtils.hasLength(this.id) ? "id='" + this.id + "'" :
|
||||
"ref='" + this.ref + "'");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -35,6 +35,7 @@ import org.springframework.aop.AopInvocationException;
|
||||
import org.springframework.aop.RawTargetAccess;
|
||||
import org.springframework.aop.TargetSource;
|
||||
import org.springframework.aop.support.AopUtils;
|
||||
import org.springframework.aot.AotDetector;
|
||||
import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy;
|
||||
import org.springframework.cglib.core.CodeGenerationException;
|
||||
import org.springframework.cglib.core.GeneratorStrategy;
|
||||
@@ -205,7 +206,7 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
enhancer.setSuperclass(proxySuperClass);
|
||||
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
|
||||
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
|
||||
enhancer.setAttemptLoad(true);
|
||||
enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts());
|
||||
enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ?
|
||||
new ClassLoaderAwareGeneratorStrategy(classLoader) :
|
||||
new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy)
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -114,10 +114,10 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator {
|
||||
boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass);
|
||||
for (String mappedName : this.beanNames) {
|
||||
if (isFactoryBean) {
|
||||
if (!mappedName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
|
||||
if (mappedName.isEmpty() || mappedName.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) {
|
||||
continue;
|
||||
}
|
||||
mappedName = mappedName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
|
||||
mappedName = mappedName.substring(1); // length of '&'
|
||||
}
|
||||
if (isMatch(beanName, mappedName)) {
|
||||
return true;
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -125,7 +125,7 @@ public class AsyncExecutionInterceptor extends AsyncExecutionAspectSupport imple
|
||||
return null;
|
||||
};
|
||||
|
||||
return doSubmit(task, executor, invocation.getMethod().getReturnType());
|
||||
return doSubmit(task, executor, userMethod.getReturnType());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -198,8 +198,8 @@ public abstract class ClassFilters {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (this == other || (other instanceof NegateClassFilter that
|
||||
&& this.original.equals(that.original)));
|
||||
return (this == other || (other instanceof NegateClassFilter that &&
|
||||
this.original.equals(that.original)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -378,8 +378,8 @@ public abstract class MethodMatchers {
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
return (this == other || (other instanceof NegateMethodMatcher that
|
||||
&& this.original.equals(that.original)));
|
||||
return (this == other || (other instanceof NegateMethodMatcher that &&
|
||||
this.original.equals(that.original)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+135
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* Copyright 2002-2025 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.aspectj;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.aspectj.lang.JoinPoint;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.assertj.core.api.InstanceOfAssertFactories;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AbstractAspectJAdvice}.
|
||||
*
|
||||
* @author Joshua Chen
|
||||
* @author Stephane Nicoll
|
||||
*/
|
||||
class AbstractAspectJAdviceTests {
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withoutJoinPointParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithNoJoinPoint");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsFirstParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsFirstParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsLastParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsLastParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "arg2", "THIS_JOIN_POINT"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withJoinPointAsMiddleParameter() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithJoinPointAsMiddleParameter");
|
||||
assertThat(advice).satisfies(hasArgumentNames("arg1", "THIS_JOIN_POINT", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withProceedingJoinPoint() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithProceedingJoinPoint");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setArgumentNamesFromStringArray_withStaticPart() {
|
||||
AbstractAspectJAdvice advice = getAspectJAdvice("methodWithStaticPart");
|
||||
assertThat(advice).satisfies(hasArgumentNames("THIS_JOIN_POINT", "arg1", "arg2"));
|
||||
}
|
||||
|
||||
private Consumer<AbstractAspectJAdvice> hasArgumentNames(String... argumentNames) {
|
||||
return advice -> assertThat(advice).extracting("argumentNames")
|
||||
.asInstanceOf(InstanceOfAssertFactories.array(String[].class))
|
||||
.containsExactly(argumentNames);
|
||||
}
|
||||
|
||||
private AbstractAspectJAdvice getAspectJAdvice(final String methodName) {
|
||||
AbstractAspectJAdvice advice = new TestAspectJAdvice(getMethod(methodName),
|
||||
mock(AspectJExpressionPointcut.class), mock(AspectInstanceFactory.class));
|
||||
advice.setArgumentNamesFromStringArray("arg1", "arg2");
|
||||
return advice;
|
||||
}
|
||||
|
||||
private Method getMethod(final String methodName) {
|
||||
return Arrays.stream(Sample.class.getDeclaredMethods())
|
||||
.filter(method -> method.getName().equals(methodName)).findFirst()
|
||||
.orElseThrow();
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public static class TestAspectJAdvice extends AbstractAspectJAdvice {
|
||||
|
||||
public TestAspectJAdvice(Method aspectJAdviceMethod, AspectJExpressionPointcut pointcut,
|
||||
AspectInstanceFactory aspectInstanceFactory) {
|
||||
super(aspectJAdviceMethod, pointcut, aspectInstanceFactory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isBeforeAdvice() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAfterAdvice() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
static class Sample {
|
||||
|
||||
void methodWithNoJoinPoint(String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsFirstParameter(JoinPoint joinPoint, String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsLastParameter(String arg1, String arg2, JoinPoint joinPoint) {
|
||||
}
|
||||
|
||||
void methodWithJoinPointAsMiddleParameter(String arg1, JoinPoint joinPoint, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithProceedingJoinPoint(ProceedingJoinPoint joinPoint, String arg1, String arg2) {
|
||||
}
|
||||
|
||||
void methodWithStaticPart(JoinPoint.StaticPart staticPart, String arg1, String arg2) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user