Compare commits

..

65 Commits

Author SHA1 Message Date
Brian Clozel 053d8e25f4 Release v6.2.16 2026-02-12 09:33:48 +01:00
Christian Schuster 8334388e20 avoid unnecessary locking in ConcurrentReferenceHashMap's implementation of computeIfAbsent and computeIfPresent
Signed-off-by: Christian Schuster <christian@dnup.de>

Closes gh-36308

(cherry picked from commit a9b1d6335e)
2026-02-11 18:08:59 +01:00
Brian Clozel 757b713f22 Use updated message in HttpEntityMethodProcessor
Prior to this commit, the `HttpEntityMethodProcessor` would create a new
`ServletServerHttpRequest` input message to parse the native Servlet
request, but would not reuse it for reading the request body using the
message converters.

In gh-32471, we applied a change that updates HTTP headers accordingly
when request parameters are read. But not reusing the input message
means that we are losing this update when instantiating the resulting
`HttpEntity`.

This commit ensures that `HttpEntityMethodProcessor` uses the input
message it just created when decoding the request body.

Fixes gh-36298
2026-02-11 14:07:21 +01:00
rstoyanchev a065563484 Optimize RequestMappingInfo hashcode calculation
Precalculated hashcode makes sense for infos in the registry, but
matched infos created on the fly don't need it.

Closes gh-36279
2026-02-11 09:42:01 +00:00
rstoyanchev 6162d89042 Cache HandlerMethod with resolved bean if singleton
See gh-36278
2026-02-11 09:12:08 +00:00
rstoyanchev 5c537db2cc Optimize single PathPattern match
Closes gh-36275
2026-02-11 09:10:54 +00:00
rstoyanchev 849553dc8e Avoid determineValidationGroups not necessary
There is no need to call determineValidationGroups if the method
itself doesn't require method validation.

See gh-36274
2026-02-11 09:05:19 +00:00
Brian Clozel 7240a5a669 Upgrade to Reactor 2024.0.15
Fixes gh-36289
2026-02-10 13:54:51 +01:00
Brian Clozel b00c387775 Upgrade to Micrometer 1.15.9
Closes gh-36290
2026-02-10 13:38:39 +01:00
Brian Clozel 8396c071af Fix wildcard MIME type support in messaging converters
Prior to this commit, the "application/*+json" wildcard MIME type was
added to the list of supported MIME types in the JSON messaging
converter. This change wasn't fully reflected in the
`AbstractMessageConverter`, because only strict matching of type and
subtybe were considered.

This commit updates the `AbstractMessageConverter` to not only check the
type and subtype, but also check whether the supported MIME type
includes the one given as a parameter.

Fixes gh-36285
2026-02-10 10:42:51 +01:00
Juergen Hoeller bb35e9f11c Make LocalEntityManagerFactoryBean#setDataSource work on Hibernate and EclipseLink
Includes fix for consistent PersistenceException in case of no unit found for name.
Includes proper tests for LocalContainerEntityManagerFactoryBean with scan setup.

Closes gh-36272
2026-02-08 18:06:58 +01:00
Juergen Hoeller a0319b1f91 Repeatedly check status while trying to lock for shutdown
Closes gh-36260

(cherry picked from commit 20970a4a37)
2026-02-06 19:04:55 +01:00
Juergen Hoeller 5973a17253 Polishing
(cherry picked from commit 153c378bb2)
2026-02-05 20:30:36 +01:00
Juergen Hoeller ed7243259b Upgrade to Groovy 4.0.30, Netty 4.1.130, Mockito 5.21 2026-02-02 12:58:03 +01:00
Juergen Hoeller a391db2ef5 Upgrade to ASM 9.9.1 and Objenesis 3.5
Closes gh-36244

(cherry picked from commit 40350653e1)
2026-02-02 12:42:53 +01:00
dependabot[bot] 957202523c Upgrade fast-xml-parser from 4.5.2 to 5.3.4 in /framework-docs
Closes gh-36234

(cherry picked from commit 6fd84e4c2f)
2026-01-31 15:33:14 +01:00
qwding a3c9166da7 Optimize NamedParameterUtils#buildValueArray by lazily fetching SqlParameter
This PR optimizes the performance of NamedParameterUtils#buildValueArray by deferring the call to findParameter(declaredParams, paramName, i).

Changes: In the original implementation, findParameter was called for every parameter in the loop, regardless of whether the paramValue retrieved from paramSource was already an instance of SqlParameterValue.

Since findParameter involves iterating through the declaredParams list (or performing lookups), skipping this call when paramValue instanceof SqlParameterValue is true reduces unnecessary CPU cycles and memory access, especially for queries with a large number of parameters or long declaredParams lists.

Signed-off-by: qwding <761945125@qq.com>
(cherry picked from commit 149397ed10)
2026-01-30 14:10:46 +01:00
Brian Clozel 0c3dd8cb00 Fix "remove" implementation in netty headers adapter
Prior to this commit, the `Netty4HeadersAdapter` `MultiValueMapi#remove`
implementation would return an empty list if no value was present. This
is not consistent with other implementations.

This change ensures that `null` is returned for those cases.

Fixes gh-36226
2026-01-29 11:31:50 +01:00
Juergen Hoeller 56d8ec25e8 Consistently close streams through try-with-resources
Also aligns byte array and Reader copying with 7.0.4.

Closes gh-36223

(cherry picked from commit 16f4b23c32)
2026-01-28 18:17:20 +01:00
Sam Brannen fb9db146f2 Consistently refer to "an SQL" statement, type, etc.
(cherry picked from commit 799a520c35)
2026-01-28 18:11:11 +01:00
Juergen Hoeller dcf98bc55c Polishing
(cherry picked from commit 5d33de943b)
2026-01-28 17:19:22 +01:00
Juergen Hoeller b9cd06a487 Support -1 for undetermined length in SqlBinaryValue/SqlCharacterValue
Closes gh-36219

(cherry picked from commit 7da65fe4fc)
2026-01-28 17:18:17 +01:00
Sam Brannen 91ff706e8c Fix formatting and spelling
(cherry picked from commit 4dacc64a30)
2026-01-28 14:31:44 +01:00
Sam Brannen 103057de2a Fix links to JUnit User Guide
Closes gh-36217

(cherry picked from commit ba47dd0714)
2026-01-28 14:31:34 +01:00
rstoyanchev b6d1e88563 Fix unfinished edit from previous commit
See gh-36198
2026-01-28 12:27:30 +00:00
rstoyanchev af1e9da3d7 Update docs on trailing slash handling
Closes gh-36198
2026-01-28 12:23:47 +00:00
rstoyanchev bc5e395a42 Update docs content types for ProblemDetail
Closes gh-36192
2026-01-28 12:21:56 +00:00
Juergen Hoeller 1ec3cb4d5f Polishing (aligned with main) 2026-01-27 21:25:23 +01:00
rstoyanchev 7b7126ae3d Polishing in ReactorClientHttpConnector 2026-01-26 10:46:59 +00:00
rstoyanchev 9f1332c716 Refine solution to clear Netty channel attribute
Closes gh-36158
2026-01-26 10:45:33 +00:00
Sam Brannen 2ff93f4207 Consistently indent with tabs instead of spaces 2026-01-25 17:57:36 +01:00
Sam Brannen 3a6c7786a1 Upgrade to AssertJ 3.27.7
(cherry picked from commit 801035bea7)
2026-01-25 17:57:36 +01:00
Padraic Slattery 539a098f8c Update GitHub upload-artifact action to version 6
Closes gh-36199

Signed-off-by: Padraic Slattery <pgoslatara@gmail.com>

(cherry picked from commit 65bdc78a1e)
2026-01-25 17:57:28 +01:00
Juergen Hoeller ecfd78ff93 Polishing (aligned with main) 2026-01-25 10:52:37 +01:00
Juergen Hoeller 1e0a85368e Revise setPersistenceUnitName javadoc
Closes gh-36205

(cherry picked from commit 22cf7958a5)
2026-01-25 10:47:56 +01:00
rstoyanchev c49d5dc860 Handle early exception from AsynchronousFileChannel#write
Closes gh-36184
2026-01-22 14:17:52 +00:00
Juergen Hoeller 58af70f2e3 Upgrade to Selenium 4.40, HtmlUnit 4.21, Protobuf 4.33.4 2026-01-21 14:59:57 +01:00
Juergen Hoeller c6e73b5d1d Fix JMSReplyTo references in javadoc (backported from main) 2026-01-21 14:52:16 +01:00
Juergen Hoeller f531cc9fdf Polishing
(cherry picked from commit 1c56ec3f7e)
2026-01-20 19:34:17 +01:00
Juergen Hoeller 1977f31821 Prevent accidental printStackTrace() usage in main codebase
Closes gh-36185

(cherry picked from commit 65565de1a8)
2026-01-20 19:33:30 +01:00
Sam Brannen cf3ed229de Fix test parameterization in CaffeineReactiveCachingTests
Prior to this commit, test methods in CaffeineReactiveCachingTests
were parameterized twice with the same configuration class.

See gh-31637
See gh-35833

(cherry picked from commit 9ebfdb8b6a)
2026-01-20 11:44:06 +01:00
Sam Brannen 154dad6587 Revise contribution
See gh-36170

(cherry picked from commit b164db35c1)
2026-01-18 17:02:09 +01:00
Tran Ngoc Nhan fd8b4d5936 Replace getErrors() with getBindingResult() in examples
DataBinder#getErrors was removed in v4.0.0.M1.

Closes gh-36170

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>

(cherry picked from commit 385e62dbf0)
2026-01-18 17:02:01 +01:00
Juergen Hoeller 13defc09f9 Fix setBody call in reactorNettyAttributes test
See gh-36158
2026-01-17 00:05:15 +01:00
Juergen Hoeller 9e512315ab Avoid javadoc build failure against HttpClient 5.5.x 2026-01-16 23:38:32 +01:00
Juergen Hoeller c750efb0a6 Bring back WebLogicJtaTransactionManager for WebLogic 15.1.1
Closes gh-36152
2026-01-16 23:19:48 +01:00
Juergen Hoeller 0033edad45 Apply transactionIsolationLock in EclipseLinkConnectionHandle as well
Closes gh-36165

(cherry picked from commit 2b96a61063)
2026-01-16 23:15:56 +01:00
rstoyanchev 183cd4c8cd Clear Netty channel attribute
Closes gh-36158
2026-01-16 17:22:11 +00:00
Juergen Hoeller 6bd0ed8e79 Polishing
(cherry picked from commit 62fd09dfa5)
2026-01-14 22:13:07 +01:00
rstoyanchev dcb7922a24 Exclude DataAccessException and MessagingException in DisconnectedClientHelper
Closes gh-36135
2026-01-14 11:55:51 +00:00
Yanming Zhou 05a9cc26ae Improve DisconnectedClientHelper to better guard ClassNotFoundException
Before this commit, WebClientException is ignored if RestClientException is not present.

Closes gh-36150

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2026-01-14 11:55:51 +00:00
Sam Brannen 05beb4ad4d Upgrade to JUnit 5.14.2
Closes gh-36148
2026-01-14 12:39:33 +01:00
rstoyanchev 5236b35687 Update Principal check in TransportHandlingSockJsService
Closes gh-35753
2026-01-12 15:21:59 +00:00
Sébastien Deleuze 7482ccc18f Upgrade Antora dependencies
Closes gh-36106
2026-01-07 09:57:09 +01:00
Sam Brannen b3fb9f4e31 Reliably resolve generic read/write methods in PropertyDescriptorUtils
Prior to this commit, the determineBasicProperties() method in
PropertyDescriptorUtils did not reliably resolve read/write methods in
type hierarchies with generics. This utility method is used by
SimpleBeanInfoFactory which is used by BeanUtils and BeanWrapperImpl.
Thus, failure to reliably resolve read/write JavaBeans methods resulted
in bugs in certain scenarios.

For example, BeanUtils.copyProperties() randomly failed to copy certain
properties if the write method for the property could not be resolved.

To address such issues, this commit revises the implementation of
PropertyDescriptorUtils as follows.

1) Read methods with covariant return types are now consistently
   resolved correctly.

2) If multiple ambiguous write methods are discovered, the algorithm now
   checks for an exact match against the resolved generic parameter type
   as a fallback.

Closes gh-36019

(cherry picked from commit 4b07edbaeb)
2025-12-29 12:41:46 +02:00
Sam Brannen add3b1a7f4 Extract CopyPropertiesTests as nested test class in BeanUtilsTests 2025-12-29 12:38:01 +02:00
Sam Brannen 476aae5ce8 Polish integration tests 2025-12-29 12:32:42 +02:00
Brian Clozel be68a777b6 Fix memory leak in WiretapConnector
Prior to this commit, we found in gh-35953 that using the `WebTestClient`
the following way leaks data buffers:

```
var body = client.get().uri("download")
  .exchange()
  .expectStatus().isOk()
  .returnResult()
  .getResponseBodyContent();
```

Here, the test performs expectations on the response status and headers,
but not on the response body. The WiretapConnector already supports this
case by subscribing to the Flux response body in those cases and
accumulating the entire content as a single byte[].

Here, the `DataBuffer` instances are not decoded by any `Decoder` and
are not released. This results in a memory leak.

This commit ensures that the automatic subscription in
`WiretapConnector` also releases the buffers automatically as the DSL
does not allow at that point to go back to performing body expectations.

Fixes gh-36050
2025-12-19 16:20:10 +01:00
Juergen Hoeller 9a0bfd7306 Do not attempt nested PropertyHandler resolution for argument conversion
This is not actually triggered on 6.2.x but nevertheless worth aligning.
Includes fix for return type declaration in PropertyAccessor subclasses.
Includes related polishing from main commits.

See gh-36024
2025-12-17 14:56:12 +01:00
Brian Clozel 91a0c28fa9 Fix RfcUriParser parsing for single char fragments
Prior to this commit, the `RfcUriParser` would ignore URI fragments if
their length is < 2. This commit fixes the length check to allow for
single char fragments when parsing URIs.

Fixes gh-36029
2025-12-17 09:54:08 +01:00
Sam Brannen c0e8a36592 Polishing
(cherry picked from commit ed451c107f)
2025-12-13 17:17:46 +01:00
Sam Brannen 836289315d Revise contribution
See gh-36022

(cherry picked from commit 452257eb96)
2025-12-13 17:16:49 +01:00
Tran Ngoc Nhan 08b77dd0ad Fix typos and grammar in reference manual
Closes gh-36022

Signed-off-by: Tran Ngoc Nhan <ngocnhan.tran1996@gmail.com>

