Compare commits

...

50 Commits

Author SHA1 Message Date
Spring Builds fb9a56f40c Release v5.3.34 2024-04-11 07:04:17 +00:00
Brian Clozel 7678286fb3 Refine UriComponentsBuilder parsing
This commit refines the expressions for the scheme, user info, host and
port parts of the URL in UriComponentsBuilder to better conform to
RFC 3986.

Fixes gh-32618
2024-04-11 08:50:46 +02:00
Stéphane Nicoll 510ff87721 Upgrade to Reactor 2020.0.43
Closes gh-32594
2024-04-09 19:57:06 +02:00
Sam Brannen 7609727433 Detect bridge methods across ApplicationContexts in MethodIntrospector
Prior to this commit, MethodIntrospector failed to properly detect
bridge methods for subsequent invocations of selectMethods() with the
same targetType and MetadataLookup, if such subsequent invocations
occurred after the ApplicationContext had been refreshed.

The reason this occurs is due to the following.

- Class#getDeclaredMethods() always returns "child copies" of the
  underlying Method instances -- which means that `equals()` should be
  used instead of `==` whenever the compared Method instances can come
  from different sources (such as the static caches mentioned below).

- BridgeMethodResolver caches resolved bridge methods in a static cache
  -- which is never cleared.

- ReflectionUtils caches declared methods in a static cache
  -- which gets cleared when an ApplicationContext is refreshed.

Consequently, if you attempt to load an ApplicationContext twice in the
same ClassLoader, the second attempt uses the existing, populated cache
for bridged methods but a cleared, empty cache for declared methods.
This results in new invocations of Class#getDeclaredMethods(), and
identity checks with `==` then fail to detect equivalent bridge methods.

This commit addresses this by additionally comparing bridge methods
using `equals()` in MethodIntrospector.selectMethods().

Note that the `==` checks remain in place as an optimization for when
`equals()` is unnecessary.

Closes gh-32586

(cherry picked from commit e702733c7b)
2024-04-09 19:14:32 +02:00
Juergen Hoeller a0ae96da69 Upgrade to Netty 4.1.108 2024-04-09 17:02:52 +02:00
Juergen Hoeller 1d2daa5d6e Log column type for limited support message in getResultSetValue
Closes gh-32601

(cherry picked from commit c5590ae9e6)
2024-04-09 16:23:19 +02:00
Stéphane Nicoll 239594595c Upgrade actions that use deprecated features 2024-03-20 10:38:57 +01:00
Juergen Hoeller 76c0017180 Polishing 2024-03-18 16:34:06 +01:00
Juergen Hoeller 0628b479f1 Propagate JMS IllegalStateException from commit/rollbackIfNecessary
Closes gh-32473

(cherry picked from commit cd7ba1835c)
2024-03-18 16:25:33 +01:00
Sam Brannen f6205d4207 Avoid unnecessary Annotation array cloning in TypeDescriptor
Closes gh-32476

(cherry picked from commit 42a4f28962)
2024-03-18 15:19:40 +01:00
Juergen Hoeller d21100fea0 Restore original toString representation (revert accidental backport)
See gh-32405
2024-03-17 20:46:59 +01:00
Juergen Hoeller f7f1028428 Remove superfluous @NonNull declarations 2024-03-16 14:39:58 +01:00
Juergen Hoeller 1cb0c7c036 Avoid cloning empty Annotation array in TypeDescriptor (backport)
Closes gh-32405
2024-03-16 14:31:24 +01:00
Juergen Hoeller 51d70dcf34 Polishing 2024-03-15 21:30:07 +01:00
Juergen Hoeller 61f7087911 Consistently apply TaskDecorator to ManagedExecutorService as well
Closes gh-32455
2024-03-15 21:23:43 +01:00
Stéphane Nicoll 2ff8a00e9a Enable backport bot on 5.3.x
Closes gh-32451
2024-03-15 11:19:11 +01:00
Stéphane Nicoll a653b85378 Harmonize Concourse configuration 2024-03-15 09:45:52 +01:00
Stéphane Nicoll 17650e0741 Move CI to GitHub Actions
Closes gh-32449
2024-03-15 09:30:15 +01:00
Spring Builds 0c17d257fb Next development version (v5.3.34-SNAPSHOT) 2024-03-14 09:14:52 +00:00
rstoyanchev 297cbae299 Extract reusable checkSchemeAndPort method
Closes gh-32440
2024-03-14 08:49:55 +00:00
Juergen Hoeller 274fba47f3 Additional unit tests for operations on empty UriTemplate
See gh-32432

(cherry picked from commit 54a6d89da7)
2024-03-13 18:34:17 +01:00
Kasper Bisgaard 5dfec09edd Allow UriTemplate to be built with an empty template
Closes gh-32438
2024-03-13 17:38:27 +01:00
Juergen Hoeller 5056e8cbfb Upgrade to Reactor 2020.0.42
Closes gh-32422
2024-03-12 20:35:39 +01:00
Juergen Hoeller 4566e8685d Polishing
(cherry picked from commit 723c94e5ac)
2024-03-12 20:35:30 +01:00
Sam Brannen 1b84f970de Disable external Javadoc URLs not supported on JDK 8
This commit comments out (disables) 3 external Javadoc URLs, since the
javadoc tool on JDK 8 cannot access them.
2024-03-11 13:03:36 +01:00
Sébastien Deleuze 41bc43b033 Build KDoc against 5.3.x Spring Framework Javadoc
Closes gh-32414
2024-03-11 10:19:49 +01:00
Juergen Hoeller 915d5bddea Polishing 2024-03-08 19:49:28 +01:00
rstoyanchev dc86feaeb6 Remove IOException that's not thrown from Javadoc 2024-03-07 16:16:30 +00:00
rstoyanchev ff412de247 Use wrapped response in HandlerFunctionAdapter
webmvc.fn now also uses the StandardServletAsyncWebRequest wrapped response
to enforce lifecycle rules from Servlet spec (section 2.3.3.4).

See gh-32342
2024-03-07 15:00:08 +00:00
rstoyanchev 5d572f6490 Fix checkstyle violation 2024-03-05 12:39:48 +00:00
rstoyanchev b8b1f5b6be Add missed merge changes from last commit
See gh-32342
2024-03-05 12:28:54 +00:00
rstoyanchev 99e38ecf41 Backport tests for wrapping of response for async requests
This is a backport of commits 4b96cd and ef0717.

Closes gh-32342
2024-03-05 12:11:55 +00:00
Juergen Hoeller ed0c2ff37f Restore ability to return original method for proxy-derived method
Closes gh-32365
2024-03-04 23:42:04 +01:00
rstoyanchev e668e7767c Wrap PrintWriter in StandardServletAsyncWebRequest
Closes gh-32342
2024-03-03 18:38:40 +00:00
rstoyanchev 30c75ffe8e Avoid locking if not handling asynchronously
Avoid the overhead of locking in the ServletOutputStream wrapper
if it is not an asynchronous request.

See gh-32342
2024-03-03 18:38:05 +00:00
rstoyanchev dd5fe68522 Dispatch again after disconnected client error
Dispatching was prevented for disconnected client errors after
recent reports like #32042 when running on Tomcat, and the
async request was completed from the onError notification.
This has the side effect of not allowing exception resolvers
to take final action even if the response is not writeable.

After all updates for this issue, it appears the dispatch no
longer causes issues. Tomcat actually does not do the dispatch,
but it doesn't seem to cause any issues, and on other servers
like Jetty where the dispatch works, applications can have a
chance to handle the exception.

This change removes the disconnected client checks and allows
dispatching again. After the change DisconnectedClientHelper
is no longer needed in the 5.3.x branch.

See gh-32342
2024-03-03 18:37:21 +00:00
rstoyanchev 2aca714010 Add locking in StandardServletAsyncWebRequest
The lock protects against race between onError/onComplete notifications
and operations on the ServletOutputStream.

See gh-32342
2024-03-01 18:21:59 +00:00
rstoyanchev 3b7c435134 Polishing
See gh-32342
2024-03-01 15:57:16 +00:00
rstoyanchev 6432b13a4c Add state and response wrapping to StandardServletAsyncWebRequest
The wrapped response prevents use after AsyncListener onError or completion
to ensure compliance with Servlet Spec 2.3.3.4.

See gh-32342
2024-03-01 14:31:41 +00:00
rstoyanchev 3478a702e5 Improve concurrent handling of result in WebAsyncManager
1. Use state transitions
2. Increase synchronized scope in setConcurrentResultAndDispatch

See gh-32342
2024-02-29 20:43:31 +00:00
rstoyanchev b31550fd85 Align 5.3.x with 6.1.x
In preparation for a larger update, start by aligning with
6.1.x, which includes changes for gh-32042 and gh-30232.

