mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
188 Commits
v5.3.0-RC2
..
v5.3.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 967637d763 | |||
| 2899652d8e | |||
| 560a504a83 | |||
| 79f79e9306 | |||
| bd2640a9d6 | |||
| 6825287360 | |||
| 737d77a739 | |||
| 1f13516528 | |||
| 6bb3ad793e | |||
| c73cff8bad | |||
| e592634290 | |||
| b25637e8ad | |||
| 0c347769a2 | |||
| 2497d4285f | |||
| 32238cc996 | |||
| d80dd50c9d | |||
| bdf120f1ef | |||
| e781d21097 | |||
| 8cef13cc30 | |||
| 2214ff8ce0 | |||
| 411539ffef | |||
| 11aa901634 | |||
| 6e51370490 | |||
| bcd2b9a8a7 | |||
| 17dd7785f6 | |||
| 21f9d05bd9 | |||
| 19911af30a | |||
| 6acb091c4e | |||
| 99ed01e3f7 | |||
| 2657757566 | |||
| e1f51b4bf8 | |||
| f2f84bfa7a | |||
| 6d67bdf04f | |||
| 97c8628bd6 | |||
| 990a9c74b9 | |||
| b815accca9 | |||
| 4e8d6bea4d | |||
| 079ca80854 | |||
| 412aa06d86 | |||
| d5b3e65718 | |||
| a4679b87b9 | |||
| fbf8e37f94 | |||
| 8c3cab7ead | |||
| 0015fd6734 | |||
| b077e4cd85 | |||
| 0a0299693b | |||
| 41bdde5d6b | |||
| 2e2d662158 | |||
| 68934f1b79 | |||
| f781c459b4 | |||
| 5628c8cd63 | |||
| 0f6038af70 | |||
| 3eb2c5e22f | |||
| 3f722a9c7f | |||
| acbbf61be8 | |||
| 154f0c71c9 | |||
| 7be7e5beb4 | |||
| 37504e75e9 | |||
| b89b37729c | |||
| 141c79bc8f | |||
| 885a5048c1 | |||
| e981bf34b3 | |||
| 44239b2d7b | |||
| a00c483927 | |||
| 96ffa14141 | |||
| 9f632cf0c1 | |||
| a9dec6a4af | |||
| 9c2fd0bd05 | |||
| 6e936a4081 | |||
| b2d337c527 | |||
| 24cef14683 | |||
| bb4e802af5 | |||
| b01adadf60 | |||
| 9c11887fe8 | |||
| 4503260282 | |||
| d91b66a04c | |||
| d1e6883d5d | |||
| 5170f3f594 | |||
| a4d546a9d6 | |||
| fc9650a9a6 | |||
| 82fa3f3fc1 | |||
| 1fb33fbd3c | |||
| b6dae6acba | |||
| a3cd8e11b0 | |||
| f50c2caf59 | |||
| 0aa3205e38 | |||
| 5b910a87c3 | |||
| 9ebd3ee1fe | |||
| 3f01af6f7c | |||
| f329748657 | |||
| d12996a6a8 | |||
| d04c5f8b2c | |||
| 41247d49ba | |||
| 16c8676e5b | |||
| d7f1813cd7 | |||
| ada255d584 | |||
| 7aef0c78a0 | |||
| 188a5a086b | |||
| 705cf09ad7 | |||
| 449377908f | |||
| b1fef925af | |||
| 9287c15583 | |||
| 9a4a593c7f | |||
| c840ba9989 | |||
| 3924947773 | |||
| 9d70d2023c | |||
| 82835b99ec | |||
| a3f3a136c4 | |||
| a2ff03074f | |||
| a3fcfc5d2a | |||
| 1247e4a0e8 | |||
| 1676470729 | |||
| 4726663f81 | |||
| 86af93a504 | |||
| efe933ad83 | |||
| 5429c7afeb | |||
| 73eefeac5f | |||
| 1a50732daa | |||
| 1c82ba425b | |||
| 946e5c9bbf | |||
| af4d68b1a5 | |||
| a40cc8bbe0 | |||
| 6e640c806f | |||
| e66e34766e | |||
| 5644a7aebb | |||
| 7af726480f | |||
| 51fb49be34 | |||
| c524849774 | |||
| f2eb67373a | |||
| cf988f81b5 | |||
| 417e7e03d4 | |||
| 8d86d61f9f | |||
| fb4363e4e0 | |||
| 1ec6843913 | |||
| cff57ca1fe | |||
| 784d72cc56 | |||
| 4480269432 | |||
| 9a1323aad5 | |||
| eec6ec8f44 | |||
| 6946fe2f74 | |||
| db9e0b0ccb | |||
| aaa2d21058 | |||
| b8b854db8c | |||
| 95110d8257 | |||
| cfc3522641 | |||
| 621295dbd8 | |||
| 07769ddcc6 | |||
| c748870853 | |||
| f7ec92c647 | |||
| bd91dc70d5 | |||
| 7bee3d1574 | |||
| dff9924a0c | |||
| bfb2ce6e2a | |||
| 76eb5e6e2c | |||
| 43f595f80e | |||
| 6f131ba1c4 | |||
| d18fbab7a8 | |||
| a4d0af802a | |||
| db3d537e72 | |||
| a0727191e1 | |||
| 702a05e926 | |||
| 0df6d60fea | |||
| 5ab1097b43 | |||
| d77ecb26a9 | |||
| 6d4b286b1a | |||
| 970afbc614 | |||
| f13c43bafe | |||
| 23211c784e | |||
| a2ff139edd | |||
| 424e28bc35 | |||
| 4952336419 | |||
| babcbd0d2b | |||
| c45b106d95 | |||
| b7d1bbb991 | |||
| 603a1dd319 | |||
| 6b978cb5dc | |||
| b79548d12e | |||
| f9e11593a6 | |||
| 57ac32b808 | |||
| 2eee56d0e2 | |||
| a271a0a2bf | |||
| 69af56c4e9 | |||
| a924677b49 | |||
| 7e647ab1d8 | |||
| abd79d43af | |||
| 431ec906c4 | |||
| 1d96f6a266 | |||
| e5ae2cb0fe |
+17
-17
@@ -26,21 +26,21 @@ configure(allprojects) { project ->
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.11.3"
|
||||
mavenBom "io.netty:netty-bom:4.1.52.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.0-RC2"
|
||||
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR7"
|
||||
mavenBom "io.rsocket:rsocket-bom:1.1.0-RC1"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.32.v20200930"
|
||||
mavenBom "io.netty:netty-bom:4.1.53.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.0"
|
||||
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8"
|
||||
mavenBom "io.rsocket:rsocket-bom:1.1.0"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102"
|
||||
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.10"
|
||||
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.0-M1"
|
||||
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1"
|
||||
mavenBom "org.junit:junit-bom:5.7.0"
|
||||
}
|
||||
dependencies {
|
||||
dependencySet(group: 'org.apache.logging.log4j', version: '2.13.3') {
|
||||
entry 'log4j-api'
|
||||
entry 'log4j-core'
|
||||
entry 'log4j-slf4j-impl'
|
||||
entry 'log4j-jul'
|
||||
entry 'log4j-slf4j-impl'
|
||||
}
|
||||
dependency "org.slf4j:slf4j-api:1.7.30"
|
||||
dependency("com.google.code.findbugs:findbugs:3.0.1") {
|
||||
@@ -65,11 +65,11 @@ configure(allprojects) { project ->
|
||||
dependency "io.reactivex:rxjava-reactive-streams:1.2.1"
|
||||
dependency "io.reactivex.rxjava2:rxjava:2.2.19"
|
||||
dependency "io.reactivex.rxjava3:rxjava:3.0.7"
|
||||
dependency "io.projectreactor.tools:blockhound:1.0.2.RELEASE"
|
||||
dependency "io.projectreactor.tools:blockhound:1.0.4.RELEASE"
|
||||
|
||||
dependency "com.caucho:hessian:4.0.63"
|
||||
dependency "com.fasterxml:aalto-xml:1.2.2"
|
||||
dependency("com.fasterxml.woodstox:woodstox-core:6.2.1") {
|
||||
dependency("com.fasterxml.woodstox:woodstox-core:6.2.3") {
|
||||
exclude group: "stax", name: "stax-api"
|
||||
}
|
||||
dependency "com.google.code.gson:gson:2.8.6"
|
||||
@@ -88,7 +88,7 @@ configure(allprojects) { project ->
|
||||
entry 'jibx-run'
|
||||
}
|
||||
dependency "org.ogce:xpp3:1.1.6"
|
||||
dependency "org.yaml:snakeyaml:1.26"
|
||||
dependency "org.yaml:snakeyaml:1.27"
|
||||
dependencySet(group: 'org.jetbrains.kotlinx', version: '1.0.0') {
|
||||
entry 'kotlinx-serialization-core'
|
||||
entry 'kotlinx-serialization-json'
|
||||
@@ -123,19 +123,19 @@ configure(allprojects) { project ->
|
||||
dependency "net.sf.ehcache:ehcache:2.10.6"
|
||||
dependency "org.ehcache:jcache:1.0.1"
|
||||
dependency "org.ehcache:ehcache:3.4.0"
|
||||
dependency "org.hibernate:hibernate-core:5.4.22.Final"
|
||||
dependency "org.hibernate:hibernate-core:5.4.23.Final"
|
||||
dependency "org.hibernate:hibernate-validator:6.1.6.Final"
|
||||
dependency "org.webjars:webjars-locator-core:0.46"
|
||||
dependency "org.webjars:underscorejs:1.8.3"
|
||||
|
||||
dependencySet(group: 'org.apache.tomcat', version: '9.0.38') {
|
||||
dependencySet(group: 'org.apache.tomcat', version: '9.0.39') {
|
||||
entry 'tomcat-util'
|
||||
entry('tomcat-websocket') {
|
||||
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
|
||||
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
|
||||
}
|
||||
}
|
||||
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.38') {
|
||||
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.39') {
|
||||
entry 'tomcat-embed-core'
|
||||
entry 'tomcat-embed-websocket'
|
||||
}
|
||||
@@ -161,7 +161,7 @@ configure(allprojects) { project ->
|
||||
exclude group: "commons-logging", name: "commons-logging"
|
||||
}
|
||||
dependency 'org.apache.httpcomponents.client5:httpclient5:5.0.3'
|
||||
dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.3'
|
||||
dependency 'org.apache.httpcomponents.core5:httpcore5-reactive:5.0.2'
|
||||
dependency "org.eclipse.jetty:jetty-reactive-httpclient:1.1.4"
|
||||
|
||||
dependency "org.jruby:jruby:9.2.13.0"
|
||||
@@ -190,14 +190,14 @@ configure(allprojects) { project ->
|
||||
dependency "org.testng:testng:7.3.0"
|
||||
dependency "org.hamcrest:hamcrest:2.1"
|
||||
dependency "org.awaitility:awaitility:3.1.6"
|
||||
dependency "org.assertj:assertj-core:3.17.2"
|
||||
dependency "org.assertj:assertj-core:3.18.0"
|
||||
dependencySet(group: 'org.xmlunit', version: '2.6.2') {
|
||||
entry 'xmlunit-assertj'
|
||||
entry('xmlunit-matchers') {
|
||||
exclude group: "org.hamcrest", name: "hamcrest-core"
|
||||
}
|
||||
}
|
||||
dependencySet(group: 'org.mockito', version: '3.5.13') {
|
||||
dependencySet(group: 'org.mockito', version: '3.6.0') {
|
||||
entry('mockito-core') {
|
||||
exclude group: "org.hamcrest", name: "hamcrest-core"
|
||||
}
|
||||
@@ -291,7 +291,6 @@ configure(allprojects) { project ->
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url "https://repo.spring.io/libs-spring-framework-build" }
|
||||
maven { url "https://repo.spring.io/milestone" } // Reactor
|
||||
}
|
||||
}
|
||||
configurations.all {
|
||||
@@ -380,6 +379,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.10/",
|
||||
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/",
|
||||
"https://hc.apache.org/httpcomponents-client-ga/httpclient/apidocs/",
|
||||
"https://projectreactor.io/docs/test/release/api/",
|
||||
"https://junit.org/junit4/javadoc/4.13.1/",
|
||||
"https://junit.org/junit5/docs/5.7.0/api/"
|
||||
] as String[]
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
changelog:
|
||||
repository: spring-projects/spring-framework
|
||||
sections:
|
||||
- title: ":star: New Features"
|
||||
labels:
|
||||
- "type: enhancement"
|
||||
- title: ":beetle: Bug Fixes"
|
||||
labels:
|
||||
- "type: bug"
|
||||
- "type: regression"
|
||||
- title: ":notebook_with_decorative_cover: Documentation"
|
||||
labels:
|
||||
- "type: documentation"
|
||||
- title: ":hammer: Dependency Upgrades"
|
||||
sort: "title"
|
||||
labels:
|
||||
- "type: dependency-upgrade"
|
||||
@@ -0,0 +1,9 @@
|
||||
logging:
|
||||
level:
|
||||
io.spring.concourse: DEBUG
|
||||
distribute:
|
||||
optional-deployments:
|
||||
- '.*\\.zip'
|
||||
spring:
|
||||
main:
|
||||
banner-mode: off
|
||||
+1
-1
@@ -11,7 +11,7 @@ rm -rf /var/lib/apt/lists/*
|
||||
|
||||
curl https://raw.githubusercontent.com/spring-io/concourse-java-scripts/v0.0.3/concourse-java.sh > /opt/concourse-java.sh
|
||||
|
||||
curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.2.0/concourse-release-scripts-0.2.0.jar
|
||||
curl --output /opt/concourse-release-scripts.jar https://repo.spring.io/release/io/spring/concourse/releasescripts/concourse-release-scripts/0.2.1/concourse-release-scripts-0.2.1.jar
|
||||
|
||||
###########################################################
|
||||
# JAVA
|
||||
|
||||
+159
-80
@@ -1,28 +1,61 @@
|
||||
anchors:
|
||||
artifactory-task-params: &artifactory-task-params
|
||||
ARTIFACTORY_SERVER: ((artifactory-server))
|
||||
ARTIFACTORY_USERNAME: ((artifactory-username))
|
||||
ARTIFACTORY_PASSWORD: ((artifactory-password))
|
||||
bintray-task-params: &bintray-task-params
|
||||
BINTRAY_SUBJECT: ((bintray-subject))
|
||||
BINTRAY_REPO: ((bintray-repo))
|
||||
BINTRAY_USERNAME: ((bintray-username))
|
||||
BINTRAY_API_KEY: ((bintray-api-key))
|
||||
docker-resource-source: &docker-resource-source
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
tag: 5.3.x
|
||||
gradle-enterprise-task-params: &gradle-enterprise-task-params
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.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
|
||||
sonatype-task-params: &sonatype-task-params
|
||||
SONATYPE_USER_TOKEN: ((sonatype-user-token))
|
||||
SONATYPE_PASSWORD_TOKEN: ((sonatype-user-token-password))
|
||||
changelog-task-params: &changelog-task-params
|
||||
name: generated-changelog/tag
|
||||
tag: generated-changelog/tag
|
||||
body: generated-changelog/changelog.md
|
||||
github-task-params: &github-task-params
|
||||
GITHUB_USERNAME: ((github-username))
|
||||
GITHUB_TOKEN: ((github-ci-release-token))
|
||||
|
||||
resource_types:
|
||||
- name: artifactory-resource
|
||||
type: docker-image
|
||||
source:
|
||||
repository: springio/artifactory-resource
|
||||
tag: 0.0.12-SNAPSHOT
|
||||
tag: 0.0.12
|
||||
- name: github-status-resource
|
||||
type: docker-image
|
||||
source:
|
||||
repository: dpb587/github-status-resource
|
||||
tag: master
|
||||
- name: email
|
||||
type: docker-image
|
||||
source:
|
||||
repository: pcfseceng/email-resource
|
||||
tag: 1.0.19
|
||||
- name: slack-notification
|
||||
type: docker-image
|
||||
source:
|
||||
repository: cfcommunity/slack-notification-resource
|
||||
tag: latest
|
||||
|
||||
resources:
|
||||
- name: git-repo
|
||||
type: git
|
||||
icon: github-circle
|
||||
icon: github
|
||||
source:
|
||||
uri: ((github-repo))
|
||||
username: ((github-username))
|
||||
@@ -37,7 +70,7 @@ resources:
|
||||
location: Europe/Vienna
|
||||
- name: ci-images-git-repo
|
||||
type: git
|
||||
icon: github-circle
|
||||
icon: github
|
||||
source:
|
||||
uri: ((github-repo))
|
||||
branch: ((branch))
|
||||
@@ -46,34 +79,26 @@ resources:
|
||||
type: docker-image
|
||||
icon: docker
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-ci-image
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
tag: 5.3.x
|
||||
- name: spring-framework-jdk11-ci-image
|
||||
type: docker-image
|
||||
icon: docker
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-jdk11-ci-image
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
tag: 5.3.x
|
||||
- name: spring-framework-jdk14-ci-image
|
||||
type: docker-image
|
||||
icon: docker
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-jdk14-ci-image
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
tag: 5.3.x
|
||||
- name: spring-framework-jdk15-ci-image
|
||||
type: docker-image
|
||||
icon: docker
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-jdk15-ci-image
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
tag: 5.3.x
|
||||
- name: artifactory-repo
|
||||
type: artifactory-resource
|
||||
icon: package-variant
|
||||
@@ -119,6 +144,24 @@ resources:
|
||||
icon: slack
|
||||
source:
|
||||
url: ((slack-webhook-url))
|
||||
- name: github-pre-release
|
||||
type: github-release
|
||||
icon: briefcase-download-outline
|
||||
source:
|
||||
owner: spring-projects
|
||||
repository: spring-framework
|
||||
access_token: ((github-ci-release-token))
|
||||
pre_release: true
|
||||
release: false
|
||||
- name: github-release
|
||||
type: github-release
|
||||
icon: briefcase-download
|
||||
source:
|
||||
owner: spring-projects
|
||||
repository: spring-framework
|
||||
access_token: ((github-ci-release-token))
|
||||
pre_release: false
|
||||
|
||||
jobs:
|
||||
- name: build-spring-framework-ci-images
|
||||
plan:
|
||||
@@ -158,23 +201,14 @@ jobs:
|
||||
file: git-repo/ci/tasks/build-project.yml
|
||||
params:
|
||||
BRANCH: ((branch))
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
|
||||
<<: *gradle-enterprise-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
text: >
|
||||
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
|
||||
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
|
||||
[<$TEXT_FILE_CONTENT|build scan>]
|
||||
text_file: git-repo/build/build-scan-uri.txt
|
||||
silent: true
|
||||
icon_emoji: ":concourse:"
|
||||
username: concourse-ci
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- put: artifactory-repo
|
||||
@@ -224,23 +258,14 @@ jobs:
|
||||
file: git-repo/ci/tasks/check-project.yml
|
||||
params:
|
||||
BRANCH: ((branch))
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
|
||||
<<: *gradle-enterprise-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk11-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
text: >
|
||||
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
|
||||
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
|
||||
[<$TEXT_FILE_CONTENT|build scan>]
|
||||
text_file: git-repo/build/build-scan-uri.txt
|
||||
silent: true
|
||||
icon_emoji: ":concourse:"
|
||||
username: concourse-ci
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk11-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: jdk14-build
|
||||
@@ -261,23 +286,14 @@ jobs:
|
||||
file: git-repo/ci/tasks/check-project.yml
|
||||
params:
|
||||
BRANCH: ((branch))
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
|
||||
<<: *gradle-enterprise-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk14-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
text: >
|
||||
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
|
||||
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
|
||||
[<$TEXT_FILE_CONTENT|build scan>]
|
||||
text_file: git-repo/build/build-scan-uri.txt
|
||||
silent: true
|
||||
icon_emoji: ":concourse:"
|
||||
username: concourse-ci
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk14-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: jdk15-build
|
||||
@@ -298,23 +314,14 @@ jobs:
|
||||
file: git-repo/ci/tasks/check-project.yml
|
||||
params:
|
||||
BRANCH: ((branch))
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
|
||||
<<: *gradle-enterprise-task-params
|
||||
on_failure:
|
||||
do:
|
||||
- put: repo-status-jdk15-build
|
||||
params: { state: "failure", commit: "git-repo" }
|
||||
- put: slack-alert
|
||||
params:
|
||||
text: >
|
||||
:concourse-failed: ${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!
|
||||
[<https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|build log>]
|
||||
[<$TEXT_FILE_CONTENT|build scan>]
|
||||
text_file: git-repo/build/build-scan-uri.txt
|
||||
silent: true
|
||||
icon_emoji: ":concourse:"
|
||||
username: concourse-ci
|
||||
<<: *slack-fail-params
|
||||
- put: repo-status-jdk15-build
|
||||
params: { state: "success", commit: "git-repo" }
|
||||
- name: stage-milestone
|
||||
@@ -328,9 +335,7 @@ jobs:
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
|
||||
<<: *gradle-enterprise-task-params
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
@@ -355,9 +360,15 @@ jobs:
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
ARTIFACTORY_SERVER: ((artifactory-server))
|
||||
ARTIFACTORY_USERNAME: ((artifactory-username))
|
||||
ARTIFACTORY_PASSWORD: ((artifactory-password))
|
||||
<<: *artifactory-task-params
|
||||
- task: generate-changelog
|
||||
file: git-repo/ci/tasks/generate-changelog.yml
|
||||
params:
|
||||
RELEASE_TYPE: M
|
||||
<<: *github-task-params
|
||||
- put: github-pre-release
|
||||
params:
|
||||
<<: *changelog-task-params
|
||||
- name: stage-rc
|
||||
serial: true
|
||||
plan:
|
||||
@@ -369,9 +380,7 @@ jobs:
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RC
|
||||
GRADLE_ENTERPRISE_ACCESS_KEY: ((gradle_enterprise_secret_access_key))
|
||||
GRADLE_ENTERPRISE_CACHE_USERNAME: ((gradle_enterprise_cache_user.username))
|
||||
GRADLE_ENTERPRISE_CACHE_PASSWORD: ((gradle_enterprise_cache_user.password))
|
||||
<<: *gradle-enterprise-task-params
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
@@ -396,13 +405,83 @@ jobs:
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RC
|
||||
ARTIFACTORY_SERVER: ((artifactory-server))
|
||||
ARTIFACTORY_USERNAME: ((artifactory-username))
|
||||
ARTIFACTORY_PASSWORD: ((artifactory-password))
|
||||
<<: *artifactory-task-params
|
||||
- task: generate-changelog
|
||||
file: git-repo/ci/tasks/generate-changelog.yml
|
||||
params:
|
||||
RELEASE_TYPE: RC
|
||||
<<: *github-task-params
|
||||
- put: github-pre-release
|
||||
params:
|
||||
<<: *changelog-task-params
|
||||
- name: stage-release
|
||||
serial: true
|
||||
plan:
|
||||
- get: spring-framework-ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- task: stage
|
||||
image: spring-framework-ci-image
|
||||
file: git-repo/ci/tasks/stage-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RELEASE
|
||||
<<: *gradle-enterprise-task-params
|
||||
- put: artifactory-repo
|
||||
params:
|
||||
<<: *artifactory-params
|
||||
repo: libs-staging-local
|
||||
- put: git-repo
|
||||
params:
|
||||
repository: stage-git-repo
|
||||
- name: promote-release
|
||||
serial: true
|
||||
plan:
|
||||
- get: spring-framework-ci-image
|
||||
- get: git-repo
|
||||
trigger: false
|
||||
- get: artifactory-repo
|
||||
trigger: false
|
||||
passed: [stage-release]
|
||||
params:
|
||||
download_artifacts: false
|
||||
save_build_info: true
|
||||
- task: promote
|
||||
image: spring-framework-ci-image
|
||||
file: git-repo/ci/tasks/promote-version.yml
|
||||
params:
|
||||
RELEASE_TYPE: RELEASE
|
||||
<<: *artifactory-task-params
|
||||
<<: *bintray-task-params
|
||||
- name: sync-to-maven-central
|
||||
serial: true
|
||||
plan:
|
||||
- get: spring-framework-ci-image
|
||||
- get: git-repo
|
||||
- get: artifactory-repo
|
||||
trigger: true
|
||||
passed: [promote-release]
|
||||
params:
|
||||
download_artifacts: false
|
||||
save_build_info: true
|
||||
- task: sync-to-maven-central
|
||||
image: spring-framework-ci-image
|
||||
file: git-repo/ci/tasks/sync-to-maven-central.yml
|
||||
params:
|
||||
<<: *bintray-task-params
|
||||
<<: *sonatype-task-params
|
||||
- task: generate-changelog
|
||||
file: git-repo/ci/tasks/generate-changelog.yml
|
||||
params:
|
||||
RELEASE_TYPE: RELEASE
|
||||
<<: *github-task-params
|
||||
- put: github-release
|
||||
params:
|
||||
<<: *changelog-task-params
|
||||
|
||||
groups:
|
||||
- name: "Build"
|
||||
- name: "builds"
|
||||
jobs: ["build", "jdk11-build", "jdk14-build", "jdk15-build"]
|
||||
- name: "Release"
|
||||
jobs: ["stage-milestone","stage-rc", "promote-milestone","promote-rc"]
|
||||
- name: "CI Images"
|
||||
- name: "releases"
|
||||
jobs: ["stage-milestone", "stage-rc", "stage-release", "promote-milestone","promote-rc", "promote-release", "sync-to-maven-central"]
|
||||
- name: "ci-images"
|
||||
jobs: ["build-spring-framework-ci-images"]
|
||||
|
||||
Executable
+12
@@ -0,0 +1,12 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
CONFIG_DIR=git-repo/ci/config
|
||||
version=$( cat version/version )
|
||||
|
||||
java -jar /github-changelog-generator.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/changelog-generator.yml \
|
||||
${version} generated-changelog/changelog.md
|
||||
|
||||
echo ${version} > generated-changelog/version
|
||||
echo v${version} > generated-changelog/tag
|
||||
@@ -1,13 +1,16 @@
|
||||
#!/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/' )
|
||||
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
|
||||
|
||||
java -jar /opt/concourse-release-scripts.jar promote $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; }
|
||||
|
||||
java -jar /opt/concourse-release-scripts.jar distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; }
|
||||
java -jar /opt/concourse-release-scripts.jar \
|
||||
--spring.config.location=${CONFIG_DIR}/release-scripts.yml \
|
||||
distribute $RELEASE_TYPE $BUILD_INFO_LOCATION > /dev/null || { exit 1; }
|
||||
|
||||
echo "Promotion complete"
|
||||
echo $version > version/version
|
||||
|
||||
Executable
+8
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
export BUILD_INFO_LOCATION=$(pwd)/artifactory-repo/build-info.json
|
||||
version=$( cat artifactory-repo/build-info.json | jq -r '.buildInfo.modules[0].id' | sed 's/.*:.*:\(.*\)/\1/' )
|
||||
java -jar /opt/concourse-release-scripts.jar syncToCentral "RELEASE" $BUILD_INFO_LOCATION || { exit 1; }
|
||||
|
||||
echo "Sync complete"
|
||||
echo $version > version/version
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
platform: linux
|
||||
image_resource:
|
||||
type: docker-image
|
||||
source:
|
||||
repository: springio/github-changelog-generator
|
||||
tag: '0.0.4'
|
||||
inputs:
|
||||
- name: git-repo
|
||||
- name: version
|
||||
outputs:
|
||||
- name: generated-changelog
|
||||
params:
|
||||
GITHUB_ORGANIZATION:
|
||||
GITHUB_REPO:
|
||||
GITHUB_USERNAME:
|
||||
GITHUB_TOKEN:
|
||||
RELEASE_TYPE:
|
||||
run:
|
||||
path: git-repo/ci/scripts/generate-changelog.sh
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
platform: linux
|
||||
inputs:
|
||||
- name: git-repo
|
||||
- name: artifactory-repo
|
||||
outputs:
|
||||
- name: version
|
||||
params:
|
||||
BINTRAY_REPO:
|
||||
BINTRAY_SUBJECT:
|
||||
BINTRAY_USERNAME:
|
||||
BINTRAY_API_KEY:
|
||||
SONATYPE_USER_TOKEN:
|
||||
SONATYPE_PASSWORD_TOKEN:
|
||||
run:
|
||||
path: git-repo/ci/scripts/sync-to-maven-central.sh
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=5.3.0-RC2
|
||||
version=5.3.1
|
||||
org.gradle.jvmargs=-Xmx1536M
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
@@ -106,6 +106,9 @@ dokka {
|
||||
externalDocumentationLink {
|
||||
url = new URL("https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/")
|
||||
}
|
||||
externalDocumentationLink {
|
||||
url = new URL("https://r2dbc.io/spec/0.8.3.RELEASE/api/")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
+5
-3
@@ -221,10 +221,12 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
|
||||
@Override
|
||||
@Nullable
|
||||
public String[] getParameterNames() {
|
||||
if (this.parameterNames == null) {
|
||||
this.parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
|
||||
String[] parameterNames = this.parameterNames;
|
||||
if (parameterNames == null) {
|
||||
parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
|
||||
this.parameterNames = parameterNames;
|
||||
}
|
||||
return this.parameterNames;
|
||||
return parameterNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+5
-16
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -160,23 +160,12 @@ public class AspectJProxyFactory extends ProxyCreatorSupport {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton aspect instance for the supplied aspect type. An instance
|
||||
* is created if one cannot be found in the instance cache.
|
||||
* Get the singleton aspect instance for the supplied aspect type.
|
||||
* An instance is created if one cannot be found in the instance cache.
|
||||
*/
|
||||
private Object getSingletonAspectInstance(Class<?> aspectClass) {
|
||||
// Quick check without a lock...
|
||||
Object instance = aspectCache.get(aspectClass);
|
||||
if (instance == null) {
|
||||
synchronized (aspectCache) {
|
||||
// To be safe, check within full lock now...
|
||||
instance = aspectCache.get(aspectClass);
|
||||
if (instance == null) {
|
||||
instance = new SimpleAspectInstanceFactory(aspectClass).getAspectInstance();
|
||||
aspectCache.put(aspectClass, instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
return instance;
|
||||
return aspectCache.computeIfAbsent(aspectClass,
|
||||
clazz -> new SimpleAspectInstanceFactory(clazz).getAspectInstance());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -114,6 +114,15 @@ public interface Advised extends TargetClassAware {
|
||||
*/
|
||||
Advisor[] getAdvisors();
|
||||
|
||||
/**
|
||||
* Return the number of advisors applying to this proxy.
|
||||
* <p>The default implementation delegates to {@code getAdvisors().length}.
|
||||
* @since 5.3.1
|
||||
*/
|
||||
default int getAdvisorCount() {
|
||||
return getAdvisors().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an advisor at the end of the advisor chain.
|
||||
* <p>The Advisor may be an {@link org.springframework.aop.IntroductionAdvisor},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -95,12 +95,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
*/
|
||||
private List<Advisor> advisors = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Array updated on changes to the advisors list, which is easier
|
||||
* to manipulate internally.
|
||||
*/
|
||||
private Advisor[] advisorArray = new Advisor[0];
|
||||
|
||||
|
||||
/**
|
||||
* No-arg constructor for use as a JavaBean.
|
||||
@@ -244,7 +238,12 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
|
||||
@Override
|
||||
public final Advisor[] getAdvisors() {
|
||||
return this.advisorArray;
|
||||
return this.advisors.toArray(new Advisor[0]);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getAdvisorCount() {
|
||||
return this.advisors.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -292,7 +291,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
}
|
||||
}
|
||||
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
|
||||
@@ -339,7 +337,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
Assert.notNull(advisor, "Advisor must not be null");
|
||||
this.advisors.add(advisor);
|
||||
}
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
}
|
||||
@@ -363,27 +360,18 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
"Illegal position " + pos + " in advisor list with size " + this.advisors.size());
|
||||
}
|
||||
this.advisors.add(pos, advisor);
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bring the array up to date with the list.
|
||||
*/
|
||||
protected final void updateAdvisorArray() {
|
||||
this.advisorArray = this.advisors.toArray(new Advisor[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows uncontrolled access to the {@link List} of {@link Advisor Advisors}.
|
||||
* <p>Use with care, and remember to {@link #updateAdvisorArray() refresh the advisor array}
|
||||
* and {@link #adviceChanged() fire advice changed events} when making any modifications.
|
||||
* <p>Use with care, and remember to {@link #adviceChanged() fire advice changed events}
|
||||
* when making any modifications.
|
||||
*/
|
||||
protected final List<Advisor> getAdvisorsInternal() {
|
||||
return this.advisors;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void addAdvice(Advice advice) throws AopConfigException {
|
||||
int pos = this.advisors.size();
|
||||
@@ -521,7 +509,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
Assert.notNull(advisor, "Advisor must not be null");
|
||||
this.advisors.add(advisor);
|
||||
}
|
||||
updateAdvisorArray();
|
||||
adviceChanged();
|
||||
}
|
||||
|
||||
@@ -536,7 +523,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
copy.advisorChainFactory = this.advisorChainFactory;
|
||||
copy.interfaces = this.interfaces;
|
||||
copy.advisors = this.advisors;
|
||||
copy.updateAdvisorArray();
|
||||
return copy;
|
||||
}
|
||||
|
||||
@@ -553,7 +539,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
this.methodCache = new ConcurrentHashMap<>(32);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public String toProxyConfigString() {
|
||||
return toString();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -206,7 +206,7 @@ public abstract class AopProxyUtils {
|
||||
* Check equality of the advisors behind the given AdvisedSupport objects.
|
||||
*/
|
||||
public static boolean equalsAdvisors(AdvisedSupport a, AdvisedSupport b) {
|
||||
return Arrays.equals(a.getAdvisors(), b.getAdvisors());
|
||||
return a.getAdvisorCount() == b.getAdvisorCount() && Arrays.equals(a.getAdvisors(), b.getAdvisors());
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -125,7 +125,7 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
*/
|
||||
public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
|
||||
Assert.notNull(config, "AdvisedSupport must not be null");
|
||||
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
throw new AopConfigException("No advisors and no TargetSource specified");
|
||||
}
|
||||
this.advised = config;
|
||||
@@ -942,11 +942,11 @@ class CglibAopProxy implements AopProxy, Serializable {
|
||||
}
|
||||
// Advice instance identity is unimportant to the proxy class:
|
||||
// All that matters is type and ordering.
|
||||
Advisor[] thisAdvisors = this.advised.getAdvisors();
|
||||
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
|
||||
if (thisAdvisors.length != thatAdvisors.length) {
|
||||
if (this.advised.getAdvisorCount() != otherAdvised.getAdvisorCount()) {
|
||||
return false;
|
||||
}
|
||||
Advisor[] thisAdvisors = this.advised.getAdvisors();
|
||||
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
|
||||
for (int i = 0; i < thisAdvisors.length; i++) {
|
||||
Advisor thisAdvisor = thisAdvisors[i];
|
||||
Advisor thatAdvisor = thatAdvisors[i];
|
||||
|
||||
@@ -104,7 +104,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
|
||||
*/
|
||||
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
|
||||
Assert.notNull(config, "AdvisedSupport must not be null");
|
||||
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
|
||||
throw new AopConfigException("No advisors and no TargetSource specified");
|
||||
}
|
||||
this.advised = config;
|
||||
|
||||
@@ -25,6 +25,7 @@ sourceSets {
|
||||
compileGroovy {
|
||||
sourceCompatibility = 1.8
|
||||
targetCompatibility = 1.8
|
||||
options.compilerArgs += "-Werror"
|
||||
}
|
||||
|
||||
// This module also builds Kotlin code and the compileKotlin task naturally depends on
|
||||
|
||||
+6
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -422,9 +422,12 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
|
||||
}
|
||||
return;
|
||||
}
|
||||
else {
|
||||
throw createNotWritablePropertyException(tokens.canonicalName);
|
||||
if (this.suppressNotWritablePropertyException) {
|
||||
// Optimization for common ignoreUnknown=true scenario since the
|
||||
// exception would be caught and swallowed higher up anyway...
|
||||
return;
|
||||
}
|
||||
throw createNotWritablePropertyException(tokens.canonicalName);
|
||||
}
|
||||
|
||||
Object oldValue = null;
|
||||
|
||||
+33
-20
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,6 +40,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
|
||||
|
||||
private boolean autoGrowNestedPaths = false;
|
||||
|
||||
boolean suppressNotWritablePropertyException = false;
|
||||
|
||||
|
||||
@Override
|
||||
public void setExtractOldValueForEditor(boolean extractOldValueForEditor) {
|
||||
@@ -89,30 +91,41 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
|
||||
List<PropertyAccessException> propertyAccessExceptions = null;
|
||||
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
|
||||
((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
|
||||
for (PropertyValue pv : propertyValues) {
|
||||
try {
|
||||
// This method may throw any BeansException, which won't be caught
|
||||
|
||||
if (ignoreUnknown) {
|
||||
this.suppressNotWritablePropertyException = true;
|
||||
}
|
||||
try {
|
||||
for (PropertyValue pv : propertyValues) {
|
||||
// setPropertyValue may throw any BeansException, which won't be caught
|
||||
// here, if there is a critical failure such as no matching field.
|
||||
// We can attempt to deal only with less serious exceptions.
|
||||
setPropertyValue(pv);
|
||||
}
|
||||
catch (NotWritablePropertyException ex) {
|
||||
if (!ignoreUnknown) {
|
||||
throw ex;
|
||||
try {
|
||||
setPropertyValue(pv);
|
||||
}
|
||||
// Otherwise, just ignore it and continue...
|
||||
}
|
||||
catch (NullValueInNestedPathException ex) {
|
||||
if (!ignoreInvalid) {
|
||||
throw ex;
|
||||
catch (NotWritablePropertyException ex) {
|
||||
if (!ignoreUnknown) {
|
||||
throw ex;
|
||||
}
|
||||
// Otherwise, just ignore it and continue...
|
||||
}
|
||||
// Otherwise, just ignore it and continue...
|
||||
}
|
||||
catch (PropertyAccessException ex) {
|
||||
if (propertyAccessExceptions == null) {
|
||||
propertyAccessExceptions = new ArrayList<>();
|
||||
catch (NullValueInNestedPathException ex) {
|
||||
if (!ignoreInvalid) {
|
||||
throw ex;
|
||||
}
|
||||
// Otherwise, just ignore it and continue...
|
||||
}
|
||||
propertyAccessExceptions.add(ex);
|
||||
catch (PropertyAccessException ex) {
|
||||
if (propertyAccessExceptions == null) {
|
||||
propertyAccessExceptions = new ArrayList<>();
|
||||
}
|
||||
propertyAccessExceptions.add(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally {
|
||||
if (ignoreUnknown) {
|
||||
this.suppressNotWritablePropertyException = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -92,8 +92,7 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
|
||||
@Override
|
||||
protected NotWritablePropertyException createNotWritablePropertyException(String propertyName) {
|
||||
PropertyMatches matches = PropertyMatches.forField(propertyName, getRootClass());
|
||||
throw new NotWritablePropertyException(
|
||||
getRootClass(), getNestedPath() + propertyName,
|
||||
throw new NotWritablePropertyException(getRootClass(), getNestedPath() + propertyName,
|
||||
matches.buildErrorMessage(), matches.getPossibleMatches());
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -644,21 +644,20 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
|
||||
}
|
||||
synchronized (this) {
|
||||
if (!this.cached) {
|
||||
Object cachedFieldValue = null;
|
||||
if (value != null || this.required) {
|
||||
this.cachedFieldValue = desc;
|
||||
cachedFieldValue = desc;
|
||||
registerDependentBeans(beanName, autowiredBeanNames);
|
||||
if (autowiredBeanNames.size() == 1) {
|
||||
String autowiredBeanName = autowiredBeanNames.iterator().next();
|
||||
if (beanFactory.containsBean(autowiredBeanName) &&
|
||||
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
|
||||
this.cachedFieldValue = new ShortcutDependencyDescriptor(
|
||||
cachedFieldValue = new ShortcutDependencyDescriptor(
|
||||
desc, autowiredBeanName, field.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.cachedFieldValue = null;
|
||||
}
|
||||
this.cachedFieldValue = cachedFieldValue;
|
||||
this.cached = true;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-12
@@ -26,9 +26,6 @@ import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.MutablePropertyValues;
|
||||
import org.springframework.beans.PropertyValues;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
@@ -69,8 +66,6 @@ public class InjectionMetadata {
|
||||
};
|
||||
|
||||
|
||||
private static final Log logger = LogFactory.getLog(InjectionMetadata.class);
|
||||
|
||||
private final Class<?> targetClass;
|
||||
|
||||
private final Collection<InjectedElement> injectedElements;
|
||||
@@ -110,9 +105,6 @@ public class InjectionMetadata {
|
||||
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
|
||||
beanDefinition.registerExternallyManagedConfigMember(member);
|
||||
checkedElements.add(element);
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.checkedElements = checkedElements;
|
||||
@@ -124,9 +116,6 @@ public class InjectionMetadata {
|
||||
(checkedElements != null ? checkedElements : this.injectedElements);
|
||||
if (!elementsToIterate.isEmpty()) {
|
||||
for (InjectedElement element : elementsToIterate) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("Processing injected element of bean '" + beanName + "': " + element);
|
||||
}
|
||||
element.inject(target, beanName, pvs);
|
||||
}
|
||||
}
|
||||
@@ -157,7 +146,8 @@ public class InjectionMetadata {
|
||||
* @since 5.2
|
||||
*/
|
||||
public static InjectionMetadata forElements(Collection<InjectedElement> elements, Class<?> clazz) {
|
||||
return (elements.isEmpty() ? InjectionMetadata.EMPTY : new InjectionMetadata(clazz, elements));
|
||||
return (elements.isEmpty() ? new InjectionMetadata(clazz, Collections.emptyList()) :
|
||||
new InjectionMetadata(clazz, elements));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -40,7 +40,6 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* {@link AutowireCandidateResolver} implementation that matches bean definition qualifiers
|
||||
@@ -188,7 +187,7 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
|
||||
foundMeta = true;
|
||||
// Only accept fallback match if @Qualifier annotation has a value...
|
||||
// Otherwise it is just a marker for a custom qualifier annotation.
|
||||
if ((fallbackToMeta && StringUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) ||
|
||||
if ((fallbackToMeta && ObjectUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) ||
|
||||
!checkQualifier(bdHolder, metaAnn, typeConverter)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
+2
-4
@@ -1288,10 +1288,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
|
||||
* @param registry the PropertyEditorRegistry to initialize
|
||||
*/
|
||||
protected void registerCustomEditors(PropertyEditorRegistry registry) {
|
||||
PropertyEditorRegistrySupport registrySupport =
|
||||
(registry instanceof PropertyEditorRegistrySupport ? (PropertyEditorRegistrySupport) registry : null);
|
||||
if (registrySupport != null) {
|
||||
registrySupport.useConfigValueEditors();
|
||||
if (registry instanceof PropertyEditorRegistrySupport) {
|
||||
((PropertyEditorRegistrySupport) registry).useConfigValueEditors();
|
||||
}
|
||||
if (!this.propertyEditorRegistrars.isEmpty()) {
|
||||
for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
|
||||
|
||||
-4
@@ -43,8 +43,6 @@ import java.util.stream.IntStream;
|
||||
import javax.annotation.Priority;
|
||||
import javax.security.auth.Subject;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
@@ -121,8 +119,6 @@ import static org.mockito.Mockito.verify;
|
||||
*/
|
||||
class DefaultListableBeanFactoryTests {
|
||||
|
||||
private static final Log factoryLog = LogFactory.getLog(DefaultListableBeanFactory.class);
|
||||
|
||||
private DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
|
||||
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -126,7 +126,7 @@ public class DefinitionMetadataEqualsHashCodeTests {
|
||||
assertThat(equal.hashCode()).as("Hash code for equal instances should match").isEqualTo(master.hashCode());
|
||||
|
||||
assertThat(notEqual).as("Should not be equal").isNotEqualTo(master);
|
||||
assertThat(notEqual.hashCode()).as("Hash code for non-equal instances should not match").isNotEqualTo((long) master.hashCode());
|
||||
assertThat(notEqual.hashCode()).as("Hash code for non-equal instances should not match").isNotEqualTo(master.hashCode());
|
||||
|
||||
assertThat(subclass).as("Subclass should be equal").isEqualTo(master);
|
||||
assertThat(subclass.hashCode()).as("Hash code for subclass should match").isEqualTo(master.hashCode());
|
||||
|
||||
+1
-2
@@ -155,8 +155,7 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing
|
||||
* @param name the name of the cache to be added
|
||||
*/
|
||||
private void updateCacheNames(String name) {
|
||||
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames.size() + 1);
|
||||
cacheNames.addAll(this.cacheNames);
|
||||
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames);
|
||||
cacheNames.add(name);
|
||||
this.cacheNames = Collections.unmodifiableSet(cacheNames);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.context;
|
||||
|
||||
import java.util.EventListener;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Interface to be implemented by application event listeners.
|
||||
@@ -45,4 +46,17 @@ public interface ApplicationListener<E extends ApplicationEvent> extends EventLi
|
||||
*/
|
||||
void onApplicationEvent(E event);
|
||||
|
||||
|
||||
/**
|
||||
* Create a new {@code ApplicationListener} for the given payload consumer.
|
||||
* @param consumer the event payload consumer
|
||||
* @param <T> the type of the event payload
|
||||
* @return a corresponding {@code ApplicationListener} instance
|
||||
* @since 5.3
|
||||
* @see PayloadApplicationEvent
|
||||
*/
|
||||
static <T> ApplicationListener<PayloadApplicationEvent<T>> forPayload(Consumer<T> consumer) {
|
||||
return event -> consumer.accept(event.getPayload());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -99,7 +99,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
|
||||
* @see #setBeanNameGenerator
|
||||
*/
|
||||
public static final AnnotationBeanNameGenerator IMPORT_BEAN_NAME_GENERATOR =
|
||||
new FullyQualifiedAnnotationBeanNameGenerator();
|
||||
FullyQualifiedAnnotationBeanNameGenerator.INSTANCE;
|
||||
|
||||
private static final String IMPORT_REGISTRY_BEAN_NAME =
|
||||
ConfigurationClassPostProcessor.class.getName() + ".importRegistry";
|
||||
|
||||
+9
@@ -43,6 +43,15 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
|
||||
|
||||
/**
|
||||
* A convenient constant for a default {@code FullyQualifiedAnnotationBeanNameGenerator}
|
||||
* instance, as used for configuration-level import purposes.
|
||||
* @since 5.2.11
|
||||
*/
|
||||
public static final FullyQualifiedAnnotationBeanNameGenerator INSTANCE =
|
||||
new FullyQualifiedAnnotationBeanNameGenerator();
|
||||
|
||||
|
||||
@Override
|
||||
protected String buildDefaultBeanName(BeanDefinition definition) {
|
||||
String beanClassName = definition.getBeanClassName();
|
||||
|
||||
+16
-1
@@ -37,6 +37,7 @@ import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
import org.springframework.context.expression.AnnotatedElementKey;
|
||||
import org.springframework.core.BridgeMethodResolver;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.ReactiveAdapter;
|
||||
import org.springframework.core.ReactiveAdapterRegistry;
|
||||
import org.springframework.core.ResolvableType;
|
||||
@@ -95,6 +96,12 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
private EventExpressionEvaluator evaluator;
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new ApplicationListenerMethodAdapter.
|
||||
* @param beanName the name of the bean to invoke the listener method on
|
||||
* @param targetClass the target class that the method is declared on
|
||||
* @param method the listener method to invoke
|
||||
*/
|
||||
public ApplicationListenerMethodAdapter(String beanName, Class<?> targetClass, Method method) {
|
||||
this.beanName = beanName;
|
||||
this.method = BridgeMethodResolver.findBridgedMethod(method);
|
||||
@@ -135,7 +142,7 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
|
||||
private static int resolveOrder(Method method) {
|
||||
Order ann = AnnotatedElementUtils.findMergedAnnotation(method, Order.class);
|
||||
return (ann != null ? ann.value() : 0);
|
||||
return (ann != null ? ann.value() : Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
|
||||
@@ -332,6 +339,14 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe
|
||||
return this.applicationContext.getBean(this.beanName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the target listener method.
|
||||
* @since 5.3
|
||||
*/
|
||||
protected Method getTargetMethod() {
|
||||
return this.targetMethod;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the condition to use.
|
||||
* <p>Matches the {@code condition} attribute of the {@link EventListener}
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,6 +29,7 @@ import org.springframework.core.Ordered;
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @since 4.2
|
||||
* @see ApplicationListenerMethodAdapter
|
||||
*/
|
||||
public class DefaultEventListenerFactory implements EventListenerFactory, Ordered {
|
||||
|
||||
|
||||
+55
@@ -307,6 +307,23 @@ public class ConfigurationClassProcessingTests {
|
||||
assertThat(tb.getLawyer()).isEqualTo(ctx.getBean(NestedTestBean.class));
|
||||
}
|
||||
|
||||
@Test // gh-26019
|
||||
public void autowiringWithDynamicPrototypeBeanClass() {
|
||||
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
|
||||
ConfigWithDynamicPrototype.class, PrototypeDependency.class);
|
||||
|
||||
PrototypeInterface p1 = ctx.getBean(PrototypeInterface.class, 1);
|
||||
assertThat(p1).isInstanceOf(PrototypeOne.class);
|
||||
assertThat(((PrototypeOne) p1).prototypeDependency).isNotNull();
|
||||
|
||||
PrototypeInterface p2 = ctx.getBean(PrototypeInterface.class, 2);
|
||||
assertThat(p2).isInstanceOf(PrototypeTwo.class);
|
||||
|
||||
PrototypeInterface p3 = ctx.getBean(PrototypeInterface.class, 1);
|
||||
assertThat(p3).isInstanceOf(PrototypeOne.class);
|
||||
assertThat(((PrototypeOne) p3).prototypeDependency).isNotNull();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Creates a new {@link BeanFactory}, populates it with a {@link BeanDefinition}
|
||||
@@ -632,4 +649,42 @@ public class ConfigurationClassProcessingTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class PrototypeDependency {
|
||||
}
|
||||
|
||||
interface PrototypeInterface {
|
||||
}
|
||||
|
||||
static class PrototypeOne extends AbstractPrototype {
|
||||
|
||||
@Autowired
|
||||
PrototypeDependency prototypeDependency;
|
||||
|
||||
}
|
||||
|
||||
static class PrototypeTwo extends AbstractPrototype {
|
||||
|
||||
// no autowired dependency here, in contrast to above
|
||||
}
|
||||
|
||||
static class AbstractPrototype implements PrototypeInterface {
|
||||
}
|
||||
|
||||
@Configuration
|
||||
static class ConfigWithDynamicPrototype {
|
||||
|
||||
@Bean
|
||||
@Scope(value = "prototype")
|
||||
public PrototypeInterface getDemoBean( int i) {
|
||||
switch ( i) {
|
||||
case 1: return new PrototypeOne();
|
||||
case 2:
|
||||
default:
|
||||
return new PrototypeTwo();
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -26,6 +26,7 @@ import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationEvent;
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.ResolvableTypeProvider;
|
||||
import org.springframework.core.annotation.Order;
|
||||
@@ -161,7 +162,7 @@ public class ApplicationListenerMethodAdapterTests extends AbstractApplicationEv
|
||||
Method method = ReflectionUtils.findMethod(
|
||||
SampleEvents.class, "handleGenericString", GenericTestEvent.class);
|
||||
ApplicationListenerMethodAdapter adapter = createTestInstance(method);
|
||||
assertThat(adapter.getOrder()).isEqualTo(0);
|
||||
assertThat(adapter.getOrder()).isEqualTo(Ordered.LOWEST_PRECEDENCE);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+34
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,8 +22,11 @@ import java.util.List;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.PayloadApplicationEvent;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -34,14 +37,42 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
public class PayloadApplicationEventTests {
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "rawtypes", "resource" })
|
||||
public void testEventClassWithInterface() {
|
||||
ApplicationContext ac = new AnnotationConfigApplicationContext(AuditableListener.class);
|
||||
AuditablePayloadEvent event = new AuditablePayloadEvent<>(this, "xyz");
|
||||
|
||||
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
|
||||
ac.publishEvent(event);
|
||||
assertThat(ac.getBean(AuditableListener.class).events.contains(event)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgrammaticEventListener() {
|
||||
List<Auditable> events = new ArrayList<>();
|
||||
ApplicationListener<AuditablePayloadEvent<String>> listener = events::add;
|
||||
|
||||
ConfigurableApplicationContext ac = new GenericApplicationContext();
|
||||
ac.addApplicationListener(listener);
|
||||
ac.refresh();
|
||||
|
||||
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
|
||||
ac.publishEvent(event);
|
||||
assertThat(events.contains(event)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProgrammaticPayloadListener() {
|
||||
List<String> events = new ArrayList<>();
|
||||
ApplicationListener<PayloadApplicationEvent<String>> listener = ApplicationListener.forPayload(events::add);
|
||||
|
||||
ConfigurableApplicationContext ac = new GenericApplicationContext();
|
||||
ac.addApplicationListener(listener);
|
||||
ac.refresh();
|
||||
|
||||
AuditablePayloadEvent<String> event = new AuditablePayloadEvent<>(this, "xyz");
|
||||
ac.publishEvent(event);
|
||||
assertThat(events.contains(event.getPayload())).isTrue();
|
||||
}
|
||||
|
||||
|
||||
public interface Auditable {
|
||||
}
|
||||
|
||||
@@ -55,20 +55,20 @@ dependencies {
|
||||
optional("io.reactivex.rxjava3:rxjava")
|
||||
optional("io.netty:netty-buffer")
|
||||
testCompile("io.projectreactor:reactor-test")
|
||||
testCompile("javax.annotation:javax.annotation-api")
|
||||
testCompile("com.google.code.findbugs:jsr305")
|
||||
testCompile("org.xmlunit:xmlunit-assertj")
|
||||
testCompile("org.xmlunit:xmlunit-matchers")
|
||||
testCompile("javax.annotation:javax.annotation-api")
|
||||
testCompile("javax.xml.bind:jaxb-api")
|
||||
testCompile("com.fasterxml.woodstox:woodstox-core")
|
||||
testCompile("org.xmlunit:xmlunit-assertj")
|
||||
testCompile("org.xmlunit:xmlunit-matchers")
|
||||
testCompile(project(":kotlin-coroutines"))
|
||||
testCompile("io.projectreactor.tools:blockhound")
|
||||
testFixturesImplementation("com.google.code.findbugs:jsr305")
|
||||
testFixturesImplementation("io.projectreactor:reactor-test")
|
||||
testFixturesImplementation("org.assertj:assertj-core")
|
||||
testFixturesImplementation("com.google.code.findbugs:jsr305")
|
||||
testFixturesImplementation("org.junit.platform:junit-platform-launcher")
|
||||
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api")
|
||||
testFixturesImplementation("org.junit.jupiter:junit-jupiter-params")
|
||||
testFixturesImplementation("org.assertj:assertj-core")
|
||||
testFixturesImplementation("org.xmlunit:xmlunit-assertj")
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ jar {
|
||||
}
|
||||
|
||||
test {
|
||||
// make sure the classes dir is used on the test classpath (required by ResourceTests)
|
||||
// Make sure the classes dir is used on the test classpath (required by ResourceTests)
|
||||
// When test fixtures are involved, the JAR is used by default
|
||||
classpath = sourceSets.main.output.classesDirs + classpath - files(jar.archiveFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.codec;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.openjdk.jmh.annotations.Benchmark;
|
||||
import org.openjdk.jmh.annotations.BenchmarkMode;
|
||||
import org.openjdk.jmh.annotations.Level;
|
||||
import org.openjdk.jmh.annotations.Mode;
|
||||
import org.openjdk.jmh.annotations.Param;
|
||||
import org.openjdk.jmh.annotations.Scope;
|
||||
import org.openjdk.jmh.annotations.Setup;
|
||||
import org.openjdk.jmh.annotations.State;
|
||||
import org.openjdk.jmh.infra.Blackhole;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.util.MimeType;
|
||||
|
||||
/**
|
||||
* Benchmarks for {@link DataBufferUtils}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
@BenchmarkMode(Mode.Throughput)
|
||||
public class StringDecoderBenchmark {
|
||||
|
||||
@Benchmark
|
||||
public void parseSseLines(SseLinesState state, Blackhole blackhole) {
|
||||
blackhole.consume(state.parseLines().blockLast());
|
||||
}
|
||||
|
||||
|
||||
@State(Scope.Benchmark)
|
||||
@SuppressWarnings({"NotNullFieldNotInitialized", "ConstantConditions"})
|
||||
public static class SseLinesState {
|
||||
|
||||
private static final Charset CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final ResolvableType ELEMENT_TYPE = ResolvableType.forClass(String.class);
|
||||
|
||||
|
||||
@Param("10240")
|
||||
int totalSize;
|
||||
|
||||
@Param("2000")
|
||||
int chunkSize;
|
||||
|
||||
List<DataBuffer> chunks;
|
||||
|
||||
StringDecoder decoder = StringDecoder.textPlainOnly(Arrays.asList("\r\n", "\n"), false);
|
||||
|
||||
MimeType mimeType = new MimeType("text", "plain", CHARSET);
|
||||
|
||||
|
||||
@Setup(Level.Trial)
|
||||
public void setup() {
|
||||
String eventTemplate = "id:$1\n" +
|
||||
"event:some-event\n" +
|
||||
":some-comment-$1-aa\n" +
|
||||
":some-comment-$1-bb\n" +
|
||||
"data:abcdefg-$1-hijklmnop-$1-qrstuvw-$1-xyz-$1\n\n";
|
||||
|
||||
int eventLength = String.format(eventTemplate, String.format("%05d", 1)).length();
|
||||
int eventCount = this.totalSize / eventLength;
|
||||
DataBufferFactory bufferFactory = new DefaultDataBufferFactory();
|
||||
|
||||
this.chunks = Flux.range(1, eventCount)
|
||||
.map(index -> String.format(eventTemplate, String.format("%05d", index)))
|
||||
.buffer(this.chunkSize > eventLength ? this.chunkSize / eventLength : 1)
|
||||
.map(strings -> String.join("", strings))
|
||||
.map(chunk -> {
|
||||
byte[] bytes = chunk.getBytes(CHARSET);
|
||||
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
|
||||
buffer.write(bytes);
|
||||
return buffer;
|
||||
})
|
||||
.collectList()
|
||||
.block();
|
||||
}
|
||||
|
||||
public Flux<String> parseLines() {
|
||||
Flux<DataBuffer> input = Flux.fromIterable(this.chunks).doOnNext(DataBufferUtils::retain);
|
||||
return this.decoder.decode(input, ELEMENT_TYPE, this.mimeType, Collections.emptyMap());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -53,7 +53,7 @@ public interface Opcodes {
|
||||
/**
|
||||
* <i>Experimental, use at your own risk. This field will be renamed when it becomes stable, this
|
||||
* will break existing code using it. Only code compiled with --enable-preview can use this.</i>
|
||||
* <p>SPRING PATCH: no preview mode check for ASM 9 experimental, enabling it by default.
|
||||
* <p>SPRING PATCH: no preview mode check for ASM 10 experimental, enabling it by default.
|
||||
*/
|
||||
int ASM10_EXPERIMENTAL = 1 << 24 | 10 << 16 | 0 << 8;
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ import org.springframework.asm.Opcodes;
|
||||
final class AsmApi {
|
||||
|
||||
/**
|
||||
* SPRING PATCH: always returns ASM7.
|
||||
* SPRING PATCH: always returns ASM9.
|
||||
*/
|
||||
static int value() {
|
||||
return Opcodes.ASM7;
|
||||
return Opcodes.ASM9;
|
||||
}
|
||||
|
||||
private AsmApi() {
|
||||
|
||||
@@ -77,7 +77,6 @@ public abstract class KotlinDetector {
|
||||
|
||||
/**
|
||||
* Return {@code true} if the method is a suspending function.
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3
|
||||
*/
|
||||
public static boolean isSuspendingFunction(Method method) {
|
||||
|
||||
@@ -264,14 +264,12 @@ public class ReactiveAdapterRegistry {
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.multiValue(io.reactivex.Observable.class, io.reactivex.Observable::empty),
|
||||
source -> ((io.reactivex.Observable<?>) source).toFlowable(io.reactivex.BackpressureStrategy.BUFFER),
|
||||
source -> io.reactivex.Flowable.fromPublisher(source)
|
||||
.toObservable()
|
||||
io.reactivex.Observable::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.Single.class),
|
||||
source -> ((io.reactivex.Single<?>) source).toFlowable(),
|
||||
source -> io.reactivex.Flowable.fromPublisher(source)
|
||||
.toObservable().singleElement().toSingle()
|
||||
io.reactivex.Single::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleOptionalValue(io.reactivex.Maybe.class, io.reactivex.Maybe::empty),
|
||||
@@ -282,8 +280,7 @@ public class ReactiveAdapterRegistry {
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.noValue(io.reactivex.Completable.class, io.reactivex.Completable::complete),
|
||||
source -> ((io.reactivex.Completable) source).toFlowable(),
|
||||
source -> io.reactivex.Flowable.fromPublisher(source)
|
||||
.toObservable().ignoreElements()
|
||||
io.reactivex.Completable::fromPublisher
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -304,30 +301,26 @@ public class ReactiveAdapterRegistry {
|
||||
io.reactivex.rxjava3.core.Observable::empty),
|
||||
source -> ((io.reactivex.rxjava3.core.Observable<?>) source).toFlowable(
|
||||
io.reactivex.rxjava3.core.BackpressureStrategy.BUFFER),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable()
|
||||
io.reactivex.rxjava3.core.Observable::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.rxjava3.core.Single.class),
|
||||
source -> ((io.reactivex.rxjava3.core.Single<?>) source).toFlowable(),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable().singleElement().toSingle()
|
||||
io.reactivex.rxjava3.core.Single::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.singleOptionalValue(
|
||||
io.reactivex.rxjava3.core.Maybe.class,
|
||||
io.reactivex.rxjava3.core.Maybe::empty),
|
||||
source -> ((io.reactivex.rxjava3.core.Maybe<?>) source).toFlowable(),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable().singleElement()
|
||||
io.reactivex.rxjava3.core.Maybe::fromPublisher
|
||||
);
|
||||
registry.registerReactiveType(
|
||||
ReactiveTypeDescriptor.noValue(
|
||||
io.reactivex.rxjava3.core.Completable.class,
|
||||
io.reactivex.rxjava3.core.Completable::complete),
|
||||
source -> ((io.reactivex.rxjava3.core.Completable) source).toFlowable(),
|
||||
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
|
||||
.toObservable().ignoreElements()
|
||||
io.reactivex.rxjava3.core.Completable::fromPublisher
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,8 +18,8 @@ package org.springframework.core.annotation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.IntFunction;
|
||||
@@ -31,10 +31,11 @@ import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Collector implementations that provide various reduction operations for
|
||||
* {@link Collector} implementations that provide various reduction operations for
|
||||
* {@link MergedAnnotation} instances.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @since 5.2
|
||||
*/
|
||||
public abstract class MergedAnnotationCollectors {
|
||||
@@ -52,13 +53,16 @@ public abstract class MergedAnnotationCollectors {
|
||||
* Create a new {@link Collector} that accumulates merged annotations to a
|
||||
* {@link LinkedHashSet} containing {@linkplain MergedAnnotation#synthesize()
|
||||
* synthesized} versions.
|
||||
* <p>The collector returned by this method is effectively equivalent to
|
||||
* {@code Collectors.mapping(MergedAnnotation::synthesize, Collectors.toCollection(LinkedHashSet::new))}
|
||||
* but avoids the creation of a composite collector.
|
||||
* @param <A> the annotation type
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
* annotations into a {@link Set}
|
||||
*/
|
||||
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Set<A>> toAnnotationSet() {
|
||||
return Collector.of(ArrayList<A>::new, (list, annotation) -> list.add(annotation.synthesize()),
|
||||
MergedAnnotationCollectors::addAll, LinkedHashSet::new);
|
||||
return Collector.of(LinkedHashSet::new, (set, annotation) -> set.add(annotation.synthesize()),
|
||||
MergedAnnotationCollectors::combiner);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,14 +94,14 @@ public abstract class MergedAnnotationCollectors {
|
||||
IntFunction<R[]> generator) {
|
||||
|
||||
return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()),
|
||||
MergedAnnotationCollectors::addAll, list -> list.toArray(generator.apply(list.size())));
|
||||
MergedAnnotationCollectors::combiner, list -> list.toArray(generator.apply(list.size())));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Collector} that accumulates merged annotations to an
|
||||
* Create a new {@link Collector} that accumulates merged annotations to a
|
||||
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
|
||||
* added} from each merged annotation
|
||||
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
|
||||
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
|
||||
* @param <A> the annotation type
|
||||
* @param adaptations the adaptations that should be applied to the annotation values
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
@@ -111,13 +115,13 @@ public abstract class MergedAnnotationCollectors {
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link Collector} that accumulates merged annotations to an
|
||||
* Create a new {@link Collector} that accumulates merged annotations to a
|
||||
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
|
||||
* added} from each merged annotation
|
||||
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
|
||||
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
|
||||
* @param <A> the annotation type
|
||||
* @param adaptations the adaptations that should be applied to the annotation values
|
||||
* @param finisher the finisher function for the new {@link MultiValueMap}
|
||||
* @param adaptations the adaptations that should be applied to the annotation values
|
||||
* @return a {@link Collector} which collects and synthesizes the
|
||||
* annotations into a {@link LinkedMultiValueMap}
|
||||
* @see #toMultiValueMap(MergedAnnotation.Adapt...)
|
||||
@@ -130,7 +134,7 @@ public abstract class MergedAnnotationCollectors {
|
||||
IDENTITY_FINISH_CHARACTERISTICS : NO_CHARACTERISTICS);
|
||||
return Collector.of(LinkedMultiValueMap::new,
|
||||
(map, annotation) -> annotation.asMap(adaptations).forEach(map::add),
|
||||
MergedAnnotationCollectors::merge, finisher, characteristics);
|
||||
MergedAnnotationCollectors::combiner, finisher, characteristics);
|
||||
}
|
||||
|
||||
|
||||
@@ -138,13 +142,22 @@ public abstract class MergedAnnotationCollectors {
|
||||
return instance == candidate;
|
||||
}
|
||||
|
||||
private static <E, L extends List<E>> L addAll(L list, L additions) {
|
||||
list.addAll(additions);
|
||||
return list;
|
||||
/**
|
||||
* {@link Collector#combiner() Combiner} for collections.
|
||||
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
|
||||
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
|
||||
*/
|
||||
private static <E, C extends Collection<E>> C combiner(C collection, C additions) {
|
||||
collection.addAll(additions);
|
||||
return collection;
|
||||
}
|
||||
|
||||
private static <K, V> MultiValueMap<K, V> merge(MultiValueMap<K, V> map,
|
||||
MultiValueMap<K, V> additions) {
|
||||
/**
|
||||
* {@link Collector#combiner() Combiner} for multi-value maps.
|
||||
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
|
||||
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
|
||||
*/
|
||||
private static <K, V> MultiValueMap<K, V> combiner(MultiValueMap<K, V> map, MultiValueMap<K, V> additions) {
|
||||
map.addAll(additions);
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -21,21 +21,20 @@ import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.io.buffer.DataBuffer;
|
||||
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||
import org.springframework.core.io.buffer.DataBufferWrapper;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.core.io.buffer.LimitedDataBufferList;
|
||||
import org.springframework.core.io.buffer.PooledDataBuffer;
|
||||
import org.springframework.core.log.LogFormatUtils;
|
||||
@@ -45,12 +44,12 @@ import org.springframework.util.MimeType;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* Decode from a data buffer stream to a {@code String} stream. Before decoding, this decoder
|
||||
* realigns the incoming data buffers so that each buffer ends with a newline.
|
||||
* This is to make sure that multibyte characters are decoded properly, and do not cross buffer
|
||||
* boundaries. The default delimiters ({@code \n}, {@code \r\n})can be customized.
|
||||
*
|
||||
* <p>Partially inspired by Netty's {@code DelimiterBasedFrameDecoder}.
|
||||
* Decode from a data buffer stream to a {@code String} stream, either splitting
|
||||
* or aggregating incoming data chunks to realign along newlines delimiters
|
||||
* and produce a stream of strings. This is useful for streaming but is also
|
||||
* necessary to ensure that that multibyte characters can be decoded correctly,
|
||||
* avoiding split-character issues. The default delimiters used by default are
|
||||
* {@code \n} and {@code \r\n} but that can be customized.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Brian Clozel
|
||||
@@ -115,21 +114,22 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
|
||||
byte[][] delimiterBytes = getDelimiterBytes(mimeType);
|
||||
|
||||
Flux<DataBuffer> inputFlux = Flux.defer(() -> {
|
||||
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes);
|
||||
LimitedDataBufferList chunks = new LimitedDataBufferList(getMaxInMemorySize());
|
||||
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delimiterBytes);
|
||||
|
||||
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
||||
LimitChecker limiter = new LimitChecker(getMaxInMemorySize());
|
||||
|
||||
return Flux.from(input)
|
||||
.concatMapIterable(buffer -> endFrameAfterDelimiter(buffer, matcher))
|
||||
.doOnNext(limiter)
|
||||
.bufferUntil(buffer -> buffer instanceof EndFrameBuffer)
|
||||
.map(list -> joinAndStrip(list, this.stripDelimiter))
|
||||
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
|
||||
});
|
||||
|
||||
return super.decode(inputFlux, elementType, mimeType, hints);
|
||||
return Flux.from(input)
|
||||
.concatMapIterable(buffer -> processDataBuffer(buffer, matcher, chunks))
|
||||
.concatWith(Mono.defer(() -> {
|
||||
if (chunks.isEmpty()) {
|
||||
return Mono.empty();
|
||||
}
|
||||
DataBuffer lastBuffer = chunks.get(0).factory().join(chunks);
|
||||
chunks.clear();
|
||||
return Mono.just(lastBuffer);
|
||||
}))
|
||||
.doOnTerminate(chunks::releaseAndClear)
|
||||
.doOnDiscard(PooledDataBuffer.class, PooledDataBuffer::release)
|
||||
.map(buffer -> decode(buffer, elementType, mimeType, hints));
|
||||
}
|
||||
|
||||
private byte[][] getDelimiterBytes(@Nullable MimeType mimeType) {
|
||||
@@ -142,6 +142,43 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
});
|
||||
}
|
||||
|
||||
private Collection<DataBuffer> processDataBuffer(
|
||||
DataBuffer buffer, DataBufferUtils.Matcher matcher, LimitedDataBufferList chunks) {
|
||||
|
||||
try {
|
||||
List<DataBuffer> result = null;
|
||||
do {
|
||||
int endIndex = matcher.match(buffer);
|
||||
if (endIndex == -1) {
|
||||
chunks.add(buffer);
|
||||
DataBufferUtils.retain(buffer); // retain after add (may raise DataBufferLimitException)
|
||||
break;
|
||||
}
|
||||
int startIndex = buffer.readPosition();
|
||||
int length = (endIndex - startIndex + 1);
|
||||
DataBuffer slice = buffer.retainedSlice(startIndex, length);
|
||||
if (this.stripDelimiter) {
|
||||
slice.writePosition(slice.writePosition() - matcher.delimiter().length);
|
||||
}
|
||||
result = (result != null ? result : new ArrayList<>());
|
||||
if (chunks.isEmpty()) {
|
||||
result.add(slice);
|
||||
}
|
||||
else {
|
||||
chunks.add(slice);
|
||||
result.add(buffer.factory().join(chunks));
|
||||
chunks.clear();
|
||||
}
|
||||
buffer.readPosition(endIndex + 1);
|
||||
}
|
||||
while (buffer.readableByteCount() > 0);
|
||||
return (result != null ? result : Collections.emptyList());
|
||||
}
|
||||
finally {
|
||||
DataBufferUtils.release(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String decode(DataBuffer dataBuffer, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
@@ -166,68 +203,6 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first match and longest delimiter, {@link EndFrameBuffer} just after it.
|
||||
* @param dataBuffer the buffer to find delimiters in
|
||||
* @param matcher used to find the first delimiters
|
||||
* @return a flux of buffers, containing {@link EndFrameBuffer} after each delimiter that was
|
||||
* found in {@code dataBuffer}. Returns Flux, because returning List (w/ flatMapIterable)
|
||||
* results in memory leaks due to pre-fetching.
|
||||
*/
|
||||
private static List<DataBuffer> endFrameAfterDelimiter(DataBuffer dataBuffer, DataBufferUtils.Matcher matcher) {
|
||||
List<DataBuffer> result = new ArrayList<>();
|
||||
try {
|
||||
do {
|
||||
int endIdx = matcher.match(dataBuffer);
|
||||
if (endIdx != -1) {
|
||||
int readPosition = dataBuffer.readPosition();
|
||||
int length = (endIdx - readPosition + 1);
|
||||
DataBuffer slice = dataBuffer.retainedSlice(readPosition, length);
|
||||
result.add(slice);
|
||||
result.add(new EndFrameBuffer(matcher.delimiter()));
|
||||
dataBuffer.readPosition(endIdx + 1);
|
||||
}
|
||||
else {
|
||||
result.add(DataBufferUtils.retain(dataBuffer));
|
||||
break;
|
||||
}
|
||||
}
|
||||
while (dataBuffer.readableByteCount() > 0);
|
||||
}
|
||||
finally {
|
||||
DataBufferUtils.release(dataBuffer);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins the given list of buffers. If the list ends with a {@link EndFrameBuffer}, it is
|
||||
* removed. If {@code stripDelimiter} is {@code true} and the resulting buffer ends with
|
||||
* a delimiter, it is removed.
|
||||
* @param dataBuffers the data buffers to join
|
||||
* @param stripDelimiter whether to strip the delimiter
|
||||
* @return the joined buffer
|
||||
*/
|
||||
private static DataBuffer joinAndStrip(List<DataBuffer> dataBuffers, boolean stripDelimiter) {
|
||||
Assert.state(!dataBuffers.isEmpty(), "DataBuffers should not be empty");
|
||||
|
||||
byte[] matchingDelimiter = null;
|
||||
|
||||
int lastIdx = dataBuffers.size() - 1;
|
||||
DataBuffer lastBuffer = dataBuffers.get(lastIdx);
|
||||
if (lastBuffer instanceof EndFrameBuffer) {
|
||||
matchingDelimiter = ((EndFrameBuffer) lastBuffer).delimiter();
|
||||
dataBuffers.remove(lastIdx);
|
||||
}
|
||||
|
||||
DataBuffer result = dataBuffers.get(0).factory().join(dataBuffers);
|
||||
if (stripDelimiter && matchingDelimiter != null) {
|
||||
result.writePosition(result.writePosition() - matchingDelimiter.length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@code StringDecoder} for {@code "text/plain"}.
|
||||
* @param stripDelimiter this flag is ignored
|
||||
@@ -285,46 +260,4 @@ public final class StringDecoder extends AbstractDataBufferDecoder<String> {
|
||||
new MimeType("text", "plain", DEFAULT_CHARSET), MimeTypeUtils.ALL);
|
||||
}
|
||||
|
||||
|
||||
private static class EndFrameBuffer extends DataBufferWrapper {
|
||||
|
||||
private static final DataBuffer BUFFER = DefaultDataBufferFactory.sharedInstance.wrap(new byte[0]);
|
||||
|
||||
private final byte[] delimiter;
|
||||
|
||||
public EndFrameBuffer(byte[] delimiter) {
|
||||
super(BUFFER);
|
||||
this.delimiter = delimiter;
|
||||
}
|
||||
|
||||
public byte[] delimiter() {
|
||||
return this.delimiter;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class LimitChecker implements Consumer<DataBuffer> {
|
||||
|
||||
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
|
||||
private final LimitedDataBufferList list;
|
||||
|
||||
LimitChecker(int maxInMemorySize) {
|
||||
this.list = new LimitedDataBufferList(maxInMemorySize);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void accept(DataBuffer buffer) {
|
||||
if (buffer instanceof EndFrameBuffer) {
|
||||
this.list.clear();
|
||||
}
|
||||
try {
|
||||
this.list.add(buffer);
|
||||
}
|
||||
catch (DataBufferLimitException ex) {
|
||||
DataBufferUtils.release(buffer);
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -118,7 +118,7 @@ public final class Property {
|
||||
}
|
||||
|
||||
|
||||
// package private
|
||||
// Package private
|
||||
|
||||
MethodParameter getMethodParameter() {
|
||||
return this.methodParameter;
|
||||
@@ -132,7 +132,7 @@ public final class Property {
|
||||
}
|
||||
|
||||
|
||||
// internal helpers
|
||||
// Internal helpers
|
||||
|
||||
private String resolveName() {
|
||||
if (this.readMethod != null) {
|
||||
@@ -142,10 +142,13 @@ public final class Property {
|
||||
}
|
||||
else {
|
||||
index = this.readMethod.getName().indexOf("is");
|
||||
if (index == -1) {
|
||||
throw new IllegalArgumentException("Not a getter method");
|
||||
if (index != -1) {
|
||||
index += 2;
|
||||
}
|
||||
else {
|
||||
// Record-style plain accessor method, e.g. name()
|
||||
index = 0;
|
||||
}
|
||||
index += 2;
|
||||
}
|
||||
return StringUtils.uncapitalize(this.readMethod.getName().substring(index));
|
||||
}
|
||||
|
||||
+307
-190
@@ -30,7 +30,6 @@ import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
@@ -568,65 +567,290 @@ public abstract class DataBufferUtils {
|
||||
|
||||
/**
|
||||
* Return a {@link Matcher} for the given delimiter.
|
||||
* The matcher can be used to find the delimiters in data buffers.
|
||||
* The matcher can be used to find the delimiters in a stream of data buffers.
|
||||
* @param delimiter the delimiter bytes to find
|
||||
* @return the matcher
|
||||
* @since 5.2
|
||||
*/
|
||||
public static Matcher matcher(byte[] delimiter) {
|
||||
Assert.isTrue(delimiter.length > 0, "Delimiter must not be empty");
|
||||
return new KnuthMorrisPrattMatcher(delimiter);
|
||||
return createMatcher(delimiter);
|
||||
}
|
||||
|
||||
/** Return a {@link Matcher} for the given delimiters.
|
||||
* The matcher can be used to find the delimiters in data buffers.
|
||||
/**
|
||||
* Return a {@link Matcher} for the given delimiters.
|
||||
* The matcher can be used to find the delimiters in a stream of data buffers.
|
||||
* @param delimiters the delimiters bytes to find
|
||||
* @return the matcher
|
||||
* @since 5.2
|
||||
*/
|
||||
public static Matcher matcher(byte[]... delimiters) {
|
||||
Assert.isTrue(delimiters.length > 0, "Delimiters must not be empty");
|
||||
if (delimiters.length == 1) {
|
||||
return matcher(delimiters[0]);
|
||||
}
|
||||
else {
|
||||
Matcher[] matchers = new Matcher[delimiters.length];
|
||||
for (int i = 0; i < delimiters.length; i++) {
|
||||
matchers[i] = matcher(delimiters[i]);
|
||||
}
|
||||
return new CompositeMatcher(matchers);
|
||||
return (delimiters.length == 1 ? createMatcher(delimiters[0]) : new CompositeMatcher(delimiters));
|
||||
}
|
||||
|
||||
private static NestedMatcher createMatcher(byte[] delimiter) {
|
||||
Assert.isTrue(delimiter.length > 0, "Delimiter must not be empty");
|
||||
switch (delimiter.length) {
|
||||
case 1:
|
||||
return (delimiter[0] == 10 ? SingleByteMatcher.NEWLINE_MATCHER : new SingleByteMatcher(delimiter));
|
||||
case 2:
|
||||
return new TwoByteMatcher(delimiter);
|
||||
default:
|
||||
return new KnuthMorrisPrattMatcher(delimiter);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Defines an object that matches a data buffer against a delimiter.
|
||||
* Contract to find delimiter(s) against one or more data buffers that can
|
||||
* be passed one at a time to the {@link #match(DataBuffer)} method.
|
||||
*
|
||||
* @since 5.2
|
||||
* @see #match(DataBuffer)
|
||||
*/
|
||||
public interface Matcher {
|
||||
|
||||
/**
|
||||
* Returns the position of the final matching delimiter byte that matches the given buffer,
|
||||
* or {@code -1} if not found.
|
||||
* @param dataBuffer the buffer in which to search for the delimiter
|
||||
* @return the position of the final matching delimiter, or {@code -1} if not found.
|
||||
* Find the first matching delimiter and return the index of the last
|
||||
* byte of the delimiter, or {@code -1} if not found.
|
||||
*/
|
||||
int match(DataBuffer dataBuffer);
|
||||
|
||||
/**
|
||||
* Return the delimiter used for this matcher.
|
||||
* @return the delimiter
|
||||
* Return the delimiter from the last invocation of {@link #match(DataBuffer)}.
|
||||
*/
|
||||
byte[] delimiter();
|
||||
|
||||
/**
|
||||
* Resets the state of this matcher.
|
||||
* Reset the state of this matcher.
|
||||
*/
|
||||
void reset();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Matcher that supports searching for multiple delimiters.
|
||||
*/
|
||||
private static class CompositeMatcher implements Matcher {
|
||||
|
||||
private static final byte[] NO_DELIMITER = new byte[0];
|
||||
|
||||
|
||||
private final NestedMatcher[] matchers;
|
||||
|
||||
byte[] longestDelimiter = NO_DELIMITER;
|
||||
|
||||
CompositeMatcher(byte[][] delimiters) {
|
||||
this.matchers = initMatchers(delimiters);
|
||||
}
|
||||
|
||||
private static NestedMatcher[] initMatchers(byte[][] delimiters) {
|
||||
NestedMatcher[] matchers = new NestedMatcher[delimiters.length];
|
||||
for (int i = 0; i < delimiters.length; i++) {
|
||||
matchers[i] = createMatcher(delimiters[i]);
|
||||
}
|
||||
return matchers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
this.longestDelimiter = NO_DELIMITER;
|
||||
|
||||
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
|
||||
byte b = dataBuffer.getByte(pos);
|
||||
|
||||
for (NestedMatcher matcher : this.matchers) {
|
||||
if (matcher.match(b) && matcher.delimiter().length > this.longestDelimiter.length) {
|
||||
this.longestDelimiter = matcher.delimiter();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.longestDelimiter != NO_DELIMITER) {
|
||||
reset();
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] delimiter() {
|
||||
Assert.state(this.longestDelimiter != NO_DELIMITER, "Illegal state!");
|
||||
return this.longestDelimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
for (NestedMatcher matcher : this.matchers) {
|
||||
matcher.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Matcher that can be nested within {@link CompositeMatcher} where multiple
|
||||
* matchers advance together using the same index, one byte at a time.
|
||||
*/
|
||||
private interface NestedMatcher extends Matcher {
|
||||
|
||||
/**
|
||||
* Perform a match against the next byte of the stream and return true
|
||||
* if the delimiter is fully matched.
|
||||
*/
|
||||
boolean match(byte b);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Matcher for a single byte delimiter.
|
||||
*/
|
||||
private static class SingleByteMatcher implements NestedMatcher {
|
||||
|
||||
static SingleByteMatcher NEWLINE_MATCHER = new SingleByteMatcher(new byte[] {10});
|
||||
|
||||
private final byte[] delimiter;
|
||||
|
||||
SingleByteMatcher(byte[] delimiter) {
|
||||
Assert.isTrue(delimiter.length == 1, "Expected a 1 byte delimiter");
|
||||
this.delimiter = delimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
|
||||
byte b = dataBuffer.getByte(pos);
|
||||
if (match(b)) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(byte b) {
|
||||
return this.delimiter[0] == b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] delimiter() {
|
||||
return this.delimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base class for a {@link NestedMatcher}.
|
||||
*/
|
||||
private static abstract class AbstractNestedMatcher implements NestedMatcher {
|
||||
|
||||
private final byte[] delimiter;
|
||||
|
||||
private int matches = 0;
|
||||
|
||||
|
||||
protected AbstractNestedMatcher(byte[] delimiter) {
|
||||
this.delimiter = delimiter;
|
||||
}
|
||||
|
||||
protected void setMatches(int index) {
|
||||
this.matches = index;
|
||||
}
|
||||
|
||||
protected int getMatches() {
|
||||
return this.matches;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
for (int pos = dataBuffer.readPosition(); pos < dataBuffer.writePosition(); pos++) {
|
||||
byte b = dataBuffer.getByte(pos);
|
||||
if (match(b)) {
|
||||
reset();
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(byte b) {
|
||||
if (b == this.delimiter[this.matches]) {
|
||||
this.matches++;
|
||||
return (this.matches == delimiter().length);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] delimiter() {
|
||||
return this.delimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.matches = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Matcher with a 2 byte delimiter that does not benefit from a
|
||||
* Knuth-Morris-Pratt suffix-prefix table.
|
||||
*/
|
||||
private static class TwoByteMatcher extends AbstractNestedMatcher {
|
||||
|
||||
protected TwoByteMatcher(byte[] delimiter) {
|
||||
super(delimiter);
|
||||
Assert.isTrue(delimiter.length == 2, "Expected a 2-byte delimiter");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of {@link Matcher} that uses the Knuth-Morris-Pratt algorithm.
|
||||
* @see <a href="https://www.nayuki.io/page/knuth-morris-pratt-string-matching">Knuth-Morris-Pratt string matching</a>
|
||||
*/
|
||||
private static class KnuthMorrisPrattMatcher extends AbstractNestedMatcher {
|
||||
|
||||
private final int[] table;
|
||||
|
||||
public KnuthMorrisPrattMatcher(byte[] delimiter) {
|
||||
super(delimiter);
|
||||
this.table = longestSuffixPrefixTable(delimiter);
|
||||
}
|
||||
|
||||
private static int[] longestSuffixPrefixTable(byte[] delimiter) {
|
||||
int[] result = new int[delimiter.length];
|
||||
result[0] = 0;
|
||||
for (int i = 1; i < delimiter.length; i++) {
|
||||
int j = result[i - 1];
|
||||
while (j > 0 && delimiter[i] != delimiter[j]) {
|
||||
j = result[j - 1];
|
||||
}
|
||||
if (delimiter[i] == delimiter[j]) {
|
||||
j++;
|
||||
}
|
||||
result[i] = j;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(byte b) {
|
||||
while (getMatches() > 0 && b != delimiter()[getMatches()]) {
|
||||
setMatches(this.table[getMatches() - 1]);
|
||||
}
|
||||
return super.match(b);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class ReadableByteChannelGenerator implements Consumer<SynchronousSink<DataBuffer>> {
|
||||
|
||||
private final ReadableByteChannel channel;
|
||||
@@ -683,9 +907,7 @@ public abstract class DataBufferUtils {
|
||||
|
||||
private final AtomicLong position;
|
||||
|
||||
private final AtomicBoolean reading = new AtomicBoolean();
|
||||
|
||||
private final AtomicBoolean disposed = new AtomicBoolean();
|
||||
private final AtomicReference<State> state = new AtomicReference<>(State.IDLE);
|
||||
|
||||
public ReadCompletionHandler(AsynchronousFileChannel channel,
|
||||
FluxSink<DataBuffer> sink, long position, DataBufferFactory dataBufferFactory, int bufferSize) {
|
||||
@@ -697,39 +919,68 @@ public abstract class DataBufferUtils {
|
||||
this.bufferSize = bufferSize;
|
||||
}
|
||||
|
||||
public void read() {
|
||||
if (this.sink.requestedFromDownstream() > 0 &&
|
||||
isNotDisposed() &&
|
||||
this.reading.compareAndSet(false, true)) {
|
||||
DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(this.bufferSize);
|
||||
ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, this.bufferSize);
|
||||
this.channel.read(byteBuffer, this.position.get(), dataBuffer, this);
|
||||
/**
|
||||
* Invoked when Reactive Streams consumer signals demand.
|
||||
*/
|
||||
public void request(long n) {
|
||||
tryRead();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoked when Reactive Streams consumer cancels.
|
||||
*/
|
||||
public void cancel() {
|
||||
this.state.getAndSet(State.DISPOSED);
|
||||
|
||||
// According java.nio.channels.AsynchronousChannel "if an I/O operation is outstanding
|
||||
// on the channel and the channel's close method is invoked, then the I/O operation
|
||||
// fails with the exception AsynchronousCloseException". That should invoke the failed
|
||||
// callback below and the current DataBuffer should be released.
|
||||
|
||||
closeChannel(this.channel);
|
||||
}
|
||||
|
||||
private void tryRead() {
|
||||
if (this.sink.requestedFromDownstream() > 0 && this.state.compareAndSet(State.IDLE, State.READING)) {
|
||||
read();
|
||||
}
|
||||
}
|
||||
|
||||
private void read() {
|
||||
DataBuffer dataBuffer = this.dataBufferFactory.allocateBuffer(this.bufferSize);
|
||||
ByteBuffer byteBuffer = dataBuffer.asByteBuffer(0, this.bufferSize);
|
||||
this.channel.read(byteBuffer, this.position.get(), dataBuffer, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completed(Integer read, DataBuffer dataBuffer) {
|
||||
if (isNotDisposed()) {
|
||||
if (read != -1) {
|
||||
this.position.addAndGet(read);
|
||||
dataBuffer.writePosition(read);
|
||||
this.sink.next(dataBuffer);
|
||||
this.reading.set(false);
|
||||
read();
|
||||
}
|
||||
else {
|
||||
release(dataBuffer);
|
||||
closeChannel(this.channel);
|
||||
if (this.disposed.compareAndSet(false, true)) {
|
||||
this.sink.complete();
|
||||
}
|
||||
this.reading.set(false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.state.get().equals(State.DISPOSED)) {
|
||||
release(dataBuffer);
|
||||
closeChannel(this.channel);
|
||||
this.reading.set(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (read == -1) {
|
||||
release(dataBuffer);
|
||||
closeChannel(this.channel);
|
||||
this.state.set(State.DISPOSED);
|
||||
this.sink.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
this.position.addAndGet(read);
|
||||
dataBuffer.writePosition(read);
|
||||
this.sink.next(dataBuffer);
|
||||
|
||||
// Stay in READING mode if there is demand
|
||||
if (this.sink.requestedFromDownstream() > 0) {
|
||||
read();
|
||||
return;
|
||||
}
|
||||
|
||||
// Release READING mode and then try again in case of concurrent "request"
|
||||
if (this.state.compareAndSet(State.READING, State.IDLE)) {
|
||||
tryRead();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -737,26 +988,12 @@ public abstract class DataBufferUtils {
|
||||
public void failed(Throwable exc, DataBuffer dataBuffer) {
|
||||
release(dataBuffer);
|
||||
closeChannel(this.channel);
|
||||
if (this.disposed.compareAndSet(false, true)) {
|
||||
this.sink.error(exc);
|
||||
}
|
||||
this.reading.set(false);
|
||||
this.state.set(State.DISPOSED);
|
||||
this.sink.error(exc);
|
||||
}
|
||||
|
||||
public void request(long n) {
|
||||
read();
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
if (this.disposed.compareAndSet(false, true)) {
|
||||
if (!this.reading.get()) {
|
||||
closeChannel(this.channel);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isNotDisposed() {
|
||||
return !this.disposed.get();
|
||||
private enum State {
|
||||
IDLE, READING, DISPOSED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -895,124 +1132,4 @@ public abstract class DataBufferUtils {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of {@link Matcher} that uses the Knuth-Morris-Pratt algorithm.
|
||||
* @see <a href="https://www.nayuki.io/page/knuth-morris-pratt-string-matching">Knuth-Morris-Pratt string matching</a>
|
||||
*/
|
||||
private static class KnuthMorrisPrattMatcher implements Matcher {
|
||||
|
||||
private final byte[] delimiter;
|
||||
|
||||
private final int[] table;
|
||||
|
||||
private int matches = 0;
|
||||
|
||||
public KnuthMorrisPrattMatcher(byte[] delimiter) {
|
||||
this.delimiter = Arrays.copyOf(delimiter, delimiter.length);
|
||||
this.table = longestSuffixPrefixTable(delimiter);
|
||||
}
|
||||
|
||||
private static int[] longestSuffixPrefixTable(byte[] delimiter) {
|
||||
int[] result = new int[delimiter.length];
|
||||
result[0] = 0;
|
||||
for (int i = 1; i < delimiter.length; i++) {
|
||||
int j = result[i - 1];
|
||||
while (j > 0 && delimiter[i] != delimiter[j]) {
|
||||
j = result[j - 1];
|
||||
}
|
||||
if (delimiter[i] == delimiter[j]) {
|
||||
j++;
|
||||
}
|
||||
result[i] = j;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
for (int i = dataBuffer.readPosition(); i < dataBuffer.writePosition(); i++) {
|
||||
byte b = dataBuffer.getByte(i);
|
||||
|
||||
while (this.matches > 0 && b != this.delimiter[this.matches]) {
|
||||
this.matches = this.table[this.matches - 1];
|
||||
}
|
||||
|
||||
if (b == this.delimiter[this.matches]) {
|
||||
this.matches++;
|
||||
if (this.matches == this.delimiter.length) {
|
||||
reset();
|
||||
return i;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] delimiter() {
|
||||
return Arrays.copyOf(this.delimiter, this.delimiter.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
this.matches = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Implementation of {@link Matcher} that wraps several other matchers.
|
||||
*/
|
||||
private static class CompositeMatcher implements Matcher {
|
||||
|
||||
private static final byte[] NO_DELIMITER = new byte[0];
|
||||
|
||||
private final Matcher[] matchers;
|
||||
|
||||
byte[] longestDelimiter = NO_DELIMITER;
|
||||
|
||||
public CompositeMatcher(Matcher[] matchers) {
|
||||
this.matchers = matchers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int match(DataBuffer dataBuffer) {
|
||||
this.longestDelimiter = NO_DELIMITER;
|
||||
int bestEndIdx = Integer.MAX_VALUE;
|
||||
|
||||
|
||||
for (Matcher matcher : this.matchers) {
|
||||
int endIdx = matcher.match(dataBuffer);
|
||||
if (endIdx != -1 &&
|
||||
endIdx <= bestEndIdx &&
|
||||
matcher.delimiter().length > this.longestDelimiter.length) {
|
||||
bestEndIdx = endIdx;
|
||||
this.longestDelimiter = matcher.delimiter();
|
||||
}
|
||||
}
|
||||
if (bestEndIdx == Integer.MAX_VALUE) {
|
||||
this.longestDelimiter = NO_DELIMITER;
|
||||
return -1;
|
||||
}
|
||||
else {
|
||||
reset();
|
||||
return bestEndIdx;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] delimiter() {
|
||||
Assert.state(this.longestDelimiter != NO_DELIMITER, "Illegal state!");
|
||||
return this.longestDelimiter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reset() {
|
||||
for (Matcher matcher : this.matchers) {
|
||||
matcher.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-4
@@ -24,10 +24,13 @@ import org.springframework.core.metrics.StartupStep;
|
||||
|
||||
/**
|
||||
* {@link ApplicationStartup} implementation for the Java Flight Recorder.
|
||||
* <p>This variant records {@link StartupStep} as Flight Recorder events; because such events
|
||||
* only support base types, the {@link StartupStep.Tags} are serialized as a single String attribute.
|
||||
* <p>Once this is configured on the application context, you can record data by launching the application
|
||||
* with recording enabled: {@code java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar app.jar}.
|
||||
* <p>This variant records {@link StartupStep} as Flight Recorder events. Because
|
||||
* such events only support base types, the
|
||||
* {@link org.springframework.core.metrics.StartupStep.Tags} are serialized as a
|
||||
* single String attribute.
|
||||
* <p>Once this is configured on the application context, you can record data by
|
||||
* launching the application with recording enabled:
|
||||
* {@code java -XX:StartFlightRecording:filename=recording.jfr,duration=10s -jar app.jar}.
|
||||
*
|
||||
* @author Brian Clozel
|
||||
* @since 5.3
|
||||
|
||||
@@ -457,7 +457,6 @@ public abstract class CollectionUtils {
|
||||
* @since 3.1
|
||||
*/
|
||||
public static <K, V> MultiValueMap<K, V> toMultiValueMap(Map<K, List<V>> targetMap) {
|
||||
Assert.notNull(targetMap, "'targetMap' must not be null");
|
||||
return new MultiValueMapAdapter<>(targetMap);
|
||||
}
|
||||
|
||||
|
||||
@@ -105,17 +105,15 @@ public class ConcurrentLruCache<K, V> {
|
||||
}
|
||||
// Generate value first, to prevent size inconsistency
|
||||
V value = this.generator.apply(key);
|
||||
int cacheSize = this.size;
|
||||
if (cacheSize == this.sizeLimit) {
|
||||
if (this.size == this.sizeLimit) {
|
||||
K leastUsed = this.queue.poll();
|
||||
if (leastUsed != null) {
|
||||
this.cache.remove(leastUsed);
|
||||
cacheSize--;
|
||||
}
|
||||
}
|
||||
this.queue.offer(key);
|
||||
this.cache.put(key, value);
|
||||
this.size = cacheSize + 1;
|
||||
this.size = this.cache.size();
|
||||
return value;
|
||||
}
|
||||
finally {
|
||||
@@ -123,6 +121,50 @@ public class ConcurrentLruCache<K, V> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the given key is present in this cache.
|
||||
* @param key the key to check for
|
||||
* @return {@code true} if the key is present,
|
||||
* {@code false} if there was no matching key
|
||||
*/
|
||||
public boolean contains(K key) {
|
||||
return this.cache.containsKey(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately remove the given key and any associated value.
|
||||
* @param key the key to evict the entry for
|
||||
* @return {@code true} if the key was present before,
|
||||
* {@code false} if there was no matching key
|
||||
*/
|
||||
public boolean remove(K key) {
|
||||
this.lock.writeLock().lock();
|
||||
try {
|
||||
boolean wasPresent = (this.cache.remove(key) != null);
|
||||
this.queue.remove(key);
|
||||
this.size = this.cache.size();
|
||||
return wasPresent;
|
||||
}
|
||||
finally {
|
||||
this.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Immediately remove all entries from this cache.
|
||||
*/
|
||||
public void clear() {
|
||||
this.lock.writeLock().lock();
|
||||
try {
|
||||
this.cache.clear();
|
||||
this.queue.clear();
|
||||
this.size = 0;
|
||||
}
|
||||
finally {
|
||||
this.lock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the current size of the cache.
|
||||
* @see #sizeLimit()
|
||||
|
||||
@@ -35,7 +35,8 @@ import java.util.Map;
|
||||
* @param <K> the key type
|
||||
* @param <V> the value element type
|
||||
*/
|
||||
public class LinkedMultiValueMap<K, V> extends MultiValueMapAdapter<K, V> implements Serializable, Cloneable {
|
||||
public class LinkedMultiValueMap<K, V> extends MultiValueMapAdapter<K, V> // new public base class in 5.3
|
||||
implements Serializable, Cloneable {
|
||||
|
||||
private static final long serialVersionUID = 3801124242820219131L;
|
||||
|
||||
|
||||
@@ -30,23 +30,30 @@ import org.springframework.lang.Nullable;
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @author Juergen Hoeller
|
||||
* @since 3.1
|
||||
* @since 5.3
|
||||
* @param <K> the key type
|
||||
* @param <V> the value element type
|
||||
* @see CollectionUtils#toMultiValueMap
|
||||
* @see LinkedMultiValueMap
|
||||
*/
|
||||
@SuppressWarnings("serial")
|
||||
class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
|
||||
public class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
|
||||
|
||||
private final Map<K, List<V>> targetMap;
|
||||
|
||||
|
||||
MultiValueMapAdapter(Map<K, List<V>> targetMap) {
|
||||
/**
|
||||
* Wrap the given target {@link Map} as a {@link MultiValueMap} adapter.
|
||||
* @param targetMap the plain target {@code Map}
|
||||
*/
|
||||
public MultiValueMapAdapter(Map<K, List<V>> targetMap) {
|
||||
Assert.notNull(targetMap, "'targetMap' must not be null");
|
||||
this.targetMap = targetMap;
|
||||
}
|
||||
|
||||
|
||||
// MultiValueMap implementation
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public V getFirst(K key) {
|
||||
@@ -96,6 +103,9 @@ class MultiValueMapAdapter<K, V> implements MultiValueMap<K, V>, Serializable {
|
||||
return singleValueMap;
|
||||
}
|
||||
|
||||
|
||||
// Map implementation
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
return this.targetMap.size();
|
||||
|
||||
@@ -128,7 +128,6 @@ public abstract class ObjectUtils {
|
||||
* @see Optional#isPresent()
|
||||
* @see ObjectUtils#isEmpty(Object[])
|
||||
* @see StringUtils#hasLength(CharSequence)
|
||||
* @see StringUtils#isEmpty(Object)
|
||||
* @see CollectionUtils#isEmpty(java.util.Collection)
|
||||
* @see CollectionUtils#isEmpty(java.util.Map)
|
||||
*/
|
||||
|
||||
@@ -91,9 +91,10 @@ public abstract class StringUtils {
|
||||
* {@link #hasLength(String)} or {@link #hasText(String)} instead.</b>
|
||||
* @param str the candidate object (possibly a {@code String})
|
||||
* @since 3.2.1
|
||||
* @see #hasLength(String)
|
||||
* @see #hasText(String)
|
||||
* @deprecated as of 5.3, in favor of {@link #hasLength(String)} and
|
||||
* {@link #hasText(String)} (or {@link ObjectUtils#isEmpty(Object)})
|
||||
*/
|
||||
@Deprecated
|
||||
public static boolean isEmpty(@Nullable Object str) {
|
||||
return (str == null || "".equals(str));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -89,7 +89,7 @@ class MethodParameterTests {
|
||||
Method method = getClass().getMethod("method", String.class, Long.TYPE);
|
||||
MethodParameter methodParameter = new MethodParameter(method, 0);
|
||||
assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode());
|
||||
assertThat(methodParameter.hashCode()).isNotEqualTo((long) longParameter.hashCode());
|
||||
assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -86,7 +86,7 @@ class SynthesizingMethodParameterTests {
|
||||
Method method = getClass().getMethod("method", String.class, Long.TYPE);
|
||||
SynthesizingMethodParameter methodParameter = new SynthesizingMethodParameter(method, 0);
|
||||
assertThat(methodParameter.hashCode()).isEqualTo(stringParameter.hashCode());
|
||||
assertThat(methodParameter.hashCode()).isNotEqualTo((long) longParameter.hashCode());
|
||||
assertThat(methodParameter.hashCode()).isNotEqualTo(longParameter.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -139,20 +139,28 @@ class StringDecoderTests extends AbstractDecoderTests<StringDecoder> {
|
||||
@Test
|
||||
void maxInMemoryLimit() {
|
||||
Flux<DataBuffer> input = Flux.just(
|
||||
stringBuffer("abc\n"), stringBuffer("defg\n"), stringBuffer("hijkl\n"));
|
||||
stringBuffer("abc\n"), stringBuffer("defg\n"),
|
||||
stringBuffer("hi"), stringBuffer("jkl"), stringBuffer("mnop"));
|
||||
|
||||
this.decoder.setMaxInMemorySize(5);
|
||||
testDecode(input, String.class, step ->
|
||||
step.expectNext("abc", "defg").verifyError(DataBufferLimitException.class));
|
||||
}
|
||||
|
||||
@Test // gh-24312
|
||||
void maxInMemoryLimitReleaseUnprocessedLinesFromCurrentBuffer() {
|
||||
@Test
|
||||
void maxInMemoryLimitDoesNotApplyToParsedItemsThatDontRequireBuffering() {
|
||||
Flux<DataBuffer> input = Flux.just(
|
||||
stringBuffer("TOO MUCH DATA\nanother line\n\nand another\n"));
|
||||
|
||||
this.decoder.setMaxInMemorySize(5);
|
||||
testDecode(input, String.class, step -> step.verifyError(DataBufferLimitException.class));
|
||||
|
||||
testDecode(input, String.class, step -> step
|
||||
.expectNext("TOO MUCH DATA")
|
||||
.expectNext("another line")
|
||||
.expectNext("")
|
||||
.expectNext("and another")
|
||||
.expectComplete()
|
||||
.verify());
|
||||
}
|
||||
|
||||
@Test // gh-24339
|
||||
|
||||
+29
-9
@@ -886,18 +886,38 @@ class DataBufferUtilsTests extends AbstractDataBufferAllocatingTests {
|
||||
void matcher2(String displayName, DataBufferFactory bufferFactory) {
|
||||
super.bufferFactory = bufferFactory;
|
||||
|
||||
DataBuffer foo = stringBuffer("fooobar");
|
||||
DataBuffer foo = stringBuffer("foooobar");
|
||||
|
||||
byte[] delims = "oo".getBytes(StandardCharsets.UTF_8);
|
||||
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims);
|
||||
int result = matcher.match(foo);
|
||||
assertThat(result).isEqualTo(2);
|
||||
foo.readPosition(2);
|
||||
result = matcher.match(foo);
|
||||
assertThat(result).isEqualTo(3);
|
||||
foo.readPosition(3);
|
||||
result = matcher.match(foo);
|
||||
assertThat(result).isEqualTo(-1);
|
||||
int endIndex = matcher.match(foo);
|
||||
assertThat(endIndex).isEqualTo(2);
|
||||
foo.readPosition(endIndex + 1);
|
||||
endIndex = matcher.match(foo);
|
||||
assertThat(endIndex).isEqualTo(4);
|
||||
foo.readPosition(endIndex + 1);
|
||||
endIndex = matcher.match(foo);
|
||||
assertThat(endIndex).isEqualTo(-1);
|
||||
|
||||
release(foo);
|
||||
}
|
||||
|
||||
@ParameterizedDataBufferAllocatingTest
|
||||
void matcher3(String displayName, DataBufferFactory bufferFactory) {
|
||||
super.bufferFactory = bufferFactory;
|
||||
|
||||
DataBuffer foo = stringBuffer("foooobar");
|
||||
|
||||
byte[] delims = "oo".getBytes(StandardCharsets.UTF_8);
|
||||
DataBufferUtils.Matcher matcher = DataBufferUtils.matcher(delims);
|
||||
int endIndex = matcher.match(foo);
|
||||
assertThat(endIndex).isEqualTo(2);
|
||||
foo.readPosition(endIndex + 1);
|
||||
endIndex = matcher.match(foo);
|
||||
assertThat(endIndex).isEqualTo(4);
|
||||
foo.readPosition(endIndex + 1);
|
||||
endIndex = matcher.match(foo);
|
||||
assertThat(endIndex).isEqualTo(-1);
|
||||
|
||||
release(foo);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Juergen Hoeller
|
||||
*/
|
||||
class ConcurrentLruCacheTests {
|
||||
|
||||
private final ConcurrentLruCache<String, String> cache = new ConcurrentLruCache<>(2, key -> key + "value");
|
||||
|
||||
|
||||
@Test
|
||||
void getAndSize() {
|
||||
assertThat(this.cache.sizeLimit()).isEqualTo(2);
|
||||
assertThat(this.cache.size()).isEqualTo(0);
|
||||
assertThat(this.cache.get("k1")).isEqualTo("k1value");
|
||||
assertThat(this.cache.size()).isEqualTo(1);
|
||||
assertThat(this.cache.contains("k1")).isTrue();
|
||||
assertThat(this.cache.get("k2")).isEqualTo("k2value");
|
||||
assertThat(this.cache.size()).isEqualTo(2);
|
||||
assertThat(this.cache.contains("k1")).isTrue();
|
||||
assertThat(this.cache.contains("k2")).isTrue();
|
||||
assertThat(this.cache.get("k3")).isEqualTo("k3value");
|
||||
assertThat(this.cache.size()).isEqualTo(2);
|
||||
assertThat(this.cache.contains("k1")).isFalse();
|
||||
assertThat(this.cache.contains("k2")).isTrue();
|
||||
assertThat(this.cache.contains("k3")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void removeAndSize() {
|
||||
assertThat(this.cache.get("k1")).isEqualTo("k1value");
|
||||
assertThat(this.cache.get("k2")).isEqualTo("k2value");
|
||||
assertThat(this.cache.size()).isEqualTo(2);
|
||||
assertThat(this.cache.contains("k1")).isTrue();
|
||||
assertThat(this.cache.contains("k2")).isTrue();
|
||||
this.cache.remove("k2");
|
||||
assertThat(this.cache.size()).isEqualTo(1);
|
||||
assertThat(this.cache.contains("k1")).isTrue();
|
||||
assertThat(this.cache.contains("k2")).isFalse();
|
||||
assertThat(this.cache.get("k3")).isEqualTo("k3value");
|
||||
assertThat(this.cache.size()).isEqualTo(2);
|
||||
assertThat(this.cache.contains("k1")).isTrue();
|
||||
assertThat(this.cache.contains("k2")).isFalse();
|
||||
assertThat(this.cache.contains("k3")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void clearAndSize() {
|
||||
assertThat(this.cache.get("k1")).isEqualTo("k1value");
|
||||
assertThat(this.cache.get("k2")).isEqualTo("k2value");
|
||||
assertThat(this.cache.size()).isEqualTo(2);
|
||||
assertThat(this.cache.contains("k1")).isTrue();
|
||||
assertThat(this.cache.contains("k2")).isTrue();
|
||||
this.cache.clear();
|
||||
assertThat(this.cache.size()).isEqualTo(0);
|
||||
assertThat(this.cache.contains("k1")).isFalse();
|
||||
assertThat(this.cache.contains("k2")).isFalse();
|
||||
assertThat(this.cache.get("k3")).isEqualTo("k3value");
|
||||
assertThat(this.cache.size()).isEqualTo(1);
|
||||
assertThat(this.cache.contains("k1")).isFalse();
|
||||
assertThat(this.cache.contains("k2")).isFalse();
|
||||
assertThat(this.cache.contains("k3")).isTrue();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,7 +24,6 @@ import org.springframework.expression.spel.CodeFlow;
|
||||
import org.springframework.expression.spel.ExpressionState;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* Represents the elvis operator ?:. For an expression "a?:b" if a is not null, the value
|
||||
@@ -52,7 +51,7 @@ public class Elvis extends SpelNodeImpl {
|
||||
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
TypedValue value = this.children[0].getValueInternal(state);
|
||||
// If this check is changed, the generateCode method will need changing too
|
||||
if (!StringUtils.isEmpty(value.getValue())) {
|
||||
if (value.getValue() != null && !"".equals(value.getValue())) {
|
||||
return value;
|
||||
}
|
||||
else {
|
||||
|
||||
+55
-30
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -63,34 +63,36 @@ import org.springframework.util.StringUtils;
|
||||
* <p>Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @since 4.1
|
||||
*/
|
||||
public final class SpelCompiler implements Opcodes {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
|
||||
|
||||
private static final int CLASSES_DEFINED_LIMIT = 100;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
|
||||
|
||||
// A compiler is created for each classloader, it manages a child class loader of that
|
||||
// classloader and the child is used to load the compiled expressions.
|
||||
private static final Map<ClassLoader, SpelCompiler> compilers = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
|
||||
// The child ClassLoader used to load the compiled expression classes
|
||||
private ChildClassLoader ccl;
|
||||
private volatile ChildClassLoader childClassLoader;
|
||||
|
||||
// Counter suffix for generated classes within this SpelCompiler instance
|
||||
private final AtomicInteger suffixId = new AtomicInteger(1);
|
||||
|
||||
|
||||
private SpelCompiler(@Nullable ClassLoader classloader) {
|
||||
this.ccl = new ChildClassLoader(classloader);
|
||||
this.childClassLoader = new ChildClassLoader(classloader);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempt compilation of the supplied expression. A check is made to see
|
||||
* if it is compilable before compilation proceeds. The check involves
|
||||
* visiting all the nodes in the expression Ast and ensuring enough state
|
||||
* visiting all the nodes in the expression AST and ensuring enough state
|
||||
* is known about them that bytecode can be generated for them.
|
||||
* @param expression the expression to compile
|
||||
* @return an instance of the class implementing the compiled expression,
|
||||
@@ -125,7 +127,7 @@ public final class SpelCompiler implements Opcodes {
|
||||
|
||||
/**
|
||||
* Generate the class that encapsulates the compiled expression and define it.
|
||||
* The generated class will be a subtype of CompiledExpression.
|
||||
* The generated class will be a subtype of CompiledExpression.
|
||||
* @param expressionToCompile the expression to be compiled
|
||||
* @return the expression call, or {@code null} if the decision was to opt out of
|
||||
* compilation during code generation
|
||||
@@ -135,7 +137,7 @@ public final class SpelCompiler implements Opcodes {
|
||||
// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'
|
||||
String className = "spel/Ex" + getNextSuffix();
|
||||
ClassWriter cw = new ExpressionClassWriter();
|
||||
cw.visit(V1_5, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
|
||||
cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
|
||||
|
||||
// Create default constructor
|
||||
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
|
||||
@@ -150,7 +152,7 @@ public final class SpelCompiler implements Opcodes {
|
||||
// Create getValue() method
|
||||
mv = cw.visitMethod(ACC_PUBLIC, "getValue",
|
||||
"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,
|
||||
new String[ ]{"org/springframework/expression/EvaluationException"});
|
||||
new String[] {"org/springframework/expression/EvaluationException"});
|
||||
mv.visitCode();
|
||||
|
||||
CodeFlow cf = new CodeFlow(className, cw);
|
||||
@@ -187,7 +189,7 @@ public final class SpelCompiler implements Opcodes {
|
||||
|
||||
/**
|
||||
* Load a compiled expression class. Makes sure the classloaders aren't used too much
|
||||
* because they anchor compiled classes in memory and prevent GC. If you have expressions
|
||||
* because they anchor compiled classes in memory and prevent GC. If you have expressions
|
||||
* continually recompiling over time then by replacing the classloader periodically
|
||||
* at least some of the older variants can be garbage collected.
|
||||
* @param name the name of the class
|
||||
@@ -196,12 +198,25 @@ public final class SpelCompiler implements Opcodes {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private Class<? extends CompiledExpression> loadClass(String name, byte[] bytes) {
|
||||
if (this.ccl.getClassesDefinedCount() > CLASSES_DEFINED_LIMIT) {
|
||||
this.ccl = new ChildClassLoader(this.ccl.getParent());
|
||||
ChildClassLoader ccl = this.childClassLoader;
|
||||
if (ccl.getClassesDefinedCount() >= CLASSES_DEFINED_LIMIT) {
|
||||
synchronized (this) {
|
||||
ChildClassLoader currentCcl = this.childClassLoader;
|
||||
if (ccl == currentCcl) {
|
||||
// Still the same ClassLoader that needs to be replaced...
|
||||
ccl = new ChildClassLoader(ccl.getParent());
|
||||
this.childClassLoader = ccl;
|
||||
}
|
||||
else {
|
||||
// Already replaced by some other thread, let's pick it up.
|
||||
ccl = currentCcl;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (Class<? extends CompiledExpression>) this.ccl.defineClass(name, bytes);
|
||||
return (Class<? extends CompiledExpression>) ccl.defineClass(name, bytes);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Factory method for compiler instances. The returned SpelCompiler will
|
||||
* attach a class loader as the child of the given class loader and this
|
||||
@@ -211,21 +226,28 @@ public final class SpelCompiler implements Opcodes {
|
||||
*/
|
||||
public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) {
|
||||
ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
|
||||
synchronized (compilers) {
|
||||
SpelCompiler compiler = compilers.get(clToUse);
|
||||
if (compiler == null) {
|
||||
compiler = new SpelCompiler(clToUse);
|
||||
compilers.put(clToUse, compiler);
|
||||
// Quick check for existing compiler without lock contention
|
||||
SpelCompiler compiler = compilers.get(clToUse);
|
||||
if (compiler == null) {
|
||||
// Full lock now since we're creating a child ClassLoader
|
||||
synchronized (compilers) {
|
||||
compiler = compilers.get(clToUse);
|
||||
if (compiler == null) {
|
||||
compiler = new SpelCompiler(clToUse);
|
||||
compilers.put(clToUse, compiler);
|
||||
}
|
||||
}
|
||||
return compiler;
|
||||
}
|
||||
return compiler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request that an attempt is made to compile the specified expression. It may fail if
|
||||
* components of the expression are not suitable for compilation or the data types
|
||||
* involved are not suitable for compilation. Used for testing.
|
||||
* @return true if the expression was successfully compiled
|
||||
* Request that an attempt is made to compile the specified expression.
|
||||
* It may fail if components of the expression are not suitable for compilation
|
||||
* or the data types involved are not suitable for compilation. Used for testing.
|
||||
* @param expression the expression to compile
|
||||
* @return {@code true} if the expression was successfully compiled,
|
||||
* {@code false} otherwise
|
||||
*/
|
||||
public static boolean compile(Expression expression) {
|
||||
return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression());
|
||||
@@ -250,24 +272,27 @@ public final class SpelCompiler implements Opcodes {
|
||||
|
||||
private static final URL[] NO_URLS = new URL[0];
|
||||
|
||||
private int classesDefinedCount = 0;
|
||||
private final AtomicInteger classesDefinedCount = new AtomicInteger(0);
|
||||
|
||||
public ChildClassLoader(@Nullable ClassLoader classLoader) {
|
||||
super(NO_URLS, classLoader);
|
||||
}
|
||||
|
||||
int getClassesDefinedCount() {
|
||||
return this.classesDefinedCount;
|
||||
}
|
||||
|
||||
public Class<?> defineClass(String name, byte[] bytes) {
|
||||
Class<?> clazz = super.defineClass(name, bytes, 0, bytes.length);
|
||||
this.classesDefinedCount++;
|
||||
this.classesDefinedCount.incrementAndGet();
|
||||
return clazz;
|
||||
}
|
||||
|
||||
public int getClassesDefinedCount() {
|
||||
return this.classesDefinedCount.get();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* An ASM ClassWriter extension bound to the SpelCompiler's ClassLoader.
|
||||
*/
|
||||
private class ExpressionClassWriter extends ClassWriter {
|
||||
|
||||
public ExpressionClassWriter() {
|
||||
@@ -276,7 +301,7 @@ public final class SpelCompiler implements Opcodes {
|
||||
|
||||
@Override
|
||||
protected ClassLoader getClassLoader() {
|
||||
return ccl;
|
||||
return childClassLoader;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -395,6 +395,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
|
||||
if (method == null) {
|
||||
method = findMethodForProperty(getPropertyMethodSuffixes(propertyName),
|
||||
"is", clazz, mustBeStatic, 0, BOOLEAN_TYPES);
|
||||
if (method == null) {
|
||||
// Record-style plain accessor method, e.g. name()
|
||||
method = findMethodForProperty(new String[] {propertyName},
|
||||
"", clazz, mustBeStatic, 0, ANY_TYPES);
|
||||
}
|
||||
}
|
||||
return method;
|
||||
}
|
||||
@@ -683,12 +688,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
|
||||
return true;
|
||||
}
|
||||
getterName = "is" + StringUtils.capitalize(name);
|
||||
return getterName.equals(method.getName());
|
||||
}
|
||||
else {
|
||||
Field field = (Field) this.member;
|
||||
return field.getName().equals(name);
|
||||
if (getterName.equals(method.getName())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this.member.getName().equals(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+16
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,6 +36,7 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext;
|
||||
import org.springframework.expression.spel.support.StandardEvaluationContext;
|
||||
import org.springframework.expression.spel.testresources.Inventor;
|
||||
import org.springframework.expression.spel.testresources.Person;
|
||||
import org.springframework.expression.spel.testresources.RecordPerson;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@@ -191,6 +192,20 @@ public class PropertyAccessTests extends AbstractExpressionTests {
|
||||
parser.parseExpression("name='p3'").getValue(context, target));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void propertyReadOnlyWithRecordStyle() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
|
||||
Expression expr = parser.parseExpression("name");
|
||||
RecordPerson target1 = new RecordPerson("p1");
|
||||
assertThat(expr.getValue(context, target1)).isEqualTo("p1");
|
||||
RecordPerson target2 = new RecordPerson("p2");
|
||||
assertThat(expr.getValue(context, target2)).isEqualTo("p2");
|
||||
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
|
||||
parser.parseExpression("name='p3'").getValue(context, target2));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void propertyReadWrite() {
|
||||
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
|
||||
|
||||
+19
-25
@@ -4180,6 +4180,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value4");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value4");
|
||||
|
||||
// record-style accessor
|
||||
expression = parser.parseExpression("strawberry");
|
||||
assertCantCompile(expression);
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value5");
|
||||
assertCanCompile(expression);
|
||||
assertThat(expression.getValue(tc)).isEqualTo("value5");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -4553,23 +4560,9 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
Object v = expression.getValue(ctx,holder);
|
||||
assertThat(v).isEqualTo("abc");
|
||||
|
||||
// // time it interpreted
|
||||
// long stime = System.currentTimeMillis();
|
||||
// for (int i = 0; i < 100000; i++) {
|
||||
// v = expression.getValue(ctx,holder);
|
||||
// }
|
||||
// System.out.println((System.currentTimeMillis() - stime));
|
||||
|
||||
assertCanCompile(expression);
|
||||
v = expression.getValue(ctx,holder);
|
||||
assertThat(v).isEqualTo("abc");
|
||||
|
||||
// // time it compiled
|
||||
// stime = System.currentTimeMillis();
|
||||
// for (int i = 0; i < 100000; i++) {
|
||||
// v = expression.getValue(ctx,holder);
|
||||
// }
|
||||
// System.out.println((System.currentTimeMillis() - stime));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -4985,13 +4978,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(fast.compileExpression()).isTrue();
|
||||
r.setValue2(null);
|
||||
// try the numbers 0,1,2,null
|
||||
for (int i=0;i<4;i++) {
|
||||
r.setValue(i<3?i:null);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
r.setValue(i < 3 ? i : null);
|
||||
boolean slowResult = (Boolean)slow.getValue(ctx);
|
||||
boolean fastResult = (Boolean)fast.getValue(ctx);
|
||||
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
|
||||
assertThat(fastResult).as(" Differing results: expression="+expressionText+
|
||||
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
|
||||
assertThat(fastResult).as("Differing results: expression=" + expressionText +
|
||||
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5002,13 +4994,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
assertThat(fast.compileExpression()).isTrue();
|
||||
Reg r = (Reg)ctx.getRootObject().getValue();
|
||||
// try the numbers 0,1,2,null
|
||||
for (int i=0;i<4;i++) {
|
||||
r.setValue(i<3?i:null);
|
||||
for (int i = 0; i < 4; i++) {
|
||||
r.setValue(i < 3 ? i : null);
|
||||
boolean slowResult = (Boolean)slow.getValue(ctx);
|
||||
boolean fastResult = (Boolean)fast.getValue(ctx);
|
||||
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
|
||||
assertThat(fastResult).as(" Differing results: expression="+expressionText+
|
||||
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
|
||||
assertThat(fastResult).as("Differing results: expression=" + expressionText +
|
||||
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5839,7 +5830,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
|
||||
public String orange = "value1";
|
||||
public static String apple = "value2";
|
||||
|
||||
public long peach = 34L;
|
||||
|
||||
public String getBanana() {
|
||||
@@ -5849,6 +5839,10 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
public static String getPlum() {
|
||||
return "value4";
|
||||
}
|
||||
|
||||
public String strawberry() {
|
||||
return "value5";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
-1
@@ -76,7 +76,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
*/
|
||||
public class SpelReproTests extends AbstractExpressionTests {
|
||||
|
||||
|
||||
@Test
|
||||
public void NPE_SPR5661() {
|
||||
evaluate("joinThreeStrings('a',null,'c')", "anullc", String.class);
|
||||
|
||||
+3
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -89,8 +89,8 @@ public class ReflectionHelperTests extends AbstractExpressionTests {
|
||||
assertThat(tv1).isNotEqualTo(tv3);
|
||||
assertThat(tv2).isNotEqualTo(tv3);
|
||||
assertThat(tv2.hashCode()).isEqualTo(tv1.hashCode());
|
||||
assertThat(tv3.hashCode()).isNotEqualTo((long) tv1.hashCode());
|
||||
assertThat(tv3.hashCode()).isNotEqualTo((long) tv2.hashCode());
|
||||
assertThat(tv3.hashCode()).isNotEqualTo(tv1.hashCode());
|
||||
assertThat(tv3.hashCode()).isNotEqualTo(tv2.hashCode());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,9 +16,10 @@
|
||||
|
||||
package org.springframework.expression.spel.testresources;
|
||||
|
||||
///CLOVER:OFF
|
||||
public class Person {
|
||||
|
||||
private String privateName;
|
||||
|
||||
Company company;
|
||||
|
||||
public Person(String name) {
|
||||
@@ -41,4 +42,5 @@ public class Person {
|
||||
public Company getCompany() {
|
||||
return company;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.expression.spel.testresources;
|
||||
|
||||
public class RecordPerson {
|
||||
|
||||
private String name;
|
||||
|
||||
private Company company;
|
||||
|
||||
public RecordPerson(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public RecordPerson(String name, Company company) {
|
||||
this.name = name;
|
||||
this.company = company;
|
||||
}
|
||||
|
||||
public String name() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Company company() {
|
||||
return company;
|
||||
}
|
||||
|
||||
}
|
||||
+21
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,19 +19,25 @@ package org.springframework.expression.spel.testresources;
|
||||
import java.util.List;
|
||||
|
||||
public class TestAddress{
|
||||
private String street;
|
||||
private List<String> crossStreets;
|
||||
|
||||
public String getStreet() {
|
||||
return street;
|
||||
}
|
||||
public void setStreet(String street) {
|
||||
this.street = street;
|
||||
}
|
||||
public List<String> getCrossStreets() {
|
||||
return crossStreets;
|
||||
}
|
||||
public void setCrossStreets(List<String> crossStreets) {
|
||||
this.crossStreets = crossStreets;
|
||||
}
|
||||
private String street;
|
||||
|
||||
private List<String> crossStreets;
|
||||
|
||||
public String getStreet() {
|
||||
return street;
|
||||
}
|
||||
|
||||
public void setStreet(String street) {
|
||||
this.street = street;
|
||||
}
|
||||
|
||||
public List<String> getCrossStreets() {
|
||||
return crossStreets;
|
||||
}
|
||||
|
||||
public void setCrossStreets(List<String> crossStreets) {
|
||||
this.crossStreets = crossStreets;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-15
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,19 +17,25 @@
|
||||
package org.springframework.expression.spel.testresources;
|
||||
|
||||
public class TestPerson {
|
||||
private String name;
|
||||
private TestAddress address;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
public TestAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
public void setAddress(TestAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
private String name;
|
||||
|
||||
private TestAddress address;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public TestAddress getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
public void setAddress(TestAddress address) {
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2012 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,7 +36,7 @@ public interface SqlProvider {
|
||||
/**
|
||||
* Return the SQL string for this object, i.e.
|
||||
* typically the SQL used for creating statements.
|
||||
* @return the SQL string, or {@code null}
|
||||
* @return the SQL string, or {@code null} if not available
|
||||
*/
|
||||
@Nullable
|
||||
String getSql();
|
||||
|
||||
+11
-9
@@ -637,21 +637,23 @@ public class CallMetaDataContext {
|
||||
schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
|
||||
}
|
||||
|
||||
String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName());
|
||||
if (isFunction() || isReturnValueRequired()) {
|
||||
callString = new StringBuilder().append("{? = call ").
|
||||
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
|
||||
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
|
||||
append(procedureNameToUse).append("(");
|
||||
callString = new StringBuilder("{? = call ");
|
||||
parameterCount = -1;
|
||||
}
|
||||
else {
|
||||
callString = new StringBuilder().append("{call ").
|
||||
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
|
||||
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
|
||||
append(procedureNameToUse).append("(");
|
||||
callString = new StringBuilder("{call ");
|
||||
}
|
||||
|
||||
if (StringUtils.hasLength(catalogNameToUse)) {
|
||||
callString.append(catalogNameToUse).append(".");
|
||||
}
|
||||
if (StringUtils.hasLength(schemaNameToUse)) {
|
||||
callString.append(schemaNameToUse).append(".");
|
||||
}
|
||||
callString.append(this.metaDataProvider.procedureNameToUse(getProcedureName()));
|
||||
callString.append("(");
|
||||
|
||||
for (SqlParameter parameter : this.callParameters) {
|
||||
if (!parameter.isResultsParameter()) {
|
||||
if (parameterCount > 0) {
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -83,7 +83,7 @@ public abstract class NamedParameterUtils {
|
||||
Assert.notNull(sql, "SQL must not be null");
|
||||
|
||||
Set<String> namedParameters = new HashSet<>();
|
||||
String sqlToUse = sql;
|
||||
StringBuilder sqlToUse = new StringBuilder(sql);
|
||||
List<ParameterHolder> parameterList = new ArrayList<>();
|
||||
|
||||
char[] statement = sql.toCharArray();
|
||||
@@ -155,7 +155,7 @@ public abstract class NamedParameterUtils {
|
||||
int j = i + 1;
|
||||
if (j < statement.length && statement[j] == ':') {
|
||||
// escaped ":" should be skipped
|
||||
sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1);
|
||||
sqlToUse.deleteCharAt(i - escapes);
|
||||
escapes++;
|
||||
i = i + 2;
|
||||
continue;
|
||||
@@ -174,7 +174,7 @@ public abstract class NamedParameterUtils {
|
||||
}
|
||||
i++;
|
||||
}
|
||||
ParsedSql parsedSql = new ParsedSql(sqlToUse);
|
||||
ParsedSql parsedSql = new ParsedSql(sqlToUse.toString());
|
||||
for (ParameterHolder ph : parameterList) {
|
||||
parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -28,11 +28,11 @@ import java.util.List;
|
||||
*/
|
||||
public class ParsedSql {
|
||||
|
||||
private String originalSql;
|
||||
private final String originalSql;
|
||||
|
||||
private List<String> parameterNames = new ArrayList<>();
|
||||
private final List<String> parameterNames = new ArrayList<>();
|
||||
|
||||
private List<int[]> parameterIndexes = new ArrayList<>();
|
||||
private final List<int[]> parameterIndexes = new ArrayList<>();
|
||||
|
||||
private int namedParameterCount;
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
description = "Spring Messaging"
|
||||
|
||||
apply plugin: "kotlin"
|
||||
apply plugin: "kotlinx-serialization"
|
||||
|
||||
dependencies {
|
||||
compile(project(":spring-beans"))
|
||||
@@ -17,6 +18,7 @@ dependencies {
|
||||
optional("javax.xml.bind:jaxb-api")
|
||||
optional("com.google.protobuf:protobuf-java-util")
|
||||
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||
optional("org.jetbrains.kotlinx:kotlinx-serialization-json")
|
||||
testCompile(project(":kotlin-coroutines"))
|
||||
testCompile(testFixtures(project(":spring-core")))
|
||||
testCompile("javax.inject:javax.inject-tck")
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.messaging.converter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlinx.serialization.KSerializer;
|
||||
import kotlinx.serialization.SerializersKt;
|
||||
import kotlinx.serialization.json.Json;
|
||||
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
import org.springframework.util.FileCopyUtils;
|
||||
|
||||
/**
|
||||
* Implementation of {@link MessageConverter} that can read and write JSON
|
||||
* using <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3
|
||||
*/
|
||||
public class KotlinSerializationJsonMessageConverter extends AbstractJsonMessageConverter {
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private final Json json;
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new {@code KotlinSerializationJsonMessageConverter} with default configuration.
|
||||
*/
|
||||
public KotlinSerializationJsonMessageConverter() {
|
||||
this(Json.Default);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new {@code KotlinSerializationJsonMessageConverter} with the given delegate.
|
||||
* @param json the Json instance to use
|
||||
*/
|
||||
public KotlinSerializationJsonMessageConverter(Json json) {
|
||||
this.json = json;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object fromJson(Reader reader, Type resolvedType) {
|
||||
try {
|
||||
return fromJson(FileCopyUtils.copyToString(reader), resolvedType);
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new MessageConversionException("Could not read JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object fromJson(String payload, Type resolvedType) {
|
||||
return this.json.decodeFromString(serializer(resolvedType), payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void toJson(Object payload, Type resolvedType, Writer writer) {
|
||||
try {
|
||||
writer.write(toJson(payload, resolvedType).toCharArray());
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String toJson(Object payload, Type resolvedType) {
|
||||
return this.json.encodeToString(serializer(resolvedType), payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find a serializer that can marshall or unmarshall instances of the given type
|
||||
* using kotlinx.serialization. If no serializer can be found, an exception is thrown.
|
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls.
|
||||
* @param type the type to find a serializer for
|
||||
* @return a resolved serializer for the given type
|
||||
* @throws RuntimeException if no serializer supporting the given type can be found
|
||||
*/
|
||||
private KSerializer<Object> serializer(Type type) {
|
||||
KSerializer<Object> serializer = serializerCache.get(type);
|
||||
if (serializer == null) {
|
||||
serializer = SerializersKt.serializer(type);
|
||||
serializerCache.put(type, serializer);
|
||||
}
|
||||
return serializer;
|
||||
}
|
||||
}
|
||||
+8
-7
@@ -247,14 +247,15 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter {
|
||||
if (byte[].class == getSerializedPayloadClass()) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream(1024);
|
||||
JsonEncoding encoding = getJsonEncoding(getMimeType(headers));
|
||||
JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding);
|
||||
if (view != null) {
|
||||
this.objectMapper.writerWithView(view).writeValue(generator, payload);
|
||||
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(out, encoding)) {
|
||||
if (view != null) {
|
||||
this.objectMapper.writerWithView(view).writeValue(generator, payload);
|
||||
}
|
||||
else {
|
||||
this.objectMapper.writeValue(generator, payload);
|
||||
}
|
||||
payload = out.toByteArray();
|
||||
}
|
||||
else {
|
||||
this.objectMapper.writeValue(generator, payload);
|
||||
}
|
||||
payload = out.toByteArray();
|
||||
}
|
||||
else {
|
||||
// Assuming a text-based target payload
|
||||
|
||||
+4
-23
@@ -21,8 +21,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import kotlin.reflect.KFunction;
|
||||
import kotlin.reflect.jvm.ReflectJvmMapping;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.reactivestreams.Publisher;
|
||||
@@ -123,7 +121,6 @@ public abstract class AbstractEncoderMethodReturnValueHandler implements Handler
|
||||
handleEncodedContent(Flux.from(publisher), returnType, message));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Flux<DataBuffer> encodeContent(
|
||||
@Nullable Object content, MethodParameter returnType, DataBufferFactory bufferFactory,
|
||||
@Nullable MimeType mimeType, Map<String, Object> hints) {
|
||||
@@ -135,11 +132,10 @@ public abstract class AbstractEncoderMethodReturnValueHandler implements Handler
|
||||
ResolvableType elementType;
|
||||
if (adapter != null) {
|
||||
publisher = adapter.toPublisher(content);
|
||||
boolean isUnwrapped = KotlinDetector.isKotlinReflectPresent() &&
|
||||
KotlinDetector.isKotlinType(returnType.getContainingClass()) &&
|
||||
KotlinDelegate.isSuspend(returnType.getMethod()) &&
|
||||
!COROUTINES_FLOW_CLASS_NAME.equals(returnValueType.toClass().getName());
|
||||
ResolvableType genericType = isUnwrapped ? returnValueType : returnValueType.getGeneric();
|
||||
Method method = returnType.getMethod();
|
||||
boolean isUnwrapped = (method != null && KotlinDetector.isSuspendingFunction(method) &&
|
||||
!COROUTINES_FLOW_CLASS_NAME.equals(returnValueType.toClass().getName()));
|
||||
ResolvableType genericType = (isUnwrapped ? returnValueType : returnValueType.getGeneric());
|
||||
elementType = getElementType(adapter, genericType);
|
||||
}
|
||||
else {
|
||||
@@ -218,19 +214,4 @@ public abstract class AbstractEncoderMethodReturnValueHandler implements Handler
|
||||
*/
|
||||
protected abstract Mono<Void> handleNoContent(MethodParameter returnType, Message<?> message);
|
||||
|
||||
|
||||
/**
|
||||
* Inner class to avoid a hard dependency on Kotlin at runtime.
|
||||
*/
|
||||
private static class KotlinDelegate {
|
||||
|
||||
private static boolean isSuspend(@Nullable Method method) {
|
||||
if (method == null) {
|
||||
return false;
|
||||
}
|
||||
KFunction<?> function = ReflectJvmMapping.getKotlinFunction(method);
|
||||
return (function != null && function.isSuspend());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-2
@@ -177,8 +177,7 @@ class MessagingRSocket implements RSocket {
|
||||
}
|
||||
})
|
||||
.thenMany(Flux.defer(() -> responseRef.get() != null ?
|
||||
responseRef.get() :
|
||||
Mono.error(new IllegalStateException("Something went wrong: reply Mono not set"))));
|
||||
responseRef.get() : Mono.error(new IllegalStateException("Expected response"))));
|
||||
}
|
||||
|
||||
private DataBuffer retainDataAndReleasePayload(Payload payload) {
|
||||
|
||||
+6
@@ -125,6 +125,12 @@ public abstract class AbstractBrokerMessageHandler
|
||||
return this.brokerChannel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return destination prefixes prefixes to use to filter messages to forward
|
||||
* to the broker. Messages that have a destination and where the destination
|
||||
* doesn't match are ignored.
|
||||
* <p>By default this is not set.
|
||||
*/
|
||||
public Collection<String> getDestinationPrefixes() {
|
||||
return this.destinationPrefixes;
|
||||
}
|
||||
|
||||
+7
@@ -36,6 +36,7 @@ import org.springframework.messaging.converter.CompositeMessageConverter;
|
||||
import org.springframework.messaging.converter.DefaultContentTypeResolver;
|
||||
import org.springframework.messaging.converter.GsonMessageConverter;
|
||||
import org.springframework.messaging.converter.JsonbMessageConverter;
|
||||
import org.springframework.messaging.converter.KotlinSerializationJsonMessageConverter;
|
||||
import org.springframework.messaging.converter.MappingJackson2MessageConverter;
|
||||
import org.springframework.messaging.converter.MessageConverter;
|
||||
import org.springframework.messaging.converter.StringMessageConverter;
|
||||
@@ -101,6 +102,8 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
|
||||
|
||||
private static final boolean jsonbPresent;
|
||||
|
||||
private static final boolean kotlinSerializationJsonPresent;
|
||||
|
||||
|
||||
static {
|
||||
ClassLoader classLoader = AbstractMessageBrokerConfiguration.class.getClassLoader();
|
||||
@@ -108,6 +111,7 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
|
||||
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
||||
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
|
||||
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
|
||||
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
|
||||
}
|
||||
|
||||
|
||||
@@ -411,6 +415,9 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC
|
||||
else if (jsonbPresent) {
|
||||
converters.add(new JsonbMessageConverter());
|
||||
}
|
||||
else if (kotlinSerializationJsonPresent) {
|
||||
converters.add(new KotlinSerializationJsonMessageConverter());
|
||||
}
|
||||
}
|
||||
return new CompositeMessageConverter(converters);
|
||||
}
|
||||
|
||||
+28
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2018 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -21,6 +21,7 @@ import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.SubscribableChannel;
|
||||
import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler;
|
||||
import org.springframework.messaging.tcp.TcpOperations;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,9 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
@Nullable
|
||||
private TcpOperations<byte[]> tcpClient;
|
||||
|
||||
@Nullable
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
private boolean autoStartup = true;
|
||||
|
||||
@Nullable
|
||||
@@ -178,8 +182,27 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
* specified are effectively ignored.
|
||||
* @since 4.3.15
|
||||
*/
|
||||
public void setTcpClient(TcpOperations<byte[]> tcpClient) {
|
||||
public StompBrokerRelayRegistration setTcpClient(TcpOperations<byte[]> tcpClient) {
|
||||
this.tcpClient = tcpClient;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Some STOMP clients (e.g. stomp-js) always send heartbeats at a fixed rate
|
||||
* but others (Spring STOMP client) do so only when no other messages are
|
||||
* sent. However messages with a non-broker {@link #getDestinationPrefixes()
|
||||
* destination prefix} aren't forwarded and as a result the broker may deem
|
||||
* the connection inactive.
|
||||
* <p>When this {@link TaskScheduler} is set, it is used to reset a count of
|
||||
* the number of messages sent from client to broker since the beginning of
|
||||
* the current heartbeat period. This is then used to decide whether to send
|
||||
* a heartbeat to the broker when ignoring a message with a non-broker
|
||||
* destination prefix.
|
||||
* @since 5.3
|
||||
*/
|
||||
public StompBrokerRelayRegistration setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -233,7 +256,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
|
||||
@Override
|
||||
protected StompBrokerRelayMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
|
||||
|
||||
StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(
|
||||
getClientInboundChannel(), getClientOutboundChannel(),
|
||||
brokerChannel, getDestinationPrefixes());
|
||||
@@ -259,6 +281,9 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
|
||||
if (this.tcpClient != null) {
|
||||
handler.setTcpClient(this.tcpClient);
|
||||
}
|
||||
if (this.taskScheduler != null) {
|
||||
handler.setTaskScheduler(this.taskScheduler);
|
||||
}
|
||||
|
||||
handler.setAutoStartup(this.autoStartup);
|
||||
|
||||
|
||||
+1
-2
@@ -16,7 +16,6 @@
|
||||
|
||||
package org.springframework.messaging.simp.stomp;
|
||||
|
||||
import org.springframework.messaging.tcp.TcpConnectionHandler;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
|
||||
/**
|
||||
@@ -30,7 +29,7 @@ import org.springframework.util.concurrent.ListenableFuture;
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 4.2
|
||||
*/
|
||||
public interface ConnectionHandlingStompSession extends StompSession, TcpConnectionHandler<byte[]> {
|
||||
public interface ConnectionHandlingStompSession extends StompSession, StompTcpConnectionHandler<byte[]> {
|
||||
|
||||
/**
|
||||
* Return a future that will complete when the session is ready for use.
|
||||
|
||||
+9
-1
@@ -45,6 +45,7 @@ import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.AlternativeJdkIdGenerator;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.IdGenerator;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureCallback;
|
||||
@@ -133,6 +134,13 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StompHeaderAccessor getConnectHeaders() {
|
||||
StompHeaderAccessor accessor = createHeaderAccessor(StompCommand.CONNECT);
|
||||
accessor.addNativeHeaders(this.connectHeaders);
|
||||
return accessor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the configured session handler.
|
||||
*/
|
||||
@@ -256,7 +264,7 @@ public class DefaultStompSession implements ConnectionHandlingStompSession {
|
||||
private Message<byte[]> createMessage(StompHeaderAccessor accessor, @Nullable Object payload) {
|
||||
accessor.updateSimpMessageHeadersFromStompHeaders();
|
||||
Message<byte[]> message;
|
||||
if (StringUtils.isEmpty(payload) || (payload instanceof byte[] && ((byte[]) payload).length == 0)) {
|
||||
if (ObjectUtils.isEmpty(payload)) {
|
||||
message = MessageBuilder.createMessage(EMPTY_PAYLOAD, accessor.getMessageHeaders());
|
||||
}
|
||||
else {
|
||||
|
||||
+144
-45
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -41,10 +41,10 @@ import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.messaging.support.MessageHeaderInitializer;
|
||||
import org.springframework.messaging.tcp.FixedIntervalReconnectStrategy;
|
||||
import org.springframework.messaging.tcp.TcpConnection;
|
||||
import org.springframework.messaging.tcp.TcpConnectionHandler;
|
||||
import org.springframework.messaging.tcp.TcpOperations;
|
||||
import org.springframework.messaging.tcp.reactor.ReactorNettyCodec;
|
||||
import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureCallback;
|
||||
@@ -99,13 +99,15 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
|
||||
private static final ListenableFutureTask<Void> EMPTY_TASK = new ListenableFutureTask<>(new VoidCallable());
|
||||
|
||||
private static final Message<byte[]> HEARTBEAT_MESSAGE;
|
||||
private static final StompHeaderAccessor HEART_BEAT_ACCESSOR;
|
||||
|
||||
private static final Message<byte[]> HEARTBEAT_MESSAGE;
|
||||
|
||||
static {
|
||||
EMPTY_TASK.run();
|
||||
StompHeaderAccessor accessor = StompHeaderAccessor.createForHeartbeat();
|
||||
HEARTBEAT_MESSAGE = MessageBuilder.createMessage(StompDecoder.HEARTBEAT_PAYLOAD, accessor.getMessageHeaders());
|
||||
HEART_BEAT_ACCESSOR = StompHeaderAccessor.createForHeartbeat();
|
||||
HEARTBEAT_MESSAGE = MessageBuilder.createMessage(
|
||||
StompDecoder.HEARTBEAT_PAYLOAD, HEART_BEAT_ACCESSOR.getMessageHeaders());
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +140,10 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
|
||||
private final DefaultStats stats = new DefaultStats();
|
||||
|
||||
private final Map<String, StompConnectionHandler> connectionHandlers = new ConcurrentHashMap<>();
|
||||
private final Map<String, RelayConnectionHandler> connectionHandlers = new ConcurrentHashMap<>();
|
||||
|
||||
@Nullable
|
||||
private TaskScheduler taskScheduler;
|
||||
|
||||
|
||||
/**
|
||||
@@ -404,6 +409,22 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
return this.connectionHandlers.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the {@link TaskScheduler} to use to reset client-to-broker
|
||||
* message count in the current heartbeat period. For more details, see
|
||||
* {@link org.springframework.messaging.simp.config.StompBrokerRelayRegistration#setTaskScheduler(TaskScheduler)}.
|
||||
* @param taskScheduler the scheduler to use
|
||||
* @since 5.3
|
||||
*/
|
||||
public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
|
||||
this.taskScheduler = taskScheduler;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TaskScheduler getTaskScheduler() {
|
||||
return this.taskScheduler;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void startInternal() {
|
||||
@@ -429,11 +450,15 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
logger.debug("Forwarding " + accessor.getShortLogMessage(EMPTY_PAYLOAD));
|
||||
}
|
||||
|
||||
SystemStompConnectionHandler handler = new SystemStompConnectionHandler(accessor);
|
||||
SystemSessionConnectionHandler handler = new SystemSessionConnectionHandler(accessor);
|
||||
this.connectionHandlers.put(handler.getSessionId(), handler);
|
||||
|
||||
this.stats.incrementConnectCount();
|
||||
this.tcpClient.connect(handler, new FixedIntervalReconnectStrategy(5000));
|
||||
|
||||
if (this.taskScheduler != null) {
|
||||
this.taskScheduler.scheduleWithFixedDelay(new ClientSendMessageCountTask(), 5000);
|
||||
}
|
||||
}
|
||||
|
||||
private ReactorNettyTcpClient<byte[]> initTcpClient() {
|
||||
@@ -469,7 +494,7 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
throw new MessageDeliveryException("Message broker not active. Consider subscribing to " +
|
||||
"receive BrokerAvailabilityEvent's from an ApplicationListener Spring bean.");
|
||||
}
|
||||
StompConnectionHandler handler = this.connectionHandlers.get(sessionId);
|
||||
RelayConnectionHandler handler = this.connectionHandlers.get(sessionId);
|
||||
if (handler != null) {
|
||||
handler.sendStompErrorFrameToClient("Broker not available.");
|
||||
handler.clearConnection();
|
||||
@@ -526,11 +551,6 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
stompAccessor.setSessionId(sessionId);
|
||||
}
|
||||
|
||||
String destination = stompAccessor.getDestination();
|
||||
if (command != null && command.requiresDestination() && !checkDestinationPrefix(destination)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(stompAccessor.getShortLogMessage(EMPTY_PAYLOAD));
|
||||
@@ -541,14 +561,14 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
if (getVirtualHost() != null) {
|
||||
stompAccessor.setHost(getVirtualHost());
|
||||
}
|
||||
StompConnectionHandler handler = new StompConnectionHandler(sessionId, stompAccessor);
|
||||
RelayConnectionHandler handler = new RelayConnectionHandler(sessionId, stompAccessor);
|
||||
this.connectionHandlers.put(sessionId, handler);
|
||||
this.stats.incrementConnectCount();
|
||||
Assert.state(this.tcpClient != null, "No TCP client available");
|
||||
this.tcpClient.connect(handler);
|
||||
}
|
||||
else if (StompCommand.DISCONNECT.equals(command)) {
|
||||
StompConnectionHandler handler = this.connectionHandlers.get(sessionId);
|
||||
RelayConnectionHandler handler = this.connectionHandlers.get(sessionId);
|
||||
if (handler == null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Ignoring DISCONNECT in session " + sessionId + ". Connection already cleaned up.");
|
||||
@@ -559,13 +579,23 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
handler.forward(message, stompAccessor);
|
||||
}
|
||||
else {
|
||||
StompConnectionHandler handler = this.connectionHandlers.get(sessionId);
|
||||
RelayConnectionHandler handler = this.connectionHandlers.get(sessionId);
|
||||
if (handler == null) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("No TCP connection for session " + sessionId + " in " + message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String destination = stompAccessor.getDestination();
|
||||
if (command != null && command.requiresDestination() && !checkDestinationPrefix(destination)) {
|
||||
// Not a broker destination but send a heartbeat to keep the connection
|
||||
if (handler.shouldSendHeartbeatForIgnoredMessage()) {
|
||||
handler.forward(HEARTBEAT_MESSAGE, HEART_BEAT_ACCESSOR);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
handler.forward(message, stompAccessor);
|
||||
}
|
||||
}
|
||||
@@ -580,14 +610,14 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
}
|
||||
|
||||
|
||||
private class StompConnectionHandler implements TcpConnectionHandler<byte[]> {
|
||||
private class RelayConnectionHandler implements StompTcpConnectionHandler<byte[]> {
|
||||
|
||||
private final String sessionId;
|
||||
|
||||
private final boolean isRemoteClientSession;
|
||||
|
||||
private final StompHeaderAccessor connectHeaders;
|
||||
|
||||
private final boolean isRemoteClientSession;
|
||||
|
||||
private final MessageChannel outboundChannel;
|
||||
|
||||
@Nullable
|
||||
@@ -595,24 +625,47 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
|
||||
private volatile boolean isStompConnected;
|
||||
|
||||
private long clientSendInterval;
|
||||
|
||||
protected StompConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders) {
|
||||
@Nullable
|
||||
private final AtomicInteger clientSendMessageCount;
|
||||
|
||||
private long clientSendMessageTimestamp;
|
||||
|
||||
|
||||
protected RelayConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders) {
|
||||
this(sessionId, connectHeaders, true);
|
||||
}
|
||||
|
||||
private StompConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders, boolean isClientSession) {
|
||||
private RelayConnectionHandler(String sessionId, StompHeaderAccessor connectHeaders, boolean isClientSession) {
|
||||
Assert.notNull(sessionId, "'sessionId' must not be null");
|
||||
Assert.notNull(connectHeaders, "'connectHeaders' must not be null");
|
||||
this.sessionId = sessionId;
|
||||
this.connectHeaders = connectHeaders;
|
||||
this.isRemoteClientSession = isClientSession;
|
||||
this.outboundChannel = getClientOutboundChannelForSession(sessionId);
|
||||
if (isClientSession && taskScheduler != null) {
|
||||
this.clientSendInterval = connectHeaders.getHeartbeat()[0];
|
||||
}
|
||||
if (this.clientSendInterval > 0) {
|
||||
this.clientSendMessageCount = new AtomicInteger();
|
||||
this.clientSendMessageTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
else {
|
||||
this.clientSendMessageCount = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public String getSessionId() {
|
||||
return this.sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StompHeaderAccessor getConnectHeaders() {
|
||||
return this.connectHeaders;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
protected TcpConnection<byte[]> getTcpConnection() {
|
||||
return this.tcpConnection;
|
||||
@@ -722,31 +775,31 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
initHeartbeats(connectedHeaders);
|
||||
}
|
||||
|
||||
private void initHeartbeats(StompHeaderAccessor connectedHeaders) {
|
||||
if (this.isRemoteClientSession) {
|
||||
return;
|
||||
protected void initHeartbeats(StompHeaderAccessor connectedHeaders) {
|
||||
if (taskScheduler != null) {
|
||||
long interval = connectedHeaders.getHeartbeat()[1];
|
||||
this.clientSendInterval = Math.max(interval, this.clientSendInterval);
|
||||
}
|
||||
}
|
||||
|
||||
TcpConnection<byte[]> con = this.tcpConnection;
|
||||
Assert.state(con != null, "No TcpConnection available");
|
||||
/**
|
||||
* Whether to forward a heartbeat message in lieu of a message with a non-broker
|
||||
* destination. This is done if client-side heartbeats are expected and if there
|
||||
* haven't been any other messages in the current heartbeat period.
|
||||
* @since 5.3
|
||||
*/
|
||||
protected boolean shouldSendHeartbeatForIgnoredMessage() {
|
||||
return (this.clientSendMessageCount != null && this.clientSendMessageCount.get() == 0);
|
||||
}
|
||||
|
||||
long clientSendInterval = this.connectHeaders.getHeartbeat()[0];
|
||||
long clientReceiveInterval = this.connectHeaders.getHeartbeat()[1];
|
||||
long serverSendInterval = connectedHeaders.getHeartbeat()[0];
|
||||
long serverReceiveInterval = connectedHeaders.getHeartbeat()[1];
|
||||
|
||||
if (clientSendInterval > 0 && serverReceiveInterval > 0) {
|
||||
long interval = Math.max(clientSendInterval, serverReceiveInterval);
|
||||
con.onWriteInactivity(() ->
|
||||
con.send(HEARTBEAT_MESSAGE).addCallback(
|
||||
result -> {},
|
||||
ex -> handleTcpConnectionFailure(
|
||||
"Failed to forward heartbeat: " + ex.getMessage(), ex)), interval);
|
||||
}
|
||||
if (clientReceiveInterval > 0 && serverSendInterval > 0) {
|
||||
final long interval = Math.max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER;
|
||||
con.onReadInactivity(
|
||||
() -> handleTcpConnectionFailure("No messages received in " + interval + " ms.", null), interval);
|
||||
/**
|
||||
* Reset the clientSendMessageCount if the current heartbeat period has expired.
|
||||
* @since 5.3
|
||||
*/
|
||||
void updateClientSendMessageCount(long now) {
|
||||
if (this.clientSendMessageCount != null && this.clientSendInterval > (now - clientSendMessageTimestamp)) {
|
||||
this.clientSendMessageCount.set(0);
|
||||
this.clientSendMessageTimestamp = now;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -824,6 +877,10 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
}
|
||||
}
|
||||
|
||||
if (this.clientSendMessageCount != null) {
|
||||
this.clientSendMessageCount.incrementAndGet();
|
||||
}
|
||||
|
||||
final Message<?> messageToSend = (accessor.isMutable() && accessor.isModified()) ?
|
||||
MessageBuilder.createMessage(message.getPayload(), accessor.getMessageHeaders()) : message;
|
||||
|
||||
@@ -911,9 +968,9 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
}
|
||||
|
||||
|
||||
private class SystemStompConnectionHandler extends StompConnectionHandler {
|
||||
private class SystemSessionConnectionHandler extends RelayConnectionHandler {
|
||||
|
||||
public SystemStompConnectionHandler(StompHeaderAccessor connectHeaders) {
|
||||
public SystemSessionConnectionHandler(StompHeaderAccessor connectHeaders) {
|
||||
super(SYSTEM_SESSION_ID, connectHeaders, false);
|
||||
}
|
||||
|
||||
@@ -927,6 +984,30 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
sendSystemSubscriptions();
|
||||
}
|
||||
|
||||
protected void initHeartbeats(StompHeaderAccessor connectedHeaders) {
|
||||
TcpConnection<byte[]> con = getTcpConnection();
|
||||
Assert.state(con != null, "No TcpConnection available");
|
||||
|
||||
long clientSendInterval = getConnectHeaders().getHeartbeat()[0];
|
||||
long clientReceiveInterval = getConnectHeaders().getHeartbeat()[1];
|
||||
long serverSendInterval = connectedHeaders.getHeartbeat()[0];
|
||||
long serverReceiveInterval = connectedHeaders.getHeartbeat()[1];
|
||||
|
||||
if (clientSendInterval > 0 && serverReceiveInterval > 0) {
|
||||
long interval = Math.max(clientSendInterval, serverReceiveInterval);
|
||||
con.onWriteInactivity(() ->
|
||||
con.send(HEARTBEAT_MESSAGE).addCallback(
|
||||
result -> {},
|
||||
ex -> handleTcpConnectionFailure(
|
||||
"Failed to forward heartbeat: " + ex.getMessage(), ex)), interval);
|
||||
}
|
||||
if (clientReceiveInterval > 0 && serverSendInterval > 0) {
|
||||
final long interval = Math.max(clientReceiveInterval, serverSendInterval) * HEARTBEAT_MULTIPLIER;
|
||||
con.onReadInactivity(
|
||||
() -> handleTcpConnectionFailure("No messages received in " + interval + " ms.", null), interval);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendSystemSubscriptions() {
|
||||
int i = 0;
|
||||
for (String destination : getSystemSubscriptions().keySet()) {
|
||||
@@ -1005,9 +1086,27 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler
|
||||
throw new MessageDeliveryException(message, ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldSendHeartbeatForIgnoredMessage() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class ClientSendMessageCountTask implements Runnable {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
long now = System.currentTimeMillis();
|
||||
for (RelayConnectionHandler handler : connectionHandlers.values()) {
|
||||
handler.updateClientSendMessageCount(now);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
private static class VoidCallable implements Callable<Void> {
|
||||
|
||||
@Override
|
||||
|
||||
+8
@@ -98,6 +98,14 @@ public abstract class StompClientSupport {
|
||||
* <p>By default this is set to "10000,10000" but subclasses may override
|
||||
* that default and for example set it to "0,0" if they require a
|
||||
* TaskScheduler to be configured first.
|
||||
* <p><strong>Note:</strong> that a heartbeat is sent only in case of
|
||||
* inactivity, i.e. when no other messages are sent. This can present a
|
||||
* challenge when using an external broker since messages with a non-broker
|
||||
* destination represent activity but aren't actually forwarded to the broker.
|
||||
* In that case you can configure a `TaskScheduler` through the
|
||||
* {@link org.springframework.messaging.simp.config.StompBrokerRelayRegistration}
|
||||
* which ensures a heartbeat is forwarded to the broker also when only
|
||||
* messages with a non-broker destination are sent.
|
||||
* @param heartbeat the value for the CONNECT "heart-beat" header
|
||||
* @see <a href="https://stomp.github.io/stomp-specification-1.2.html#Heart-beating">
|
||||
* https://stomp.github.io/stomp-specification-1.2.html#Heart-beating</a>
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.messaging.simp.stomp;
|
||||
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.messaging.tcp.TcpConnectionHandler;
|
||||
|
||||
/**
|
||||
* A {@link TcpConnectionHandler} for use with STOMP connections, exposing
|
||||
* further information about the connection.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 5.3
|
||||
* @param <P> the type of payload for inbound and outbound messages
|
||||
*/
|
||||
public interface StompTcpConnectionHandler<P> extends TcpConnectionHandler<P> {
|
||||
|
||||
/**
|
||||
* Return the {@link SimpMessageHeaderAccessor#getSessionId() sessionId}
|
||||
* associated with the STOMP connection.
|
||||
*/
|
||||
String getSessionId();
|
||||
|
||||
/**
|
||||
* Return the headers that will be sent in the STOMP CONNECT frame.
|
||||
*/
|
||||
StompHeaderAccessor getConnectHeaders();
|
||||
|
||||
}
|
||||
+7
@@ -18,6 +18,7 @@ package org.springframework.messaging.simp.user;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.security.Principal;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
@@ -310,6 +311,12 @@ public class MultiServerUserRegistry implements SimpUserRegistry, SmartApplicati
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Principal getPrincipal() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSessions() {
|
||||
if (this.sessionLookup != null) {
|
||||
|
||||
+11
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.messaging.simp.user;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -33,6 +34,15 @@ public interface SimpUser {
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* Return the user associated with the session, if available. Typically, the
|
||||
* user information is available unless the user is connected to a different
|
||||
* server in a multi-server user registry scenario.
|
||||
* @since 5.3
|
||||
*/
|
||||
@Nullable
|
||||
Principal getPrincipal();
|
||||
|
||||
/**
|
||||
* Whether the user has any sessions.
|
||||
*/
|
||||
|
||||
+15
-2
@@ -186,7 +186,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||
return handleShuttingDownConnectFailure(handler);
|
||||
}
|
||||
|
||||
Mono<Void> connectMono = this.tcpClient
|
||||
Mono<Void> connectMono = extendTcpClient(this.tcpClient, handler)
|
||||
.handle(new ReactorNettyHandler(handler))
|
||||
.connect()
|
||||
.doOnError(handler::afterConnectFailure)
|
||||
@@ -195,6 +195,19 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||
return new MonoToListenableFutureAdapter<>(connectMono);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides an opportunity to initialize the {@link TcpClient} for the given
|
||||
* {@link TcpConnectionHandler} which may implement sub-interfaces such as
|
||||
* {@link org.springframework.messaging.simp.stomp.StompTcpConnectionHandler}
|
||||
* that expose further information.
|
||||
* @param tcpClient the candidate TcpClient
|
||||
* @param handler the handler for the TCP connection
|
||||
* @return the same handler or an updated instance
|
||||
*/
|
||||
protected TcpClient extendTcpClient(TcpClient tcpClient, TcpConnectionHandler<P> handler) {
|
||||
return tcpClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ListenableFuture<Void> connect(TcpConnectionHandler<P> handler, ReconnectStrategy strategy) {
|
||||
Assert.notNull(handler, "TcpConnectionHandler is required");
|
||||
@@ -207,7 +220,7 @@ public class ReactorNettyTcpClient<P> implements TcpOperations<P> {
|
||||
// Report first connect to the ListenableFuture
|
||||
CompletableFuture<Void> connectFuture = new CompletableFuture<>();
|
||||
|
||||
this.tcpClient
|
||||
extendTcpClient(this.tcpClient, handler)
|
||||
.handle(new ReactorNettyHandler(handler))
|
||||
.connect()
|
||||
.doOnNext(conn -> connectFuture.complete(null))
|
||||
|
||||
+2
-3
@@ -274,8 +274,7 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
|
||||
}
|
||||
|
||||
public void expectMessages(MessageExchange... messageExchanges) throws InterruptedException {
|
||||
List<MessageExchange> expectedMessages =
|
||||
new ArrayList<>(Arrays.<MessageExchange>asList(messageExchanges));
|
||||
List<MessageExchange> expectedMessages = new ArrayList<>(Arrays.asList(messageExchanges));
|
||||
while (expectedMessages.size() > 0) {
|
||||
Message<?> message = this.queue.poll(10000, TimeUnit.MILLISECONDS);
|
||||
assertThat(message).as("Timed out waiting for messages, expected [" + expectedMessages + "]").isNotNull();
|
||||
@@ -451,7 +450,7 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
|
||||
@Override
|
||||
public final boolean match(Message<?> message) {
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
|
||||
if (!this.command.equals(headers.getCommand()) || (this.sessionId != headers.getSessionId())) {
|
||||
if (!this.command.equals(headers.getCommand()) || !this.sessionId.equals(headers.getSessionId())) {
|
||||
return false;
|
||||
}
|
||||
return matchInternal(headers, message.getPayload());
|
||||
|
||||
+50
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.messaging.simp.stomp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Callable;
|
||||
@@ -39,10 +38,12 @@ import org.springframework.messaging.tcp.ReconnectStrategy;
|
||||
import org.springframework.messaging.tcp.TcpConnection;
|
||||
import org.springframework.messaging.tcp.TcpConnectionHandler;
|
||||
import org.springframework.messaging.tcp.TcpOperations;
|
||||
import org.springframework.scheduling.TaskScheduler;
|
||||
import org.springframework.util.concurrent.ListenableFuture;
|
||||
import org.springframework.util.concurrent.ListenableFutureTask;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
@@ -59,6 +60,8 @@ class StompBrokerRelayMessageHandlerTests {
|
||||
|
||||
private StubTcpOperations tcpClient;
|
||||
|
||||
ArgumentCaptor<Runnable> messageCountTaskCaptor = ArgumentCaptor.forClass(Runnable.class);
|
||||
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
@@ -66,7 +69,7 @@ class StompBrokerRelayMessageHandlerTests {
|
||||
this.outboundChannel = new StubMessageChannel();
|
||||
|
||||
this.brokerRelay = new StompBrokerRelayMessageHandler(new StubMessageChannel(),
|
||||
this.outboundChannel, new StubMessageChannel(), Arrays.asList("/topic")) {
|
||||
this.outboundChannel, new StubMessageChannel(), Collections.singletonList("/topic")) {
|
||||
|
||||
@Override
|
||||
protected void startInternal() {
|
||||
@@ -77,6 +80,8 @@ class StompBrokerRelayMessageHandlerTests {
|
||||
|
||||
this.tcpClient = new StubTcpOperations();
|
||||
this.brokerRelay.setTcpClient(this.tcpClient);
|
||||
|
||||
this.brokerRelay.setTaskScheduler(mock(TaskScheduler.class));
|
||||
}
|
||||
|
||||
|
||||
@@ -127,18 +132,51 @@ class StompBrokerRelayMessageHandlerTests {
|
||||
|
||||
@Test
|
||||
void destinationExcluded() {
|
||||
this.brokerRelay.start();
|
||||
this.brokerRelay.handleMessage(connectMessage("sess1", "joe"));
|
||||
|
||||
SimpMessageHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
accessor.setLeaveMutable(true);
|
||||
this.tcpClient.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
|
||||
|
||||
accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
accessor.setSessionId("sess1");
|
||||
accessor.setDestination("/user/daisy/foo");
|
||||
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
|
||||
|
||||
assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(2);
|
||||
StompHeaderAccessor headers = this.tcpClient.getSentHeaders(0);
|
||||
assertThat(headers.getCommand()).isEqualTo(StompCommand.CONNECT);
|
||||
assertThat(headers.getSessionId()).isEqualTo(StompBrokerRelayMessageHandler.SYSTEM_SESSION_ID);
|
||||
|
||||
headers = this.tcpClient.getSentHeaders(1);
|
||||
assertThat(headers.getCommand()).isEqualTo(StompCommand.CONNECT);
|
||||
assertThat(headers.getSessionId()).isEqualTo("sess1");
|
||||
}
|
||||
|
||||
@Test // gh-22822
|
||||
void destinationExcludedWithHeartbeat() {
|
||||
Message<byte[]> connectMessage = connectMessage("sess1", "joe");
|
||||
MessageHeaderAccessor.getAccessor(connectMessage, StompHeaderAccessor.class).setHeartbeat(10000, 10000);
|
||||
|
||||
this.brokerRelay.start();
|
||||
this.brokerRelay.handleMessage(connectMessage);
|
||||
|
||||
SimpMessageHeaderAccessor headers = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
headers.setSessionId("sess1");
|
||||
headers.setDestination("/user/daisy/foo");
|
||||
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders()));
|
||||
SimpMessageHeaderAccessor accessor = StompHeaderAccessor.create(StompCommand.CONNECTED);
|
||||
accessor.setLeaveMutable(true);
|
||||
this.tcpClient.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
|
||||
|
||||
assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(1);
|
||||
StompHeaderAccessor headers1 = this.tcpClient.getSentHeaders(0);
|
||||
assertThat(headers1.getCommand()).isEqualTo(StompCommand.CONNECT);
|
||||
assertThat(headers1.getSessionId()).isEqualTo(StompBrokerRelayMessageHandler.SYSTEM_SESSION_ID);
|
||||
// Run the messageCountTask to clear the message count
|
||||
verify(this.brokerRelay.getTaskScheduler()).scheduleWithFixedDelay(this.messageCountTaskCaptor.capture(), eq(5000L));
|
||||
this.messageCountTaskCaptor.getValue().run();
|
||||
|
||||
accessor = SimpMessageHeaderAccessor.create(SimpMessageType.MESSAGE);
|
||||
accessor.setSessionId("sess1");
|
||||
accessor.setDestination("/user/daisy/foo");
|
||||
this.brokerRelay.handleMessage(MessageBuilder.createMessage(new byte[0], accessor.getMessageHeaders()));
|
||||
|
||||
assertThat(this.tcpClient.getSentMessages().size()).isEqualTo(3);
|
||||
assertThat(this.tcpClient.getSentHeaders(2).getMessageType()).isEqualTo(SimpMessageType.HEARTBEAT);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -227,6 +265,7 @@ class StompBrokerRelayMessageHandlerTests {
|
||||
StompHeaderAccessor headers = StompHeaderAccessor.create(StompCommand.CONNECT);
|
||||
headers.setSessionId(sessionId);
|
||||
headers.setUser(new TestPrincipal(user));
|
||||
headers.setLeaveMutable(true);
|
||||
return MessageBuilder.createMessage(new byte[0], headers.getMessageHeaders());
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,11 +16,14 @@
|
||||
|
||||
package org.springframework.messaging.simp.user;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
@@ -41,6 +44,12 @@ public class TestSimpUser implements SimpUser {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Principal getPrincipal() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<SimpSession> getSessions() {
|
||||
return new HashSet<>(this.sessions.values());
|
||||
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* Copyright 2002-2019 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* 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.messaging.converter
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.MethodParameter
|
||||
import org.springframework.messaging.support.MessageBuilder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import kotlin.reflect.typeOf
|
||||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
class KotlinSerializationJsonMessageConverterTests {
|
||||
|
||||
private val converter = KotlinSerializationJsonMessageConverter()
|
||||
|
||||
@Test
|
||||
fun readObject() {
|
||||
val payload = """
|
||||
{
|
||||
"bytes": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"array": [
|
||||
"Foo",
|
||||
"Bar"
|
||||
],
|
||||
"number": 42,
|
||||
"string": "Foo",
|
||||
"bool": true,
|
||||
"fraction": 42
|
||||
}
|
||||
""".trimIndent()
|
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
|
||||
val result = converter.fromMessage(message, SerializableBean::class.java) as SerializableBean
|
||||
|
||||
Assertions.assertThat(result.bytes).containsExactly(0x1, 0x2)
|
||||
Assertions.assertThat(result.array).containsExactly("Foo", "Bar")
|
||||
Assertions.assertThat(result.number).isEqualTo(42)
|
||||
Assertions.assertThat(result.string).isEqualTo("Foo")
|
||||
Assertions.assertThat(result.bool).isTrue()
|
||||
Assertions.assertThat(result.fraction).isEqualTo(42.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
fun readArrayOfObjects() {
|
||||
val payload = """
|
||||
[
|
||||
{
|
||||
"bytes": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"array": [
|
||||
"Foo",
|
||||
"Bar"
|
||||
],
|
||||
"number": 42,
|
||||
"string": "Foo",
|
||||
"bool": true,
|
||||
"fraction": 42
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
|
||||
val result = converter.fromMessage(message, Array<SerializableBean>::class.java) as Array<SerializableBean>
|
||||
|
||||
Assertions.assertThat(result).hasSize(1)
|
||||
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2)
|
||||
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar")
|
||||
Assertions.assertThat(result[0].number).isEqualTo(42)
|
||||
Assertions.assertThat(result[0].string).isEqualTo("Foo")
|
||||
Assertions.assertThat(result[0].bool).isTrue()
|
||||
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@ExperimentalStdlibApi
|
||||
fun readGenericCollection() {
|
||||
val payload = """
|
||||
[
|
||||
{
|
||||
"bytes": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"array": [
|
||||
"Foo",
|
||||
"Bar"
|
||||
],
|
||||
"number": 42,
|
||||
"string": "Foo",
|
||||
"bool": true,
|
||||
"fraction": 42
|
||||
}
|
||||
]
|
||||
""".trimIndent()
|
||||
val method = javaClass.getDeclaredMethod("handleList", List::class.java)
|
||||
val param = MethodParameter(method, 0)
|
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
|
||||
val result = converter.fromMessage(message, typeOf<List<SerializableBean>>()::class.java, param) as List<SerializableBean>
|
||||
|
||||
Assertions.assertThat(result).hasSize(1)
|
||||
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2)
|
||||
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar")
|
||||
Assertions.assertThat(result[0].number).isEqualTo(42)
|
||||
Assertions.assertThat(result[0].string).isEqualTo("Foo")
|
||||
Assertions.assertThat(result[0].bool).isTrue()
|
||||
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun readFailsOnInvalidJson() {
|
||||
val payload = """
|
||||
this is an invalid JSON document
|
||||
""".trimIndent()
|
||||
|
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build()
|
||||
Assertions.assertThatExceptionOfType(MessageConversionException::class.java).isThrownBy {
|
||||
converter.fromMessage(message, SerializableBean::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeObject() {
|
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
|
||||
val message = converter.toMessage(serializableBean, null)
|
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
|
||||
|
||||
Assertions.assertThat(result)
|
||||
.contains("\"bytes\":[1,2]")
|
||||
.contains("\"array\":[\"Foo\",\"Bar\"]")
|
||||
.contains("\"number\":42")
|
||||
.contains("\"string\":\"Foo\"")
|
||||
.contains("\"bool\":true")
|
||||
.contains("\"fraction\":42.0")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeObjectWithNullableProperty() {
|
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, null, true, 42.0f)
|
||||
val message = converter.toMessage(serializableBean, null)
|
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
|
||||
|
||||
Assertions.assertThat(result)
|
||||
.contains("\"bytes\":[1,2]")
|
||||
.contains("\"array\":[\"Foo\",\"Bar\"]")
|
||||
.contains("\"number\":42")
|
||||
.contains("\"string\":null")
|
||||
.contains("\"bool\":true")
|
||||
.contains("\"fraction\":42.0")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun writeArrayOfObjects() {
|
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
|
||||
val expectedJson = """
|
||||
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
|
||||
""".trimIndent()
|
||||
|
||||
val message = converter.toMessage(arrayOf(serializableBean), null)
|
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
|
||||
|
||||
Assertions.assertThat(result).isEqualTo(expectedJson)
|
||||
}
|
||||
|
||||
@Test
|
||||
@ExperimentalStdlibApi
|
||||
fun writeGenericCollection() {
|
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f)
|
||||
val expectedJson = """
|
||||
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}]
|
||||
""".trimIndent()
|
||||
|
||||
val method = javaClass.getDeclaredMethod("handleList", List::class.java)
|
||||
val param = MethodParameter(method, 0)
|
||||
val message = converter.toMessage(arrayListOf(serializableBean), null, param)
|
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8)
|
||||
|
||||
Assertions.assertThat(result).isEqualTo(expectedJson)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun handleList(payload: List<SerializableBean>) {}
|
||||
|
||||
@Serializable
|
||||
@Suppress("ArrayInDataClass")
|
||||
data class SerializableBean(
|
||||
val bytes: ByteArray,
|
||||
val array: Array<String>,
|
||||
val number: Int,
|
||||
val string: String?,
|
||||
val bool: Boolean,
|
||||
val fraction: Float
|
||||
)
|
||||
}
|
||||
+25
-52
@@ -29,8 +29,6 @@ import io.r2dbc.spi.R2dbcTimeoutException;
|
||||
import io.r2dbc.spi.R2dbcTransientException;
|
||||
import io.r2dbc.spi.R2dbcTransientResourceException;
|
||||
import io.r2dbc.spi.Wrapped;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.Ordered;
|
||||
@@ -69,11 +67,6 @@ public abstract class ConnectionFactoryUtils {
|
||||
*/
|
||||
public static final int CONNECTION_SYNCHRONIZATION_ORDER = 1000;
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ConnectionFactoryUtils.class);
|
||||
|
||||
|
||||
private ConnectionFactoryUtils() {}
|
||||
|
||||
|
||||
/**
|
||||
* Obtain a {@link Connection} from the given {@link ConnectionFactory}.
|
||||
@@ -112,48 +105,34 @@ public abstract class ConnectionFactoryUtils {
|
||||
if (conHolder != null && (conHolder.hasConnection() || conHolder.isSynchronizedWithTransaction())) {
|
||||
conHolder.requested();
|
||||
if (!conHolder.hasConnection()) {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Fetching resumed R2DBC Connection from ConnectionFactory");
|
||||
}
|
||||
return fetchConnection(connectionFactory).doOnNext(conHolder::setConnection);
|
||||
}
|
||||
return Mono.just(conHolder.getConnection());
|
||||
}
|
||||
// Else we either got no holder or an empty thread-bound holder here.
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Fetching R2DBC Connection from ConnectionFactory");
|
||||
}
|
||||
|
||||
Mono<Connection> con = fetchConnection(connectionFactory);
|
||||
|
||||
if (synchronizationManager.isSynchronizationActive()) {
|
||||
|
||||
return con.flatMap(connection -> {
|
||||
return Mono.just(connection).doOnNext(conn -> {
|
||||
|
||||
// Use same Connection for further R2DBC actions within the transaction.
|
||||
// Thread-bound object will get removed by synchronization at transaction completion.
|
||||
ConnectionHolder holderToUse = conHolder;
|
||||
if (holderToUse == null) {
|
||||
holderToUse = new ConnectionHolder(conn);
|
||||
}
|
||||
else {
|
||||
holderToUse.setConnection(conn);
|
||||
}
|
||||
holderToUse.requested();
|
||||
synchronizationManager
|
||||
.registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory));
|
||||
holderToUse.setSynchronizedWithTransaction(true);
|
||||
if (holderToUse != conHolder) {
|
||||
synchronizationManager.bindResource(connectionFactory, holderToUse);
|
||||
}
|
||||
}) // Unexpected exception from external delegation call -> close Connection and rethrow.
|
||||
.onErrorResume(e -> releaseConnection(connection, connectionFactory).then(Mono.error(e)));
|
||||
});
|
||||
return con.flatMap(connection -> Mono.just(connection).doOnNext(conn -> {
|
||||
// Use same Connection for further R2DBC actions within the transaction.
|
||||
// Thread-bound object will get removed by synchronization at transaction completion.
|
||||
ConnectionHolder holderToUse = conHolder;
|
||||
if (holderToUse == null) {
|
||||
holderToUse = new ConnectionHolder(conn);
|
||||
}
|
||||
else {
|
||||
holderToUse.setConnection(conn);
|
||||
}
|
||||
holderToUse.requested();
|
||||
synchronizationManager
|
||||
.registerSynchronization(new ConnectionSynchronization(holderToUse, connectionFactory));
|
||||
holderToUse.setSynchronizedWithTransaction(true);
|
||||
if (holderToUse != conHolder) {
|
||||
synchronizationManager.bindResource(connectionFactory, holderToUse);
|
||||
}
|
||||
}) // Unexpected exception from external delegation call -> close Connection and rethrow.
|
||||
.onErrorResume(e -> releaseConnection(connection, connectionFactory).then(Mono.error(e))));
|
||||
}
|
||||
|
||||
return con;
|
||||
}).onErrorResume(NoTransactionException.class, e -> Mono.from(connectionFactory.create()));
|
||||
}
|
||||
@@ -356,9 +335,7 @@ public abstract class ConnectionFactoryUtils {
|
||||
@Override
|
||||
public Mono<Void> suspend() {
|
||||
if (this.holderActive) {
|
||||
return TransactionSynchronizationManager.forCurrentTransaction()
|
||||
.flatMap(synchronizationManager -> {
|
||||
|
||||
return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {
|
||||
synchronizationManager.unbindResource(this.connectionFactory);
|
||||
if (this.connectionHolder.hasConnection() && !this.connectionHolder.isOpen()) {
|
||||
// Release Connection on suspend if the application doesn't keep
|
||||
@@ -371,7 +348,6 @@ public abstract class ConnectionFactoryUtils {
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@@ -388,11 +364,10 @@ public abstract class ConnectionFactoryUtils {
|
||||
|
||||
@Override
|
||||
public Mono<Void> beforeCompletion() {
|
||||
// Release Connection early if the holder is not open anymore
|
||||
// (that is, not used by another resource
|
||||
// that has its own cleanup via transaction synchronization),
|
||||
// to avoid issues with strict transaction implementations that expect
|
||||
// the close call before transaction completion.
|
||||
// Release Connection early if the holder is not open anymore (that is,
|
||||
// not used by another resource that has its own cleanup via transaction
|
||||
// synchronization), to avoid issues with strict transaction implementations
|
||||
// that expect the close call before transaction completion.
|
||||
if (!this.connectionHolder.isOpen()) {
|
||||
return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {
|
||||
synchronizationManager.unbindResource(this.connectionFactory);
|
||||
@@ -414,8 +389,7 @@ public abstract class ConnectionFactoryUtils {
|
||||
if (this.holderActive) {
|
||||
// The bound ConnectionHolder might not be available anymore,
|
||||
// since afterCompletion might get called from a different thread.
|
||||
return TransactionSynchronizationManager.forCurrentTransaction()
|
||||
.flatMap(synchronizationManager -> {
|
||||
return TransactionSynchronizationManager.forCurrentTransaction().flatMap(synchronizationManager -> {
|
||||
synchronizationManager.unbindResourceIfPossible(this.connectionFactory);
|
||||
this.holderActive = false;
|
||||
if (this.connectionHolder.hasConnection()) {
|
||||
@@ -426,7 +400,6 @@ public abstract class ConnectionFactoryUtils {
|
||||
return Mono.empty();
|
||||
});
|
||||
}
|
||||
|
||||
this.connectionHolder.reset();
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
+15
-23
@@ -107,8 +107,6 @@ public abstract class ScriptUtils {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(ScriptUtils.class);
|
||||
|
||||
// utility constructor
|
||||
private ScriptUtils() {}
|
||||
|
||||
/**
|
||||
* Split an SQL script into separate statements delimited by the provided
|
||||
@@ -335,14 +333,11 @@ public abstract class ScriptUtils {
|
||||
|
||||
return DataBufferUtils.join(DataBufferUtils.read(resource.getResource(), dataBufferFactory, 8192))
|
||||
.handle((it, sink) -> {
|
||||
|
||||
try (InputStream is = it.asInputStream()) {
|
||||
|
||||
InputStreamReader in = resource.getCharset() != null ? new InputStreamReader(is, resource.getCharset())
|
||||
: new InputStreamReader(is);
|
||||
InputStreamReader in = (resource.getCharset() != null ?
|
||||
new InputStreamReader(is, resource.getCharset()) : new InputStreamReader(is));
|
||||
LineNumberReader lnr = new LineNumberReader(in);
|
||||
String script = readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter);
|
||||
|
||||
sink.next(script);
|
||||
sink.complete();
|
||||
}
|
||||
@@ -548,9 +543,9 @@ public abstract class ScriptUtils {
|
||||
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
|
||||
*/
|
||||
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource,
|
||||
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops, String commentPrefix,
|
||||
@Nullable String separator, String blockCommentStartDelimiter, String blockCommentEndDelimiter)
|
||||
throws ScriptException {
|
||||
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops,
|
||||
String commentPrefix, @Nullable String separator, String blockCommentStartDelimiter,
|
||||
String blockCommentEndDelimiter) throws ScriptException {
|
||||
|
||||
return executeSqlScript(connection, resource, dataBufferFactory, continueOnError,
|
||||
ignoreFailedDrops, new String[] { commentPrefix }, separator,
|
||||
@@ -587,10 +582,10 @@ public abstract class ScriptUtils {
|
||||
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#getConnection
|
||||
* @see org.springframework.r2dbc.connection.ConnectionFactoryUtils#releaseConnection
|
||||
*/
|
||||
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource, DataBufferFactory dataBufferFactory,
|
||||
boolean continueOnError,
|
||||
boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator,
|
||||
String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
|
||||
public static Mono<Void> executeSqlScript(Connection connection, EncodedResource resource,
|
||||
DataBufferFactory dataBufferFactory, boolean continueOnError, boolean ignoreFailedDrops,
|
||||
String[] commentPrefixes, @Nullable String separator, String blockCommentStartDelimiter,
|
||||
String blockCommentEndDelimiter) throws ScriptException {
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing SQL script from " + resource);
|
||||
@@ -622,17 +617,15 @@ public abstract class ScriptUtils {
|
||||
});
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
|
||||
executeScript = executeScript.doOnComplete(() -> {
|
||||
|
||||
long elapsedTime = System.currentTimeMillis() - startTime;
|
||||
logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
|
||||
});
|
||||
}
|
||||
|
||||
return executeScript.onErrorMap(ex -> !(ex instanceof ScriptException),
|
||||
ex -> new UncategorizedScriptException("Failed to execute database script from resource [" + resource + "]",
|
||||
ex))
|
||||
ex -> new UncategorizedScriptException(
|
||||
"Failed to execute database script from resource [" + resource + "]", ex))
|
||||
.then();
|
||||
}
|
||||
|
||||
@@ -644,22 +637,21 @@ public abstract class ScriptUtils {
|
||||
.collect(Collectors.summingLong(count -> count));
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
execution = execution.doOnNext(rowsAffected -> logger.debug(rowsAffected + " returned as update count for SQL: " + statement));
|
||||
execution = execution.doOnNext(rowsAffected ->
|
||||
logger.debug(rowsAffected + " returned as update count for SQL: " + statement));
|
||||
}
|
||||
|
||||
return execution.onErrorResume(ex -> {
|
||||
|
||||
boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
|
||||
if (continueOnError || (dropStatement && ignoreFailedDrops)) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, statementNumber.get(), resource),
|
||||
ex);
|
||||
logger.debug(ScriptStatementFailedException.buildErrorMessage(
|
||||
statement, statementNumber.get(), resource), ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Mono.error(new ScriptStatementFailedException(statement, statementNumber.get(), resource, ex));
|
||||
}
|
||||
|
||||
return Mono.empty();
|
||||
}).then();
|
||||
}
|
||||
|
||||
+4
-6
@@ -167,6 +167,7 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact
|
||||
*/
|
||||
protected ConnectionFactory resolveSpecifiedConnectionFactory(Object connectionFactory)
|
||||
throws IllegalArgumentException {
|
||||
|
||||
if (connectionFactory instanceof ConnectionFactory) {
|
||||
return (ConnectionFactory) connectionFactory;
|
||||
}
|
||||
@@ -174,17 +175,14 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact
|
||||
return this.connectionFactoryLookup.getConnectionFactory((String) connectionFactory);
|
||||
}
|
||||
else {
|
||||
throw new IllegalArgumentException(
|
||||
"Illegal connection factory value - only 'io.r2dbc.spi.ConnectionFactory' and 'String' supported: "
|
||||
+ connectionFactory);
|
||||
throw new IllegalArgumentException("Illegal connection factory value - " +
|
||||
"only 'io.r2dbc.spi.ConnectionFactory' and 'String' supported: " + connectionFactory);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Connection> create() {
|
||||
return determineTargetConnectionFactory() //
|
||||
.map(ConnectionFactory::create) //
|
||||
.flatMap(Mono::from);
|
||||
return determineTargetConnectionFactory().map(ConnectionFactory::create).flatMap(Mono::from);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
-1
@@ -56,7 +56,6 @@ public class MapConnectionFactoryLookup implements ConnectionFactoryLookup {
|
||||
|
||||
/**
|
||||
* Create a new instance of the {@link MapConnectionFactoryLookup} class.
|
||||
*
|
||||
* @param connectionFactoryName the name under which the supplied {@link ConnectionFactory} is to be added
|
||||
* @param connectionFactory the {@link ConnectionFactory} to be added
|
||||
*/
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user