(cherry picked from commit a7863a0877)
2025-12-13 17:16:04 +01:00
Stéphane Nicoll 5eb16a6a17 Refresh GitHub Actions 2025-12-12 11:58:22 +01:00
Brian Clozel d4705804cc Next development version (v6.2.16-SNAPSHOT) 2025-12-11 11:34:10 +01:00
129 changed files with 1892 additions and 765 deletions
@@ -24,14 +24,14 @@ runs:
using: composite
steps:
- name: Set Up Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }}
java-version: |
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
${{ inputs.java-toolchain == 'true' && '17' || '' }}
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
@@ -17,14 +17,14 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts
shell: bash
run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}'
- name: Sync
uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0
uses: spring-io/central-publish-action@0c03960e9b16fdfe70e2443e1d5393cbc3a35622 # v0.3.0
with:
token: ${{ inputs.central-token-password }}
token-name: ${{ inputs.central-token-username }}
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Set up Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'liberica'
java-version: 17
@@ -13,7 +13,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -21,7 +21,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Deploy
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build
id: build
uses: ./.github/actions/build
@@ -19,7 +19,7 @@ jobs:
uses: ./.github/actions/print-jvm-thread-dumps
- name: Upload Build Reports
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: build-reports
path: '**/build/reports/'
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
git config --global core.longPaths true
Stop-Service -name Docker
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build
id: build
uses: ./.github/actions/build
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 1
ref: docs-build
+5 -5
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -20,7 +20,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
@@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@v6
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
@@ -72,7 +72,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
@@ -85,7 +85,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
uses: actions/checkout@v6
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
+5 -5
View File
@@ -30,23 +30,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Release Verification Tests
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: 'v0.0.2'
repository: spring-projects/spring-framework-release-verification
token: ${{ secrets.token }}
- name: Check Out Send Notification Action
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
path: send-notification
sparse-checkout: .github/actions/send-notification
- name: Set Up Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: 'liberica'
java-version: 17
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
cache-read-only: false
- name: Configure Gradle Properties
@@ -64,7 +64,7 @@ jobs:
run: ./gradlew spring-framework-release-verification-tests:test
- name: Upload Build Reports on Failure
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: build-reports
path: '**/build/reports/'
+3 -3
View File
@@ -91,14 +91,14 @@ configure([rootProject] + javaProjects) { project ->
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.14/",
"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
//"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.14.1/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
// "https://junit.org/junit5/docs/5.14.2/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
// Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages,
@@ -104,7 +104,7 @@ Note that pointcut definitions are generally matched against any intercepted met
If a pointcut is strictly meant to be public-only, even in a CGLIB proxy scenario with
potential non-public interactions through proxies, it needs to be defined accordingly.
If your interception needs include method calls or even constructors within the target
If your interception needs to include method calls or even constructors within the target
class, consider the use of Spring-driven xref:core/aop/using-aspectj.adoc#aop-aj-ltw[native AspectJ weaving] instead
of Spring's proxy-based AOP framework. This constitutes a different mode of AOP usage
with different characteristics, so be sure to make yourself familiar with weaving
@@ -6,7 +6,7 @@ are set by using the `setVariable()` method in `EvaluationContext` implementatio
[NOTE]
====
Variable names must be begin with a letter (as defined below), an underscore, or a dollar
Variable names must begin with a letter (as defined below), an underscore, or a dollar
sign.
Variable names must be composed of one or more of the following supported types of
@@ -15,7 +15,7 @@ configuration options. You should instantiate the `SimpleJdbcInsert` in the data
layer's initialization method. For this example, the initializing method is the
`setDataSource` method. You do not need to subclass the `SimpleJdbcInsert` class. Instead,
you can create a new instance and set the table name by using the `withTableName` method.
Configuration methods for this class follow the `fluid` style that returns the instance
Configuration methods for this class follow the `fluent` style that returns the instance
of the `SimpleJdbcInsert`, which lets you chain all configuration methods. The following
example uses only one configuration method (we show examples of multiple methods later):
@@ -349,11 +349,11 @@ parameters return the data read from the table.
You can declare `SimpleJdbcCall` in a manner similar to declaring `SimpleJdbcInsert`. You
should instantiate and configure the class in the initialization method of your data-access
layer. Compared to the `StoredProcedure` class, you need not create a subclass
and you need not to declare parameters that can be looked up in the database metadata.
The following example of a `SimpleJdbcCall` configuration uses the preceding stored
procedure (the only configuration option, in addition to the `DataSource`, is the name
of the stored procedure):
layer. In contrast to the `StoredProcedure` class, you do not need to create a subclass,
and you do not need to declare parameters that can be looked up in the database metadata.
The following `SimpleJdbcCall` configuration example uses the preceding stored procedure.
The only configuration option (other than the `DataSource`) is the name of the stored
procedure.
[tabs]
======
@@ -1,16 +1,11 @@
[[appendix]]
= Appendix
[[appendix.xsd-schemas]]
== XML Schemas
This part of the appendix lists XML schemas related to integration technologies.
[[appendix.xsd-schemas-jee]]
=== The `jee` Schema
@@ -172,7 +167,7 @@ different properties with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -184,7 +179,7 @@ without `jee`:
</bean>
----
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -200,7 +195,7 @@ with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
and a number of properties without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -215,7 +210,7 @@ and a number of properties without `jee`:
</bean>
----
The following example shows how to configures a reference to a local EJB Stateless Session Bean
The following example shows how to configure a reference to a local EJB Stateless Session Bean
and a number of properties with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -234,7 +229,7 @@ and a number of properties with `jee`:
The `<jee:remote-slsb/>` element configures a reference to a `remote` EJB Stateless Session Bean.
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -251,7 +246,7 @@ without `jee`:
</bean>
----
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -313,7 +308,7 @@ xref:integration/jmx/naming.adoc#jmx-context-mbeanexport[Configuring Annotation-
=== The `cache` Schema
You can use the `cache` elements to enable support for Spring's `@CacheEvict`, `@CachePut`,
and `@Caching` annotations. It it also supports declarative XML-based caching. See
and `@Caching` annotations. The `cache` schema also supports declarative XML-based caching. See
xref:integration/cache/annotations.adoc#cache-annotation-enable[Enabling Caching Annotations] and
xref:integration/cache/declarative-xml.adoc[Declarative XML-based Caching] for details.
@@ -13,4 +13,4 @@ running the Kotlin compiler with its `-java-parameters` flag for standard Java p
You can declare configuration classes as
{kotlin-docs}/nested-classes.html[top level or nested but not inner],
since the later requires a reference to the outer class.
since the latter requires a reference to the outer class.
@@ -187,7 +187,7 @@ default mode may be set via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
The default mode may also be configured as a
https://docs.junit.org/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
https://docs.junit.org/current/running-tests/configuration-parameters.html[JUnit Platform configuration parameter].
If the `spring.test.constructor.autowire.mode` property is not set, test class
constructors will not be automatically autowired.
@@ -22,7 +22,7 @@ TestNG:
* Dependency injection for test constructors, test methods, and test lifecycle callback
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
Injection with the `SpringExtension`] for further details.
* Powerful support for link:https://docs.junit.org/current/user-guide/#extensions-conditions[conditional
* Powerful support for link:https://docs.junit.org/current/extensions/conditional-test-execution.html[conditional
test execution] based on SpEL expressions, environment variables, system properties,
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
@@ -160,7 +160,7 @@ for further details.
=== Dependency Injection with the `SpringExtension`
The `SpringExtension` implements the
link:https://docs.junit.org/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
link:https://docs.junit.org/current/extensions/parameter-resolution.html[`ParameterResolver`]
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
constructors, test methods, and test lifecycle callback methods.
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
- The Jackson JSON and XML message converters use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
@@ -418,16 +418,23 @@ See the section on xref:web/webflux-cors.adoc[CORS] and the xref:web/webflux-cor
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
Adding trailing slash variants to all mapping declarations is not the best way to handle this use case.
The `UrlHandlerFilter` web filter has been designed for this purpose. It can be configured to:
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* mutate the request to act as if the request was sent without a trailing slash and continue the processing of the request.
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* mutate the request to remove the trailing slash.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
[[webflux-exception-handler]]
== Exceptions
@@ -120,17 +120,26 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
== URL Handler
[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]#
In previous Spring Framework versions, Spring MVC could be configured to ignore trailing slashes in URL paths
when mapping incoming requests on controller methods. This could be done by enabling the `setUseTrailingSlashMatch`
option on the `PathMatchConfigurer`. This means that sending a "GET /home/" request would be handled by a controller
method annotated with `@GetMapping("/home")`.
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
This option has been retired, but applications are still expected to handle such requests in a safe way.
The `UrlHandlerFilter` Servlet filter has been designed for this purpose. It can be configured to:
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request.
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* wrap the request to remove the trailing slash.
NOTE: Historically Spring MVC supported trailing slash matching of URL paths.
This capability was deprecated in 6.0 for security reasons and removed in 7.0 with
`UrlHandlerFilter` providing a safer alternative.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
- The Jackson JSON and XML codecs use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
To enable RFC 9457 responses for Spring MVC exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
+5 -5
View File
@@ -1,11 +1,11 @@
{
"dependencies": {
"antora": "3.2.0-alpha.4",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-alpha.3",
"antora": "3.2.0-alpha.11",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.2",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "4.5.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
"fast-xml-parser": "5.3.4",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
}
}
+9 -10
View File
@@ -200,6 +200,7 @@
See the License for the specific language governing permissions and
limitations under the License.
=======================================================================
SPRING FRAMEWORK ${version} SUBCOMPONENTS:
@@ -212,7 +213,7 @@ code for these subcomponents is subject to the terms and
conditions of the following licenses.
>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1):
>>> ASM 9.9.1 (org.ow2.asm:asm:9.9.1):
Copyright (c) 2000-2011 INRIA, France Telecom
All rights reserved.
@@ -249,10 +250,8 @@ Copyright (c) 1999-2009, OW2 Consortium <https://www.ow2.org/>
>>> CGLIB 3.3 (cglib:cglib:3.3):
Per the LICENSE file in the CGLIB JAR distribution downloaded from
https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar,
CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which
is included above.
Per the LICENSE file in the CGLIB distribution, CGLIB 3.3 is licensed
under the Apache License, version 2.0, the text of which is included above.
>>> JavaPoet 1.13.0 (com.squareup:javapoet:1.13.0):
@@ -263,18 +262,18 @@ JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of
which is included above.
>>> Objenesis 3.4 (org.objenesis:objenesis:3.4):
>>> Objenesis 3.5 (org.objenesis:objenesis:3.5):
Per the LICENSE file in the Objenesis ZIP distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.4 is licensed under the
Per the LICENSE file in the Objenesis distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.5 is licensed under the
Apache License, version 2.0, the text of which is included above.
Per the NOTICE file in the Objenesis ZIP distribution downloaded from
Per the NOTICE file in the Objenesis distribution downloaded from
http://objenesis.org/download.html and corresponding to section 4d of the
Apache License, Version 2.0, in this case for Objenesis:
Objenesis
Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita
Copyright 2006-2026 Joe Walnes, Henri Tremblay, Leonardo Mesquita
===============================================================================
+16 -16
View File
@@ -8,20 +8,20 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.5"))
api(platform("io.micrometer:micrometer-bom:1.14.14"))
api(platform("io.netty:netty-bom:4.1.128.Final"))
api(platform("io.micrometer:micrometer-bom:1.15.9"))
api(platform("io.netty:netty-bom:4.1.130.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.13"))
api(platform("io.projectreactor:reactor-bom:2024.0.15"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.29"))
api(platform("org.apache.groovy:groovy-bom:4.0.30"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.6"))
api(platform("org.assertj:assertj-bom:3.27.7"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.30"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.30"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.14.1"))
api(platform("org.mockito:mockito-bom:5.20.0"))
api(platform("org.junit:junit-bom:5.14.2"))
api(platform("org.mockito:mockito-bom:5.21.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.4")
@@ -31,7 +31,7 @@ dependencies {
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.13.2")
api("com.google.protobuf:protobuf-java-util:4.32.1")
api("com.google.protobuf:protobuf-java-util:4.33.4")
api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.10.0")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
@@ -90,11 +90,11 @@ dependencies {
api("junit:junit:4.13.2")
api("net.sf.jopt-simple:jopt-simple:5.0.4")
api("org.apache-extras.beanshell:bsh:2.0b6")
api("org.apache.activemq:activemq-broker:5.17.7")
api("org.apache.activemq:activemq-kahadb-store:5.17.7")
api("org.apache.activemq:activemq-stomp:5.17.7")
api("org.apache.activemq:artemis-jakarta-client:2.42.0")
api("org.apache.activemq:artemis-junit-5:2.42.0")
api("org.apache.activemq:activemq-broker:5.17.7")
api("org.apache.activemq:activemq-kahadb-store:5.17.7")
api("org.apache.activemq:activemq-stomp:5.17.7")
api("org.apache.activemq:artemis-jakarta-client:2.42.0")
api("org.apache.activemq:artemis-junit-5:2.42.0")
api("org.apache.commons:commons-pool2:2.9.0")
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
@@ -129,7 +129,7 @@ dependencies {
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.18.0")
api("org.htmlunit:htmlunit:4.21.0")
api("org.javamoney:moneta:1.4.4")
api("org.jruby:jruby:9.4.13.0")
api("org.junit.support:testng-engine:1.0.5")
@@ -137,8 +137,8 @@ dependencies {
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.seleniumhq.selenium:htmlunit3-driver:4.38.0")
api("org.seleniumhq.selenium:selenium-java:4.38.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.40.0")
api("org.seleniumhq.selenium:selenium-java:4.40.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.17")
api("org.testng:testng:7.11.0")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.15
version=6.2.16
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -489,6 +489,9 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
@Override
@Nullable
public Class<?> getPropertyType(String propertyName) throws BeansException {
if (this.wrappedObject == null) {
return null;
}
try {
PropertyHandler ph = getPropertyHandler(propertyName);
if (ph != null) {
@@ -192,7 +192,7 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements
@Override
@Nullable
protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) {
protected PropertyHandler getLocalPropertyHandler(String propertyName) {
PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName);
return (pd != null ? new BeanPropertyHandler((GenericTypeAwarePropertyDescriptor) pd) : null);
}
@@ -73,7 +73,7 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
@Override
@Nullable
protected FieldPropertyHandler getLocalPropertyHandler(String propertyName) {
protected PropertyHandler getLocalPropertyHandler(String propertyName) {
FieldPropertyHandler propertyHandler = this.fieldMap.get(propertyName);
if (propertyHandler == null) {
Field field = ReflectionUtils.findField(getWrappedClass(), propertyName);
@@ -26,6 +26,7 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -35,6 +36,7 @@ import org.springframework.util.StringUtils;
*
* @author Chris Beams
* @author Juergen Hoeller
* @author Sam Brannen
*/
abstract class PropertyDescriptorUtils {
@@ -99,14 +101,13 @@ abstract class PropertyDescriptorUtils {
}
else {
Method readMethod = pd.getReadMethod();
if (readMethod == null ||
(readMethod.getReturnType() == method.getReturnType() && method.getName().startsWith("is"))) {
if (readMethod == null || readMethod.getReturnType().isAssignableFrom(method.getReturnType())) {
pd.setReadMethod(method);
}
}
}
else {
pd = new BasicPropertyDescriptor(propertyName, (!setter ? method : null), (setter ? method : null));
pd = new BasicPropertyDescriptor(propertyName, beanClass, (!setter ? method : null), (setter ? method : null));
pdMap.put(propertyName, pd);
}
}
@@ -264,6 +265,8 @@ abstract class PropertyDescriptorUtils {
*/
private static class BasicPropertyDescriptor extends PropertyDescriptor {
private final Class<?> beanClass;
@Nullable
private Method readMethod;
@@ -272,10 +275,11 @@ abstract class PropertyDescriptorUtils {
private final List<Method> alternativeWriteMethods = new ArrayList<>();
public BasicPropertyDescriptor(String propertyName, @Nullable Method readMethod, @Nullable Method writeMethod)
public BasicPropertyDescriptor(String propertyName, Class<?> beanClass, @Nullable Method readMethod, @Nullable Method writeMethod)
throws IntrospectionException {
super(propertyName, readMethod, writeMethod);
this.beanClass = beanClass;
}
@Override
@@ -307,14 +311,24 @@ abstract class PropertyDescriptorUtils {
public Method getWriteMethod() {
if (this.writeMethod == null && !this.alternativeWriteMethods.isEmpty()) {
if (this.readMethod == null) {
return this.alternativeWriteMethods.get(0);
this.writeMethod = this.alternativeWriteMethods.get(0);
}
else {
for (Method method : this.alternativeWriteMethods) {
// Check subtype match first.
if (this.readMethod.getReturnType().isAssignableFrom(method.getParameterTypes()[0])) {
this.writeMethod = method;
break;
}
// Check exact match against resolved generic parameter type as a fallback.
if (!(method.getGenericParameterTypes()[0] instanceof Class<?>)) {
Class<?> resolvedParameterType =
ResolvableType.forMethodParameter(method, 0, this.beanClass).toClass();
if (this.readMethod.getReturnType().equals(resolvedParameterType)) {
this.writeMethod = method;
break;
}
}
}
}
}
@@ -322,5 +336,4 @@ abstract class PropertyDescriptorUtils {
}
}
}
@@ -33,10 +33,12 @@ import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.PropertyDescriptorUtilsPropertyResolutionTests.ServiceWithOverriddenGetterAndOverloadedSetter;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.testfixture.beans.DerivedTestBean;
@@ -67,14 +69,14 @@ class BeanUtilsTests {
@Test
void instantiateClassGivenInterface() {
assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() ->
BeanUtils.instantiateClass(List.class));
assertThatExceptionOfType(FatalBeanException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(List.class));
}
@Test
void instantiateClassGivenClassWithoutDefaultConstructor() {
assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() ->
BeanUtils.instantiateClass(CustomDateEditor.class));
assertThatExceptionOfType(FatalBeanException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(CustomDateEditor.class));
}
@Test // gh-22531
@@ -91,16 +93,16 @@ class BeanUtilsTests {
void instantiateClassWithFewerArgsThanParameters() throws NoSuchMethodException {
Constructor<BeanWithPrimitiveTypes> constructor = getBeanWithPrimitiveTypesConstructor();
assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() ->
BeanUtils.instantiateClass(constructor, null, null, "foo"));
assertThatExceptionOfType(BeanInstantiationException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(constructor, null, null, "foo"));
}
@Test // gh-22531
void instantiateClassWithMoreArgsThanParameters() throws NoSuchMethodException {
Constructor<BeanWithPrimitiveTypes> constructor = getBeanWithPrimitiveTypesConstructor();
assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() ->
BeanUtils.instantiateClass(constructor, null, null, null, null, null, null, null, null, "foo", null));
assertThatExceptionOfType(BeanInstantiationException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(constructor, null, null, null, null, null, null, null, null, "foo", null));
}
@Test // gh-22531, gh-27390
@@ -157,267 +159,6 @@ class BeanUtilsTests {
assertThat(BeanUtils.findEditorByConvention(Resource.class).getClass()).isEqualTo(ResourceEditor.class);
}
@Test
void copyProperties() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes1() throws Exception {
DerivedTestBean tb = new DerivedTestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes2() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
DerivedTestBean tb2 = new DerivedTestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
/**
* {@code Integer} can be copied to {@code Number}.
*/
@Test
void copyPropertiesFromSubTypeToSuperType() {
IntegerHolder integerHolder = new IntegerHolder();
integerHolder.setNumber(42);
NumberHolder numberHolder = new NumberHolder();
BeanUtils.copyProperties(integerHolder, numberHolder);
assertThat(integerHolder.getNumber()).isEqualTo(42);
assertThat(numberHolder.getNumber()).isEqualTo(42);
}
/**
* {@code List<Integer>} can be copied to {@code List<Integer>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
BeanUtils.copyProperties(integerListHolder1, integerListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(integerListHolder2.getList()).containsExactly(42);
}
/**
* {@code List<?>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromWildcardToWildcard() {
List<?> list = List.of("foo", 42);
WildcardListHolder1 wildcardListHolder1 = new WildcardListHolder1();
wildcardListHolder1.setList(list);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
assertThat(wildcardListHolder2.getList()).isEmpty();
BeanUtils.copyProperties(wildcardListHolder1, wildcardListHolder2);
assertThat(wildcardListHolder1.getList()).isEqualTo(list);
assertThat(wildcardListHolder2.getList()).isEqualTo(list);
}
/**
* {@code List<Integer>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(wildcardListHolder2.getList()).isEqualTo(List.of(42));
}
/**
* {@code List<Integer>} can be copied to {@code List<? extends Number>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder();
BeanUtils.copyProperties(integerListHolder1, numberListHolder);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEqualTo(List.of(42));
}
/**
* {@code Number} can NOT be copied to {@code Integer}.
*/
@Test
void copyPropertiesDoesNotCopyFromSuperTypeToSubType() {
NumberHolder numberHolder = new NumberHolder();
numberHolder.setNumber(42);
IntegerHolder integerHolder = new IntegerHolder();
BeanUtils.copyProperties(numberHolder, integerHolder);
assertThat(numberHolder.getNumber()).isEqualTo(42);
assertThat(integerHolder.getNumber()).isNull();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Long>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatches() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
LongListHolder longListHolder = new LongListHolder();
BeanUtils.copyProperties(integerListHolder, longListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(longListHolder.getList()).isEmpty();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Number>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
NumberListHolder numberListHolder = new NumberListHolder();
BeanUtils.copyProperties(integerListHolder, numberListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEmpty();
}
@Test // gh-26531
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
Order original = new Order("test", List.of("foo", "bar"));
// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
assertThat(proxy.getLineItems()).containsExactly("foo", "bar");
// Copy from proxy to target.
Order target = new Order();
BeanUtils.copyProperties(proxy, target);
assertThat(target.getId()).isEqualTo("test");
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}
@Test // gh-32888
public void copyPropertiesWithGenericCglibClass() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
User user = (User) enhancer.create();
user.setId(1);
user.setName("proxy");
user.setAddress("addr");
User target = new User();
BeanUtils.copyProperties(user, target);
assertThat(target.getId()).isEqualTo(user.getId());
assertThat(target.getName()).isEqualTo(user.getName());
assertThat(target.getAddress()).isEqualTo(user.getAddress());
}
@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "touchy" should not be copied: it's not defined in ITestBean
BeanUtils.copyProperties(tb, tb2, ITestBean.class);
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age copied").isEqualTo(32);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnore() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "spouse", "touchy", "age" should not be copied
BeanUtils.copyProperties(tb, tb2, "spouse", "touchy", "age");
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age still empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnoredNonExistingProperty() {
NameAndSpecialProperty source = new NameAndSpecialProperty();
source.setName("name");
TestBean target = new TestBean();
BeanUtils.copyProperties(source, target, "specialProperty");
assertThat(target.getName()).isEqualTo("name");
}
@Test
void copyPropertiesWithInvalidProperty() {
InvalidProperty source = new InvalidProperty();
source.setName("name");
source.setFlag1(true);
source.setFlag2(true);
InvalidProperty target = new InvalidProperty();
BeanUtils.copyProperties(source, target);
assertThat(target.getName()).isEqualTo("name");
assertThat((boolean) target.getFlag1()).isTrue();
assertThat(target.getFlag2()).isTrue();
}
@Test
void resolveSimpleSignature() throws Exception {
Method desiredMethod = MethodSignatureBean.class.getMethod("doSomething");
@@ -427,14 +168,14 @@ class BeanUtilsTests {
@Test
void resolveInvalidSignatureEndParen() {
assertThatIllegalArgumentException().isThrownBy(() ->
BeanUtils.resolveSignature("doSomething(", MethodSignatureBean.class));
assertThatIllegalArgumentException()
.isThrownBy(() -> BeanUtils.resolveSignature("doSomething(", MethodSignatureBean.class));
}
@Test
void resolveInvalidSignatureStartParen() {
assertThatIllegalArgumentException().isThrownBy(() ->
BeanUtils.resolveSignature("doSomething)", MethodSignatureBean.class));
assertThatIllegalArgumentException()
.isThrownBy(() -> BeanUtils.resolveSignature("doSomething)", MethodSignatureBean.class));
}
@Test
@@ -538,6 +279,283 @@ class BeanUtilsTests {
}
@Nested
class CopyPropertiesTests {
@Test
void copyProperties() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes1() throws Exception {
DerivedTestBean tb = new DerivedTestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes2() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
DerivedTestBean tb2 = new DerivedTestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
/**
* {@code Integer} can be copied to {@code Number}.
*/
@Test
void copyPropertiesFromSubTypeToSuperType() {
IntegerHolder integerHolder = new IntegerHolder();
integerHolder.setNumber(42);
NumberHolder numberHolder = new NumberHolder();
BeanUtils.copyProperties(integerHolder, numberHolder);
assertThat(integerHolder.getNumber()).isEqualTo(42);
assertThat(numberHolder.getNumber()).isEqualTo(42);
}
/**
* {@code List<Integer>} can be copied to {@code List<Integer>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
BeanUtils.copyProperties(integerListHolder1, integerListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(integerListHolder2.getList()).containsExactly(42);
}
/**
* {@code List<?>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromWildcardToWildcard() {
List<?> list = List.of("foo", 42);
WildcardListHolder1 wildcardListHolder1 = new WildcardListHolder1();
wildcardListHolder1.setList(list);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
assertThat(wildcardListHolder2.getList()).isEmpty();
BeanUtils.copyProperties(wildcardListHolder1, wildcardListHolder2);
assertThat(wildcardListHolder1.getList()).isEqualTo(list);
assertThat(wildcardListHolder2.getList()).isEqualTo(list);
}
/**
* {@code List<Integer>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(wildcardListHolder2.getList()).isEqualTo(List.of(42));
}
/**
* {@code List<Integer>} can be copied to {@code List<? extends Number>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder();
BeanUtils.copyProperties(integerListHolder1, numberListHolder);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEqualTo(List.of(42));
}
/**
* {@code Number} can NOT be copied to {@code Integer}.
*/
@Test
void copyPropertiesDoesNotCopyFromSuperTypeToSubType() {
NumberHolder numberHolder = new NumberHolder();
numberHolder.setNumber(42);
IntegerHolder integerHolder = new IntegerHolder();
BeanUtils.copyProperties(numberHolder, integerHolder);
assertThat(numberHolder.getNumber()).isEqualTo(42);
assertThat(integerHolder.getNumber()).isNull();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Long>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatches() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
LongListHolder longListHolder = new LongListHolder();
BeanUtils.copyProperties(integerListHolder, longListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(longListHolder.getList()).isEmpty();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Number>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
NumberListHolder numberListHolder = new NumberListHolder();
BeanUtils.copyProperties(integerListHolder, numberListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEmpty();
}
@Test // gh-26531
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
Order original = new Order("test", List.of("foo", "bar"));
// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
assertThat(proxy.getLineItems()).containsExactly("foo", "bar");
// Copy from proxy to target.
Order target = new Order();
BeanUtils.copyProperties(proxy, target);
assertThat(target.getId()).isEqualTo("test");
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}
@Test // gh-32888
void copyPropertiesWithGenericCglibClass() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
User user = (User) enhancer.create();
user.setId(1);
user.setName("proxy");
user.setAddress("addr");
User target = new User();
BeanUtils.copyProperties(user, target);
assertThat(target.getId()).isEqualTo(user.getId());
assertThat(target.getName()).isEqualTo(user.getName());
assertThat(target.getAddress()).isEqualTo(user.getAddress());
}
@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "touchy" should not be copied: it's not defined in ITestBean
BeanUtils.copyProperties(tb, tb2, ITestBean.class);
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age copied").isEqualTo(32);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnore() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "spouse", "touchy", "age" should not be copied
BeanUtils.copyProperties(tb, tb2, "spouse", "touchy", "age");
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age still empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnoredNonExistingProperty() {
NameAndSpecialProperty source = new NameAndSpecialProperty();
source.setName("name");
TestBean target = new TestBean();
BeanUtils.copyProperties(source, target, "specialProperty");
assertThat(target.getName()).isEqualTo("name");
}
@Test
void copyPropertiesWithInvalidProperty() {
InvalidProperty source = new InvalidProperty();
source.setName("name");
source.setFlag1(true);
source.setFlag2(true);
InvalidProperty target = new InvalidProperty();
BeanUtils.copyProperties(source, target);
assertThat(target.getName()).isEqualTo("name");
assertThat((boolean) target.getFlag1()).isTrue();
assertThat(target.getFlag2()).isTrue();
}
@Test // gh-36019
void copyPropertiesHonorsGenericsInTypeHieararchyAndIgnoresOverloadedSetterMethod() {
var source = new ServiceWithOverriddenGetterAndOverloadedSetter();
var target = new ServiceWithOverriddenGetterAndOverloadedSetter();
source.setId(1);
BeanUtils.copyProperties(source, target);
assertThat(target.getId()).isEqualTo(source.getId()).isEqualTo("1");
}
}
public record RecordWithMultiplePublicConstructors(String value, String name) {
@SuppressWarnings("unused")
public RecordWithMultiplePublicConstructors(String value) {
@@ -0,0 +1,265 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.FieldSource;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.junit.jupiter.api.Named.named;
/**
* Unit tests for property descriptor resolution via
* {@link PropertyDescriptorUtils#determineBasicProperties(Class)}.
*
* <p>Results are compared to the behavior of the standard {@link Introspector}.
*
* @author Sam Brannen
* @since 6.2.16
*/
@ParameterizedClass(name = "{0}")
@FieldSource("resolvers")
class PropertyDescriptorUtilsPropertyResolutionTests {
static final List<Named<PropertiesResolver>> resolvers = List.of(
named("Basic Properties", new BasicPropertiesResolver()),
named("Standard Properties", new StandardPropertiesResolver()));
@Parameter
PropertiesResolver resolver;
@Test
void determineBasicPropertiesWithUnresolvedGenericsInInterface() {
var pdMap = resolver.resolve(GenericService.class);
assertThat(pdMap).containsOnlyKeys("id");
assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class);
}
@Test
void determineBasicPropertiesWithUnresolvedGenericsInSubInterface() {
// FYI: java.beans.Introspector does not resolve properties for sub-interfaces.
assumeThat(resolver).isNotInstanceOf(StandardPropertiesResolver.class);
var pdMap = resolver.resolve(SubGenericService.class);
assertThat(pdMap).containsOnlyKeys("id");
assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class);
}
@Test
void resolvePropertiesWithUnresolvedGenericsInClass() {
var pdMap = resolver.resolve(BaseService.class);
assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class);
}
@Test // gh-36019
void resolvePropertiesInSubclassWithOverriddenGetterAndSetter() {
var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndSetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, String.class, String.class);
}
@Test // gh-36019
void resolvePropertiesWithUnresolvedGenericsInSubclassWithOverloadedSetter() {
var pdMap = resolver.resolve(ServiceWithOverloadedSetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class);
}
@Test // gh-36019
void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetter() {
var pdMap = resolver.resolve(ServiceWithOverriddenGetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class);
}
@Test // gh-36019
void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetterAndOverloadedSetter() {
var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndOverloadedSetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class);
}
private static void assertReadAndWriteMethodsForClassAndId(Map<String, List<PropertyDescriptor>> pdMap,
Class<?> readType, Class<?> writeType) {
assertThat(pdMap).containsOnlyKeys("class", "id");
assertReadAndWriteMethodsForClass(pdMap.get("class"));
assertReadAndWriteMethodsForId(pdMap.get("id"), readType, writeType);
}
private static void assertReadAndWriteMethodsForClass(List<PropertyDescriptor> pds) {
assertThat(pds).hasSize(1);
var pd = pds.get(0);
assertThat(pd.getName()).isEqualTo("class");
var readMethod = pd.getReadMethod();
assertThat(readMethod.getName()).isEqualTo("getClass");
assertThat(readMethod.getReturnType()).as("read type").isEqualTo(Class.class);
assertThat(readMethod.getParameterCount()).isZero();
assertThat(pd.getWriteMethod()).as("write method").isNull();
}
private static void assertReadAndWriteMethodsForId(List<PropertyDescriptor> pds, Class<?> readType, Class<?> writeType) {
assertThat(pds).hasSize(1);
var pd = pds.get(0);
assertThat(pd.getName()).isEqualTo("id");
var readMethod = pd.getReadMethod();
var writeMethod = pd.getWriteMethod();
assertSoftly(softly -> {
softly.assertThat(readMethod.getName()).isEqualTo("getId");
softly.assertThat(readMethod.getReturnType()).as("read type").isEqualTo(readType);
softly.assertThat(readMethod.getParameterCount()).isZero();
softly.assertThat(writeMethod).as("write method").isNotNull();
if (writeMethod != null) {
softly.assertThat(writeMethod.getName()).isEqualTo("setId");
softly.assertThat(writeMethod.getReturnType()).isEqualTo(void.class);
softly.assertThat(writeMethod.getParameterCount()).isEqualTo(1);
softly.assertThat(writeMethod.getParameterTypes()[0]).as("write type").isEqualTo(writeType);
}
});
}
private static Map<String, List<PropertyDescriptor>> toMap(Stream<? extends PropertyDescriptor> stream) {
return stream.collect(groupingBy(PropertyDescriptor::getName, toList()));
}
interface PropertiesResolver {
Map<String, List<PropertyDescriptor>> resolve(Class<?> beanClass);
}
static class BasicPropertiesResolver implements PropertiesResolver {
@Override
public Map<String, List<PropertyDescriptor>> resolve(Class<?> beanClass) {
try {
var pds = PropertyDescriptorUtils.determineBasicProperties(beanClass);
return toMap(pds.stream());
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
static class StandardPropertiesResolver implements PropertiesResolver {
@Override
public Map<String, List<PropertyDescriptor>> resolve(Class<?> beanClass) {
try {
var beanInfo = Introspector.getBeanInfo(beanClass);
return toMap(Arrays.stream(beanInfo.getPropertyDescriptors()));
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
interface GenericService<T> {
void setId(T id);
T getId();
}
interface SubGenericService<T> extends GenericService<T> {
}
static class BaseService<T> {
private T id;
public T getId() {
return id;
}
public void setId(T id) {
this.id = id;
}
}
static class ServiceWithOverriddenGetterAndSetter extends BaseService<String>
implements SubGenericService<String> {
@Override
public String getId() {
return super.getId();
}
@Override
public void setId(String id) {
super.setId(id);
}
}
static class ServiceWithOverloadedSetter extends BaseService<String>
implements SubGenericService<String> {
public void setId(int id) {
setId(String.valueOf(id));
}
}
static class ServiceWithOverriddenGetter extends BaseService<String>
implements SubGenericService<String> {
@Override
public String getId() {
return super.getId();
}
}
static class ServiceWithOverriddenGetterAndOverloadedSetter extends BaseService<String>
implements SubGenericService<String> {
@Override
public String getId() {
return super.getId();
}
public void setId(int id) {
setId(String.valueOf(id));
}
}
}
@@ -380,7 +380,7 @@ class UtilNamespaceHandlerTests {
// For DependencyDescriptor resolution
private Map<String, TestBean> mapWithRef;
private Map<String, TestBean> mapWithTypes;
Map<String, TestBean> mapWithRef;
Map<String, TestBean> mapWithTypes;
}
@@ -44,7 +44,7 @@ import static org.assertj.core.api.Assertions.assertThat;
class CaffeineReactiveCachingTests {
@ParameterizedTest
@ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeConfig.class})
@ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeWithoutNullValuesConfig.class})
void cacheHitDetermination(Class<?> configClass) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
@@ -106,9 +106,8 @@ class CaffeineReactiveCachingTests {
ctx.close();
}
@ParameterizedTest
@ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeConfig.class})
@ValueSource(classes = {AsyncCacheModeConfig.class, AsyncCacheModeWithoutNullValuesConfig.class})
void fluxCacheDoesntDependOnFirstRequest(Class<?> configClass) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
@@ -39,10 +39,12 @@ class CacheEvaluationContextFactory {
@Nullable
private Supplier<ParameterNameDiscoverer> parameterNameDiscoverer;
CacheEvaluationContextFactory(StandardEvaluationContext originalContext) {
this.originalContext = originalContext;
}
public void setParameterNameDiscoverer(Supplier<ParameterNameDiscoverer> parameterNameDiscoverer) {
this.parameterNameDiscoverer = parameterNameDiscoverer;
}
@@ -54,6 +56,7 @@ class CacheEvaluationContextFactory {
return this.parameterNameDiscoverer.get();
}
/**
* Creates a {@link CacheEvaluationContext} for the specified operation.
* @param rootObject the {@code root} object to use for the context
@@ -27,6 +27,7 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -1067,11 +1068,9 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
@Override
public void run() {
if (isStartupShutdownThreadStuck()) {
active.set(false);
if (!tryLockForShutdown()) {
return;
}
startupShutdownLock.lock();
try {
doClose();
}
@@ -1084,6 +1083,30 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
}
}
/**
* Try to acquire the common startup/shutdown lock, backing out if
* the main startup/shutdown thread is stuck or on interruption.
* @see #isStartupShutdownThreadStuck()
*/
private boolean tryLockForShutdown() {
try {
while (!this.startupShutdownLock.tryLock(100, TimeUnit.MILLISECONDS)) {
if (!this.active.get() || this.closed.get()) {
return false;
}
if (isStartupShutdownThreadStuck()) {
this.active.set(false);
return false;
}
}
return true;
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* Determine whether an active startup/shutdown thread is currently stuck,
* for example, through a {@code System.exit} call in a user component.
@@ -1095,7 +1118,7 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
activeThread.interrupt();
try {
// Leave just a little bit of time for the interruption to show effect
Thread.sleep(1);
Thread.sleep(10);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
@@ -1117,12 +1140,10 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
*/
@Override
public void close() {
if (isStartupShutdownThreadStuck()) {
this.active.set(false);
if (!tryLockForShutdown()) {
return;
}
this.startupShutdownLock.lock();
try {
this.startupShutdownThread = Thread.currentThread();
@@ -377,47 +377,45 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter
protected void postProcessConfiguration(Configuration<?> configuration) {
}
private ValidatorFactory obtainValidatorFactory() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory;
}
@Override
public Validator getValidator() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory.getValidator();
return obtainValidatorFactory().getValidator();
}
@Override
public ValidatorContext usingContext() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory.usingContext();
return obtainValidatorFactory().usingContext();
}
@Override
public MessageInterpolator getMessageInterpolator() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory.getMessageInterpolator();
return obtainValidatorFactory().getMessageInterpolator();
}
@Override
public TraversableResolver getTraversableResolver() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory.getTraversableResolver();
return obtainValidatorFactory().getTraversableResolver();
}
@Override
public ConstraintValidatorFactory getConstraintValidatorFactory() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory.getConstraintValidatorFactory();
return obtainValidatorFactory().getConstraintValidatorFactory();
}
@Override
public ParameterNameProvider getParameterNameProvider() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory.getParameterNameProvider();
return obtainValidatorFactory().getParameterNameProvider();
}
@Override
public ClockProvider getClockProvider() {
Assert.state(this.validatorFactory != null, "No target ValidatorFactory set");
return this.validatorFactory.getClockProvider();
return obtainValidatorFactory().getClockProvider();
}
@Override
@@ -427,8 +425,8 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter
try {
return super.unwrap(type);
}
catch (ValidationException ex) {
// Ignore - we'll try ValidatorFactory unwrapping next
catch (ValidationException ignored) {
// Trying ValidatorFactory unwrapping next
}
}
if (this.validatorFactory != null) {
@@ -26,7 +26,7 @@ import org.springframework.context.support.MessageSourceResourceBundle;
import org.springframework.util.Assert;
/**
* Implementation of Hibernate Validator 4.3/5.x's {@link ResourceBundleLocator} interface,
* Implementation of Hibernate Validator's {@link ResourceBundleLocator} interface,
* exposing a Spring {@link MessageSource} as localized {@link MessageSourceResourceBundle}.
*
* @author Juergen Hoeller
@@ -39,6 +39,7 @@ public class MessageSourceResourceBundleLocator implements ResourceBundleLocator
private final MessageSource messageSource;
/**
* Build a MessageSourceResourceBundleLocator for the given MessageSource.
* @param messageSource the Spring MessageSource to wrap
@@ -48,6 +49,7 @@ public class MessageSourceResourceBundleLocator implements ResourceBundleLocator
this.messageSource = messageSource;
}
@Override
public ResourceBundle getResourceBundle(Locale locale) {
return new MessageSourceResourceBundle(this.messageSource, locale);
@@ -134,20 +134,15 @@ class ResourceElementResolverMethodTests {
private String one;
private String test;
private Integer count;
public void setOne(String one) {
this.one = one;
}
public void setTest(String test) {
this.test = test;
}
public void setCount(Integer count) {
this.count = count;
}
}
@@ -184,7 +184,7 @@ class PeriodicTriggerTests {
void equalsVerification() {
PeriodicTrigger trigger1 = new PeriodicTrigger(Duration.ofMillis(3000));
PeriodicTrigger trigger2 = new PeriodicTrigger(Duration.ofMillis(3000));
assertThat(trigger1.equals(new String("not a trigger"))).isFalse();
assertThat(trigger1).isNotEqualTo(new String("not a trigger"));
assertThat(trigger1).isNotEqualTo(null);
assertThat(trigger1).isEqualTo(trigger1);
assertThat(trigger2).isEqualTo(trigger2);
@@ -60,18 +60,13 @@ class BshScriptFactoryTests {
Calculator calc = (Calculator) ctx.getBean("calculator");
Messenger messenger = (Messenger) ctx.getBean("messenger");
boolean condition3 = calc instanceof Refreshable;
assertThat(condition3).as("Scripted object should not be instance of Refreshable").isFalse();
boolean condition2 = messenger instanceof Refreshable;
assertThat(condition2).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(calc).as("Scripted object should not be instance of Refreshable").isNotInstanceOf(Refreshable.class);
assertThat(messenger).as("Scripted object should not be instance of Refreshable").isNotInstanceOf(Refreshable.class);
assertThat(calc).isEqualTo(calc);
assertThat(messenger).isEqualTo(messenger);
boolean condition1 = !messenger.equals(calc);
assertThat(condition1).isTrue();
assertThat(messenger.hashCode()).isNotEqualTo(calc.hashCode());
boolean condition = !messenger.toString().equals(calc.toString());
assertThat(condition).isTrue();
assertThat(messenger.toString()).isNotEqualTo(calc.toString());
assertThat(calc.add(2, 3)).isEqualTo(5);
@@ -144,8 +139,7 @@ class BshScriptFactoryTests {
ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype");
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger).as("Scripted object should not be instance of Refreshable").isNotInstanceOf(Refreshable.class);
assertThat(messenger2).isNotSameAs(messenger);
assertThat(messenger2.getClass()).isSameAs(messenger.getClass());
@@ -164,8 +158,7 @@ class BshScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("messenger");
assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Should be an instance of Refreshable").isTrue();
assertThat(messenger).as("Should be an instance of Refreshable").isInstanceOf(Refreshable.class);
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -185,8 +178,7 @@ class BshScriptFactoryTests {
ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype");
assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Should be an instance of Refreshable").isTrue();
assertThat(messenger).as("Should be an instance of Refreshable").isInstanceOf(Refreshable.class);
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
assertThat(messenger2.getMessage()).isEqualTo("Hello World!");
@@ -206,9 +198,9 @@ class BshScriptFactoryTests {
@Test
void scriptCompilationException() {
assertThatExceptionOfType(NestedRuntimeException.class).isThrownBy(() ->
new ClassPathXmlApplicationContext("org/springframework/scripting/bsh/bshBrokenContext.xml"))
.matches(ex -> ex.contains(ScriptCompilationException.class));
assertThatExceptionOfType(NestedRuntimeException.class)
.isThrownBy(() -> new ClassPathXmlApplicationContext("org/springframework/scripting/bsh/bshBrokenContext.xml"))
.matches(ex -> ex.contains(ScriptCompilationException.class));
}
@Test
@@ -227,20 +219,17 @@ class BshScriptFactoryTests {
@Test
void ctorWithNullScriptSourceLocator() {
assertThatIllegalArgumentException().isThrownBy(() ->
new BshScriptFactory(null, Messenger.class));
assertThatIllegalArgumentException().isThrownBy(() -> new BshScriptFactory(null, Messenger.class));
}
@Test
void ctorWithEmptyScriptSourceLocator() {
assertThatIllegalArgumentException().isThrownBy(() ->
new BshScriptFactory("", Messenger.class));
assertThatIllegalArgumentException().isThrownBy(() -> new BshScriptFactory("", Messenger.class));
}
@Test
void ctorWithWhitespacedScriptSourceLocator() {
assertThatIllegalArgumentException().isThrownBy(() ->
new BshScriptFactory("\n ", Messenger.class));
assertThatIllegalArgumentException().isThrownBy(() -> new BshScriptFactory("\n ", Messenger.class));
}
@Test
@@ -255,8 +244,7 @@ class BshScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("messenger");
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
boolean condition = messenger instanceof Refreshable;
assertThat(condition).isFalse();
assertThat(messenger).isNotInstanceOf(Refreshable.class);
Messenger messengerImpl = (Messenger) ctx.getBean("messengerImpl");
assertThat(messengerImpl.getMessage()).isEqualTo("Hello World!");
@@ -305,8 +293,7 @@ class BshScriptFactoryTests {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("bsh-with-xsd.xml", getClass());
Calculator calculator = (Calculator) ctx.getBean("calculator");
assertThat(calculator).isNotNull();
boolean condition = calculator instanceof Refreshable;
assertThat(condition).isFalse();
assertThat(calculator).isNotInstanceOf(Refreshable.class);
ctx.close();
}
@@ -315,8 +302,7 @@ class BshScriptFactoryTests {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext("bsh-with-xsd.xml", getClass());
Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger");
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Messenger should be Refreshable").isTrue();
assertThat(messenger).as("Messenger should be Refreshable").isInstanceOf(Refreshable.class);
ctx.close();
}
@@ -78,18 +78,14 @@ public class GroovyScriptFactoryTests {
assertThat(AopUtils.isAopProxy(calc)).as("Shouldn't get proxy when refresh is disabled").isFalse();
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition3 = calc instanceof Refreshable;
assertThat(condition3).as("Scripted object should not be instance of Refreshable").isFalse();
boolean condition2 = messenger instanceof Refreshable;
assertThat(condition2).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(calc instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(calc).isEqualTo(calc);
assertThat(messenger).isEqualTo(messenger);
boolean condition1 = !messenger.equals(calc);
assertThat(condition1).isTrue();
assertThat(messenger).isNotEqualTo(calc);
assertThat(messenger.hashCode()).isNotEqualTo(calc.hashCode());
boolean condition = !messenger.toString().equals(calc.toString());
assertThat(condition).isTrue();
assertThat(messenger.toString()).isNotEqualTo(calc.toString());
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -111,18 +107,14 @@ public class GroovyScriptFactoryTests {
assertThat(AopUtils.isAopProxy(calc)).as("Shouldn't get proxy when refresh is disabled").isFalse();
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition3 = calc instanceof Refreshable;
assertThat(condition3).as("Scripted object should not be instance of Refreshable").isFalse();
boolean condition2 = messenger instanceof Refreshable;
assertThat(condition2).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(calc instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(calc).isEqualTo(calc);
assertThat(messenger).isEqualTo(messenger);
boolean condition1 = !messenger.equals(calc);
assertThat(condition1).isTrue();
assertThat(messenger).isNotEqualTo(calc);
assertThat(messenger.hashCode()).isNotEqualTo(calc.hashCode());
boolean condition = !messenger.toString().equals(calc.toString());
assertThat(condition).isTrue();
assertThat(messenger.toString()).isNotEqualTo(calc.toString());
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -138,8 +130,7 @@ public class GroovyScriptFactoryTests {
ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype");
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger2).isNotSameAs(messenger);
assertThat(messenger2.getClass()).isSameAs(messenger.getClass());
@@ -158,8 +149,7 @@ public class GroovyScriptFactoryTests {
ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype");
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger2).isNotSameAs(messenger);
assertThat(messenger2.getClass()).isSameAs(messenger.getClass());
@@ -178,8 +168,7 @@ public class GroovyScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("messengerInstance");
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -193,8 +182,7 @@ public class GroovyScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("messengerInstance");
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -208,8 +196,7 @@ public class GroovyScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("messengerInstanceInline");
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -223,8 +210,7 @@ public class GroovyScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("messengerInstanceInline");
assertThat(AopUtils.isAopProxy(messenger)).as("Shouldn't get proxy when refresh is disabled").isFalse();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Scripted object should not be instance of Refreshable").isFalse();
assertThat(messenger instanceof Refreshable).as("Scripted object should not be instance of Refreshable").isFalse();
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -237,8 +223,7 @@ public class GroovyScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("messenger");
assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Should be an instance of Refreshable").isTrue();
assertThat(messenger instanceof Refreshable).as("Should be an instance of Refreshable").isTrue();
String desiredMessage = "Hello World!";
assertThat(messenger.getMessage()).as("Message is incorrect").isEqualTo(desiredMessage);
@@ -257,8 +242,7 @@ public class GroovyScriptFactoryTests {
ConfigurableMessenger messenger2 = (ConfigurableMessenger) ctx.getBean("messengerPrototype");
assertThat(AopUtils.isAopProxy(messenger)).as("Should be a proxy for refreshable scripts").isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).as("Should be an instance of Refreshable").isTrue();
assertThat(messenger instanceof Refreshable).as("Should be an instance of Refreshable").isTrue();
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
assertThat(messenger2.getMessage()).isEqualTo("Hello World!");
@@ -371,8 +355,7 @@ public class GroovyScriptFactoryTests {
CallCounter countingAspect = (CallCounter) ctx.getBean("getMessageAspect");
assertThat(AopUtils.isAopProxy(messenger)).isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).isFalse();
assertThat(messenger instanceof Refreshable).isFalse();
assertThat(countingAspect.getCalls()).isEqualTo(0);
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
assertThat(countingAspect.getCalls()).isEqualTo(1);
@@ -404,8 +387,7 @@ public class GroovyScriptFactoryTests {
assertThat(ObjectUtils.containsElement(bd.getDependsOn(), "messenger")).isTrue();
Calculator calculator = (Calculator) ctx.getBean("calculator");
assertThat(calculator).isNotNull();
boolean condition = calculator instanceof Refreshable;
assertThat(condition).isFalse();
assertThat(calculator instanceof Refreshable).isFalse();
}
@Test
@@ -417,8 +399,7 @@ public class GroovyScriptFactoryTests {
CallCounter countingAspect = (CallCounter) ctx.getBean("getMessageAspect");
assertThat(AopUtils.isAopProxy(messenger)).isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).isTrue();
assertThat(messenger instanceof Refreshable).isTrue();
assertThat(countingAspect.getCalls()).isEqualTo(0);
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
assertThat(countingAspect.getCalls()).isEqualTo(1);
@@ -435,8 +416,7 @@ public class GroovyScriptFactoryTests {
Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger");
assertThat(AopUtils.isAopProxy(messenger)).isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).isTrue();
assertThat(messenger instanceof Refreshable).isTrue();
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
assertThat(ctx.getBeansOfType(ConcreteMessenger.class)).containsValue((ConcreteMessenger) messenger);
@@ -487,8 +467,7 @@ public class GroovyScriptFactoryTests {
assertThat(Arrays.asList(ctx.getBeanNamesForType(Messenger.class))).contains("refreshableMessenger");
Messenger messenger = (Messenger) ctx.getBean("refreshableMessenger");
assertThat(AopUtils.isAopProxy(messenger)).isTrue();
boolean condition = messenger instanceof Refreshable;
assertThat(condition).isTrue();
assertThat(messenger instanceof Refreshable).isTrue();
assertThat(messenger.getMessage()).isEqualTo("Hello World!");
}
@@ -542,20 +521,17 @@ public class GroovyScriptFactoryTests {
// expect the exception we threw in the custom metaclass to show it got invoked
ApplicationContext ctx = new ClassPathXmlApplicationContext(xmlFile);
Calculator calc = (Calculator) ctx.getBean("delegatingCalculator");
assertThatIllegalStateException().isThrownBy(() ->
calc.add(1, 2))
.withMessage("Gotcha");
assertThatIllegalStateException()
.isThrownBy(() -> calc.add(1, 2))
.withMessage("Gotcha");
}
@Test
void testFactoryBean() {
ApplicationContext context = new ClassPathXmlApplicationContext("groovyContext.xml", getClass());
Object factory = context.getBean("&factory");
boolean condition1 = factory instanceof FactoryBean;
assertThat(condition1).isTrue();
assertThat(factory instanceof FactoryBean).isTrue();
Object result = context.getBean("factory");
boolean condition = result instanceof String;
assertThat(condition).isTrue();
assertThat(result).isEqualTo("test");
}
@@ -563,11 +539,8 @@ public class GroovyScriptFactoryTests {
void testRefreshableFactoryBean() {
ApplicationContext context = new ClassPathXmlApplicationContext("groovyContext.xml", getClass());
Object factory = context.getBean("&refreshableFactory");
boolean condition1 = factory instanceof FactoryBean;
assertThat(condition1).isTrue();
assertThat(factory instanceof FactoryBean).isTrue();
Object result = context.getBean("refreshableFactory");
boolean condition = result instanceof String;
assertThat(condition).isTrue();
assertThat(result).isEqualTo("test");
}
@@ -43,6 +43,7 @@ class InvocationsRecorderClassTransformer implements ClassFileTransformer {
private final String[] ignoredPackages;
public InvocationsRecorderClassTransformer(String[] instrumentedPackages, String[] ignoredPackages) {
Assert.notNull(instrumentedPackages, "instrumentedPackages must not be null");
Assert.notNull(ignoredPackages, "ignoredPackages must not be null");
@@ -55,6 +56,7 @@ class InvocationsRecorderClassTransformer implements ClassFileTransformer {
.toArray(String[]::new);
}
@Override
public byte[] transform(@Nullable ClassLoader classLoader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
@@ -101,7 +103,7 @@ class InvocationsRecorderClassTransformer implements ClassFileTransformer {
fileReader.accept(classVisitor, 0);
}
catch (Exception ex) {
ex.printStackTrace();
System.err.println("Failed to transform class: " + ex);
return classfileBuffer;
}
if (classVisitor.isTransformed()) {
@@ -109,4 +111,5 @@ class InvocationsRecorderClassTransformer implements ClassFileTransformer {
}
return classfileBuffer;
}
}
@@ -45,7 +45,6 @@ public final class RuntimeHintsAgent {
private static boolean loaded = false;
private RuntimeHintsAgent() {
}
public static void premain(@Nullable String agentArgs, Instrumentation inst) {
@@ -64,6 +63,7 @@ public final class RuntimeHintsAgent {
return loaded;
}
private static final class ParsedArguments {
List<String> instrumentedPackages;
@@ -104,6 +104,6 @@ public final class RuntimeHintsAgent {
}
return new ParsedArguments(included, excluded);
}
}
}
+1 -1
View File
@@ -15,7 +15,7 @@ multiRelease {
}
def javapoetVersion = "1.13.0"
def objenesisVersion = "3.4"
def objenesisVersion = "3.5"
configurations {
java21Api.extendsFrom(api)
@@ -95,7 +95,7 @@ public class Attribute {
* a Code attribute that contains labels.
* @deprecated no longer used by ASM.
*/
@Deprecated
@Deprecated(forRemoval = false)
protected Label[] getLabels() {
return new Label[0];
}
@@ -174,6 +174,7 @@ public class Attribute {
* ClassReader overrides {@link ClassReader#readLabel}. Hence {@link #read(ClassReader, int, int,
* char[], int, Label[])} must not manually create {@link Label} instances.
*
* @param classReader the class that contains the attribute to be read.
* @param bytecodeOffset a bytecode offset in a method.
* @param labels the already created labels, indexed by their offset. If a label already exists
* for bytecodeOffset this method does not create a new one. Otherwise it stores the new label
@@ -100,7 +100,7 @@ public class ClassReader {
* @deprecated Use {@link #readByte(int)} and the other read methods instead. This field will
* eventually be deleted.
*/
@Deprecated
@Deprecated(forRemoval = false)
// DontCheck(MemberName): can't be renamed (for backward binary compatibility).
public final byte[] b;
@@ -197,7 +197,7 @@ public class ClassReader {
// Check the class' major_version. This field is after the magic and minor_version fields, which
// use 4 and 2 bytes respectively.
// SPRING PATCH: leniently try to parse newer class files as well
// if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V26) {
// if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V27) {
// throw new IllegalArgumentException(
// "Unsupported class file major version " + readShort(classFileOffset + 6));
// }
@@ -3550,6 +3550,9 @@ public class ClassReader {
final char[] charBuffer,
final int codeAttributeOffset,
final Label[] labels) {
if (length > classFileBuffer.length - offset) {
throw new IllegalArgumentException();
}
for (Attribute attributePrototype : attributePrototypes) {
if (attributePrototype.type.equals(type)) {
return attributePrototype.read(
@@ -897,7 +897,7 @@ public class ClassWriter extends ClassVisitor {
* @deprecated this method is superseded by {@link #newHandle(int, String, String, String,
* boolean)}.
*/
@Deprecated
@Deprecated(forRemoval = false)
public int newHandle(
final int tag, final String owner, final String name, final String descriptor) {
return newHandle(tag, owner, name, descriptor, tag == Opcodes.H_INVOKEINTERFACE);
@@ -71,7 +71,7 @@ public final class Handle {
* @deprecated this constructor has been superseded by {@link #Handle(int, String, String, String,
* boolean)}.
*/
@Deprecated
@Deprecated(forRemoval = false)
public Handle(final int tag, final String owner, final String name, final String descriptor) {
this(tag, owner, name, descriptor, tag == Opcodes.H_INVOKEINTERFACE);
}
@@ -414,7 +414,7 @@ public abstract class MethodVisitor {
* @param descriptor the method's descriptor (see {@link Type}).
* @deprecated use {@link #visitMethodInsn(int, String, String, String, boolean)} instead.
*/
@Deprecated
@Deprecated(forRemoval = false)
public void visitMethodInsn(
final int opcode, final String owner, final String name, final String descriptor) {
int opcodeAndSource = opcode | (api < Opcodes.ASM5 ? Opcodes.SOURCE_DEPRECATED : 0);
@@ -291,6 +291,7 @@ public interface Opcodes {
int V24 = 0 << 16 | 68;
int V25 = 0 << 16 | 69;
int V26 = 0 << 16 | 70;
int V27 = 0 << 16 | 71;
/**
* Version flag indicating that the class is using 'preview' features.
@@ -168,6 +168,7 @@ public interface Resource extends InputStreamSource {
/**
* Determine the content length for this resource.
* @return the content length (or -1 if undetermined)
* @throws IOException if the resource cannot be resolved
* (in the file system or as some other known physical resource type)
*/
@@ -175,6 +176,7 @@ public interface Resource extends InputStreamSource {
/**
* Determine the last-modified timestamp for this resource.
* @return the last-modified timestamp (or 0 if not known)
* @throws IOException if the resource cannot be resolved
* (in the file system or as some other known physical resource type)
*/
@@ -1205,7 +1205,13 @@ public abstract class DataBufferUtils {
long pos = this.position.get();
Attachment attachment = new Attachment(byteBuffer, dataBuffer, iterator);
this.writing.set(true);
this.channel.write(byteBuffer, pos, attachment, this);
try {
this.channel.write(byteBuffer, pos, attachment, this);
}
catch (RuntimeException ex) {
// If the exception escapes, route it to the failure handler
failed(ex, attachment);
}
}
}
@@ -25,14 +25,10 @@ import org.springframework.util.concurrent.FutureUtils;
/**
* Extended interface for asynchronous {@link TaskExecutor} implementations,
* offering support for {@link java.util.concurrent.Callable}.
*
* <p>Note: The {@link java.util.concurrent.Executors} class includes a set of
* methods that can convert some other common closure-like objects, for example,
* {@link java.security.PrivilegedAction} to {@link Callable} before executing them.
* offering support for {@link Future}/{@link CompletableFuture} handles.
*
* <p>Implementing this interface also indicates that the {@link #execute(Runnable)}
* method will not execute its Runnable in the caller's thread but rather
* method will not execute the given Runnable in the caller's thread but rather
* asynchronously in some other thread.
*
* @author Juergen Hoeller
@@ -125,8 +121,7 @@ public interface AsyncTaskExecutor extends TaskExecutor {
/**
* Submit a {@code Callable} task for execution, receiving a {@code CompletableFuture}
* representing that task. The Future will return the Callable's result upon
* completion.
* representing that task. The Future will return the Callable's result upon completion.
* @param task the {@code Callable} to execute (never {@code null})
* @return a {@code CompletableFuture} representing pending completion of the task
* @throws TaskRejectedException if the given task was not accepted
@@ -32,13 +32,13 @@ import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureTask;
/**
* {@link TaskExecutor} implementation that fires up a new Thread for each task,
* executing it asynchronously. Provides a virtual thread option on JDK 21.
* {@link TaskExecutor} implementation that fires up a new Thread for each task.
* Provides a {@link #setVirtualThreads virtual threads} option on JDK 21+.
*
* <p>Supports a graceful shutdown through {@link #setTaskTerminationTimeout},
* at the expense of task tracking overhead per execution thread at runtime.
* Supports limiting concurrent threads through {@link #setConcurrencyLimit}.
* By default, the number of concurrent task executions is unlimited.
* Supports limiting concurrent threads through {@link #setConcurrencyLimit};
* by default, the number of concurrent task executions is unlimited.
*
* <p><b>NOTE: This implementation does not reuse threads!</b> Consider a
* thread-pooling TaskExecutor implementation instead, in particular for
@@ -217,10 +217,11 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
/**
* Specify whether to reject tasks when the concurrency limit has been reached,
* throwing {@link TaskRejectedException} on any further submission attempts.
* throwing {@link TaskRejectedException} on any further execution attempts.
* <p>The default is {@code false}, blocking the caller until the submission can
* be accepted. Switch this to {@code true} for immediate rejection instead.
* @since 6.2.6
* @see #setConcurrencyLimit
*/
public void setRejectTasksWhenLimitReached(boolean rejectTasksWhenLimitReached) {
this.rejectTasksWhenLimitReached = rejectTasksWhenLimitReached;
@@ -1,6 +1,6 @@
/**
* Spring's repackaging of
* <a href="http://objenesis.org">Objenesis 3.4</a>
* <a href="http://objenesis.org">Objenesis 3.5</a>
* (with SpringObjenesis entry point; for internal use only).
*
* <p>This repackaging technique avoids any potential conflicts with
@@ -386,6 +386,13 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
@Override
@Nullable
public V computeIfAbsent(@Nullable K key, Function<? super K, ? extends V> mappingFunction) {
// Avoid locking if entry is present
Reference<K, V> ref = getReference(key, Restructure.NEVER);
Entry<K, V> entry = (ref != null ? ref.get() : null);
if (entry != null) {
return entry.getValue();
}
return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) {
@Override
protected @Nullable V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V> entry, @Nullable Entries<V> entries) {
@@ -406,6 +413,13 @@ public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V> implemen
@Override
@Nullable
public V computeIfPresent(@Nullable K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
// Avoid locking if entry is absent
Reference<K, V> ref = getReference(key, Restructure.NEVER);
Entry<K, V> entry = (ref != null ? ref.get() : null);
if (entry == null) {
return null;
}
return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) {
@Override
protected @Nullable V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V> entry, @Nullable Entries<V> entries) {
@@ -16,8 +16,6 @@
package org.springframework.util;
import java.io.ByteArrayInputStream;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -30,9 +28,11 @@ import java.nio.file.Files;
import org.springframework.lang.Nullable;
/**
* Simple utility methods for file and stream copying. All copy methods use a block size
* of 4096 bytes, and close all affected streams when done. A variation of the copy
* methods from this class that leave streams open can be found in {@link StreamUtils}.
* Simple utility methods for file and stream copying.
*
* <p>All copy methods use a block size of {@value #BUFFER_SIZE} bytes and
* close all affected streams when done. A variation of the copy methods from
* this class that leave streams open can be found in {@link StreamUtils}.
*
* <p>Mainly for use within the framework, but also useful for application code.
*
@@ -76,7 +76,7 @@ public abstract class FileCopyUtils {
public static void copy(byte[] in, File out) throws IOException {
Assert.notNull(in, "No input byte array specified");
Assert.notNull(out, "No output File specified");
copy(new ByteArrayInputStream(in), Files.newOutputStream(out.toPath()));
Files.write(out.toPath(), in);
}
/**
@@ -87,7 +87,7 @@ public abstract class FileCopyUtils {
*/
public static byte[] copyToByteArray(File in) throws IOException {
Assert.notNull(in, "No input File specified");
return copyToByteArray(Files.newInputStream(in.toPath()));
return Files.readAllBytes(in.toPath());
}
@@ -125,12 +125,9 @@ public abstract class FileCopyUtils {
Assert.notNull(in, "No input byte array specified");
Assert.notNull(out, "No OutputStream specified");
try {
try (out) {
out.write(in);
}
finally {
close(out);
}
}
/**
@@ -167,21 +164,11 @@ public abstract class FileCopyUtils {
Assert.notNull(in, "No Reader specified");
Assert.notNull(out, "No Writer specified");
try {
int charCount = 0;
char[] buffer = new char[BUFFER_SIZE];
int charsRead;
while ((charsRead = in.read(buffer)) != -1) {
out.write(buffer, 0, charsRead);
charCount += charsRead;
}
try (in; out) {
int charCount = (int) in.transferTo(out);
out.flush();
return charCount;
}
finally {
close(in);
close(out);
}
}
/**
@@ -195,12 +182,9 @@ public abstract class FileCopyUtils {
Assert.notNull(in, "No input String specified");
Assert.notNull(out, "No Writer specified");
try {
try (out) {
out.write(in);
}
finally {
close(out);
}
}
/**
@@ -220,18 +204,4 @@ public abstract class FileCopyUtils {
return out.toString();
}
/**
* Attempt to close the supplied {@link Closeable}, silently swallowing any
* exceptions.
* @param closeable the {@code Closeable} to close
*/
private static void close(Closeable closeable) {
try {
closeable.close();
}
catch (IOException ex) {
// ignore
}
}
}
@@ -237,7 +237,8 @@ class MethodParameterTests {
assertThat(m3.getTypeIndexForCurrentLevel()).isEqualTo(3);
}
public int method(String p1, long p2) {
public int method(String str, long lng) {
return 42;
}
@@ -362,12 +362,12 @@ public abstract class NamedParameterUtils {
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
try {
SqlParameter param = findParameter(declaredParams, paramName, i);
Object paramValue = paramSource.getValue(paramName);
if (paramValue instanceof SqlParameterValue) {
paramArray[i] = paramValue;
}
else {
SqlParameter param = findParameter(declaredParams, paramName, i);
paramArray[i] = (param != null ? new SqlParameterValue(param, paramValue) :
SqlParameterSourceUtils.getTypedValue(paramSource, paramName));
}
@@ -78,7 +78,7 @@ final class DefaultJdbcClient implements JdbcClient {
}
public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate) {
Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null");
Assert.notNull(jdbcTemplate, "NamedParameterJdbcTemplate must not be null");
this.classicOps = jdbcTemplate.getJdbcOperations();
this.namedParamOps = jdbcTemplate;
}
@@ -29,7 +29,7 @@ import org.springframework.jdbc.core.SqlTypeValue;
import org.springframework.lang.Nullable;
/**
* Object to represent a binary parameter value for a SQL statement, for example,
* Object to represent a binary parameter value for an SQL statement, for example,
* a binary stream for a BLOB or a LONGVARBINARY or PostgreSQL BYTEA column.
*
* <p>Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate}
@@ -37,7 +37,7 @@ import org.springframework.lang.Nullable;
* passed in as a parameter value wrapping the target content value.
*
* <p>Can be combined with {@link org.springframework.jdbc.core.SqlParameterValue}
* for specifying a SQL type, for example,
* for specifying an SQL type, for example,
* {@code new SqlParameterValue(Types.BLOB, new SqlBinaryValue(myContent))}.
* With most database drivers, the type hint is not actually necessary.
*
@@ -70,7 +70,7 @@ public class SqlBinaryValue implements SqlTypeValue {
/**
* Create a new {@code SqlBinaryValue} for the given content.
* @param stream the content stream
* @param length the length of the content
* @param length the length of the content (or -1 if undetermined)
*/
public SqlBinaryValue(InputStream stream, long length) {
this.content = stream;
@@ -82,7 +82,7 @@ public class SqlBinaryValue implements SqlTypeValue {
* <p>Consider specifying a {@link Resource} with content length support
* when available: {@link SqlBinaryValue#SqlBinaryValue(Resource)}.
* @param resource the resource to obtain a content stream from
* @param length the length of the content
* @param length the length of the content (or -1 if undetermined)
*/
public SqlBinaryValue(InputStreamSource resource, long length) {
this.content = resource;
@@ -146,10 +146,20 @@ public class SqlBinaryValue implements SqlTypeValue {
throws SQLException {
if (sqlType == Types.BLOB) {
ps.setBlob(paramIndex, is, length);
if (length >= 0) {
ps.setBlob(paramIndex, is, length);
}
else {
ps.setBlob(paramIndex, is);
}
}
else {
ps.setBinaryStream(paramIndex, is, length);
if (length >= 0) {
ps.setBinaryStream(paramIndex, is, length);
}
else {
ps.setBinaryStream(paramIndex, is);
}
}
}
@@ -28,7 +28,7 @@ import org.springframework.jdbc.core.SqlTypeValue;
import org.springframework.lang.Nullable;
/**
* Object to represent a character-based parameter value for a SQL statement,
* Object to represent a character-based parameter value for an SQL statement,
* for example, a character stream for a CLOB/NCLOB or a LONGVARCHAR column.
*
* <p>Designed for use with {@link org.springframework.jdbc.core.JdbcTemplate}
@@ -36,7 +36,7 @@ import org.springframework.lang.Nullable;
* passed in as a parameter value wrapping the target content value.
*
* <p>Can be combined with {@link org.springframework.jdbc.core.SqlParameterValue}
* for specifying a SQL type, for example,
* for specifying an SQL type, for example,
* {@code new SqlParameterValue(Types.CLOB, new SqlCharacterValue(myContent))}.
* With most database drivers, the type hint is not actually necessary.
*
@@ -77,7 +77,7 @@ public class SqlCharacterValue implements SqlTypeValue {
/**
* Create a new {@code SqlCharacterValue} for the given content.
* @param reader the content reader
* @param length the length of the content
* @param length the length of the content (or -1 if undetermined)
*/
public SqlCharacterValue(Reader reader, long length) {
this.content = reader;
@@ -87,7 +87,7 @@ public class SqlCharacterValue implements SqlTypeValue {
/**
* Create a new {@code SqlCharacterValue} for the given content.
* @param asciiStream the content as ASCII stream
* @param length the length of the content
* @param length the length of the content (or -1 if undetermined)
*/
public SqlCharacterValue(InputStream asciiStream, long length) {
this.content = asciiStream;
@@ -108,8 +108,8 @@ public class SqlCharacterValue implements SqlTypeValue {
else if (this.content instanceof Reader reader) {
setReader(ps, paramIndex, sqlType, reader, this.length);
}
else if (this.content instanceof InputStream asciiStream) {
ps.setAsciiStream(paramIndex, asciiStream, this.length);
else if (this.content instanceof InputStream inputStream) {
setInputStream(ps, paramIndex, inputStream, this.length);
}
else {
throw new IllegalArgumentException("Illegal content type: " + this.content.getClass().getName());
@@ -134,13 +134,39 @@ public class SqlCharacterValue implements SqlTypeValue {
throws SQLException {
if (sqlType == Types.CLOB) {
ps.setClob(paramIndex, reader, length);
if (length >= 0) {
ps.setClob(paramIndex, reader, length);
}
else {
ps.setClob(paramIndex, reader);
}
}
else if (sqlType == Types.NCLOB) {
ps.setNClob(paramIndex, reader, length);
if (length >= 0) {
ps.setNClob(paramIndex, reader, length);
}
else {
ps.setNClob(paramIndex, reader);
}
}
else {
ps.setCharacterStream(paramIndex, reader, length);
if (length >= 0) {
ps.setCharacterStream(paramIndex, reader, length);
}
else {
ps.setCharacterStream(paramIndex, reader);
}
}
}
private void setInputStream(PreparedStatement ps, int paramIndex, InputStream is, long length)
throws SQLException {
if (length >= 0) {
ps.setAsciiStream(paramIndex, is, length);
}
else {
ps.setAsciiStream(paramIndex, is);
}
}
@@ -65,6 +65,15 @@ class SqlBinaryValueTests {
verify(ps).setBinaryStream(1, content, 3L);
}
@Test
void withInputStreamUndeterminedLength() throws SQLException {
InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2});
SqlBinaryValue value = new SqlBinaryValue(content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null);
verify(ps).setBinaryStream(1, content);
}
@Test
void withInputStreamForBlob() throws SQLException {
InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2});
@@ -74,6 +83,15 @@ class SqlBinaryValueTests {
verify(ps).setBlob(1, content, 3L);
}
@Test
void withInputStreamForBlobUndeterminedLength() throws SQLException {
InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2});
SqlBinaryValue value = new SqlBinaryValue(content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, Types.BLOB, null);
verify(ps).setBlob(1, content);
}
@Test
void withInputStreamSource() throws SQLException {
InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2});
@@ -83,6 +101,15 @@ class SqlBinaryValueTests {
verify(ps).setBinaryStream(1, content, 3L);
}
@Test
void withInputStreamSourceUndeterminedLength() throws SQLException {
InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2});
SqlBinaryValue value = new SqlBinaryValue(() -> content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null);
verify(ps).setBinaryStream(1, content);
}
@Test
void withInputStreamSourceForBlob() throws SQLException {
InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2});
@@ -92,6 +119,15 @@ class SqlBinaryValueTests {
verify(ps).setBlob(1, content, 3L);
}
@Test
void withInputStreamSourceForBlobUndeterminedLength() throws SQLException {
InputStream content = new ByteArrayInputStream(new byte[] {0, 1, 2});
SqlBinaryValue value = new SqlBinaryValue(() -> content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, Types.BLOB, null);
verify(ps).setBlob(1, content);
}
@Test
void withResource() throws SQLException {
byte[] content = new byte[] {0, 1, 2};
@@ -104,6 +104,15 @@ class SqlCharacterValueTests {
verify(ps).setCharacterStream(1, content, 3L);
}
@Test
void withReaderUndeterminedLength() throws SQLException {
Reader content = new StringReader("abc");
SqlCharacterValue value = new SqlCharacterValue(content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null);
verify(ps).setCharacterStream(1, content);
}
@Test
void withReaderForClob() throws SQLException {
Reader content = new StringReader("abc");
@@ -113,6 +122,15 @@ class SqlCharacterValueTests {
verify(ps).setClob(1, content, 3L);
}
@Test
void withReaderForClobUndeterminedLength() throws SQLException {
Reader content = new StringReader("abc");
SqlCharacterValue value = new SqlCharacterValue(content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, Types.CLOB, null);
verify(ps).setClob(1, content);
}
@Test
void withReaderForNClob() throws SQLException {
Reader content = new StringReader("abc");
@@ -122,6 +140,15 @@ class SqlCharacterValueTests {
verify(ps).setNClob(1, content, 3L);
}
@Test
void withReaderForNClobUndeterminedLength() throws SQLException {
Reader content = new StringReader("abc");
SqlCharacterValue value = new SqlCharacterValue(content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, Types.NCLOB, null);
verify(ps).setNClob(1, content);
}
@Test
void withAsciiStream() throws SQLException {
InputStream content = new ByteArrayInputStream("abc".getBytes(StandardCharsets.US_ASCII));
@@ -131,4 +158,13 @@ class SqlCharacterValueTests {
verify(ps).setAsciiStream(1, content, 3L);
}
@Test
void withAsciiStreamUndeterminedLength() throws SQLException {
InputStream content = new ByteArrayInputStream("abc".getBytes(StandardCharsets.US_ASCII));
SqlCharacterValue value = new SqlCharacterValue(content, -1);
PreparedStatement ps = mock();
value.setTypeValue(ps, 1, JdbcUtils.TYPE_UNKNOWN, null);
verify(ps).setAsciiStream(1, content);
}
}
@@ -26,15 +26,15 @@ import org.springframework.lang.Nullable;
/**
* Specifies a basic set of JMS operations.
*
* <p>Implemented by {@link JmsTemplate}. Not often used but a useful option
* to enhance testability, as it can easily be mocked or stubbed.
* <p>Implemented by {@link JmsTemplate}. Not often used directly, but a useful
* option to enhance testability, as it can easily be mocked or stubbed.
*
* <p>Provides {@code JmsTemplate's} {@code send(..)} and
* {@code receive(..)} methods that mirror various JMS API methods.
* See the JMS specification and javadocs for details on those methods.
*
* <p>Provides also basic request reply operation using a temporary
* queue to collect the reply.
* <p>Provides also basic request-reply operations using a temporary queue to
* receive the reply.
*
* @author Mark Pollack
* @author Juergen Hoeller
@@ -59,7 +59,7 @@ public interface JmsOperations {
/**
* Send messages to the default JMS destination (or one specified
* for each send operation). The callback gives access to the JMS Session
* for each send operation). The callback provides access to the JMS Session
* and MessageProducer in order to perform complex send operations.
* @param action callback object that exposes the session/producer pair
* @return the result object from working with the session
@@ -69,7 +69,7 @@ public interface JmsOperations {
<T> T execute(ProducerCallback<T> action) throws JmsException;
/**
* Send messages to a JMS destination. The callback gives access to the JMS Session
* Send messages to a JMS destination. The callback provides access to the JMS Session
* and MessageProducer in order to perform complex send operations.
* @param destination the destination to send messages to
* @param action callback object that exposes the session/producer pair
@@ -80,7 +80,7 @@ public interface JmsOperations {
<T> T execute(Destination destination, ProducerCallback<T> action) throws JmsException;
/**
* Send messages to a JMS destination. The callback gives access to the JMS Session
* Send messages to a JMS destination. The callback provides access to the JMS Session
* and MessageProducer in order to perform complex send operations.
* @param destinationName the name of the destination to send messages to
* (to be resolved to an actual destination by a DestinationResolver)
@@ -377,7 +377,7 @@ public interface JmsOperations {
/**
* Send a request message and receive the reply from a default destination. The
* {@link MessageCreator} callback creates the message given a Session. A temporary
* queue is created as part of this operation and is set in the {@code JMSReplyTO}
* queue is created as part of this operation and is set in the {@code JMSReplyTo}
* header of the message.
* <p>This will only work with a default destination specified!
* @param messageCreator callback to create a request message
@@ -390,10 +390,10 @@ public interface JmsOperations {
Message sendAndReceive(MessageCreator messageCreator) throws JmsException;
/**
* Send a message and receive the reply from the specified destination. The
* {@link MessageCreator} callback creates the message given a Session. A temporary
* queue is created as part of this operation and is set in the {@code JMSReplyTO}
* header of the message.
* Send a message and receive the reply from the specified destination.
* <p>The {@link MessageCreator} callback creates the message given a Session.
* A temporary queue is created as part of this operation and is set in the
* {@code JMSReplyTo} header of the message.
* @param destination the destination to send this message to
* @param messageCreator callback to create a message
* @return the reply, possibly {@code null} if the message could not be received,
@@ -405,10 +405,10 @@ public interface JmsOperations {
Message sendAndReceive(Destination destination, MessageCreator messageCreator) throws JmsException;
/**
* Send a message and receive the reply from the specified destination. The
* {@link MessageCreator} callback creates the message given a Session. A temporary
* queue is created as part of this operation and is set in the {@code JMSReplyTO}
* header of the message.
* Send a message and receive the reply from the specified destination.
* <p>The {@link MessageCreator} callback creates the message given a Session.
* A temporary queue is created as part of this operation and is set in the
* {@code JMSReplyTo} header of the message.
* @param destinationName the name of the destination to send this message to
* (to be resolved to an actual destination by a DestinationResolver)
* @param messageCreator callback to create a message
@@ -426,7 +426,7 @@ public interface JmsOperations {
//---------------------------------------------------------------------------------------
/**
* Browse messages in the default JMS queue. The callback gives access to the JMS
* Browse messages in the default JMS queue. The callback provides access to the JMS
* Session and QueueBrowser in order to browse the queue and react to the contents.
* @param action callback object that exposes the session/browser pair
* @return the result object from working with the session
@@ -436,7 +436,7 @@ public interface JmsOperations {
<T> T browse(BrowserCallback<T> action) throws JmsException;
/**
* Browse messages in a JMS queue. The callback gives access to the JMS Session
* Browse messages in a JMS queue. The callback provides access to the JMS Session
* and QueueBrowser in order to browse the queue and react to the contents.
* @param queue the queue to browse
* @param action callback object that exposes the session/browser pair
@@ -447,7 +447,7 @@ public interface JmsOperations {
<T> T browse(Queue queue, BrowserCallback<T> action) throws JmsException;
/**
* Browse messages in a JMS queue. The callback gives access to the JMS Session
* Browse messages in a JMS queue. The callback provides access to the JMS Session
* and QueueBrowser in order to browse the queue and react to the contents.
* @param queueName the name of the queue to browse
* (to be resolved to an actual destination by a DestinationResolver)
@@ -459,7 +459,7 @@ public interface JmsOperations {
<T> T browse(String queueName, BrowserCallback<T> action) throws JmsException;
/**
* Browse selected messages in a JMS queue. The callback gives access to the JMS
* Browse selected messages in a JMS queue. The callback provides access to the JMS
* Session and QueueBrowser in order to browse the queue and react to the contents.
* @param messageSelector the JMS message selector expression (or {@code null} if none).
* See the JMS specification for a detailed definition of selector expressions.
@@ -471,7 +471,7 @@ public interface JmsOperations {
<T> T browseSelected(@Nullable String messageSelector, BrowserCallback<T> action) throws JmsException;
/**
* Browse selected messages in a JMS queue. The callback gives access to the JMS
* Browse selected messages in a JMS queue. The callback provides access to the JMS
* Session and QueueBrowser in order to browse the queue and react to the contents.
* @param queue the queue to browse
* @param messageSelector the JMS message selector expression (or {@code null} if none).
@@ -484,7 +484,7 @@ public interface JmsOperations {
<T> T browseSelected(Queue queue, @Nullable String messageSelector, BrowserCallback<T> action) throws JmsException;
/**
* Browse selected messages in a JMS queue. The callback gives access to the JMS
* Browse selected messages in a JMS queue. The callback provides access to the JMS
* Session and QueueBrowser in order to browse the queue and react to the contents.
* @param queueName the name of the queue to browse
* (to be resolved to an actual destination by a DestinationResolver)
@@ -197,9 +197,7 @@ public abstract class AbstractJmsListeningContainer extends JmsDestinationAccess
/**
* Initialize this container.
* <p>Creates a JMS Connection, starts the {@link jakarta.jms.Connection}
* (if {@link #setAutoStartup(boolean) "autoStartup"} hasn't been turned off),
* and calls {@link #doInitialize()}.
* <p>Marks the container as active and calls {@link #doInitialize()}.
* @throws org.springframework.jms.JmsException if startup failed
*/
public void initialize() throws JmsException {
@@ -489,7 +487,7 @@ public abstract class AbstractJmsListeningContainer extends JmsDestinationAccess
/**
* Stop the shared Connection.
* @throws JMSException if thrown by JMS API methods
* @see jakarta.jms.Connection#start()
* @see jakarta.jms.Connection#stop()
*/
protected void stopSharedConnection() throws JMSException {
this.sharedConnectionLock.lock();
@@ -1026,14 +1026,15 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen
private static class MessageRejectedWhileStoppingException extends RuntimeException {
}
private abstract static class MicrometerInstrumentation {
static Session instrumentSession(Session session, ObservationRegistry registry) {
return JmsInstrumentation.instrumentSession(session, registry);
}
}
private abstract static class ObservationFactory {
private static final JmsProcessObservationConvention DEFAULT_CONVENTION = new DefaultJmsProcessObservationConvention();
@@ -280,8 +280,8 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
* between recovery attempts. If the {@link BackOffExecution} implementation
* returns {@link BackOffExecution#STOP}, this listener container will not further
* attempt to recover.
* <p>The {@link #setRecoveryInterval(long) recovery interval} is ignored
* when this property is set.
* <p>Note that setting the {@linkplain #setRecoveryInterval(long) recovery
* interval} overrides this property.
* @since 4.1
*/
public void setBackOff(BackOff backOff) {
@@ -290,10 +290,12 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
/**
* Specify the interval between recovery attempts, in <b>milliseconds</b>.
* The default is 5000 ms, that is, 5 seconds. This is a convenience method
* to create a {@link FixedBackOff} with the specified interval.
* <p>For more recovery options, consider specifying a {@link BackOff}
* instance instead.
* <p>The default is 5000 ms, that is, 5 seconds.
* <p>This is a convenience method to create a {@link FixedBackOff} with the
* specified interval. For more recovery options, consider specifying a
* {@link #setBackOff(BackOff) BackOff} instance instead. Note, however, that
* explicitly setting the {@link #setBackOff(BackOff) BackOff} overrides this
* property.
* @see #setBackOff(BackOff)
* @see #handleListenerSetupFailure
*/
@@ -1085,7 +1087,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
}
}
else {
// Recovery during active operation..
// Recovery during active operation...
if (alreadyRecovered) {
logger.debug("Setup of JMS message listener invoker failed - already recovered by other invoker", ex);
}
@@ -1519,7 +1521,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
/**
* Apply the back-off time once. In a regular scenario, the back-off is only applied if we
* failed to recover with the broker. This additional wait period avoids a burst retry
* scenario when the broker is actually up but something else if failing (i.e. listener
* scenario when the broker is actually up but something else is failing (i.e. listener
* specific).
*/
private void waitBeforeRecoveryAttempt() {
@@ -242,7 +242,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter
return !isStrictContentTypeMatch();
}
for (MimeType current : getSupportedMimeTypes()) {
if (current.getType().equals(mimeType.getType()) && current.getSubtype().equals(mimeType.getSubtype())) {
if (current.includes(mimeType)) {
return true;
}
}
@@ -54,7 +54,7 @@ class MappingJackson2MessageConverterTests {
}
@Test // SPR-12724
public void mimetypeParametrizedConstructor() {
void mimetypeParametrizedConstructor() {
MimeType mimetype = new MimeType("application", "xml", StandardCharsets.UTF_8);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(mimetype);
assertThat(converter.getSupportedMimeTypes()).contains(mimetype);
@@ -63,7 +63,7 @@ class MappingJackson2MessageConverterTests {
}
@Test // SPR-12724
public void mimetypesParametrizedConstructor() {
void mimetypesParametrizedConstructor() {
MimeType jsonMimetype = new MimeType("application", "json", StandardCharsets.UTF_8);
MimeType xmlMimetype = new MimeType("application", "xml", StandardCharsets.UTF_8);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(jsonMimetype, xmlMimetype);
@@ -72,6 +72,22 @@ class MappingJackson2MessageConverterTests {
.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
}
@Test
void supportJsonMimeType() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build();
assertThat(converter.supportsMimeType(message.getHeaders())).isTrue();
}
@Test
void supportVendorJsonMimeTypes() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader(MessageHeaders.CONTENT_TYPE, "application/vnd.springframework.type+json").build();
assertThat(converter.supportsMimeType(message.getHeaders())).isTrue();
}
@Test
void fromMessage() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
@@ -69,7 +69,7 @@ import org.springframework.util.CollectionUtils;
* a Spring application context.
*
* <p>Encapsulates the common functionality between the different JPA bootstrap
* contracts (standalone as well as container).
* contracts: standalone as well as container.
*
* <p>Implements support for standard JPA configuration conventions as well as
* Spring's customizable {@link JpaVendorAdapter} mechanism, and controls the
@@ -178,10 +178,10 @@ public abstract class AbstractEntityManagerFactoryBean implements
}
/**
* Specify the name of the EntityManagerFactory configuration.
* <p>Default is none, indicating the default EntityManagerFactory
* configuration. The persistence provider will throw an exception if
* ambiguous EntityManager configurations are found.
* Specify the name of the persistence unit configuration to use.
* <p>Default is none, indicating the default persistence unit configuration.
* The persistence provider will throw an exception if ambiguous persistence
* unit configurations are found.
* @see jakarta.persistence.Persistence#createEntityManagerFactory(String)
*/
public void setPersistenceUnitName(@Nullable String persistenceUnitName) {
@@ -16,6 +16,7 @@
package org.springframework.orm.jpa;
import java.sql.Connection;
import java.sql.SQLException;
import jakarta.persistence.EntityManager;
@@ -78,7 +79,7 @@ public interface JpaDialect extends PersistenceExceptionTranslator {
* @throws org.springframework.transaction.TransactionException in case of invalid arguments
* @see #cleanupTransaction
* @see jakarta.persistence.EntityTransaction#begin
* @see org.springframework.jdbc.datasource.DataSourceUtils#prepareConnectionForTransaction
* @see org.springframework.jdbc.datasource.DataSourceUtils#prepareConnectionForTransaction(Connection, TransactionDefinition)
*/
@Nullable
Object beginTransaction(EntityManager entityManager, TransactionDefinition definition)
@@ -75,14 +75,11 @@ import org.springframework.util.ClassUtils;
* @author Juergen Hoeller
* @author Rod Johnson
* @since 2.0
* @see #setPersistenceXmlLocation
* @see #setJpaProperties
* @see #setJpaVendorAdapter
* @see #setLoadTimeWeaver
* @see #setDataSource
* @see EntityManagerFactoryInfo
* @see LocalEntityManagerFactoryBean
* @see org.springframework.orm.jpa.support.SharedEntityManagerBean
* @see jakarta.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory
*/
@SuppressWarnings("serial")
@@ -133,9 +130,10 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManage
}
/**
* Uses the specified persistence unit name as the name of the default
* persistence unit, if applicable.
* <p><b>NOTE: Only applied if no external PersistenceUnitManager specified.</b>
* Specify the name of the persistence unit configuration to use.
* <p>Uses the specified persistence unit name as the name of the
* default persistence unit, if applicable. Otherwise, it selects
* among the available persistence units.
* @see DefaultPersistenceUnitManager#setDefaultPersistenceUnitName
*/
@Override
@@ -51,10 +51,8 @@ import org.springframework.lang.Nullable;
* @since 2.0
* @see #setJpaProperties
* @see #setJpaVendorAdapter
* @see JpaTransactionManager#setEntityManagerFactory
* @see #setDataSource
* @see LocalContainerEntityManagerFactoryBean
* @see org.springframework.jndi.JndiObjectFactoryBean
* @see org.springframework.orm.jpa.support.SharedEntityManagerBean
* @see jakarta.persistence.Persistence#createEntityManagerFactory
* @see jakarta.persistence.spi.PersistenceProvider#createEntityManagerFactory
*/
@@ -63,6 +61,8 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
private static final String DATASOURCE_PROPERTY = "jakarta.persistence.dataSource";
private static final String NON_JTA_DATASOURCE_PROPERTY = "jakarta.persistence.nonJtaDataSource";
/**
* Specify the JDBC DataSource that the JPA persistence provider is supposed
@@ -78,9 +78,11 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
public void setDataSource(@Nullable DataSource dataSource) {
if (dataSource != null) {
getJpaPropertyMap().put(DATASOURCE_PROPERTY, dataSource);
getJpaPropertyMap().put(NON_JTA_DATASOURCE_PROPERTY, dataSource);
}
else {
getJpaPropertyMap().remove(DATASOURCE_PROPERTY);
getJpaPropertyMap().remove(NON_JTA_DATASOURCE_PROPERTY);
}
}
@@ -111,8 +113,8 @@ public class LocalEntityManagerFactoryBean extends AbstractEntityManagerFactoryB
// Create EntityManagerFactory directly through PersistenceProvider.
EntityManagerFactory emf = provider.createEntityManagerFactory(getPersistenceUnitName(), getJpaPropertyMap());
if (emf == null) {
throw new IllegalStateException(
"PersistenceProvider [" + provider + "] did not return an EntityManagerFactory for name '" +
throw new PersistenceException(
"PersistenceProvider [" + provider + "] could not find persistence unit for name '" +
getPersistenceUnitName() + "'");
}
return emf;
@@ -168,7 +168,7 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
* This is useful to defer the early transaction begin that obtaining a
* JDBC Connection implies within an EclipseLink EntityManager.
*/
private static class EclipseLinkConnectionHandle implements ConnectionHandle {
private class EclipseLinkConnectionHandle implements ConnectionHandle {
private final EntityManager entityManager;
@@ -182,7 +182,13 @@ public class EclipseLinkJpaDialect extends DefaultJpaDialect {
@Override
public Connection getConnection() {
if (this.connection == null) {
this.connection = this.entityManager.unwrap(Connection.class);
transactionIsolationLock.lock();
try {
this.connection = this.entityManager.unwrap(Connection.class);
}
finally {
transactionIsolationLock.unlock();
}
}
return this.connection;
}
@@ -49,8 +49,7 @@ public abstract class AbstractContainerEntityManagerFactoryIntegrationTests
@Test
protected void testEntityManagerFactoryImplementsEntityManagerFactoryInfo() {
boolean condition = entityManagerFactory instanceof EntityManagerFactoryInfo;
assertThat(condition).as("Must have introduced config interface").isTrue();
assertThat(entityManagerFactory).isInstanceOf(EntityManagerFactoryInfo.class);
EntityManagerFactoryInfo emfi = (EntityManagerFactoryInfo) entityManagerFactory;
assertThat(emfi.getPersistenceUnitName()).isEqualTo("Person");
assertThat(emfi.getPersistenceUnitInfo()).as("PersistenceUnitInfo must be available").isNotNull();
@@ -172,8 +171,8 @@ public abstract class AbstractContainerEntityManagerFactoryIntegrationTests
@Test
void testEntityManagerProxyRejectsProgrammaticTxManagement() {
assertThatIllegalStateException().as("Should not be able to create transactions on container managed EntityManager").isThrownBy(
sharedEntityManager::getTransaction);
assertThatIllegalStateException().as("Should not be able to create transactions on container managed EntityManager")
.isThrownBy(sharedEntityManager::getTransaction);
}
@Test
@@ -234,9 +233,7 @@ public abstract class AbstractContainerEntityManagerFactoryIntegrationTests
q.setFlushMode(FlushModeType.AUTO);
List<Person> people = q.getResultList();
assertThat(people).isEmpty();
assertThatException()
.isThrownBy(q::getSingleResult)
.withMessageContaining("closed");
assertThatException().isThrownBy(q::getSingleResult).withMessageContaining("closed");
// We would typically expect an IllegalStateException, but Hibernate throws a
// PersistenceException. So we assert the contents of the exception message instead.
@@ -88,8 +88,7 @@ class EntityManagerFactoryUtilsTests {
IllegalStateException ise = new IllegalStateException();
DataAccessException dex = EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ise);
assertThat(dex.getCause()).isSameAs(ise);
boolean condition = dex instanceof InvalidDataAccessApiUsageException;
assertThat(condition).isTrue();
assertThat(dex).isInstanceOf(InvalidDataAccessApiUsageException.class);
}
@Test
@@ -97,8 +96,7 @@ class EntityManagerFactoryUtilsTests {
IllegalArgumentException iae = new IllegalArgumentException();
DataAccessException dex = EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(iae);
assertThat(dex.getCause()).isSameAs(iae);
boolean condition = dex instanceof InvalidDataAccessApiUsageException;
assertThat(condition).isTrue();
assertThat(dex).isInstanceOf(InvalidDataAccessApiUsageException.class);
}
/**
@@ -47,4 +47,5 @@ public class Car {
String getModel() {
return model;
}
}
@@ -86,4 +86,5 @@ public class Employee {
@PreRemove
public void preRemove() {
}
}
@@ -17,6 +17,7 @@
package org.springframework.orm.jpa.domain;
public class EmployeeCategory {
private String name;
public String getName() {
@@ -26,4 +27,5 @@ public class EmployeeCategory {
public void setName(String name) {
this.name = name;
}
}
@@ -17,7 +17,9 @@
package org.springframework.orm.jpa.domain;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter
public class EmployeeCategoryConverter implements AttributeConverter<EmployeeCategory, String> {
@Override
@@ -37,4 +39,5 @@ public class EmployeeCategoryConverter implements AttributeConverter<EmployeeCat
}
return null;
}
}
@@ -17,6 +17,7 @@
package org.springframework.orm.jpa.domain;
public class EmployeeKind {
private String name;
public String getName() {
@@ -26,4 +27,5 @@ public class EmployeeKind {
public void setName(String name) {
this.name = name;
}
}
@@ -17,7 +17,9 @@
package org.springframework.orm.jpa.domain;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
@Converter(autoApply = true)
public class EmployeeKindConverter implements AttributeConverter<EmployeeKind, String> {
@Override
@@ -37,4 +39,5 @@ public class EmployeeKindConverter implements AttributeConverter<EmployeeKind, S
}
return null;
}
}
@@ -27,4 +27,5 @@ public class EmployeeLocation {
public void setLocation(String location) {
this.location = location;
}
}
@@ -39,4 +39,5 @@ public class EmployeeLocationConverter implements AttributeConverter<EmployeeLoc
}
return null;
}
}
@@ -39,8 +39,7 @@ class EclipseLinkEntityManagerFactoryIntegrationTests extends AbstractContainerE
@Test
void testCanCastSharedEntityManagerProxyToEclipseLinkEntityManager() {
boolean condition = sharedEntityManager instanceof JpaEntityManager;
assertThat(condition).isTrue();
assertThat(sharedEntityManager).isInstanceOf(JpaEntityManager.class);
JpaEntityManager eclipselinkEntityManager = (JpaEntityManager) sharedEntityManager;
assertThat(eclipselinkEntityManager.getActiveSession()).isNotNull();
}
@@ -0,0 +1,30 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.orm.jpa.eclipselink;
/**
* @author Juergen Hoeller
*/
class EclipseLinkEntityManagerFactoryScanIntegrationTests extends EclipseLinkEntityManagerFactoryIntegrationTests {
@Override
protected String[] getConfigLocations() {
return new String[] {"/org/springframework/orm/jpa/eclipselink/eclipselink-manager-scan.xml",
"/org/springframework/orm/jpa/memdb.xml", "/org/springframework/orm/jpa/inject.xml"};
}
}
@@ -0,0 +1,30 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.orm.jpa.hibernate;
/**
* @author Juergen Hoeller
*/
class HibernateEntityManagerFactoryScanIntegrationTests extends HibernateEntityManagerFactoryIntegrationTests {
@Override
protected String[] getConfigLocations() {
return new String[] {"/org/springframework/orm/jpa/hibernate/hibernate-manager-scan.xml",
"/org/springframework/orm/jpa/memdb.xml", "/org/springframework/orm/jpa/inject.xml"};
}
}
@@ -48,8 +48,7 @@ class HibernateMultiEntityManagerFactoryIntegrationTests extends AbstractContain
@Override
@Test
protected void testEntityManagerFactoryImplementsEntityManagerFactoryInfo() {
boolean condition = this.entityManagerFactory instanceof EntityManagerFactoryInfo;
assertThat(condition).as("Must have introduced config interface").isTrue();
assertThat(this.entityManagerFactory).isInstanceOf(EntityManagerFactoryInfo.class);
EntityManagerFactoryInfo emfi = (EntityManagerFactoryInfo) this.entityManagerFactory;
assertThat(emfi.getPersistenceUnitName()).isEqualTo("Drivers");
assertThat(emfi.getPersistenceUnitInfo()).as("PersistenceUnitInfo must be available").isNotNull();
@@ -71,7 +71,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessorTests {
PersistenceManagedTypes persistenceManagedTypes = freshApplicationContext.getBean(
"persistenceManagedTypes", PersistenceManagedTypes.class);
assertThat(persistenceManagedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
DriversLicense.class.getName(), Person.class.getName(), Employee.class.getName(),
Person.class.getName(), DriversLicense.class.getName(), Employee.class.getName(),
EmployeeCategoryConverter.class.getName(), EmployeeKindConverter.class.getName(),
EmployeeLocationConverter.class.getName(), Car.class.getName());
assertThat(persistenceManagedTypes.getManagedPackages()).isEmpty();
assertThat(freshApplicationContext.getBean(
@@ -26,6 +26,8 @@ import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.orm.jpa.domain.Car;
import org.springframework.orm.jpa.domain.DriversLicense;
import org.springframework.orm.jpa.domain.Employee;
import org.springframework.orm.jpa.domain.EmployeeCategoryConverter;
import org.springframework.orm.jpa.domain.EmployeeKindConverter;
import org.springframework.orm.jpa.domain.EmployeeLocationConverter;
import org.springframework.orm.jpa.domain.Person;
import org.springframework.orm.jpa.domain2.entity.User;
@@ -44,15 +46,17 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
*/
class PersistenceManagedTypesScannerTests {
public static final DefaultResourceLoader RESOURCE_LOADER = new DefaultResourceLoader();
private final DefaultResourceLoader resourceLoader = new DefaultResourceLoader();
private final PersistenceManagedTypesScanner scanner = new PersistenceManagedTypesScanner(resourceLoader);
private final PersistenceManagedTypesScanner scanner = new PersistenceManagedTypesScanner(RESOURCE_LOADER);
@Test
void scanPackageWithOnlyEntities() {
PersistenceManagedTypes managedTypes = this.scanner.scan("org.springframework.orm.jpa.domain");
assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
Person.class.getName(), DriversLicense.class.getName(), Employee.class.getName(),
EmployeeCategoryConverter.class.getName(), EmployeeKindConverter.class.getName(),
EmployeeLocationConverter.class.getName(), Car.class.getName());
assertThat(managedTypes.getManagedPackages()).isEmpty();
}
@@ -61,11 +65,13 @@ class PersistenceManagedTypesScannerTests {
void scanPackageInvokesManagedClassNamesFilter() {
ManagedClassNameFilter filter = mock(ManagedClassNameFilter.class);
given(filter.matches(anyString())).willReturn(true);
new PersistenceManagedTypesScanner(RESOURCE_LOADER, filter)
new PersistenceManagedTypesScanner(resourceLoader, filter)
.scan("org.springframework.orm.jpa.domain");
verify(filter).matches(Person.class.getName());
verify(filter).matches(DriversLicense.class.getName());
verify(filter).matches(Employee.class.getName());
verify(filter).matches(EmployeeCategoryConverter.class.getName());
verify(filter).matches(EmployeeKindConverter.class.getName());
verify(filter).matches(EmployeeLocationConverter.class.getName());
verify(filter).matches(Car.class.getName());
verifyNoMoreInteractions(filter);
@@ -75,7 +81,7 @@ class PersistenceManagedTypesScannerTests {
void scanPackageWithUseManagedClassNamesFilter() {
List<String> candidates = List.of(Person.class.getName(), DriversLicense.class.getName());
PersistenceManagedTypes managedTypes = new PersistenceManagedTypesScanner(
RESOURCE_LOADER, candidates::contains).scan("org.springframework.orm.jpa.domain");
resourceLoader, candidates::contains).scan("org.springframework.orm.jpa.domain");
assertThat(managedTypes.getManagedClassNames()).containsExactlyInAnyOrder(
Person.class.getName(), DriversLicense.class.getName());
assertThat(managedTypes.getManagedPackages()).isEmpty();
@@ -58,8 +58,7 @@ class SharedEntityManagerFactoryTests {
assertThat(proxyFactoryBean.getObject()).isSameAs(proxy);
assertThat(proxy.contains(o)).isFalse();
boolean condition = proxy instanceof EntityManagerProxy;
assertThat(condition).isTrue();
assertThat(proxy).isInstanceOf(EntityManagerProxy.class);
EntityManagerProxy emProxy = (EntityManagerProxy) proxy;
assertThatIllegalStateException().as("outside of transaction").isThrownBy(
emProxy::getTargetEntityManager);
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceUnitName" value="Person"/>
<property name="packagesToScan" value="org.springframework.orm.jpa.domain*"/>
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
<property name="database" value="HSQL"/>
<property name="showSql" value="true"/>
<property name="generateDdl" value="true"/>
</bean>
</property>
<property name="jpaPropertyMap">
<props>
<prop key="eclipselink.weaving">false</prop>
</props>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
</beans>
@@ -5,10 +5,6 @@
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="persistenceXmlLocation" value="org/springframework/orm/jpa/domain/persistence.xml"/>
<!--
<property name="persistenceUnitName" value="Person"/>
<property name="packagesToScan" value="org.springframework.orm.jpa.domain"/>
-->
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" primary="true">
<property name="persistenceUnitName" value="Person"/>
<property name="packagesToScan" value="org.springframework.orm.jpa.domain*"/>
<property name="dataSource" ref="dataSource"/>
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="database" value="HSQL"/>
<property name="showSql" value="true"/>
<property name="generateDdl" value="true"/>
</bean>
</property>
<property name="jpaPropertyMap">
<props>
<prop key="hibernate.current_session_context_class">org.springframework.orm.jpa.hibernate.SpringSessionContext</prop>
<prop key="hibernate.cache.provider_class">org.hibernate.cache.HashtableCacheProvider</prop>
</props>
</property>
<property name="bootstrapExecutor">
<bean class="org.springframework.core.task.SimpleAsyncTaskExecutor"/>
</property>
</bean>
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>
<bean id="hibernateStatistics" factory-bean="entityManagerFactory" factory-method="getStatistics" lazy-init="true"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>
</beans>
@@ -91,7 +91,7 @@ public @interface TestConstructor {
* {@link org.springframework.core.SpringProperties SpringProperties}
* mechanism.
* <p>This property may also be configured as a
* <a href="https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params">JUnit
* <a href="https://docs.junit.org/current/running-tests/configuration-parameters.html">JUnit
* Platform configuration parameter</a>.
* @see #autowireMode
*/
@@ -30,6 +30,7 @@ import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -193,11 +194,21 @@ class WiretapConnector implements ClientHttpConnector {
// 1. Mock server never consumed request body (for example, error before read)
// 2. FluxExchangeResult: getResponseBodyContent called before getResponseBody
//noinspection ConstantConditions
(this.publisher != null ? this.publisher : this.publisherNested)
.onErrorMap(ex -> new IllegalStateException(
"Content has not been consumed, and " +
"an error was raised while attempting to produce it.", ex))
.subscribe();
if (this.publisher != null) {
this.publisher.doOnNext(DataBufferUtils::release)
.onErrorMap(ex -> new IllegalStateException(
"Content has not been consumed, and " +
"an error was raised while attempting to produce it.", ex))
.subscribe();
}
else if (this.publisherNested != null) {
this.publisherNested
.map(pub -> Flux.from(pub).doOnNext(DataBufferUtils::release))
.onErrorMap(ex -> new IllegalStateException(
"Content has not been consumed, and " +
"an error was raised while attempting to produce it.", ex))
.subscribe();
}
}
return this.content.asMono();
});
@@ -17,11 +17,18 @@
package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import io.netty.buffer.PooledByteBufAllocator;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.NettyDataBufferFactory;
import org.springframework.core.testfixture.io.buffer.LeakAwareDataBufferFactory;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.reactive.ClientHttpConnector;
@@ -30,6 +37,7 @@ import org.springframework.http.client.reactive.ClientHttpResponse;
import org.springframework.mock.http.client.reactive.MockClientHttpRequest;
import org.springframework.mock.http.client.reactive.MockClientHttpResponse;
import org.springframework.web.reactive.function.client.ClientRequest;
import org.springframework.web.reactive.function.client.ClientResponse;
import org.springframework.web.reactive.function.client.ExchangeFunction;
import org.springframework.web.reactive.function.client.ExchangeFunctions;
@@ -44,11 +52,19 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
public class WiretapConnectorTests {
private final LeakAwareDataBufferFactory bufferFactory =
new LeakAwareDataBufferFactory(new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT));
@AfterEach
void tearDown() {
this.bufferFactory.checkForLeaks();
}
@Test
public void captureAndClaim() {
ClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test");
ClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
ClientHttpConnector connector = (method, uri, fn) -> fn.apply(request).then(Mono.just(response));
ClientHttpConnector connector = createConnector(request, response);
ClientRequest clientRequest = ClientRequest.create(HttpMethod.GET, URI.create("/test"))
.header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1").build();
@@ -62,4 +78,47 @@ public class WiretapConnectorTests {
assertThat(result.getUrl().toString()).isEqualTo("/test");
}
@Test
void shouldReleaseBuffers() {
MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test");
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
response.setBody(Flux.just(toDataBuffer("Hello Spring")));
ClientHttpConnector connector = createConnector(request, response);
ClientRequest clientRequest = ClientRequest.create(HttpMethod.GET, URI.create("/test"))
.header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1").build();
WiretapConnector wiretapConnector = new WiretapConnector(connector);
ExchangeFunction function = ExchangeFunctions.create(wiretapConnector);
function.exchange(clientRequest).block(ofMillis(0));
ExchangeResult result = wiretapConnector.getExchangeResult("1", null, Duration.ZERO);
result.getResponseBodyContent();
}
@Test
void shouldReleaseBuffersOnlyOnce() {
MockClientHttpRequest request = new MockClientHttpRequest(HttpMethod.GET, "/test");
MockClientHttpResponse response = new MockClientHttpResponse(HttpStatus.OK);
response.setBody(Flux.just(toDataBuffer("Hello Spring"), toDataBuffer("Hello Spring"), toDataBuffer("Hello Spring"), toDataBuffer("Hello Spring")));
ClientHttpConnector connector = createConnector(request, response);
ClientRequest clientRequest = ClientRequest.create(HttpMethod.GET, URI.create("/test"))
.header(WebTestClient.WEBTESTCLIENT_REQUEST_ID, "1").build();
WiretapConnector wiretapConnector = new WiretapConnector(connector);
ExchangeFunction function = ExchangeFunctions.create(wiretapConnector);
function.exchange(clientRequest).flatMap(ClientResponse::releaseBody).block(ofMillis(0));
ExchangeResult result = wiretapConnector.getExchangeResult("1", null, Duration.ZERO);
result.getResponseBodyContent();
}
private ClientHttpConnector createConnector(ClientHttpRequest request, ClientHttpResponse response) {
return (method, uri, fn) -> fn.apply(request).then(Mono.just(response));
}
private DataBuffer toDataBuffer(String s) {
DataBuffer buffer = this.bufferFactory.allocateBuffer(256);
buffer.write(s.getBytes(StandardCharsets.UTF_8));
return buffer;
}
}
@@ -0,0 +1,362 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.transaction.jta;
import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import jakarta.transaction.InvalidTransactionException;
import jakarta.transaction.NotSupportedException;
import jakarta.transaction.SystemException;
import jakarta.transaction.Transaction;
import jakarta.transaction.TransactionManager;
import jakarta.transaction.UserTransaction;
import org.springframework.lang.Nullable;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionSystemException;
import org.springframework.util.Assert;
/**
* Special {@link JtaTransactionManager} variant for Oracle WebLogic 15.1.1 and higher.
* Supports the full power of Spring's transaction definitions on WebLogic's
* transaction coordinator, <i>beyond standard JTA</i>: transaction names,
* per-transaction isolation levels, and proper resuming of transactions in all cases.
*
* <p>Uses WebLogic's special {@code begin(name)} method to start a JTA transaction,
* in order to make <b>Spring-driven transactions visible in WebLogic's transaction
* monitor</b>. In case of Spring's declarative transactions, the exposed name will
* (by default) be the fully-qualified class name + "." + method name.
*
* <p>Supports a <b>per-transaction isolation level</b> through WebLogic's corresponding
* JTA transaction property "ISOLATION LEVEL". This will apply the specified isolation
* level (e.g. ISOLATION_SERIALIZABLE) to all JDBC Connections that participate in the
* given transaction.
*
* <p>Invokes WebLogic's special {@code forceResume} method if standard JTA resume
* failed, to <b>also resume if the target transaction was marked rollback-only</b>.
* If you're not relying on this feature of transaction suspension in the first
* place, Spring's standard JtaTransactionManager will behave properly too.
*
* <p>By default, the JTA UserTransaction and TransactionManager handles are
* fetched directly from WebLogic's {@code TransactionHelper}. This can be
* overridden by specifying "userTransaction"/"userTransactionName" and
* "transactionManager"/"transactionManagerName", passing in existing handles
* or specifying corresponding JNDI locations to look up.
*
* <p>Note: This class was initially removed as of Spring Framework 6.0 but then
* brought back after the WebLogic 15.1.1 release which finally delivers Jakarta EE 9
* compatibility. As of Spring Framework 6.2.16, it is available again for manual
* configuration - as a replacement for the standard {@link JtaTransactionManager}.
*
* @author Juergen Hoeller
* @since 6.2.16
* @see org.springframework.transaction.TransactionDefinition#getName()
* @see org.springframework.transaction.TransactionDefinition#getIsolationLevel()
*/
@SuppressWarnings("serial")
public class WebLogicJtaTransactionManager extends JtaTransactionManager {
private static final String USER_TRANSACTION_CLASS_NAME = "weblogic.transaction.UserTransaction";
private static final String CLIENT_TRANSACTION_MANAGER_CLASS_NAME = "weblogic.transaction.ClientTransactionManager";
private static final String TRANSACTION_CLASS_NAME = "weblogic.transaction.Transaction";
private static final String TRANSACTION_HELPER_CLASS_NAME = "weblogic.transaction.TransactionHelper";
private static final String ISOLATION_LEVEL_KEY = "ISOLATION LEVEL";
private boolean weblogicUserTransactionAvailable;
@Nullable
private Method beginWithNameMethod;
@Nullable
private Method beginWithNameAndTimeoutMethod;
private boolean weblogicTransactionManagerAvailable;
@Nullable
private Method forceResumeMethod;
@Nullable
private Method setPropertyMethod;
@Nullable
private Object transactionHelper;
@Override
public void afterPropertiesSet() throws TransactionSystemException {
super.afterPropertiesSet();
loadWebLogicTransactionClasses();
}
@Override
@Nullable
protected UserTransaction retrieveUserTransaction() throws TransactionSystemException {
Object helper = loadWebLogicTransactionHelper();
try {
logger.trace("Retrieving JTA UserTransaction from WebLogic TransactionHelper");
Method getUserTransactionMethod = helper.getClass().getMethod("getUserTransaction");
return (UserTransaction) getUserTransactionMethod.invoke(this.transactionHelper);
}
catch (InvocationTargetException ex) {
throw new TransactionSystemException(
"WebLogic's TransactionHelper.getUserTransaction() method failed", ex.getTargetException());
}
catch (Exception ex) {
throw new TransactionSystemException(
"Could not invoke WebLogic's TransactionHelper.getUserTransaction() method", ex);
}
}
@Override
@Nullable
protected TransactionManager retrieveTransactionManager() throws TransactionSystemException {
Object helper = loadWebLogicTransactionHelper();
try {
logger.trace("Retrieving JTA TransactionManager from WebLogic TransactionHelper");
Method getTransactionManagerMethod = helper.getClass().getMethod("getTransactionManager");
return (TransactionManager) getTransactionManagerMethod.invoke(this.transactionHelper);
}
catch (InvocationTargetException ex) {
throw new TransactionSystemException(
"WebLogic's TransactionHelper.getTransactionManager() method failed", ex.getTargetException());
}
catch (Exception ex) {
throw new TransactionSystemException(
"Could not invoke WebLogic's TransactionHelper.getTransactionManager() method", ex);
}
}
private Object loadWebLogicTransactionHelper() throws TransactionSystemException {
Object helper = this.transactionHelper;
if (helper == null) {
try {
Class<?> transactionHelperClass = getClass().getClassLoader().loadClass(TRANSACTION_HELPER_CLASS_NAME);
Method getTransactionHelperMethod = transactionHelperClass.getMethod("getTransactionHelper");
helper = getTransactionHelperMethod.invoke(null);
this.transactionHelper = helper;
logger.trace("WebLogic TransactionHelper found");
}
catch (InvocationTargetException ex) {
throw new TransactionSystemException(
"WebLogic's TransactionHelper.getTransactionHelper() method failed", ex.getTargetException());
}
catch (Exception ex) {
throw new TransactionSystemException(
"Could not initialize WebLogicJtaTransactionManager because WebLogic API classes are not available",
ex);
}
}
return helper;
}
private void loadWebLogicTransactionClasses() throws TransactionSystemException {
try {
Class<?> userTransactionClass = getClass().getClassLoader().loadClass(USER_TRANSACTION_CLASS_NAME);
this.weblogicUserTransactionAvailable = userTransactionClass.isInstance(getUserTransaction());
if (this.weblogicUserTransactionAvailable) {
this.beginWithNameMethod = userTransactionClass.getMethod("begin", String.class);
this.beginWithNameAndTimeoutMethod = userTransactionClass.getMethod("begin", String.class, int.class);
logger.debug("Support for WebLogic transaction names available");
}
else {
logger.debug("Support for WebLogic transaction names not available");
}
// Obtain WebLogic ClientTransactionManager interface.
Class<?> transactionManagerClass =
getClass().getClassLoader().loadClass(CLIENT_TRANSACTION_MANAGER_CLASS_NAME);
logger.trace("WebLogic ClientTransactionManager found");
this.weblogicTransactionManagerAvailable = transactionManagerClass.isInstance(getTransactionManager());
if (this.weblogicTransactionManagerAvailable) {
Class<?> transactionClass = getClass().getClassLoader().loadClass(TRANSACTION_CLASS_NAME);
this.forceResumeMethod = transactionManagerClass.getMethod("forceResume", Transaction.class);
this.setPropertyMethod = transactionClass.getMethod("setProperty", String.class, Serializable.class);
logger.debug("Support for WebLogic forceResume available");
}
else {
logger.debug("Support for WebLogic forceResume not available");
}
}
catch (Exception ex) {
throw new TransactionSystemException(
"Could not initialize WebLogicJtaTransactionManager because WebLogic API classes are not available",
ex);
}
}
private TransactionManager obtainTransactionManager() {
TransactionManager tm = getTransactionManager();
Assert.state(tm != null, "No TransactionManager set");
return tm;
}
@Override
protected void doJtaBegin(JtaTransactionObject txObject, TransactionDefinition definition)
throws NotSupportedException, SystemException {
int timeout = determineTimeout(definition);
// Apply transaction name (if any) to WebLogic transaction.
if (this.weblogicUserTransactionAvailable && definition.getName() != null) {
try {
if (timeout > TransactionDefinition.TIMEOUT_DEFAULT) {
/*
weblogic.transaction.UserTransaction wut = (weblogic.transaction.UserTransaction) ut;
wut.begin(definition.getName(), timeout);
*/
Assert.state(this.beginWithNameAndTimeoutMethod != null, "WebLogic JTA API not initialized");
this.beginWithNameAndTimeoutMethod.invoke(txObject.getUserTransaction(), definition.getName(), timeout);
}
else {
/*
weblogic.transaction.UserTransaction wut = (weblogic.transaction.UserTransaction) ut;
wut.begin(definition.getName());
*/
Assert.state(this.beginWithNameMethod != null, "WebLogic JTA API not initialized");
this.beginWithNameMethod.invoke(txObject.getUserTransaction(), definition.getName());
}
}
catch (InvocationTargetException ex) {
throw new TransactionSystemException(
"WebLogic's UserTransaction.begin() method failed", ex.getTargetException());
}
catch (Exception ex) {
throw new TransactionSystemException(
"Could not invoke WebLogic's UserTransaction.begin() method", ex);
}
}
else {
// No WebLogic UserTransaction available or no transaction name specified
// -> standard JTA begin call.
applyTimeout(txObject, timeout);
txObject.getUserTransaction().begin();
}
// Specify isolation level, if any, through corresponding WebLogic transaction property.
if (this.weblogicTransactionManagerAvailable) {
if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) {
try {
Transaction tx = obtainTransactionManager().getTransaction();
Integer isolationLevel = definition.getIsolationLevel();
/*
weblogic.transaction.Transaction wtx = (weblogic.transaction.Transaction) tx;
wtx.setProperty(ISOLATION_LEVEL_KEY, isolationLevel);
*/
Assert.state(this.setPropertyMethod != null, "WebLogic JTA API not initialized");
this.setPropertyMethod.invoke(tx, ISOLATION_LEVEL_KEY, isolationLevel);
}
catch (InvocationTargetException ex) {
throw new TransactionSystemException(
"WebLogic's Transaction.setProperty(String, Serializable) method failed", ex.getTargetException());
}
catch (Exception ex) {
throw new TransactionSystemException(
"Could not invoke WebLogic's Transaction.setProperty(String, Serializable) method", ex);
}
}
}
else {
applyIsolationLevel(txObject, definition.getIsolationLevel());
}
}
@Override
protected void doJtaResume(@Nullable JtaTransactionObject txObject, Object suspendedTransaction)
throws InvalidTransactionException, SystemException {
try {
obtainTransactionManager().resume((Transaction) suspendedTransaction);
}
catch (InvalidTransactionException ex) {
if (!this.weblogicTransactionManagerAvailable) {
throw ex;
}
if (logger.isDebugEnabled()) {
logger.debug("Standard JTA resume threw InvalidTransactionException: " + ex.getMessage() +
" - trying WebLogic JTA forceResume");
}
/*
weblogic.transaction.TransactionManager wtm =
(weblogic.transaction.TransactionManager) getTransactionManager();
wtm.forceResume(suspendedTransaction);
*/
try {
Assert.state(this.forceResumeMethod != null, "WebLogic JTA API not initialized");
this.forceResumeMethod.invoke(getTransactionManager(), suspendedTransaction);
}
catch (InvocationTargetException ex2) {
throw new TransactionSystemException(
"WebLogic's TransactionManager.forceResume(Transaction) method failed", ex2.getTargetException());
}
catch (Exception ex2) {
throw new TransactionSystemException(
"Could not access WebLogic's TransactionManager.forceResume(Transaction) method", ex2);
}
}
}
@Override
public Transaction createTransaction(@Nullable String name, int timeout) throws NotSupportedException, SystemException {
if (this.weblogicUserTransactionAvailable && name != null) {
try {
if (timeout >= 0) {
Assert.state(this.beginWithNameAndTimeoutMethod != null, "WebLogic JTA API not initialized");
this.beginWithNameAndTimeoutMethod.invoke(getUserTransaction(), name, timeout);
}
else {
Assert.state(this.beginWithNameMethod != null, "WebLogic JTA API not initialized");
this.beginWithNameMethod.invoke(getUserTransaction(), name);
}
}
catch (InvocationTargetException ex) {
if (ex.getTargetException() instanceof NotSupportedException) {
throw (NotSupportedException) ex.getTargetException();
}
else if (ex.getTargetException() instanceof SystemException) {
throw (SystemException) ex.getTargetException();
}
else if (ex.getTargetException() instanceof RuntimeException) {
throw (RuntimeException) ex.getTargetException();
}
else {
throw new SystemException(
"WebLogic's begin() method failed with an unexpected error: " + ex.getTargetException());
}
}
catch (Exception ex) {
throw new SystemException("Could not invoke WebLogic's UserTransaction.begin() method: " + ex);
}
return new ManagedTransactionAdapter(obtainTransactionManager());
}
else {
// No name specified - standard JTA is sufficient.
return super.createTransaction(name, timeout);
}
}
}
+2
View File
@@ -76,6 +76,8 @@ dependencies {
testImplementation(testFixtures(project(":spring-beans")))
testImplementation(testFixtures(project(":spring-context")))
testImplementation(testFixtures(project(":spring-core")))
testImplementation(testFixtures(project(":spring-messaging")))
testImplementation(testFixtures(project(":spring-tx")))
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8")
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin")

Some files were not shown because too many files have changed in this diff Show More