See gh-32342
2024-02-29 17:48:09 +00:00
Juergen Hoeller 701e9e410f Polishing 2024-02-28 21:13:10 +01:00
Juergen Hoeller ce385d136d Consistent nullability for internal field access 2024-02-28 21:12:57 +01:00
Arjen Poutsma e44f84e8ee Restore Jetty 10 compatibility in JettyClientHttpResponse
Closes gh-32337
2024-02-28 13:33:18 +01:00
Sam Brannen 464fa7ea0e Do not cache Content-Type in ContentCachingResponseWrapper
Based on feedback from several members of the community, we have
decided to revert the caching of the Content-Type header that was
introduced in ContentCachingResponseWrapper in 375e0e6827.

This commit therefore completely removes Content-Type caching in
ContentCachingResponseWrapper and updates the existing tests
accordingly.

To provide guards against future regressions in this area, this commit
also introduces explicit tests for the 6 ways to set the content length
in ContentCachingResponseWrapper and modifies a test in
ShallowEtagHeaderFilterTests to ensure that a Content-Type header set
directly on ContentCachingResponseWrapper is propagated to the
underlying response even if content caching is disabled for the
ShallowEtagHeaderFilter.

See gh-32039
See gh-32317
Closes gh-32322
2024-02-28 11:13:59 +01:00
Sam Brannen 6e1f583c06 Polish ShallowEtagHeaderFilterTests 2024-02-28 11:06:55 +01:00
Sébastien Deleuze 57646a092e Refine *HttpMessageConverter#getContentLength null safety
Closes gh-32332
2024-02-27 15:50:06 +01:00
Sam Brannen 380f5d5ea4 Honor Content-[Type|Length] headers from wrapped response again
Commit 375e0e6827 introduced a regression in
ContentCachingResponseWrapper (CCRW). Specifically, CCRW no longer
honors Content-Type and Content-Length headers that have been set in
the wrapped response and now incorrectly returns null for those header
values if they have not been set directly in the CCRW.

This commit fixes this regression as follows.

- The Content-Type and Content-Length headers set in the wrapped
  response are honored in getContentType(), containsHeader(),
  getHeader(), and getHeaders() unless those headers have been set
  directly in the CCRW.

- In copyBodyToResponse(), the Content-Type in the wrapped response is
  only overridden if the Content-Type has been set directly in the CCRW.

Furthermore, prior to this commit, getHeaderNames() returned duplicates
for the Content-Type and Content-Length headers if they were set in the
wrapped response as well as in CCRW.

This commit fixes that by returning a unique set from getHeaderNames().

This commit also updates ContentCachingResponseWrapperTests to verify
the expected behavior for Content-Type and Content-Length headers that
are set in the wrapped response as well as in CCRW.

