mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-18 18:22:23 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82a6b40b93 | ||
|
|
a7b1b59cbd | ||
|
|
996e3d3f18 | ||
|
|
73f5ddddcd | ||
|
|
675f25de72 | ||
|
|
692dbc9160 | ||
|
|
8647e90bc7 | ||
|
|
b9379e33d5 | ||
|
|
a784dbe286 | ||
|
|
3b492f3908 | ||
|
|
07cbd482a0 | ||
|
|
dadd474d21 | ||
|
|
0d08f8dfaf | ||
|
|
baae93f20a | ||
|
|
d186b381b9 | ||
|
|
ac0f8be0d8 | ||
|
|
6e3dc633f0 | ||
|
|
35921cc01f | ||
|
|
062032373e | ||
|
|
1994e0ebd0 | ||
|
|
dc7fc89aec | ||
|
|
f2a7f13d13 | ||
|
|
f8a2bdad87 | ||
|
|
9aeda49273 | ||
|
|
abd323d428 | ||
|
|
176bc2a133 | ||
|
|
8df51ad6cc | ||
|
|
957df686c4 | ||
|
|
11bb7b54e5 | ||
|
|
70ca103b22 | ||
|
|
7f1966f5f5 | ||
|
|
b556766e1a | ||
|
|
0c281fd1ce | ||
|
|
63f0894621 | ||
|
|
0c966029e2 | ||
|
|
05619b7450 | ||
|
|
f3ba3c9e1f | ||
|
|
89e62e7e31 | ||
|
|
e4d5ec9c0d | ||
|
|
f5564e7e31 | ||
|
|
6d04ea9e84 | ||
|
|
0f5bd82c5d | ||
|
|
a894818c9e | ||
|
|
9b42a40a2a | ||
|
|
719311f09b | ||
|
|
28a78170b5 | ||
|
|
8fa7d88a0c | ||
|
|
b1d025d2c6 | ||
|
|
181a5d3403 | ||
|
|
ae4214aa95 | ||
|
|
4f086322d0 | ||
|
|
e255ccca7d | ||
|
|
b5de644cbb | ||
|
|
7de2b24d81 | ||
|
|
b90624472e | ||
|
|
1600e2479b | ||
|
|
233725c8f5 | ||
|
|
8fb2f72282 | ||
|
|
a2feb9ffe6 | ||
|
|
27aa9e46c5 | ||
|
|
56d706d591 | ||
|
|
bbfe6a0473 | ||
|
|
13a43e76cb | ||
|
|
16e82cd693 | ||
|
|
516a2ca511 | ||
|
|
9726c7ed5f | ||
|
|
7e0818f4e9 | ||
|
|
ed13afa0d4 | ||
|
|
c08e5e9c1c | ||
|
|
97e9ddb2d7 | ||
|
|
f0e69a702c | ||
|
|
d1470bbb25 | ||
|
|
347f23a73f | ||
|
|
99b3e205d1 | ||
|
|
78f05d8f8e | ||
|
|
996b337f37 | ||
|
|
66ffc04fe5 | ||
|
|
ade96d275d | ||
|
|
072fa3f43d | ||
|
|
78dcdab3fc | ||
|
|
872b1addeb | ||
|
|
98d552f15f | ||
|
|
4d6e88dc98 | ||
|
|
0044c4c8a3 | ||
|
|
b00f691655 | ||
|
|
6ac642e301 | ||
|
|
11bdb43aad | ||
|
|
4074155d76 | ||
|
|
1277279527 | ||
|
|
d0331a049a | ||
|
|
7de9c8d58a | ||
|
|
bae022a0fc | ||
|
|
325c3e1ec7 | ||
|
|
67e5ea9509 | ||
|
|
41b8f13e91 | ||
|
|
167afd91be | ||
|
|
12f9a5c2a5 | ||
|
|
1a89363a5c | ||
|
|
34a307d4a6 | ||
|
|
0e4842062b | ||
|
|
76089feeb4 | ||
|
|
ad5bd67b31 | ||
|
|
0ee636af7f | ||
|
|
c64f2ee104 | ||
|
|
5059bbd58e | ||
|
|
cc0ca1b6a5 | ||
|
|
ee81785afc | ||
|
|
846a6a8f7c | ||
|
|
0d706f8da6 | ||
|
|
924849f55b | ||
|
|
136f78ebd0 | ||
|
|
0fc724b348 | ||
|
|
292959e2ca | ||
|
|
5383520388 | ||
|
|
0b5a9ea30a | ||
|
|
298e1db625 | ||
|
|
3f7f75abdb |
@@ -1,20 +0,0 @@
|
||||
name: Await HTTP Resource
|
||||
description: 'Waits for an HTTP resource to be available (a HEAD request succeeds)'
|
||||
inputs:
|
||||
url:
|
||||
description: 'URL of the resource to await'
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Await HTTP resource
|
||||
shell: bash
|
||||
run: |
|
||||
url=${{ inputs.url }}
|
||||
echo "Waiting for $url"
|
||||
until curl --fail --head --silent ${{ inputs.url }} > /dev/null
|
||||
do
|
||||
echo "."
|
||||
sleep 60
|
||||
done
|
||||
echo "$url is available"
|
||||
@@ -1,6 +1,18 @@
|
||||
name: 'Build'
|
||||
description: 'Builds the project, optionally publishing it to a local deployment repository'
|
||||
inputs:
|
||||
commercial-release-repository-url:
|
||||
description: 'URL of the release repository'
|
||||
required: false
|
||||
commercial-repository-password:
|
||||
description: 'Password for authentication with the commercial repository'
|
||||
required: false
|
||||
commercial-repository-username:
|
||||
description: 'Username for authentication with the commercial repository'
|
||||
required: false
|
||||
commercial-snapshot-repository-url:
|
||||
description: 'URL of the snapshot repository'
|
||||
required: false
|
||||
develocity-access-key:
|
||||
description: 'Access key for authentication with ge.spring.io'
|
||||
required: false
|
||||
@@ -46,11 +58,21 @@ runs:
|
||||
id: build
|
||||
if: ${{ inputs.publish == 'false' }}
|
||||
shell: bash
|
||||
env:
|
||||
COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }}
|
||||
COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }}
|
||||
COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }}
|
||||
COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }}
|
||||
run: ./gradlew check antora
|
||||
- name: Publish
|
||||
id: publish
|
||||
if: ${{ inputs.publish == 'true' }}
|
||||
shell: bash
|
||||
env:
|
||||
COMMERCIAL_RELEASE_REPO_URL: ${{ inputs.commercial-release-repository-url }}
|
||||
COMMERCIAL_REPO_PASSWORD: ${{ inputs.commercial-repository-password }}
|
||||
COMMERCIAL_REPO_USERNAME: ${{ inputs.commercial-repository-username }}
|
||||
COMMERCIAL_SNAPSHOT_REPO_URL: ${{ inputs.commercial-snapshot-repository-url }}
|
||||
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository
|
||||
- name: Read Version From gradle.properties
|
||||
id: read-version
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
name: Create GitHub Release
|
||||
description: 'Create the release on GitHub with a changelog'
|
||||
inputs:
|
||||
commercial:
|
||||
description: 'Whether to generate the changelog for the commercial release'
|
||||
required: true
|
||||
latest:
|
||||
description: 'Whether the release is the latest release'
|
||||
required: false
|
||||
default: 'false'
|
||||
milestone:
|
||||
description: 'Name of the GitHub milestone for which a release will be created'
|
||||
required: true
|
||||
@@ -15,13 +22,13 @@ runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Generate Changelog
|
||||
uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12
|
||||
uses: spring-io/github-changelog-generator@f7d7a87a3e7c627ecb8c26cf086c38ac5a939721 #v0.0.14
|
||||
with:
|
||||
config-file: .github/actions/create-github-release/changelog-generator.yml
|
||||
config-file: ${{ inputs.commercial && '.github/actions/create-github-release/changelog-generator-commercial.yml' || '.github/actions/create-github-release/changelog-generator-oss.yml' }}
|
||||
milestone: ${{ inputs.milestone }}
|
||||
token: ${{ inputs.token }}
|
||||
- name: Create GitHub Release
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ inputs.token }}
|
||||
run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md ${{ inputs.pre-release == 'true' && '--prerelease' || '' }}
|
||||
run: gh release create ${{ format('v{0}', inputs.milestone) }} --notes-file changelog.md ${{ inputs.pre-release == 'true' && '--prerelease' || format('--latest={0}', inputs.latest) }}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
changelog:
|
||||
repository: spring-projects/spring-framework-commercial
|
||||
sections:
|
||||
- title: ":warning: Attention Required"
|
||||
labels:
|
||||
- "for: upgrade-attention"
|
||||
summary:
|
||||
mode: "member-comment"
|
||||
config:
|
||||
prefix: "Attention Required:"
|
||||
- title: ":star: New Features"
|
||||
labels:
|
||||
- "type: enhancement"
|
||||
- title: ":lady_beetle: Bug Fixes"
|
||||
labels:
|
||||
- "type: bug"
|
||||
- "type: regression"
|
||||
- title: ":notebook_with_decorative_cover: Documentation"
|
||||
labels:
|
||||
- "type: documentation"
|
||||
- title: ":hammer: Dependency Upgrades"
|
||||
sort: "title"
|
||||
labels:
|
||||
- "type: dependency-upgrade"
|
||||
contributors:
|
||||
exclude:
|
||||
names:
|
||||
- "bclozel"
|
||||
- "jhoeller"
|
||||
- "rstoyanchev"
|
||||
- "sbrannen"
|
||||
- "sdeleuze"
|
||||
- "snicoll"
|
||||
-2
@@ -27,9 +27,7 @@ changelog:
|
||||
names:
|
||||
- "bclozel"
|
||||
- "jhoeller"
|
||||
- "poutsma"
|
||||
- "rstoyanchev"
|
||||
- "sbrannen"
|
||||
- "sdeleuze"
|
||||
- "simonbasle"
|
||||
- "snicoll"
|
||||
@@ -29,27 +29,25 @@ runs:
|
||||
distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }}
|
||||
java-version: |
|
||||
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
|
||||
${{ inputs.java-toolchain == 'true' && '17' || '' }}
|
||||
25
|
||||
${{ inputs.java-toolchain == 'true' && '25' || '' }}
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # 6.2.0
|
||||
with:
|
||||
cache-provider: basic
|
||||
cache-read-only: false
|
||||
develocity-access-key: ${{ inputs.develocity-access-key }}
|
||||
develocity-token-expiry: 4
|
||||
- name: Configure Gradle Properties
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p $HOME/.gradle
|
||||
echo 'systemProp.user.name=spring-builds+github' >> $HOME/.gradle/gradle.properties
|
||||
echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $HOME/.gradle/gradle.properties
|
||||
echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties
|
||||
echo 'org.gradle.daemon=4' >> $HOME/.gradle/gradle.properties
|
||||
echo 'systemProp.user.name=spring-builds+github' >> $GRADLE_USER_HOME/gradle.properties
|
||||
echo 'systemProp.org.gradle.internal.launcher.welcomeMessageEnabled=false' >> $GRADLE_USER_HOME/gradle.properties
|
||||
echo 'org.gradle.daemon=false' >> $GRADLE_USER_HOME/gradle.properties
|
||||
- name: Configure Toolchain Properties
|
||||
if: ${{ inputs.java-toolchain == 'true' }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo toolchainVersion=${{ inputs.java-version }} >> $HOME/.gradle/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.auto-detect=false >> $HOME/.gradle/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.auto-download=false >> $HOME/.gradle/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $HOME/.gradle/gradle.properties
|
||||
echo toolchainVersion=${{ inputs.java-version }} >> $GRADLE_USER_HOME/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.auto-detect=false >> $GRADLE_USER_HOME/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.auto-download=false >> $GRADLE_USER_HOME/gradle.properties
|
||||
echo systemProp.org.gradle.java.installations.paths=${{ format('$JAVA_HOME_{0}_X64', inputs.java-version) }} >> $GRADLE_USER_HOME/gradle.properties
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Build Release
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Build Release
|
||||
shell: bash
|
||||
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository publishAllPublicationsToDeploymentRepository
|
||||
@@ -0,0 +1,16 @@
|
||||
artifactory:
|
||||
artifacts:
|
||||
- pattern: "/**/framework-api-*.zip"
|
||||
properties:
|
||||
zip.deployed: "false"
|
||||
zip.name: "spring-framework"
|
||||
- pattern: "/**/framework-api-*-docs.zip"
|
||||
properties:
|
||||
zip.type: "docs"
|
||||
- pattern: "/**/framework-api-*-schema.zip"
|
||||
properties:
|
||||
zip.type: "schema"
|
||||
maven-central:
|
||||
excludes:
|
||||
- "org/springframework/framework-api/**"
|
||||
- "org/springframework/framework-docs/**"
|
||||
@@ -0,0 +1,7 @@
|
||||
name: Test Release
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Test Release
|
||||
shell: bash
|
||||
run: ./gradlew check
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Sync to Maven Central
|
||||
description: 'Syncs a release to Maven Central and waits for it to be available for use'
|
||||
inputs:
|
||||
central-token-password:
|
||||
description: 'Password for authentication with central.sonatype.com'
|
||||
required: true
|
||||
central-token-username:
|
||||
description: 'Username for authentication with central.sonatype.com'
|
||||
required: true
|
||||
jfrog-cli-config-token:
|
||||
description: 'Config token for the JFrog CLI'
|
||||
required: true
|
||||
spring-framework-version:
|
||||
description: 'Version of Spring Framework that is being synced to Central'
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Set Up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
|
||||
- name: Download Release Artifacts
|
||||
shell: bash
|
||||
run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}'
|
||||
- name: Sync
|
||||
uses: spring-io/central-publish-action@0c03960e9b16fdfe70e2443e1d5393cbc3a35622 # v0.3.0
|
||||
with:
|
||||
token: ${{ inputs.central-token-password }}
|
||||
token-name: ${{ inputs.central-token-username }}
|
||||
- name: Await
|
||||
uses: ./.github/actions/await-http-resource
|
||||
with:
|
||||
url: ${{ format('https://repo.maven.apache.org/maven2/org/springframework/spring-context/{0}/spring-context-{0}.jar', inputs.spring-framework-version) }}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"aql": {
|
||||
"items.find": {
|
||||
"$and": [
|
||||
{
|
||||
"@build.name": "${buildName}",
|
||||
"@build.number": "${buildNumber}",
|
||||
"path": {
|
||||
"$nmatch": "org/springframework/framework-api/*"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"target": "nexus/"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
workflow:
|
||||
generator:
|
||||
project:
|
||||
java:
|
||||
versions:
|
||||
primary: 25
|
||||
workflows:
|
||||
release-train:
|
||||
build:
|
||||
env:
|
||||
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
|
||||
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
|
||||
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
|
||||
test:
|
||||
env:
|
||||
COMMERCIAL_REPO_USERNAME: secrets.COMMERCIAL_ARTIFACTORY_USERNAME
|
||||
COMMERCIAL_REPO_PASSWORD: secrets.COMMERCIAL_ARTIFACTORY_PASSWORD
|
||||
COMMERCIAL_RELEASE_REPO_URL: vars.COMMERCIAL_RELEASE_REPO_URL
|
||||
@@ -7,27 +7,15 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- '*.x'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
backport-issue:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v5
|
||||
- name: Create Backport Issue
|
||||
uses: spring-io/backport-bot@v0.0.2
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
- name: Download BackportBot
|
||||
run: wget https://github.com/spring-io/backport-bot/releases/download/latest/backport-bot-0.0.1-SNAPSHOT.jar
|
||||
- name: Backport
|
||||
env:
|
||||
GITHUB_EVENT: ${{ toJSON(github.event) }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT"
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -2,23 +2,27 @@ name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 7.0.x
|
||||
- '7.0.x-internal'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-deploy-snapshot:
|
||||
name: Build and Deploy Snapshot
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' || github.repository == 'spring-projects/spring-framework-commercial' }}
|
||||
runs-on: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }}
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }}
|
||||
commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}
|
||||
commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}
|
||||
commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }}
|
||||
#develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Deploy
|
||||
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
|
||||
@@ -27,32 +31,47 @@ jobs:
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: 'spring-framework-7.0.x'
|
||||
build-name: ${{ vars.COMMERCIAL && format('spring-framework-commercial-{0}', '7.0.x') || format('spring-framework-{0}', '7.0.x') }}
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-snapshot-local'
|
||||
project: ${{ vars.COMMERCIAL && 'spring' }}
|
||||
repository: ${{ vars.COMMERCIAL && 'spring-enterprise-maven-dev-local' || 'libs-snapshot-local' }}
|
||||
uri: ${{ vars.COMMERCIAL_DEPLOY_REPO_URL || 'https://repo.spring.io' }}
|
||||
username: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_USERNAME || secrets.ARTIFACTORY_USERNAME }}
|
||||
password: ${{ vars.COMMERCIAL && secrets.COMMERCIAL_ARTIFACTORY_PASSWORD || secrets.ARTIFACTORY_PASSWORD }}
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
- name: Send Notification
|
||||
if: always()
|
||||
uses: ./.github/actions/send-notification
|
||||
with:
|
||||
build-scan-url: ${{ steps.build-and-publish.outputs.build-scan-url }}
|
||||
run-name: ${{ format('{0} | Linux | Java 17', github.ref_name) }}
|
||||
run-name: ${{ format('{0} | Linux | Java 25', github.ref_name) }}
|
||||
status: ${{ job.status }}
|
||||
webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
trigger-docs-build:
|
||||
name: Trigger Docs Build
|
||||
needs: build-and-deploy-snapshot
|
||||
if: ${{ !vars.COMMERCIAL }} # remove when commercial support
|
||||
permissions:
|
||||
actions: write
|
||||
runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Run Deploy Docs Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-deploy-snapshot
|
||||
uses: ./.github/workflows/verify.yml
|
||||
secrets:
|
||||
commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}
|
||||
commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
opensource-repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
opensource-repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
with:
|
||||
version: ${{ needs.build-and-deploy-snapshot.outputs.version }}
|
||||
|
||||
@@ -7,10 +7,9 @@ jobs:
|
||||
name: Build Pull Request
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Build
|
||||
id: build
|
||||
uses: ./.github/actions/build
|
||||
@@ -19,7 +18,7 @@ jobs:
|
||||
uses: ./.github/actions/print-jvm-thread-dumps
|
||||
- name: Upload Build Reports
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: build-reports
|
||||
path: '**/build/reports/'
|
||||
path: '**/build/reports/'
|
||||
@@ -2,31 +2,33 @@ name: CI
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 9 * * *'
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
ci:
|
||||
name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}'
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' || github.repository == 'spring-projects/spring-framework-commercial' }}
|
||||
runs-on: ${{ matrix.os.id }}
|
||||
timeout-minutes: 60
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- id: ubuntu-latest
|
||||
- id: ${{ vars.UBUNTU_MEDIUM || 'ubuntu-latest' }}
|
||||
name: Linux
|
||||
java:
|
||||
- version: 17
|
||||
toolchain: false
|
||||
toolchain: true
|
||||
- version: 21
|
||||
toolchain: true
|
||||
- version: 25
|
||||
toolchain: false
|
||||
- version: 26
|
||||
toolchain: true
|
||||
exclude:
|
||||
- os:
|
||||
name: Linux
|
||||
java:
|
||||
version: 17
|
||||
version: 25
|
||||
steps:
|
||||
- name: Prepare Windows runner
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
@@ -35,11 +37,15 @@ jobs:
|
||||
git config --global core.longPaths true
|
||||
Stop-Service -name Docker
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Build
|
||||
id: build
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
commercial-release-repository-url: ${{ vars.COMMERCIAL_RELEASE_REPO_URL }}
|
||||
commercial-repository-password: ${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}
|
||||
commercial-repository-username: ${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}
|
||||
commercial-snapshot-repository-url: ${{ vars.COMMERCIAL_SNAPSHOT_REPO_URL }}
|
||||
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
java-early-access: ${{ matrix.java.early-access || 'false' }}
|
||||
java-distribution: ${{ matrix.java.distribution }}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
name: Deploy Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- '*.x'
|
||||
- '!gh-pages'
|
||||
tags:
|
||||
- 'v*'
|
||||
repository_dispatch:
|
||||
types: request-build-reference # legacy
|
||||
workflow_dispatch:
|
||||
permissions:
|
||||
actions: write
|
||||
jobs:
|
||||
build:
|
||||
name: Dispatch docs deployment
|
||||
if: github.repository_owner == 'spring-projects'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 1
|
||||
ref: docs-build
|
||||
- name: Dispatch (partial build)
|
||||
if: github.ref_type == 'branch'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
|
||||
- name: Dispatch (full build)
|
||||
if: github.ref_type == 'tag'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
|
||||
@@ -0,0 +1,39 @@
|
||||
name: Release Milestone
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.0-M[1-9]
|
||||
- v7.0.0-RC[1-9]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
trigger-docs-build:
|
||||
name: Trigger Docs Build
|
||||
permissions:
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Determine Version
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||
- name: Run Deploy Docs Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }}
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- trigger-docs-build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
commercial: ${{ vars.COMMERCIAL }}
|
||||
milestone: ${{ needs.trigger-docs-build.outputs.version }}
|
||||
pre-release: true
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.[0-9]+
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
trigger-docs-build:
|
||||
name: Trigger Docs Build
|
||||
permissions:
|
||||
actions: write
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- name: Determine Version
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
||||
- name: Run Deploy Docs Workflow
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: gh workflow run deploy-docs.yml --repo ${{ github.repository }} -r docs-build -f build-refname=${{ github.ref_name }}
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- trigger-docs-build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
commercial: ${{ vars.COMMERCIAL }}
|
||||
latest: true
|
||||
milestone: ${{ needs.trigger-docs-build.outputs.version }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -1,95 +0,0 @@
|
||||
name: Release Milestone
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.0-M[1-9]
|
||||
- v7.0.0-RC[1-9]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-stage-release:
|
||||
name: Build and Stage Release
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Stage Release
|
||||
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-staging-local'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-stage-release
|
||||
uses: ./.github/workflows/verify.yml
|
||||
secrets:
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
with:
|
||||
staging: true
|
||||
version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
sync-to-maven-central:
|
||||
name: Sync to Maven Central
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Sync to Maven Central
|
||||
uses: ./.github/actions/sync-to-maven-central
|
||||
with:
|
||||
central-token-password: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
|
||||
central-token-username: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
|
||||
jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
promote-release:
|
||||
name: Promote Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- sync-to-maven-central
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- promote-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
milestone: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
pre-release: true
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -0,0 +1,93 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Build"
|
||||
run-name: "${{ inputs.callback-ref }} – Build"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
callback:
|
||||
description: "Repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
callback-ref:
|
||||
description: "Ref in the callback repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-maven-repository-url:
|
||||
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "read"
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
jobs:
|
||||
build-release:
|
||||
name: "Build Release"
|
||||
runs-on: "ubuntu22-2-8"
|
||||
steps:
|
||||
- name: "Prevent Re-runs"
|
||||
id: "prevent-re-runs"
|
||||
run: |-
|
||||
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
|
||||
echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry build failures"
|
||||
exit 1
|
||||
fi
|
||||
- name: "Set up Java"
|
||||
id: "set-up-java"
|
||||
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
|
||||
with:
|
||||
distribution: "liberica"
|
||||
java-version: "25"
|
||||
- name: "Check Out Code"
|
||||
id: "check-out-code"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
- name: "Build Release"
|
||||
id: "build-release"
|
||||
uses: "./.github/actions/release-train-build"
|
||||
env:
|
||||
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
|
||||
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
|
||||
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
|
||||
- name: "Upload Deployment Repository"
|
||||
id: "upload-deployment-repository"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
name: "deployment-repository"
|
||||
path: "deployment-repository/**"
|
||||
- name: "Upload Deployment Spec"
|
||||
id: "upload-deployment-spec"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
archive: "false"
|
||||
if-no-files-found: "ignore"
|
||||
name: "deployment-spec"
|
||||
path: ".github/actions/release-train-build/deployment-spec.yml"
|
||||
- name: "Save Build System Caches"
|
||||
id: "save-build-system-caches"
|
||||
uses: "actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
|
||||
with:
|
||||
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
|
||||
path: |-
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
- name: "Send Callback"
|
||||
id: "send-callback"
|
||||
if: "${{ !cancelled() }}"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run callback \
|
||||
--repo ${{ inputs.callback }} \
|
||||
--ref ${{ inputs.callback-ref }} \
|
||||
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
|
||||
--field deployment-repository-artifact-identifier=${{ steps.upload-deployment-repository.outputs.artifact-id }} \
|
||||
--field deployment-spec-artifact-identifier=${{ steps.upload-deployment-spec.outputs.artifact-id }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }} \
|
||||
--field result=${{ job.status == 'success' && 'built' || 'build-failed' }} \
|
||||
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@@ -0,0 +1,55 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Join"
|
||||
run-name: "${{ inputs.release-train }} – Join"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
deployment-destination:
|
||||
description: "Destination to which the release should be deployed"
|
||||
options:
|
||||
- "Maven Central"
|
||||
- "Spring Enterprise"
|
||||
required: true
|
||||
type: "choice"
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
join-release-train:
|
||||
name: "Join Release Train"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Join Release Train"
|
||||
id: "join-release-train"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
run_url=$(
|
||||
gh workflow run join \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field commit-hash=${{ github.sha }} \
|
||||
--field deployment-destination=${{ inputs.deployment-destination == 'Maven Central' && 'maven-central' || 'spring-enterprise' }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
)
|
||||
echo "Dispatched workflow run. Waiting for $run_url to complete."
|
||||
run_id=${run_url##*/}
|
||||
watch_exit_code=0
|
||||
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
|
||||
if [[ $watch_exit_code -eq 0 ]]; then
|
||||
echo "Workflow run succeeded."
|
||||
else
|
||||
echo "Workflow run failed."
|
||||
fi
|
||||
exit $watch_exit_code
|
||||
@@ -0,0 +1,46 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Leave"
|
||||
run-name: "${{ inputs.release-train }} – Leave"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
leave:
|
||||
name: "Leave"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Leave"
|
||||
id: "leave"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
run_url=$(
|
||||
gh workflow run leave \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
)
|
||||
echo "Dispatched workflow run. Waiting for $run_url to complete."
|
||||
run_id=${run_url##*/}
|
||||
watch_exit_code=0
|
||||
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
|
||||
if [[ $watch_exit_code -eq 0 ]]; then
|
||||
echo "Workflow run succeeded."
|
||||
else
|
||||
echo "Workflow run failed."
|
||||
fi
|
||||
exit $watch_exit_code
|
||||
@@ -0,0 +1,47 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Ready"
|
||||
run-name: "${{ inputs.release-train }} – Ready"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
ready:
|
||||
name: "Ready"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Ready"
|
||||
id: "ready"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
run_url=$(
|
||||
gh workflow run ready \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field commit-hash=${{ github.sha }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
)
|
||||
echo "Dispatched workflow run. Waiting for $run_url to complete."
|
||||
run_id=${run_url##*/}
|
||||
watch_exit_code=0
|
||||
gh run watch $run_id --repo ${{ inputs.release-train-repository }} --exit-status --interval=3 > /dev/null 2>&1 || watch_exit_code=$?
|
||||
if [[ $watch_exit_code -eq 0 ]]; then
|
||||
echo "Workflow run succeeded."
|
||||
else
|
||||
echo "Workflow run failed."
|
||||
fi
|
||||
exit $watch_exit_code
|
||||
@@ -0,0 +1,34 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Retry"
|
||||
run-name: "${{ inputs.release-train }} – Retry"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
release-train:
|
||||
description: "Release train"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-repository:
|
||||
default: "spring-io/release-train"
|
||||
description: "Release train repository"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "none"
|
||||
jobs:
|
||||
trigger-retry:
|
||||
name: "Trigger Retry"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Trigger Retry"
|
||||
id: "trigger-retry"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run retry \
|
||||
--repo ${{ inputs.release-train-repository }} \
|
||||
--ref ${{ inputs.release-train }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }}
|
||||
@@ -0,0 +1,84 @@
|
||||
# This file was auto-generated by github-actions-workflow-generator 0.0.6. Do not edit.
|
||||
# To update it, modify .github/workflow-generator.yml as needed and re-run the generator.
|
||||
|
||||
name: "Release Train – Test"
|
||||
run-name: "${{ inputs.callback-ref }} – Test"
|
||||
"on":
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
callback:
|
||||
description: "Repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
callback-ref:
|
||||
description: "Ref in the callback repository to which a callback should be made upon completion"
|
||||
required: true
|
||||
type: "string"
|
||||
release-train-maven-repository-url:
|
||||
description: "URL of a Maven repository to be used to resolve artifacts of projects earlier in the train"
|
||||
required: true
|
||||
type: "string"
|
||||
permissions:
|
||||
contents: "read"
|
||||
concurrency:
|
||||
group: "${{ github.workflow }}-${{ github.ref }}"
|
||||
jobs:
|
||||
test-release:
|
||||
name: "Test Release"
|
||||
runs-on: "ubuntu22-2-8"
|
||||
steps:
|
||||
- name: "Prevent Re-runs"
|
||||
id: "prevent-re-runs"
|
||||
run: |-
|
||||
if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then
|
||||
echo "Re-runs are prohibited. Use the 'Release Train – Retry' workflow to retry test failures"
|
||||
exit 1
|
||||
fi
|
||||
- name: "Set up Java"
|
||||
id: "set-up-java"
|
||||
uses: "actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95" # v5.6.0
|
||||
with:
|
||||
distribution: "liberica"
|
||||
java-version: "25"
|
||||
- name: "Check Out Code"
|
||||
id: "check-out-code"
|
||||
uses: "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" # v7.0.0
|
||||
- name: "Restore Build System Caches"
|
||||
id: "restore-build-system-caches"
|
||||
uses: "actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9" # v6.1.0
|
||||
with:
|
||||
key: "release-train-${{ inputs.callback-ref }}-${{ github.ref_name }}"
|
||||
path: |-
|
||||
~/.gradle/caches
|
||||
~/.gradle/wrapper
|
||||
- name: "Test Release"
|
||||
id: "test-release"
|
||||
uses: "./.github/actions/release-train-test"
|
||||
env:
|
||||
COMMERCIAL_RELEASE_REPO_URL: "${{ vars.COMMERCIAL_RELEASE_REPO_URL }}"
|
||||
COMMERCIAL_REPO_PASSWORD: "${{ secrets.COMMERCIAL_ARTIFACTORY_PASSWORD }}"
|
||||
COMMERCIAL_REPO_USERNAME: "${{ secrets.COMMERCIAL_ARTIFACTORY_USERNAME }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_PASSWORD }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_URL: "${{ inputs.release-train-maven-repository-url }}"
|
||||
RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_MAVEN_REPOSITORY_USERNAME }}"
|
||||
- name: "Send Callback"
|
||||
id: "send-callback"
|
||||
if: "${{ !cancelled() }}"
|
||||
env:
|
||||
GH_TOKEN: "${{ secrets.RELEASE_TRAIN_PARTICIPANT_GITHUB_TOKEN }}"
|
||||
run: |-
|
||||
gh workflow run callback \
|
||||
--repo ${{ inputs.callback }} \
|
||||
--ref ${{ inputs.callback-ref }} \
|
||||
--field commit-hash=${{ steps.check-out-code.outputs.commit }} \
|
||||
--field release-branch=${{ github.ref_name }} \
|
||||
--field release-repository=${{ github.repository }} \
|
||||
--field result=${{ job.status == 'success' && 'tested' || 'test-failed' }} \
|
||||
--field workflow-run-url=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
- name: "Upload Build System Reports"
|
||||
id: "upload-build-system-reports"
|
||||
if: "${{ failure() }}"
|
||||
uses: "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a" # v7.0.1
|
||||
with:
|
||||
name: "build-system-reports"
|
||||
path: "**/build/reports"
|
||||
@@ -1,93 +0,0 @@
|
||||
name: Release
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v7.0.[0-9]+
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-stage-release:
|
||||
name: Build and Stage Release
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Stage Release
|
||||
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-staging-local'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-stage-release
|
||||
uses: ./.github/workflows/verify.yml
|
||||
secrets:
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
with:
|
||||
staging: true
|
||||
version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
sync-to-maven-central:
|
||||
name: Sync to Maven Central
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Sync to Maven Central
|
||||
uses: ./.github/actions/sync-to-maven-central
|
||||
with:
|
||||
central-token-password: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
|
||||
central-token-username: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
|
||||
jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
promote-release:
|
||||
name: Promote Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- sync-to-maven-central
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-release-local
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- promote-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v6
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
milestone: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -12,31 +12,39 @@ on:
|
||||
required: true
|
||||
type: string
|
||||
secrets:
|
||||
commercial-repository-password:
|
||||
description: 'Password for authentication with the commercial repository'
|
||||
required: false
|
||||
commercial-repository-username:
|
||||
description: 'Username for authentication with the commercial repository'
|
||||
required: false
|
||||
google-chat-webhook-url:
|
||||
description: 'Google Chat Webhook URL'
|
||||
required: true
|
||||
repository-password:
|
||||
description: 'Password for authentication with the repository'
|
||||
opensource-repository-password:
|
||||
description: 'Password for authentication with the open-source repository'
|
||||
required: false
|
||||
repository-username:
|
||||
description: 'Username for authentication with the repository'
|
||||
opensource-repository-username:
|
||||
description: 'Username for authentication with the open-source repository'
|
||||
required: false
|
||||
token:
|
||||
description: 'Token to use for authentication with GitHub'
|
||||
required: true
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
verify:
|
||||
name: Verify
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ vars.UBUNTU_SMALL || 'ubuntu-latest' }}
|
||||
steps:
|
||||
- name: Check Out Release Verification Tests
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
ref: 'v0.0.2'
|
||||
ref: 'v0.0.3'
|
||||
repository: spring-projects/spring-framework-release-verification
|
||||
token: ${{ secrets.token }}
|
||||
- name: Check Out Send Notification Action
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
path: send-notification
|
||||
sparse-checkout: .github/actions/send-notification
|
||||
@@ -46,8 +54,9 @@ jobs:
|
||||
distribution: 'liberica'
|
||||
java-version: 17
|
||||
- name: Set Up Gradle
|
||||
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
with:
|
||||
cache-provider: basic
|
||||
cache-read-only: false
|
||||
- name: Configure Gradle Properties
|
||||
shell: bash
|
||||
@@ -56,15 +65,17 @@ jobs:
|
||||
echo 'org.gradle.daemon=false' >> $HOME/.gradle/gradle.properties
|
||||
- name: Run Release Verification Tests
|
||||
env:
|
||||
RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.repository-password }}
|
||||
RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.repository-username }}
|
||||
RVT_RELEASE_TYPE: oss
|
||||
RVT_COMMERCIAL_REPOSITORY_PASSWORD: ${{ secrets.commercial-repository-password }}
|
||||
RVT_COMMERCIAL_REPOSITORY_USERNAME: ${{ secrets.commercial-repository-username }}
|
||||
RVT_OSS_REPOSITORY_PASSWORD: ${{ secrets.opensource-repository-password }}
|
||||
RVT_OSS_REPOSITORY_USERNAME: ${{ secrets.opensource-repository-username }}
|
||||
RVT_RELEASE_TYPE: ${{ vars.COMMERCIAL && 'commercial' || 'oss' }}
|
||||
RVT_STAGING: ${{ inputs.staging }}
|
||||
RVT_VERSION: ${{ inputs.version }}
|
||||
run: ./gradlew spring-framework-release-verification-tests:test
|
||||
- name: Upload Build Reports on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v6
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: build-reports
|
||||
path: '**/build/reports/'
|
||||
|
||||
+2
-10
@@ -6,7 +6,7 @@ plugins {
|
||||
id 'com.github.bjornvester.xjc' version '1.8.2' apply false
|
||||
id 'com.gradleup.shadow' version "9.2.2" apply false
|
||||
id 'me.champeau.jmh' version '0.7.2' apply false
|
||||
id 'io.spring.nullability' version '0.0.11' apply false
|
||||
id 'io.spring.nullability' version '0.0.14' apply false
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -18,16 +18,8 @@ description = "Spring Framework"
|
||||
|
||||
configure(allprojects) { project ->
|
||||
apply plugin: "org.springframework.build.localdev"
|
||||
apply plugin: "org.springframework.build.repositories"
|
||||
group = "org.springframework"
|
||||
repositories {
|
||||
mavenCentral()
|
||||
if (version.contains('-')) {
|
||||
maven { url = "https://repo.spring.io/milestone" }
|
||||
}
|
||||
if (version.endsWith('-SNAPSHOT')) {
|
||||
maven { url = "https://repo.spring.io/snapshot" }
|
||||
}
|
||||
}
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
cacheChangingModulesFor 0, "seconds"
|
||||
|
||||
@@ -50,6 +50,10 @@ gradlePlugin {
|
||||
id = "org.springframework.build.multiReleaseJar"
|
||||
implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin"
|
||||
}
|
||||
repositoriesPlugin {
|
||||
id = "org.springframework.build.repositories"
|
||||
implementationClass = "org.springframework.build.RepositoriesPlugin"
|
||||
}
|
||||
optionalDependenciesPlugin {
|
||||
id = "org.springframework.build.optional-dependencies"
|
||||
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
|
||||
|
||||
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("13.4.2");
|
||||
checkstyle.setToolVersion("13.10.0");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.build;
|
||||
|
||||
import org.gradle.api.Plugin;
|
||||
import org.gradle.api.Project;
|
||||
|
||||
/**
|
||||
* Plugin that configures the OSS, commercial and release train repositories in the build.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class RepositoriesPlugin implements Plugin <Project> {
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
configureOssRepositories(project);
|
||||
configureCommercialRepositories(project);
|
||||
configureReleaseTrainRepository(project);
|
||||
}
|
||||
|
||||
private void configureOssRepositories(Project project) {
|
||||
project.getRepositories().mavenCentral();
|
||||
if (project.getVersion().toString().contains("-")) {
|
||||
project.getRepositories().maven(repository -> {
|
||||
repository.setName("spring-oss-milestone");
|
||||
repository.setUrl("https://repo.spring.io/milestone/");
|
||||
});
|
||||
}
|
||||
if (project.getVersion().toString().endsWith("-SNAPSHOT")) {
|
||||
project.getRepositories().maven(repository -> {
|
||||
repository.setName("spring-oss-snapshot");
|
||||
repository.setUrl("https://repo.spring.io/snapshot/");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void configureCommercialRepositories(Project project) {
|
||||
String releaseRepositoryUrl = getEnv("COMMERCIAL_RELEASE_REPO_URL");
|
||||
if (releaseRepositoryUrl != null) {
|
||||
project.getRepositories().maven((repository) -> {
|
||||
repository.setName("spring-commercial-release");
|
||||
repository.setUrl(releaseRepositoryUrl);
|
||||
repository.credentials((creds) -> {
|
||||
creds.setUsername(System.getenv("COMMERCIAL_REPO_USERNAME"));
|
||||
creds.setPassword(System.getenv("COMMERCIAL_REPO_PASSWORD"));
|
||||
});
|
||||
});
|
||||
}
|
||||
String snapshotRepositoryUrl = getEnv("COMMERCIAL_SNAPSHOT_REPO_URL");
|
||||
if (snapshotRepositoryUrl != null && project.getVersion().toString().endsWith("-SNAPSHOT")) {
|
||||
project.getRepositories().maven((repository) -> {
|
||||
repository.setName("spring-commercial-snapshot");
|
||||
repository.setUrl(snapshotRepositoryUrl);
|
||||
repository.credentials((creds) -> {
|
||||
creds.setUsername(System.getenv("COMMERCIAL_REPO_USERNAME"));
|
||||
creds.setPassword(System.getenv("COMMERCIAL_REPO_PASSWORD"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void configureReleaseTrainRepository(Project project) {
|
||||
String releaseTrainRepositoryUrl = getEnv("RELEASE_TRAIN_MAVEN_REPOSITORY_URL");
|
||||
if (releaseTrainRepositoryUrl != null) {
|
||||
project.getRepositories().maven(repository -> {
|
||||
repository.setName("spring-release-train");
|
||||
repository.setUrl(releaseTrainRepositoryUrl);
|
||||
repository.credentials((creds) -> {
|
||||
creds.setUsername(System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_USERNAME"));
|
||||
creds.setPassword(System.getenv("RELEASE_TRAIN_MAVEN_REPOSITORY_PASSWORD"));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the environment variable's value, or {@code null} if it is unset or blank.
|
||||
*/
|
||||
private static String getEnv(String name) {
|
||||
String value = System.getenv(name);
|
||||
return (value != null && !value.isBlank()) ? value : null;
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ class TestConventions {
|
||||
"junit.platform.discovery.issue.severity.critical", "INFO"
|
||||
));
|
||||
if (project.hasProperty("testGroups")) {
|
||||
test.systemProperty("testGroups", project.getProperties().get("testGroups"));
|
||||
test.systemProperty("testGroups", project.findProperty("testGroups"));
|
||||
}
|
||||
test.jvmArgs(
|
||||
"--add-opens=java.base/java.lang=ALL-UNNAMED",
|
||||
@@ -82,7 +82,7 @@ class TestConventions {
|
||||
|
||||
private void configureByteBuddyAgent(Project project) {
|
||||
if (project.hasProperty("byteBuddyVersion")) {
|
||||
String byteBuddyVersion = (String) project.getProperties().get("byteBuddyVersion");
|
||||
String byteBuddyVersion = (String) project.findProperty("byteBuddyVersion");
|
||||
Configuration byteBuddyAgentConfig = project.getConfigurations().create("byteBuddyAgentConfig");
|
||||
byteBuddyAgentConfig.setTransitive(false);
|
||||
Dependency byteBuddyAgent = project.getDependencies().create("net.bytebuddy:byte-buddy-agent:" + byteBuddyVersion);
|
||||
|
||||
@@ -15,8 +15,8 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
moduleProjects.each { moduleProject ->
|
||||
javadoc moduleProject
|
||||
rootProject.ext.moduleProjects.each { moduleProject ->
|
||||
javadoc project(moduleProject.path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ javadoc {
|
||||
// ensure the javadoc process can resolve types compiled from .aj sources
|
||||
springAspectsOutput
|
||||
)
|
||||
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
|
||||
classpath += files(rootProject.ext.moduleProjects.collect { it.sourceSets.main.compileClasspath })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ tasks.register('schemaZip', Zip) {
|
||||
description = "Builds -${archiveClassifier} archive containing all " +
|
||||
"XSDs for deployment at https://springframework.org/schema."
|
||||
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
|
||||
moduleProjects.each { module ->
|
||||
rootProject.ext.moduleProjects.each { module ->
|
||||
def Properties schemas = new Properties();
|
||||
|
||||
module.sourceSets.main.resources.find {
|
||||
|
||||
@@ -8,7 +8,7 @@ group = "org.springframework"
|
||||
dependencies {
|
||||
constraints {
|
||||
parent.moduleProjects.sort { "$it.name" }.each {
|
||||
api it
|
||||
api project(it.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,6 +74,12 @@ expressions used in XML bean definitions, `@Value`, etc.
|
||||
| The mode to use when compiling expressions for the
|
||||
xref:core/expressions/evaluation.adoc#expressions-compiler-configuration[Spring Expression Language].
|
||||
|
||||
| `spring.expression.maxBigPowerBits`
|
||||
| The default maximum number of bits permitted in the result of a `BigDecimal` or
|
||||
`BigInteger` power operation within a
|
||||
xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language]
|
||||
expression.
|
||||
|
||||
| `spring.expression.maxOperations`
|
||||
| The default maximum number of operations permitted during
|
||||
xref:core/expressions/evaluation.adoc#expressions-parser-configuration[Spring Expression Language]
|
||||
|
||||
@@ -253,6 +253,175 @@ properties. Alternatively, configure custom accessors via
|
||||
`SimpleEvaluationContext.forPropertyAccessors(...)`, potentially disable assignment, and
|
||||
optionally activate method resolution and/or a type converter through the builder.
|
||||
|
||||
[[expressions-evaluation-context-security]]
|
||||
=== Security Considerations
|
||||
|
||||
SpEL is a powerful expression language that can invoke constructors and methods, read and
|
||||
write properties and fields, and reference beans – all backed by reflection. Because of
|
||||
this power, evaluating a SpEL expression obtained from an untrusted source is inherently
|
||||
dangerous and should generally be avoided, since doing so can effectively grant that
|
||||
source the ability to execute arbitrary code within the application, regardless of which
|
||||
`EvaluationContext` implementation is used.
|
||||
|
||||
Throughout this section, a source of a SpEL expression is considered "trusted" only if it
|
||||
is a developer of the application or an administrator responsible for configuring or
|
||||
operating the application. Any other source of a SpEL expression must be treated as
|
||||
untrusted – for example, an expression supplied by an end user of the application or
|
||||
received from an external system.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
`StandardEvaluationContext` exposes the complete SpEL language and must *never* be used
|
||||
to evaluate an expression obtained from an untrusted source.
|
||||
====
|
||||
|
||||
Although `SimpleEvaluationContext` restricts the SpEL language to a subset of its
|
||||
features, that restriction is provided on a best-effort basis and does not guarantee that
|
||||
expression evaluation is safe. Since an expression can potentially invoke any property,
|
||||
method, or function reachable via the configured root object, property accessors, method
|
||||
resolvers, variables, and functions, care must be taken if you choose to evaluate
|
||||
expressions from an untrusted source. It is therefore the responsibility of the code that
|
||||
configures an `EvaluationContext` – for example, by supplying a root object or by
|
||||
registering property accessors, resolvers, variables, or functions – to ensure that none
|
||||
of the objects reachable via the context expose operations that would be dangerous if
|
||||
invoked by an expression from an untrusted source.
|
||||
|
||||
Furthermore, a property "getter" reachable from an expression is not necessarily a pure,
|
||||
side-effect-free read operation. A JavaBean-style accessor (such as `getName()` or
|
||||
`isActive()`) and a plain accessor method used to support data classes such as Java
|
||||
records and Kotlin data classes (such as `name()`) are indistinguishable from a method
|
||||
that performs an action and happens to return a value (that is, a method which is
|
||||
*accessor-shaped*). For example, the `public boolean delete()` method in `java.io.File`
|
||||
looks like a plain accessor method to SpEL. Specifically, neither
|
||||
`ReflectivePropertyAccessor` nor `DataBindingPropertyAccessor` can determine whether such
|
||||
a method is free of side effects. Moreover, restricting a `SimpleEvaluationContext` to
|
||||
read-only data binding governs only whether *assignment* to a property is permitted: it
|
||||
does not verify that reading a property is side-effect-free. When exposing a root object
|
||||
or other reachable object to an untrusted expression, you must ensure that none of its
|
||||
accessor-shaped methods perform an action that would be unsafe if triggered by that
|
||||
expression.
|
||||
|
||||
[NOTE]
|
||||
.What makes a method "accessor-shaped"?
|
||||
====
|
||||
A method is accessor-shaped if it is `public`, takes no arguments, and returns a value –
|
||||
the same shape that `ReflectivePropertyAccessor` and `DataBindingPropertyAccessor` look
|
||||
for when resolving a property "getter" by name. That shape says nothing about whether
|
||||
invoking the method is actually free of side effects. For example, the following methods
|
||||
are all accessor-shaped, but only some of them are safe to invoke as a property read.
|
||||
|
||||
Side-effect-free (safe to expose as properties):
|
||||
|
||||
* `getName()` and `isActive()`: conventional JavaBean-style accessors.
|
||||
* `name()` and `active()`: plain accessor methods used by data classes such as Java
|
||||
records and Kotlin data classes.
|
||||
|
||||
Side-effecting (unsafe to expose as properties, despite the identical shape):
|
||||
|
||||
* `java.io.File#delete()`: deletes the underlying file and returns whether the deletion
|
||||
succeeded.
|
||||
* `java.util.Queue#poll()`: removes and returns the head element, mutating the queue.
|
||||
* `java.util.concurrent.atomic.AtomicInteger#incrementAndGet()`: increments and returns
|
||||
a counter, mutating it.
|
||||
|
||||
If an untrusted expression can reference `someFile.delete`, `someQueue.poll`, or
|
||||
`someCounter.incrementAndGet` as a property, SpEL invokes the corresponding method just
|
||||
as readily as it would invoke a genuine getter.
|
||||
====
|
||||
|
||||
[[expressions-evaluation-context-object-design]]
|
||||
=== Object Design
|
||||
|
||||
Similar to the design guidance for
|
||||
xref:web/webmvc/mvc-data-binding.adoc#mvc-data-binding-design[web data binding], you
|
||||
should carefully design any object that may be reached from a SpEL expression evaluated
|
||||
against untrusted input. This applies not only to the root object supplied to an
|
||||
`EvaluationContext` but also to every object that such an expression can navigate to from
|
||||
that root object – for example, an object returned by a property, a method, an index
|
||||
operation, a variable, or a function.
|
||||
|
||||
When exposing an object to expressions from an untrusted source, consider the following
|
||||
recommendations.
|
||||
|
||||
Use a dedicated type::
|
||||
Prefer a dedicated type, designed specifically to be evaluated against untrusted
|
||||
expressions, over passing an existing domain or infrastructure type "as is". A
|
||||
dedicated type lets you control exactly which properties and methods are reachable from
|
||||
an expression, rather than exposing the full surface area of a class such as a JPA
|
||||
entity, `java.io.File`, or a JDBC `Connection` – most of which were never designed with
|
||||
SpEL evaluation in mind.
|
||||
|
||||
Prefer immutability::
|
||||
An immutable type – for example, a Java record or a Kotlin data class exposing only
|
||||
`val` properties – rules out property writes and eliminates any concern that a "getter"
|
||||
might mutate state as a side effect, since there is no mutable state to affect.
|
||||
Immutability does not, on its own, rule out an accessor-shaped method with an external
|
||||
side effect (such as a network call or a file system operation), but it removes an
|
||||
entire class of risk.
|
||||
|
||||
Limit scope::
|
||||
Expose only the properties and methods that the expression is expected to use, and
|
||||
nothing more. Because a `PropertyAccessor` cannot restrict access to specific
|
||||
properties or methods on a per-expression basis, every accessor-shaped method reachable
|
||||
on an exposed object is reachable by any expression that can reach that object –
|
||||
regardless of which property or method the application intended the expression to use.
|
||||
|
||||
Audit accessor-shaped methods::
|
||||
Review every accessor-shaped method exposed by a type before making it reachable from
|
||||
an untrusted expression, keeping the <<expressions-evaluation-context-security,
|
||||
security considerations>> discussed above in mind. None of the reachable methods should
|
||||
perform an action that would be unsafe if triggered by that expression.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
These recommendations apply transitively. If the root object exposes a property or method
|
||||
that returns another object, and an untrusted expression can navigate to it (for example,
|
||||
`rootObject.child.grandchild`), the nested object is just as reachable as the root object
|
||||
itself and must meet the same design requirements. The same is true for an object reached
|
||||
via indexing (for example, `rootObject.items[0]` or `rootObject.items['key']`): whatever
|
||||
is returned by the index operation is just as reachable as any other nested object.
|
||||
====
|
||||
|
||||
[[expressions-evaluation-context-lifecycle]]
|
||||
=== Lifecycle and Reuse
|
||||
|
||||
For performance, the AST nodes that make up a parsed `Expression` may cache the specific
|
||||
`PropertyAccessor`, `IndexAccessor`, `MethodExecutor`, or `ConstructorExecutor` that
|
||||
satisfied a previous evaluation, so that later evaluations of the same node can avoid
|
||||
asking every registered accessor or resolver in turn. Understanding this caching behavior
|
||||
is essential to using `Expression` and `EvaluationContext` correctly, in addition to the
|
||||
<<expressions-evaluation-context-security,security considerations>> discussed previously.
|
||||
|
||||
A parsed `Expression` is designed to be created once and evaluated repeatedly, and doing
|
||||
so is both supported and encouraged. In particular:
|
||||
|
||||
* A parsed `Expression` may be evaluated against different root objects, and against
|
||||
different `EvaluationContext` instances of the *same type and with equivalent
|
||||
configuration* – for example, several `StandardEvaluationContext` instances each
|
||||
registering the same kind of custom `PropertyAccessor`. Changing the accessors or
|
||||
resolvers registered with a context between evaluations of the same expression is
|
||||
atypical and generally not advised, but is expected to work correctly: the registered
|
||||
state of the *current* context is what is consulted, not a snapshot taken during an
|
||||
earlier evaluation.
|
||||
* A parsed `Expression` must *not* be evaluated first against a context with one set of
|
||||
security implications and later against a context with different, typically more
|
||||
restrictive, security implications – for example, first against a
|
||||
`StandardEvaluationContext` and later against a `SimpleEvaluationContext`. Doing so is
|
||||
analogous to executing a database query on behalf of an administrator, caching the
|
||||
resulting administrator-privileged execution plan, and then reusing that cached plan for
|
||||
a lower-privileged user while expecting the lower-privileged user's restrictions to
|
||||
apply: cached state from the first, more permissive evaluation may be reused during the
|
||||
second, and the second context's restrictions cannot be reliably enforced as a result.
|
||||
If the same expression string must be evaluated under contexts with different security
|
||||
implications, parse it into *distinct* `Expression` instances, one per context.
|
||||
|
||||
[WARNING]
|
||||
====
|
||||
Reusing a single parsed `Expression` across `EvaluationContext` instances with different
|
||||
security implications is not a supported usage pattern and must be avoided, regardless of
|
||||
which `EvaluationContext` implementations are involved.
|
||||
====
|
||||
|
||||
[[expressions-type-conversion]]
|
||||
=== Type Conversion
|
||||
|
||||
@@ -405,6 +574,19 @@ property or Spring property named `spring.expression.maxOperations` to the maxim
|
||||
of operations required by your application (see
|
||||
xref:appendix.adoc#appendix-spring-properties[Supported Spring Properties]).
|
||||
|
||||
In addition, the result of a `BigDecimal` or `BigInteger` power operation within a SpEL
|
||||
expression cannot exceed 1,000,000 bits by default – approximately equivalent to a
|
||||
decimal number with 300,000 digits. Power operations involving large base values or large
|
||||
exponents can be computationally expensive, and this limit ensures that evaluations
|
||||
remain bounded; however, the `maximumBigPowerBits` value is configurable. If you create a
|
||||
`SpelExpressionParser` programmatically (the recommended approach), you can specify a
|
||||
custom `maximumBigPowerBits` value when creating the `SpelParserConfiguration` that you
|
||||
provide to the `SpelExpressionParser`. To remove this limit entirely, pass
|
||||
`Integer.MAX_VALUE` as the `maximumBigPowerBits` value. If you are not able to configure
|
||||
an explicit value for `maximumBigPowerBits` via `SpelParserConfiguration`, you can set a
|
||||
JVM system property or Spring property named `spring.expression.maxBigPowerBits` to the
|
||||
maximum result size in bits (see xref:appendix.adoc#appendix-spring-properties[Supported
|
||||
Spring Properties]).
|
||||
|
||||
[[expressions-spel-compilation]]
|
||||
== SpEL Compilation
|
||||
|
||||
@@ -81,6 +81,13 @@ public void sendNotification() {
|
||||
}
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
When `delay` is `0` combined with a positive `jitter`, the delay never grows
|
||||
regardless of any configured `multiplier`, so the full configured `jitter` is
|
||||
applied directly as a random delay in the range from `0` to `min(jitter, maxDelay)`.
|
||||
====
|
||||
|
||||
Last but not least, `@Retryable` also works for reactive methods with a reactive return
|
||||
type, decorating the pipeline with Reactor's retry capabilities:
|
||||
|
||||
@@ -263,6 +270,13 @@ and an exponential back-off strategy with a bit of jitter.
|
||||
() -> jmsClient.destination("notifications").send(...));
|
||||
----
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
When `delay` is zero combined with a positive `jitter`, the delay never grows
|
||||
regardless of any configured `multiplier`, so the full configured `jitter` is
|
||||
applied directly as a random delay in the range from zero to `min(jitter, maxDelay)`.
|
||||
====
|
||||
|
||||
[TIP]
|
||||
====
|
||||
Although the factory methods and builder API for `RetryPolicy` cover most common
|
||||
|
||||
+295
-4
@@ -68,12 +68,22 @@ 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)`.
|
||||
for example, `@MockitoBean(enforceOverride = true)`. Because this strategy replaces the
|
||||
bean directly, bypassing the container's normal bean post-processing, the resulting mock
|
||||
is a bare object: it is never wrapped in a Spring AOP proxy, even if the original bean
|
||||
would have been — for example, due to `@Transactional`, `@Cacheable`, or `@Retryable`. See
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean
|
||||
Overrides and Spring AOP Proxies] for details.
|
||||
|
||||
The `@MockitoSpyBean` annotation uses the `WRAP`
|
||||
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.
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy]:
|
||||
an early instance of the original bean is captured and used to create a Mockito spy.
|
||||
This strategy requires that exactly one candidate bean exists. In contrast to
|
||||
`@MockitoBean`, if the original bean would have been wrapped in a Spring AOP proxy, that
|
||||
proxy is still created — but it now wraps the spy instead of the original bean. See
|
||||
<<spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies,`@MockitoSpyBean` and Spring AOP Proxies>>
|
||||
for a diagram and further details on the consequences this has for stubbing and
|
||||
verification.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
@@ -468,3 +478,284 @@ Kotlin::
|
||||
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.
|
||||
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies]]
|
||||
== `@MockitoSpyBean` and Spring AOP Proxies
|
||||
|
||||
As explained in
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean
|
||||
Overrides and Spring AOP Proxies], if the bean being spied on would normally be wrapped in
|
||||
a Spring AOP proxy — for example, due to `@Transactional`, `@Cacheable`, or `@Retryable`
|
||||
— that proxy is still created, with the spy as its target. The bean injected into the
|
||||
test class and into other beans in the `ApplicationContext` is therefore the proxy, not
|
||||
the spy itself.
|
||||
|
||||
Verification via Mockito's `verify()` API is unaffected by this and works transparently,
|
||||
regardless of whether it is invoked on the proxy or on the underlying spy.
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies-stubbing]]
|
||||
=== Stubbing Through the Proxy
|
||||
|
||||
Stubbing requires more care than verification, since `Mockito.doReturn(...).when(...)`,
|
||||
`Mockito.doThrow(...).when(...)`, and similar methods behave differently depending on the
|
||||
nature of the AOP advice involved when invoked on the proxy.
|
||||
|
||||
NOTE: Since `when` is a reserved keyword in Kotlin, the Kotlin examples below use the
|
||||
`given(...)`, `willReturn(...)`, and `willThrow(...)` methods from `BDDMockito` instead
|
||||
of `Mockito.doReturn(...).when(...)` and `Mockito.doThrow(...).when(...)`.
|
||||
|
||||
Advice that does not retain state between invocations — such as
|
||||
xref:core/resilience.adoc#resilience-annotations-retryable[`@Retryable`] — has no adverse
|
||||
effect on stubbing. The following stubbing sequence, invoked on the proxy, behaves exactly
|
||||
as it would on the underlying spy directly, including triggering a retry when the thrown
|
||||
exception is encountered.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
doReturn("ok")
|
||||
.doThrow(new RuntimeException("Message delivery failed"))
|
||||
.doReturn("ok again")
|
||||
.when(clientService).sendMessage(any()); // <1>
|
||||
----
|
||||
<1> `clientService` is the injected proxy. Since `@Retryable` advice is a stateless
|
||||
pass-through, each call — including the one that throws — reaches the spy directly.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
willReturn("ok")
|
||||
.willThrow(RuntimeException("Message delivery failed"))
|
||||
.willReturn("ok again")
|
||||
.given(clientService).sendMessage(any()) // <1>
|
||||
----
|
||||
<1> `clientService` is the injected proxy. Since `@Retryable` advice is a stateless
|
||||
pass-through, each call — including the one that throws — reaches the spy directly.
|
||||
======
|
||||
|
||||
Advice that caches or otherwise memoizes the outcome of an invocation — such as
|
||||
`@Cacheable` — does not behave the same way. While a `doReturn(...)`, `doThrow(...)`, or
|
||||
similar declaration is being recorded, Mockito does not invoke the spy's real or
|
||||
previously stubbed behavior; instead, the invocation used to declare the stubbing returns
|
||||
an empty value (for example, `null`). If that invocation is made on the proxy, the caching
|
||||
advice caches this empty value, which then permanently shadows the spy for that
|
||||
combination of arguments — including for the very invocation that was supposed to
|
||||
configure the stubbing.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
doReturn(1L).when(dateService).getDate(false); // <1>
|
||||
dateService.getDate(false); // <2>
|
||||
----
|
||||
<1> `dateService` is the injected proxy. This invocation is intercepted by Mockito's
|
||||
stubbing infrastructure before it reaches the spy, so the caching advice ends up
|
||||
caching an empty value for argument `false`.
|
||||
<2> Returns the empty value cached by the previous invocation — not `1L` — because the
|
||||
cache was already populated.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
willReturn(1L).given(dateService).getDate(false) // <1>
|
||||
dateService.getDate(false) // <2>
|
||||
----
|
||||
<1> `dateService` is the injected proxy. This invocation is intercepted by Mockito's
|
||||
stubbing infrastructure before it reaches the spy, so the caching advice ends up
|
||||
caching an empty value for argument `false`.
|
||||
<2> Returns the empty value cached by the previous invocation — not `1L` — because the
|
||||
cache was already populated.
|
||||
======
|
||||
|
||||
To avoid this, stub directly on the spy instead of on the proxy, by unwrapping the proxy
|
||||
with
|
||||
{spring-framework-api}/test/util/AopTestUtils.html#getUltimateTargetObject(java.lang.Object)[`AopTestUtils.getUltimateTargetObject(...)`].
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
DateService spy = AopTestUtils.getUltimateTargetObject(dateService);
|
||||
doReturn(1L).when(spy).getDate(false);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val spy = AopTestUtils.getUltimateTargetObject<DateService>(dateService)
|
||||
willReturn(1L).given(spy).getDate(false)
|
||||
----
|
||||
======
|
||||
|
||||
[[spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies-disabling]]
|
||||
=== Disabling AOP Advice for Tests
|
||||
|
||||
Rather than working around the proxy as shown above, you may instead prefer to disable
|
||||
the underlying AOP advice for the duration of the test, while keeping `@Retryable`,
|
||||
`@Cacheable`, or similar annotations in place in production code. Common reasons include
|
||||
avoiding retry delays that slow down the test suite, or avoiding caching altogether so
|
||||
that every invocation reaches the spy directly — which also sidesteps the stubbing
|
||||
pitfall described above, without having to unwrap the proxy at all.
|
||||
|
||||
The general technique is to externalize whatever controls the advice's effective behavior
|
||||
— for example, the number of retry attempts or the `CacheManager` backing `@Cacheable`
|
||||
— and override that configuration for tests only, typically by using a bean override or a
|
||||
test-specific property. The proxy and its advice are still created, but their behavior is
|
||||
simply made a no-op or pure pass-through for the test.
|
||||
|
||||
For `@Retryable`, bind the `maxRetriesString` attribute to a property placeholder with a
|
||||
sensible default (so that production configuration is unaffected if the property is not
|
||||
set), and override that property in the test with
|
||||
xref:testing/annotations/integration-spring/annotation-testpropertysource.adoc[`@TestPropertySource`]
|
||||
so that no retries are attempted.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Retryable(maxRetriesString = "${sendMessage.maxRetries:3}", delay = 10)
|
||||
public String sendMessage(String request) {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@Retryable(maxRetriesString = "\${sendMessage.maxRetries:3}", delay = 10)
|
||||
fun sendMessage(request: String): String {
|
||||
// ...
|
||||
}
|
||||
----
|
||||
======
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig
|
||||
@TestPropertySource(properties = "sendMessage.maxRetries = 0") // <1>
|
||||
class ClientServiceTests {
|
||||
|
||||
@MockitoSpyBean
|
||||
ClientService clientService;
|
||||
|
||||
// test case body...
|
||||
}
|
||||
----
|
||||
<1> With no retries permitted, the first (and only) attempt is made, and a thrown
|
||||
exception propagates immediately, so the spy's stubbing chain behaves exactly as
|
||||
declared, including for `doThrow(...)` answers.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig
|
||||
@TestPropertySource(properties = ["sendMessage.maxRetries = 0"]) // <1>
|
||||
class ClientServiceTests {
|
||||
|
||||
@MockitoSpyBean
|
||||
lateinit var clientService: ClientService
|
||||
|
||||
// test case body...
|
||||
}
|
||||
----
|
||||
<1> With no retries permitted, the first (and only) attempt is made, and a thrown
|
||||
exception propagates immediately, so the spy's stubbing chain behaves exactly as
|
||||
declared, including for `doThrow(...)` answers.
|
||||
======
|
||||
|
||||
For `@Cacheable`, Spring provides
|
||||
{spring-framework-api}/cache/support/NoOpCacheManager.html[`NoOpCacheManager`] — a
|
||||
`CacheManager` that accepts cache entries but never actually stores them, so every
|
||||
invocation results in a cache miss and therefore an invocation of the target method.
|
||||
Overriding the `CacheManager` bean with a `NoOpCacheManager` — for example, with
|
||||
xref:testing/annotations/integration-spring/annotation-testbean.adoc[`@TestBean`] —
|
||||
effectively disables caching for the test without touching the `@Cacheable` annotation in
|
||||
production code.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig
|
||||
class DateServiceTests {
|
||||
|
||||
@MockitoSpyBean
|
||||
DateService dateService;
|
||||
|
||||
@TestBean // <1>
|
||||
CacheManager cacheManager;
|
||||
|
||||
static CacheManager cacheManager() { // <2>
|
||||
return new NoOpCacheManager();
|
||||
}
|
||||
|
||||
@Test
|
||||
void test() {
|
||||
doReturn(1L).when(dateService).getDate(false);
|
||||
assertThat(dateService.getDate(false)).isEqualTo(1L);
|
||||
|
||||
doReturn(2L).when(dateService).getDate(false);
|
||||
assertThat(dateService.getDate(false)).isEqualTo(2L); // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Override the `CacheManager` bean for this test.
|
||||
<2> Replace it with a `NoOpCacheManager`, so `@Cacheable` never actually caches anything.
|
||||
<3> No longer masked by a stale cache entry, since every call reaches the spy.
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@SpringJUnitConfig
|
||||
class DateServiceTests {
|
||||
|
||||
@MockitoSpyBean
|
||||
lateinit var dateService: DateService
|
||||
|
||||
@TestBean // <1>
|
||||
lateinit var cacheManager: CacheManager
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun cacheManager(): CacheManager { // <2>
|
||||
return NoOpCacheManager()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test() {
|
||||
willReturn(1L).given(dateService).getDate(false)
|
||||
assertThat(dateService.getDate(false)).isEqualTo(1L)
|
||||
|
||||
willReturn(2L).given(dateService).getDate(false)
|
||||
assertThat(dateService.getDate(false)).isEqualTo(2L) // <3>
|
||||
}
|
||||
}
|
||||
----
|
||||
<1> Override the `CacheManager` bean for this test.
|
||||
<2> Replace it with a `NoOpCacheManager`, so `@Cacheable` never actually caches anything.
|
||||
<3> No longer masked by a stale cache entry, since every call reaches the spy.
|
||||
======
|
||||
|
||||
+12
@@ -171,3 +171,15 @@ Similarly, when overriding a bean created by a `FactoryBean`, the `FactoryBean`
|
||||
replaced with a singleton bean corresponding to the value returned from the `@TestBean`
|
||||
factory method.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
`@TestBean` uses the `REPLACE` or `REPLACE_OR_CREATE`
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-strategy[strategy
|
||||
for bean overrides], which registers the value returned from the factory method directly
|
||||
as the bean, bypassing the container's normal bean post-processing. Consequently, none of
|
||||
the Spring AOP advice that would otherwise apply to the original bean (for example,
|
||||
`@Transactional`, `@Cacheable`, or `@Retryable`) is present on the override instance. See
|
||||
xref:testing/testcontext-framework/bean-overriding.adoc#testcontext-bean-overriding-aop-proxies[Bean
|
||||
Overrides and Spring AOP Proxies] for details.
|
||||
====
|
||||
|
||||
@@ -90,3 +90,67 @@ Alternatively, the user can directly provide the bean name in the custom annotat
|
||||
`BeanOverrideProcessor` implementations may also internally compute a bean name based on
|
||||
a convention or some other method.
|
||||
====
|
||||
|
||||
[[testcontext-bean-overriding-aop-proxies]]
|
||||
== Bean Overrides and Spring AOP Proxies
|
||||
|
||||
Beans in a Spring `ApplicationContext` are frequently wrapped in an AOP proxy — for
|
||||
example, to support `@Transactional`, `@Cacheable`, or `@Retryable` semantics. Whether an
|
||||
overridden bean retains such a proxy depends on the `BeanOverrideStrategy` used to create
|
||||
the override.
|
||||
|
||||
* Overrides that use the `REPLACE` or `REPLACE_OR_CREATE` strategy (such as `@TestBean`
|
||||
and `@MockitoBean`) register their override instance directly as a manual singleton,
|
||||
which bypasses the container's normal bean post-processing. Consequently, the override
|
||||
instance is a bare object: none of the AOP advice that would otherwise apply to the
|
||||
original bean (`@Transactional`, `@Cacheable`, `@Retryable`, method security, and so
|
||||
on) is present.
|
||||
* Overrides that use the `WRAP` strategy (such as `@MockitoSpyBean`) capture an early
|
||||
reference to the original bean and use it to create the override instance, before the
|
||||
rest of the container's post-processors — including the one responsible for creating
|
||||
AOP proxies — have run. Consequently, if the original bean would have been proxied,
|
||||
that proxy is still created, but it now wraps the override instance instead of the
|
||||
original bean. The bean that ends up in the `ApplicationContext`, and that is injected
|
||||
into collaborating beans and test classes, is therefore the AOP proxy, with the
|
||||
override instance as its target — not the bare override instance itself.
|
||||
|
||||
The following diagrams illustrate the resulting shape of the bean for each strategy, from
|
||||
the perspective of a caller invoking a method on the injected bean.
|
||||
|
||||
With the `REPLACE` or `REPLACE_OR_CREATE` strategy, there is no AOP proxy at all: the
|
||||
caller invokes the override instance directly.
|
||||
|
||||
[source]
|
||||
----
|
||||
caller
|
||||
│
|
||||
▼
|
||||
[ override instance ]
|
||||
----
|
||||
|
||||
With the `WRAP` strategy, any AOP proxy that would normally have wrapped the original
|
||||
bean is still created, but now wraps the override instance instead:
|
||||
|
||||
[source]
|
||||
----
|
||||
caller
|
||||
│
|
||||
▼
|
||||
[ AOP proxy ] (for example, retry, caching, or transaction advice)
|
||||
│
|
||||
│ delegates to its target
|
||||
▼
|
||||
[ override instance ] (for example, a Mockito spy created by @MockitoSpyBean)
|
||||
----
|
||||
|
||||
For a `WRAP`-based override such as `@MockitoSpyBean`, the "wrapping" performed by the
|
||||
AOP proxy is unrelated to the manner in which the resulting Mockito spy itself "wraps"
|
||||
the original bean instance it was created from. The proxy shown above determines which
|
||||
object a caller actually invokes, whereas the spy's relationship to the original
|
||||
instance only determines what happens when an unstubbed method is invoked on the spy: it
|
||||
falls through to that instance's real behavior.
|
||||
|
||||
This distinction has practical consequences when combining bean overrides with Mockito's
|
||||
stubbing and verification APIs. See
|
||||
xref:testing/annotations/integration-spring/annotation-mockitobean.adoc#spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies[`@MockitoSpyBean`
|
||||
and Spring AOP Proxies] for details.
|
||||
|
||||
@@ -99,6 +99,11 @@ mock to configure expectations on it and perform verifications. For Spring's cor
|
||||
utilities, see {spring-framework-api}/aop/support/AopUtils.html[`AopUtils`] and
|
||||
{spring-framework-api}/aop/framework/AopProxyUtils.html[`AopProxyUtils`].
|
||||
|
||||
TIP: For guidance on using `AopTestUtils` together with `@MockitoSpyBean` when the spied
|
||||
bean is wrapped in a Spring AOP proxy, see
|
||||
xref:testing/annotations/integration-spring/annotation-mockitobean.adoc#spring-testing-annotation-beanoverriding-mockitospybean-aop-proxies[`@MockitoSpyBean`
|
||||
and Spring AOP Proxies].
|
||||
|
||||
{spring-framework-api}/test/util/ReflectionTestUtils.html[`ReflectionTestUtils`] is a
|
||||
collection of reflection-based utility methods. You can use these methods in testing
|
||||
scenarios where you need to change the value of a constant, set a non-`public` field,
|
||||
|
||||
@@ -338,21 +338,26 @@ include::partial$web/forwarded-headers.adoc[]
|
||||
[[webflux-forwarded-headers-transformer]]
|
||||
=== ForwardedHeaderTransformer
|
||||
|
||||
`ForwardedHeaderTransformer` is a component that modifies the host, port, and scheme of
|
||||
the request, based on forwarded headers, and then removes those headers. If you declare
|
||||
it as a bean with the name `forwardedHeaderTransformer`, it will be
|
||||
`ForwardedHeaderTransformer` is a component that modifies the request to match information
|
||||
from the standard `"Forwarded"` or `"X-Forwarded"` headers, and also removes those headers
|
||||
to eliminate further impact. If you declare it as a bean with the name
|
||||
`forwardedHeaderTransformer`, it will be
|
||||
xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api-special-beans[detected] and used.
|
||||
|
||||
|
||||
[[webflux-forwarded-headers-security]]
|
||||
=== Security Considerations
|
||||
|
||||
There are security considerations for forwarded headers since an application cannot know
|
||||
if the headers were added by a proxy, as intended, or by a malicious client. This is why
|
||||
a proxy at the boundary of trust should be configured to remove untrusted forwarded traffic coming
|
||||
from the outside. You can also configure the `ForwardedHeaderTransformer` with
|
||||
`removeOnly=true`, in which case it removes but does not use the headers.
|
||||
Forwarded headers are intended to be set by trusted proxies and never allowed in from the
|
||||
outside. A proxy at the edge of trust must remove forwarded headers including both the
|
||||
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
|
||||
to protect applications which may check both.
|
||||
|
||||
When creating `ForwardedHeaderTransformer` you can specify whether to use the
|
||||
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the transformer
|
||||
lets you turn use of `"X-Forwarded-Prefix"` on and off.
|
||||
|
||||
`ForwardedHeaderTransformer` can be configured in `removeOnly` mode, in which case it removes
|
||||
forwarded headers from the request without using them.
|
||||
|
||||
[[webflux-filters]]
|
||||
== Filters
|
||||
|
||||
@@ -24,7 +24,7 @@ ignoring further handling via `FORWARD` dispatches. The filter also provides con
|
||||
over whether the `Filter` gets involved in `ASYNC` and `ERROR` dispatches.
|
||||
|
||||
Servlet filters can be configured in `web.xml` or via Servlet annotations.
|
||||
In a Spring Boot application , you can
|
||||
In a Spring Boot application, you can
|
||||
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare Filter's as beans]
|
||||
and Boot will have them configured.
|
||||
|
||||
@@ -51,20 +51,29 @@ include::partial$web/forwarded-headers.adoc[]
|
||||
[[filters-forwarded-headers-non-forwardedheaderfilter]]
|
||||
=== ForwardedHeaderFilter
|
||||
|
||||
`ForwardedHeaderFilter` is a Servlet filter that modifies the request in order to
|
||||
a) change the host, port, and scheme based on `Forwarded` headers, and b) to remove those
|
||||
headers to eliminate further impact. The filter relies on wrapping the request, and
|
||||
therefore it must be ordered ahead of other filters, such as `RequestContextFilter`, that
|
||||
should work with the modified and not the original request.
|
||||
`ForwardedHeaderFilter` is a Servlet filter that modifies the request to match information
|
||||
from the standard `"Forwarded"` or `"X-Forwarded"` headers, and also removes those headers
|
||||
to eliminate further impact. The filter wraps the request and must be ordered ahead
|
||||
of other filters such as `RequestContextFilter` in order for all downstream
|
||||
handlers to see the modified request.
|
||||
|
||||
|
||||
[[filters-forwarded-headers-security]]
|
||||
=== Security Considerations
|
||||
|
||||
There are security considerations for forwarded headers since an application cannot know
|
||||
if the headers were added by a proxy, as intended, or by a malicious client. This is why
|
||||
a proxy at the boundary of trust should be configured to remove untrusted `Forwarded`
|
||||
headers that come from the outside. You can also configure the `ForwardedHeaderFilter`
|
||||
with `removeOnly=true`, in which case it removes but does not use the headers.
|
||||
Forwarded headers are intended to be set by trusted proxies and never allowed in from the
|
||||
outside. A proxy at the edge of trust must remove forwarded headers including both the
|
||||
standard `"Forwarded"` and `"X-Forwarded"` headers, regardless of which one they use,
|
||||
to protect applications which may check both.
|
||||
|
||||
When creating `ForwardedHeaderFilter` you can specify whether to use the
|
||||
standard `"Forwarded"` or `"X-Forwarded"` headers. A separate property on the filter
|
||||
lets you turn use of `"X-Forwarded-Prefix"` on and off.
|
||||
|
||||
`ForwardedHeaderFilter` can be configured in `removeOnly` mode, in which case it removes
|
||||
forwarded headers from the request without using them.
|
||||
|
||||
|
||||
|
||||
[[filters-forwarded-headers-dispatcher]]
|
||||
=== Dispatcher Types
|
||||
|
||||
@@ -112,8 +112,8 @@ Kotlin::
|
||||
|
||||
NOTE: As of 5.1, `ServletUriComponentsBuilder` ignores information from the `Forwarded` and
|
||||
`X-Forwarded-*` headers, which specify the client-originated address. Consider using the
|
||||
xref:web/webmvc/filters.adoc#filters-forwarded-headers[`ForwardedHeaderFilter`] to extract and use or to discard
|
||||
such headers.
|
||||
xref:web/webmvc/filters.adoc#filters-forwarded-headers[`ForwardedHeaderFilter`]
|
||||
to extract and use or to discard such headers.
|
||||
|
||||
|
||||
[[mvc-links-to-controllers]]
|
||||
@@ -261,8 +261,8 @@ Kotlin::
|
||||
|
||||
NOTE: As of 5.1, `MvcUriComponentsBuilder` ignores information from the `Forwarded` and
|
||||
`X-Forwarded-*` headers, which specify the client-originated address. Consider using the
|
||||
xref:web/webmvc/filters.adoc#filters-forwarded-headers[ForwardedHeaderFilter] to extract and use or to discard
|
||||
such headers.
|
||||
xref:web/webmvc/filters.adoc#filters-forwarded-headers[ForwardedHeaderFilter] to extract
|
||||
and use or to discard such headers.
|
||||
|
||||
|
||||
[[mvc-links-to-controllers-from-views]]
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
As a request goes through proxies such as load balancers the host, port, and
|
||||
scheme may change, and that makes it a challenge to create links that point to the correct
|
||||
host, port, and scheme from a client perspective.
|
||||
As a request goes through a chain of proxies, request details such as the scheme, host,
|
||||
port, remote address, and local address change. Proxies can insert headers that keep track of
|
||||
the hops, and that can help to restore the request from the original client's perspective.
|
||||
This allows an application to create self-reference links for external clients.
|
||||
|
||||
There are two alternatives for headers that proxies can use:
|
||||
|
||||
- {rfc-site}/rfc7239[RFC 7239] defines the `"Forwarded"` HTTP header, a single header with
|
||||
individual attributes for each component in the chain of proxied requests with the
|
||||
following syntax.
|
||||
- `"X-Forwarded-"` prefixed headers are the original approach that predates the standard
|
||||
and uses a separate header for each request component.
|
||||
|
||||
The Spring Framework supports both approaches. Most proxies today support the original
|
||||
`"X-Forwarded"` headers only as a de facto standard.
|
||||
|
||||
WARNING: For maximum security, a proxy at the edge of trust must be configured to reset both
|
||||
the standard `"Forwarded"` and `"X-Forwarded-"` headers regardless of which ones are chosen
|
||||
for use. Likewise, when configuring forwarded header handling in Spring, you need to indicate
|
||||
which type of headers to use. More on security considerations later in this section.
|
||||
|
||||
|
||||
{rfc-site}/rfc7239[RFC 7239] defines the `Forwarded` HTTP header
|
||||
that proxies can use to provide information about the original request.
|
||||
|
||||
[[forwarded-headers-non-standard]]
|
||||
=== Non-standard Headers
|
||||
=== X-Forwarded Headers
|
||||
|
||||
There are other non-standard headers, too, including `X-Forwarded-Host`, `X-Forwarded-Port`,
|
||||
`X-Forwarded-Proto`, `X-Forwarded-Ssl`, `X-Forwarded-Prefix`, and `X-Forwarded-For`.
|
||||
This section describes supported `"X-Forwarded"` headers.
|
||||
|
||||
[[x-forwarded-host]]
|
||||
==== X-Forwarded-Host
|
||||
|
||||
@@ -8,15 +8,15 @@ javaPlatform {
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.20.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.6"))
|
||||
api(platform("io.netty:netty-bom:4.2.15.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.6"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.16.7"))
|
||||
api(platform("io.netty:netty-bom:4.2.17.Final"))
|
||||
api(platform("io.projectreactor:reactor-bom:2025.0.7"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.5"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.6"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.26.0"))
|
||||
api(platform("org.apache.groovy:groovy-bom:5.0.8"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.26.1"))
|
||||
api(platform("org.assertj:assertj-bom:3.27.7"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.9"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.9"))
|
||||
api(platform("org.eclipse.jetty:jetty-bom:12.1.12"))
|
||||
api(platform("org.eclipse.jetty.ee11:jetty-ee11-bom:12.1.12"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.10.2"))
|
||||
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.9.0"))
|
||||
api(platform("org.junit:junit-bom:6.0.3"))
|
||||
@@ -26,12 +26,12 @@ dependencies {
|
||||
constraints {
|
||||
api("com.fasterxml:aalto-xml:1.3.4")
|
||||
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.3")
|
||||
api("com.github.ben-manes.caffeine:caffeine:3.2.4")
|
||||
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.13.2")
|
||||
api("com.google.protobuf:protobuf-java-util:4.34.1")
|
||||
api("com.google.protobuf:protobuf-java-util:4.35.1")
|
||||
api("com.h2database:h2:2.4.240")
|
||||
api("com.jayway.jsonpath:json-path:2.10.0")
|
||||
api("com.networknt:json-schema-validator:1.5.3")
|
||||
@@ -96,10 +96,10 @@ dependencies {
|
||||
api("org.apache.httpcomponents.client5:httpclient5:5.6")
|
||||
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.4.2")
|
||||
api("org.apache.poi:poi-ooxml:5.5.1")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.22")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.22")
|
||||
api("org.apache.tomcat:tomcat-util:11.0.22")
|
||||
api("org.apache.tomcat:tomcat-websocket:11.0.22")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-core:11.0.24")
|
||||
api("org.apache.tomcat.embed:tomcat-embed-websocket:11.0.24")
|
||||
api("org.apache.tomcat:tomcat-util:11.0.24")
|
||||
api("org.apache.tomcat:tomcat-websocket:11.0.24")
|
||||
api("org.aspectj:aspectjrt:1.9.25")
|
||||
api("org.aspectj:aspectjtools:1.9.25")
|
||||
api("org.aspectj:aspectjweaver:1.9.25")
|
||||
@@ -110,8 +110,8 @@ dependencies {
|
||||
api("org.dom4j:dom4j:2.2.0")
|
||||
api("org.easymock:easymock:5.6.0")
|
||||
api("org.eclipse.angus:angus-mail:2.0.3")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.4")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0")
|
||||
api("org.eclipse.jetty:jetty-reactive-httpclient:4.1.5")
|
||||
api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.1")
|
||||
api("org.eclipse:yasson:3.0.4")
|
||||
api("org.ehcache:ehcache:3.10.8")
|
||||
api("org.ehcache:jcache:1.0.1")
|
||||
@@ -120,8 +120,8 @@ dependencies {
|
||||
api("org.glassfish:jakarta.el:4.0.2")
|
||||
api("org.graalvm.sdk:graal-sdk:22.3.1")
|
||||
api("org.hamcrest:hamcrest:3.0")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.17.Final")
|
||||
api("org.hibernate.validator:hibernate-validator:9.1.0.Final")
|
||||
api("org.hibernate.orm:hibernate-core:7.2.24.Final")
|
||||
api("org.hibernate.validator:hibernate-validator:9.1.3.Final")
|
||||
api("org.hsqldb:hsqldb:2.7.4")
|
||||
api("org.htmlunit:htmlunit:4.21.0")
|
||||
api("org.javamoney:moneta:1.4.4")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=7.0.8-SNAPSHOT
|
||||
version=7.0.9
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
@@ -7,7 +7,6 @@ apply plugin: 'eclipse'
|
||||
eclipse.jdt {
|
||||
sourceCompatibility = 17
|
||||
targetCompatibility = 17
|
||||
javaRuntimeName = "JavaSE-17"
|
||||
}
|
||||
|
||||
// Replace classpath entries with project dependencies (GRADLE-1116)
|
||||
|
||||
@@ -84,6 +84,8 @@ javadoc {
|
||||
// cross-module @see and @link references which are only reachable
|
||||
// when running the global 'framework-api:javadoc' task.
|
||||
addBooleanOption('Werror', false)
|
||||
// do not ship 4MB of web fonts for single modules
|
||||
addBooleanOption("-no-fonts", true)
|
||||
}
|
||||
|
||||
// Attempt to suppress warnings due to cross-module @see and @link references.
|
||||
|
||||
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-9.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -29,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
|
||||
Vendored
+2
-2
@@ -19,7 +19,7 @@
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
plugins {
|
||||
id "io.spring.develocity.conventions" version "0.0.22"
|
||||
id "io.spring.develocity.conventions" version "0.0.25"
|
||||
}
|
||||
|
||||
include "spring-aop"
|
||||
|
||||
+5
@@ -649,6 +649,11 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
else if (value instanceof List list) {
|
||||
int index = Integer.parseInt(key);
|
||||
growCollectionIfNecessary(list, index, indexedPropertyName.toString(), ph, i + 1);
|
||||
if (index < 0 || index >= list.size()) {
|
||||
throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
|
||||
"Cannot get element with index " + index + " from List of size " +
|
||||
list.size() + ", accessed using property path '" + propertyName + "'");
|
||||
}
|
||||
value = list.get(index);
|
||||
}
|
||||
else if (value instanceof Map map) {
|
||||
|
||||
@@ -49,7 +49,7 @@ import java.beans.PropertyDescriptor;
|
||||
public interface BeanWrapper extends ConfigurablePropertyAccessor {
|
||||
|
||||
/**
|
||||
* Specify a limit for array and collection auto-growing.
|
||||
* Specify a limit for array and collection/set/list auto-growing.
|
||||
* <p>Default is unlimited on a plain BeanWrapper.
|
||||
* @since 4.1
|
||||
*/
|
||||
|
||||
@@ -295,8 +295,13 @@ class TypeConverterDelegate {
|
||||
ClassLoader cl = this.targetObject.getClass().getClassLoader();
|
||||
try {
|
||||
Class<?> enumValueType = ClassUtils.forName(enumType, cl);
|
||||
Field enumField = enumValueType.getField(fieldName);
|
||||
convertedValue = enumField.get(null);
|
||||
if (enumValueType.isEnum()) {
|
||||
Field enumField = enumValueType.getField(fieldName);
|
||||
convertedValue = enumField.get(null);
|
||||
}
|
||||
else if (logger.isTraceEnabled()) {
|
||||
logger.trace("Specified enum class [" + enumType + "] is not a Java enum");
|
||||
}
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
@@ -313,8 +318,7 @@ class TypeConverterDelegate {
|
||||
|
||||
if (convertedValue == currentConvertedValue) {
|
||||
// Try field lookup as fallback: for Java enum or custom enum
|
||||
// with values defined as static fields. Resulting value still needs
|
||||
// to be checked, hence we don't return it right away.
|
||||
// with values defined as static fields.
|
||||
try {
|
||||
Field enumField = requiredType.getField(trimmedValue);
|
||||
ReflectionUtils.makeAccessible(enumField);
|
||||
|
||||
+49
-1
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.beans;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -183,7 +184,34 @@ class BeanWrapperAutoGrowingTests {
|
||||
wrapper.setAutoGrowCollectionLimit(2);
|
||||
assertThatExceptionOfType(InvalidPropertyException.class)
|
||||
.isThrownBy(() -> wrapper.getPropertyValue("list[4]"))
|
||||
.withRootCauseInstanceOf(IndexOutOfBoundsException.class);
|
||||
.withMessageContainingAll(
|
||||
"Invalid property 'list[4]'",
|
||||
"Cannot get element with index 4 from List of size 0");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPropertyValueSelfPopulatingListWorksWithinLimit() {
|
||||
bean.setList(new SelfPopulatingList());
|
||||
assertThat(wrapper.getPropertyValue("list[2]")).isInstanceOf(Bean.class);
|
||||
assertThat(bean.getList())
|
||||
.hasSize(3)
|
||||
.allSatisfy(entry -> assertThat(entry).isInstanceOf(Bean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getPropertyValueSelfPopulatingListFailsAgainstLimit() {
|
||||
bean.setList(new SelfPopulatingList());
|
||||
wrapper.setAutoGrowCollectionLimit(2);
|
||||
assertThatExceptionOfType(InvalidPropertyException.class)
|
||||
.isThrownBy(() -> wrapper.getPropertyValue("list[4]"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void setPropertyValueSelfPopulatingListFailsAgainstLimitForNestedPath() {
|
||||
bean.setList(new SelfPopulatingList());
|
||||
wrapper.setAutoGrowCollectionLimit(2);
|
||||
assertThatExceptionOfType(InvalidPropertyException.class)
|
||||
.isThrownBy(() -> wrapper.setPropertyValue("list[4].prop", "test"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -382,4 +410,24 @@ class BeanWrapperAutoGrowingTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link List} implementation that creates elements on demand in {@link #get(int)}
|
||||
* instead of throwing {@link IndexOutOfBoundsException} for out-of-range indexes.
|
||||
*
|
||||
* <p>Used to verify that {@link BeanWrapperImpl} does not delegate to
|
||||
* {@link List#get(int)} for indexes beyond the configured auto-grow limit.
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
private static class SelfPopulatingList extends ArrayList<Bean> {
|
||||
|
||||
@Override
|
||||
public Bean get(int index) {
|
||||
while (size() <= index) {
|
||||
add(new Bean());
|
||||
}
|
||||
return super.get(index);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -687,7 +687,7 @@ class BeanWrapperGenericsTests {
|
||||
}
|
||||
|
||||
|
||||
public class Bar implements Foo {
|
||||
public static class Bar implements Foo {
|
||||
|
||||
private double version;
|
||||
|
||||
@@ -710,7 +710,7 @@ class BeanWrapperGenericsTests {
|
||||
}
|
||||
|
||||
|
||||
public class Promotion implements ObjectWithId<Long> {
|
||||
public static class Promotion implements ObjectWithId<Long> {
|
||||
|
||||
private Long id;
|
||||
|
||||
|
||||
@@ -934,7 +934,7 @@ class ExtendedBeanInfoTests {
|
||||
}
|
||||
|
||||
|
||||
abstract class Library {
|
||||
abstract static class Library {
|
||||
|
||||
public Book getBook() {
|
||||
return null;
|
||||
@@ -945,7 +945,7 @@ class ExtendedBeanInfoTests {
|
||||
}
|
||||
|
||||
|
||||
class LawLibrary extends Library implements TextBookOperations {
|
||||
static class LawLibrary extends Library implements TextBookOperations {
|
||||
|
||||
@Override
|
||||
public LawBook getBook() {
|
||||
|
||||
+3
-2
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.beans.factory.config;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -150,7 +151,7 @@ class YamlProcessorTests {
|
||||
|
||||
@Test
|
||||
void customTypeNotSupportedByDefault() throws Exception {
|
||||
URL url = new URL("https://localhost:9000/");
|
||||
URL url = URI.create("https://localhost:9000/").toURL();
|
||||
setYaml("value: !!java.net.URL [\"" + url + "\"]");
|
||||
assertThatExceptionOfType(ComposerException.class)
|
||||
.isThrownBy(() -> this.processor.process((properties, map) -> {}))
|
||||
@@ -161,7 +162,7 @@ class YamlProcessorTests {
|
||||
void customTypesSupportedDueToExplicitConfiguration() throws Exception {
|
||||
this.processor.setSupportedTypes(URL.class, String.class);
|
||||
|
||||
URL url = new URL("https://localhost:9000/");
|
||||
URL url = URI.create("https://localhost:9000/").toURL();
|
||||
setYaml("value: !!java.net.URL [!!java.lang.String [\"" + url + "\"]]");
|
||||
|
||||
this.processor.process((properties, map) -> {
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ class AutowireUtilsTests {
|
||||
public interface MyInterfaceType<T> {
|
||||
}
|
||||
|
||||
public class MySimpleInterfaceType implements MyInterfaceType<String> {
|
||||
public static class MySimpleInterfaceType implements MyInterfaceType<String> {
|
||||
}
|
||||
|
||||
public static class MyTypeWithMethods<T> {
|
||||
|
||||
+4
-4
@@ -523,7 +523,7 @@ class BeanFactoryGenericsTests {
|
||||
new ClassPathResource("genericBeanTests.xml", getClass()));
|
||||
|
||||
NamedUrlList list = bf.getBean("list", NamedUrlList.class);
|
||||
assertThat(list).containsExactly(new URL("http://localhost:8080"));
|
||||
assertThat(list).containsExactly(URI.create("http://localhost:8080").toURL());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -533,7 +533,7 @@ class BeanFactoryGenericsTests {
|
||||
new ClassPathResource("genericBeanTests.xml", getClass()));
|
||||
|
||||
NamedUrlSet set = bf.getBean("set", NamedUrlSet.class);
|
||||
assertThat(set).containsExactly(new URL("http://localhost:8080"));
|
||||
assertThat(set).containsExactly(URI.create("http://localhost:8080").toURL());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -543,7 +543,7 @@ class BeanFactoryGenericsTests {
|
||||
new ClassPathResource("genericBeanTests.xml", getClass()));
|
||||
|
||||
NamedUrlMap map = bf.getBean("map", NamedUrlMap.class);
|
||||
assertThat(map).containsExactly(entry(10, new URL("http://localhost:8080")));
|
||||
assertThat(map).containsExactly(entry(10, URI.create("http://localhost:8080").toURL()));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -577,7 +577,7 @@ class BeanFactoryGenericsTests {
|
||||
new ClassPathResource("genericBeanTests.xml", getClass()));
|
||||
|
||||
UrlSet urlSet = bf.getBean("setBean", UrlSet.class);
|
||||
assertThat(urlSet).containsExactly(new URL("https://www.springframework.org"));
|
||||
assertThat(urlSet).containsExactly(URI.create("https://www.springframework.org").toURL());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+3
@@ -72,6 +72,9 @@ public class SpringTemplateLoader implements TemplateLoader {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Looking for FreeMarker template with name [" + name + "]");
|
||||
}
|
||||
if (name.indexOf('\\') != -1) {
|
||||
return null;
|
||||
}
|
||||
Resource resource = this.resourceLoader.getResource(this.templateLoaderPath + name);
|
||||
return (resource.exists() ? resource : null);
|
||||
}
|
||||
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2002-present the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.ui.freemarker;
|
||||
|
||||
import java.io.File;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link SpringTemplateLoader}.
|
||||
*
|
||||
* @author Sébastien Deleuze
|
||||
*/
|
||||
class SpringTemplateLoaderTests {
|
||||
|
||||
@Test
|
||||
void findTemplateSourceResolvesTemplateInsidePath(@TempDir Path tempDir) throws Exception {
|
||||
Path templates = Files.createDirectory(tempDir.resolve("templates"));
|
||||
Files.writeString(templates.resolve("hello.ftl"), "Hello");
|
||||
SpringTemplateLoader loader = new SpringTemplateLoader(new DefaultResourceLoader(),
|
||||
"file:" + templates.toAbsolutePath() + File.separator);
|
||||
assertThat(loader.findTemplateSource("hello.ftl")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void findTemplateSourceRejectsBackslash(@TempDir Path tempDir) throws Exception {
|
||||
Path templates = Files.createDirectory(tempDir.resolve("templates"));
|
||||
Files.writeString(tempDir.resolve("other.txt"), "other");
|
||||
SpringTemplateLoader loader = new SpringTemplateLoader(new DefaultResourceLoader(),
|
||||
"file:" + templates.toAbsolutePath() + File.separator);
|
||||
assertThat(loader.findTemplateSource("..\\other.txt")).isNull();
|
||||
}
|
||||
|
||||
}
|
||||
+41
-16
@@ -17,15 +17,19 @@
|
||||
package org.springframework.context.support;
|
||||
|
||||
import java.text.MessageFormat;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Base class for message source implementations, providing support infrastructure
|
||||
@@ -41,6 +45,9 @@ import org.springframework.util.ObjectUtils;
|
||||
*/
|
||||
public abstract class MessageSourceSupport {
|
||||
|
||||
static final Set<Locale> JVM_LOCALES = Arrays.stream(Locale.getAvailableLocales()).
|
||||
filter(l -> StringUtils.hasLength(l.getLanguage())).collect(Collectors.toSet());
|
||||
|
||||
private static final MessageFormat INVALID_MESSAGE_FORMAT = new MessageFormat("");
|
||||
|
||||
/** Logger available to subclasses. */
|
||||
@@ -116,22 +123,17 @@ public abstract class MessageSourceSupport {
|
||||
if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) {
|
||||
return msg;
|
||||
}
|
||||
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
|
||||
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
|
||||
MessageFormat messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> {
|
||||
try {
|
||||
return createMessageFormat(msg, locale);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Invalid message format - probably not intended for formatting,
|
||||
// rather using a message structure with no arguments involved...
|
||||
if (isAlwaysUseMessageFormat()) {
|
||||
throw ex;
|
||||
}
|
||||
// Silently proceed with raw message if format not enforced...
|
||||
return INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
});
|
||||
|
||||
MessageFormat messageFormat;
|
||||
if (locale != null && JVM_LOCALES.contains(locale)) {
|
||||
Map<Locale, MessageFormat> messageFormatsPerLocale = this.messageFormatsPerMessage
|
||||
.computeIfAbsent(msg, key -> new ConcurrentHashMap<>());
|
||||
messageFormat = messageFormatsPerLocale.computeIfAbsent(locale, key -> resolveMessageFormat(msg, key));
|
||||
}
|
||||
else {
|
||||
messageFormat = resolveMessageFormat(msg, locale);
|
||||
}
|
||||
|
||||
if (messageFormat == INVALID_MESSAGE_FORMAT) {
|
||||
return msg;
|
||||
}
|
||||
@@ -140,6 +142,29 @@ public abstract class MessageSourceSupport {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a {@code MessageFormat} for the given message and Locale.
|
||||
* @param msg the message to create a {@code MessageFormat} for
|
||||
* @param locale the Locale to create a {@code MessageFormat} for
|
||||
* @return the {@code MessageFormat} instance, or otherwise
|
||||
* {@link #INVALID_MESSAGE_FORMAT} if not resolvable
|
||||
* @see #createMessageFormat
|
||||
*/
|
||||
private MessageFormat resolveMessageFormat(String msg, @Nullable Locale locale) {
|
||||
try {
|
||||
return createMessageFormat(msg, locale);
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
// Invalid message format - probably not intended for formatting,
|
||||
// rather using a message structure with no arguments involved...
|
||||
if (isAlwaysUseMessageFormat()) {
|
||||
throw ex;
|
||||
}
|
||||
// Silently proceed with raw message if format not enforced...
|
||||
return INVALID_MESSAGE_FORMAT;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code MessageFormat} for the given message and Locale.
|
||||
* @param msg the message to create a {@code MessageFormat} for
|
||||
|
||||
+67
-45
@@ -40,6 +40,7 @@ import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ConcurrentLruCache;
|
||||
import org.springframework.util.DefaultPropertiesPersister;
|
||||
import org.springframework.util.PropertiesPersister;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -110,15 +111,19 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
|
||||
private ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
// Cache to hold filename lists per Locale
|
||||
// Cache to hold filename lists per Locale.
|
||||
private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFilenames = new ConcurrentHashMap<>();
|
||||
|
||||
// Cache to hold already loaded properties per filename
|
||||
// Cache to hold already loaded properties per filename.
|
||||
private final ConcurrentMap<String, PropertiesHolder> cachedProperties = new ConcurrentHashMap<>();
|
||||
|
||||
// Cache to hold already loaded properties per filename
|
||||
// Cache to hold already merged properties per Locale.
|
||||
private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap<>();
|
||||
|
||||
// Cache to hold merged properties per non-JVM Locale.
|
||||
private final ConcurrentLruCache<Locale, PropertiesHolder> customLocaleProperties =
|
||||
new ConcurrentLruCache<>(64, locale -> mergeProperties(collectPropertiesToMerge(locale)));
|
||||
|
||||
|
||||
/**
|
||||
* Set the list of supported file extensions.
|
||||
@@ -197,20 +202,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) {
|
||||
if (getCacheMillis() < 0) {
|
||||
PropertiesHolder propHolder = getMergedProperties(locale);
|
||||
String result = propHolder.getProperty(code);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
return propHolder.getProperty(code);
|
||||
}
|
||||
else {
|
||||
for (String basename : getBasenameSet()) {
|
||||
List<String> filenames = calculateAllFilenames(basename, locale);
|
||||
for (String filename : filenames) {
|
||||
PropertiesHolder propHolder = getProperties(filename);
|
||||
String result = propHolder.getProperty(code);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (String basename : getBasenameSet()) {
|
||||
List<String> filenames = calculateAllFilenames(basename, locale);
|
||||
for (String filename : filenames) {
|
||||
PropertiesHolder propHolder = getProperties(filename, locale);
|
||||
String result = propHolder.getProperty(code);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -225,20 +226,16 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
protected @Nullable MessageFormat resolveCode(String code, Locale locale) {
|
||||
if (getCacheMillis() < 0) {
|
||||
PropertiesHolder propHolder = getMergedProperties(locale);
|
||||
MessageFormat result = propHolder.getMessageFormat(code, locale);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
return propHolder.getMessageFormat(code, locale);
|
||||
}
|
||||
else {
|
||||
for (String basename : getBasenameSet()) {
|
||||
List<String> filenames = calculateAllFilenames(basename, locale);
|
||||
for (String filename : filenames) {
|
||||
PropertiesHolder propHolder = getProperties(filename);
|
||||
MessageFormat result = propHolder.getMessageFormat(code, locale);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
|
||||
for (String basename : getBasenameSet()) {
|
||||
List<String> filenames = calculateAllFilenames(basename, locale);
|
||||
for (String filename : filenames) {
|
||||
PropertiesHolder propHolder = getProperties(filename, locale);
|
||||
MessageFormat result = propHolder.getMessageFormat(code, locale);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,12 +258,18 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
if (mergedHolder != null) {
|
||||
return mergedHolder;
|
||||
}
|
||||
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
|
||||
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
|
||||
if (existing != null) {
|
||||
mergedHolder = existing;
|
||||
|
||||
if (JVM_LOCALES.contains(locale)) {
|
||||
mergedHolder = mergeProperties(collectPropertiesToMerge(locale));
|
||||
PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
|
||||
if (existing != null) {
|
||||
mergedHolder = existing;
|
||||
}
|
||||
return mergedHolder;
|
||||
}
|
||||
else {
|
||||
return this.customLocaleProperties.get(locale);
|
||||
}
|
||||
return mergedHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -285,7 +288,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
List<String> filenames = calculateAllFilenames(basenames[i], locale);
|
||||
for (int j = filenames.size() - 1; j >= 0; j--) {
|
||||
String filename = filenames.get(j);
|
||||
PropertiesHolder propHolder = getProperties(filename);
|
||||
PropertiesHolder propHolder = getProperties(filename, locale);
|
||||
if (propHolder.getProperties() != null) {
|
||||
holders.add(propHolder);
|
||||
}
|
||||
@@ -334,11 +337,11 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
}
|
||||
}
|
||||
|
||||
// Filenames for given Locale
|
||||
// Filenames for given Locale.
|
||||
List<String> filenames = new ArrayList<>(7);
|
||||
filenames.addAll(calculateFilenamesForLocale(basename, locale));
|
||||
|
||||
// Filenames for default Locale, if any
|
||||
// Filenames for default Locale, if any.
|
||||
Locale defaultLocale = getDefaultLocale();
|
||||
if (defaultLocale != null && !defaultLocale.equals(locale)) {
|
||||
List<String> fallbackFilenames = calculateFilenamesForLocale(basename, defaultLocale);
|
||||
@@ -350,24 +353,27 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
}
|
||||
}
|
||||
|
||||
// Filename for default bundle file
|
||||
// Filename for default bundle file.
|
||||
filenames.add(basename);
|
||||
|
||||
if (localeMap == null) {
|
||||
localeMap = new ConcurrentHashMap<>();
|
||||
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
|
||||
if (existing != null) {
|
||||
localeMap = existing;
|
||||
if (JVM_LOCALES.contains(locale)) {
|
||||
if (localeMap == null) {
|
||||
localeMap = new ConcurrentHashMap<>();
|
||||
Map<Locale, List<String>> existing = this.cachedFilenames.putIfAbsent(basename, localeMap);
|
||||
if (existing != null) {
|
||||
localeMap = existing;
|
||||
}
|
||||
}
|
||||
localeMap.put(locale, filenames);
|
||||
}
|
||||
localeMap.put(locale, filenames);
|
||||
|
||||
return filenames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the filenames for the given bundle basename and Locale,
|
||||
* appending language code, country code, and variant code.
|
||||
* <p>For example, basename "messages", Locale "de_AT_oo" → "messages_de_AT_OO",
|
||||
* <p>For example, basename "messages", Locale "de_AT_OO" → "messages_de_AT_OO",
|
||||
* "messages_de_AT", "messages_de".
|
||||
* <p>Follows the rules defined by {@link java.util.Locale#toString()}.
|
||||
* @param basename the basename of the bundle
|
||||
@@ -402,6 +408,22 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a PropertiesHolder for the given filename, either from the
|
||||
* cache or freshly loaded.
|
||||
* @param filename the bundle filename (basename + Locale)
|
||||
* @param locale the requested locale (for cache filtering)
|
||||
* @return the current PropertiesHolder for the bundle
|
||||
* @see #getProperties(String)
|
||||
*/
|
||||
private PropertiesHolder getProperties(String filename, Locale locale) {
|
||||
PropertiesHolder propHolder = getProperties(filename);
|
||||
if (propHolder.getProperties() == null && !JVM_LOCALES.contains(locale)) {
|
||||
this.cachedProperties.remove(filename);
|
||||
}
|
||||
return propHolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a PropertiesHolder for the given filename, either from the
|
||||
* cache or freshly loaded.
|
||||
|
||||
+27
-23
@@ -188,36 +188,35 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
|
||||
* found for the given basename and Locale
|
||||
*/
|
||||
protected @Nullable ResourceBundle getResourceBundle(String basename, Locale locale) {
|
||||
if (getCacheMillis() >= 0) {
|
||||
if (getCacheMillis() >= 0 || !JVM_LOCALES.contains(locale)) {
|
||||
// Fresh ResourceBundle.getBundle call in order to let ResourceBundle
|
||||
// do its native caching, at the expense of more extensive lookup steps.
|
||||
return doGetBundle(basename, locale);
|
||||
}
|
||||
else {
|
||||
// Cache forever: prefer locale cache over repeated getBundle calls.
|
||||
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
|
||||
if (localeMap != null) {
|
||||
ResourceBundle bundle = localeMap.get(locale);
|
||||
if (bundle != null) {
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
try {
|
||||
ResourceBundle bundle = doGetBundle(basename, locale);
|
||||
if (localeMap == null) {
|
||||
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
|
||||
}
|
||||
localeMap.put(locale, bundle);
|
||||
|
||||
// Cache forever: prefer local cache over repeated getBundle calls.
|
||||
Map<Locale, ResourceBundle> localeMap = this.cachedResourceBundles.get(basename);
|
||||
if (localeMap != null) {
|
||||
ResourceBundle bundle = localeMap.get(locale);
|
||||
if (bundle != null) {
|
||||
return bundle;
|
||||
}
|
||||
catch (MissingResourceException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
|
||||
}
|
||||
// Assume bundle not found
|
||||
// -> do NOT throw the exception to allow for checking parent message source.
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ResourceBundle bundle = doGetBundle(basename, locale);
|
||||
if (localeMap == null) {
|
||||
localeMap = this.cachedResourceBundles.computeIfAbsent(basename, bn -> new ConcurrentHashMap<>());
|
||||
}
|
||||
localeMap.put(locale, bundle);
|
||||
return bundle;
|
||||
}
|
||||
catch (MissingResourceException ex) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("ResourceBundle [" + basename + "] not found for MessageSource: " + ex.getMessage());
|
||||
}
|
||||
// Assume bundle not found
|
||||
// -> do NOT throw the exception to allow for checking parent message source.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,6 +310,11 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou
|
||||
protected @Nullable MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale)
|
||||
throws MissingResourceException {
|
||||
|
||||
if (!JVM_LOCALES.contains(locale)) {
|
||||
String msg = getStringOrNull(bundle, code);
|
||||
return (msg != null ? createMessageFormat(msg, locale) : null);
|
||||
}
|
||||
|
||||
Map<String, Map<Locale, MessageFormat>> codeMap = this.cachedBundleMessageFormats.get(bundle);
|
||||
Map<Locale, MessageFormat> localeMap = null;
|
||||
if (codeMap != null) {
|
||||
|
||||
@@ -197,6 +197,10 @@ public @interface Retryable {
|
||||
* and {@code delay + jitter} but never below the base {@link #delay()} or
|
||||
* above {@link #maxDelay()}. If a multiplier is specified, it is applied
|
||||
* to the jitter value as well.
|
||||
* <p>When {@link #delay()} is {@code 0} combined with a positive jitter,
|
||||
* the delay never grows regardless of any configured multiplier, so the
|
||||
* full configured jitter is applied directly as a random delay in the range
|
||||
* from {@code 0} to {@code min(jitter, maxDelay)}.
|
||||
* <p>The time unit is milliseconds by default but can be overridden via
|
||||
* {@link #timeUnit}.
|
||||
* <p>The default is 0 (no jitter).
|
||||
|
||||
@@ -270,12 +270,12 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the limit for array and collection auto-growing.
|
||||
* Specify the limit for array and collection/set/list auto-growing.
|
||||
* <p>Default is 256, preventing OutOfMemoryErrors in case of large indexes.
|
||||
* Raise this limit if your auto-growing needs are unusually high.
|
||||
* <p>Used for setter injection via {@link #bind(PropertyValues)};
|
||||
* not applicable to field injection, and not to constructor binding
|
||||
* via {@link #construct} either.
|
||||
* not applicable to map properties and not to field injection and
|
||||
* constructor binding via {@link #construct} either.
|
||||
* @see #initBeanPropertyAccess()
|
||||
* @see org.springframework.beans.BeanWrapper#setAutoGrowCollectionLimit
|
||||
*/
|
||||
@@ -326,6 +326,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
|
||||
/**
|
||||
* Initialize direct field access for this DataBinder,
|
||||
* as alternative to the default bean property access.
|
||||
* <p><b>NOTE: This is an advanced option for trusted scenarios.</b>
|
||||
* Do not use direct field access for data binding from untrusted sources.
|
||||
* @see #initBeanPropertyAccess()
|
||||
* @see #createDirectFieldBindingResult()
|
||||
*/
|
||||
|
||||
+2
@@ -97,6 +97,7 @@ class ResourceBundleMessageSourceTests {
|
||||
doTestMessageAccess(true, false, true, true, false);
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // for Locale constructors on JDK 19
|
||||
protected void doTestMessageAccess(
|
||||
boolean reloadable, boolean fallbackToSystemLocale,
|
||||
boolean expectGermanFallback, boolean useCodeAsDefaultMessage, boolean alwaysUseMessageFormat) {
|
||||
@@ -406,6 +407,7 @@ class ResourceBundleMessageSourceTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation") // for Locale constructors on JDK 19
|
||||
void reloadableResourceBundleMessageSourceFileNameCalculation() {
|
||||
ReloadableResourceBundleMessageSource ms = new ReloadableResourceBundleMessageSource();
|
||||
|
||||
|
||||
+1
@@ -225,6 +225,7 @@ class MBeanClientInterceptorTests extends AbstractMBeanServerTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("deprecation") // for Thread.getId() on JDK 19
|
||||
void mxBeanOperationAccess() throws Exception {
|
||||
MBeanClientInterceptor interceptor = new MBeanClientInterceptor();
|
||||
interceptor.setServer(ManagementFactory.getPlatformMBeanServer());
|
||||
|
||||
+11
-2
@@ -19,6 +19,7 @@ package org.springframework.resilience;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ForkJoinPool;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -120,6 +121,10 @@ class ConcurrencyLimitTests {
|
||||
|
||||
@Test
|
||||
void withPostProcessorForMethodWithRejection() throws Exception{
|
||||
if (ForkJoinPool.getCommonPoolParallelism() < 4) {
|
||||
return; // not enough concurrency possible
|
||||
}
|
||||
|
||||
AnnotatedMethodBean proxy = createProxy(AnnotatedMethodBean.class);
|
||||
AnnotatedMethodBean target = (AnnotatedMethodBean) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@@ -128,7 +133,7 @@ class ConcurrencyLimitTests {
|
||||
futures.add(CompletableFuture.runAsync(proxy::rejectingOperation));
|
||||
}
|
||||
Thread.sleep(10);
|
||||
for (int i = 2; i < 10; i++) {
|
||||
for (int i = 2; i < 4; i++) {
|
||||
futures.add(CompletableFuture.runAsync(() ->
|
||||
assertThatExceptionOfType(InvocationRejectedException.class).isThrownBy(proxy::rejectingOperation)
|
||||
.withMessageContaining(AnnotatedMethodBean.class.getName() + ".rejectingOperation")
|
||||
@@ -159,6 +164,10 @@ class ConcurrencyLimitTests {
|
||||
|
||||
@Test
|
||||
void withPostProcessorForClassWithRejection() throws Exception {
|
||||
if (ForkJoinPool.getCommonPoolParallelism() < 4) {
|
||||
return; // not enough concurrency possible
|
||||
}
|
||||
|
||||
AnnotatedClassBeanWithRejection proxy = createProxy(AnnotatedClassBeanWithRejection.class);
|
||||
AnnotatedClassBeanWithRejection target = (AnnotatedClassBeanWithRejection) AopProxyUtils.getSingletonTarget(proxy);
|
||||
|
||||
@@ -174,7 +183,7 @@ class ConcurrencyLimitTests {
|
||||
assertThatExceptionOfType(InvocationRejectedException.class).isThrownBy(proxy::otherOperation)
|
||||
.withMessageContaining(AnnotatedClassBeanWithRejection.class.getName())
|
||||
.satisfies(ex -> assertThat(ex.getTarget() == target))));
|
||||
for (int i = 0; i < 10; i++) {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
futures.add(CompletableFuture.runAsync(proxy::overrideOperation));
|
||||
}
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
|
||||
+1
-1
@@ -95,7 +95,7 @@ class ScheduledTaskTests {
|
||||
assertThat(scheduledTask.nextExecution()).isNull();
|
||||
}
|
||||
|
||||
class CountingRunnable implements Runnable {
|
||||
static class CountingRunnable implements Runnable {
|
||||
|
||||
int executionCount;
|
||||
|
||||
|
||||
+2
-2
@@ -525,7 +525,7 @@ class SpringValidatorAdapterTests {
|
||||
}
|
||||
|
||||
|
||||
public class BeanWithListElementConstraint {
|
||||
public static class BeanWithListElementConstraint {
|
||||
|
||||
@Valid
|
||||
private List<@NotNull String> property;
|
||||
@@ -540,7 +540,7 @@ class SpringValidatorAdapterTests {
|
||||
}
|
||||
|
||||
|
||||
public class BeanWithMapEntryConstraint {
|
||||
public static class BeanWithMapEntryConstraint {
|
||||
|
||||
@Valid
|
||||
private Map<@NotNull String, @NotNull String> property;
|
||||
|
||||
+2
-1
@@ -20,6 +20,7 @@ import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.Writer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -59,7 +60,7 @@ public class FileNativeConfigurationWriter extends NativeConfigurationWriter {
|
||||
protected void writeTo(String fileName, Consumer<BasicJsonWriter> writer) {
|
||||
try {
|
||||
File file = createIfNecessary(fileName);
|
||||
try (FileWriter out = new FileWriter(file)) {
|
||||
try (FileWriter out = new FileWriter(file, StandardCharsets.UTF_8)) {
|
||||
writer.accept(createJsonWriter(out));
|
||||
}
|
||||
}
|
||||
|
||||
+1
@@ -46,6 +46,7 @@ public abstract class NativeConfigurationWriter {
|
||||
private boolean hasAnyHint(RuntimeHints hints) {
|
||||
return (hints.proxies().jdkProxyHints().findAny().isPresent() ||
|
||||
hints.reflection().typeHints().findAny().isPresent() ||
|
||||
hints.reflection().lambdaHints().findAny().isPresent() ||
|
||||
hints.resources().resourcePatternHints().findAny().isPresent() ||
|
||||
hints.resources().resourceBundleHints().findAny().isPresent() ||
|
||||
hints.jni().typeHints().findAny().isPresent() ||
|
||||
|
||||
@@ -44,6 +44,7 @@ import org.jspecify.annotations.Nullable;
|
||||
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME
|
||||
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
|
||||
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
|
||||
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_MAX_BIG_POWER_BITS_PROPERTY_NAME
|
||||
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_MAX_OPERATIONS_PROPERTY_NAME
|
||||
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
|
||||
* @see org.springframework.jndi.JndiLocatorDelegate#IGNORE_JNDI_PROPERTY_NAME
|
||||
|
||||
+2
@@ -82,6 +82,7 @@ public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
|
||||
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public Flux<T> decode(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
@@ -90,6 +91,7 @@ public abstract class AbstractDataBufferDecoder<T> extends AbstractDecoder<T> {
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public Mono<T> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
|
||||
@@ -917,6 +917,14 @@ public abstract class DataBufferUtils {
|
||||
super(delimiter);
|
||||
Assert.isTrue(delimiter.length == 2, "Expected a 2-byte delimiter");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(byte b) {
|
||||
if (getMatches() > 0 && b != delimiter()[getMatches()]) {
|
||||
setMatches(0);
|
||||
}
|
||||
return super.match(b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -100,20 +100,20 @@ public class LocalizedResourceHelper {
|
||||
String variant = locale.getVariant();
|
||||
|
||||
// Check for file with language, country and variant localization.
|
||||
if (variant.length() > 0) {
|
||||
if (!variant.isEmpty()) {
|
||||
String location =
|
||||
name + this.separator + lang + this.separator + country + this.separator + variant + extension;
|
||||
resource = this.resourceLoader.getResource(location);
|
||||
}
|
||||
|
||||
// Check for file with language and country localization.
|
||||
if ((resource == null || !resource.exists()) && country.length() > 0) {
|
||||
if ((resource == null || !resource.exists()) && !country.isEmpty()) {
|
||||
String location = name + this.separator + lang + this.separator + country + extension;
|
||||
resource = this.resourceLoader.getResource(location);
|
||||
}
|
||||
|
||||
// Check for document with language localization.
|
||||
if ((resource == null || !resource.exists()) && lang.length() > 0) {
|
||||
if ((resource == null || !resource.exists()) && !lang.isEmpty()) {
|
||||
String location = name + this.separator + lang + extension;
|
||||
resource = this.resourceLoader.getResource(location);
|
||||
}
|
||||
|
||||
+1
@@ -611,6 +611,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation") // for deprecated URL constructor on JDK 20
|
||||
private Set<ClassPathManifestEntry> getClassPathManifestEntriesFromJar(File jar) throws IOException {
|
||||
URL base = jar.toURI().toURL();
|
||||
File parent = jar.getAbsoluteFile().getParentFile();
|
||||
|
||||
@@ -67,7 +67,7 @@ public interface RetryPolicy {
|
||||
* @see Builder#timeout(Duration)
|
||||
*/
|
||||
default Duration getTimeout() {
|
||||
return Duration.ZERO;
|
||||
return Builder.DEFAULT_TIMEOUT;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,23 +158,33 @@ public interface RetryPolicy {
|
||||
public static final long DEFAULT_DELAY = 1000;
|
||||
|
||||
/**
|
||||
* The default {@linkplain #maxDelay(Duration) max delay}: {@value} ms.
|
||||
* @see Long#MAX_VALUE
|
||||
* The default {@linkplain #jitter(Duration) jitter}: {@value}.
|
||||
* @since 7.0.9
|
||||
*/
|
||||
public static final long DEFAULT_MAX_DELAY = Long.MAX_VALUE;
|
||||
public static final long DEFAULT_JITTER = 0;
|
||||
|
||||
/**
|
||||
* The default {@linkplain #multiplier(double) multiplier}: {@value}.
|
||||
*/
|
||||
public static final double DEFAULT_MULTIPLIER = 1.0;
|
||||
|
||||
/**
|
||||
* The default {@linkplain #maxDelay(Duration) max delay}: {@value} ms.
|
||||
* @see Long#MAX_VALUE
|
||||
*/
|
||||
public static final long DEFAULT_MAX_DELAY = Long.MAX_VALUE;
|
||||
|
||||
/**
|
||||
* The default {@linkplain #timeout(Duration) timeout}: {@link Duration#ZERO}.
|
||||
* @since 7.0.9
|
||||
*/
|
||||
public static final Duration DEFAULT_TIMEOUT = Duration.ZERO;
|
||||
|
||||
|
||||
private @Nullable BackOff backOff;
|
||||
|
||||
private @Nullable Long maxRetries;
|
||||
|
||||
private Duration timeout = Duration.ZERO;
|
||||
|
||||
private @Nullable Duration delay;
|
||||
|
||||
private @Nullable Duration jitter;
|
||||
@@ -183,6 +193,8 @@ public interface RetryPolicy {
|
||||
|
||||
private @Nullable Duration maxDelay;
|
||||
|
||||
private Duration timeout = DEFAULT_TIMEOUT;
|
||||
|
||||
private final Set<Class<? extends Throwable>> includes = new LinkedHashSet<>();
|
||||
|
||||
private final Set<Class<? extends Throwable>> excludes = new LinkedHashSet<>();
|
||||
@@ -234,24 +246,6 @@ public interface RetryPolicy {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a timeout for the maximum amount of elapsed time allowed for
|
||||
* the initial invocation and any subsequent retry attempts, including
|
||||
* delays.
|
||||
* <p>The default is {@link Duration#ZERO}, which signals that no timeout
|
||||
* should be applied.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* @param timeout the timeout, typically in milliseconds or seconds;
|
||||
* must be greater than or equal to zero
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @since 7.0.2
|
||||
*/
|
||||
public Builder timeout(Duration timeout) {
|
||||
assertIsNotNegative("timeout", timeout);
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the base delay after the initial invocation.
|
||||
* <p>If a {@linkplain #multiplier(double) multiplier} is specified, this
|
||||
@@ -281,6 +275,11 @@ public interface RetryPolicy {
|
||||
* {@linkplain #maxDelay(Duration) max delay}.
|
||||
* <p>If a {@linkplain #multiplier(double) multiplier} is specified, it
|
||||
* is applied to the jitter value as well.
|
||||
* <p>When the configured {@linkplain #delay(Duration) delay} is zero
|
||||
* combined with a positive jitter, the delay never grows regardless of
|
||||
* any configured multiplier, so the full configured jitter is applied
|
||||
* directly as a random delay in the range from zero to
|
||||
* {@code min(jitter, maxDelay)}.
|
||||
* <p>The default is no jitter.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* <p>You should not specify this configuration option if you have
|
||||
@@ -342,6 +341,24 @@ public interface RetryPolicy {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a timeout for the maximum amount of elapsed time allowed for
|
||||
* the initial invocation and any subsequent retry attempts, including
|
||||
* delays.
|
||||
* <p>The default is {@link Duration#ZERO}, which signals that no timeout
|
||||
* should be applied.
|
||||
* <p>The supplied value will override any previously configured value.
|
||||
* @param timeout the timeout, typically in milliseconds or seconds;
|
||||
* must be greater than or equal to zero
|
||||
* @return this {@code Builder} instance for chained method invocations
|
||||
* @since 7.0.2
|
||||
*/
|
||||
public Builder timeout(Duration timeout) {
|
||||
assertIsNotNegative("timeout", timeout);
|
||||
this.timeout = timeout;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify the types of exceptions for which the {@link RetryPolicy}
|
||||
* should retry a failed operation.
|
||||
@@ -472,11 +489,9 @@ public interface RetryPolicy {
|
||||
ExponentialBackOff exponentialBackOff = new ExponentialBackOff();
|
||||
exponentialBackOff.setMaxAttempts(this.maxRetries != null ? this.maxRetries : DEFAULT_MAX_RETRIES);
|
||||
exponentialBackOff.setInitialInterval(this.delay != null ? this.delay.toMillis() : DEFAULT_DELAY);
|
||||
exponentialBackOff.setMaxInterval(this.maxDelay != null ? this.maxDelay.toMillis() : DEFAULT_MAX_DELAY);
|
||||
exponentialBackOff.setJitter(this.jitter != null ? this.jitter.toMillis() : DEFAULT_JITTER);
|
||||
exponentialBackOff.setMultiplier(this.multiplier != null ? this.multiplier : DEFAULT_MULTIPLIER);
|
||||
if (this.jitter != null) {
|
||||
exponentialBackOff.setJitter(this.jitter.toMillis());
|
||||
}
|
||||
exponentialBackOff.setMaxInterval(this.maxDelay != null ? this.maxDelay.toMillis() : DEFAULT_MAX_DELAY);
|
||||
backOff = exponentialBackOff;
|
||||
}
|
||||
return new DefaultRetryPolicy(this.includes, this.excludes, this.predicate, this.timeout, backOff);
|
||||
|
||||
+33
-1
@@ -18,6 +18,7 @@ package org.springframework.core.serializer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.ObjectInputFilter;
|
||||
import java.io.ObjectInputStream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
@@ -38,6 +39,8 @@ public class DefaultDeserializer implements Deserializer<Object> {
|
||||
|
||||
private final @Nullable ClassLoader classLoader;
|
||||
|
||||
private final @Nullable ObjectInputFilter objectInputFilter;
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code DefaultDeserializer} with default {@link ObjectInputStream}
|
||||
@@ -45,6 +48,7 @@ public class DefaultDeserializer implements Deserializer<Object> {
|
||||
*/
|
||||
public DefaultDeserializer() {
|
||||
this.classLoader = null;
|
||||
this.objectInputFilter = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,6 +60,21 @@ public class DefaultDeserializer implements Deserializer<Object> {
|
||||
*/
|
||||
public DefaultDeserializer(@Nullable ClassLoader classLoader) {
|
||||
this.classLoader = classLoader;
|
||||
this.objectInputFilter = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a {@code DefaultDeserializer} for using an {@link ObjectInputStream}
|
||||
* with the given {@code ClassLoader}.
|
||||
* @param classLoader the ClassLoader to use
|
||||
* @param objectInputFilter a custom ObjectInputFilter to apply
|
||||
* @since 7.0.9
|
||||
* @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader)
|
||||
* @see ObjectInputStream#setObjectInputFilter
|
||||
*/
|
||||
public DefaultDeserializer(@Nullable ClassLoader classLoader, @Nullable ObjectInputFilter objectInputFilter) {
|
||||
this.classLoader = classLoader;
|
||||
this.objectInputFilter = objectInputFilter;
|
||||
}
|
||||
|
||||
|
||||
@@ -65,10 +84,20 @@ public class DefaultDeserializer implements Deserializer<Object> {
|
||||
* @since 6.2.19
|
||||
* @see ConfigurableObjectInputStream#ConfigurableObjectInputStream(InputStream, ClassLoader)
|
||||
*/
|
||||
public @Nullable ClassLoader getClassLoader() {
|
||||
public final @Nullable ClassLoader getClassLoader() {
|
||||
return this.classLoader;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the {@link ObjectInputFilter} to apply to the {@link ObjectInputStream},
|
||||
* if any.
|
||||
* @since 7.0.9
|
||||
* @see ObjectInputStream#setObjectInputFilter
|
||||
*/
|
||||
public final @Nullable ObjectInputFilter getObjectInputFilter() {
|
||||
return this.objectInputFilter;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read from the supplied {@code InputStream} and deserialize the contents
|
||||
@@ -78,6 +107,9 @@ public class DefaultDeserializer implements Deserializer<Object> {
|
||||
@Override
|
||||
public Object deserialize(InputStream inputStream) throws IOException {
|
||||
ObjectInputStream objectInputStream = new ConfigurableObjectInputStream(inputStream, this.classLoader);
|
||||
if (this.objectInputFilter != null) {
|
||||
objectInputStream.setObjectInputFilter(this.objectInputFilter);
|
||||
}
|
||||
try {
|
||||
return objectInputStream.readObject();
|
||||
}
|
||||
|
||||
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
|
||||
* exposed through {@link CompositeMap#values()}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Yanming Zhou
|
||||
* @since 6.2
|
||||
* @param <E> the type of elements maintained by this collection
|
||||
*/
|
||||
class CompositeCollection<E> implements Collection<E> {
|
||||
class CompositeCollection<E extends @Nullable Object> implements Collection<E> {
|
||||
|
||||
private final Collection<E> first;
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ import java.util.LinkedHashSet;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Set;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Composite iterator that combines multiple other iterators,
|
||||
* as registered via {@link #add(Iterator)}.
|
||||
@@ -30,10 +32,11 @@ import java.util.Set;
|
||||
*
|
||||
* @author Erwin Vervaet
|
||||
* @author Juergen Hoeller
|
||||
* @author Yanming Zhou
|
||||
* @since 3.0
|
||||
* @param <E> the element type
|
||||
*/
|
||||
public class CompositeIterator<E> implements Iterator<E> {
|
||||
public class CompositeIterator<E extends @Nullable Object> implements Iterator<E> {
|
||||
|
||||
private final Set<Iterator<E>> iterators = new LinkedHashSet<>();
|
||||
|
||||
|
||||
@@ -32,17 +32,18 @@ import org.jspecify.annotations.Nullable;
|
||||
* {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Yanming Zhou
|
||||
* @since 6.2
|
||||
* @param <K> the type of keys maintained by this map
|
||||
* @param <V> the type of mapped values
|
||||
*/
|
||||
final class CompositeMap<K, V> implements Map<K, V> {
|
||||
final class CompositeMap<K, V extends @Nullable Object> implements Map<K, V> {
|
||||
|
||||
private final Map<K,V> first;
|
||||
private final Map<K, V> first;
|
||||
|
||||
private final Map<K,V> second;
|
||||
private final Map<K, V> second;
|
||||
|
||||
private final @Nullable BiFunction<K,V,V> putFunction;
|
||||
private final @Nullable BiFunction<K, V, V> putFunction;
|
||||
|
||||
private final @Nullable Consumer<Map<K, V>> putAllFunction;
|
||||
|
||||
@@ -53,7 +54,7 @@ final class CompositeMap<K, V> implements Map<K, V> {
|
||||
|
||||
CompositeMap(Map<K, V> first, Map<K, V> second,
|
||||
@Nullable BiFunction<K, V, V> putFunction,
|
||||
@Nullable Consumer<Map<K,V>> putAllFunction) {
|
||||
@Nullable Consumer<Map<K, V>> putAllFunction) {
|
||||
|
||||
Assert.notNull(first, "First must not be null");
|
||||
Assert.notNull(second, "Second must not be null");
|
||||
@@ -106,7 +107,7 @@ final class CompositeMap<K, V> implements Map<K, V> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable V put(K key, V value) {
|
||||
public V put(K key, V value) {
|
||||
if (this.putFunction == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@@ -25,10 +25,11 @@ import org.jspecify.annotations.Nullable;
|
||||
* {@link CompositeMap#keySet()} and {@link CompositeMap#entrySet()}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Yanming Zhou
|
||||
* @since 6.2
|
||||
* @param <E> the type of elements maintained by this set
|
||||
*/
|
||||
final class CompositeSet<E> extends CompositeCollection<E> implements Set<E> {
|
||||
final class CompositeSet<E extends @Nullable Object> extends CompositeCollection<E> implements Set<E> {
|
||||
|
||||
CompositeSet(Set<E> first, Set<E> second) {
|
||||
super(first, second);
|
||||
|
||||
@@ -118,7 +118,7 @@ public abstract class FileSystemUtils {
|
||||
|
||||
/**
|
||||
* Recursively copy the contents of the {@code src} file/directory
|
||||
* to the {@code dest} file/directory.
|
||||
* to the {@code dest} file/directory, including symbolic links.
|
||||
* @param src the source directory
|
||||
* @param dest the destination directory
|
||||
* @throws IOException in the case of I/O errors
|
||||
|
||||
@@ -21,15 +21,18 @@ import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
/**
|
||||
* Collection that filters out values that do not match a predicate.
|
||||
* This type is used by {@link CompositeMap}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Yanming Zhou
|
||||
* @since 6.2
|
||||
* @param <E> the type of elements maintained by this collection
|
||||
*/
|
||||
class FilteredCollection<E> extends AbstractCollection<E> {
|
||||
class FilteredCollection<E extends @Nullable Object> extends AbstractCollection<E> {
|
||||
|
||||
private final Collection<E> delegate;
|
||||
|
||||
|
||||
@@ -28,10 +28,11 @@ import org.jspecify.annotations.Nullable;
|
||||
* <p>This type is used by {@link CompositeMap}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Yanming Zhou
|
||||
* @since 6.2
|
||||
* @param <E> the type of elements returned by this iterator
|
||||
*/
|
||||
final class FilteredIterator<E> implements Iterator<E> {
|
||||
final class FilteredIterator<E extends @Nullable Object> implements Iterator<E> {
|
||||
|
||||
private final Iterator<E> delegate;
|
||||
|
||||
@@ -56,12 +57,11 @@ final class FilteredIterator<E> implements Iterator<E> {
|
||||
}
|
||||
|
||||
@Override
|
||||
public E next() {
|
||||
public @Nullable E next() {
|
||||
if (!this.hasNext && !setNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
this.hasNext = false;
|
||||
Assert.state(this.next != null, "Next should not be null");
|
||||
return this.next;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,11 +28,12 @@ import org.jspecify.annotations.Nullable;
|
||||
* This type is used by {@link CompositeMap}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Yanming Zhou
|
||||
* @since 6.2
|
||||
* @param <K> the type of keys maintained by this map
|
||||
* @param <V> the type of mapped values
|
||||
*/
|
||||
final class FilteredMap<K, V> extends AbstractMap<K, V> {
|
||||
final class FilteredMap<K, V extends @Nullable Object> extends AbstractMap<K, V> {
|
||||
|
||||
private final Map<K, V> delegate;
|
||||
|
||||
|
||||
@@ -26,10 +26,11 @@ import org.jspecify.annotations.Nullable;
|
||||
* This type is used by {@link CompositeMap}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Yanming Zhou
|
||||
* @since 6.2
|
||||
* @param <E> the type of elements maintained by this set
|
||||
*/
|
||||
final class FilteredSet<E> extends FilteredCollection<E> implements Set<E> {
|
||||
final class FilteredSet<E extends @Nullable Object> extends FilteredCollection<E> implements Set<E> {
|
||||
|
||||
public FilteredSet(Set<E> delegate, Predicate<E> filter) {
|
||||
super(delegate, filter);
|
||||
|
||||
@@ -187,7 +187,7 @@ public abstract class MimeTypeUtils {
|
||||
|
||||
/**
|
||||
* Parse the given String into a single {@code MimeType}.
|
||||
* Recently parsed {@code MimeType} are cached for further retrieval.
|
||||
* <p>Recently parsed {@code MimeType} values are cached for future retrieval.
|
||||
* @param mimeType the string to parse
|
||||
* @return the mime type
|
||||
* @throws InvalidMimeTypeException if the string cannot be parsed
|
||||
@@ -238,7 +238,7 @@ public abstract class MimeTypeUtils {
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (ch == '"' && mimeType.charAt(nextIndex - 1) != '\\') {
|
||||
else if (ch == '"' && (nextIndex == 0 || mimeType.charAt(nextIndex - 1) != '\\')) {
|
||||
quoted = !quoted;
|
||||
}
|
||||
nextIndex++;
|
||||
|
||||
@@ -132,7 +132,7 @@ public abstract class StringUtils {
|
||||
*/
|
||||
@Contract("null -> false")
|
||||
public static boolean hasLength(@Nullable CharSequence str) {
|
||||
return (str != null && !str.isEmpty()); // as of JDK 15
|
||||
return (str != null && !str.isEmpty());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -956,6 +956,7 @@ public abstract class StringUtils {
|
||||
String country = tokens[1];
|
||||
validateLocalePart(country);
|
||||
String variant = Arrays.stream(tokens).skip(2).collect(Collectors.joining(delimiter));
|
||||
validateLocalePart(variant);
|
||||
return new Locale(language, country, variant);
|
||||
}
|
||||
|
||||
|
||||
@@ -154,6 +154,10 @@ public class ExponentialBackOff implements BackOff {
|
||||
* {@code initialInterval} or above {@code maxInterval}.
|
||||
* <p>If a {@code multiplier} is specified, it is applied to the jitter value
|
||||
* as well.
|
||||
* <p>When {@code initialInterval} is {@code 0} combined with a positive
|
||||
* jitter, the interval never grows regardless of any configured multiplier,
|
||||
* so the full configured jitter is applied directly as a random interval in
|
||||
* the range from {@code 0} to {@code min(jitter, maxInterval)}.
|
||||
* @param jitter the jitter value in milliseconds
|
||||
* @since 7.0
|
||||
*/
|
||||
@@ -311,7 +315,9 @@ public class ExponentialBackOff implements BackOff {
|
||||
long jitter = getJitter();
|
||||
if (jitter > 0) {
|
||||
long initialInterval = getInitialInterval();
|
||||
long applicableJitter = jitter * (interval / initialInterval);
|
||||
// When initialInterval is 0 the interval never grows, so the scale factor
|
||||
// stays at its baseline value of 1 and the full configured jitter is applied.
|
||||
long applicableJitter = jitter * (initialInterval > 0 ? (interval / initialInterval) : 1);
|
||||
long min = Math.max(interval - applicableJitter, initialInterval);
|
||||
long max = Math.min(interval + applicableJitter, getMaxInterval());
|
||||
return min + (long) (Math.random() * (max - min));
|
||||
|
||||
+3
-1
@@ -151,7 +151,9 @@ public class XmlValidationModeDetector {
|
||||
private String consumeCommentTokens(String line) {
|
||||
int indexOfStartComment = line.indexOf(START_COMMENT);
|
||||
if (indexOfStartComment == -1 && !line.contains(END_COMMENT)) {
|
||||
return line;
|
||||
// If we are inside a multi-line comment, the entire line is comment
|
||||
// data and must not be treated as content.
|
||||
return (this.inComment ? "" : line);
|
||||
}
|
||||
|
||||
String result = "";
|
||||
|
||||
+23
-41
@@ -27,7 +27,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
@@ -77,7 +76,7 @@ abstract class ClassFileAnnotationDelegate {
|
||||
}
|
||||
}
|
||||
Map<String, Object> compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes);
|
||||
return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes);
|
||||
return MergedAnnotation.of(classLoader, new Source(className), annotationType, compactedAttributes);
|
||||
}
|
||||
catch (ClassNotFoundException | LinkageError ex) {
|
||||
// Non-loadable annotation type -> ignore.
|
||||
@@ -108,27 +107,13 @@ abstract class ClassFileAnnotationDelegate {
|
||||
}
|
||||
|
||||
private static Object parseArrayValue(String className, @Nullable ClassLoader classLoader, AnnotationValue.OfArray arrayValue) {
|
||||
if (arrayValue.values().isEmpty()) {
|
||||
return new Object[0];
|
||||
}
|
||||
Stream<AnnotationValue> stream = arrayValue.values().stream();
|
||||
switch (arrayValue.values().getFirst()) {
|
||||
case AnnotationValue.OfInt _ -> {
|
||||
return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray();
|
||||
}
|
||||
case AnnotationValue.OfDouble _ -> {
|
||||
return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray();
|
||||
}
|
||||
case AnnotationValue.OfLong _ -> {
|
||||
return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray();
|
||||
}
|
||||
default -> {
|
||||
Class<?> arrayElementType = resolveArrayElementType(arrayValue.values(), classLoader);
|
||||
return stream
|
||||
.map(rawValue -> readAnnotationValue(className, rawValue, classLoader))
|
||||
.toArray(length -> (Object[]) Array.newInstance(arrayElementType, length));
|
||||
}
|
||||
List<AnnotationValue> values = arrayValue.values();
|
||||
Class<?> arrayElementType = (values.isEmpty() ? Object.class : resolveArrayElementType(values, classLoader));
|
||||
Object array = Array.newInstance(arrayElementType, values.size());
|
||||
for (int i = 0; i < values.size(); i++) {
|
||||
Array.set(array, i, readAnnotationValue(className, values.get(i), classLoader));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@@ -143,28 +128,25 @@ abstract class ClassFileAnnotationDelegate {
|
||||
}
|
||||
|
||||
private static Class<?> resolveArrayElementType(List<AnnotationValue> values, @Nullable ClassLoader classLoader) {
|
||||
AnnotationValue firstValue = values.getFirst();
|
||||
switch (firstValue) {
|
||||
case AnnotationValue.OfConstant constantValue -> {
|
||||
return constantValue.resolvedValue().getClass();
|
||||
}
|
||||
case AnnotationValue.OfAnnotation _ -> {
|
||||
return MergedAnnotation.class;
|
||||
}
|
||||
case AnnotationValue.OfClass _ -> {
|
||||
return String.class;
|
||||
}
|
||||
case AnnotationValue.OfEnum enumValue -> {
|
||||
return loadEnumClass(enumValue, classLoader);
|
||||
}
|
||||
default -> {
|
||||
return Object.class;
|
||||
}
|
||||
}
|
||||
return switch (values.getFirst()) {
|
||||
case AnnotationValue.OfByte _ -> byte.class;
|
||||
case AnnotationValue.OfChar _ -> char.class;
|
||||
case AnnotationValue.OfDouble _ -> double.class;
|
||||
case AnnotationValue.OfFloat _ -> float.class;
|
||||
case AnnotationValue.OfInt _ -> int.class;
|
||||
case AnnotationValue.OfLong _ -> long.class;
|
||||
case AnnotationValue.OfShort _ -> short.class;
|
||||
case AnnotationValue.OfBoolean _ -> boolean.class;
|
||||
case AnnotationValue.OfString _ -> String.class;
|
||||
case AnnotationValue.OfAnnotation _ -> MergedAnnotation.class;
|
||||
case AnnotationValue.OfClass _ -> String.class;
|
||||
case AnnotationValue.OfEnum enumValue -> loadEnumClass(enumValue, classLoader);
|
||||
case AnnotationValue.OfArray _ -> Object.class;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
record Source(Annotation annotation) {
|
||||
record Source(String className) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+48
-7
@@ -18,12 +18,13 @@ package org.springframework.aot.nativex;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.json.JSONException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.skyscreamer.jsonassert.JSONAssert;
|
||||
@@ -63,7 +64,7 @@ class FileNativeConfigurationWriterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void serializationConfig() throws IOException, JSONException {
|
||||
void serializationConfig() throws Exception {
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ReflectionHints reflectionHints = hints.reflection();
|
||||
@@ -81,7 +82,7 @@ class FileNativeConfigurationWriterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyConfig() throws IOException, JSONException {
|
||||
void proxyConfig() throws Exception {
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ProxyHints proxyHints = hints.proxies();
|
||||
@@ -99,7 +100,7 @@ class FileNativeConfigurationWriterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void reflectionConfig() throws IOException, JSONException {
|
||||
void reflectionConfig() throws Exception {
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ReflectionHints reflectionHints = hints.reflection();
|
||||
@@ -137,8 +138,36 @@ class FileNativeConfigurationWriterTests {
|
||||
""");
|
||||
}
|
||||
|
||||
@Test // gh-36989
|
||||
void lambdaConfig() throws Exception {
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
hints.reflection().registerLambda(Integer.class, builder -> builder
|
||||
.withDeclaringMethod("getCell", Integer.class, Integer.class)
|
||||
.withInterfaces(Supplier.class));
|
||||
generator.write(hints);
|
||||
assertEquals("""
|
||||
{
|
||||
"reflection": [
|
||||
{
|
||||
"type": {
|
||||
"lambda": {
|
||||
"declaringClass": "java.lang.Integer",
|
||||
"declaringMethod": {
|
||||
"name": "getCell",
|
||||
"parameterTypes": [ "java.lang.Integer", "java.lang.Integer" ]
|
||||
},
|
||||
"interfaces": [ "java.util.function.Supplier" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
}
|
||||
|
||||
@Test
|
||||
void jniConfig() throws IOException, JSONException {
|
||||
void jniConfig() throws Exception {
|
||||
// same format as reflection so just test basic file generation
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
@@ -157,7 +186,7 @@ class FileNativeConfigurationWriterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void resourceConfig() throws IOException, JSONException {
|
||||
void resourceConfig() throws Exception {
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
ResourceHints resourceHints = hints.resources();
|
||||
@@ -176,6 +205,17 @@ class FileNativeConfigurationWriterTests {
|
||||
}""");
|
||||
}
|
||||
|
||||
@Test // gh-36972
|
||||
void resourceConfigWithNonAsciiPatternIsWrittenAsUtf8() throws IOException {
|
||||
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
|
||||
RuntimeHints hints = new RuntimeHints();
|
||||
hints.resources().registerPattern("com/example/café/**");
|
||||
generator.write(hints);
|
||||
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve("reachability-metadata.json");
|
||||
byte[] content = Files.readAllBytes(jsonFile);
|
||||
assertThat(content).containsSequence("café".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@Test
|
||||
void namespace() {
|
||||
String groupId = "foo.bar";
|
||||
@@ -190,7 +230,8 @@ class FileNativeConfigurationWriterTests {
|
||||
assertThat(jsonFile.toFile()).exists();
|
||||
}
|
||||
|
||||
private void assertEquals(String expectedString) throws IOException, JSONException {
|
||||
|
||||
private static void assertEquals(String expectedString) throws Exception {
|
||||
Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve("reachability-metadata.json");
|
||||
String content = Files.readString(jsonFile);
|
||||
JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user