mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1827776d2e |
@@ -1,33 +0,0 @@
|
||||
name: Send notification
|
||||
description: Sends a Google Chat message as a notification of the job's outcome
|
||||
inputs:
|
||||
webhook-url:
|
||||
description: 'Google Chat Webhook URL'
|
||||
required: true
|
||||
status:
|
||||
description: 'Status of the job'
|
||||
required: true
|
||||
build-scan-url:
|
||||
description: 'URL of the build scan to include in the notification'
|
||||
run-name:
|
||||
description: 'Name of the run to include in the notification'
|
||||
default: ${{ format('{0} {1}', github.ref_name, github.job) }}
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- shell: bash
|
||||
run: |
|
||||
echo "BUILD_SCAN=${{ inputs.build-scan-url == '' && ' [build scan unavailable]' || format(' [<{0}|Build Scan>]', inputs.build-scan-url) }}" >> "$GITHUB_ENV"
|
||||
echo "RUN_URL=${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" >> "$GITHUB_ENV"
|
||||
- shell: bash
|
||||
if: ${{ inputs.status == 'success' }}
|
||||
run: |
|
||||
curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was successful ${{ env.BUILD_SCAN }}"}' || true
|
||||
- shell: bash
|
||||
if: ${{ inputs.status == 'failure' }}
|
||||
run: |
|
||||
curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<users/all> *<${{ env.RUN_URL }}|${{ inputs.run-name }}> failed* ${{ env.BUILD_SCAN }}"}' || true
|
||||
- shell: bash
|
||||
if: ${{ inputs.status == 'cancelled' }}
|
||||
run: |
|
||||
curl -X POST '${{ inputs.webhook-url }}' -H 'Content-Type: application/json' -d '{ text: "<${{ env.RUN_URL }}|${{ inputs.run-name }}> was cancelled"}' || true
|
||||
@@ -1,34 +0,0 @@
|
||||
name: Backport Bot
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
push:
|
||||
branches:
|
||||
- '*.x'
|
||||
permissions:
|
||||
contents: read
|
||||
jobs:
|
||||
build:
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
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_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_EVENT: ${{ toJSON(github.event) }}
|
||||
run: java -jar backport-bot-0.0.1-SNAPSHOT.jar --github.accessToken="$GITHUB_TOKEN" --github.event_name "$GITHUB_EVENT_NAME" --github.event "$GITHUB_EVENT"
|
||||
@@ -1,64 +0,0 @@
|
||||
name: Build and deploy snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 5.3.x
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-deploy-snapshot:
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
name: Build and deploy snapshot
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: 8
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5
|
||||
with:
|
||||
cache-read-only: false
|
||||
- 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
|
||||
- name: Build and publish
|
||||
id: build
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
run: ./gradlew -PdeploymentRepository=$(pwd)/deployment-repository build publishAllPublicationsToDeploymentRepository
|
||||
- name: Deploy
|
||||
uses: spring-io/artifactory-deploy-action@v0.0.1
|
||||
with:
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
build-name: ${{ format('spring-framework-{0}', github.ref_name)}}
|
||||
repository: 'libs-snapshot-local'
|
||||
folder: 'deployment-repository'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
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
|
||||
- name: Send notification
|
||||
uses: ./.github/actions/send-notification
|
||||
if: always()
|
||||
with:
|
||||
webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
status: ${{ job.status }}
|
||||
build-scan-url: ${{ steps.build.outputs.build-scan-url }}
|
||||
run-name: ${{ format('{0} | Linux | Java 8', github.ref_name) }}
|
||||
@@ -1,82 +0,0 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 5.3.x
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
ci:
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
strategy:
|
||||
matrix:
|
||||
os:
|
||||
- id: ubuntu-latest
|
||||
name: Linux
|
||||
java:
|
||||
- version: 8
|
||||
toolchain: false
|
||||
- version: 17
|
||||
toolchain: true
|
||||
- version: 21
|
||||
toolchain: true
|
||||
exclude:
|
||||
- os:
|
||||
name: Linux
|
||||
java:
|
||||
version: 8
|
||||
name: '${{ matrix.os.name}} | Java ${{ matrix.java.version}}'
|
||||
runs-on: ${{ matrix.os.id }}
|
||||
steps:
|
||||
- name: Set up Java
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'liberica'
|
||||
java-version: |
|
||||
${{ matrix.java.version }}
|
||||
${{ matrix.java.toolchain && '8' || '' }}
|
||||
- name: Prepare Windows runner
|
||||
if: ${{ runner.os == 'Windows' }}
|
||||
run: |
|
||||
git config --global core.autocrlf true
|
||||
git config --global core.longPaths true
|
||||
Stop-Service -name Docker
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5
|
||||
with:
|
||||
cache-read-only: false
|
||||
- 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
|
||||
- name: Configure toolchain properties
|
||||
if: ${{ matrix.java.toolchain }}
|
||||
shell: bash
|
||||
run: |
|
||||
echo toolchainVersion=${{ matrix.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', matrix.java.version) }} >> $HOME/.gradle/gradle.properties
|
||||
- name: Build
|
||||
id: build
|
||||
env:
|
||||
CI: 'true'
|
||||
GRADLE_ENTERPRISE_URL: 'https://ge.spring.io'
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ${{ secrets.GRADLE_ENTERPRISE_CACHE_USER }}
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ${{ secrets.GRADLE_ENTERPRISE_CACHE_PASSWORD }}
|
||||
run: ./gradlew check
|
||||
- name: Send notification
|
||||
uses: ./.github/actions/send-notification
|
||||
if: always()
|
||||
with:
|
||||
webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
status: ${{ job.status }}
|
||||
build-scan-url: ${{ steps.build.outputs.build-scan-url }}
|
||||
run-name: ${{ format('{0} | {1} | Java {2}', github.ref_name, matrix.os.name, matrix.java.version) }}
|
||||
@@ -9,5 +9,5 @@ jobs:
|
||||
name: "Validation"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: gradle/wrapper-validation-action@v2
|
||||
- uses: actions/checkout@v3
|
||||
- uses: gradle/wrapper-validation-action@v1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# <img src="framework-docs/src/docs/spring-framework.png" width="80" height="80"> Spring Framework [](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3A5.3.x) [](https://ge.spring.io/scans?search.rootProjectNames=spring)
|
||||
# <img src="src/docs/spring-framework.png" width="80" height="80"> Spring Framework [](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [](https://ge.spring.io/scans?search.rootProjectNames=spring)
|
||||
|
||||
This is the home of the Spring Framework: the foundation for all [Spring projects](https://spring.io/projects). Collectively the Spring Framework and the family of Spring projects are often referred to simply as "Spring".
|
||||
|
||||
|
||||
+5
-7
@@ -28,8 +28,8 @@ configure(allprojects) { project ->
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7"
|
||||
mavenBom "io.netty:netty-bom:4.1.108.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.43"
|
||||
mavenBom "io.netty:netty-bom:4.1.107.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.41"
|
||||
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13"
|
||||
mavenBom "io.rsocket:rsocket-bom:1.1.3"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.54.v20240208"
|
||||
@@ -375,9 +375,8 @@ configure([rootProject] + javaProjects) { project ->
|
||||
"https://tiles.apache.org/tiles-request/apidocs/",
|
||||
"https://tiles.apache.org/framework/apidocs/",
|
||||
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
|
||||
// Temporarily commenting out Ehcache and Quartz since javadoc on JDK 8 cannot access them.
|
||||
// "https://www.ehcache.org/apidocs/2.10.4/",
|
||||
// "https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://www.ehcache.org/apidocs/2.10.4/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.12.7/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.12.7/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.12.7/",
|
||||
@@ -389,8 +388,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
// "https://junit.org/junit5/docs/5.8.2/api/",
|
||||
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
|
||||
"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
|
||||
// Temporarily commenting out R2DBC since javadoc on JDK 8 cannot access it.
|
||||
// "https://r2dbc.io/spec/0.8.5.RELEASE/api/",
|
||||
"https://r2dbc.io/spec/0.8.5.RELEASE/api/",
|
||||
// The external Javadoc link for JSR 305 must come last to ensure that types from
|
||||
// JSR 250 (such as @PostConstruct) are still supported. This is due to the fact
|
||||
// that JSR 250 and JSR 305 both define types in javax.annotation, which results
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
== Spring Framework Concourse pipeline
|
||||
|
||||
NOTE: CI is being migrated to GitHub Actions.
|
||||
|
||||
The Spring Framework uses https://concourse-ci.org/[Concourse] for its CI build and other automated tasks.
|
||||
The Spring team has a dedicated Concourse instance available at https://ci.spring.io with a build pipeline
|
||||
for https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x[Spring Framework 5.3.x].
|
||||
|
||||
@@ -17,4 +17,4 @@ changelog:
|
||||
- "type: dependency-upgrade"
|
||||
contributors:
|
||||
exclude:
|
||||
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"]
|
||||
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll"]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ubuntu:jammy-20240125
|
||||
FROM ubuntu:jammy-20240111
|
||||
|
||||
ADD setup.sh /setup.sh
|
||||
ADD get-jdk-url.sh /get-jdk-url.sh
|
||||
|
||||
@@ -8,3 +8,4 @@ milestone: "5.3.x"
|
||||
build-name: "spring-framework"
|
||||
pipeline-name: "spring-framework"
|
||||
concourse-url: "https://ci.spring.io"
|
||||
task-timeout: 1h00m
|
||||
|
||||
+119
-13
@@ -23,6 +23,14 @@ anchors:
|
||||
docker-resource-source: &docker-resource-source
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
slack-fail-params: &slack-fail-params
|
||||
text: >
|
||||
:concourse-failed: <https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!>
|
||||
[$TEXT_FILE_CONTENT]
|
||||
text_file: git-repo/build/build-scan-uri.txt
|
||||
silent: true
|
||||
icon_emoji: ":concourse:"
|
||||
username: concourse-ci
|
||||
changelog-task-params: &changelog-task-params
|
||||
name: generated-changelog/tag
|
||||
tag: generated-changelog/tag
|
||||
@@ -37,7 +45,7 @@ resource_types:
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/registry-image-resource
|
||||
tag: 1.8.0
|
||||
tag: 1.7.1
|
||||
- name: artifactory-resource
|
||||
type: registry-image
|
||||
source:
|
||||
@@ -56,12 +64,25 @@ resource_types:
|
||||
<<: *docker-resource-source
|
||||
repository: dpb587/github-status-resource
|
||||
tag: master
|
||||
- name: slack-notification
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: cfcommunity/slack-notification-resource
|
||||
tag: latest
|
||||
resources:
|
||||
- name: git-repo
|
||||
type: git
|
||||
icon: github
|
||||
source:
|
||||
<<: *git-repo-resource-source
|
||||
- name: every-morning
|
||||
type: time
|
||||
icon: alarm
|
||||
source:
|
||||
start: 8:00 AM
|
||||
stop: 9:00 AM
|
||||
location: Europe/Vienna
|
||||
- name: ci-images-git-repo
|
||||
type: git
|
||||
icon: github
|
||||
@@ -84,6 +105,27 @@ resources:
|
||||
username: ((artifactory-username))
|
||||
password: ((artifactory-password))
|
||||
build_name: ((build-name))
|
||||
- name: repo-status-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
source:
|
||||
repository: ((github-repo-name))
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: build
|
||||
- name: repo-status-jdk17-build
|
||||
type: github-status-resource
|
||||
icon: eye-check-outline
|
||||
source:
|
||||
repository: ((github-repo-name))
|
||||
access_token: ((github-ci-status-token))
|
||||
branch: ((branch))
|
||||
context: jdk17-build
|
||||
- name: slack-alert
|
||||
type: slack-notification
|
||||
icon: slack
|
||||
source:
|
||||
url: ((slack-webhook-url))
|
||||
- name: github-pre-release
|
||||
type: github-release
|
||||
icon: briefcase-download-outline
|
||||
@@ -118,23 +160,37 @@ jobs:
|
||||
- put: ci-image
|
||||
params:
|
||||
image: ci-image/image.tar
|
||||
- name: stage-milestone
|
||||
- name: build
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- task: stage
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
<<: *gradle-enterprise-task-params
|
||||
trigger: true
|
||||
- put: repo-status-build
|
||||
params: { state: "pending", commit: "git-repo" }
|
||||
- do:
|
||||
- task: build-project
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/build-project.yml
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- put: artifactory-repo
|
||||
params: &artifactory-params
|
||||
signing_key: ((signing-key))
|
||||
signing_passphrase: ((signing-passphrase))
|
||||
repo: libs-staging-local
|
||||
repo: libs-snapshot-local
|
||||
folder: distribution-repository
|
||||
build_uri: "https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}"
|
||||
build_number: "${BUILD_PIPELINE_NAME}-${BUILD_JOB_NAME}-${BUILD_NAME}"
|
||||
@@ -159,9 +215,55 @@ jobs:
|
||||
- "/**/spring-*-schema.zip"
|
||||
properties:
|
||||
"zip.type": "schema"
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
get_params:
|
||||
threads: 8
|
||||
- name: jdk17-build
|
||||
serial: true
|
||||
public: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
- get: every-morning
|
||||
trigger: true
|
||||
- put: repo-status-jdk17-build
|
||||
params: { state: "pending", commit: "git-repo" }
|
||||
- do:
|
||||
- task: check-project
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/check-project.yml
|
||||
privileged: true
|
||||
timeout: ((task-timeout))
|
||||
params:
|
||||
TEST_TOOLCHAIN: 17
|
||||
<<: *build-project-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk17-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk17-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: stage-milestone
|
||||
serial: true
|
||||
plan:
|
||||
- get: ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- task: stage
|
||||
image: ci-image
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
<<: *gradle-enterprise-task-params
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
repo: libs-staging-local
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
- name: promote-milestone
|
||||
serial: true
|
||||
plan:
|
||||
@@ -202,6 +304,7 @@ jobs:
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
repo: libs-staging-local
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
@@ -245,6 +348,7 @@ jobs:
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
repo: libs-staging-local
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
@@ -287,6 +391,8 @@ jobs:
|
||||
<<: *changelog-task-params
|
||||
|
||||
groups:
|
||||
- name: "builds"
|
||||
jobs: ["build", "jdk17-build"]
|
||||
- name: "releases"
|
||||
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone", "promote-rc", "promote-release", "create-github-release"]
|
||||
- name: "ci-images"
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 check
|
||||
popd > /dev/null
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
source $(dirname $0)/common.sh
|
||||
repository=$(pwd)/distribution-repository
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false --no-daemon --max-workers=4 -PdeploymentRepository=${repository} build publishAllPublicationsToDeploymentRepository
|
||||
popd > /dev/null
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
source $(dirname $0)/common.sh
|
||||
|
||||
pushd git-repo > /dev/null
|
||||
./gradlew -Dorg.gradle.internal.launcher.welcomeMessageEnabled=false -Porg.gradle.java.installations.fromEnv=JDK17 \
|
||||
-PmainToolchain=${MAIN_TOOLCHAIN} -PtestToolchain=${TEST_TOOLCHAIN} --no-daemon --max-workers=4 check
|
||||
popd > /dev/null
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
source $(dirname $0)/common.sh
|
||||
CONFIG_DIR=git-repo/ci/config
|
||||
|
||||
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
|
||||
|
||||
@@ -26,5 +26,4 @@ run:
|
||||
cat > /root/.docker/config.json <<EOF
|
||||
{ "auths": { "https://index.docker.io/v1/": { "auth": "$DOCKER_HUB_AUTH" }}}
|
||||
EOF
|
||||
build
|
||||
|
||||
build
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
platform: linux
|
||||
inputs:
|
||||
- name: git-repo
|
||||
caches:
|
||||
- path: gradle
|
||||
params:
|
||||
BRANCH:
|
||||
CI: true
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME:
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD:
|
||||
GRADLE_ENTERPRISE_URL: https://ge.spring.io
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
${PWD}/git-repo/ci/scripts/build-pr.sh
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
platform: linux
|
||||
inputs:
|
||||
- name: git-repo
|
||||
outputs:
|
||||
- name: distribution-repository
|
||||
- name: git-repo
|
||||
caches:
|
||||
- path: gradle
|
||||
params:
|
||||
BRANCH:
|
||||
CI: true
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME:
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD:
|
||||
GRADLE_ENTERPRISE_URL: https://ge.spring.io
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
${PWD}/git-repo/ci/scripts/build-project.sh
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
platform: linux
|
||||
inputs:
|
||||
- name: git-repo
|
||||
outputs:
|
||||
- name: distribution-repository
|
||||
- name: git-repo
|
||||
caches:
|
||||
- path: gradle
|
||||
params:
|
||||
BRANCH:
|
||||
CI: true
|
||||
MAIN_TOOLCHAIN:
|
||||
TEST_TOOLCHAIN:
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY:
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME:
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD:
|
||||
GRADLE_ENTERPRISE_URL: https://ge.spring.io
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
${PWD}/git-repo/ci/scripts/check-project.sh
|
||||
@@ -4,7 +4,7 @@ image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: springio/github-changelog-generator
|
||||
tag: '0.0.8'
|
||||
tag: '0.0.7'
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
inputs:
|
||||
|
||||
@@ -4,7 +4,7 @@ image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: springio/concourse-release-scripts
|
||||
tag: '0.4.0'
|
||||
tag: '0.3.4'
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
inputs:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=5.3.34
|
||||
version=5.3.32
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -5,7 +5,7 @@ tasks.findByName("dokkaHtmlPartial")?.configure {
|
||||
sourceRoots.setFrom(file("src/main/kotlin"))
|
||||
classpath.from(sourceSets["main"].runtimeClasspath)
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://docs.spring.io/spring-framework/docs/5.3.x/javadoc-api/"))
|
||||
url.set(new URL("https://docs.spring.io/spring-framework/docs/current/javadoc-api/"))
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url.set(new URL("https://projectreactor.io/docs/core/release/api/"))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -186,10 +186,10 @@ public abstract class AopUtils {
|
||||
* this method resolves bridge methods in order to retrieve attributes from
|
||||
* the <i>original</i> method definition.
|
||||
* @param method the method to be invoked, which may come from an interface
|
||||
* @param targetClass the target class for the current invocation
|
||||
* (can be {@code null} or may not even implement the method)
|
||||
* @param targetClass the target class for the current invocation.
|
||||
* May be {@code null} or may not even implement the method.
|
||||
* @return the specific target method, or the original method if the
|
||||
* {@code targetClass} does not implement it
|
||||
* {@code targetClass} doesn't implement it or is {@code null}
|
||||
* @see org.springframework.util.ClassUtils#getMostSpecificMethod
|
||||
*/
|
||||
public static Method getMostSpecificMethod(Method method, @Nullable Class<?> targetClass) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
@@ -17,35 +17,29 @@
|
||||
package org.springframework.aop.support;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.ClassFilter;
|
||||
import org.springframework.aop.MethodMatcher;
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.interceptor.ExposeInvocationInterceptor;
|
||||
import org.springframework.aop.target.EmptyTargetSource;
|
||||
import org.springframework.aop.testfixture.interceptor.NopInterceptor;
|
||||
import org.springframework.beans.testfixture.beans.TestBean;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.testfixture.io.SerializationTestUtils;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Rod Johnson
|
||||
* @author Chris Beams
|
||||
* @author Sebastien Deleuze
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class AopUtilsTests {
|
||||
public class AopUtilsTests {
|
||||
|
||||
@Test
|
||||
void testPointcutCanNeverApply() {
|
||||
public void testPointcutCanNeverApply() {
|
||||
class TestPointcut extends StaticMethodMatcherPointcut {
|
||||
@Override
|
||||
public boolean matches(Method method, @Nullable Class<?> clazzy) {
|
||||
@@ -58,13 +52,13 @@ class AopUtilsTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPointcutAlwaysApplies() {
|
||||
public void testPointcutAlwaysApplies() {
|
||||
assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue();
|
||||
assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPointcutAppliesToOneMethodOnObject() {
|
||||
public void testPointcutAppliesToOneMethodOnObject() {
|
||||
class TestPointcut extends StaticMethodMatcherPointcut {
|
||||
@Override
|
||||
public boolean matches(Method method, @Nullable Class<?> clazz) {
|
||||
@@ -84,7 +78,7 @@ class AopUtilsTests {
|
||||
* that's subverted the singleton construction limitation.
|
||||
*/
|
||||
@Test
|
||||
void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception {
|
||||
public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception {
|
||||
assertThat(SerializationTestUtils.serializeAndDeserialize(MethodMatcher.TRUE)).isSameAs(MethodMatcher.TRUE);
|
||||
assertThat(SerializationTestUtils.serializeAndDeserialize(ClassFilter.TRUE)).isSameAs(ClassFilter.TRUE);
|
||||
assertThat(SerializationTestUtils.serializeAndDeserialize(Pointcut.TRUE)).isSameAs(Pointcut.TRUE);
|
||||
@@ -94,45 +88,4 @@ class AopUtilsTests {
|
||||
assertThat(SerializationTestUtils.serializeAndDeserialize(ExposeInvocationInterceptor.INSTANCE)).isSameAs(ExposeInvocationInterceptor.INSTANCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testInvokeJoinpointUsingReflection() throws Throwable {
|
||||
String name = "foo";
|
||||
TestBean testBean = new TestBean(name);
|
||||
Method method = ReflectionUtils.findMethod(TestBean.class, "getName");
|
||||
Object result = AopUtils.invokeJoinpointUsingReflection(testBean, method, new Object[0]);
|
||||
assertThat(result).isEqualTo(name);
|
||||
}
|
||||
|
||||
@Test // gh-32365
|
||||
void mostSpecificMethodBetweenJdkProxyAndTarget() throws Exception {
|
||||
Class<?> proxyClass = new ProxyFactory(new WithInterface()).getProxy(getClass().getClassLoader()).getClass();
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithInterface.class);
|
||||
assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class);
|
||||
}
|
||||
|
||||
@Test // gh-32365
|
||||
void mostSpecificMethodBetweenCglibProxyAndTarget() throws Exception {
|
||||
Class<?> proxyClass = new ProxyFactory(new WithoutInterface()).getProxy(getClass().getClassLoader()).getClass();
|
||||
Method specificMethod = AopUtils.getMostSpecificMethod(proxyClass.getMethod("handle", List.class), WithoutInterface.class);
|
||||
assertThat(ResolvableType.forMethodParameter(specificMethod, 0).getGeneric().toClass()).isEqualTo(String.class);
|
||||
}
|
||||
|
||||
|
||||
interface ProxyInterface {
|
||||
|
||||
void handle(List<String> list);
|
||||
}
|
||||
|
||||
static class WithInterface implements ProxyInterface {
|
||||
|
||||
public void handle(List<String> list) {
|
||||
}
|
||||
}
|
||||
|
||||
static class WithoutInterface {
|
||||
|
||||
public void handle(List<String> list) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -166,9 +166,6 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
/** Map from scope identifier String to corresponding Scope. */
|
||||
private final Map<String, Scope> scopes = new LinkedHashMap<>(8);
|
||||
|
||||
/** Application startup metrics. **/
|
||||
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
|
||||
|
||||
/** Security context used when running with a SecurityManager. */
|
||||
@Nullable
|
||||
private SecurityContextProvider securityContextProvider;
|
||||
@@ -183,6 +180,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
private final ThreadLocal<Object> prototypesCurrentlyInCreation =
|
||||
new NamedThreadLocal<>("Prototype beans currently in creation");
|
||||
|
||||
/** Application startup metrics. **/
|
||||
private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT;
|
||||
|
||||
/**
|
||||
* Create a new AbstractBeanFactory.
|
||||
|
||||
+7
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -600,10 +600,13 @@ class ConstructorResolver {
|
||||
String argDesc = StringUtils.collectionToCommaDelimitedString(argTypes);
|
||||
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
|
||||
"No matching factory method found on class [" + factoryClass.getName() + "]: " +
|
||||
(mbd.getFactoryBeanName() != null ? "factory bean '" + mbd.getFactoryBeanName() + "'; " : "") +
|
||||
(mbd.getFactoryBeanName() != null ?
|
||||
"factory bean '" + mbd.getFactoryBeanName() + "'; " : "") +
|
||||
"factory method '" + mbd.getFactoryMethodName() + "(" + argDesc + ")'. " +
|
||||
"Check that a method with the specified name " + (minNrOfArgs > 0 ? "and arguments " : "") +
|
||||
"exists and that it is " + (isStatic ? "static" : "non-static") + ".");
|
||||
"Check that a method with the specified name " +
|
||||
(minNrOfArgs > 0 ? "and arguments " : "") +
|
||||
"exists and that it is " +
|
||||
(isStatic ? "static" : "non-static") + ".");
|
||||
}
|
||||
else if (void.class == factoryMethodToUse.getReturnType()) {
|
||||
throw new BeanCreationException(mbd.getResourceDescription(), beanName,
|
||||
|
||||
+6
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -567,16 +567,16 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
*/
|
||||
protected void destroyBean(String beanName, @Nullable DisposableBean bean) {
|
||||
// Trigger destruction of dependent beans first...
|
||||
Set<String> dependentBeanNames;
|
||||
Set<String> dependencies;
|
||||
synchronized (this.dependentBeanMap) {
|
||||
// Within full synchronization in order to guarantee a disconnected Set
|
||||
dependentBeanNames = this.dependentBeanMap.remove(beanName);
|
||||
dependencies = this.dependentBeanMap.remove(beanName);
|
||||
}
|
||||
if (dependentBeanNames != null) {
|
||||
if (dependencies != null) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependentBeanNames);
|
||||
logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependencies);
|
||||
}
|
||||
for (String dependentBeanName : dependentBeanNames) {
|
||||
for (String dependentBeanName : dependencies) {
|
||||
destroySingleton(dependentBeanName);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-12
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -72,6 +72,7 @@ final class ConfigurationClass {
|
||||
* Create a new {@link ConfigurationClass} with the given name.
|
||||
* @param metadataReader reader used to parse the underlying {@link Class}
|
||||
* @param beanName must not be {@code null}
|
||||
* @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
|
||||
*/
|
||||
ConfigurationClass(MetadataReader metadataReader, String beanName) {
|
||||
Assert.notNull(beanName, "Bean name must not be null");
|
||||
@@ -85,10 +86,10 @@ final class ConfigurationClass {
|
||||
* using the {@link Import} annotation or automatically processed as a nested
|
||||
* configuration class (if importedBy is not {@code null}).
|
||||
* @param metadataReader reader used to parse the underlying {@link Class}
|
||||
* @param importedBy the configuration class importing this one
|
||||
* @param importedBy the configuration class importing this one or {@code null}
|
||||
* @since 3.1.1
|
||||
*/
|
||||
ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) {
|
||||
ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) {
|
||||
this.metadata = metadataReader.getAnnotationMetadata();
|
||||
this.resource = metadataReader.getResource();
|
||||
this.importedBy.add(importedBy);
|
||||
@@ -98,6 +99,7 @@ final class ConfigurationClass {
|
||||
* Create a new {@link ConfigurationClass} with the given name.
|
||||
* @param clazz the underlying {@link Class} to represent
|
||||
* @param beanName name of the {@code @Configuration} class bean
|
||||
* @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
|
||||
*/
|
||||
ConfigurationClass(Class<?> clazz, String beanName) {
|
||||
Assert.notNull(beanName, "Bean name must not be null");
|
||||
@@ -111,10 +113,10 @@ final class ConfigurationClass {
|
||||
* using the {@link Import} annotation or automatically processed as a nested
|
||||
* configuration class (if imported is {@code true}).
|
||||
* @param clazz the underlying {@link Class} to represent
|
||||
* @param importedBy the configuration class importing this one
|
||||
* @param importedBy the configuration class importing this one (or {@code null})
|
||||
* @since 3.1.1
|
||||
*/
|
||||
ConfigurationClass(Class<?> clazz, ConfigurationClass importedBy) {
|
||||
ConfigurationClass(Class<?> clazz, @Nullable ConfigurationClass importedBy) {
|
||||
this.metadata = AnnotationMetadata.introspect(clazz);
|
||||
this.resource = new DescriptiveResource(clazz.getName());
|
||||
this.importedBy.add(importedBy);
|
||||
@@ -124,6 +126,7 @@ final class ConfigurationClass {
|
||||
* Create a new {@link ConfigurationClass} with the given name.
|
||||
* @param metadata the metadata for the underlying class to represent
|
||||
* @param beanName name of the {@code @Configuration} class bean
|
||||
* @see ConfigurationClass#ConfigurationClass(Class, ConfigurationClass)
|
||||
*/
|
||||
ConfigurationClass(AnnotationMetadata metadata, String beanName) {
|
||||
Assert.notNull(beanName, "Bean name must not be null");
|
||||
@@ -145,12 +148,12 @@ final class ConfigurationClass {
|
||||
return ClassUtils.getShortName(getMetadata().getClassName());
|
||||
}
|
||||
|
||||
void setBeanName(@Nullable String beanName) {
|
||||
void setBeanName(String beanName) {
|
||||
this.beanName = beanName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
String getBeanName() {
|
||||
public String getBeanName() {
|
||||
return this.beanName;
|
||||
}
|
||||
|
||||
@@ -160,7 +163,7 @@ final class ConfigurationClass {
|
||||
* @since 3.1.1
|
||||
* @see #getImportedBy()
|
||||
*/
|
||||
boolean isImported() {
|
||||
public boolean isImported() {
|
||||
return !this.importedBy.isEmpty();
|
||||
}
|
||||
|
||||
@@ -194,10 +197,6 @@ final class ConfigurationClass {
|
||||
this.importedResources.put(importedResource, readerClass);
|
||||
}
|
||||
|
||||
Map<String, Class<? extends BeanDefinitionReader>> getImportedResources() {
|
||||
return this.importedResources;
|
||||
}
|
||||
|
||||
void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) {
|
||||
this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata);
|
||||
}
|
||||
@@ -206,6 +205,10 @@ final class ConfigurationClass {
|
||||
return this.importBeanDefinitionRegistrars;
|
||||
}
|
||||
|
||||
Map<String, Class<? extends BeanDefinitionReader>> getImportedResources() {
|
||||
return this.importedResources;
|
||||
}
|
||||
|
||||
void validate(ProblemReporter problemReporter) {
|
||||
// A configuration class may not be final (CGLIB limitation) unless it declares proxyBeanMethods=false
|
||||
Map<String, Object> attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName());
|
||||
|
||||
+21
-24
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -136,6 +136,17 @@ import org.springframework.util.ReflectionUtils;
|
||||
public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
implements ConfigurableApplicationContext {
|
||||
|
||||
/**
|
||||
* The name of the {@link LifecycleProcessor} bean in the context.
|
||||
* If none is supplied, a {@link DefaultLifecycleProcessor} is used.
|
||||
* @since 3.0
|
||||
* @see org.springframework.context.LifecycleProcessor
|
||||
* @see org.springframework.context.support.DefaultLifecycleProcessor
|
||||
* @see #start()
|
||||
* @see #stop()
|
||||
*/
|
||||
public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor";
|
||||
|
||||
/**
|
||||
* The name of the {@link MessageSource} bean in the context.
|
||||
* If none is supplied, message resolution is delegated to the parent.
|
||||
@@ -156,18 +167,6 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
*/
|
||||
public static final String APPLICATION_EVENT_MULTICASTER_BEAN_NAME = "applicationEventMulticaster";
|
||||
|
||||
/**
|
||||
* The name of the {@link LifecycleProcessor} bean in the context.
|
||||
* If none is supplied, a {@link DefaultLifecycleProcessor} is used.
|
||||
* @since 3.0
|
||||
* @see org.springframework.context.LifecycleProcessor
|
||||
* @see org.springframework.context.support.DefaultLifecycleProcessor
|
||||
* @see #start()
|
||||
* @see #stop()
|
||||
*/
|
||||
public static final String LIFECYCLE_PROCESSOR_BEAN_NAME = "lifecycleProcessor";
|
||||
|
||||
|
||||
/**
|
||||
* Boolean flag controlled by a {@code spring.spel.ignore} system property that
|
||||
* instructs Spring to ignore SpEL, i.e. to not initialize the SpEL infrastructure.
|
||||
@@ -571,6 +570,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
|
||||
// Invoke factory processors registered as beans in the context.
|
||||
invokeBeanFactoryPostProcessors(beanFactory);
|
||||
|
||||
// Register bean processors that intercept bean creation.
|
||||
registerBeanPostProcessors(beanFactory);
|
||||
beanPostProcess.end();
|
||||
@@ -774,9 +774,8 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the {@link MessageSource}.
|
||||
* <p>Uses parent's {@code MessageSource} if none defined in this context.
|
||||
* @see #MESSAGE_SOURCE_BEAN_NAME
|
||||
* Initialize the MessageSource.
|
||||
* Use parent's if none defined in this context.
|
||||
*/
|
||||
protected void initMessageSource() {
|
||||
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
|
||||
@@ -808,9 +807,8 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the {@link ApplicationEventMulticaster}.
|
||||
* <p>Uses {@link SimpleApplicationEventMulticaster} if none defined in the context.
|
||||
* @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME
|
||||
* Initialize the ApplicationEventMulticaster.
|
||||
* Uses SimpleApplicationEventMulticaster if none defined in the context.
|
||||
* @see org.springframework.context.event.SimpleApplicationEventMulticaster
|
||||
*/
|
||||
protected void initApplicationEventMulticaster() {
|
||||
@@ -833,16 +831,15 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the {@link LifecycleProcessor}.
|
||||
* <p>Uses {@link DefaultLifecycleProcessor} if none defined in the context.
|
||||
* @since 3.0
|
||||
* @see #LIFECYCLE_PROCESSOR_BEAN_NAME
|
||||
* Initialize the LifecycleProcessor.
|
||||
* Uses DefaultLifecycleProcessor if none defined in the context.
|
||||
* @see org.springframework.context.support.DefaultLifecycleProcessor
|
||||
*/
|
||||
protected void initLifecycleProcessor() {
|
||||
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
|
||||
if (beanFactory.containsLocalBean(LIFECYCLE_PROCESSOR_BEAN_NAME)) {
|
||||
this.lifecycleProcessor = beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class);
|
||||
this.lifecycleProcessor =
|
||||
beanFactory.getBean(LIFECYCLE_PROCESSOR_BEAN_NAME, LifecycleProcessor.class);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Using LifecycleProcessor [" + this.lifecycleProcessor + "]");
|
||||
}
|
||||
|
||||
+11
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -133,6 +133,11 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche
|
||||
* execution callback (which may be a wrapper around the user-supplied task).
|
||||
* <p>The primary use case is to set some execution context around the task's
|
||||
* invocation, or to provide some monitoring/statistics for task execution.
|
||||
* <p><b>NOTE:</b> Exception handling in {@code TaskDecorator} implementations
|
||||
* is limited to plain {@code Runnable} execution via {@code execute} calls.
|
||||
* In case of {@code #submit} calls, the exposed {@code Runnable} will be a
|
||||
* {@code FutureTask} which does not propagate any exceptions; you might
|
||||
* have to cast it and call {@code Future#get} to evaluate exceptions.
|
||||
* @since 4.3
|
||||
*/
|
||||
public final void setTaskDecorator(TaskDecorator taskDecorator) {
|
||||
@@ -173,10 +178,11 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche
|
||||
}
|
||||
|
||||
|
||||
private TaskExecutorAdapter getAdaptedExecutor(Executor originalExecutor) {
|
||||
TaskExecutorAdapter adapter =
|
||||
(managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(originalExecutor) ?
|
||||
new ManagedTaskExecutorAdapter(originalExecutor) : new TaskExecutorAdapter(originalExecutor));
|
||||
private TaskExecutorAdapter getAdaptedExecutor(Executor concurrentExecutor) {
|
||||
if (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(concurrentExecutor)) {
|
||||
return new ManagedTaskExecutorAdapter(concurrentExecutor);
|
||||
}
|
||||
TaskExecutorAdapter adapter = new TaskExecutorAdapter(concurrentExecutor);
|
||||
if (this.taskDecorator != null) {
|
||||
adapter.setTaskDecorator(this.taskDecorator);
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -177,7 +177,6 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T
|
||||
* @see Clock#systemDefaultZone()
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "Clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
|
||||
+3
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,9 +45,8 @@ import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureTask;
|
||||
|
||||
/**
|
||||
* A standard implementation of Spring's {@link TaskScheduler} interface, wrapping
|
||||
* a native {@link java.util.concurrent.ScheduledThreadPoolExecutor} and providing
|
||||
* all applicable configuration options for it.
|
||||
* Implementation of Spring's {@link TaskScheduler} interface, wrapping
|
||||
* a native {@link java.util.concurrent.ScheduledThreadPoolExecutor}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Mark Fisher
|
||||
@@ -155,7 +154,6 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport
|
||||
* @see Clock#systemDefaultZone()
|
||||
*/
|
||||
public void setClock(Clock clock) {
|
||||
Assert.notNull(clock, "Clock must not be null");
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -268,7 +268,6 @@ public class ClassPathBeanDefinitionScannerTests {
|
||||
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
|
||||
scanner.setIncludeAnnotationConfig(false);
|
||||
scanner.scan("org.springframework.context.annotation2");
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> scanner.scan(BASE_PACKAGE))
|
||||
.withMessageContaining("myNamedDao")
|
||||
.withMessageContaining(NamedStubDao.class.getName())
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -17,8 +17,8 @@
|
||||
package org.springframework.scheduling.concurrent;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.concurrent.RunnableFuture;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -52,8 +52,8 @@ class ConcurrentTaskExecutorTests extends AbstractSchedulingTaskExecutorTests {
|
||||
@AfterEach
|
||||
void shutdownExecutor() {
|
||||
for (Runnable task : concurrentExecutor.shutdownNow()) {
|
||||
if (task instanceof Future) {
|
||||
((Future<?>) task).cancel(true);
|
||||
if (task instanceof RunnableFuture) {
|
||||
((RunnableFuture<?>) task).cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
*/
|
||||
class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests {
|
||||
public class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests {
|
||||
|
||||
private final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
|
||||
@@ -97,7 +97,7 @@ class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void scheduleOneTimeFailingTaskWithoutErrorHandler() {
|
||||
void scheduleOneTimeFailingTaskWithoutErrorHandler() throws Exception {
|
||||
TestTask task = new TestTask(this.testName, 0);
|
||||
Future<?> future = scheduler.schedule(task, new Date());
|
||||
assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> future.get(1000, TimeUnit.MILLISECONDS));
|
||||
@@ -149,7 +149,7 @@ class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests {
|
||||
catch (InterruptedException ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
assertThat(latch.getCount()).as("latch did not count down").isEqualTo(0);
|
||||
assertThat(latch.getCount()).as("latch did not count down,").isEqualTo(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -36,7 +36,6 @@ import org.springframework.util.ReflectionUtils;
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @since 4.2.3
|
||||
*/
|
||||
public final class MethodIntrospector {
|
||||
@@ -76,7 +75,6 @@ public final class MethodIntrospector {
|
||||
if (result != null) {
|
||||
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
|
||||
if (bridgedMethod == specificMethod || bridgedMethod == method ||
|
||||
bridgedMethod.equals(specificMethod) || bridgedMethod.equals(method) ||
|
||||
metadataLookup.inspect(bridgedMethod) == null) {
|
||||
methodMap.put(specificMethod, result);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -98,9 +98,7 @@ public final class ReactiveTypeDescriptor {
|
||||
*/
|
||||
public Object getEmptyValue() {
|
||||
Assert.state(this.emptySupplier != null, "Empty values not supported");
|
||||
Object emptyValue = this.emptySupplier.get();
|
||||
Assert.notNull(emptyValue, "Invalid null return value from emptySupplier");
|
||||
return emptyValue;
|
||||
return this.emptySupplier.get();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +130,7 @@ public final class ReactiveTypeDescriptor {
|
||||
|
||||
|
||||
/**
|
||||
* Descriptor for a reactive type that can produce {@code 0..N} values.
|
||||
* Descriptor for a reactive type that can produce 0..N values.
|
||||
* @param type the reactive type
|
||||
* @param emptySupplier a supplier of an empty-value instance of the reactive type
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -750,7 +750,7 @@ public class ResolvableType implements Serializable {
|
||||
* Convenience method that will {@link #getGenerics() get} and
|
||||
* {@link #resolve() resolve} generic parameters.
|
||||
* @return an array of resolved generic parameters (the resulting array
|
||||
* will never be {@code null}, but it may contain {@code null} elements)
|
||||
* will never be {@code null}, but it may contain {@code null} elements})
|
||||
* @see #getGenerics()
|
||||
* @see #resolve()
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -95,19 +95,20 @@ public interface Decoder<T> {
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
|
||||
|
||||
CompletableFuture<T> future = decodeToMono(Mono.just(buffer), targetType, mimeType, hints).toFuture();
|
||||
Assert.state(future.isDone(), "DataBuffer decoding should have completed");
|
||||
Assert.state(future.isDone(), "DataBuffer decoding should have completed.");
|
||||
|
||||
Throwable failure;
|
||||
try {
|
||||
return future.get();
|
||||
}
|
||||
catch (ExecutionException ex) {
|
||||
Throwable cause = ex.getCause();
|
||||
throw (cause instanceof CodecException ? (CodecException) cause :
|
||||
new DecodingException("Failed to decode: " + (cause != null ? cause.getMessage() : ex), cause));
|
||||
failure = ex.getCause();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
throw new DecodingException("Interrupted during decode", ex);
|
||||
failure = ex;
|
||||
}
|
||||
throw (failure instanceof CodecException ? (CodecException) failure :
|
||||
new DecodingException("Failed to decode: " + failure.getMessage(), failure));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -76,11 +76,10 @@ public class ResourceDecoder extends AbstractDataBufferDecoder<Resource> {
|
||||
}
|
||||
|
||||
Class<?> clazz = elementType.toClass();
|
||||
String filename = (hints != null ? (String) hints.get(FILENAME_HINT) : null);
|
||||
String filename = hints != null ? (String) hints.get(FILENAME_HINT) : null;
|
||||
if (clazz == InputStreamResource.class) {
|
||||
return new InputStreamResource(new ByteArrayInputStream(bytes)) {
|
||||
@Override
|
||||
@Nullable
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
@@ -93,7 +92,6 @@ public class ResourceDecoder extends AbstractDataBufferDecoder<Resource> {
|
||||
else if (Resource.class.isAssignableFrom(clazz)) {
|
||||
return new ByteArrayResource(bytes) {
|
||||
@Override
|
||||
@Nullable
|
||||
public String getFilename() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -52,6 +52,8 @@ import org.springframework.util.ObjectUtils;
|
||||
@SuppressWarnings("serial")
|
||||
public class TypeDescriptor implements Serializable {
|
||||
|
||||
private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0];
|
||||
|
||||
private static final Map<Class<?>, TypeDescriptor> commonTypesCache = new HashMap<>(32);
|
||||
|
||||
private static final Class<?>[] CACHED_COMMON_TYPES = {
|
||||
@@ -82,7 +84,7 @@ public class TypeDescriptor implements Serializable {
|
||||
public TypeDescriptor(MethodParameter methodParameter) {
|
||||
this.resolvableType = ResolvableType.forMethodParameter(methodParameter);
|
||||
this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType());
|
||||
this.annotatedElement = AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ?
|
||||
this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ?
|
||||
methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations());
|
||||
}
|
||||
|
||||
@@ -94,7 +96,7 @@ public class TypeDescriptor implements Serializable {
|
||||
public TypeDescriptor(Field field) {
|
||||
this.resolvableType = ResolvableType.forField(field);
|
||||
this.type = this.resolvableType.resolve(field.getType());
|
||||
this.annotatedElement = AnnotatedElementAdapter.from(field.getAnnotations());
|
||||
this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,7 +109,7 @@ public class TypeDescriptor implements Serializable {
|
||||
Assert.notNull(property, "Property must not be null");
|
||||
this.resolvableType = ResolvableType.forMethodParameter(property.getMethodParameter());
|
||||
this.type = this.resolvableType.resolve(property.getType());
|
||||
this.annotatedElement = AnnotatedElementAdapter.from(property.getAnnotations());
|
||||
this.annotatedElement = new AnnotatedElementAdapter(property.getAnnotations());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -123,7 +125,7 @@ public class TypeDescriptor implements Serializable {
|
||||
public TypeDescriptor(ResolvableType resolvableType, @Nullable Class<?> type, @Nullable Annotation[] annotations) {
|
||||
this.resolvableType = resolvableType;
|
||||
this.type = (type != null ? type : resolvableType.toClass());
|
||||
this.annotatedElement = AnnotatedElementAdapter.from(annotations);
|
||||
this.annotatedElement = new AnnotatedElementAdapter(annotations);
|
||||
}
|
||||
|
||||
|
||||
@@ -732,26 +734,18 @@ public class TypeDescriptor implements Serializable {
|
||||
* @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class)
|
||||
* @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class)
|
||||
*/
|
||||
private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable {
|
||||
|
||||
private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]);
|
||||
private class AnnotatedElementAdapter implements AnnotatedElement, Serializable {
|
||||
|
||||
@Nullable
|
||||
private final Annotation[] annotations;
|
||||
|
||||
private AnnotatedElementAdapter(Annotation[] annotations) {
|
||||
public AnnotatedElementAdapter(@Nullable Annotation[] annotations) {
|
||||
this.annotations = annotations;
|
||||
}
|
||||
|
||||
private static AnnotatedElementAdapter from(@Nullable Annotation[] annotations) {
|
||||
if (annotations == null || annotations.length == 0) {
|
||||
return EMPTY;
|
||||
}
|
||||
return new AnnotatedElementAdapter(annotations);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) {
|
||||
for (Annotation annotation : this.annotations) {
|
||||
for (Annotation annotation : getAnnotations()) {
|
||||
if (annotation.annotationType() == annotationClass) {
|
||||
return true;
|
||||
}
|
||||
@@ -763,7 +757,7 @@ public class TypeDescriptor implements Serializable {
|
||||
@Nullable
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
|
||||
for (Annotation annotation : this.annotations) {
|
||||
for (Annotation annotation : getAnnotations()) {
|
||||
if (annotation.annotationType() == annotationClass) {
|
||||
return (T) annotation;
|
||||
}
|
||||
@@ -773,7 +767,7 @@ public class TypeDescriptor implements Serializable {
|
||||
|
||||
@Override
|
||||
public Annotation[] getAnnotations() {
|
||||
return (isEmpty() ? this.annotations : this.annotations.clone());
|
||||
return (this.annotations != null ? this.annotations.clone() : EMPTY_ANNOTATION_ARRAY);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -782,7 +776,7 @@ public class TypeDescriptor implements Serializable {
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return (this.annotations.length == 0);
|
||||
return ObjectUtils.isEmpty(this.annotations);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -798,7 +792,7 @@ public class TypeDescriptor implements Serializable {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return Arrays.toString(this.annotations);
|
||||
return TypeDescriptor.this.toString();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -138,7 +138,7 @@ final class ObjectToObjectConverter implements ConditionalGenericConverter {
|
||||
@Nullable
|
||||
private static Executable getValidatedExecutable(Class<?> targetClass, Class<?> sourceClass) {
|
||||
Executable executable = conversionExecutableCache.get(targetClass);
|
||||
if (executable != null && isApplicable(executable, sourceClass)) {
|
||||
if (isApplicable(executable, sourceClass)) {
|
||||
return executable;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -62,7 +62,7 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public abstract class DataBufferUtils {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(DataBufferUtils.class);
|
||||
private final static Log logger = LogFactory.getLog(DataBufferUtils.class);
|
||||
|
||||
private static final Consumer<DataBuffer> RELEASE_CONSUMER = DataBufferUtils::release;
|
||||
|
||||
@@ -728,7 +728,7 @@ public abstract class DataBufferUtils {
|
||||
*/
|
||||
private static class SingleByteMatcher implements NestedMatcher {
|
||||
|
||||
static final SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10});
|
||||
static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10});
|
||||
|
||||
private final byte[] delimiter;
|
||||
|
||||
@@ -767,7 +767,7 @@ public abstract class DataBufferUtils {
|
||||
/**
|
||||
* Base class for a {@link NestedMatcher}.
|
||||
*/
|
||||
private abstract static class AbstractNestedMatcher implements NestedMatcher {
|
||||
private static abstract class AbstractNestedMatcher implements NestedMatcher {
|
||||
|
||||
private final byte[] delimiter;
|
||||
|
||||
@@ -1005,11 +1005,11 @@ public abstract class DataBufferUtils {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable ex, DataBuffer dataBuffer) {
|
||||
public void failed(Throwable exc, DataBuffer dataBuffer) {
|
||||
release(dataBuffer);
|
||||
closeChannel(this.channel);
|
||||
this.state.set(State.DISPOSED);
|
||||
this.sink.error(ex);
|
||||
this.sink.error(exc);
|
||||
}
|
||||
|
||||
private enum State {
|
||||
@@ -1064,6 +1064,7 @@ public abstract class DataBufferUtils {
|
||||
public Context currentContext() {
|
||||
return Context.of(this.sink.contextView());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -1144,9 +1145,9 @@ public abstract class DataBufferUtils {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable ex, ByteBuffer byteBuffer) {
|
||||
public void failed(Throwable exc, ByteBuffer byteBuffer) {
|
||||
sinkDataBuffer();
|
||||
this.sink.error(ex);
|
||||
this.sink.error(exc);
|
||||
}
|
||||
|
||||
private void sinkDataBuffer() {
|
||||
@@ -1160,6 +1161,7 @@ public abstract class DataBufferUtils {
|
||||
public Context currentContext() {
|
||||
return Context.of(this.sink.contextView());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -496,7 +496,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
String rootDirPath = determineRootDir(locationPattern);
|
||||
String subPattern = locationPattern.substring(rootDirPath.length());
|
||||
Resource[] rootDirResources = getResources(rootDirPath);
|
||||
Set<Resource> result = new LinkedHashSet<>(64);
|
||||
Set<Resource> result = new LinkedHashSet<>(16);
|
||||
for (Resource rootDirResource : rootDirResources) {
|
||||
rootDirResource = resolveRootDirResource(rootDirResource);
|
||||
URL rootDirUrl = rootDirResource.getURL();
|
||||
@@ -648,7 +648,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
// The Sun JRE does not return a slash here, but BEA JRockit does.
|
||||
rootEntryPath = rootEntryPath + "/";
|
||||
}
|
||||
Set<Resource> result = new LinkedHashSet<>(64);
|
||||
Set<Resource> result = new LinkedHashSet<>(8);
|
||||
for (Enumeration<JarEntry> entries = jarFile.entries(); entries.hasMoreElements();) {
|
||||
JarEntry entry = entries.nextElement();
|
||||
String entryPath = entry.getName();
|
||||
@@ -864,7 +864,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
|
||||
private final String rootPath;
|
||||
|
||||
private final Set<Resource> resources = new LinkedHashSet<>(64);
|
||||
private final Set<Resource> resources = new LinkedHashSet<>();
|
||||
|
||||
public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) {
|
||||
this.subPattern = subPattern;
|
||||
@@ -895,6 +895,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
else if ("toString".equals(methodName)) {
|
||||
return toString();
|
||||
}
|
||||
|
||||
throw new IllegalStateException("Unexpected method invocation: " + method);
|
||||
}
|
||||
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -20,8 +20,6 @@ import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Default "no op" {@code ApplicationStartup} implementation.
|
||||
*
|
||||
@@ -54,7 +52,6 @@ class DefaultApplicationStartup implements ApplicationStartup {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Long getParentId() {
|
||||
return null;
|
||||
}
|
||||
@@ -76,6 +73,7 @@ class DefaultApplicationStartup implements ApplicationStartup {
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2012-2024 the original author or authors.
|
||||
* Copyright 2012-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,10 +21,10 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.core.metrics.StartupStep;
|
||||
import org.springframework.lang.NonNull;
|
||||
|
||||
/**
|
||||
* {@link StartupStep} implementation for the Java Flight Recorder.
|
||||
*
|
||||
* <p>This variant delegates to a {@link FlightRecorderStartupEvent JFR event extension}
|
||||
* to collect and record data in Java Flight Recorder.
|
||||
*
|
||||
@@ -114,12 +114,12 @@ class FlightRecorderStartupStep implements StartupStep {
|
||||
add(key, value.get());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Iterator<Tag> iterator() {
|
||||
return new TagsIterator();
|
||||
}
|
||||
|
||||
|
||||
private class TagsIterator implements Iterator<Tag> {
|
||||
|
||||
private int idx = 0;
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -73,20 +73,20 @@ public abstract class AbstractTypeHierarchyTraversingFilter implements TypeFilte
|
||||
// Optimization to avoid creating ClassReader for superclass.
|
||||
Boolean superClassMatch = matchSuperClass(superClassName);
|
||||
if (superClassMatch != null) {
|
||||
if (superClassMatch) {
|
||||
if (superClassMatch.booleanValue()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Need to read superclass to determine a match...
|
||||
try {
|
||||
if (match(superClassName, metadataReaderFactory)) {
|
||||
if (match(metadata.getSuperClassName(), metadataReaderFactory)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Could not read superclass [" + superClassName +
|
||||
logger.debug("Could not read superclass [" + metadata.getSuperClassName() +
|
||||
"] of type-filtered class [" + metadata.getClassName() + "]");
|
||||
}
|
||||
}
|
||||
@@ -99,7 +99,7 @@ public abstract class AbstractTypeHierarchyTraversingFilter implements TypeFilte
|
||||
// Optimization to avoid creating ClassReader for superclass
|
||||
Boolean interfaceMatch = matchInterface(ifc);
|
||||
if (interfaceMatch != null) {
|
||||
if (interfaceMatch) {
|
||||
if (interfaceMatch.booleanValue()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -44,8 +44,7 @@ import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Miscellaneous {@code java.lang.Class} utility methods.
|
||||
*
|
||||
* <p>Mainly for internal use within the framework.
|
||||
* Mainly for internal use within the framework.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Keith Donald
|
||||
@@ -244,7 +243,7 @@ public abstract class ClassUtils {
|
||||
* style (e.g. "java.lang.Thread.State" instead of "java.lang.Thread$State").
|
||||
* @param name the name of the Class
|
||||
* @param classLoader the class loader to use
|
||||
* (can be {@code null}, which indicates the default class loader)
|
||||
* (may be {@code null}, which indicates the default class loader)
|
||||
* @return a class instance for the supplied name
|
||||
* @throws ClassNotFoundException if the class was not found
|
||||
* @throws LinkageError if the class file could not be loaded
|
||||
@@ -315,7 +314,7 @@ public abstract class ClassUtils {
|
||||
* the exceptions thrown in case of class loading failure.
|
||||
* @param className the name of the Class
|
||||
* @param classLoader the class loader to use
|
||||
* (can be {@code null}, which indicates the default class loader)
|
||||
* (may be {@code null}, which indicates the default class loader)
|
||||
* @return a class instance for the supplied name
|
||||
* @throws IllegalArgumentException if the class name was not resolvable
|
||||
* (that is, the class could not be found or the class file could not be loaded)
|
||||
@@ -349,7 +348,7 @@ public abstract class ClassUtils {
|
||||
* one of its dependencies is not present or cannot be loaded.
|
||||
* @param className the name of the class to check
|
||||
* @param classLoader the class loader to use
|
||||
* (can be {@code null} which indicates the default class loader)
|
||||
* (may be {@code null} which indicates the default class loader)
|
||||
* @return whether the specified class is present (including all of its
|
||||
* superclasses and interfaces)
|
||||
* @throws IllegalStateException if the corresponding class is resolvable but
|
||||
@@ -376,7 +375,7 @@ public abstract class ClassUtils {
|
||||
* Check whether the given class is visible in the given ClassLoader.
|
||||
* @param clazz the class to check (typically an interface)
|
||||
* @param classLoader the ClassLoader to check against
|
||||
* (can be {@code null} in which case this method will always return {@code true})
|
||||
* (may be {@code null} in which case this method will always return {@code true})
|
||||
*/
|
||||
public static boolean isVisible(Class<?> clazz, @Nullable ClassLoader classLoader) {
|
||||
if (classLoader == null) {
|
||||
@@ -400,7 +399,7 @@ public abstract class ClassUtils {
|
||||
* i.e. whether it is loaded by the given ClassLoader or a parent of it.
|
||||
* @param clazz the class to analyze
|
||||
* @param classLoader the ClassLoader to potentially cache metadata in
|
||||
* (can be {@code null} which indicates the system class loader)
|
||||
* (may be {@code null} which indicates the system class loader)
|
||||
*/
|
||||
public static boolean isCacheSafe(Class<?> clazz, @Nullable ClassLoader classLoader) {
|
||||
Assert.notNull(clazz, "Class must not be null");
|
||||
@@ -540,10 +539,9 @@ public abstract class ClassUtils {
|
||||
* Check if the right-hand side type may be assigned to the left-hand side
|
||||
* type, assuming setting by reflection. Considers primitive wrapper
|
||||
* classes as assignable to the corresponding primitive types.
|
||||
* @param lhsType the target type (left-hand side (LHS) type)
|
||||
* @param rhsType the value type (right-hand side (RHS) type) that should
|
||||
* be assigned to the target type
|
||||
* @return {@code true} if {@code rhsType} is assignable to {@code lhsType}
|
||||
* @param lhsType the target type
|
||||
* @param rhsType the value type that should be assigned to the target type
|
||||
* @return if the target type is assignable from the value type
|
||||
* @see TypeUtils#isAssignable(java.lang.reflect.Type, java.lang.reflect.Type)
|
||||
*/
|
||||
public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
|
||||
@@ -664,7 +662,7 @@ public abstract class ClassUtils {
|
||||
* in the given collection.
|
||||
* <p>Basically like {@code AbstractCollection.toString()}, but stripping
|
||||
* the "class "/"interface " prefix before every class name.
|
||||
* @param classes a Collection of Class objects (can be {@code null})
|
||||
* @param classes a Collection of Class objects (may be {@code null})
|
||||
* @return a String of form "[com.foo.Bar, com.foo.Baz]"
|
||||
* @see java.util.AbstractCollection#toString()
|
||||
*/
|
||||
@@ -719,7 +717,7 @@ public abstract class ClassUtils {
|
||||
* <p>If the class itself is an interface, it gets returned as sole interface.
|
||||
* @param clazz the class to analyze for interfaces
|
||||
* @param classLoader the ClassLoader that the interfaces need to be visible in
|
||||
* (can be {@code null} when accepting all declared interfaces)
|
||||
* (may be {@code null} when accepting all declared interfaces)
|
||||
* @return all interfaces that the given object implements as an array
|
||||
*/
|
||||
public static Class<?>[] getAllInterfacesForClass(Class<?> clazz, @Nullable ClassLoader classLoader) {
|
||||
@@ -754,7 +752,7 @@ public abstract class ClassUtils {
|
||||
* <p>If the class itself is an interface, it gets returned as sole interface.
|
||||
* @param clazz the class to analyze for interfaces
|
||||
* @param classLoader the ClassLoader that the interfaces need to be visible in
|
||||
* (can be {@code null} when accepting all declared interfaces)
|
||||
* (may be {@code null} when accepting all declared interfaces)
|
||||
* @return all interfaces that the given object implements as a Set
|
||||
*/
|
||||
public static Set<Class<?>> getAllInterfacesForClassAsSet(Class<?> clazz, @Nullable ClassLoader classLoader) {
|
||||
@@ -868,9 +866,9 @@ public abstract class ClassUtils {
|
||||
/**
|
||||
* Check whether the given object is a CGLIB proxy.
|
||||
* @param object the object to check
|
||||
* @see #isCglibProxyClass(Class)
|
||||
* @see org.springframework.aop.support.AopUtils#isCglibProxy(Object)
|
||||
* @deprecated as of 5.2, in favor of custom (possibly narrower) checks
|
||||
* such as for a Spring AOP proxy
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isCglibProxy(Object object) {
|
||||
@@ -880,9 +878,8 @@ public abstract class ClassUtils {
|
||||
/**
|
||||
* Check whether the specified class is a CGLIB-generated class.
|
||||
* @param clazz the class to check
|
||||
* @see #getUserClass(Class)
|
||||
* @see #isCglibProxyClassName(String)
|
||||
* @deprecated as of 5.2, in favor of custom (possibly narrower) checks
|
||||
* or simply a check for containing {@link #CGLIB_CLASS_SEPARATOR}
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isCglibProxyClass(@Nullable Class<?> clazz) {
|
||||
@@ -892,9 +889,7 @@ public abstract class ClassUtils {
|
||||
/**
|
||||
* Check whether the specified class name is a CGLIB-generated class.
|
||||
* @param className the class name to check
|
||||
* @see #CGLIB_CLASS_SEPARATOR
|
||||
* @deprecated as of 5.2, in favor of custom (possibly narrower) checks
|
||||
* or simply a check for containing {@link #CGLIB_CLASS_SEPARATOR}
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isCglibProxyClassName(@Nullable String className) {
|
||||
@@ -918,7 +913,6 @@ public abstract class ClassUtils {
|
||||
* class, but the original class in case of a CGLIB-generated subclass.
|
||||
* @param clazz the class to check
|
||||
* @return the user-defined class
|
||||
* @see #CGLIB_CLASS_SEPARATOR
|
||||
*/
|
||||
public static Class<?> getUserClass(Class<?> clazz) {
|
||||
if (clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) {
|
||||
@@ -1071,7 +1065,7 @@ public abstract class ClassUtils {
|
||||
* fully qualified interface/class name + "." + method name.
|
||||
* @param method the method
|
||||
* @param clazz the clazz that the method is being invoked on
|
||||
* (can be {@code null} to indicate the method's declaring class)
|
||||
* (may be {@code null} to indicate the method's declaring class)
|
||||
* @return the qualified name of the method
|
||||
* @since 4.3.4
|
||||
*/
|
||||
@@ -1152,7 +1146,7 @@ public abstract class ClassUtils {
|
||||
* @param clazz the clazz to analyze
|
||||
* @param methodName the name of the method
|
||||
* @param paramTypes the parameter types of the method
|
||||
* (can be {@code null} to indicate any signature)
|
||||
* (may be {@code null} to indicate any signature)
|
||||
* @return the method (never {@code null})
|
||||
* @throws IllegalStateException if the method has not been found
|
||||
* @see Class#getMethod
|
||||
@@ -1191,7 +1185,7 @@ public abstract class ClassUtils {
|
||||
* @param clazz the clazz to analyze
|
||||
* @param methodName the name of the method
|
||||
* @param paramTypes the parameter types of the method
|
||||
* (can be {@code null} to indicate any signature)
|
||||
* (may be {@code null} to indicate any signature)
|
||||
* @return the method, or {@code null} if not found
|
||||
* @see Class#getMethod
|
||||
*/
|
||||
@@ -1267,27 +1261,26 @@ public abstract class ClassUtils {
|
||||
/**
|
||||
* Given a method, which may come from an interface, and a target class used
|
||||
* in the current reflective invocation, find the corresponding target method
|
||||
* if there is one — for example, the method may be {@code IFoo.bar()},
|
||||
* and the target class may be {@code DefaultFoo}. In this case, the method may be
|
||||
* if there is one. E.g. the method may be {@code IFoo.bar()} and the
|
||||
* target class may be {@code DefaultFoo}. In this case, the method may be
|
||||
* {@code DefaultFoo.bar()}. This enables attributes on that method to be found.
|
||||
* <p><b>NOTE:</b> In contrast to {@link org.springframework.aop.support.AopUtils#getMostSpecificMethod},
|
||||
* this method does <i>not</i> resolve bridge methods automatically.
|
||||
* Call {@link org.springframework.core.BridgeMethodResolver#findBridgedMethod}
|
||||
* if bridge method resolution is desirable — for example, to obtain
|
||||
* metadata from the original method definition.
|
||||
* <p><b>NOTE:</b> If Java security settings disallow reflective access —
|
||||
* for example, calls to {@code Class#getDeclaredMethods}, etc. — this
|
||||
* implementation will fall back to returning the originally provided method.
|
||||
* if bridge method resolution is desirable (e.g. for obtaining metadata from
|
||||
* the original method definition).
|
||||
* <p><b>NOTE:</b> Since Spring 3.1.1, if Java security settings disallow reflective
|
||||
* access (e.g. calls to {@code Class#getDeclaredMethods} etc, this implementation
|
||||
* will fall back to returning the originally provided method.
|
||||
* @param method the method to be invoked, which may come from an interface
|
||||
* @param targetClass the target class for the current invocation
|
||||
* (can be {@code null} or may not even implement the method)
|
||||
* (may be {@code null} or may not even implement the method)
|
||||
* @return the specific target method, or the original method if the
|
||||
* {@code targetClass} does not implement it
|
||||
* @see #getInterfaceMethodIfPossible(Method, Class)
|
||||
*/
|
||||
public static Method getMostSpecificMethod(Method method, @Nullable Class<?> targetClass) {
|
||||
if (targetClass != null && targetClass != method.getDeclaringClass() &&
|
||||
(isOverridable(method, targetClass) || !method.getDeclaringClass().isAssignableFrom(targetClass))) {
|
||||
if (targetClass != null && targetClass != method.getDeclaringClass() && isOverridable(method, targetClass)) {
|
||||
try {
|
||||
if (Modifier.isPublic(method.getModifiers())) {
|
||||
try {
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.core;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.MethodIntrospector.MetadataLookup;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.core.annotation.MergedAnnotations.SearchStrategy.TYPE_HIERARCHY;
|
||||
|
||||
/**
|
||||
* Tests for {@link MethodIntrospector}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 5.3.34
|
||||
*/
|
||||
class MethodIntrospectorTests {
|
||||
|
||||
@Test // gh-32586
|
||||
void selectMethodsAndClearDeclaredMethodsCacheBetweenInvocations() {
|
||||
Class<?> targetType = ActualController.class;
|
||||
|
||||
// Preconditions for this use case.
|
||||
assertThat(targetType).isPublic();
|
||||
assertThat(targetType.getSuperclass()).isPackagePrivate();
|
||||
|
||||
MetadataLookup<String> metadataLookup = (MetadataLookup<String>) method -> {
|
||||
if (MergedAnnotations.from(method, TYPE_HIERARCHY).isPresent(Mapped.class)) {
|
||||
return method.getName();
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// Start with a clean slate.
|
||||
ReflectionUtils.clearCache();
|
||||
|
||||
// Round #1
|
||||
Map<Method, String> methods = MethodIntrospector.selectMethods(targetType, metadataLookup);
|
||||
assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete");
|
||||
|
||||
// Simulate ConfigurableApplicationContext#refresh() which clears the
|
||||
// ReflectionUtils#declaredMethodsCache but NOT the BridgeMethodResolver#cache.
|
||||
// As a consequence, ReflectionUtils.getDeclaredMethods(...) will return a
|
||||
// new set of methods that are logically equivalent to but not identical
|
||||
// to (in terms of object identity) any bridged methods cached in the
|
||||
// BridgeMethodResolver cache.
|
||||
ReflectionUtils.clearCache();
|
||||
|
||||
// Round #2
|
||||
methods = MethodIntrospector.selectMethods(targetType, metadataLookup);
|
||||
assertThat(methods.values()).containsExactlyInAnyOrder("update", "delete");
|
||||
}
|
||||
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface Mapped {
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
|
||||
void unmappedMethod();
|
||||
|
||||
@Mapped
|
||||
void update();
|
||||
|
||||
@Mapped
|
||||
void delete();
|
||||
}
|
||||
|
||||
// Must NOT be public.
|
||||
abstract static class AbstractController implements Controller {
|
||||
|
||||
@Override
|
||||
public void unmappedMethod() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete() {
|
||||
}
|
||||
}
|
||||
|
||||
// MUST be public.
|
||||
public static class ActualController extends AbstractController {
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+8
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -48,14 +48,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*/
|
||||
class PathMatchingResourcePatternResolverTests {
|
||||
|
||||
private static final String[] CLASSES_IN_CORE_IO_SUPPORT = {"EncodedResource.class",
|
||||
private static final String[] CLASSES_IN_CORE_IO_SUPPORT = { "EncodedResource.class",
|
||||
"LocalizedResourceHelper.class", "PathMatchingResourcePatternResolver.class", "PropertiesLoaderSupport.class",
|
||||
"PropertiesLoaderUtils.class", "ResourceArrayPropertyEditor.class", "ResourcePatternResolver.class",
|
||||
"ResourcePatternUtils.class", "SpringFactoriesLoader.class"};
|
||||
"ResourcePatternUtils.class", "SpringFactoriesLoader.class" };
|
||||
|
||||
private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = {"PathMatchingResourcePatternResolverTests.class"};
|
||||
private static final String[] TEST_CLASSES_IN_CORE_IO_SUPPORT = { "PathMatchingResourcePatternResolverTests.class" };
|
||||
|
||||
private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = {"NonNull.class", "NonNullApi.class", "Nullable.class"};
|
||||
private static final String[] CLASSES_IN_REACTOR_UTIL_ANNOTATION = { "NonNull.class", "NonNullApi.class", "Nullable.class" };
|
||||
|
||||
|
||||
private final PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
|
||||
@@ -166,7 +166,7 @@ class PathMatchingResourcePatternResolverTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() {
|
||||
void usingFileProtocolWithoutWildcardInPatternAndEndingInSlashStarStar() {
|
||||
Path testResourcesDir = Paths.get("src/test/resources").toAbsolutePath();
|
||||
String pattern = String.format("file:%s/scanned-resources/**", testResourcesDir);
|
||||
String pathPrefix = ".+?resources/";
|
||||
@@ -294,8 +294,8 @@ class PathMatchingResourcePatternResolverTests {
|
||||
}
|
||||
|
||||
private String getPath(Resource resource) {
|
||||
// Tests fail if we use resource.getURL().getPath(). They would also fail on macOS when
|
||||
// using resource.getURI().getPath() if the resource paths are not Unicode normalized.
|
||||
// Tests fail if we use resouce.getURL().getPath(). They would also fail on Mac OS when
|
||||
// using resouce.getURI().getPath() if the resource paths are not Unicode normalized.
|
||||
//
|
||||
// On the JVM, all tests should pass when using resouce.getFile().getPath(); however,
|
||||
// we use FileSystemResource#getPath since this test class is sometimes run within a
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2018 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.
|
||||
@@ -20,7 +20,7 @@ import org.springframework.dao.DataRetrievalFailureException;
|
||||
|
||||
/**
|
||||
* Data access exception thrown when a result set did not have the correct column count,
|
||||
* for example when expecting a single column but getting 0 or more than 1 column.
|
||||
* for example when expecting a single column but getting 0 or more than 1 columns.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @since 2.0
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2018 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.
|
||||
@@ -20,7 +20,7 @@ import org.springframework.dao.IncorrectUpdateSemanticsDataAccessException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a JDBC update affects an unexpected number of rows.
|
||||
* Typically, we expect an update to affect a single row, meaning it is an
|
||||
* Typically we expect an update to affect a single row, meaning it's an
|
||||
* error if it affects multiple rows.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -22,7 +22,7 @@ package org.springframework.jdbc.core;
|
||||
*
|
||||
* <p>This interface allows you to signal the end of a batch rather than
|
||||
* having to determine the exact batch size upfront. Batch size is still
|
||||
* being honored, but it is now the maximum size of the batch.
|
||||
* being honored but it is now the maximum size of the batch.
|
||||
*
|
||||
* <p>The {@link #isBatchExhausted} method is called after each call to
|
||||
* {@link #setValues} to determine whether there were some values added,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -128,8 +128,8 @@ public abstract class RdbmsOperation implements InitializingBean {
|
||||
|
||||
/**
|
||||
* Set the maximum number of rows for this RDBMS operation. This is important
|
||||
* for processing subsets of large result sets, in order to avoid reading and
|
||||
* holding the entire result set in the database or in the JDBC driver.
|
||||
* for processing subsets of large result sets, avoiding to read and hold
|
||||
* the entire result set in the database or in the JDBC driver.
|
||||
* <p>Default is -1, indicating to use the driver's default.
|
||||
* @see org.springframework.jdbc.core.JdbcTemplate#setMaxRows
|
||||
*/
|
||||
@@ -175,7 +175,7 @@ public abstract class RdbmsOperation implements InitializingBean {
|
||||
public void setUpdatableResults(boolean updatableResults) {
|
||||
if (isCompiled()) {
|
||||
throw new InvalidDataAccessApiUsageException(
|
||||
"The updatableResults flag must be set before the operation is compiled");
|
||||
"The updateableResults flag must be set before the operation is compiled");
|
||||
}
|
||||
this.updatableResults = updatableResults;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -29,7 +29,7 @@ import org.springframework.jdbc.core.SqlParameter;
|
||||
|
||||
/**
|
||||
* Superclass for object abstractions of RDBMS stored procedures.
|
||||
* This class is abstract, and it is intended that subclasses will provide a typed
|
||||
* This class is abstract and it is intended that subclasses will provide a typed
|
||||
* method for invocation that delegates to the supplied {@link #execute} method.
|
||||
*
|
||||
* <p>The inherited {@link #setSql sql} property is the name of the stored procedure
|
||||
|
||||
+5
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
@@ -25,9 +25,9 @@ import org.apache.commons.logging.LogFactory;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Registry for custom {@link SQLExceptionTranslator} instances associated with
|
||||
* specific databases allowing for overriding translation based on values
|
||||
* contained in the configuration file named "sql-error-codes.xml".
|
||||
* Registry for custom {@link org.springframework.jdbc.support.SQLExceptionTranslator} instances associated with
|
||||
* specific databases allowing for overriding translation based on values contained in the configuration file
|
||||
* named "sql-error-codes.xml".
|
||||
*
|
||||
* @author Thomas Risberg
|
||||
* @since 3.1.1
|
||||
@@ -38,7 +38,7 @@ public final class CustomSQLExceptionTranslatorRegistry {
|
||||
private static final Log logger = LogFactory.getLog(CustomSQLExceptionTranslatorRegistry.class);
|
||||
|
||||
/**
|
||||
* Keep track of a single instance, so we can return it to classes that request it.
|
||||
* Keep track of a single instance so we can return it to classes that request it.
|
||||
*/
|
||||
private static final CustomSQLExceptionTranslatorRegistry instance = new CustomSQLExceptionTranslatorRegistry();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -218,7 +218,7 @@ public abstract class JdbcUtils {
|
||||
return NumberUtils.convertNumberToTargetClass((Number) obj, Integer.class);
|
||||
}
|
||||
else {
|
||||
// e.g. on Postgres: getObject returns a PGObject, but we need a String
|
||||
// e.g. on Postgres: getObject returns a PGObject but we need a String
|
||||
return rs.getString(index);
|
||||
}
|
||||
}
|
||||
@@ -228,14 +228,14 @@ public abstract class JdbcUtils {
|
||||
try {
|
||||
return rs.getObject(index, requiredType);
|
||||
}
|
||||
catch (SQLFeatureNotSupportedException | AbstractMethodError ex) {
|
||||
catch (AbstractMethodError err) {
|
||||
logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err);
|
||||
}
|
||||
catch (SQLFeatureNotSupportedException ex) {
|
||||
logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex);
|
||||
}
|
||||
catch (SQLException ex) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("JDBC driver has limited support for 'getObject(int, Class)' with column type: " +
|
||||
requiredType.getName(), ex);
|
||||
}
|
||||
logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex);
|
||||
}
|
||||
|
||||
// Corresponding SQL types for JSR-310 / Joda-Time types, left up
|
||||
@@ -416,14 +416,14 @@ public abstract class JdbcUtils {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the given JDBC driver supports JDBC batch updates.
|
||||
* Return whether the given JDBC driver supports JDBC 2.0 batch updates.
|
||||
* <p>Typically invoked right before execution of a given set of statements:
|
||||
* to decide whether the set of SQL statements should be executed through
|
||||
* the JDBC batch mechanism or simply in a traditional one-by-one fashion.
|
||||
* the JDBC 2.0 batch mechanism or simply in a traditional one-by-one fashion.
|
||||
* <p>Logs a warning if the "supportsBatchUpdates" methods throws an exception
|
||||
* and simply returns {@code false} in that case.
|
||||
* @param con the Connection to check
|
||||
* @return whether JDBC batch updates are supported
|
||||
* @return whether JDBC 2.0 batch updates are supported
|
||||
* @see java.sql.DatabaseMetaData#supportsBatchUpdates()
|
||||
*/
|
||||
public static boolean supportsBatchUpdates(Connection con) {
|
||||
@@ -496,8 +496,8 @@ public abstract class JdbcUtils {
|
||||
/**
|
||||
* Determine the column name to use. The column name is determined based on a
|
||||
* lookup using ResultSetMetaData.
|
||||
* <p>This method's implementation takes into account clarifications expressed
|
||||
* in the JDBC 4.0 specification:
|
||||
* <p>This method implementation takes into account recent clarifications
|
||||
* expressed in the JDBC 4.0 specification:
|
||||
* <p><i>columnLabel - the label for the column specified with the SQL AS clause.
|
||||
* If the SQL AS clause was not specified, then the label is the name of the column</i>.
|
||||
* @param resultSetMetaData the current meta-data to use
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -67,7 +67,7 @@ public class SQLErrorCodesFactory {
|
||||
private static final Log logger = LogFactory.getLog(SQLErrorCodesFactory.class);
|
||||
|
||||
/**
|
||||
* Keep track of a single instance, so we can return it to classes that request it.
|
||||
* Keep track of a single instance so we can return it to classes that request it.
|
||||
*/
|
||||
private static final SQLErrorCodesFactory instance = new SQLErrorCodesFactory();
|
||||
|
||||
@@ -101,7 +101,6 @@ public class SQLErrorCodesFactory {
|
||||
* @see #loadResource(String)
|
||||
*/
|
||||
protected SQLErrorCodesFactory() {
|
||||
|
||||
Map<String, SQLErrorCodes> errorCodes;
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -217,7 +217,7 @@ public abstract class JmsUtils {
|
||||
try {
|
||||
session.commit();
|
||||
}
|
||||
catch (javax.jms.TransactionInProgressException ex) {
|
||||
catch (javax.jms.TransactionInProgressException | javax.jms.IllegalStateException ex) {
|
||||
// Ignore -> can only happen in case of a JTA transaction.
|
||||
}
|
||||
}
|
||||
@@ -232,7 +232,7 @@ public abstract class JmsUtils {
|
||||
try {
|
||||
session.rollback();
|
||||
}
|
||||
catch (javax.jms.TransactionInProgressException ex) {
|
||||
catch (javax.jms.TransactionInProgressException | javax.jms.IllegalStateException ex) {
|
||||
// Ignore -> can only happen in case of a JTA transaction.
|
||||
}
|
||||
}
|
||||
|
||||
+4
-7
@@ -253,9 +253,8 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
|
||||
* @see javax.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource()
|
||||
* @see #setPersistenceUnitManager
|
||||
*/
|
||||
public void setDataSource(@Nullable DataSource dataSource) {
|
||||
this.internalPersistenceUnitManager.setDataSourceLookup(
|
||||
dataSource != null ? new SingleDataSourceLookup(dataSource) : null);
|
||||
public void setDataSource(DataSource dataSource) {
|
||||
this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(dataSource));
|
||||
this.internalPersistenceUnitManager.setDefaultDataSource(dataSource);
|
||||
}
|
||||
|
||||
@@ -271,9 +270,8 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
|
||||
* @see javax.persistence.spi.PersistenceUnitInfo#getJtaDataSource()
|
||||
* @see #setPersistenceUnitManager
|
||||
*/
|
||||
public void setJtaDataSource(@Nullable DataSource jtaDataSource) {
|
||||
this.internalPersistenceUnitManager.setDataSourceLookup(
|
||||
jtaDataSource != null ? new SingleDataSourceLookup(jtaDataSource) : null);
|
||||
public void setJtaDataSource(DataSource jtaDataSource) {
|
||||
this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(jtaDataSource));
|
||||
this.internalPersistenceUnitManager.setDefaultJtaDataSource(jtaDataSource);
|
||||
}
|
||||
|
||||
@@ -418,7 +416,6 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public DataSource getDataSource() {
|
||||
if (this.persistenceUnitInfo != null) {
|
||||
return (this.persistenceUnitInfo.getJtaDataSource() != null ?
|
||||
|
||||
+18
-8
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,18 +28,28 @@ import javax.persistence.spi.PersistenceProvider;
|
||||
* shared JPA EntityManagerFactory in a Spring application context; the
|
||||
* EntityManagerFactory can then be passed to JPA-based DAOs via
|
||||
* dependency injection. Note that switching to a JNDI lookup or to a
|
||||
* {@link LocalContainerEntityManagerFactoryBean} definition based on the
|
||||
* JPA container contract is just a matter of configuration!
|
||||
* {@link LocalContainerEntityManagerFactoryBean}
|
||||
* definition is just a matter of configuration!
|
||||
*
|
||||
* <p>Configuration settings are usually read from a {@code META-INF/persistence.xml}
|
||||
* config file, residing in the class path, according to the JPA standalone bootstrap
|
||||
* contract. See the Java Persistence API specification and your persistence provider
|
||||
* documentation for setup details. Additionally, JPA properties can also be added
|
||||
* on this FactoryBean via {@link #setJpaProperties}/{@link #setJpaPropertyMap}.
|
||||
* contract. Additionally, most JPA providers will require a special VM agent
|
||||
* (specified on JVM startup) that allows them to instrument application classes.
|
||||
* See the Java Persistence API specification and your provider documentation
|
||||
* for setup details.
|
||||
*
|
||||
* <p>This EntityManagerFactory bootstrap is appropriate for standalone applications
|
||||
* which solely use JPA for data access. If you want to set up your persistence
|
||||
* provider for an external DataSource and/or for global transactions which span
|
||||
* multiple resources, you will need to either deploy it into a full Java EE
|
||||
* application server and access the deployed EntityManagerFactory via JNDI,
|
||||
* or use Spring's {@link LocalContainerEntityManagerFactoryBean} with appropriate
|
||||
* configuration for local setup according to JPA's container contract.
|
||||
*
|
||||
* <p><b>Note:</b> This FactoryBean has limited configuration power in terms of
|
||||
* the configuration that it is able to pass to the JPA provider. If you need
|
||||
* more flexible configuration options, consider using Spring's more powerful
|
||||
* what configuration it is able to pass to the JPA provider. If you need more
|
||||
* flexible configuration, for example passing a Spring-managed JDBC DataSource
|
||||
* to the JPA provider, consider using Spring's more powerful
|
||||
* {@link LocalContainerEntityManagerFactoryBean} instead.
|
||||
*
|
||||
* <p><b>NOTE: Spring's JPA support requires JPA 2.1 or higher, as of Spring 5.0.</b>
|
||||
|
||||
+3
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -70,15 +70,13 @@ public class SingleConnectionFactory extends DelegatingConnectionFactory
|
||||
private boolean suppressClose;
|
||||
|
||||
/** Override auto-commit state?. */
|
||||
@Nullable
|
||||
private Boolean autoCommit;
|
||||
private @Nullable Boolean autoCommit;
|
||||
|
||||
/** Wrapped Connection. */
|
||||
private final AtomicReference<Connection> target = new AtomicReference<>();
|
||||
|
||||
/** Proxy Connection. */
|
||||
@Nullable
|
||||
private Connection connection;
|
||||
private @Nullable Connection connection;
|
||||
|
||||
private final Mono<? extends Connection> connectionEmitter;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -60,6 +60,7 @@ import javax.servlet.http.Part;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
@@ -989,7 +990,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
}
|
||||
}
|
||||
|
||||
private static String encodeCookies(Cookie... cookies) {
|
||||
private static String encodeCookies(@NonNull Cookie... cookies) {
|
||||
return Arrays.stream(cookies)
|
||||
.map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue()))
|
||||
.collect(Collectors.joining("; "));
|
||||
|
||||
+3
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -31,6 +31,7 @@ import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.TestContext;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
@@ -279,6 +280,7 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ResourceDatabasePopulator createDatabasePopulator(MergedSqlConfig mergedSqlConfig) {
|
||||
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
|
||||
populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding());
|
||||
|
||||
+3
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2012 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.
|
||||
@@ -44,11 +44,10 @@ public class IncorrectUpdateSemanticsDataAccessException extends InvalidDataAcce
|
||||
super(msg, cause);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return whether data was updated.
|
||||
* If this method returns {@code false}, there is nothing to roll back.
|
||||
* <p>The default implementation always returns {@code true}.
|
||||
* If this method returns false, there's nothing to roll back.
|
||||
* <p>The default implementation always returns true.
|
||||
* This can be overridden in subclasses.
|
||||
*/
|
||||
public boolean wasDataUpdated() {
|
||||
|
||||
+6
-11
@@ -21,6 +21,7 @@ import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.eclipse.jetty.http.HttpField;
|
||||
import org.eclipse.jetty.reactive.client.ReactiveResponse;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@@ -54,29 +55,23 @@ class JettyClientHttpResponse extends AbstractClientHttpResponse {
|
||||
}
|
||||
|
||||
private static HttpHeaders adaptHeaders(ReactiveResponse response) {
|
||||
MultiValueMap<String, String> headers = (Jetty10HttpFieldsHelper.jetty10Present() ?
|
||||
Jetty10HttpFieldsHelper.getHttpHeaders(response.getResponse()) :
|
||||
new JettyHeadersAdapter(response.getHeaders()));
|
||||
MultiValueMap<String, String> headers = new JettyHeadersAdapter(response.getHeaders());
|
||||
return HttpHeaders.readOnlyHttpHeaders(headers);
|
||||
}
|
||||
|
||||
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response) {
|
||||
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
|
||||
MultiValueMap<String, String> headers = adaptHeaders(response);
|
||||
List<String> cookieHeader = headers.get(HttpHeaders.SET_COOKIE);
|
||||
if (!CollectionUtils.isEmpty(cookieHeader)) {
|
||||
cookieHeader.forEach(header ->
|
||||
HttpCookie.parse(header).forEach(cookie -> result.add(cookie.getName(),
|
||||
List<HttpField> cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE);
|
||||
cookieHeaders.forEach(header ->
|
||||
HttpCookie.parse(header.getValue()).forEach(cookie -> result.add(cookie.getName(),
|
||||
ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
|
||||
.domain(cookie.getDomain())
|
||||
.path(cookie.getPath())
|
||||
.maxAge(cookie.getMaxAge())
|
||||
.secure(cookie.getSecure())
|
||||
.httpOnly(cookie.isHttpOnly())
|
||||
.sameSite(parseSameSite(header))
|
||||
.sameSite(parseSameSite(header.getValue()))
|
||||
.build()))
|
||||
);
|
||||
}
|
||||
return CollectionUtils.unmodifiableMultiValueMap(result);
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -112,7 +112,6 @@ public class ResourceHttpMessageConverter extends AbstractHttpMessageConverter<R
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Long getContentLength(Resource resource, @Nullable MediaType contentType) throws IOException {
|
||||
// Don't try to determine contentLength on InputStreamResource - cannot be read afterwards...
|
||||
// Note: custom InputStreamResource subclasses could provide a pre-calculated content length!
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -519,7 +519,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
protected Long getContentLength(Object object, @Nullable MediaType contentType) throws IOException {
|
||||
if (object instanceof MappingJacksonValue) {
|
||||
object = ((MappingJacksonValue) object).getValue();
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.context.request.async;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Raised when the response for an asynchronous request becomes unusable as
|
||||
* indicated by a write failure, or a Servlet container error notification, or
|
||||
* after the async request has completed.
|
||||
*
|
||||
* <p>The exception relies on response wrapping, and on {@code AsyncListener}
|
||||
* notifications, managed by {@link StandardServletAsyncWebRequest}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3.33
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
public class AsyncRequestNotUsableException extends IOException {
|
||||
|
||||
|
||||
public AsyncRequestNotUsableException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public AsyncRequestNotUsableException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
}
|
||||
+18
-558
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -17,28 +17,22 @@
|
||||
package org.springframework.web.context.request.async;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import javax.servlet.AsyncContext;
|
||||
import javax.servlet.AsyncEvent;
|
||||
import javax.servlet.AsyncListener;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.servlet.http.HttpServletResponseWrapper;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.context.request.ServletWebRequest;
|
||||
|
||||
/**
|
||||
* A Servlet implementation of {@link AsyncWebRequest}.
|
||||
* A Servlet 3.0 implementation of {@link AsyncWebRequest}.
|
||||
*
|
||||
* <p>The servlet and all filters involved in an async request must have async
|
||||
* support enabled using the Servlet API or by adding an
|
||||
@@ -50,22 +44,18 @@ import org.springframework.web.context.request.ServletWebRequest;
|
||||
*/
|
||||
public class StandardServletAsyncWebRequest extends ServletWebRequest implements AsyncWebRequest, AsyncListener {
|
||||
|
||||
private Long timeout;
|
||||
|
||||
private AsyncContext asyncContext;
|
||||
|
||||
private AtomicBoolean asyncCompleted = new AtomicBoolean();
|
||||
|
||||
private final List<Runnable> timeoutHandlers = new ArrayList<>();
|
||||
|
||||
private final List<Consumer<Throwable>> exceptionHandlers = new ArrayList<>();
|
||||
|
||||
private final List<Runnable> completionHandlers = new ArrayList<>();
|
||||
|
||||
@Nullable
|
||||
private Long timeout;
|
||||
|
||||
@Nullable
|
||||
private AsyncContext asyncContext;
|
||||
|
||||
private State state;
|
||||
|
||||
private final ReentrantLock stateLock = new ReentrantLock();
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance for the given request/response pair.
|
||||
@@ -73,26 +63,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
* @param response current HTTP response
|
||||
*/
|
||||
public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
|
||||
this(request, response, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor to wrap the request and response for the current dispatch that
|
||||
* also picks up the state of the last (probably the REQUEST) dispatch.
|
||||
* @param request current HTTP request
|
||||
* @param response current HTTP response
|
||||
* @param previousRequest the existing request from the last dispatch
|
||||
* @since 5.3.33
|
||||
*/
|
||||
StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response,
|
||||
@Nullable StandardServletAsyncWebRequest previousRequest) {
|
||||
|
||||
super(request, new LifecycleHttpServletResponse(response));
|
||||
|
||||
this.state = (previousRequest != null ? previousRequest.state : State.NEW);
|
||||
|
||||
//noinspection DataFlowIssue
|
||||
((LifecycleHttpServletResponse) getResponse()).setAsyncWebRequest(this);
|
||||
super(request, response);
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +104,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
*/
|
||||
@Override
|
||||
public boolean isAsyncComplete() {
|
||||
return (this.state == State.COMPLETED);
|
||||
return this.asyncCompleted.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -143,18 +114,11 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
"in async request processing. This is done in Java code using the Servlet API " +
|
||||
"or by adding \"<async-supported>true</async-supported>\" to servlet and " +
|
||||
"filter declarations in web.xml.");
|
||||
Assert.state(!isAsyncComplete(), "Async processing has already completed");
|
||||
|
||||
if (isAsyncStarted()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.state == State.NEW) {
|
||||
this.state = State.ASYNC;
|
||||
}
|
||||
else {
|
||||
Assert.state(this.state == State.ASYNC, "Cannot start async: [" + this.state + "]");
|
||||
}
|
||||
|
||||
this.asyncContext = getRequest().startAsync(getRequest(), getResponse());
|
||||
this.asyncContext.addListener(this);
|
||||
if (this.timeout != null) {
|
||||
@@ -164,10 +128,8 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void dispatch() {
|
||||
Assert.state(this.asyncContext != null, "AsyncContext not yet initialized");
|
||||
if (!this.isAsyncComplete()) {
|
||||
this.asyncContext.dispatch();
|
||||
}
|
||||
Assert.state(this.asyncContext != null, "Cannot dispatch without an AsyncContext");
|
||||
this.asyncContext.dispatch();
|
||||
}
|
||||
|
||||
|
||||
@@ -186,516 +148,14 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
|
||||
|
||||
@Override
|
||||
public void onError(AsyncEvent event) throws IOException {
|
||||
this.stateLock.lock();
|
||||
try {
|
||||
this.state = State.ERROR;
|
||||
Throwable ex = event.getThrowable();
|
||||
this.exceptionHandlers.forEach(consumer -> consumer.accept(ex));
|
||||
}
|
||||
finally {
|
||||
this.stateLock.unlock();
|
||||
}
|
||||
this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(AsyncEvent event) throws IOException {
|
||||
this.stateLock.lock();
|
||||
try {
|
||||
this.completionHandlers.forEach(Runnable::run);
|
||||
this.asyncContext = null;
|
||||
this.state = State.COMPLETED;
|
||||
}
|
||||
finally {
|
||||
this.stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Package private access for testing only.
|
||||
*/
|
||||
ReentrantLock stateLock() {
|
||||
return this.stateLock;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Response wrapper to wrap the output stream with {@link LifecycleServletOutputStream}.
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private static final class LifecycleHttpServletResponse extends HttpServletResponseWrapper {
|
||||
|
||||
@Nullable
|
||||
private StandardServletAsyncWebRequest asyncWebRequest;
|
||||
|
||||
@Nullable
|
||||
private ServletOutputStream outputStream;
|
||||
|
||||
@Nullable
|
||||
private PrintWriter writer;
|
||||
|
||||
public LifecycleHttpServletResponse(HttpServletResponse response) {
|
||||
super(response);
|
||||
}
|
||||
|
||||
public void setAsyncWebRequest(StandardServletAsyncWebRequest asyncWebRequest) {
|
||||
this.asyncWebRequest = asyncWebRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() throws IOException {
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
if (this.outputStream == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
ServletOutputStream delegate = getResponse().getOutputStream();
|
||||
this.outputStream = new LifecycleServletOutputStream(delegate, this);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleIOException(ex, "Failed to get ServletResponseOutput");
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
return this.outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter getWriter() throws IOException {
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
if (this.writer == null) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
this.writer = new LifecyclePrintWriter(getResponse().getWriter(), this.asyncWebRequest);
|
||||
}
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleIOException(ex, "Failed to get PrintWriter");
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
return this.writer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
int level = obtainLockAndCheckState();
|
||||
try {
|
||||
getResponse().flushBuffer();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
handleIOException(ex, "ServletResponse failed to flushBuffer");
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 0 if checks passed and lock is not needed, 1 if checks passed
|
||||
* and lock is held, or raise AsyncRequestNotUsableException.
|
||||
*/
|
||||
private int obtainLockAndCheckState() throws AsyncRequestNotUsableException {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
if (this.asyncWebRequest.state == State.NEW) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
this.asyncWebRequest.stateLock.lock();
|
||||
if (this.asyncWebRequest.state == State.ASYNC) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
throw new AsyncRequestNotUsableException("Response not usable after " +
|
||||
(this.asyncWebRequest.state == State.COMPLETED ?
|
||||
"async request completion" : "response errors") + ".");
|
||||
}
|
||||
|
||||
void handleIOException(IOException ex, String msg) throws AsyncRequestNotUsableException {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
this.asyncWebRequest.state = State.ERROR;
|
||||
throw new AsyncRequestNotUsableException(msg + ": " + ex.getMessage(), ex);
|
||||
}
|
||||
|
||||
void releaseLock(int level) {
|
||||
Assert.notNull(this.asyncWebRequest, "Not initialized");
|
||||
if (level > 0) {
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wraps a ServletOutputStream to prevent use after Servlet container onError
|
||||
* notifications, and after async request completion.
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private static final class LifecycleServletOutputStream extends ServletOutputStream {
|
||||
|
||||
private final ServletOutputStream delegate;
|
||||
|
||||
private final LifecycleHttpServletResponse response;
|
||||
|
||||
private LifecycleServletOutputStream(ServletOutputStream delegate, LifecycleHttpServletResponse response) {
|
||||
this.delegate = delegate;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return this.delegate.isReady();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
this.delegate.setWriteListener(writeListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.write(b);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to write");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
public void write(byte[] buf, int offset, int len) throws IOException {
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.write(buf, offset, len);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to write");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.flush();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to flush");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
int level = this.response.obtainLockAndCheckState();
|
||||
try {
|
||||
this.delegate.close();
|
||||
}
|
||||
catch (IOException ex) {
|
||||
this.response.handleIOException(ex, "ServletOutputStream failed to close");
|
||||
}
|
||||
finally {
|
||||
this.response.releaseLock(level);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Wraps a PrintWriter to prevent use after Servlet container onError
|
||||
* notifications, and after async request completion.
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private static final class LifecyclePrintWriter extends PrintWriter {
|
||||
|
||||
private final PrintWriter delegate;
|
||||
|
||||
private final StandardServletAsyncWebRequest asyncWebRequest;
|
||||
|
||||
private LifecyclePrintWriter(PrintWriter delegate, StandardServletAsyncWebRequest asyncWebRequest) {
|
||||
super(delegate);
|
||||
this.delegate = delegate;
|
||||
this.asyncWebRequest = asyncWebRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.flush();
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.close();
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean checkError() {
|
||||
return this.delegate.checkError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int c) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(c);
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] buf, int off, int len) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(buf, off, len);
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(char[] buf) {
|
||||
this.delegate.write(buf);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String s, int off, int len) {
|
||||
int level = tryObtainLockAndCheckState();
|
||||
if (level > -1) {
|
||||
try {
|
||||
this.delegate.write(s, off, len);
|
||||
}
|
||||
finally {
|
||||
releaseLock(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(String s) {
|
||||
this.delegate.write(s);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return 0 if checks passed and lock is not needed, 1 if checks passed
|
||||
* and lock is held, and -1 if checks did not pass.
|
||||
*/
|
||||
private int tryObtainLockAndCheckState() {
|
||||
if (this.asyncWebRequest.state == State.NEW) {
|
||||
return 0;
|
||||
}
|
||||
this.asyncWebRequest.stateLock.lock();
|
||||
if (this.asyncWebRequest.state == State.ASYNC) {
|
||||
return 1;
|
||||
}
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
return -1;
|
||||
}
|
||||
|
||||
private void releaseLock(int level) {
|
||||
if (level > 0) {
|
||||
this.asyncWebRequest.stateLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
// Plain delegates
|
||||
|
||||
@Override
|
||||
public void print(boolean b) {
|
||||
this.delegate.print(b);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(char c) {
|
||||
this.delegate.print(c);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(int i) {
|
||||
this.delegate.print(i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(long l) {
|
||||
this.delegate.print(l);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(float f) {
|
||||
this.delegate.print(f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(double d) {
|
||||
this.delegate.print(d);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(char[] s) {
|
||||
this.delegate.print(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(String s) {
|
||||
this.delegate.print(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void print(Object obj) {
|
||||
this.delegate.print(obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println() {
|
||||
this.delegate.println();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(boolean x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(char x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(int x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(long x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(float x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(double x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(char[] x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(String x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void println(Object x) {
|
||||
this.delegate.println(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter printf(String format, Object... args) {
|
||||
return this.delegate.printf(format, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter printf(Locale l, String format, Object... args) {
|
||||
return this.delegate.printf(l, format, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter format(String format, Object... args) {
|
||||
return this.delegate.format(format, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter format(Locale l, String format, Object... args) {
|
||||
return this.delegate.format(l, format, args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter append(CharSequence csq) {
|
||||
return this.delegate.append(csq);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter append(CharSequence csq, int start, int end) {
|
||||
return this.delegate.append(csq, start, end);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrintWriter append(char c) {
|
||||
return this.delegate.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a state for {@link StandardServletAsyncWebRequest} to be in.
|
||||
* <p><pre>
|
||||
* +------ NEW
|
||||
* | |
|
||||
* | v
|
||||
* | ASYNC ----> +
|
||||
* | | |
|
||||
* | v |
|
||||
* +----> ERROR |
|
||||
* | |
|
||||
* v |
|
||||
* COMPLETED <---+
|
||||
* </pre>
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private enum State {
|
||||
|
||||
/** New request (may not start async handling). */
|
||||
NEW,
|
||||
|
||||
/** Async handling has started. */
|
||||
ASYNC,
|
||||
|
||||
/** ServletOutputStream failed, or onError notification received. */
|
||||
ERROR,
|
||||
|
||||
/** onComplete notification received. */
|
||||
COMPLETED
|
||||
|
||||
this.completionHandlers.forEach(Runnable::run);
|
||||
this.asyncContext = null;
|
||||
this.asyncCompleted.set(true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+65
-121
@@ -22,7 +22,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -89,7 +89,12 @@ public final class WebAsyncManager {
|
||||
@Nullable
|
||||
private volatile Object[] concurrentResultContext;
|
||||
|
||||
private final AtomicReference<State> state = new AtomicReference<>(State.NOT_STARTED);
|
||||
/*
|
||||
* Whether the concurrentResult is an error. If such errors remain unhandled, some
|
||||
* Servlet containers will call AsyncListener#onError at the end, after the ASYNC
|
||||
* and/or the ERROR dispatch (Boot's case), and we need to ignore those.
|
||||
*/
|
||||
private volatile boolean errorHandlingInProgress;
|
||||
|
||||
private final Map<Object, CallableProcessingInterceptor> callableInterceptors = new LinkedHashMap<>();
|
||||
|
||||
@@ -121,15 +126,6 @@ public final class WebAsyncManager {
|
||||
WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST));
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current {@link AsyncWebRequest}.
|
||||
* @since 5.3.33
|
||||
*/
|
||||
@Nullable
|
||||
public AsyncWebRequest getAsyncWebRequest() {
|
||||
return this.asyncWebRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure an AsyncTaskExecutor for use with concurrent processing via
|
||||
* {@link #startCallableProcessing(Callable, Object...)}.
|
||||
@@ -140,8 +136,8 @@ public final class WebAsyncManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the selected handler for the current request chose to handle
|
||||
* the request asynchronously. A return value of "true" indicates concurrent
|
||||
* Whether the selected handler for the current request chose to handle the
|
||||
* request asynchronously. A return value of "true" indicates concurrent
|
||||
* handling is under way and the response will remain open. A return value
|
||||
* of "false" means concurrent handling was either not started or possibly
|
||||
* that it has completed and the request was dispatched for further
|
||||
@@ -152,16 +148,16 @@ public final class WebAsyncManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether a result value exists as a result of concurrent handling.
|
||||
* Whether a result value exists as a result of concurrent handling.
|
||||
*/
|
||||
public boolean hasConcurrentResult() {
|
||||
return (this.concurrentResult != RESULT_NONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the result from concurrent handling.
|
||||
* Provides access to the result from concurrent handling.
|
||||
* @return an Object, possibly an {@code Exception} or {@code Throwable} if
|
||||
* concurrent handling raised one
|
||||
* concurrent handling raised one.
|
||||
* @see #clearConcurrentResult()
|
||||
*/
|
||||
@Nullable
|
||||
@@ -170,7 +166,8 @@ public final class WebAsyncManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the additional processing context saved at the start of concurrent handling.
|
||||
* Provides access to additional processing context saved at the start of
|
||||
* concurrent handling.
|
||||
* @see #clearConcurrentResult()
|
||||
*/
|
||||
@Nullable
|
||||
@@ -211,7 +208,7 @@ public final class WebAsyncManager {
|
||||
|
||||
/**
|
||||
* Register a {@link CallableProcessingInterceptor} without a key.
|
||||
* The key is derived from the class name and hash code.
|
||||
* The key is derived from the class name and hashcode.
|
||||
* @param interceptors one or more interceptors to register
|
||||
*/
|
||||
public void registerCallableInterceptors(CallableProcessingInterceptor... interceptors) {
|
||||
@@ -234,8 +231,8 @@ public final class WebAsyncManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors}
|
||||
* without a specified key. The default key is derived from the interceptor class name and hash code.
|
||||
* Register one or more {@link DeferredResultProcessingInterceptor DeferredResultProcessingInterceptors} without a specified key.
|
||||
* The default key is derived from the interceptor class name and hash code.
|
||||
* @param interceptors one or more interceptors to register
|
||||
*/
|
||||
public void registerDeferredResultInterceptors(DeferredResultProcessingInterceptor... interceptors) {
|
||||
@@ -251,12 +248,6 @@ public final class WebAsyncManager {
|
||||
* {@linkplain #getConcurrentResultContext() concurrentResultContext}.
|
||||
*/
|
||||
public void clearConcurrentResult() {
|
||||
if (!this.state.compareAndSet(State.RESULT_SET, State.NOT_STARTED)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Unexpected call to clear: [" + this.state.get() + "]");
|
||||
}
|
||||
return;
|
||||
}
|
||||
synchronized (WebAsyncManager.this) {
|
||||
this.concurrentResult = RESULT_NONE;
|
||||
this.concurrentResultContext = null;
|
||||
@@ -297,11 +288,6 @@ public final class WebAsyncManager {
|
||||
Assert.notNull(webAsyncTask, "WebAsyncTask must not be null");
|
||||
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
|
||||
|
||||
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
|
||||
throw new IllegalStateException(
|
||||
"Unexpected call to startCallableProcessing: [" + this.state.get() + "]");
|
||||
}
|
||||
|
||||
Long timeout = webAsyncTask.getTimeout();
|
||||
if (timeout != null) {
|
||||
this.asyncWebRequest.setTimeout(timeout);
|
||||
@@ -312,7 +298,7 @@ public final class WebAsyncManager {
|
||||
this.taskExecutor = executor;
|
||||
}
|
||||
else {
|
||||
logExecutorWarning(this.asyncWebRequest);
|
||||
logExecutorWarning();
|
||||
}
|
||||
|
||||
List<CallableProcessingInterceptor> interceptors = new ArrayList<>();
|
||||
@@ -325,7 +311,7 @@ public final class WebAsyncManager {
|
||||
|
||||
this.asyncWebRequest.addTimeoutHandler(() -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
|
||||
logger.debug("Async request timeout for " + formatRequestUri());
|
||||
}
|
||||
Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable);
|
||||
if (result != CallableProcessingInterceptor.RESULT_NONE) {
|
||||
@@ -334,12 +320,14 @@ public final class WebAsyncManager {
|
||||
});
|
||||
|
||||
this.asyncWebRequest.addErrorHandler(ex -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex);
|
||||
if (!this.errorHandlingInProgress) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Async request error for " + formatRequestUri() + ": " + ex);
|
||||
}
|
||||
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
|
||||
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
|
||||
setConcurrentResultAndDispatch(result);
|
||||
}
|
||||
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
|
||||
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
|
||||
setConcurrentResultAndDispatch(result);
|
||||
});
|
||||
|
||||
this.asyncWebRequest.addCompletionHandler(() ->
|
||||
@@ -364,13 +352,14 @@ public final class WebAsyncManager {
|
||||
});
|
||||
interceptorChain.setTaskFuture(future);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
catch (RejectedExecutionException ex) {
|
||||
Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);
|
||||
setConcurrentResultAndDispatch(result);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
private void logExecutorWarning(AsyncWebRequest asyncWebRequest) {
|
||||
private void logExecutorWarning() {
|
||||
if (taskExecutorWarning && logger.isWarnEnabled()) {
|
||||
synchronized (DEFAULT_TASK_EXECUTOR) {
|
||||
AsyncTaskExecutor executor = this.taskExecutor;
|
||||
@@ -382,7 +371,7 @@ public final class WebAsyncManager {
|
||||
"Please, configure a TaskExecutor in the MVC config under \"async support\".\n" +
|
||||
"The " + executorTypeName + " currently in use is not suitable under load.\n" +
|
||||
"-------------------------------\n" +
|
||||
"Request URI: '" + formatUri(asyncWebRequest) + "'\n" +
|
||||
"Request URI: '" + formatRequestUri() + "'\n" +
|
||||
"!!!");
|
||||
taskExecutorWarning = false;
|
||||
}
|
||||
@@ -390,35 +379,32 @@ public final class WebAsyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
private String formatRequestUri() {
|
||||
HttpServletRequest request = this.asyncWebRequest.getNativeRequest(HttpServletRequest.class);
|
||||
return request != null ? request.getRequestURI() : "servlet container";
|
||||
}
|
||||
|
||||
private void setConcurrentResultAndDispatch(@Nullable Object result) {
|
||||
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
|
||||
synchronized (WebAsyncManager.this) {
|
||||
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Async result already set: " +
|
||||
"[" + this.state.get() + "], ignored result: " + result +
|
||||
" for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
if (this.concurrentResult != RESULT_NONE) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.concurrentResult = result;
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
|
||||
if (this.asyncWebRequest.isAsyncComplete()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
this.asyncWebRequest.dispatch();
|
||||
this.errorHandlingInProgress = (result instanceof Throwable);
|
||||
}
|
||||
|
||||
if (this.asyncWebRequest.isAsyncComplete()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Async result set but request already complete: " + formatRequestUri());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
boolean isError = result instanceof Throwable;
|
||||
logger.debug("Async " + (isError ? "error" : "result set") + ", dispatch to " + formatRequestUri());
|
||||
}
|
||||
this.asyncWebRequest.dispatch();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -441,11 +427,6 @@ public final class WebAsyncManager {
|
||||
Assert.notNull(deferredResult, "DeferredResult must not be null");
|
||||
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
|
||||
|
||||
if (!this.state.compareAndSet(State.NOT_STARTED, State.ASYNC_PROCESSING)) {
|
||||
throw new IllegalStateException(
|
||||
"Unexpected call to startDeferredResultProcessing: [" + this.state.get() + "]");
|
||||
}
|
||||
|
||||
Long timeout = deferredResult.getTimeoutValue();
|
||||
if (timeout != null) {
|
||||
this.asyncWebRequest.setTimeout(timeout);
|
||||
@@ -459,9 +440,6 @@ public final class WebAsyncManager {
|
||||
final DeferredResultInterceptorChain interceptorChain = new DeferredResultInterceptorChain(interceptors);
|
||||
|
||||
this.asyncWebRequest.addTimeoutHandler(() -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
try {
|
||||
interceptorChain.triggerAfterTimeout(this.asyncWebRequest, deferredResult);
|
||||
}
|
||||
@@ -471,22 +449,21 @@ public final class WebAsyncManager {
|
||||
});
|
||||
|
||||
this.asyncWebRequest.addErrorHandler(ex -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
try {
|
||||
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
|
||||
return;
|
||||
if (!this.errorHandlingInProgress) {
|
||||
try {
|
||||
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
|
||||
return;
|
||||
}
|
||||
deferredResult.setErrorResult(ex);
|
||||
}
|
||||
catch (Throwable interceptorEx) {
|
||||
setConcurrentResultAndDispatch(interceptorEx);
|
||||
}
|
||||
deferredResult.setErrorResult(ex);
|
||||
}
|
||||
catch (Throwable interceptorEx) {
|
||||
setConcurrentResultAndDispatch(interceptorEx);
|
||||
}
|
||||
});
|
||||
|
||||
this.asyncWebRequest.addCompletionHandler(() ->
|
||||
interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult));
|
||||
this.asyncWebRequest.addCompletionHandler(()
|
||||
-> interceptorChain.triggerAfterCompletion(this.asyncWebRequest, deferredResult));
|
||||
|
||||
interceptorChain.applyBeforeConcurrentHandling(this.asyncWebRequest, deferredResult);
|
||||
startAsyncProcessing(processingContext);
|
||||
@@ -507,46 +484,13 @@ public final class WebAsyncManager {
|
||||
synchronized (WebAsyncManager.this) {
|
||||
this.concurrentResult = RESULT_NONE;
|
||||
this.concurrentResultContext = processingContext;
|
||||
this.errorHandlingInProgress = false;
|
||||
}
|
||||
|
||||
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Started async request for " + formatUri(this.asyncWebRequest));
|
||||
}
|
||||
|
||||
this.asyncWebRequest.startAsync();
|
||||
}
|
||||
|
||||
private static String formatUri(AsyncWebRequest asyncWebRequest) {
|
||||
HttpServletRequest request = asyncWebRequest.getNativeRequest(HttpServletRequest.class);
|
||||
return (request != null ? "\"" + request.getRequestURI() + "\"" : "servlet container");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Represents a state for {@link WebAsyncManager} to be in.
|
||||
* <p><pre>
|
||||
* NOT_STARTED <------+
|
||||
* | |
|
||||
* v |
|
||||
* ASYNC_PROCESSING |
|
||||
* | |
|
||||
* v |
|
||||
* RESULT_SET -------+
|
||||
* </pre>
|
||||
* @since 5.3.33
|
||||
*/
|
||||
private enum State {
|
||||
|
||||
/** No async processing in progress. */
|
||||
NOT_STARTED,
|
||||
|
||||
/** Async handling has started, but the result hasn't been set yet. */
|
||||
ASYNC_PROCESSING,
|
||||
|
||||
/** The result is set, and an async dispatch was performed, unless there is a network error. */
|
||||
RESULT_SET
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Started async request");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2018 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.
|
||||
@@ -30,7 +30,6 @@ import org.springframework.web.context.request.NativeWebRequest;
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.2
|
||||
* @param <V> the value type
|
||||
*/
|
||||
@@ -38,25 +37,18 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
|
||||
|
||||
private final Callable<V> callable;
|
||||
|
||||
@Nullable
|
||||
private final Long timeout;
|
||||
private Long timeout;
|
||||
|
||||
@Nullable
|
||||
private final AsyncTaskExecutor executor;
|
||||
private AsyncTaskExecutor executor;
|
||||
|
||||
@Nullable
|
||||
private final String executorName;
|
||||
private String executorName;
|
||||
|
||||
@Nullable
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
@Nullable
|
||||
private Callable<V> timeoutCallback;
|
||||
|
||||
@Nullable
|
||||
private Callable<V> errorCallback;
|
||||
|
||||
@Nullable
|
||||
private Runnable completionCallback;
|
||||
|
||||
|
||||
@@ -67,9 +59,6 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
|
||||
public WebAsyncTask(Callable<V> callable) {
|
||||
Assert.notNull(callable, "Callable must not be null");
|
||||
this.callable = callable;
|
||||
this.timeout = null;
|
||||
this.executor = null;
|
||||
this.executorName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -78,11 +67,8 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
|
||||
* @param callable the callable for concurrent handling
|
||||
*/
|
||||
public WebAsyncTask(long timeout, Callable<V> callable) {
|
||||
Assert.notNull(callable, "Callable must not be null");
|
||||
this.callable = callable;
|
||||
this(callable);
|
||||
this.timeout = timeout;
|
||||
this.executor = null;
|
||||
this.executorName = null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,12 +78,10 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
|
||||
* @param callable the callable for concurrent handling
|
||||
*/
|
||||
public WebAsyncTask(@Nullable Long timeout, String executorName, Callable<V> callable) {
|
||||
Assert.notNull(callable, "Callable must not be null");
|
||||
this(callable);
|
||||
Assert.notNull(executorName, "Executor name must not be null");
|
||||
this.callable = callable;
|
||||
this.timeout = timeout;
|
||||
this.executor = null;
|
||||
this.executorName = executorName;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,12 +91,10 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
|
||||
* @param callable the callable for concurrent handling
|
||||
*/
|
||||
public WebAsyncTask(@Nullable Long timeout, AsyncTaskExecutor executor, Callable<V> callable) {
|
||||
Assert.notNull(callable, "Callable must not be null");
|
||||
this(callable);
|
||||
Assert.notNull(executor, "Executor must not be null");
|
||||
this.callable = callable;
|
||||
this.timeout = timeout;
|
||||
this.executor = executor;
|
||||
this.executorName = null;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
|
||||
|
||||
+2
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2018 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.
|
||||
@@ -82,10 +82,7 @@ public abstract class WebAsyncUtils {
|
||||
* @return an AsyncWebRequest instance (never {@code null})
|
||||
*/
|
||||
public static AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
|
||||
AsyncWebRequest prev = getAsyncManager(request).getAsyncWebRequest();
|
||||
return (prev instanceof StandardServletAsyncWebRequest ?
|
||||
new StandardServletAsyncWebRequest(request, response, (StandardServletAsyncWebRequest) prev) :
|
||||
new StandardServletAsyncWebRequest(request, response));
|
||||
return new StandardServletAsyncWebRequest(request, response);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+57
-22
@@ -21,10 +21,10 @@ import java.io.InputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.WriteListener;
|
||||
@@ -38,13 +38,12 @@ import org.springframework.util.FastByteArrayOutputStream;
|
||||
/**
|
||||
* {@link javax.servlet.http.HttpServletResponse} wrapper that caches all content written to
|
||||
* the {@linkplain #getOutputStream() output stream} and {@linkplain #getWriter() writer},
|
||||
* and allows this content to be retrieved via a {@linkplain #getContentAsByteArray() byte array}.
|
||||
* and allows this content to be retrieved via a {@link #getContentAsByteArray() byte array}.
|
||||
*
|
||||
* <p>Used e.g. by {@link org.springframework.web.filter.ShallowEtagHeaderFilter}.
|
||||
* Note: As of Spring Framework 5.0, this wrapper is built on the Servlet 3.1 API.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 4.1.3
|
||||
* @see ContentCachingRequestWrapper
|
||||
*/
|
||||
@@ -61,6 +60,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
@Nullable
|
||||
private Integer contentLength;
|
||||
|
||||
@Nullable
|
||||
private String contentType;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new ContentCachingResponseWrapper for the given servlet response.
|
||||
@@ -120,16 +122,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
return this.writer;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method neither flushes content to the client nor commits the underlying
|
||||
* response, since the content has not yet been copied to the response.
|
||||
* <p>Invoke {@link #copyBodyToResponse()} to copy the cached body content to
|
||||
* the wrapped response object and flush its buffer.
|
||||
* @see javax.servlet.ServletResponseWrapper#flushBuffer()
|
||||
*/
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
// no-op
|
||||
// do not flush the underlying response as the content has not been copied to it yet
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -147,13 +142,31 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" +
|
||||
Integer.MAX_VALUE + "): " + len);
|
||||
}
|
||||
setContentLength((int) len);
|
||||
int lenInt = (int) len;
|
||||
if (lenInt > this.content.size()) {
|
||||
this.content.resize(lenInt);
|
||||
}
|
||||
this.contentLength = lenInt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setContentType(String type) {
|
||||
this.contentType = type;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public String getContentType() {
|
||||
return this.contentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsHeader(String name) {
|
||||
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
return true;
|
||||
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
return this.contentLength != null;
|
||||
}
|
||||
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
|
||||
return this.contentType != null;
|
||||
}
|
||||
else {
|
||||
return super.containsHeader(name);
|
||||
@@ -165,6 +178,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
this.contentLength = Integer.valueOf(value);
|
||||
}
|
||||
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
|
||||
this.contentType = value;
|
||||
}
|
||||
else {
|
||||
super.setHeader(name, value);
|
||||
}
|
||||
@@ -175,6 +191,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
this.contentLength = Integer.valueOf(value);
|
||||
}
|
||||
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
|
||||
this.contentType = value;
|
||||
}
|
||||
else {
|
||||
super.addHeader(name, value);
|
||||
}
|
||||
@@ -203,8 +222,11 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
@Override
|
||||
@Nullable
|
||||
public String getHeader(String name) {
|
||||
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
return this.contentLength.toString();
|
||||
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
return (this.contentLength != null) ? this.contentLength.toString() : null;
|
||||
}
|
||||
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
|
||||
return this.contentType;
|
||||
}
|
||||
else {
|
||||
return super.getHeader(name);
|
||||
@@ -213,8 +235,12 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
|
||||
@Override
|
||||
public Collection<String> getHeaders(String name) {
|
||||
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
return Collections.singleton(this.contentLength.toString());
|
||||
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
|
||||
return this.contentLength != null ? Collections.singleton(this.contentLength.toString()) :
|
||||
Collections.emptySet();
|
||||
}
|
||||
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
|
||||
return this.contentType != null ? Collections.singleton(this.contentType) : Collections.emptySet();
|
||||
}
|
||||
else {
|
||||
return super.getHeaders(name);
|
||||
@@ -224,9 +250,14 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
@Override
|
||||
public Collection<String> getHeaderNames() {
|
||||
Collection<String> headerNames = super.getHeaderNames();
|
||||
if (this.contentLength != null) {
|
||||
Set<String> result = new LinkedHashSet<>(headerNames);
|
||||
result.add(HttpHeaders.CONTENT_LENGTH);
|
||||
if (this.contentLength != null || this.contentType != null) {
|
||||
List<String> result = new ArrayList<>(headerNames);
|
||||
if (this.contentLength != null) {
|
||||
result.add(HttpHeaders.CONTENT_LENGTH);
|
||||
}
|
||||
if (this.contentType != null) {
|
||||
result.add(HttpHeaders.CONTENT_TYPE);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
@@ -308,6 +339,10 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
|
||||
}
|
||||
this.contentLength = null;
|
||||
}
|
||||
if (complete || this.contentType != null) {
|
||||
rawResponse.setContentType(this.contentType);
|
||||
this.contentType = null;
|
||||
}
|
||||
}
|
||||
this.content.writeTo(rawResponse.getOutputStream());
|
||||
this.content.reset();
|
||||
|
||||
@@ -73,19 +73,19 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
|
||||
|
||||
private static final Pattern QUERY_PARAM_PATTERN = Pattern.compile("([^&=]+)(=?)([^&]+)?");
|
||||
|
||||
private static final String SCHEME_PATTERN = "([^:/?#\\\\]+):";
|
||||
private static final String SCHEME_PATTERN = "([^:/?#]+):";
|
||||
|
||||
private static final String HTTP_PATTERN = "(?i)(http|https):";
|
||||
|
||||
private static final String USERINFO_PATTERN = "([^/?#\\\\]*)";
|
||||
private static final String USERINFO_PATTERN = "([^@/?#]*)";
|
||||
|
||||
private static final String HOST_IPV4_PATTERN = "[^/?#:\\\\]*";
|
||||
private static final String HOST_IPV4_PATTERN = "[^\\[/?#:]*";
|
||||
|
||||
private static final String HOST_IPV6_PATTERN = "\\[[\\p{XDigit}:.]*[%\\p{Alnum}]*]";
|
||||
|
||||
private static final String HOST_PATTERN = "(" + HOST_IPV6_PATTERN + "|" + HOST_IPV4_PATTERN + ")";
|
||||
|
||||
private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#\\\\]*)";
|
||||
private static final String PORT_PATTERN = "(\\{[^}]+\\}?|[^/?#]*)";
|
||||
|
||||
private static final String PATH_PATTERN = "([^?#]*)";
|
||||
|
||||
@@ -252,7 +252,9 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
|
||||
builder.schemeSpecificPart(ssp);
|
||||
}
|
||||
else {
|
||||
checkSchemeAndHost(uri, scheme, host);
|
||||
if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) {
|
||||
throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL");
|
||||
}
|
||||
builder.userInfo(userInfo);
|
||||
builder.host(host);
|
||||
if (StringUtils.hasLength(port)) {
|
||||
@@ -294,7 +296,9 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
|
||||
builder.scheme(scheme != null ? scheme.toLowerCase() : null);
|
||||
builder.userInfo(matcher.group(4));
|
||||
String host = matcher.group(5);
|
||||
checkSchemeAndHost(httpUrl, scheme, host);
|
||||
if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) {
|
||||
throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
|
||||
}
|
||||
builder.host(host);
|
||||
String port = matcher.group(7);
|
||||
if (StringUtils.hasLength(port)) {
|
||||
@@ -313,15 +317,6 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkSchemeAndHost(String uri, @Nullable String scheme, @Nullable String host) {
|
||||
if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) {
|
||||
throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL");
|
||||
}
|
||||
if (StringUtils.hasLength(host) && host.startsWith("[") && !host.endsWith("]")) {
|
||||
throw new IllegalArgumentException("Invalid IPV6 host in [" + uri + "]");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code UriComponents} object from the URI associated with
|
||||
* the given HttpRequest while also overlaying with values from the headers
|
||||
@@ -407,7 +402,6 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
|
||||
if (StringUtils.hasLength(port)) {
|
||||
builder.port(port);
|
||||
}
|
||||
checkSchemeAndHost(origin, scheme, host);
|
||||
return builder;
|
||||
}
|
||||
else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -66,7 +66,7 @@ public class UriTemplate implements Serializable {
|
||||
* @param uriTemplate the URI template string
|
||||
*/
|
||||
public UriTemplate(String uriTemplate) {
|
||||
Assert.notNull(uriTemplate, "'uriTemplate' must not be null");
|
||||
Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
|
||||
this.uriTemplate = uriTemplate;
|
||||
this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build();
|
||||
|
||||
|
||||
-358
@@ -1,358 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.context.request.async;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.servlet.AsyncEvent;
|
||||
import javax.servlet.ServletOutputStream;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.web.testfixture.servlet.MockAsyncContext;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.BDDMockito.doAnswer;
|
||||
import static org.mockito.BDDMockito.doThrow;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
import static org.mockito.BDDMockito.verify;
|
||||
import static org.mockito.BDDMockito.verifyNoInteractions;
|
||||
|
||||
/**
|
||||
* {@link StandardServletAsyncWebRequest} tests related to response wrapping in
|
||||
* order to enforce thread safety and prevent use after errors.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class AsyncRequestNotUsableTests {
|
||||
|
||||
private final MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
|
||||
private final HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
|
||||
private final ServletOutputStream outputStream = mock(ServletOutputStream.class);
|
||||
|
||||
private final PrintWriter writer = mock(PrintWriter.class);
|
||||
|
||||
private StandardServletAsyncWebRequest asyncRequest;
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() throws IOException {
|
||||
this.request.setAsyncSupported(true);
|
||||
given(this.response.getOutputStream()).willReturn(this.outputStream);
|
||||
given(this.response.getWriter()).willReturn(this.writer);
|
||||
|
||||
this.asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
assertThat(this.asyncRequest.stateLock().isLocked()).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
private ServletOutputStream getWrappedOutputStream() throws IOException {
|
||||
return this.asyncRequest.getResponse().getOutputStream();
|
||||
}
|
||||
|
||||
@SuppressWarnings("DataFlowIssue")
|
||||
private PrintWriter getWrappedWriter() throws IOException {
|
||||
return this.asyncRequest.getResponse().getWriter();
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class ResponseTests {
|
||||
|
||||
@Test
|
||||
void notUsableAfterError() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
asyncRequest.onError(new AsyncEvent(new MockAsyncContext(request, response), new Exception()));
|
||||
|
||||
HttpServletResponse wrapped = asyncRequest.getResponse();
|
||||
assertThat(wrapped).isNotNull();
|
||||
assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after response errors.");
|
||||
assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after response errors.");
|
||||
assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after response errors.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void notUsableAfterCompletion() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
|
||||
|
||||
HttpServletResponse wrapped = asyncRequest.getResponse();
|
||||
assertThat(wrapped).isNotNull();
|
||||
assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void notUsableWhenRecreatedAfterCompletion() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
|
||||
|
||||
StandardServletAsyncWebRequest newWebRequest =
|
||||
new StandardServletAsyncWebRequest(request, response, asyncRequest);
|
||||
|
||||
HttpServletResponse wrapped = newWebRequest.getResponse();
|
||||
assertThat(wrapped).isNotNull();
|
||||
assertThatThrownBy(wrapped::getOutputStream).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(wrapped::getWriter).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(wrapped::flushBuffer).hasMessage("Response not usable after async request completion.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class OutputStreamTests {
|
||||
|
||||
@Test
|
||||
void use() throws IOException {
|
||||
testUseOutputStream();
|
||||
}
|
||||
|
||||
@Test
|
||||
void useInAsyncState() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
testUseOutputStream();
|
||||
}
|
||||
|
||||
private void testUseOutputStream() throws IOException {
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
wrapped.write('a');
|
||||
wrapped.write(new byte[0], 1, 2);
|
||||
wrapped.flush();
|
||||
wrapped.close();
|
||||
|
||||
verify(outputStream).write('a');
|
||||
verify(outputStream).write(new byte[0], 1, 2);
|
||||
verify(outputStream).flush();
|
||||
verify(outputStream).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void notUsableAfterCompletion() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
|
||||
|
||||
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(() -> wrapped.write(new byte[0])).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(() -> wrapped.write(new byte[0], 0, 0)).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(wrapped::flush).hasMessage("Response not usable after async request completion.");
|
||||
assertThatThrownBy(wrapped::close).hasMessage("Response not usable after async request completion.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockingNotUsed() throws IOException {
|
||||
AtomicInteger count = new AtomicInteger(-1);
|
||||
doAnswer((Answer<Void>) invocation -> {
|
||||
count.set(asyncRequest.stateLock().getHoldCount());
|
||||
return null;
|
||||
}).when(outputStream).write('a');
|
||||
|
||||
// Access ServletOutputStream in NEW state (no async handling) without locking
|
||||
getWrappedOutputStream().write('a');
|
||||
|
||||
assertThat(count.get()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockingUsedInAsyncState() throws IOException {
|
||||
AtomicInteger count = new AtomicInteger(-1);
|
||||
doAnswer((Answer<Void>) invocation -> {
|
||||
count.set(asyncRequest.stateLock().getHoldCount());
|
||||
return null;
|
||||
}).when(outputStream).write('a');
|
||||
|
||||
// Access ServletOutputStream in ASYNC state with locking
|
||||
asyncRequest.startAsync();
|
||||
getWrappedOutputStream().write('a');
|
||||
|
||||
assertThat(count.get()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class OutputStreamErrorTests {
|
||||
|
||||
@Test
|
||||
void writeInt() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
doThrow(new IOException("Broken pipe")).when(outputStream).write('a');
|
||||
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeBytesFull() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
byte[] bytes = new byte[0];
|
||||
doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0);
|
||||
assertThatThrownBy(() -> wrapped.write(bytes)).hasMessage("ServletOutputStream failed to write: Broken pipe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeBytes() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
byte[] bytes = new byte[0];
|
||||
doThrow(new IOException("Broken pipe")).when(outputStream).write(bytes, 0, 0);
|
||||
assertThatThrownBy(() -> wrapped.write(bytes, 0, 0)).hasMessage("ServletOutputStream failed to write: Broken pipe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void flush() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
doThrow(new IOException("Broken pipe")).when(outputStream).flush();
|
||||
assertThatThrownBy(wrapped::flush).hasMessage("ServletOutputStream failed to flush: Broken pipe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void close() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
doThrow(new IOException("Broken pipe")).when(outputStream).close();
|
||||
assertThatThrownBy(wrapped::close).hasMessage("ServletOutputStream failed to close: Broken pipe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeErrorPreventsFurtherWriting() throws IOException {
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
doThrow(new IOException("Broken pipe")).when(outputStream).write('a');
|
||||
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe");
|
||||
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors.");
|
||||
}
|
||||
|
||||
@Test
|
||||
void writeErrorInAsyncStatePreventsFurtherWriting() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
ServletOutputStream wrapped = getWrappedOutputStream();
|
||||
|
||||
doThrow(new IOException("Broken pipe")).when(outputStream).write('a');
|
||||
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("ServletOutputStream failed to write: Broken pipe");
|
||||
assertThatThrownBy(() -> wrapped.write('a')).hasMessage("Response not usable after response errors.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class WriterTests {
|
||||
|
||||
@Test
|
||||
void useWriter() throws IOException {
|
||||
testUseWriter();
|
||||
}
|
||||
|
||||
@Test
|
||||
void useWriterInAsyncState() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
testUseWriter();
|
||||
}
|
||||
|
||||
private void testUseWriter() throws IOException {
|
||||
PrintWriter wrapped = getWrappedWriter();
|
||||
|
||||
wrapped.write('a');
|
||||
wrapped.write(new char[0], 1, 2);
|
||||
wrapped.write("abc", 1, 2);
|
||||
wrapped.flush();
|
||||
wrapped.close();
|
||||
|
||||
verify(writer).write('a');
|
||||
verify(writer).write(new char[0], 1, 2);
|
||||
verify(writer).write("abc", 1, 2);
|
||||
verify(writer).flush();
|
||||
verify(writer).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void writerNotUsableAfterCompletion() throws IOException {
|
||||
asyncRequest.startAsync();
|
||||
PrintWriter wrapped = getWrappedWriter();
|
||||
|
||||
asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(request, response)));
|
||||
|
||||
char[] chars = new char[0];
|
||||
wrapped.write('a');
|
||||
wrapped.write(chars, 1, 2);
|
||||
wrapped.flush();
|
||||
wrapped.close();
|
||||
|
||||
verifyNoInteractions(writer);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockingNotUsed() throws IOException {
|
||||
AtomicInteger count = new AtomicInteger(-1);
|
||||
|
||||
doAnswer((Answer<Void>) invocation -> {
|
||||
count.set(asyncRequest.stateLock().getHoldCount());
|
||||
return null;
|
||||
}).when(writer).write('a');
|
||||
|
||||
// Use Writer in NEW state (no async handling) without locking
|
||||
PrintWriter wrapped = getWrappedWriter();
|
||||
wrapped.write('a');
|
||||
|
||||
assertThat(count.get()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void lockingUsedInAsyncState() throws IOException {
|
||||
AtomicInteger count = new AtomicInteger(-1);
|
||||
|
||||
doAnswer((Answer<Void>) invocation -> {
|
||||
count.set(asyncRequest.stateLock().getHoldCount());
|
||||
return null;
|
||||
}).when(writer).write('a');
|
||||
|
||||
// Use Writer in ASYNC state with locking
|
||||
asyncRequest.startAsync();
|
||||
PrintWriter wrapped = getWrappedWriter();
|
||||
wrapped.write('a');
|
||||
|
||||
assertThat(count.get()).isEqualTo(1);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+4
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2019 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.
|
||||
@@ -96,8 +96,9 @@ public class StandardServletAsyncWebRequestTests {
|
||||
@Test
|
||||
public void startAsyncAfterCompleted() throws Exception {
|
||||
this.asyncRequest.onComplete(new AsyncEvent(new MockAsyncContext(this.request, this.response)));
|
||||
assertThatIllegalStateException().isThrownBy(this.asyncRequest::startAsync)
|
||||
.withMessage("Cannot start async: [COMPLETED]");
|
||||
assertThatIllegalStateException().isThrownBy(
|
||||
this.asyncRequest::startAsync)
|
||||
.withMessage("Async processing has already completed");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+14
-214
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,255 +16,55 @@
|
||||
|
||||
package org.springframework.web.filter;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.Named;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Named.named;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||
import static org.springframework.http.HttpHeaders.TRANSFER_ENCODING;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ContentCachingResponseWrapper}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class ContentCachingResponseWrapperTests {
|
||||
public class ContentCachingResponseWrapperTests {
|
||||
|
||||
@Test
|
||||
void copyBodyToResponse() throws Exception {
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
|
||||
responseWrapper.setStatus(HttpServletResponse.SC_OK);
|
||||
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
|
||||
responseWrapper.copyBodyToResponse();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
|
||||
assertThat(response.getContentLength()).isGreaterThan(0);
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getContentLength() > 0).isTrue();
|
||||
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyBodyToResponseWithPresetHeaders() throws Exception {
|
||||
String PUZZLE = "puzzle";
|
||||
String ENIGMA = "enigma";
|
||||
String NUMBER = "number";
|
||||
String MAGIC = "42";
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
int responseLength = responseBody.length;
|
||||
int originalContentLength = 999;
|
||||
String contentType = MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
response.setContentType(contentType);
|
||||
response.setContentLength(originalContentLength);
|
||||
response.setHeader(PUZZLE, ENIGMA);
|
||||
response.setIntHeader(NUMBER, 42);
|
||||
|
||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
|
||||
|
||||
assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
|
||||
assertThat(responseWrapper.getContentSize()).isZero();
|
||||
assertThat(responseWrapper.getHeaderNames())
|
||||
.containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH);
|
||||
|
||||
assertHeader(responseWrapper, PUZZLE, ENIGMA);
|
||||
assertHeader(responseWrapper, NUMBER, MAGIC);
|
||||
assertHeader(responseWrapper, CONTENT_LENGTH, originalContentLength);
|
||||
assertContentTypeHeader(responseWrapper, contentType);
|
||||
|
||||
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
|
||||
assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength);
|
||||
|
||||
responseWrapper.copyBodyToResponse();
|
||||
|
||||
assertThat(responseWrapper.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
|
||||
assertThat(responseWrapper.getContentSize()).isZero();
|
||||
assertThat(responseWrapper.getHeaderNames())
|
||||
.containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH);
|
||||
|
||||
assertHeader(responseWrapper, PUZZLE, ENIGMA);
|
||||
assertHeader(responseWrapper, NUMBER, MAGIC);
|
||||
assertHeader(responseWrapper, CONTENT_LENGTH, responseLength);
|
||||
assertContentTypeHeader(responseWrapper, contentType);
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
|
||||
assertThat(response.getContentLength()).isEqualTo(responseLength);
|
||||
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
|
||||
assertThat(response.getHeaderNames())
|
||||
.containsExactlyInAnyOrder(PUZZLE, NUMBER, CONTENT_TYPE, CONTENT_LENGTH);
|
||||
|
||||
assertHeader(response, PUZZLE, ENIGMA);
|
||||
assertHeader(response, NUMBER, MAGIC);
|
||||
assertHeader(response, CONTENT_LENGTH, responseLength);
|
||||
assertContentTypeHeader(response, contentType);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "[{index}] {0}")
|
||||
@MethodSource("setContentLengthFunctions")
|
||||
void copyBodyToResponseWithOverridingContentLength(SetContentLength setContentLength) throws Exception {
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
int responseLength = responseBody.length;
|
||||
int originalContentLength = 11;
|
||||
int overridingContentLength = 22;
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
response.setContentLength(originalContentLength);
|
||||
|
||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
responseWrapper.setContentLength(overridingContentLength);
|
||||
|
||||
setContentLength.invoke(responseWrapper, overridingContentLength);
|
||||
|
||||
assertThat(responseWrapper.getContentSize()).isZero();
|
||||
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH);
|
||||
|
||||
assertHeader(response, CONTENT_LENGTH, originalContentLength);
|
||||
assertHeader(responseWrapper, CONTENT_LENGTH, overridingContentLength);
|
||||
|
||||
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
|
||||
assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength);
|
||||
|
||||
responseWrapper.copyBodyToResponse();
|
||||
|
||||
assertThat(responseWrapper.getContentSize()).isZero();
|
||||
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH);
|
||||
|
||||
assertHeader(response, CONTENT_LENGTH, responseLength);
|
||||
assertHeader(responseWrapper, CONTENT_LENGTH, responseLength);
|
||||
|
||||
assertThat(response.getContentLength()).isEqualTo(responseLength);
|
||||
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
|
||||
assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_LENGTH);
|
||||
}
|
||||
|
||||
private static Stream<Named<SetContentLength>> setContentLengthFunctions() {
|
||||
return Stream.of(
|
||||
named("setContentLength()", HttpServletResponse::setContentLength),
|
||||
named("setContentLengthLong()", HttpServletResponse::setContentLengthLong),
|
||||
named("setIntHeader()", (response, contentLength) -> response.setIntHeader(CONTENT_LENGTH, contentLength)),
|
||||
named("addIntHeader()", (response, contentLength) -> response.addIntHeader(CONTENT_LENGTH, contentLength)),
|
||||
named("setHeader()", (response, contentLength) -> response.setHeader(CONTENT_LENGTH, "" + contentLength)),
|
||||
named("addHeader()", (response, contentLength) -> response.addHeader(CONTENT_LENGTH, "" + contentLength))
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "[{index}] {0}")
|
||||
@MethodSource("setContentTypeFunctions")
|
||||
void copyBodyToResponseWithOverridingContentType(SetContentType setContentType) throws Exception {
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
int responseLength = responseBody.length;
|
||||
String originalContentType = MediaType.TEXT_PLAIN_VALUE;
|
||||
String overridingContentType = MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
response.setContentType(originalContentType);
|
||||
|
||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
|
||||
assertContentTypeHeader(response, originalContentType);
|
||||
assertContentTypeHeader(responseWrapper, originalContentType);
|
||||
|
||||
setContentType.invoke(responseWrapper, overridingContentType);
|
||||
|
||||
assertThat(responseWrapper.getContentSize()).isZero();
|
||||
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE);
|
||||
|
||||
assertContentTypeHeader(response, overridingContentType);
|
||||
assertContentTypeHeader(responseWrapper, overridingContentType);
|
||||
|
||||
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
|
||||
assertThat(responseWrapper.getContentSize()).isEqualTo(responseLength);
|
||||
|
||||
responseWrapper.copyBodyToResponse();
|
||||
|
||||
assertThat(responseWrapper.getContentSize()).isZero();
|
||||
assertThat(responseWrapper.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH);
|
||||
|
||||
assertHeader(response, CONTENT_LENGTH, responseLength);
|
||||
assertHeader(responseWrapper, CONTENT_LENGTH, responseLength);
|
||||
assertContentTypeHeader(response, overridingContentType);
|
||||
assertContentTypeHeader(responseWrapper, overridingContentType);
|
||||
|
||||
assertThat(response.getContentLength()).isEqualTo(responseLength);
|
||||
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
|
||||
assertThat(response.getHeaderNames()).containsExactlyInAnyOrder(CONTENT_TYPE, CONTENT_LENGTH);
|
||||
}
|
||||
|
||||
private static Stream<Named<SetContentType>> setContentTypeFunctions() {
|
||||
return Stream.of(
|
||||
named("setContentType()", HttpServletResponse::setContentType),
|
||||
named("setHeader()", (response, contentType) -> response.setHeader(CONTENT_TYPE, contentType)),
|
||||
named("addHeader()", (response, contentType) -> response.addHeader(CONTENT_TYPE, contentType))
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
void copyBodyToResponseWithTransferEncoding() throws Exception {
|
||||
byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(UTF_8);
|
||||
byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(StandardCharsets.UTF_8);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
|
||||
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
|
||||
responseWrapper.setHeader(TRANSFER_ENCODING, "chunked");
|
||||
responseWrapper.setStatus(HttpServletResponse.SC_OK);
|
||||
responseWrapper.setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked");
|
||||
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
|
||||
responseWrapper.copyBodyToResponse();
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
|
||||
assertHeader(response, TRANSFER_ENCODING, "chunked");
|
||||
assertHeader(response, CONTENT_LENGTH, null);
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getHeader(HttpHeaders.TRANSFER_ENCODING)).isEqualTo("chunked");
|
||||
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isNull();
|
||||
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
|
||||
}
|
||||
|
||||
private void assertHeader(HttpServletResponse response, String header, int value) {
|
||||
assertHeader(response, header, Integer.toString(value));
|
||||
}
|
||||
|
||||
private void assertHeader(HttpServletResponse response, String header, String value) {
|
||||
if (value == null) {
|
||||
assertThat(response.containsHeader(header)).as(header).isFalse();
|
||||
assertThat(response.getHeader(header)).as(header).isNull();
|
||||
assertThat(response.getHeaders(header)).as(header).isEmpty();
|
||||
}
|
||||
else {
|
||||
assertThat(response.containsHeader(header)).as(header).isTrue();
|
||||
assertThat(response.getHeader(header)).as(header).isEqualTo(value);
|
||||
assertThat(response.getHeaders(header)).as(header).containsExactly(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void assertContentTypeHeader(HttpServletResponse response, String contentType) {
|
||||
assertHeader(response, CONTENT_TYPE, contentType);
|
||||
assertThat(response.getContentType()).as(CONTENT_TYPE).isEqualTo(contentType);
|
||||
}
|
||||
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SetContentLength {
|
||||
void invoke(HttpServletResponse response, int contentLength);
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
private interface SetContentType {
|
||||
void invoke(HttpServletResponse response, String contentType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+49
-52
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,36 +16,33 @@
|
||||
|
||||
package org.springframework.web.filter;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
|
||||
|
||||
/**
|
||||
* Tests for {@link ShallowEtagHeaderFilter}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Brian Clozel
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class ShallowEtagHeaderFilterTests {
|
||||
public class ShallowEtagHeaderFilterTests {
|
||||
|
||||
private final ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter();
|
||||
|
||||
|
||||
@Test
|
||||
void isEligibleForEtag() {
|
||||
public void isEligibleForEtag() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
@@ -64,60 +61,60 @@ class ShallowEtagHeaderFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterNoMatch() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterNoMatch() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
|
||||
filterResponse.setContentType(TEXT_PLAIN_VALUE);
|
||||
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
};
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertThat(response.getStatus()).as("Invalid status").isEqualTo(200);
|
||||
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
|
||||
assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0);
|
||||
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
|
||||
assertThat(response.getContentLength() > 0).as("Invalid Content-Length header").isTrue();
|
||||
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterNoMatchWeakETag() throws Exception {
|
||||
public void filterNoMatchWeakETag() throws Exception {
|
||||
this.filter.setWriteWeakETag(true);
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
|
||||
filterResponse.setContentType(TEXT_PLAIN_VALUE);
|
||||
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
};
|
||||
filter.doFilter(request, response, filterChain);
|
||||
|
||||
assertThat(response.getStatus()).as("Invalid status").isEqualTo(200);
|
||||
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("W/\"0b10a8db164e0754105b7a99be72e3fe5\"");
|
||||
assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0);
|
||||
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
|
||||
assertThat(response.getContentLength() > 0).as("Invalid Content-Length header").isTrue();
|
||||
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterMatch() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterMatch() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
|
||||
request.addHeader("If-None-Match", etag);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
filterResponse.setContentLength(responseBody.length);
|
||||
filterResponse.setContentType(TEXT_PLAIN_VALUE);
|
||||
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
};
|
||||
filter.doFilter(request, response, filterChain);
|
||||
@@ -125,20 +122,21 @@ class ShallowEtagHeaderFilterTests {
|
||||
assertThat(response.getStatus()).as("Invalid status").isEqualTo(304);
|
||||
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
|
||||
assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse();
|
||||
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
|
||||
assertThat(response.containsHeader("Content-Type")).as("Response has Content-Type header").isFalse();
|
||||
byte[] expecteds = new byte[0];
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterMatchWeakEtag() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterMatchWeakEtag() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
|
||||
request.addHeader("If-None-Match", "W/" + etag);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
filterResponse.setContentLength(responseBody.length);
|
||||
};
|
||||
@@ -147,12 +145,13 @@ class ShallowEtagHeaderFilterTests {
|
||||
assertThat(response.getStatus()).as("Invalid status").isEqualTo(304);
|
||||
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
|
||||
assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse();
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
|
||||
byte[] expecteds = new byte[0];
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterWriter() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterWriter() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
|
||||
request.addHeader("If-None-Match", etag);
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
@@ -168,20 +167,19 @@ class ShallowEtagHeaderFilterTests {
|
||||
assertThat(response.getStatus()).as("Invalid status").isEqualTo(304);
|
||||
assertThat(response.getHeader("ETag")).as("Invalid ETag").isEqualTo("\"0b10a8db164e0754105b7a99be72e3fe5\"");
|
||||
assertThat(response.containsHeader("Content-Length")).as("Response has Content-Length header").isFalse();
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
|
||||
byte[] expecteds = new byte[0];
|
||||
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
|
||||
}
|
||||
|
||||
@Test // SPR-12960
|
||||
void filterWriterWithDisabledCaching() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterWriterWithDisabledCaching() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
response.setContentType(TEXT_PLAIN_VALUE);
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
|
||||
filterResponse.setContentType(APPLICATION_JSON_VALUE);
|
||||
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
|
||||
};
|
||||
|
||||
@@ -190,16 +188,15 @@ class ShallowEtagHeaderFilterTests {
|
||||
|
||||
assertThat(response.getStatus()).isEqualTo(200);
|
||||
assertThat(response.getHeader("ETag")).isNull();
|
||||
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(APPLICATION_JSON_VALUE);
|
||||
assertThat(response.getContentAsByteArray()).isEqualTo(responseBody);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterSendError() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterSendError() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
response.setContentLength(100);
|
||||
@@ -215,11 +212,11 @@ class ShallowEtagHeaderFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterSendErrorMessage() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterSendErrorMessage() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
response.setContentLength(100);
|
||||
@@ -236,11 +233,11 @@ class ShallowEtagHeaderFilterTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void filterSendRedirect() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterSendRedirect() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
response.setContentLength(100);
|
||||
@@ -257,11 +254,11 @@ class ShallowEtagHeaderFilterTests {
|
||||
}
|
||||
|
||||
@Test // SPR-13717
|
||||
void filterFlushResponse() throws Exception {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
public void filterFlushResponse() throws Exception {
|
||||
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
|
||||
MockHttpServletResponse response = new MockHttpServletResponse();
|
||||
|
||||
byte[] responseBody = "Hello World".getBytes(UTF_8);
|
||||
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
|
||||
FilterChain filterChain = (filterRequest, filterResponse) -> {
|
||||
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
|
||||
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,104 +27,79 @@ import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
|
||||
/**
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
class UriTemplateTests {
|
||||
public class UriTemplateTests {
|
||||
|
||||
@Test
|
||||
void emptyPathDoesNotThrowException() {
|
||||
assertThatNoException().isThrownBy(() -> new UriTemplate(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nullPathThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new UriTemplate(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getVariableNames() {
|
||||
public void getVariableNames() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
|
||||
List<String> variableNames = template.getVariableNames();
|
||||
assertThat(variableNames).as("Invalid variable names").isEqualTo(Arrays.asList("hotel", "booking"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void getVariableNamesFromEmpty() {
|
||||
UriTemplate template = new UriTemplate("");
|
||||
List<String> variableNames = template.getVariableNames();
|
||||
assertThat(variableNames).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandVarArgs() {
|
||||
public void expandVarArgs() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
|
||||
URI result = template.expand("1", "42");
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandVarArgsFromEmpty() {
|
||||
UriTemplate template = new UriTemplate("");
|
||||
URI result = template.expand();
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create(""));
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42"));
|
||||
}
|
||||
|
||||
@Test // SPR-9712
|
||||
void expandVarArgsWithArrayValue() {
|
||||
public void expandVarArgsWithArrayValue() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/sum?numbers={numbers}");
|
||||
URI result = template.expand(new int[] {1, 2, 3});
|
||||
assertThat(result).isEqualTo(URI.create("/sum?numbers=1,2,3"));
|
||||
assertThat(result).isEqualTo(new URI("/sum?numbers=1,2,3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandVarArgsNotEnoughVariables() {
|
||||
public void expandVarArgsNotEnoughVariables() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> template.expand("1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandMap() {
|
||||
public void expandMap() throws Exception {
|
||||
Map<String, String> uriVariables = new HashMap<>(2);
|
||||
uriVariables.put("booking", "42");
|
||||
uriVariables.put("hotel", "1");
|
||||
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
|
||||
URI result = template.expand(uriVariables);
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42"));
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandMapDuplicateVariables() {
|
||||
public void expandMapDuplicateVariables() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}");
|
||||
assertThat(template.getVariableNames()).isEqualTo(Arrays.asList("c", "c", "c"));
|
||||
URI result = template.expand(Collections.singletonMap("c", "cheeseburger"));
|
||||
assertThat(result).isEqualTo(URI.create("/order/cheeseburger/cheeseburger/cheeseburger"));
|
||||
assertThat(result).isEqualTo(new URI("/order/cheeseburger/cheeseburger/cheeseburger"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandMapNonString() {
|
||||
public void expandMapNonString() throws Exception {
|
||||
Map<String, Integer> uriVariables = new HashMap<>(2);
|
||||
uriVariables.put("booking", 42);
|
||||
uriVariables.put("hotel", 1);
|
||||
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
|
||||
URI result = template.expand(uriVariables);
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42"));
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandMapEncoded() {
|
||||
public void expandMapEncoded() throws Exception {
|
||||
Map<String, String> uriVariables = Collections.singletonMap("hotel", "Z\u00fcrich");
|
||||
UriTemplate template = new UriTemplate("/hotel list/{hotel}");
|
||||
URI result = template.expand(uriVariables);
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotel%20list/Z%C3%BCrich"));
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotel%20list/Z%C3%BCrich"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandMapUnboundVariables() {
|
||||
public void expandMapUnboundVariables() throws Exception {
|
||||
Map<String, String> uriVariables = new HashMap<>(2);
|
||||
uriVariables.put("booking", "42");
|
||||
uriVariables.put("bar", "1");
|
||||
@@ -134,14 +109,14 @@ class UriTemplateTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandEncoded() {
|
||||
public void expandEncoded() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/hotel list/{hotel}");
|
||||
URI result = template.expand("Z\u00fcrich");
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotel%20list/Z%C3%BCrich"));
|
||||
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotel%20list/Z%C3%BCrich"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matches() {
|
||||
public void matches() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
|
||||
assertThat(template.matches("/hotels/1/bookings/42")).as("UriTemplate does not match").isTrue();
|
||||
assertThat(template.matches("/hotels/bookings")).as("UriTemplate matches").isFalse();
|
||||
@@ -150,23 +125,14 @@ class UriTemplateTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesAgainstEmpty() {
|
||||
UriTemplate template = new UriTemplate("");
|
||||
assertThat(template.matches("/hotels/1/bookings/42")).as("UriTemplate matches").isFalse();
|
||||
assertThat(template.matches("/hotels/bookings")).as("UriTemplate matches").isFalse();
|
||||
assertThat(template.matches("")).as("UriTemplate does not match").isTrue();
|
||||
assertThat(template.matches(null)).as("UriTemplate matches").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesCustomRegex() {
|
||||
public void matchesCustomRegex() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/hotels/{hotel:\\d+}");
|
||||
assertThat(template.matches("/hotels/42")).as("UriTemplate does not match").isTrue();
|
||||
assertThat(template.matches("/hotels/foo")).as("UriTemplate matches").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void match() {
|
||||
public void match() throws Exception {
|
||||
Map<String, String> expected = new HashMap<>(2);
|
||||
expected.put("booking", "42");
|
||||
expected.put("hotel", "1");
|
||||
@@ -177,14 +143,7 @@ class UriTemplateTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchAgainstEmpty() {
|
||||
UriTemplate template = new UriTemplate("");
|
||||
Map<String, String> result = template.match("/hotels/1/bookings/42");
|
||||
assertThat(result).as("Invalid match").isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchCustomRegex() {
|
||||
public void matchCustomRegex() throws Exception {
|
||||
Map<String, String> expected = new HashMap<>(2);
|
||||
expected.put("booking", "42");
|
||||
expected.put("hotel", "1");
|
||||
@@ -195,14 +154,14 @@ class UriTemplateTests {
|
||||
}
|
||||
|
||||
@Test // SPR-13627
|
||||
void matchCustomRegexWithNestedCurlyBraces() {
|
||||
public void matchCustomRegexWithNestedCurlyBraces() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/site.{domain:co.[a-z]{2}}");
|
||||
Map<String, String> result = template.match("/site.co.eu");
|
||||
assertThat(result).as("Invalid match").isEqualTo(Collections.singletonMap("domain", "co.eu"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchDuplicate() {
|
||||
public void matchDuplicate() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}");
|
||||
Map<String, String> result = template.match("/order/cheeseburger/cheeseburger/cheeseburger");
|
||||
Map<String, String> expected = Collections.singletonMap("c", "cheeseburger");
|
||||
@@ -210,7 +169,7 @@ class UriTemplateTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchMultipleInOneSegment() {
|
||||
public void matchMultipleInOneSegment() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/{foo}-{bar}");
|
||||
Map<String, String> result = template.match("/12-34");
|
||||
Map<String, String> expected = new HashMap<>(2);
|
||||
@@ -220,19 +179,19 @@ class UriTemplateTests {
|
||||
}
|
||||
|
||||
@Test // SPR-16169
|
||||
void matchWithMultipleSegmentsAtTheEnd() {
|
||||
public void matchWithMultipleSegmentsAtTheEnd() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/account/{accountId}");
|
||||
assertThat(template.matches("/account/15/alias/5")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryVariables() {
|
||||
public void queryVariables() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/search?q={query}");
|
||||
assertThat(template.matches("/search?q=foo")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void fragments() {
|
||||
public void fragments() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/search#{fragment}");
|
||||
assertThat(template.matches("/search#foo")).isTrue();
|
||||
|
||||
@@ -241,19 +200,19 @@ class UriTemplateTests {
|
||||
}
|
||||
|
||||
@Test // SPR-13705
|
||||
void matchesWithSlashAtTheEnd() {
|
||||
public void matchesWithSlashAtTheEnd() throws Exception {
|
||||
assertThat(new UriTemplate("/test/").matches("/test/")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandWithDollar() {
|
||||
public void expandWithDollar() throws Exception {
|
||||
UriTemplate template = new UriTemplate("/{a}");
|
||||
URI uri = template.expand("$replacement");
|
||||
assertThat(uri.toString()).isEqualTo("/$replacement");
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandWithAtSign() {
|
||||
public void expandWithAtSign() throws Exception {
|
||||
UriTemplate template = new UriTemplate("http://localhost/query={query}");
|
||||
URI uri = template.expand("foo@bar");
|
||||
assertThat(uri.toString()).isEqualTo("http://localhost/query=foo@bar");
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 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.
|
||||
@@ -61,6 +61,7 @@ import javax.servlet.http.Part;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.LinkedCaseInsensitiveMap;
|
||||
@@ -992,7 +993,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
|
||||
}
|
||||
}
|
||||
|
||||
private static String encodeCookies(Cookie... cookies) {
|
||||
private static String encodeCookies(@NonNull Cookie... cookies) {
|
||||
return Arrays.stream(cookies)
|
||||
.map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue()))
|
||||
.collect(Collectors.joining("; "));
|
||||
|
||||
+1
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -94,7 +94,6 @@ public class HandlerFunctionAdapter implements HandlerAdapter, Ordered {
|
||||
Object handler) throws Exception {
|
||||
|
||||
WebAsyncManager asyncManager = getWebAsyncManager(servletRequest, servletResponse);
|
||||
servletResponse = getWrappedResponse(asyncManager);
|
||||
|
||||
ServerRequest serverRequest = getServerRequest(servletRequest);
|
||||
ServerResponse serverResponse;
|
||||
@@ -124,22 +123,6 @@ public class HandlerFunctionAdapter implements HandlerAdapter, Ordered {
|
||||
return asyncManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain response wrapped by
|
||||
* {@link org.springframework.web.context.request.async.StandardServletAsyncWebRequest}
|
||||
* to enforce lifecycle rules from Servlet spec (section 2.3.3.4)
|
||||
* in case of async handling.
|
||||
*/
|
||||
private static HttpServletResponse getWrappedResponse(WebAsyncManager asyncManager) {
|
||||
AsyncWebRequest asyncRequest = asyncManager.getAsyncWebRequest();
|
||||
Assert.notNull(asyncRequest, "No AsyncWebRequest");
|
||||
|
||||
HttpServletResponse servletResponse = asyncRequest.getNativeResponse(HttpServletResponse.class);
|
||||
Assert.notNull(servletResponse, "No HttpServletResponse");
|
||||
|
||||
return servletResponse;
|
||||
}
|
||||
|
||||
private ServerRequest getServerRequest(HttpServletRequest servletRequest) {
|
||||
ServerRequest serverRequest =
|
||||
(ServerRequest) servletRequest.getAttribute(RouterFunctions.REQUEST_ATTRIBUTE);
|
||||
|
||||
+10
-15
@@ -853,21 +853,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
|
||||
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
|
||||
|
||||
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
|
||||
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
|
||||
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
|
||||
|
||||
asyncManager.setTaskExecutor(this.taskExecutor);
|
||||
asyncManager.setAsyncWebRequest(asyncWebRequest);
|
||||
asyncManager.registerCallableInterceptors(this.callableInterceptors);
|
||||
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
|
||||
|
||||
// Obtain wrapped response to enforce lifecycle rule from Servlet spec, section 2.3.3.4
|
||||
response = asyncWebRequest.getNativeResponse(HttpServletResponse.class);
|
||||
|
||||
ServletWebRequest webRequest = (asyncWebRequest instanceof ServletWebRequest ?
|
||||
(ServletWebRequest) asyncWebRequest : new ServletWebRequest(request, response));
|
||||
|
||||
ServletWebRequest webRequest = new ServletWebRequest(request, response);
|
||||
try {
|
||||
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
|
||||
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
|
||||
@@ -887,6 +873,15 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
|
||||
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
|
||||
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);
|
||||
|
||||
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
|
||||
asyncWebRequest.setTimeout(this.asyncRequestTimeout);
|
||||
|
||||
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
|
||||
asyncManager.setTaskExecutor(this.taskExecutor);
|
||||
asyncManager.setAsyncWebRequest(asyncWebRequest);
|
||||
asyncManager.registerCallableInterceptors(this.callableInterceptors);
|
||||
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);
|
||||
|
||||
if (asyncManager.hasConcurrentResult()) {
|
||||
Object result = asyncManager.getConcurrentResult();
|
||||
Object[] resultContext = asyncManager.getConcurrentResultContext();
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2020 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.
|
||||
@@ -203,6 +203,7 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume
|
||||
"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
|
||||
}
|
||||
return pushBuilder;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -46,7 +46,6 @@ import org.springframework.web.bind.ServletRequestBindingException;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.support.MissingServletRequestPartException;
|
||||
@@ -229,10 +228,6 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
|
||||
return handleAsyncRequestTimeoutException(
|
||||
(AsyncRequestTimeoutException) ex, request, response, handler);
|
||||
}
|
||||
else if (ex instanceof AsyncRequestNotUsableException) {
|
||||
return handleAsyncRequestNotUsableException(
|
||||
(AsyncRequestNotUsableException) ex, request, response, handler);
|
||||
}
|
||||
}
|
||||
catch (Exception handlerEx) {
|
||||
if (logger.isWarnEnabled()) {
|
||||
@@ -546,23 +541,6 @@ public class DefaultHandlerExceptionResolver extends AbstractHandlerExceptionRes
|
||||
return new ModelAndView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the case of an I/O failure from the ServletOutputStream.
|
||||
* <p>By default, do nothing since the response is not usable.
|
||||
* @param ex the {@link AsyncRequestTimeoutException} to be handled
|
||||
* @param request current HTTP request
|
||||
* @param response current HTTP response
|
||||
* @param handler the executed handler, or {@code null} if none chosen
|
||||
* at the time of the exception (for example, if multipart resolution failed)
|
||||
* @return an empty ModelAndView indicating the exception was handled
|
||||
* @since 5.3.33
|
||||
*/
|
||||
protected ModelAndView handleAsyncRequestNotUsableException(AsyncRequestNotUsableException ex,
|
||||
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) {
|
||||
|
||||
return new ModelAndView();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked to send a server error. Sets the status to 500 and also sets the
|
||||
* request attribute "javax.servlet.error.exception" to the Exception.
|
||||
|
||||
-108
@@ -1,108 +0,0 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.web.servlet.function.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.servlet.AsyncEvent;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
|
||||
import org.springframework.web.context.request.async.WebAsyncManager;
|
||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||
import org.springframework.web.servlet.function.HandlerFunction;
|
||||
import org.springframework.web.servlet.function.RouterFunctions;
|
||||
import org.springframework.web.servlet.function.ServerRequest;
|
||||
import org.springframework.web.servlet.function.ServerResponse;
|
||||
import org.springframework.web.testfixture.servlet.MockAsyncContext;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.BDDMockito.doThrow;
|
||||
import static org.mockito.BDDMockito.mock;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link HandlerFunctionAdapter}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class HandlerFunctionAdapterTests {
|
||||
|
||||
private final MockHttpServletRequest servletRequest = new MockHttpServletRequest("GET", "/");
|
||||
|
||||
private final MockHttpServletResponse servletResponse = new MockHttpServletResponse();
|
||||
|
||||
private final HandlerFunctionAdapter adapter = new HandlerFunctionAdapter();
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.servletRequest.setAttribute(RouterFunctions.REQUEST_ATTRIBUTE,
|
||||
ServerRequest.create(this.servletRequest, Collections.singletonList(new StringHttpMessageConverter())));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void asyncRequestNotUsable() throws Exception {
|
||||
|
||||
HandlerFunction<?> handler = request -> ServerResponse.sse(sseBuilder -> {
|
||||
try {
|
||||
sseBuilder.data("data 1");
|
||||
sseBuilder.data("data 2");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
});
|
||||
|
||||
this.servletRequest.setAsyncSupported(true);
|
||||
|
||||
HttpServletResponse mockServletResponse = mock(HttpServletResponse.class);
|
||||
doThrow(new IOException("Broken pipe")).when(mockServletResponse).getOutputStream();
|
||||
|
||||
// Use of response should be rejected
|
||||
assertThatThrownBy(() -> adapter.handle(servletRequest, mockServletResponse, handler))
|
||||
.hasRootCauseInstanceOf(IOException.class)
|
||||
.hasRootCauseMessage("Broken pipe");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asyncRequestNotUsableOnAsyncDispatch() throws Exception {
|
||||
|
||||
HandlerFunction<?> handler = request -> ServerResponse.ok().body("body");
|
||||
|
||||
// Put AsyncWebRequest in ERROR state
|
||||
StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(servletRequest, servletResponse);
|
||||
asyncRequest.onError(new AsyncEvent(new MockAsyncContext(servletRequest, servletResponse), new Exception()));
|
||||
|
||||
// Set it as the current AsyncWebRequest, from the initial REQUEST dispatch
|
||||
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(servletRequest);
|
||||
asyncManager.setAsyncWebRequest(asyncRequest);
|
||||
|
||||
// Use of response should be rejected
|
||||
assertThatThrownBy(() -> adapter.handle(servletRequest, servletResponse, handler))
|
||||
.isInstanceOf(AsyncRequestNotUsableException.class);
|
||||
}
|
||||
|
||||
}
|
||||
+7
-38
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,11 +16,8 @@
|
||||
|
||||
package org.springframework.web.servlet.mvc.method.annotation;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -28,8 +25,6 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.servlet.AsyncEvent;
|
||||
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -51,10 +46,6 @@ import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.SessionAttributes;
|
||||
import org.springframework.web.context.request.async.AsyncRequestNotUsableException;
|
||||
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
|
||||
import org.springframework.web.context.request.async.WebAsyncManager;
|
||||
import org.springframework.web.context.request.async.WebAsyncUtils;
|
||||
import org.springframework.web.context.support.StaticWebApplicationContext;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.method.annotation.ModelMethodProcessor;
|
||||
@@ -64,15 +55,13 @@ import org.springframework.web.method.support.InvocableHandlerMethod;
|
||||
import org.springframework.web.servlet.DispatcherServlet;
|
||||
import org.springframework.web.servlet.FlashMap;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.testfixture.servlet.MockAsyncContext;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
|
||||
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link RequestMappingHandlerAdapter}.
|
||||
* Unit tests for {@link RequestMappingHandlerAdapter}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
@@ -261,7 +250,9 @@ public class RequestMappingHandlerAdapterTests {
|
||||
assertThat(mav.getModel().get("attr3")).isNull();
|
||||
}
|
||||
|
||||
@Test // gh-15486
|
||||
// SPR-10859
|
||||
|
||||
@Test
|
||||
public void responseBodyAdvice() throws Exception {
|
||||
List<HttpMessageConverter<?>> converters = new ArrayList<>();
|
||||
converters.add(new MappingJackson2HttpMessageConverter());
|
||||
@@ -281,26 +272,6 @@ public class RequestMappingHandlerAdapterTests {
|
||||
assertThat(this.response.getContentAsString()).isEqualTo("{\"status\":400,\"message\":\"body\"}");
|
||||
}
|
||||
|
||||
@Test
|
||||
void asyncRequestNotUsable() throws Exception {
|
||||
|
||||
// Put AsyncWebRequest in ERROR state
|
||||
StandardServletAsyncWebRequest asyncRequest = new StandardServletAsyncWebRequest(this.request, this.response);
|
||||
asyncRequest.onError(new AsyncEvent(new MockAsyncContext(this.request, this.response), new Exception()));
|
||||
|
||||
// Set it as the current AsyncWebRequest, from the initial REQUEST dispatch
|
||||
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(this.request);
|
||||
asyncManager.setAsyncWebRequest(asyncRequest);
|
||||
|
||||
// AsyncWebRequest created for current dispatch should inherit state
|
||||
HandlerMethod handlerMethod = handlerMethod(new SimpleController(), "handleOutputStream", OutputStream.class);
|
||||
this.handlerAdapter.afterPropertiesSet();
|
||||
|
||||
// Use of response should be rejected
|
||||
assertThatThrownBy(() -> this.handlerAdapter.handle(this.request, this.response, handlerMethod))
|
||||
.isInstanceOf(AsyncRequestNotUsableException.class);
|
||||
}
|
||||
|
||||
private HandlerMethod handlerMethod(Object handler, String methodName, Class<?>... paramTypes) throws Exception {
|
||||
Method method = handler.getClass().getDeclaredMethod(methodName, paramTypes);
|
||||
return new InvocableHandlerMethod(handler, method);
|
||||
@@ -326,16 +297,14 @@ public class RequestMappingHandlerAdapterTests {
|
||||
}
|
||||
|
||||
public ResponseEntity<Map<String, String>> handleWithResponseEntity() {
|
||||
return new ResponseEntity<>(Collections.singletonMap("foo", "bar"), HttpStatus.OK);
|
||||
return new ResponseEntity<>(Collections.singletonMap(
|
||||
"foo", "bar"), HttpStatus.OK);
|
||||
}
|
||||
|
||||
public ResponseEntity<String> handleBadRequest() {
|
||||
return new ResponseEntity<>("body", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
public void handleOutputStream(OutputStream outputStream) throws IOException {
|
||||
outputStream.write("body".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+1
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2021 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.
|
||||
@@ -92,9 +92,6 @@ public class ResponseEntityExceptionHandlerTests {
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
if (method.getName().startsWith("handle") && (paramTypes.length == 4)) {
|
||||
String name = paramTypes[0].getSimpleName();
|
||||
if (name.equals("AsyncRequestNotUsableException")) {
|
||||
continue;
|
||||
}
|
||||
assertThat(exceptionTypes.contains(paramTypes[0])).as("@ExceptionHandler is missing " + name).isTrue();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user