See gh-32039
See gh-32317
Closes gh-32322
2024-02-25 18:08:12 +01:00
Sam Brannen 1acc1c3a27 Polish ContentCachingResponseWrapper[Tests] 2024-02-25 18:02:59 +01:00
Spring Builds 6ad75bdb4a Next development version (v5.3.33-SNAPSHOT) 2024-02-15 13:40:21 +00:00
91 changed files with 2380 additions and 771 deletions
@@ -0,0 +1,33 @@
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
+34
View File
@@ -0,0 +1,34 @@
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"
@@ -0,0 +1,64 @@
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) }}
+82
View File
@@ -0,0 +1,82 @@
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@v3
- uses: gradle/wrapper-validation-action@v1
- uses: actions/checkout@v4
- uses: gradle/wrapper-validation-action@v2
+1 -1
View File
@@ -1,4 +1,4 @@
# <img src="src/docs/spring-framework.png" width="80" height="80"> Spring Framework [![Build Status](https://ci.spring.io/api/v1/teams/spring-framework/pipelines/spring-framework-5.3.x/jobs/build/badge)](https://ci.spring.io/teams/spring-framework/pipelines/spring-framework-5.3.x?groups=Build") [![Revved up by Gradle Enterprise](https://img.shields.io/badge/Revved%20up%20by-Gradle%20Enterprise-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.spring.io/scans?search.rootProjectNames=spring)
# <img src="framework-docs/src/docs/spring-framework.png" width="80" height="80"> Spring Framework [![Build Status](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml/badge.svg?branch=5.3.x)](https://github.com/spring-projects/spring-framework/actions/workflows/build-and-deploy-snapshot.yml?query=branch%3A5.3.x) [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](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".
+7 -5
View File
@@ -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.107.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.41"
mavenBom "io.netty:netty-bom:4.1.108.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.43"
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,8 +375,9 @@ 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/",
"https://www.ehcache.org/apidocs/2.10.4/",
"https://www.quartz-scheduler.org/api/2.3.0/",
// 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.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/",
@@ -388,7 +389,8 @@ 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/",
"https://r2dbc.io/spec/0.8.5.RELEASE/api/",
// Temporarily commenting out R2DBC since javadoc on JDK 8 cannot access it.
// "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
+2
View File
@@ -1,5 +1,7 @@
== 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].
+1 -1
View File
@@ -17,4 +17,4 @@ changelog:
- "type: dependency-upgrade"
contributors:
exclude:
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll"]
names: ["bclozel", "jhoeller", "poutsma", "rstoyanchev", "sbrannen", "sdeleuze", "snicoll", "simonbasle"]
+1 -1
View File
@@ -1,4 +1,4 @@
FROM ubuntu:jammy-20240111
FROM ubuntu:jammy-20240125
ADD setup.sh /setup.sh
ADD get-jdk-url.sh /get-jdk-url.sh
-1
View File
@@ -8,4 +8,3 @@ milestone: "5.3.x"
build-name: "spring-framework"
pipeline-name: "spring-framework"
concourse-url: "https://ci.spring.io"
task-timeout: 1h00m
+13 -119
View File
@@ -23,14 +23,6 @@ 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
@@ -45,7 +37,7 @@ resource_types:
source:
<<: *docker-resource-source
repository: concourse/registry-image-resource
tag: 1.7.1
tag: 1.8.0
- name: artifactory-resource
type: registry-image
source:
@@ -64,25 +56,12 @@ 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
@@ -105,27 +84,6 @@ 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
@@ -160,37 +118,23 @@ jobs:
- put: ci-image
params:
image: ci-image/image.tar
- name: build
- name: stage-milestone
serial: true
public: true
plan:
- get: ci-image
- get: git-repo
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" }
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
signing_key: ((signing-key))
signing_passphrase: ((signing-passphrase))
repo: libs-snapshot-local
repo: libs-staging-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}"
@@ -215,55 +159,9 @@ jobs:
- "/**/spring-*-schema.zip"
properties:
"zip.type": "schema"
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
- put: git-repo
params:
repository: stage-git-repo
- name: promote-milestone
serial: true
plan:
@@ -304,7 +202,6 @@ jobs:
- put: artifactory-repo
params:
<<: *artifactory-params
repo: libs-staging-local
- put: git-repo
params:
repository: stage-git-repo
@@ -348,7 +245,6 @@ jobs:
- put: artifactory-repo
params:
<<: *artifactory-params
repo: libs-staging-local
- put: git-repo
params:
repository: stage-git-repo
@@ -391,8 +287,6 @@ 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"
-8
View File
@@ -1,8 +0,0 @@
#!/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
-9
View File
@@ -1,9 +0,0 @@
#!/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
-9
View File
@@ -1,9 +0,0 @@
#!/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
View File
@@ -1,6 +1,5 @@
#!/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/' )
+2 -1
View File
@@ -26,4 +26,5 @@ run:
cat > /root/.docker/config.json <<EOF
{ "auths": { "https://index.docker.io/v1/": { "auth": "$DOCKER_HUB_AUTH" }}}
EOF
build
build
-19
View File
@@ -1,19 +0,0 @@
---
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
-22
View File
@@ -1,22 +0,0 @@
---
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
-24
View File
@@ -1,24 +0,0 @@
---
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
+1 -1
View File
@@ -4,7 +4,7 @@ image_resource:
type: registry-image
source:
repository: springio/github-changelog-generator
tag: '0.0.7'
tag: '0.0.8'
username: ((docker-hub-username))
password: ((docker-hub-password))
inputs:
+1 -1
View File
@@ -4,7 +4,7 @@ image_resource:
type: registry-image
source:
repository: springio/concourse-release-scripts
tag: '0.3.4'
tag: '0.4.0'
username: ((docker-hub-username))
password: ((docker-hub-password))
inputs:
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.32-SNAPSHOT
version=5.3.34
org.gradle.jvmargs=-Xmx2048m
org.gradle.caching=true
org.gradle.parallel=true
+1 -1
View File
@@ -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/current/javadoc-api/"))
url.set(new URL("https://docs.spring.io/spring-framework/docs/5.3.x/javadoc-api/"))
}
externalDocumentationLink {
url.set(new URL("https://projectreactor.io/docs/core/release/api/"))
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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.
* May be {@code null} or may not even implement the method.
* @param targetClass the target class for the current invocation
* (can be {@code null} or may not even implement the method)
* @return the specific target method, or the original method if the
* {@code targetClass} doesn't implement it or is {@code null}
* {@code targetClass} does not implement it
* @see org.springframework.util.ClassUtils#getMostSpecificMethod
*/
public static Method getMostSpecificMethod(Method method, @Nullable Class<?> targetClass) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,29 +17,35 @@
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
*/
public class AopUtilsTests {
class AopUtilsTests {
@Test
public void testPointcutCanNeverApply() {
void testPointcutCanNeverApply() {
class TestPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, @Nullable Class<?> clazzy) {
@@ -52,13 +58,13 @@ public class AopUtilsTests {
}
@Test
public void testPointcutAlwaysApplies() {
void testPointcutAlwaysApplies() {
assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), Object.class)).isTrue();
assertThat(AopUtils.canApply(new DefaultPointcutAdvisor(new NopInterceptor()), TestBean.class)).isTrue();
}
@Test
public void testPointcutAppliesToOneMethodOnObject() {
void testPointcutAppliesToOneMethodOnObject() {
class TestPointcut extends StaticMethodMatcherPointcut {
@Override
public boolean matches(Method method, @Nullable Class<?> clazz) {
@@ -78,7 +84,7 @@ public class AopUtilsTests {
* that's subverted the singleton construction limitation.
*/
@Test
public void testCanonicalFrameworkClassesStillCanonicalOnDeserialization() throws Exception {
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);
@@ -88,4 +94,45 @@ public 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) {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -166,6 +166,9 @@ 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;
@@ -180,8 +183,6 @@ 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.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -600,13 +600,10 @@ 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,
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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> dependencies;
Set<String> dependentBeanNames;
synchronized (this.dependentBeanMap) {
// Within full synchronization in order to guarantee a disconnected Set
dependencies = this.dependentBeanMap.remove(beanName);
dependentBeanNames = this.dependentBeanMap.remove(beanName);
}
if (dependencies != null) {
if (dependentBeanNames != null) {
if (logger.isTraceEnabled()) {
logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependencies);
logger.trace("Retrieved dependent beans for bean '" + beanName + "': " + dependentBeanNames);
}
for (String dependentBeanName : dependencies) {
for (String dependentBeanName : dependentBeanNames) {
destroySingleton(dependentBeanName);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -72,7 +72,6 @@ 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");
@@ -86,10 +85,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 or {@code null}
* @param importedBy the configuration class importing this one
* @since 3.1.1
*/
ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) {
ConfigurationClass(MetadataReader metadataReader, ConfigurationClass importedBy) {
this.metadata = metadataReader.getAnnotationMetadata();
this.resource = metadataReader.getResource();
this.importedBy.add(importedBy);
@@ -99,7 +98,6 @@ 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");
@@ -113,10 +111,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 (or {@code null})
* @param importedBy the configuration class importing this one
* @since 3.1.1
*/
ConfigurationClass(Class<?> clazz, @Nullable ConfigurationClass importedBy) {
ConfigurationClass(Class<?> clazz, ConfigurationClass importedBy) {
this.metadata = AnnotationMetadata.introspect(clazz);
this.resource = new DescriptiveResource(clazz.getName());
this.importedBy.add(importedBy);
@@ -126,7 +124,6 @@ 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");
@@ -148,12 +145,12 @@ final class ConfigurationClass {
return ClassUtils.getShortName(getMetadata().getClassName());
}
void setBeanName(String beanName) {
void setBeanName(@Nullable String beanName) {
this.beanName = beanName;
}
@Nullable
public String getBeanName() {
String getBeanName() {
return this.beanName;
}
@@ -163,7 +160,7 @@ final class ConfigurationClass {
* @since 3.1.1
* @see #getImportedBy()
*/
public boolean isImported() {
boolean isImported() {
return !this.importedBy.isEmpty();
}
@@ -197,6 +194,10 @@ 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);
}
@@ -205,10 +206,6 @@ 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());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -136,17 +136,6 @@ 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.
@@ -167,6 +156,18 @@ 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.
@@ -570,7 +571,6 @@ 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,8 +774,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
/**
* Initialize the MessageSource.
* Use parent's if none defined in this context.
* Initialize the {@link MessageSource}.
* <p>Uses parent's {@code MessageSource} if none defined in this context.
* @see #MESSAGE_SOURCE_BEAN_NAME
*/
protected void initMessageSource() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
@@ -807,8 +808,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
/**
* Initialize the ApplicationEventMulticaster.
* Uses SimpleApplicationEventMulticaster if none defined in the context.
* Initialize the {@link ApplicationEventMulticaster}.
* <p>Uses {@link SimpleApplicationEventMulticaster} if none defined in the context.
* @see #APPLICATION_EVENT_MULTICASTER_BEAN_NAME
* @see org.springframework.context.event.SimpleApplicationEventMulticaster
*/
protected void initApplicationEventMulticaster() {
@@ -831,15 +833,16 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
/**
* Initialize the LifecycleProcessor.
* Uses DefaultLifecycleProcessor if none defined in the context.
* Initialize the {@link LifecycleProcessor}.
* <p>Uses {@link DefaultLifecycleProcessor} if none defined in the context.
* @since 3.0
* @see #LIFECYCLE_PROCESSOR_BEAN_NAME
* @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 + "]");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -133,11 +133,6 @@ 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) {
@@ -178,11 +173,10 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche
}
private TaskExecutorAdapter getAdaptedExecutor(Executor concurrentExecutor) {
if (managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(concurrentExecutor)) {
return new ManagedTaskExecutorAdapter(concurrentExecutor);
}
TaskExecutorAdapter adapter = new TaskExecutorAdapter(concurrentExecutor);
private TaskExecutorAdapter getAdaptedExecutor(Executor originalExecutor) {
TaskExecutorAdapter adapter =
(managedExecutorServiceClass != null && managedExecutorServiceClass.isInstance(originalExecutor) ?
new ManagedTaskExecutorAdapter(originalExecutor) : new TaskExecutorAdapter(originalExecutor));
if (this.taskDecorator != null) {
adapter.setTaskDecorator(this.taskDecorator);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -177,6 +177,7 @@ 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;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,8 +45,9 @@ import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureTask;
/**
* Implementation of Spring's {@link TaskScheduler} interface, wrapping
* a native {@link java.util.concurrent.ScheduledThreadPoolExecutor}.
* 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.
*
* @author Juergen Hoeller
* @author Mark Fisher
@@ -154,6 +155,7 @@ 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,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -268,6 +268,7 @@ 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())
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 RunnableFuture) {
((RunnableFuture<?>) task).cancel(true);
if (task instanceof Future) {
((Future<?>) task).cancel(true);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -39,7 +39,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* @author Sam Brannen
* @since 3.0
*/
public class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests {
class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests {
private final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
@@ -97,7 +97,7 @@ public class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutor
}
@Test
void scheduleOneTimeFailingTaskWithoutErrorHandler() throws Exception {
void scheduleOneTimeFailingTaskWithoutErrorHandler() {
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 @@ public class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutor
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-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import org.springframework.util.ReflectionUtils;
*
* @author Juergen Hoeller
* @author Rossen Stoyanchev
* @author Sam Brannen
* @since 4.2.3
*/
public final class MethodIntrospector {
@@ -75,6 +76,7 @@ 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-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -98,7 +98,9 @@ public final class ReactiveTypeDescriptor {
*/
public Object getEmptyValue() {
Assert.state(this.emptySupplier != null, "Empty values not supported");
return this.emptySupplier.get();
Object emptyValue = this.emptySupplier.get();
Assert.notNull(emptyValue, "Invalid null return value from emptySupplier");
return emptyValue;
}
/**
@@ -130,7 +132,7 @@ public final class ReactiveTypeDescriptor {
/**
* Descriptor for a reactive type that can produce 0..N values.
* Descriptor for a reactive type that can produce {@code 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-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -95,20 +95,19 @@ 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) {
failure = ex.getCause();
Throwable cause = ex.getCause();
throw (cause instanceof CodecException ? (CodecException) cause :
new DecodingException("Failed to decode: " + (cause != null ? cause.getMessage() : ex), cause));
}
catch (InterruptedException ex) {
failure = ex;
throw new DecodingException("Interrupted during decode", ex);
}
throw (failure instanceof CodecException ? (CodecException) failure :
new DecodingException("Failed to decode: " + failure.getMessage(), failure));
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -76,10 +76,11 @@ 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;
}
@@ -92,6 +93,7 @@ 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-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -52,8 +52,6 @@ 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 = {
@@ -84,7 +82,7 @@ public class TypeDescriptor implements Serializable {
public TypeDescriptor(MethodParameter methodParameter) {
this.resolvableType = ResolvableType.forMethodParameter(methodParameter);
this.type = this.resolvableType.resolve(methodParameter.getNestedParameterType());
this.annotatedElement = new AnnotatedElementAdapter(methodParameter.getParameterIndex() == -1 ?
this.annotatedElement = AnnotatedElementAdapter.from(methodParameter.getParameterIndex() == -1 ?
methodParameter.getMethodAnnotations() : methodParameter.getParameterAnnotations());
}
@@ -96,7 +94,7 @@ public class TypeDescriptor implements Serializable {
public TypeDescriptor(Field field) {
this.resolvableType = ResolvableType.forField(field);
this.type = this.resolvableType.resolve(field.getType());
this.annotatedElement = new AnnotatedElementAdapter(field.getAnnotations());
this.annotatedElement = AnnotatedElementAdapter.from(field.getAnnotations());
}
/**
@@ -109,7 +107,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 = new AnnotatedElementAdapter(property.getAnnotations());
this.annotatedElement = AnnotatedElementAdapter.from(property.getAnnotations());
}
/**
@@ -125,7 +123,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 = new AnnotatedElementAdapter(annotations);
this.annotatedElement = AnnotatedElementAdapter.from(annotations);
}
@@ -734,18 +732,26 @@ public class TypeDescriptor implements Serializable {
* @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class)
* @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class)
*/
private class AnnotatedElementAdapter implements AnnotatedElement, Serializable {
private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable {
private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]);
@Nullable
private final Annotation[] annotations;
public AnnotatedElementAdapter(@Nullable Annotation[] annotations) {
private AnnotatedElementAdapter(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 : getAnnotations()) {
for (Annotation annotation : this.annotations) {
if (annotation.annotationType() == annotationClass) {
return true;
}
@@ -757,7 +763,7 @@ public class TypeDescriptor implements Serializable {
@Nullable
@SuppressWarnings("unchecked")
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
for (Annotation annotation : getAnnotations()) {
for (Annotation annotation : this.annotations) {
if (annotation.annotationType() == annotationClass) {
return (T) annotation;
}
@@ -767,7 +773,7 @@ public class TypeDescriptor implements Serializable {
@Override
public Annotation[] getAnnotations() {
return (this.annotations != null ? this.annotations.clone() : EMPTY_ANNOTATION_ARRAY);
return (isEmpty() ? this.annotations : this.annotations.clone());
}
@Override
@@ -776,7 +782,7 @@ public class TypeDescriptor implements Serializable {
}
public boolean isEmpty() {
return ObjectUtils.isEmpty(this.annotations);
return (this.annotations.length == 0);
}
@Override
@@ -792,7 +798,7 @@ public class TypeDescriptor implements Serializable {
@Override
public String toString() {
return TypeDescriptor.this.toString();
return Arrays.toString(this.annotations);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -138,7 +138,7 @@ final class ObjectToObjectConverter implements ConditionalGenericConverter {
@Nullable
private static Executable getValidatedExecutable(Class<?> targetClass, Class<?> sourceClass) {
Executable executable = conversionExecutableCache.get(targetClass);
if (isApplicable(executable, sourceClass)) {
if (executable != null && isApplicable(executable, sourceClass)) {
return executable;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,7 +62,7 @@ import org.springframework.util.Assert;
*/
public abstract class DataBufferUtils {
private final static Log logger = LogFactory.getLog(DataBufferUtils.class);
private static final 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 SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10});
static final 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 static abstract class AbstractNestedMatcher implements NestedMatcher {
private abstract static class AbstractNestedMatcher implements NestedMatcher {
private final byte[] delimiter;
@@ -1005,11 +1005,11 @@ public abstract class DataBufferUtils {
}
@Override
public void failed(Throwable exc, DataBuffer dataBuffer) {
public void failed(Throwable ex, DataBuffer dataBuffer) {
release(dataBuffer);
closeChannel(this.channel);
this.state.set(State.DISPOSED);
this.sink.error(exc);
this.sink.error(ex);
}
private enum State {
@@ -1064,7 +1064,6 @@ public abstract class DataBufferUtils {
public Context currentContext() {
return Context.of(this.sink.contextView());
}
}
@@ -1145,9 +1144,9 @@ public abstract class DataBufferUtils {
}
@Override
public void failed(Throwable exc, ByteBuffer byteBuffer) {
public void failed(Throwable ex, ByteBuffer byteBuffer) {
sinkDataBuffer();
this.sink.error(exc);
this.sink.error(ex);
}
private void sinkDataBuffer() {
@@ -1161,7 +1160,6 @@ public abstract class DataBufferUtils {
public Context currentContext() {
return Context.of(this.sink.contextView());
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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<>(16);
Set<Resource> result = new LinkedHashSet<>(64);
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<>(8);
Set<Resource> result = new LinkedHashSet<>(64);
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<>();
private final Set<Resource> resources = new LinkedHashSet<>(64);
public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher pathMatcher) {
this.subPattern = subPattern;
@@ -895,7 +895,6 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
else if ("toString".equals(methodName)) {
return toString();
}
throw new IllegalStateException("Unexpected method invocation: " + method);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
/**
* Default "no op" {@code ApplicationStartup} implementation.
*
@@ -52,6 +54,7 @@ class DefaultApplicationStartup implements ApplicationStartup {
}
@Override
@Nullable
public Long getParentId() {
return null;
}
@@ -73,7 +76,6 @@ class DefaultApplicationStartup implements ApplicationStartup {
@Override
public void end() {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-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.
@@ -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;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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.booleanValue()) {
if (superClassMatch) {
return true;
}
}
else {
// Need to read superclass to determine a match...
try {
if (match(metadata.getSuperClassName(), metadataReaderFactory)) {
if (match(superClassName, metadataReaderFactory)) {
return true;
}
}
catch (IOException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Could not read superclass [" + metadata.getSuperClassName() +
logger.debug("Could not read superclass [" + superClassName +
"] 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.booleanValue()) {
if (interfaceMatch) {
return true;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,7 +44,8 @@ import org.springframework.lang.Nullable;
/**
* Miscellaneous {@code java.lang.Class} utility methods.
* Mainly for internal use within the framework.
*
* <p>Mainly for internal use within the framework.
*
* @author Juergen Hoeller
* @author Keith Donald
@@ -243,7 +244,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
* (may be {@code null}, which indicates the default class loader)
* (can 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
@@ -314,7 +315,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
* (may be {@code null}, which indicates the default class loader)
* (can 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)
@@ -348,7 +349,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
* (may be {@code null} which indicates the default class loader)
* (can 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
@@ -375,7 +376,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
* (may be {@code null} in which case this method will always return {@code true})
* (can 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) {
@@ -399,7 +400,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
* (may be {@code null} which indicates the system class loader)
* (can 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");
@@ -539,9 +540,10 @@ 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
* @param rhsType the value type that should be assigned to the target type
* @return if the target type is assignable from the value type
* @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}
* @see TypeUtils#isAssignable(java.lang.reflect.Type, java.lang.reflect.Type)
*/
public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
@@ -662,7 +664,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 (may be {@code null})
* @param classes a Collection of Class objects (can be {@code null})
* @return a String of form "[com.foo.Bar, com.foo.Baz]"
* @see java.util.AbstractCollection#toString()
*/
@@ -717,7 +719,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
* (may be {@code null} when accepting all declared interfaces)
* (can 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) {
@@ -752,7 +754,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
* (may be {@code null} when accepting all declared interfaces)
* (can 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) {
@@ -866,9 +868,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) {
@@ -878,8 +880,9 @@ public abstract class ClassUtils {
/**
* Check whether the specified class is a CGLIB-generated class.
* @param clazz the class to check
* @see #isCglibProxyClassName(String)
* @see #getUserClass(Class)
* @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) {
@@ -889,7 +892,9 @@ 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) {
@@ -913,6 +918,7 @@ 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)) {
@@ -1065,7 +1071,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
* (may be {@code null} to indicate the method's declaring class)
* (can be {@code null} to indicate the method's declaring class)
* @return the qualified name of the method
* @since 4.3.4
*/
@@ -1146,7 +1152,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
* (may be {@code null} to indicate any signature)
* (can 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
@@ -1185,7 +1191,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
* (may be {@code null} to indicate any signature)
* (can be {@code null} to indicate any signature)
* @return the method, or {@code null} if not found
* @see Class#getMethod
*/
@@ -1261,26 +1267,27 @@ 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. E.g. 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 &mdash; for example, 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 (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.
* if bridge method resolution is desirable &mdash; for example, to obtain
* metadata from the original method definition.
* <p><b>NOTE:</b> If Java security settings disallow reflective access &mdash;
* for example, calls to {@code Class#getDeclaredMethods}, etc. &mdash; 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
* (may be {@code null} or may not even implement the method)
* (can 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)) {
if (targetClass != null && targetClass != method.getDeclaringClass() &&
(isOverridable(method, targetClass) || !method.getDeclaringClass().isAssignableFrom(targetClass))) {
try {
if (Modifier.isPublic(method.getModifiers())) {
try {
@@ -0,0 +1,112 @@
/*
* 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() {
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 resouce.getURL().getPath(). They would also fail on Mac OS when
// using resouce.getURI().getPath() if the resource paths are not Unicode normalized.
// 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.
//
// 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
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 columns.
* for example when expecting a single column but getting 0 or more than 1 column.
*
* @author Juergen Hoeller
* @since 2.0
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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's an
* Typically, we expect an update to affect a single row, meaning it is an
* error if it affects multiple rows.
*
* @author Rod Johnson
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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, avoiding to read and hold
* the entire result set in the database or in the JDBC driver.
* 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.
* <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 updateableResults flag must be set before the operation is compiled");
"The updatableResults flag must be set before the operation is compiled");
}
this.updatableResults = updatableResults;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,9 +25,9 @@ import org.apache.commons.logging.LogFactory;
import org.springframework.lang.Nullable;
/**
* 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".
* 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".
*
* @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-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 (AbstractMethodError err) {
logger.debug("JDBC driver does not implement JDBC 4.1 'getObject(int, Class)' method", err);
}
catch (SQLFeatureNotSupportedException ex) {
catch (SQLFeatureNotSupportedException | AbstractMethodError ex) {
logger.debug("JDBC driver does not support JDBC 4.1 'getObject(int, Class)' method", ex);
}
catch (SQLException ex) {
logger.debug("JDBC driver has limited support for JDBC 4.1 'getObject(int, Class)' method", ex);
if (logger.isDebugEnabled()) {
logger.debug("JDBC driver has limited support for 'getObject(int, Class)' with column type: " +
requiredType.getName(), 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 2.0 batch updates.
* Return whether the given JDBC driver supports JDBC 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 2.0 batch mechanism or simply in a traditional one-by-one fashion.
* the JDBC 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 2.0 batch updates are supported
* @return whether JDBC 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 implementation takes into account recent clarifications
* expressed in the JDBC 4.0 specification:
* <p>This method's implementation takes into account 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
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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,6 +101,7 @@ public class SQLErrorCodesFactory {
* @see #loadResource(String)
*/
protected SQLErrorCodesFactory() {
Map<String, SQLErrorCodes> errorCodes;
try {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -217,7 +217,7 @@ public abstract class JmsUtils {
try {
session.commit();
}
catch (javax.jms.TransactionInProgressException | javax.jms.IllegalStateException ex) {
catch (javax.jms.TransactionInProgressException 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 | javax.jms.IllegalStateException ex) {
catch (javax.jms.TransactionInProgressException ex) {
// Ignore -> can only happen in case of a JTA transaction.
}
}
@@ -253,8 +253,9 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
* @see javax.persistence.spi.PersistenceUnitInfo#getNonJtaDataSource()
* @see #setPersistenceUnitManager
*/
public void setDataSource(DataSource dataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(dataSource));
public void setDataSource(@Nullable DataSource dataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(
dataSource != null ? new SingleDataSourceLookup(dataSource) : null);
this.internalPersistenceUnitManager.setDefaultDataSource(dataSource);
}
@@ -270,8 +271,9 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
* @see javax.persistence.spi.PersistenceUnitInfo#getJtaDataSource()
* @see #setPersistenceUnitManager
*/
public void setJtaDataSource(DataSource jtaDataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(new SingleDataSourceLookup(jtaDataSource));
public void setJtaDataSource(@Nullable DataSource jtaDataSource) {
this.internalPersistenceUnitManager.setDataSourceLookup(
jtaDataSource != null ? new SingleDataSourceLookup(jtaDataSource) : null);
this.internalPersistenceUnitManager.setDefaultJtaDataSource(jtaDataSource);
}
@@ -416,6 +418,7 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
}
@Override
@Nullable
public DataSource getDataSource() {
if (this.persistenceUnitInfo != null) {
return (this.persistenceUnitInfo.getJtaDataSource() != null ?
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,28 +28,18 @@ 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 is just a matter of configuration!
* {@link LocalContainerEntityManagerFactoryBean} definition based on the
* JPA container contract 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. 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.
* 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}.
*
* <p><b>Note:</b> This FactoryBean has limited configuration power in terms of
* 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
* 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
* {@link LocalContainerEntityManagerFactoryBean} instead.
*
* <p><b>NOTE: Spring's JPA support requires JPA 2.1 or higher, as of Spring 5.0.</b>
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -70,13 +70,15 @@ public class SingleConnectionFactory extends DelegatingConnectionFactory
private boolean suppressClose;
/** Override auto-commit state?. */
private @Nullable Boolean autoCommit;
@Nullable
private Boolean autoCommit;
/** Wrapped Connection. */
private final AtomicReference<Connection> target = new AtomicReference<>();
/** Proxy Connection. */
private @Nullable Connection connection;
@Nullable
private Connection connection;
private final Mono<? extends Connection> connectionEmitter;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -60,7 +60,6 @@ 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;
@@ -990,7 +989,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
}
}
private static String encodeCookies(@NonNull Cookie... cookies) {
private static String encodeCookies(Cookie... cookies) {
return Arrays.stream(cookies)
.map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue()))
.collect(Collectors.joining("; "));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,7 +31,6 @@ 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;
@@ -280,7 +279,6 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen
}
}
@NonNull
private ResourceDatabasePopulator createDatabasePopulator(MergedSqlConfig mergedSqlConfig) {
ResourceDatabasePopulator populator = new ResourceDatabasePopulator();
populator.setSqlScriptEncoding(mergedSqlConfig.getEncoding());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,10 +44,11 @@ public class IncorrectUpdateSemanticsDataAccessException extends InvalidDataAcce
super(msg, cause);
}
/**
* Return whether data was updated.
* If this method returns false, there's nothing to roll back.
* <p>The default implementation always returns true.
* If this method returns {@code false}, there is nothing to roll back.
* <p>The default implementation always returns {@code true}.
* This can be overridden in subclasses.
*/
public boolean wasDataUpdated() {
@@ -21,7 +21,6 @@ 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;
@@ -55,23 +54,29 @@ class JettyClientHttpResponse extends AbstractClientHttpResponse {
}
private static HttpHeaders adaptHeaders(ReactiveResponse response) {
MultiValueMap<String, String> headers = new JettyHeadersAdapter(response.getHeaders());
MultiValueMap<String, String> headers = (Jetty10HttpFieldsHelper.jetty10Present() ?
Jetty10HttpFieldsHelper.getHttpHeaders(response.getResponse()) :
new JettyHeadersAdapter(response.getHeaders()));
return HttpHeaders.readOnlyHttpHeaders(headers);
}
private static MultiValueMap<String, ResponseCookie> adaptCookies(ReactiveResponse response) {
MultiValueMap<String, ResponseCookie> result = new LinkedMultiValueMap<>();
List<HttpField> cookieHeaders = response.getHeaders().getFields(HttpHeaders.SET_COOKIE);
cookieHeaders.forEach(header ->
HttpCookie.parse(header.getValue()).forEach(cookie -> result.add(cookie.getName(),
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(),
ResponseCookie.fromClientResponse(cookie.getName(), cookie.getValue())
.domain(cookie.getDomain())
.path(cookie.getPath())
.maxAge(cookie.getMaxAge())
.secure(cookie.getSecure())
.httpOnly(cookie.isHttpOnly())
.sameSite(parseSameSite(header.getValue()))
.sameSite(parseSameSite(header))
.build()))
);
}
return CollectionUtils.unmodifiableMultiValueMap(result);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -112,6 +112,7 @@ 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,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -519,6 +519,7 @@ 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();
@@ -0,0 +1,44 @@
/*
* 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);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,22 +17,28 @@
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.concurrent.atomic.AtomicBoolean;
import java.util.Locale;
import java.util.concurrent.locks.ReentrantLock;
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 3.0 implementation of {@link AsyncWebRequest}.
* A Servlet 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
@@ -44,18 +50,22 @@ 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.
@@ -63,7 +73,26 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
* @param response current HTTP response
*/
public StandardServletAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
super(request, 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);
}
@@ -104,7 +133,7 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
*/
@Override
public boolean isAsyncComplete() {
return this.asyncCompleted.get();
return (this.state == State.COMPLETED);
}
@Override
@@ -114,11 +143,18 @@ 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) {
@@ -128,8 +164,10 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Override
public void dispatch() {
Assert.state(this.asyncContext != null, "Cannot dispatch without an AsyncContext");
this.asyncContext.dispatch();
Assert.state(this.asyncContext != null, "AsyncContext not yet initialized");
if (!this.isAsyncComplete()) {
this.asyncContext.dispatch();
}
}
@@ -148,14 +186,516 @@ public class StandardServletAsyncWebRequest extends ServletWebRequest implements
@Override
public void onError(AsyncEvent event) throws IOException {
this.exceptionHandlers.forEach(consumer -> consumer.accept(event.getThrowable()));
this.stateLock.lock();
try {
this.state = State.ERROR;
Throwable ex = event.getThrowable();
this.exceptionHandlers.forEach(consumer -> consumer.accept(ex));
}
finally {
this.stateLock.unlock();
}
}
@Override
public void onComplete(AsyncEvent event) throws IOException {
this.completionHandlers.forEach(Runnable::run);
this.asyncContext = null;
this.asyncCompleted.set(true);
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
}
}
@@ -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.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletRequest;
@@ -89,12 +89,7 @@ public final class WebAsyncManager {
@Nullable
private volatile Object[] concurrentResultContext;
/*
* 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 AtomicReference<State> state = new AtomicReference<>(State.NOT_STARTED);
private final Map<Object, CallableProcessingInterceptor> callableInterceptors = new LinkedHashMap<>();
@@ -126,6 +121,15 @@ 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...)}.
@@ -136,8 +140,8 @@ public final class WebAsyncManager {
}
/**
* Whether the selected handler for the current request chose to handle the
* request asynchronously. A return value of "true" indicates concurrent
* Return 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
@@ -148,16 +152,16 @@ public final class WebAsyncManager {
}
/**
* Whether a result value exists as a result of concurrent handling.
* Return whether a result value exists as a result of concurrent handling.
*/
public boolean hasConcurrentResult() {
return (this.concurrentResult != RESULT_NONE);
}
/**
* Provides access to the result from concurrent handling.
* Get 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
@@ -166,8 +170,7 @@ public final class WebAsyncManager {
}
/**
* Provides access to additional processing context saved at the start of
* concurrent handling.
* Get the additional processing context saved at the start of concurrent handling.
* @see #clearConcurrentResult()
*/
@Nullable
@@ -208,7 +211,7 @@ public final class WebAsyncManager {
/**
* Register a {@link CallableProcessingInterceptor} without a key.
* The key is derived from the class name and hashcode.
* The key is derived from the class name and hash code.
* @param interceptors one or more interceptors to register
*/
public void registerCallableInterceptors(CallableProcessingInterceptor... interceptors) {
@@ -231,8 +234,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) {
@@ -248,6 +251,12 @@ 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;
@@ -288,6 +297,11 @@ 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);
@@ -298,7 +312,7 @@ public final class WebAsyncManager {
this.taskExecutor = executor;
}
else {
logExecutorWarning();
logExecutorWarning(this.asyncWebRequest);
}
List<CallableProcessingInterceptor> interceptors = new ArrayList<>();
@@ -311,7 +325,7 @@ public final class WebAsyncManager {
this.asyncWebRequest.addTimeoutHandler(() -> {
if (logger.isDebugEnabled()) {
logger.debug("Async request timeout for " + formatRequestUri());
logger.debug("Servlet container timeout notification for " + formatUri(this.asyncWebRequest));
}
Object result = interceptorChain.triggerAfterTimeout(this.asyncWebRequest, callable);
if (result != CallableProcessingInterceptor.RESULT_NONE) {
@@ -320,14 +334,12 @@ public final class WebAsyncManager {
});
this.asyncWebRequest.addErrorHandler(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);
if (logger.isDebugEnabled()) {
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest) + ": " + ex);
}
Object result = interceptorChain.triggerAfterError(this.asyncWebRequest, callable, ex);
result = (result != CallableProcessingInterceptor.RESULT_NONE ? result : ex);
setConcurrentResultAndDispatch(result);
});
this.asyncWebRequest.addCompletionHandler(() ->
@@ -352,14 +364,13 @@ public final class WebAsyncManager {
});
interceptorChain.setTaskFuture(future);
}
catch (RejectedExecutionException ex) {
catch (Throwable ex) {
Object result = interceptorChain.applyPostProcess(this.asyncWebRequest, callable, ex);
setConcurrentResultAndDispatch(result);
throw ex;
}
}
private void logExecutorWarning() {
private void logExecutorWarning(AsyncWebRequest asyncWebRequest) {
if (taskExecutorWarning && logger.isWarnEnabled()) {
synchronized (DEFAULT_TASK_EXECUTOR) {
AsyncTaskExecutor executor = this.taskExecutor;
@@ -371,7 +382,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: '" + formatRequestUri() + "'\n" +
"Request URI: '" + formatUri(asyncWebRequest) + "'\n" +
"!!!");
taskExecutorWarning = false;
}
@@ -379,32 +390,35 @@ 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.concurrentResult != RESULT_NONE) {
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));
}
return;
}
this.concurrentResult = result;
this.errorHandlingInProgress = (result instanceof Throwable);
}
if (this.asyncWebRequest.isAsyncComplete()) {
if (logger.isDebugEnabled()) {
logger.debug("Async result set but request already complete: " + formatRequestUri());
logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest));
}
return;
}
if (logger.isDebugEnabled()) {
boolean isError = result instanceof Throwable;
logger.debug("Async " + (isError ? "error" : "result set") + ", dispatch to " + formatRequestUri());
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.asyncWebRequest.dispatch();
}
/**
@@ -427,6 +441,11 @@ 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);
@@ -440,6 +459,9 @@ 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);
}
@@ -449,21 +471,22 @@ public final class WebAsyncManager {
});
this.asyncWebRequest.addErrorHandler(ex -> {
if (!this.errorHandlingInProgress) {
try {
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
return;
}
deferredResult.setErrorResult(ex);
}
catch (Throwable interceptorEx) {
setConcurrentResultAndDispatch(interceptorEx);
if (logger.isDebugEnabled()) {
logger.debug("Servlet container error notification for " + formatUri(this.asyncWebRequest));
}
try {
if (!interceptorChain.triggerAfterError(this.asyncWebRequest, deferredResult, ex)) {
return;
}
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);
@@ -484,13 +507,46 @@ public final class WebAsyncManager {
synchronized (WebAsyncManager.this) {
this.concurrentResult = RESULT_NONE;
this.concurrentResultContext = processingContext;
this.errorHandlingInProgress = false;
}
this.asyncWebRequest.startAsync();
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
if (logger.isDebugEnabled()) {
logger.debug("Started async request");
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
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@ import org.springframework.web.context.request.NativeWebRequest;
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.2
* @param <V> the value type
*/
@@ -37,18 +38,25 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
private final Callable<V> callable;
private Long timeout;
@Nullable
private final Long timeout;
private AsyncTaskExecutor executor;
@Nullable
private final AsyncTaskExecutor executor;
private String executorName;
@Nullable
private final String executorName;
@Nullable
private BeanFactory beanFactory;
@Nullable
private Callable<V> timeoutCallback;
@Nullable
private Callable<V> errorCallback;
@Nullable
private Runnable completionCallback;
@@ -59,6 +67,9 @@ 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;
}
/**
@@ -67,8 +78,11 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
* @param callable the callable for concurrent handling
*/
public WebAsyncTask(long timeout, Callable<V> callable) {
this(callable);
Assert.notNull(callable, "Callable must not be null");
this.callable = callable;
this.timeout = timeout;
this.executor = null;
this.executorName = null;
}
/**
@@ -78,10 +92,12 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
* @param callable the callable for concurrent handling
*/
public WebAsyncTask(@Nullable Long timeout, String executorName, Callable<V> callable) {
this(callable);
Assert.notNull(callable, "Callable must not be null");
Assert.notNull(executorName, "Executor name must not be null");
this.executorName = executorName;
this.callable = callable;
this.timeout = timeout;
this.executor = null;
this.executorName = executorName;
}
/**
@@ -91,10 +107,12 @@ public class WebAsyncTask<V> implements BeanFactoryAware {
* @param callable the callable for concurrent handling
*/
public WebAsyncTask(@Nullable Long timeout, AsyncTaskExecutor executor, Callable<V> callable) {
this(callable);
Assert.notNull(callable, "Callable must not be null");
Assert.notNull(executor, "Executor must not be null");
this.executor = executor;
this.callable = callable;
this.timeout = timeout;
this.executor = executor;
this.executorName = null;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -82,7 +82,10 @@ public abstract class WebAsyncUtils {
* @return an AsyncWebRequest instance (never {@code null})
*/
public static AsyncWebRequest createAsyncWebRequest(HttpServletRequest request, HttpServletResponse response) {
return new StandardServletAsyncWebRequest(request, response);
AsyncWebRequest prev = getAsyncManager(request).getAsyncWebRequest();
return (prev instanceof StandardServletAsyncWebRequest ?
new StandardServletAsyncWebRequest(request, response, (StandardServletAsyncWebRequest) prev) :
new StandardServletAsyncWebRequest(request, response));
}
}
@@ -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.List;
import java.util.LinkedHashSet;
import java.util.Set;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
@@ -38,12 +38,13 @@ 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 {@link #getContentAsByteArray() byte array}.
* and allows this content to be retrieved via a {@linkplain #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
*/
@@ -60,9 +61,6 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Nullable
private Integer contentLength;
@Nullable
private String contentType;
/**
* Create a new ContentCachingResponseWrapper for the given servlet response.
@@ -122,9 +120,16 @@ 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 {
// do not flush the underlying response as the content has not been copied to it yet
// no-op
}
@Override
@@ -142,31 +147,13 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
throw new IllegalArgumentException("Content-Length exceeds ContentCachingResponseWrapper's maximum (" +
Integer.MAX_VALUE + "): " + 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;
setContentLength((int) len);
}
@Override
public boolean containsHeader(String name) {
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return this.contentLength != null;
}
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
return this.contentType != null;
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return true;
}
else {
return super.containsHeader(name);
@@ -178,9 +165,6 @@ 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);
}
@@ -191,9 +175,6 @@ 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);
}
@@ -222,11 +203,8 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
@Nullable
public String getHeader(String name) {
if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return (this.contentLength != null) ? this.contentLength.toString() : null;
}
else if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) {
return this.contentType;
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return this.contentLength.toString();
}
else {
return super.getHeader(name);
@@ -235,12 +213,8 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public Collection<String> getHeaders(String name) {
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();
if (this.contentLength != null && HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) {
return Collections.singleton(this.contentLength.toString());
}
else {
return super.getHeaders(name);
@@ -250,14 +224,9 @@ public class ContentCachingResponseWrapper extends HttpServletResponseWrapper {
@Override
public Collection<String> getHeaderNames() {
Collection<String> headerNames = super.getHeaderNames();
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);
}
if (this.contentLength != null) {
Set<String> result = new LinkedHashSet<>(headerNames);
result.add(HttpHeaders.CONTENT_LENGTH);
return result;
}
else {
@@ -339,10 +308,6 @@ 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,9 +252,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
builder.schemeSpecificPart(ssp);
}
else {
if (StringUtils.hasLength(scheme) && scheme.startsWith("http") && !StringUtils.hasLength(host)) {
throw new IllegalArgumentException("[" + uri + "] is not a valid HTTP URL");
}
checkSchemeAndHost(uri, scheme, host);
builder.userInfo(userInfo);
builder.host(host);
if (StringUtils.hasLength(port)) {
@@ -296,9 +294,7 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
builder.scheme(scheme != null ? scheme.toLowerCase() : null);
builder.userInfo(matcher.group(4));
String host = matcher.group(5);
if (StringUtils.hasLength(scheme) && !StringUtils.hasLength(host)) {
throw new IllegalArgumentException("[" + httpUrl + "] is not a valid HTTP URL");
}
checkSchemeAndHost(httpUrl, scheme, host);
builder.host(host);
String port = matcher.group(7);
if (StringUtils.hasLength(port)) {
@@ -317,6 +313,15 @@ 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
@@ -402,6 +407,7 @@ 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-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,7 +66,7 @@ public class UriTemplate implements Serializable {
* @param uriTemplate the URI template string
*/
public UriTemplate(String uriTemplate) {
Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
Assert.notNull(uriTemplate, "'uriTemplate' must not be null");
this.uriTemplate = uriTemplate;
this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build();
@@ -0,0 +1,358 @@
/*
* 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);
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -96,9 +96,8 @@ 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("Async processing has already completed");
assertThatIllegalStateException().isThrownBy(this.asyncRequest::startAsync)
.withMessage("Cannot start async: [COMPLETED]");
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,55 +16,255 @@
package org.springframework.web.filter;
import java.nio.charset.StandardCharsets;
import java.util.stream.Stream;
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.HttpHeaders;
import org.springframework.http.MediaType;
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
*/
public class ContentCachingResponseWrapperTests {
class ContentCachingResponseWrapperTests {
@Test
void copyBodyToResponse() throws Exception {
byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
MockHttpServletResponse response = new MockHttpServletResponse();
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setStatus(HttpServletResponse.SC_OK);
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
responseWrapper.copyBodyToResponse();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getContentLength() > 0).isTrue();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
assertThat(response.getContentLength()).isGreaterThan(0);
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(StandardCharsets.UTF_8);
byte[] responseBody = "6\r\nHello 5\r\nWorld0\r\n\r\n".getBytes(UTF_8);
MockHttpServletResponse response = new MockHttpServletResponse();
ContentCachingResponseWrapper responseWrapper = new ContentCachingResponseWrapper(response);
responseWrapper.setStatus(HttpServletResponse.SC_OK);
responseWrapper.setHeader(HttpHeaders.TRANSFER_ENCODING, "chunked");
responseWrapper.setStatus(HttpServletResponse.SC_CREATED);
responseWrapper.setHeader(TRANSFER_ENCODING, "chunked");
FileCopyUtils.copy(responseBody, responseWrapper.getOutputStream());
responseWrapper.copyBodyToResponse();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getHeader(HttpHeaders.TRANSFER_ENCODING)).isEqualTo("chunked");
assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isNull();
assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_CREATED);
assertHeader(response, TRANSFER_ENCODING, "chunked");
assertHeader(response, CONTENT_LENGTH, null);
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);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,33 +16,36 @@
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
*/
public class ShallowEtagHeaderFilterTests {
class ShallowEtagHeaderFilterTests {
private final ShallowEtagHeaderFilter filter = new ShallowEtagHeaderFilter();
@Test
public void isEligibleForEtag() {
void isEligibleForEtag() {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
@@ -61,60 +64,60 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
public void filterNoMatch() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterNoMatch() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
filterResponse.setContentType(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() > 0).as("Invalid Content-Length header").isTrue();
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody);
}
@Test
public void filterNoMatchWeakETag() throws Exception {
void filterNoMatchWeakETag() throws Exception {
this.filter.setWriteWeakETag(true);
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
((HttpServletResponse) filterResponse).setStatus(HttpServletResponse.SC_OK);
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
filterResponse.setContentType(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() > 0).as("Invalid Content-Length header").isTrue();
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(MediaType.TEXT_PLAIN_VALUE);
assertThat(response.getContentLength()).as("Invalid Content-Length header").isGreaterThan(0);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(responseBody);
}
@Test
public void filterMatch() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterMatch() throws Exception {
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(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
filterResponse.setContentLength(responseBody.length);
filterResponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
filterResponse.setContentType(TEXT_PLAIN_VALUE);
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
};
filter.doFilter(request, response, filterChain);
@@ -122,21 +125,20 @@ public 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.containsHeader("Content-Type")).as("Response has Content-Type header").isFalse();
byte[] expecteds = new byte[0];
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
assertThat(response.getContentType()).as("Invalid Content-Type header").isEqualTo(TEXT_PLAIN_VALUE);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
}
@Test
public void filterMatchWeakEtag() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterMatchWeakEtag() throws Exception {
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(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FileCopyUtils.copy(responseBody, filterResponse.getOutputStream());
filterResponse.setContentLength(responseBody.length);
};
@@ -145,13 +147,12 @@ public 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();
byte[] expecteds = new byte[0];
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
}
@Test
public void filterWriter() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterWriter() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
String etag = "\"0b10a8db164e0754105b7a99be72e3fe5\"";
request.addHeader("If-None-Match", etag);
MockHttpServletResponse response = new MockHttpServletResponse();
@@ -167,19 +168,20 @@ public 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();
byte[] expecteds = new byte[0];
assertThat(response.getContentAsByteArray()).as("Invalid content").isEqualTo(expecteds);
assertThat(response.getContentAsByteArray()).as("Invalid content").isEmpty();
}
@Test // SPR-12960
public void filterWriterWithDisabledCaching() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterWriterWithDisabledCaching() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
response.setContentType(TEXT_PLAIN_VALUE);
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(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());
};
@@ -188,15 +190,16 @@ public 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
public void filterSendError() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterSendError() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
response.setContentLength(100);
@@ -212,11 +215,11 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
public void filterSendErrorMessage() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterSendErrorMessage() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
response.setContentLength(100);
@@ -233,11 +236,11 @@ public class ShallowEtagHeaderFilterTests {
}
@Test
public void filterSendRedirect() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterSendRedirect() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(UTF_8);
FilterChain filterChain = (filterRequest, filterResponse) -> {
assertThat(filterRequest).as("Invalid request passed").isEqualTo(request);
response.setContentLength(100);
@@ -254,11 +257,11 @@ public class ShallowEtagHeaderFilterTests {
}
@Test // SPR-13717
public void filterFlushResponse() throws Exception {
final MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
void filterFlushResponse() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/hotels");
MockHttpServletResponse response = new MockHttpServletResponse();
final byte[] responseBody = "Hello World".getBytes(StandardCharsets.UTF_8);
byte[] responseBody = "Hello World".getBytes(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-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,79 +27,104 @@ 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
*/
public class UriTemplateTests {
class UriTemplateTests {
@Test
public void getVariableNames() throws Exception {
void emptyPathDoesNotThrowException() {
assertThatNoException().isThrownBy(() -> new UriTemplate(""));
}
@Test
void nullPathThrowsException() {
assertThatIllegalArgumentException().isThrownBy(() -> new UriTemplate(null));
}
@Test
void getVariableNames() {
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
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(new URI("/hotels/1/bookings/42"));
}
@Test // SPR-9712
public void expandVarArgsWithArrayValue() throws Exception {
UriTemplate template = new UriTemplate("/sum?numbers={numbers}");
URI result = template.expand(new int[] {1, 2, 3});
assertThat(result).isEqualTo(new URI("/sum?numbers=1,2,3"));
void getVariableNamesFromEmpty() {
UriTemplate template = new UriTemplate("");
List<String> variableNames = template.getVariableNames();
assertThat(variableNames).isEmpty();
}
@Test
public void expandVarArgsNotEnoughVariables() throws Exception {
void expandVarArgs() {
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(""));
}
@Test // SPR-9712
void expandVarArgsWithArrayValue() {
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"));
}
@Test
void expandVarArgsNotEnoughVariables() {
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
assertThatIllegalArgumentException().isThrownBy(() -> template.expand("1"));
}
@Test
public void expandMap() throws Exception {
void expandMap() {
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(new URI("/hotels/1/bookings/42"));
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42"));
}
@Test
public void expandMapDuplicateVariables() throws Exception {
void expandMapDuplicateVariables() {
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(new URI("/order/cheeseburger/cheeseburger/cheeseburger"));
assertThat(result).isEqualTo(URI.create("/order/cheeseburger/cheeseburger/cheeseburger"));
}
@Test
public void expandMapNonString() throws Exception {
void expandMapNonString() {
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(new URI("/hotels/1/bookings/42"));
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotels/1/bookings/42"));
}
@Test
public void expandMapEncoded() throws Exception {
void expandMapEncoded() {
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(new URI("/hotel%20list/Z%C3%BCrich"));
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotel%20list/Z%C3%BCrich"));
}
@Test
public void expandMapUnboundVariables() throws Exception {
void expandMapUnboundVariables() {
Map<String, String> uriVariables = new HashMap<>(2);
uriVariables.put("booking", "42");
uriVariables.put("bar", "1");
@@ -109,14 +134,14 @@ public class UriTemplateTests {
}
@Test
public void expandEncoded() throws Exception {
void expandEncoded() {
UriTemplate template = new UriTemplate("/hotel list/{hotel}");
URI result = template.expand("Z\u00fcrich");
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotel%20list/Z%C3%BCrich"));
assertThat(result).as("Invalid expanded template").isEqualTo(URI.create("/hotel%20list/Z%C3%BCrich"));
}
@Test
public void matches() throws Exception {
void matches() {
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();
@@ -125,14 +150,23 @@ public class UriTemplateTests {
}
@Test
public void matchesCustomRegex() throws Exception {
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() {
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
public void match() throws Exception {
void match() {
Map<String, String> expected = new HashMap<>(2);
expected.put("booking", "42");
expected.put("hotel", "1");
@@ -143,7 +177,14 @@ public class UriTemplateTests {
}
@Test
public void matchCustomRegex() throws Exception {
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() {
Map<String, String> expected = new HashMap<>(2);
expected.put("booking", "42");
expected.put("hotel", "1");
@@ -154,14 +195,14 @@ public class UriTemplateTests {
}
@Test // SPR-13627
public void matchCustomRegexWithNestedCurlyBraces() throws Exception {
void matchCustomRegexWithNestedCurlyBraces() {
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
public void matchDuplicate() throws Exception {
void matchDuplicate() {
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");
@@ -169,7 +210,7 @@ public class UriTemplateTests {
}
@Test
public void matchMultipleInOneSegment() throws Exception {
void matchMultipleInOneSegment() {
UriTemplate template = new UriTemplate("/{foo}-{bar}");
Map<String, String> result = template.match("/12-34");
Map<String, String> expected = new HashMap<>(2);
@@ -179,19 +220,19 @@ public class UriTemplateTests {
}
@Test // SPR-16169
public void matchWithMultipleSegmentsAtTheEnd() throws Exception {
void matchWithMultipleSegmentsAtTheEnd() {
UriTemplate template = new UriTemplate("/account/{accountId}");
assertThat(template.matches("/account/15/alias/5")).isFalse();
}
@Test
public void queryVariables() throws Exception {
void queryVariables() {
UriTemplate template = new UriTemplate("/search?q={query}");
assertThat(template.matches("/search?q=foo")).isTrue();
}
@Test
public void fragments() throws Exception {
void fragments() {
UriTemplate template = new UriTemplate("/search#{fragment}");
assertThat(template.matches("/search#foo")).isTrue();
@@ -200,19 +241,19 @@ public class UriTemplateTests {
}
@Test // SPR-13705
public void matchesWithSlashAtTheEnd() throws Exception {
void matchesWithSlashAtTheEnd() {
assertThat(new UriTemplate("/test/").matches("/test/")).isTrue();
}
@Test
public void expandWithDollar() throws Exception {
void expandWithDollar() {
UriTemplate template = new UriTemplate("/{a}");
URI uri = template.expand("$replacement");
assertThat(uri.toString()).isEqualTo("/$replacement");
}
@Test
public void expandWithAtSign() throws Exception {
void expandWithAtSign() {
UriTemplate template = new UriTemplate("http://localhost/query={query}");
URI uri = template.expand("foo@bar");
assertThat(uri.toString()).isEqualTo("http://localhost/query=foo@bar");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -61,7 +61,6 @@ 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;
@@ -993,7 +992,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
}
}
private static String encodeCookies(@NonNull Cookie... cookies) {
private static String encodeCookies(Cookie... cookies) {
return Arrays.stream(cookies)
.map(c -> c.getName() + '=' + (c.getValue() == null ? "" : c.getValue()))
.collect(Collectors.joining("; "));
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -94,6 +94,7 @@ public class HandlerFunctionAdapter implements HandlerAdapter, Ordered {
Object handler) throws Exception {
WebAsyncManager asyncManager = getWebAsyncManager(servletRequest, servletResponse);
servletResponse = getWrappedResponse(asyncManager);
ServerRequest serverRequest = getServerRequest(servletRequest);
ServerResponse serverResponse;
@@ -123,6 +124,22 @@ 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);
@@ -853,7 +853,21 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
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));
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);
@@ -873,15 +887,6 @@ 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();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -203,7 +203,6 @@ public class ServletRequestMethodArgumentResolver implements HandlerMethodArgume
"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
}
return pushBuilder;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -46,6 +46,7 @@ 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;
@@ -228,6 +229,10 @@ 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()) {
@@ -541,6 +546,23 @@ 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.
@@ -0,0 +1,108 @@
/*
* 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);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,11 @@
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;
@@ -25,6 +28,8 @@ 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;
@@ -46,6 +51,10 @@ 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;
@@ -55,13 +64,15 @@ 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;
/**
* Unit tests for {@link RequestMappingHandlerAdapter}.
* Tests for {@link RequestMappingHandlerAdapter}.
*
* @author Rossen Stoyanchev
* @author Sam Brannen
@@ -250,9 +261,7 @@ public class RequestMappingHandlerAdapterTests {
assertThat(mav.getModel().get("attr3")).isNull();
}
// SPR-10859
@Test
@Test // gh-15486
public void responseBodyAdvice() throws Exception {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
converters.add(new MappingJackson2HttpMessageConverter());
@@ -272,6 +281,26 @@ 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);
@@ -297,14 +326,16 @@ 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,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -92,6 +92,9 @@ 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();
}
}