mirror of
https://github.com/spring-projects/spring-framework
synced 2026-06-08 17:33:33 +00:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b4f10d4f1d | |||
| 0aa721cad0 | |||
| 63af572ce8 | |||
| 72c2343f63 | |||
| 8aeced9f80 | |||
| 70c326ed30 | |||
| 7b4e19c69b | |||
| 3b95d2c449 | |||
| 640e570583 | |||
| 66f33a8265 | |||
| 68997d8416 | |||
| 52006b71bc | |||
| 41d9f21ab9 | |||
| 7206b28272 | |||
| 0c688742e1 | |||
| 7de1dc826a | |||
| 3e3ca74020 | |||
| 66da5d7ab9 | |||
| 68d6cb9d35 | |||
| 6649a6e0f7 | |||
| 54948a4e88 | |||
| a00ba8dbcf | |||
| 4f815b0055 | |||
| 5494d78018 | |||
| cd60a0013b | |||
| 2137750591 | |||
| 13df9058a4 | |||
| aee52b53a1 | |||
| 92bbaa21e0 | |||
| 837579c2e5 | |||
| 03a75cf03a | |||
| aa7b459803 | |||
| 03fe1f0df3 | |||
| 0d72477742 | |||
| d26ee8f852 | |||
| 47c66f4352 | |||
| b5dd0a60f8 | |||
| e618f922c2 | |||
| 81a9f3d50b | |||
| c807fa597f | |||
| 576f109987 | |||
| 384dc2a9b8 | |||
| edf7f3cd43 | |||
| 58c64cba2c | |||
| 307411631d | |||
| 8d69370b95 | |||
| d990449b0d | |||
| 15dcc449a2 | |||
| 320831b18a | |||
| 172c8b2c35 | |||
| 51956fad89 | |||
| ddec8d2653 | |||
| 1aede291bb | |||
| ea3bd7ae0c | |||
| 9b0253e117 | |||
| f3753e6d64 | |||
| 41421d106b | |||
| 8c0ac8e062 | |||
| 0088b9c7f8 | |||
| 2b840ee7ef | |||
| 051f1dac24 | |||
| 5e7b3a3bed | |||
| 1910d32405 | |||
| 986ffc2072 | |||
| b9cf03f8f0 | |||
| 08a789cee9 | |||
| 7a6e401d17 | |||
| 3569cfe990 | |||
| 8b66d3c79a | |||
| a3c132c442 | |||
| 91791c1756 | |||
| dd92eac3ad | |||
| 874f056984 | |||
| b77db64459 | |||
| d597d2e159 | |||
| f5c3f3522e | |||
| d421f61a4a | |||
| 883254e1d0 | |||
| e919d0adcc | |||
| 4aafae1c33 | |||
| afef439c1f | |||
| 807e1e6126 | |||
| 5ac56bda87 | |||
| 7196f3f554 | |||
| a12d40e10b | |||
| 7f7819329c | |||
| 2494ecb47b | |||
| 241b8b48f2 | |||
| 173084f81a | |||
| 6544698078 | |||
| e0e96c487f | |||
| bb32df0a06 | |||
| 66c3aeee84 | |||
| 828980bd44 | |||
| fe8573c0ab | |||
| 130627f630 |
@@ -2,7 +2,7 @@ name: Build and Deploy Snapshot
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- 6.2.x
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
name: CI
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 9 * * *'
|
||||
push:
|
||||
branches:
|
||||
- 6.2.x
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
name: Release Milestone
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v6.2.0-M[1-9]
|
||||
- v6.2.0-RC[1-9]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
jobs:
|
||||
build-and-stage-release:
|
||||
if: ${{ github.repository == 'spring-projects/spring-framework' }}
|
||||
name: Build and Stage Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v4
|
||||
- name: Build and Publish
|
||||
id: build-and-publish
|
||||
uses: ./.github/actions/build
|
||||
with:
|
||||
develocity-access-key: ${{ secrets.GRADLE_ENTERPRISE_SECRET_ACCESS_KEY }}
|
||||
publish: true
|
||||
- name: Stage Release
|
||||
uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1
|
||||
with:
|
||||
artifact-properties: |
|
||||
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
|
||||
/**/framework-api-*-docs.zip::zip.type=docs
|
||||
/**/framework-api-*-schema.zip::zip.type=schema
|
||||
build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}}
|
||||
folder: 'deployment-repository'
|
||||
password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository: 'libs-staging-local'
|
||||
signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
|
||||
signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
|
||||
uri: 'https://repo.spring.io'
|
||||
username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
outputs:
|
||||
version: ${{ steps.build-and-publish.outputs.version }}
|
||||
verify:
|
||||
name: Verify
|
||||
needs: build-and-stage-release
|
||||
uses: ./.github/workflows/verify.yml
|
||||
with:
|
||||
staging: true
|
||||
version: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
secrets:
|
||||
google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }}
|
||||
repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }}
|
||||
repository-username: ${{ secrets.ARTIFACTORY_USERNAME }}
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
promote-release:
|
||||
name: Promote Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- verify
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up JFrog CLI
|
||||
uses: jfrog/setup-jfrog-cli@9fe0f98bd45b19e6e931d457f4e98f8f84461fb5 # v4.4.1
|
||||
env:
|
||||
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
|
||||
- name: Promote build
|
||||
run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local
|
||||
create-github-release:
|
||||
name: Create GitHub Release
|
||||
needs:
|
||||
- build-and-stage-release
|
||||
- promote-release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
|
||||
- name: Create GitHub Release
|
||||
uses: ./.github/actions/create-github-release
|
||||
with:
|
||||
milestone: ${{ needs.build-and-stage-release.outputs.version }}
|
||||
pre-release: true
|
||||
token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }}
|
||||
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
|
||||
project.getPlugins().apply(CheckstylePlugin.class);
|
||||
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
|
||||
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
|
||||
checkstyle.setToolVersion("10.20.1");
|
||||
checkstyle.setToolVersion("10.20.2");
|
||||
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
|
||||
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
|
||||
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
|
||||
|
||||
@@ -36,4 +36,4 @@ runtime:
|
||||
failure_level: warn
|
||||
ui:
|
||||
bundle:
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.17/ui-bundle.zip
|
||||
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
**** xref:core/expressions/language-ref/constructors.adoc[]
|
||||
**** xref:core/expressions/language-ref/variables.adoc[]
|
||||
**** xref:core/expressions/language-ref/functions.adoc[]
|
||||
**** xref:core/expressions/language-ref/varargs.adoc[]
|
||||
**** xref:core/expressions/language-ref/bean-references.adoc[]
|
||||
**** xref:core/expressions/language-ref/operator-ternary.adoc[]
|
||||
**** xref:core/expressions/language-ref/operator-elvis.adoc[]
|
||||
|
||||
@@ -33,11 +33,11 @@ arbitrary advice types. This section describes the basic concepts and standard a
|
||||
[[aop-api-advice-around]]
|
||||
=== Interception Around Advice
|
||||
|
||||
The most fundamental advice type in Spring is interception around advice.
|
||||
The most fundamental advice type in Spring is _interception around advice_.
|
||||
|
||||
Spring is compliant with the AOP `Alliance` interface for around advice that uses method
|
||||
interception. Classes that implement `MethodInterceptor` and that implement around advice should also implement the
|
||||
following interface:
|
||||
Spring is compliant with the AOP Alliance interface for around advice that uses method
|
||||
interception. Classes that implement around advice should therefore implement the
|
||||
following `MethodInterceptor` interface from the `org.aopalliance.intercept` package:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
@@ -49,8 +49,8 @@ following interface:
|
||||
|
||||
The `MethodInvocation` argument to the `invoke()` method exposes the method being
|
||||
invoked, the target join point, the AOP proxy, and the arguments to the method. The
|
||||
`invoke()` method should return the invocation's result: the return value of the join
|
||||
point.
|
||||
`invoke()` method should return the invocation's result: typically the return value of
|
||||
the join point.
|
||||
|
||||
The following example shows a simple `MethodInterceptor` implementation:
|
||||
|
||||
@@ -64,9 +64,9 @@ Java::
|
||||
|
||||
public Object invoke(MethodInvocation invocation) throws Throwable {
|
||||
System.out.println("Before: invocation=[" + invocation + "]");
|
||||
Object rval = invocation.proceed();
|
||||
Object result = invocation.proceed();
|
||||
System.out.println("Invocation returned");
|
||||
return rval;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -79,9 +79,9 @@ Kotlin::
|
||||
|
||||
override fun invoke(invocation: MethodInvocation): Any {
|
||||
println("Before: invocation=[$invocation]")
|
||||
val rval = invocation.proceed()
|
||||
val result = invocation.proceed()
|
||||
println("Invocation returned")
|
||||
return rval
|
||||
return result
|
||||
}
|
||||
}
|
||||
----
|
||||
@@ -105,7 +105,7 @@ currently define pointcut interfaces.
|
||||
[[aop-api-advice-before]]
|
||||
=== Before Advice
|
||||
|
||||
A simpler advice type is a before advice. This does not need a `MethodInvocation`
|
||||
A simpler advice type is a _before advice_. This does not need a `MethodInvocation`
|
||||
object, since it is called only before entering the method.
|
||||
|
||||
The main advantage of a before advice is that there is no need to invoke the `proceed()`
|
||||
@@ -122,10 +122,6 @@ The following listing shows the `MethodBeforeAdvice` interface:
|
||||
}
|
||||
----
|
||||
|
||||
(Spring's API design would allow for
|
||||
field before advice, although the usual objects apply to field interception and it is
|
||||
unlikely for Spring to ever implement it.)
|
||||
|
||||
Note that the return type is `void`. Before advice can insert custom behavior before the join
|
||||
point runs but cannot change the return value. If a before advice throws an
|
||||
exception, it stops further execution of the interceptor chain. The exception
|
||||
@@ -176,10 +172,10 @@ TIP: Before advice can be used with any pointcut.
|
||||
[[aop-api-advice-throws]]
|
||||
=== Throws Advice
|
||||
|
||||
Throws advice is invoked after the return of the join point if the join point threw
|
||||
_Throws advice_ is invoked after the return of the join point if the join point threw
|
||||
an exception. Spring offers typed throws advice. Note that this means that the
|
||||
`org.springframework.aop.ThrowsAdvice` interface does not contain any methods. It is a
|
||||
tag interface identifying that the given object implements one or more typed throws
|
||||
marker interface identifying that the given object implements one or more typed throws
|
||||
advice methods. These should be in the following form:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
@@ -189,9 +185,10 @@ advice methods. These should be in the following form:
|
||||
|
||||
Only the last argument is required. The method signatures may have either one or four
|
||||
arguments, depending on whether the advice method is interested in the method and
|
||||
arguments. The next two listing show classes that are examples of throws advice.
|
||||
arguments. The next two listings show classes that are examples of throws advice.
|
||||
|
||||
The following advice is invoked if a `RemoteException` is thrown (including from subclasses):
|
||||
The following advice is invoked if a `RemoteException` is thrown (including subclasses of
|
||||
`RemoteException`):
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -220,9 +217,9 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
Unlike the preceding
|
||||
advice, the next example declares four arguments, so that it has access to the invoked method, method
|
||||
arguments, and target object. The following advice is invoked if a `ServletException` is thrown:
|
||||
Unlike the preceding advice, the next example declares four arguments, so that it has
|
||||
access to the invoked method, method arguments, and target object. The following advice
|
||||
is invoked if a `ServletException` is thrown:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -304,7 +301,7 @@ TIP: Throws advice can be used with any pointcut.
|
||||
[[aop-api-advice-after-returning]]
|
||||
=== After Returning Advice
|
||||
|
||||
An after returning advice in Spring must implement the
|
||||
An _after returning advice_ in Spring must implement the
|
||||
`org.springframework.aop.AfterReturningAdvice` interface, which the following listing shows:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
@@ -368,7 +365,7 @@ TIP: After returning advice can be used with any pointcut.
|
||||
[[aop-api-advice-introduction]]
|
||||
=== Introduction Advice
|
||||
|
||||
Spring treats introduction advice as a special kind of interception advice.
|
||||
Spring treats _introduction advice_ as a special kind of interception advice.
|
||||
|
||||
Introduction requires an `IntroductionAdvisor` and an `IntroductionInterceptor` that
|
||||
implement the following interface:
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
|
||||
You can invoke constructors by using the `new` operator. You should use the fully
|
||||
qualified class name for all types except those located in the `java.lang` package
|
||||
(`Integer`, `Float`, `String`, and so on). The following example shows how to use the
|
||||
`new` operator to invoke constructors:
|
||||
(`Integer`, `Float`, `String`, and so on).
|
||||
xref:core/expressions/language-ref/varargs.adoc[Varargs] are also supported.
|
||||
|
||||
The following example shows how to use the `new` operator to invoke constructors.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -12,30 +14,29 @@ Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Inventor einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
Inventor einstein = parser.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor.class);
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor(
|
||||
'Albert Einstein', 'German'))").getValue(societyContext);
|
||||
parser.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
.getValue(societyContext);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
val einstein = p.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
val einstein = parser.parseExpression(
|
||||
"new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German')")
|
||||
.getValue(Inventor::class.java)
|
||||
|
||||
// create new Inventor instance within the add() method of List
|
||||
p.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
parser.parseExpression(
|
||||
"Members.add(new org.spring.samples.spel.inventor.Inventor('Albert Einstein', 'German'))")
|
||||
.getValue(societyContext)
|
||||
----
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,12 @@
|
||||
= Functions
|
||||
|
||||
You can extend SpEL by registering user-defined functions that can be called within
|
||||
expressions by using the `#functionName(...)` syntax. Functions can be registered as
|
||||
variables in `EvaluationContext` implementations via the `setVariable()` method.
|
||||
expressions by using the `#functionName(...)` syntax, and like with standard method
|
||||
invocations, xref:core/expressions/language-ref/varargs.adoc[varargs] are also supported
|
||||
for function invocations.
|
||||
|
||||
Functions can be registered as _variables_ in `EvaluationContext` implementations via the
|
||||
`setVariable()` method.
|
||||
|
||||
[TIP]
|
||||
====
|
||||
@@ -110,8 +114,9 @@ potentially more efficient use cases if the `MethodHandle` target and parameters
|
||||
been fully bound prior to registration; however, partially bound handles are also
|
||||
supported.
|
||||
|
||||
Consider the `String#formatted(String, Object...)` instance method, which produces a
|
||||
message according to a template and a variable number of arguments.
|
||||
Consider the `String#formatted(Object...)` instance method, which produces a message
|
||||
according to a template and a variable number of arguments
|
||||
(xref:core/expressions/language-ref/varargs.adoc[varargs]).
|
||||
|
||||
You can register and use the `formatted` method as a `MethodHandle`, as the following
|
||||
example shows:
|
||||
@@ -151,10 +156,10 @@ Kotlin::
|
||||
----
|
||||
======
|
||||
|
||||
As hinted above, binding a `MethodHandle` and registering the bound `MethodHandle` is also
|
||||
supported. This is likely to be more performant if both the target and all the arguments
|
||||
are bound. In that case no arguments are necessary in the SpEL expression, as the
|
||||
following example shows:
|
||||
As mentioned above, binding a `MethodHandle` and registering the bound `MethodHandle` is
|
||||
also supported. This is likely to be more performant if both the target and all the
|
||||
arguments are bound. In that case no arguments are necessary in the SpEL expression, as
|
||||
the following example shows:
|
||||
|
||||
[tabs]
|
||||
======
|
||||
@@ -168,9 +173,10 @@ Java::
|
||||
String template = "This is a %s message with %s words: <%s>";
|
||||
Object varargs = new Object[] { "prerecorded", 3, "Oh Hello World!", "ignored" };
|
||||
MethodHandle mh = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs); //here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs);
|
||||
context.setVariable("message", mh);
|
||||
|
||||
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
@@ -189,9 +195,10 @@ Kotlin::
|
||||
val varargs = arrayOf("prerecorded", 3, "Oh Hello World!", "ignored")
|
||||
|
||||
val mh = MethodHandles.lookup().findVirtual(String::class.java, "formatted",
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
MethodType.methodType(String::class.java, Array<Any>::class.java))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs) //here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs)
|
||||
context.setVariable("message", mh)
|
||||
|
||||
// evaluates to "This is a prerecorded message with 3 words: <Oh Hello World!>"
|
||||
@@ -201,4 +208,3 @@ Kotlin::
|
||||
======
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
[[expressions-methods]]
|
||||
= Methods
|
||||
|
||||
You can invoke methods by using typical Java programming syntax. You can also invoke methods
|
||||
on literals. Variable arguments are also supported. The following examples show how to
|
||||
invoke methods:
|
||||
You can invoke methods by using the typical Java programming syntax. You can also invoke
|
||||
methods directly on literals such as strings or numbers.
|
||||
xref:core/expressions/language-ref/varargs.adoc[Varargs] are supported as well.
|
||||
|
||||
The following examples show how to invoke methods.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
[[expressions-varargs]]
|
||||
= Varargs Invocations
|
||||
|
||||
The Spring Expression Language supports
|
||||
https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html[varargs]
|
||||
invocations for xref:core/expressions/language-ref/constructors.adoc[constructors],
|
||||
xref:core/expressions/language-ref/methods.adoc[methods], and user-defined
|
||||
xref:core/expressions/language-ref/functions.adoc[functions].
|
||||
|
||||
The following example shows how to invoke the `java.lang.String#formatted(Object...)`
|
||||
_varargs_ method within an expression by supplying the variable argument list as separate
|
||||
arguments (`'blue', 1`).
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted('blue', 1)";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%d'.formatted('blue', 1)"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
A variable argument list can also be supplied as an array, as demonstrated in the
|
||||
following example (`new Object[] {'blue', 1}`).
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
As an alternative, a variable argument list can be supplied as a `java.util.List` – for
|
||||
example, as an xref:core/expressions/language-ref/inline-lists.adoc[inline list]
|
||||
(`{'blue', 1}`). The following example shows how to do that.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted({'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%d'.formatted({'blue', 1})"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
[[expressions-varargs-type-conversion]]
|
||||
== Varargs Type Conversion
|
||||
|
||||
In contrast to the standard support for varargs invocations in Java,
|
||||
xref:core/expressions/evaluation.adoc#expressions-type-conversion[type conversion] may be
|
||||
applied to the individual arguments when invoking varargs constructors, methods, or
|
||||
functions in SpEL.
|
||||
|
||||
For example, if we have registered a custom
|
||||
xref:core/expressions/language-ref/functions.adoc[function] in the `EvaluationContext`
|
||||
under the name `#reverseStrings` for a method with the signature
|
||||
`String reverseStrings(String... strings)`, we can invoke that function within a SpEL
|
||||
expression with any argument that can be converted to a `String`, as demonstrated in the
|
||||
following example.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "3.0, 2.0, 1, SpEL"
|
||||
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
|
||||
String message = parser.parseExpression(expression)
|
||||
.getValue(evaluationContext, String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "3.0, 2.0, 1, SpEL"
|
||||
val expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)"
|
||||
val message = parser.parseExpression(expression)
|
||||
.getValue(evaluationContext, String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
Similarly, any array whose component type is a subtype of the required varargs type can
|
||||
be supplied as the variable argument list for a varargs invocation. For example, a
|
||||
`String[]` array can be supplied to a varargs invocation that accepts an `Object...`
|
||||
argument list.
|
||||
|
||||
The following listing demonstrates that we can supply a `String[]` array to the
|
||||
`java.lang.String#formatted(Object...)` _varargs_ method. It also highlights that `1`
|
||||
will be automatically converted to `"1"`.
|
||||
|
||||
[tabs]
|
||||
======
|
||||
Java::
|
||||
+
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
----
|
||||
|
||||
Kotlin::
|
||||
+
|
||||
[source,kotlin,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
// evaluates to "blue is color #1"
|
||||
val expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})"
|
||||
val message = parser.parseExpression(expression).getValue(String::class.java)
|
||||
----
|
||||
======
|
||||
|
||||
@@ -6,7 +6,7 @@ the same way as Spring's integration does for the JDBC API.
|
||||
|
||||
JMS can be roughly divided into two areas of functionality, namely the production and
|
||||
consumption of messages. The `JmsTemplate` class is used for message production and
|
||||
synchronous message reception. For asynchronous reception similar to Jakarta EE's
|
||||
synchronous message receipt. For asynchronous receipt similar to Jakarta EE's
|
||||
message-driven bean style, Spring provides a number of message-listener containers that
|
||||
you can use to create Message-Driven POJOs (MDPs). Spring also provides a declarative way
|
||||
to create message listeners.
|
||||
|
||||
@@ -5,7 +5,7 @@ This describes how to receive messages with JMS in Spring.
|
||||
|
||||
|
||||
[[jms-receiving-sync]]
|
||||
== Synchronous Reception
|
||||
== Synchronous Receipt
|
||||
|
||||
While JMS is typically associated with asynchronous processing, you can
|
||||
consume messages synchronously. The overloaded `receive(..)` methods provide this
|
||||
@@ -16,11 +16,11 @@ the receiver should wait before giving up waiting for a message.
|
||||
|
||||
|
||||
[[jms-receiving-async]]
|
||||
== Asynchronous reception: Message-Driven POJOs
|
||||
== Asynchronous Receipt: Message-Driven POJOs
|
||||
|
||||
NOTE: Spring also supports annotated-listener endpoints through the use of the `@JmsListener`
|
||||
annotation and provides an open infrastructure to register endpoints programmatically.
|
||||
This is, by far, the most convenient way to setup an asynchronous receiver.
|
||||
annotation and provides open infrastructure to register endpoints programmatically.
|
||||
This is, by far, the most convenient way to set up an asynchronous receiver.
|
||||
See xref:integration/jms/annotated.adoc#jms-annotated-support[Enable Listener Endpoint Annotations] for more details.
|
||||
|
||||
In a fashion similar to a Message-Driven Bean (MDB) in the EJB world, the Message-Driven
|
||||
@@ -154,7 +154,7 @@ listener container.
|
||||
|
||||
You can activate local resource transactions through the `sessionTransacted` flag
|
||||
on the listener container definition. Each message listener invocation then operates
|
||||
within an active JMS transaction, with message reception rolled back in case of listener
|
||||
within an active JMS transaction, with message receipt rolled back in case of listener
|
||||
execution failure. Sending a response message (through `SessionAwareMessageListener`) is
|
||||
part of the same local transaction, but any other resource operations (such as
|
||||
database access) operate independently. This usually requires duplicate message
|
||||
@@ -173,7 +173,7 @@ To configure a message listener container for XA transaction participation, you
|
||||
to configure a `JtaTransactionManager` (which, by default, delegates to the Jakarta EE
|
||||
server's transaction subsystem). Note that the underlying JMS `ConnectionFactory` needs to
|
||||
be XA-capable and properly registered with your JTA transaction coordinator. (Check your
|
||||
Jakarta EE server's configuration of JNDI resources.) This lets message reception as well
|
||||
Jakarta EE server's configuration of JNDI resources.) This lets message receipt as well
|
||||
as (for example) database access be part of the same transaction (with unified commit
|
||||
semantics, at the expense of XA transaction log overhead).
|
||||
|
||||
|
||||
@@ -167,13 +167,15 @@ operations that do not refer to a specific destination.
|
||||
|
||||
One of the most common uses of JMS messages in the EJB world is to drive message-driven
|
||||
beans (MDBs). Spring offers a solution to create message-driven POJOs (MDPs) in a way
|
||||
that does not tie a user to an EJB container. (See xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous reception: Message-Driven POJOs] for detailed
|
||||
coverage of Spring's MDP support.) Since Spring Framework 4.1, endpoint methods can be
|
||||
annotated with `@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints] for more details.
|
||||
that does not tie a user to an EJB container. (See
|
||||
xref:integration/jms/receiving.adoc#jms-receiving-async[Asynchronous Receipt: Message-Driven POJOs]
|
||||
for detailed coverage of Spring's MDP support.) Endpoint methods can be annotated with
|
||||
`@JmsListener` -- see xref:integration/jms/annotated.adoc[Annotation-driven Listener Endpoints]
|
||||
for more details.
|
||||
|
||||
A message listener container is used to receive messages from a JMS message queue and
|
||||
drive the `MessageListener` that is injected into it. The listener container is
|
||||
responsible for all threading of message reception and dispatches into the listener for
|
||||
responsible for all threading of message receipt and dispatches into the listener for
|
||||
processing. A message listener container is the intermediary between an MDP and a
|
||||
messaging provider and takes care of registering to receive messages, participating in
|
||||
transactions, resource acquisition and release, exception conversion, and so on. This
|
||||
@@ -227,7 +229,7 @@ the JMS provider, advanced functionality (such as participation in externally ma
|
||||
transactions), and compatibility with Jakarta EE environments.
|
||||
|
||||
You can customize the cache level of the container. Note that, when no caching is enabled,
|
||||
a new connection and a new session is created for each message reception. Combining this
|
||||
a new connection and a new session is created for each message receipt. Combining this
|
||||
with a non-durable subscription with high loads may lead to message loss. Make sure to
|
||||
use a proper cache level in such a case.
|
||||
|
||||
@@ -246,7 +248,7 @@ in the form of a business entity existence check or a protocol table check.
|
||||
Any such arrangements are significantly more efficient than the alternative:
|
||||
wrapping your entire processing with an XA transaction (through configuring your
|
||||
`DefaultMessageListenerContainer` with an `JtaTransactionManager`) to cover the
|
||||
reception of the JMS message as well as the execution of the business logic in your
|
||||
receipt of the JMS message as well as the execution of the business logic in your
|
||||
message listener (including database operations, etc.).
|
||||
|
||||
IMPORTANT: The default `AUTO_ACKNOWLEDGE` mode does not provide proper reliability guarantees.
|
||||
|
||||
@@ -37,7 +37,7 @@ As outlined xref:integration/observability.adoc[at the beginning of this section
|
||||
|Processing time for an execution of a `@Scheduled` task
|
||||
|===
|
||||
|
||||
NOTE: Observations are using Micrometer's official naming convention, but Metrics names will be automatically converted
|
||||
NOTE: Observations use Micrometer's official naming convention, but Metrics names will be automatically converted
|
||||
{micrometer-docs}/concepts/naming.html[to the format preferred by the monitoring system backend]
|
||||
(Prometheus, Atlas, Graphite, InfluxDB...).
|
||||
|
||||
@@ -97,7 +97,7 @@ This can be done by declaring a `SchedulingConfigurer` bean that sets the observ
|
||||
|
||||
include-code::./ObservationSchedulingConfigurer[]
|
||||
|
||||
It is using the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
|
||||
It uses the `org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention` by default, backed by the `ScheduledTaskObservationContext`.
|
||||
You can configure a custom implementation on the `ObservationRegistry` directly.
|
||||
During the execution of the scheduled method, the current observation is restored in the `ThreadLocal` context or the Reactor context (if the scheduled method returns a `Mono` or `Flux` type).
|
||||
|
||||
@@ -107,7 +107,7 @@ By default, the following `KeyValues` are created:
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`code.function` _(required)_|Name of Java `Method` that is scheduled for execution.
|
||||
|`code.function` _(required)_|Name of the Java `Method` that is scheduled for execution.
|
||||
|`code.namespace` _(required)_|Canonical name of the class of the bean instance that holds the scheduled method, or `"ANONYMOUS"` for anonymous classes.
|
||||
|`error` _(required)_|Class name of the exception thrown during the execution, or `"none"` if no exception happened.
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
@@ -126,7 +126,7 @@ This instrumentation will create 2 types of observations:
|
||||
* `"jms.message.publish"` when a JMS message is sent to the broker, typically with `JmsTemplate`.
|
||||
* `"jms.message.process"` when a JMS message is processed by the application, typically with a `MessageListener` or a `@JmsListener` annotated method.
|
||||
|
||||
NOTE: currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the reception of a message.
|
||||
NOTE: Currently there is no instrumentation for `"jms.message.receive"` observations as there is little value in measuring the time spent waiting for the receipt of a message.
|
||||
Such an integration would typically instrument `MessageConsumer#receive` method calls. But once those return, the processing time is not measured and the trace scope cannot be propagated to the application.
|
||||
|
||||
By default, both observations share the same set of possible `KeyValues`:
|
||||
@@ -138,7 +138,7 @@ By default, both observations share the same set of possible `KeyValues`:
|
||||
|`error` |Class name of the exception thrown during the messaging operation (or "none").
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
|`messaging.destination.temporary` _(required)_|Whether the destination is a `TemporaryQueue` or `TemporaryTopic` (values: `"true"` or `"false"`).
|
||||
|`messaging.operation` _(required)_|Name of JMS operation being performed (values: `"publish"` or `"process"`).
|
||||
|`messaging.operation` _(required)_|Name of the JMS operation being performed (values: `"publish"` or `"process"`).
|
||||
|===
|
||||
|
||||
.High cardinality Keys
|
||||
@@ -146,7 +146,7 @@ By default, both observations share the same set of possible `KeyValues`:
|
||||
|===
|
||||
|Name | Description
|
||||
|`messaging.message.conversation_id` |The correlation ID of the JMS message.
|
||||
|`messaging.destination.name` |The name of destination the current message was sent to.
|
||||
|`messaging.destination.name` |The name of the destination the current message was sent to.
|
||||
|`messaging.message.id` |Value used by the messaging system as an identifier for the message.
|
||||
|===
|
||||
|
||||
@@ -213,7 +213,7 @@ By default, the following `KeyValues` are created:
|
||||
|Name | Description
|
||||
|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened.
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|
||||
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
|
||||
@@ -235,10 +235,10 @@ This can be done on the `WebHttpHandlerBuilder`, as follows:
|
||||
|
||||
include-code::./HttpHandlerConfiguration[]
|
||||
|
||||
It is using the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
It uses the `org.springframework.http.server.reactive.observation.DefaultServerRequestObservationConvention` by default, backed by the `ServerRequestObservationContext`.
|
||||
|
||||
This will only record an observation as an error if the `Exception` has not been handled by an application Controller.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and <<web.adoc#webflux-ann-rest-exceptions,`ProblemDetail` support>> will not be recorded with the observation.
|
||||
Typically, all exceptions handled by Spring WebFlux's `@ExceptionHandler` and xref:web/webflux/ann-rest-exceptions.adoc[`ProblemDetail` support] will not be recorded with the observation.
|
||||
You can, at any point during request processing, set the error field on the `ObservationContext` yourself:
|
||||
|
||||
include-code::./UserController[]
|
||||
@@ -251,7 +251,7 @@ By default, the following `KeyValues` are created:
|
||||
|Name | Description
|
||||
|`error` _(required)_|Class name of the exception thrown during the exchange, or `"none"` if no exception happened.
|
||||
|`exception` _(deprecated)_|Duplicates the `error` key and might be removed in the future.
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`outcome` _(required)_|Outcome of the HTTP server exchange.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"UNKNOWN"` if no response was created.
|
||||
|`uri` _(required)_|URI pattern for the matching handler if available, falling back to `REDIRECTION` for 3xx responses, `NOT_FOUND` for 404 responses, `root` for requests with no path info, and `UNKNOWN` for all other requests.
|
||||
@@ -284,7 +284,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
@@ -312,7 +312,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if the request could not be created.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if the request could not be created.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
@@ -332,7 +332,7 @@ Instrumentation uses the `org.springframework.http.client.observation.ClientRequ
|
||||
[[observability.http-client.webclient]]
|
||||
=== WebClient
|
||||
|
||||
Applications must configure an `ObservationRegistry` on the `WebClient` builder to enable the instrumentation; without that, observations are "no-ops".
|
||||
Applications must configure an `ObservationRegistry` on the `WebClient.Builder` to enable the instrumentation; without that, observations are "no-ops".
|
||||
Spring Boot will auto-configure `WebClient.Builder` beans with the observation registry already set.
|
||||
|
||||
Instrumentation uses the `org.springframework.web.reactive.function.client.ClientRequestObservationConvention` by default, backed by the `ClientRequestObservationContext`.
|
||||
@@ -341,7 +341,7 @@ Instrumentation uses the `org.springframework.web.reactive.function.client.Clien
|
||||
[cols="a,a"]
|
||||
|===
|
||||
|Name | Description
|
||||
|`method` _(required)_|Name of HTTP request method or `"none"` if not a well-known method.
|
||||
|`method` _(required)_|Name of the HTTP request method or `"none"` if not a well-known method.
|
||||
|`uri` _(required)_|URI template used for HTTP request, or `"none"` if none was provided. Only the path part of the URI is considered.
|
||||
|`client.name` _(required)_|Client name derived from the request URI host.
|
||||
|`status` _(required)_|HTTP response raw status code, or `"IO_ERROR"` in case of `IOException`, or `"CLIENT_ERROR"` if no response was received.
|
||||
|
||||
+17
-7
@@ -1,9 +1,10 @@
|
||||
[[spring-testing-annotation-beanoverriding-mockitobean]]
|
||||
= `@MockitoBean` and `@MockitoSpyBean`
|
||||
|
||||
`@MockitoBean` and `@MockitoSpyBean` are used on fields in test classes to override beans
|
||||
in the test's `ApplicationContext` with a Mockito _mock_ or _spy_, respectively. In the
|
||||
latter case, an early instance of the original bean is captured and wrapped by the spy.
|
||||
`@MockitoBean` and `@MockitoSpyBean` are used on non-static fields in test classes to
|
||||
override beans in the test's `ApplicationContext` with a Mockito _mock_ or _spy_,
|
||||
respectively. In the latter case, an early instance of the original bean is captured and
|
||||
wrapped by the spy.
|
||||
|
||||
By default, the annotated field's type is used to search for candidate beans to override.
|
||||
If multiple candidates match, `@Qualifier` can be provided to narrow the candidate to
|
||||
@@ -51,6 +52,15 @@ When using `@MockitoSpyBean` to create a spy for a `FactoryBean`, a spy will be
|
||||
for the object created by the `FactoryBean`, not for the `FactoryBean` itself.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
There are no restrictions on the visibility of `@MockitoBean` and `@MockitoSpyBean`
|
||||
fields.
|
||||
|
||||
Such fields can therefore be `public`, `protected`, package-private (default visibility),
|
||||
or `private` depending on the needs or coding practices of the project.
|
||||
====
|
||||
|
||||
The following example shows how to use the default behavior of the `@MockitoBean` annotation:
|
||||
|
||||
[tabs]
|
||||
@@ -61,7 +71,7 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@MockitoBean // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
}
|
||||
@@ -85,7 +95,7 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@MockitoBean("service") // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
@@ -106,7 +116,7 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@MockitoSpyBean // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
}
|
||||
@@ -129,7 +139,7 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@MockitoSpyBean("service") // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
|
||||
+14
-6
@@ -1,8 +1,8 @@
|
||||
[[spring-testing-annotation-beanoverriding-testbean]]
|
||||
= `@TestBean`
|
||||
|
||||
`@TestBean` is used on a field in a test class to override a specific bean in the test's
|
||||
`ApplicationContext` with an instance provided by a factory method.
|
||||
`@TestBean` is used on a non-static field in a test class to override a specific bean in
|
||||
the test's `ApplicationContext` with an instance provided by a factory method.
|
||||
|
||||
The associated factory method name is derived from the annotated field's name, or the
|
||||
bean name if specified. The factory method must be `static`, accept no arguments, and
|
||||
@@ -30,6 +30,14 @@ same bean in several tests, make sure to name the field consistently to avoid cr
|
||||
unnecessary contexts.
|
||||
====
|
||||
|
||||
[NOTE]
|
||||
====
|
||||
There are no restrictions on the visibility of `@TestBean` fields or factory methods.
|
||||
|
||||
Such fields and methods can therefore be `public`, `protected`, package-private (default
|
||||
visibility), or `private` depending on the needs or coding practices of the project.
|
||||
====
|
||||
|
||||
The following example shows how to use the default behavior of the `@TestBean` annotation:
|
||||
|
||||
[tabs]
|
||||
@@ -40,11 +48,11 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@TestBean // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
private static CustomService customService() { // <2>
|
||||
static CustomService customService() { // <2>
|
||||
return new MyFakeCustomService();
|
||||
}
|
||||
}
|
||||
@@ -68,11 +76,11 @@ Java::
|
||||
----
|
||||
class OverrideBeanTests {
|
||||
@TestBean(name = "service", methodName = "createCustomService") // <1>
|
||||
private CustomService customService;
|
||||
CustomService customService;
|
||||
|
||||
// test case body...
|
||||
|
||||
private static CustomService createCustomService() { // <2>
|
||||
static CustomService createCustomService() { // <2>
|
||||
return new MyFakeCustomService();
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -2,7 +2,8 @@
|
||||
= Bean Overriding in Tests
|
||||
|
||||
Bean overriding in tests refers to the ability to override specific beans in the
|
||||
`ApplicationContext` for a test class, by annotating one or more fields in the test class.
|
||||
`ApplicationContext` for a test class, by annotating one or more non-static fields in the
|
||||
test class.
|
||||
|
||||
NOTE: This feature is intended as a less risky alternative to the practice of registering
|
||||
a bean via `@Bean` with the `DefaultListableBeanFactory`
|
||||
@@ -41,9 +42,10 @@ The `spring-test` module registers implementations of the latter two
|
||||
{spring-framework-code}/spring-test/src/main/resources/META-INF/spring.factories[`META-INF/spring.factories`
|
||||
properties file].
|
||||
|
||||
The bean overriding infrastructure searches in test classes for any field meta-annotated
|
||||
with `@BeanOverride` and instantiates the corresponding `BeanOverrideProcessor` which is
|
||||
responsible for creating an appropriate `BeanOverrideHandler`.
|
||||
The bean overriding infrastructure searches in test classes for any non-static field that
|
||||
is meta-annotated with `@BeanOverride` and instantiates the corresponding
|
||||
`BeanOverrideProcessor` which is responsible for creating an appropriate
|
||||
`BeanOverrideHandler`.
|
||||
|
||||
The internal `BeanOverrideBeanFactoryPostProcessor` then uses bean override handlers to
|
||||
alter the test's `ApplicationContext` by creating, replacing, or wrapping beans as
|
||||
|
||||
@@ -127,7 +127,7 @@ Kotlin::
|
||||
======
|
||||
|
||||
For Spring MVC, use the following where the Spring `ApplicationContext` is passed to
|
||||
{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup-org.springframework.web.context.WebApplicationContext-[MockMvcBuilders.webAppContextSetup]
|
||||
{spring-framework-api}/test/web/servlet/setup/MockMvcBuilders.html#webAppContextSetup(org.springframework.web.context.WebApplicationContext)[MockMvcBuilders.webAppContextSetup]
|
||||
to create a xref:testing/mockmvc.adoc[MockMvc] instance to handle
|
||||
requests:
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ available through the `ServletRequest.getParameter{asterisk}()` family of method
|
||||
|
||||
|
||||
|
||||
[[forwarded-headers]]
|
||||
[[filters-forwarded-headers]]
|
||||
== Forwarded Headers
|
||||
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-forwarded-headers[See equivalent in the Reactive stack]#
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ javaPlatform {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.1"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.0"))
|
||||
api(platform("com.fasterxml.jackson:jackson-bom:2.18.2"))
|
||||
api(platform("io.micrometer:micrometer-bom:1.14.2"))
|
||||
api(platform("io.netty:netty-bom:4.1.115.Final"))
|
||||
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.0"))
|
||||
api(platform("io.projectreactor:reactor-bom:2024.0.1"))
|
||||
api(platform("io.rsocket:rsocket-bom:1.1.4"))
|
||||
api(platform("org.apache.groovy:groovy-bom:4.0.24"))
|
||||
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
|
||||
@@ -54,11 +54,11 @@ dependencies {
|
||||
api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE")
|
||||
api("io.r2dbc:r2dbc-spi:1.0.0.RELEASE")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.9")
|
||||
api("io.reactivex.rxjava3:rxjava:3.1.10")
|
||||
api("io.smallrye.reactive:mutiny:1.10.0")
|
||||
api("io.undertow:undertow-core:2.3.18.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.17.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.17.Final")
|
||||
api("io.undertow:undertow-servlet:2.3.18.Final")
|
||||
api("io.undertow:undertow-websockets-jsr:2.3.18.Final")
|
||||
api("io.vavr:vavr:0.10.4")
|
||||
api("jakarta.activation:jakarta.activation-api:2.0.1")
|
||||
api("jakarta.annotation:jakarta.annotation-api:2.0.0")
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=6.2.0-SNAPSHOT
|
||||
version=6.2.1
|
||||
|
||||
org.gradle.caching=true
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2008 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -27,11 +27,11 @@ package org.springframework.aop;
|
||||
* <p>Some examples of valid methods would be:
|
||||
*
|
||||
* <pre class="code">public void afterThrowing(Exception ex)</pre>
|
||||
* <pre class="code">public void afterThrowing(RemoteException)</pre>
|
||||
* <pre class="code">public void afterThrowing(RemoteException ex)</pre>
|
||||
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, Exception ex)</pre>
|
||||
* <pre class="code">public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)</pre>
|
||||
*
|
||||
* The first three arguments are optional, and only useful if we want further
|
||||
* <p>The first three arguments are optional, and only useful if we want further
|
||||
* information about the joinpoint, as in AspectJ <b>after-throwing</b> advice.
|
||||
*
|
||||
* <p><b>Note:</b> If a throws-advice method throws an exception itself, it will
|
||||
|
||||
@@ -668,7 +668,8 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object other) {
|
||||
return (this == other || (other instanceof MethodCacheKey that && this.method == that.method));
|
||||
return (this == other || (other instanceof MethodCacheKey that &&
|
||||
(this.method == that.method || this.method.equals(that.method))));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+3
-2
@@ -60,11 +60,12 @@ public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
|
||||
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
|
||||
if (config.isOptimize() || config.isProxyTargetClass() || !config.hasUserSuppliedInterfaces()) {
|
||||
Class<?> targetClass = config.getTargetClass();
|
||||
if (targetClass == null) {
|
||||
if (targetClass == null && config.getProxiedInterfaces().length == 0) {
|
||||
throw new AopConfigException("TargetSource cannot determine target class: " +
|
||||
"Either an interface or a target is required for proxy creation.");
|
||||
}
|
||||
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
|
||||
if (targetClass == null || targetClass.isInterface() ||
|
||||
Proxy.isProxyClass(targetClass) || ClassUtils.isLambdaClass(targetClass)) {
|
||||
return new JdkDynamicAopProxy(config);
|
||||
}
|
||||
return new ObjenesisCglibAopProxy(config);
|
||||
|
||||
@@ -340,6 +340,18 @@ class ProxyFactoryTests {
|
||||
assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(MyDate.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyInterfaceInCaseOfIntroducedInterfaceOnly() {
|
||||
ProxyFactory pf = new ProxyFactory();
|
||||
pf.addInterface(TimeStamped.class);
|
||||
TimestampIntroductionInterceptor ti = new TimestampIntroductionInterceptor(0L);
|
||||
pf.addAdvisor(new DefaultIntroductionAdvisor(ti, TimeStamped.class));
|
||||
Object proxy = pf.getProxy();
|
||||
assertThat(AopUtils.isJdkDynamicProxy(proxy)).as("Proxy is a JDK proxy").isTrue();
|
||||
assertThat(proxy).isInstanceOf(TimeStamped.class);
|
||||
assertThat(AopProxyUtils.ultimateTargetClass(proxy)).isEqualTo(proxy.getClass());
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyInterfaceInCaseOfNonTargetInterface() {
|
||||
ProxyFactory pf = new ProxyFactory();
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ class DefaultBeanRegistrationCodeFragments implements BeanRegistrationCodeFragme
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
RootBeanDefinition mbd = this.registeredBean.getMergedBeanDefinition();
|
||||
Class<?> beanClass = (mbd.hasBeanClass() ? mbd.getBeanClass() : null);
|
||||
Class<?> beanClass = (mbd.hasBeanClass() ? ClassUtils.getUserClass(mbd.getBeanClass()) : null);
|
||||
CodeBlock beanClassCode = generateBeanClassCode(
|
||||
beanRegistrationCode.getClassName().packageName(),
|
||||
(beanClass != null ? beanClass : beanType.toClass()));
|
||||
|
||||
+42
-30
@@ -156,91 +156,96 @@ public class InstanceSupplierCodeGenerator {
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Constructor<?> constructor) {
|
||||
String beanName = registeredBean.getBeanName();
|
||||
Class<?> beanClass = registeredBean.getBeanClass();
|
||||
ConstructorDescriptor descriptor = new ConstructorDescriptor(
|
||||
registeredBean.getBeanName(), constructor, registeredBean.getBeanClass());
|
||||
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(beanClass)) {
|
||||
return generateCodeForInaccessibleConstructor(beanName, constructor,
|
||||
hints -> hints.registerType(beanClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
|
||||
Class<?> publicType = descriptor.publicType();
|
||||
if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) {
|
||||
return generateCodeForInaccessibleConstructor(descriptor,
|
||||
hints -> hints.registerType(publicType, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS));
|
||||
}
|
||||
|
||||
if (!isVisible(constructor, constructor.getDeclaringClass())) {
|
||||
return generateCodeForInaccessibleConstructor(beanName, constructor,
|
||||
return generateCodeForInaccessibleConstructor(descriptor,
|
||||
hints -> hints.registerConstructor(constructor, ExecutableMode.INVOKE));
|
||||
}
|
||||
return generateCodeForAccessibleConstructor(beanName, constructor);
|
||||
return generateCodeForAccessibleConstructor(descriptor);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForAccessibleConstructor(String beanName, Constructor<?> constructor) {
|
||||
private CodeBlock generateCodeForAccessibleConstructor(ConstructorDescriptor descriptor) {
|
||||
Constructor<?> constructor = descriptor.constructor();
|
||||
this.generationContext.getRuntimeHints().reflection().registerConstructor(
|
||||
constructor, ExecutableMode.INTROSPECT);
|
||||
|
||||
if (constructor.getParameterCount() == 0) {
|
||||
if (!this.allowDirectSupplierShortcut) {
|
||||
return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, constructor.getDeclaringClass());
|
||||
return CodeBlock.of("$T.using($T::new)", InstanceSupplier.class, descriptor.actualType());
|
||||
}
|
||||
if (!isThrowingCheckedException(constructor)) {
|
||||
return CodeBlock.of("$T::new", constructor.getDeclaringClass());
|
||||
return CodeBlock.of("$T::new", descriptor.actualType());
|
||||
}
|
||||
return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, constructor.getDeclaringClass());
|
||||
return CodeBlock.of("$T.of($T::new)", ThrowingSupplier.class, descriptor.actualType());
|
||||
}
|
||||
|
||||
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method ->
|
||||
buildGetInstanceMethodForConstructor(method, beanName, constructor, PRIVATE_STATIC));
|
||||
buildGetInstanceMethodForConstructor(method, descriptor, PRIVATE_STATIC));
|
||||
return generateReturnStatement(generatedMethod);
|
||||
}
|
||||
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(String beanName,
|
||||
Constructor<?> constructor, Consumer<ReflectionHints> hints) {
|
||||
private CodeBlock generateCodeForInaccessibleConstructor(ConstructorDescriptor descriptor,
|
||||
Consumer<ReflectionHints> hints) {
|
||||
|
||||
Constructor<?> constructor = descriptor.constructor();
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
codeWarnings.detectDeprecation(constructor.getDeclaringClass(), constructor)
|
||||
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
|
||||
hints.accept(this.generationContext.getRuntimeHints().reflection());
|
||||
|
||||
GeneratedMethod generatedMethod = generateGetInstanceSupplierMethod(method -> {
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName());
|
||||
method.addModifiers(PRIVATE_STATIC);
|
||||
codeWarnings.suppress(method);
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, constructor.getDeclaringClass()));
|
||||
method.addStatement(generateResolverForConstructor(constructor));
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, descriptor.publicType()));
|
||||
method.addStatement(generateResolverForConstructor(descriptor));
|
||||
});
|
||||
|
||||
return generateReturnStatement(generatedMethod);
|
||||
}
|
||||
|
||||
private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, String beanName,
|
||||
Constructor<?> constructor, javax.lang.model.element.Modifier... modifiers) {
|
||||
private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, ConstructorDescriptor descriptor,
|
||||
javax.lang.model.element.Modifier... modifiers) {
|
||||
|
||||
Class<?> declaringClass = constructor.getDeclaringClass();
|
||||
Constructor<?> constructor = descriptor.constructor();
|
||||
Class<?> publicType = descriptor.publicType();
|
||||
Class<?> actualType = descriptor.actualType();
|
||||
|
||||
CodeWarnings codeWarnings = new CodeWarnings();
|
||||
codeWarnings.detectDeprecation(declaringClass, constructor)
|
||||
codeWarnings.detectDeprecation(actualType, constructor)
|
||||
.detectDeprecation(Arrays.stream(constructor.getParameters()).map(Parameter::getType));
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", beanName);
|
||||
method.addJavadoc("Get the bean instance supplier for '$L'.", descriptor.beanName());
|
||||
method.addModifiers(modifiers);
|
||||
codeWarnings.suppress(method);
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, declaringClass));
|
||||
method.returns(ParameterizedTypeName.get(BeanInstanceSupplier.class, publicType));
|
||||
|
||||
CodeBlock.Builder code = CodeBlock.builder();
|
||||
code.add(generateResolverForConstructor(constructor));
|
||||
code.add(generateResolverForConstructor(descriptor));
|
||||
boolean hasArguments = constructor.getParameterCount() > 0;
|
||||
boolean onInnerClass = ClassUtils.isInnerClass(declaringClass);
|
||||
boolean onInnerClass = ClassUtils.isInnerClass(actualType);
|
||||
|
||||
CodeBlock arguments = hasArguments ?
|
||||
new AutowiredArgumentsCodeGenerator(declaringClass, constructor)
|
||||
new AutowiredArgumentsCodeGenerator(actualType, constructor)
|
||||
.generateCode(constructor.getParameterTypes(), (onInnerClass ? 1 : 0))
|
||||
: NO_ARGS;
|
||||
|
||||
CodeBlock newInstance = generateNewInstanceCodeForConstructor(declaringClass, arguments);
|
||||
CodeBlock newInstance = generateNewInstanceCodeForConstructor(actualType, arguments);
|
||||
code.add(generateWithGeneratorCode(hasArguments, newInstance));
|
||||
method.addStatement(code.build());
|
||||
}
|
||||
|
||||
private CodeBlock generateResolverForConstructor(Constructor<?> constructor) {
|
||||
CodeBlock parameterTypes = generateParameterTypesCode(constructor.getParameterTypes());
|
||||
private CodeBlock generateResolverForConstructor(ConstructorDescriptor descriptor) {
|
||||
CodeBlock parameterTypes = generateParameterTypesCode(descriptor.constructor().getParameterTypes());
|
||||
return CodeBlock.of("return $T.<$T>forConstructor($L)", BeanInstanceSupplier.class,
|
||||
constructor.getDeclaringClass(), parameterTypes);
|
||||
descriptor.publicType(), parameterTypes);
|
||||
}
|
||||
|
||||
private CodeBlock generateNewInstanceCodeForConstructor(Class<?> declaringClass, CodeBlock args) {
|
||||
@@ -438,4 +443,11 @@ public class InstanceSupplierCodeGenerator {
|
||||
}
|
||||
}
|
||||
|
||||
record ConstructorDescriptor(String beanName, Constructor<?> constructor, Class<?> publicType) {
|
||||
|
||||
Class<?> actualType() {
|
||||
return this.constructor.getDeclaringClass();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+46
-40
@@ -991,54 +991,60 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
|
||||
*/
|
||||
@Nullable
|
||||
private FactoryBean<?> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
|
||||
BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName);
|
||||
if (bw != null) {
|
||||
return (FactoryBean<?>) bw.getWrappedInstance();
|
||||
}
|
||||
Object beanInstance = getSingleton(beanName, false);
|
||||
if (beanInstance instanceof FactoryBean<?> factoryBean) {
|
||||
return factoryBean;
|
||||
}
|
||||
if (isSingletonCurrentlyInCreation(beanName) ||
|
||||
(mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object instance;
|
||||
this.singletonLock.lock();
|
||||
try {
|
||||
// Mark this bean as currently in creation, even if just partially.
|
||||
beforeSingletonCreation(beanName);
|
||||
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
|
||||
instance = resolveBeforeInstantiation(beanName, mbd);
|
||||
if (instance == null) {
|
||||
bw = createBeanInstance(beanName, mbd, null);
|
||||
instance = bw.getWrappedInstance();
|
||||
this.factoryBeanInstanceCache.put(beanName, bw);
|
||||
BeanWrapper bw = this.factoryBeanInstanceCache.get(beanName);
|
||||
if (bw != null) {
|
||||
return (FactoryBean<?>) bw.getWrappedInstance();
|
||||
}
|
||||
}
|
||||
catch (UnsatisfiedDependencyException ex) {
|
||||
// Don't swallow, probably misconfiguration...
|
||||
throw ex;
|
||||
}
|
||||
catch (BeanCreationException ex) {
|
||||
// Don't swallow a linkage error since it contains a full stacktrace on
|
||||
// first occurrence... and just a plain NoClassDefFoundError afterwards.
|
||||
if (ex.contains(LinkageError.class)) {
|
||||
Object beanInstance = getSingleton(beanName, false);
|
||||
if (beanInstance instanceof FactoryBean<?> factoryBean) {
|
||||
return factoryBean;
|
||||
}
|
||||
if (isSingletonCurrentlyInCreation(beanName) ||
|
||||
(mbd.getFactoryBeanName() != null && isSingletonCurrentlyInCreation(mbd.getFactoryBeanName()))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Object instance;
|
||||
try {
|
||||
// Mark this bean as currently in creation, even if just partially.
|
||||
beforeSingletonCreation(beanName);
|
||||
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
|
||||
instance = resolveBeforeInstantiation(beanName, mbd);
|
||||
if (instance == null) {
|
||||
bw = createBeanInstance(beanName, mbd, null);
|
||||
instance = bw.getWrappedInstance();
|
||||
this.factoryBeanInstanceCache.put(beanName, bw);
|
||||
}
|
||||
}
|
||||
catch (UnsatisfiedDependencyException ex) {
|
||||
// Don't swallow, probably misconfiguration...
|
||||
throw ex;
|
||||
}
|
||||
// Instantiation failure, maybe too early...
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex);
|
||||
catch (BeanCreationException ex) {
|
||||
// Don't swallow a linkage error since it contains a full stacktrace on
|
||||
// first occurrence... and just a plain NoClassDefFoundError afterwards.
|
||||
if (ex.contains(LinkageError.class)) {
|
||||
throw ex;
|
||||
}
|
||||
// Instantiation failure, maybe too early...
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Bean creation exception on singleton FactoryBean type check: " + ex);
|
||||
}
|
||||
onSuppressedException(ex);
|
||||
return null;
|
||||
}
|
||||
onSuppressedException(ex);
|
||||
return null;
|
||||
finally {
|
||||
// Finished partial creation of this bean.
|
||||
afterSingletonCreation(beanName);
|
||||
}
|
||||
|
||||
return getFactoryBean(beanName, instance);
|
||||
}
|
||||
finally {
|
||||
// Finished partial creation of this bean.
|
||||
afterSingletonCreation(beanName);
|
||||
this.singletonLock.unlock();
|
||||
}
|
||||
|
||||
return getFactoryBean(beanName, instance);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -54,6 +54,22 @@ public class BeanDefinitionOverrideException extends BeanDefinitionStoreExceptio
|
||||
this.existingDefinition = existingDefinition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new BeanDefinitionOverrideException for the given new and existing definition.
|
||||
* @param beanName the name of the bean
|
||||
* @param beanDefinition the newly registered bean definition
|
||||
* @param existingDefinition the existing bean definition for the same name
|
||||
* @param msg the detail message to include
|
||||
* @since 6.2.1
|
||||
*/
|
||||
public BeanDefinitionOverrideException(
|
||||
String beanName, BeanDefinition beanDefinition, BeanDefinition existingDefinition, String msg) {
|
||||
|
||||
super(beanDefinition.getResourceDescription(), beanName, msg);
|
||||
this.beanDefinition = beanDefinition;
|
||||
this.existingDefinition = existingDefinition;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return the description of the resource that the bean definition came from.
|
||||
|
||||
+5
@@ -1170,6 +1170,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Removing alias '" + beanName + "' for bean '" + aliasedName +
|
||||
"' due to registration of bean definition for bean '" + beanName + "': [" +
|
||||
beanDefinition + "]");
|
||||
}
|
||||
removeAlias(beanName);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -76,6 +76,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
private static final int SUPPRESSED_EXCEPTIONS_LIMIT = 100;
|
||||
|
||||
|
||||
/** Common lock for singleton creation. */
|
||||
final Lock singletonLock = new ReentrantLock();
|
||||
|
||||
/** Cache of singleton objects: bean name to bean instance. */
|
||||
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
|
||||
|
||||
@@ -91,8 +94,6 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
|
||||
/** Set of registered singletons, containing the bean names in registration order. */
|
||||
private final Set<String> registeredSingletons = Collections.synchronizedSet(new LinkedHashSet<>(256));
|
||||
|
||||
private final Lock singletonLock = new ReentrantLock();
|
||||
|
||||
/** Names of beans that are currently in creation. */
|
||||
private final Set<String> singletonsCurrentlyInCreation = ConcurrentHashMap.newKeySet(16);
|
||||
|
||||
|
||||
+34
-28
@@ -118,39 +118,45 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
|
||||
*/
|
||||
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
|
||||
if (factory.isSingleton() && containsSingleton(beanName)) {
|
||||
Object object = this.factoryBeanObjectCache.get(beanName);
|
||||
if (object == null) {
|
||||
object = doGetObjectFromFactoryBean(factory, beanName);
|
||||
// Only post-process and store if not put there already during getObject() call above
|
||||
// (for example, because of circular reference processing triggered by custom getBean calls)
|
||||
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
|
||||
if (alreadyThere != null) {
|
||||
object = alreadyThere;
|
||||
}
|
||||
else {
|
||||
if (shouldPostProcess) {
|
||||
if (isSingletonCurrentlyInCreation(beanName)) {
|
||||
// Temporarily return non-post-processed object, not storing it yet
|
||||
return object;
|
||||
this.singletonLock.lock();
|
||||
try {
|
||||
Object object = this.factoryBeanObjectCache.get(beanName);
|
||||
if (object == null) {
|
||||
object = doGetObjectFromFactoryBean(factory, beanName);
|
||||
// Only post-process and store if not put there already during getObject() call above
|
||||
// (for example, because of circular reference processing triggered by custom getBean calls)
|
||||
Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
|
||||
if (alreadyThere != null) {
|
||||
object = alreadyThere;
|
||||
}
|
||||
else {
|
||||
if (shouldPostProcess) {
|
||||
if (isSingletonCurrentlyInCreation(beanName)) {
|
||||
// Temporarily return non-post-processed object, not storing it yet
|
||||
return object;
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
try {
|
||||
object = postProcessObjectFromFactoryBean(object, beanName);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException(beanName,
|
||||
"Post-processing of FactoryBean's singleton object failed", ex);
|
||||
}
|
||||
finally {
|
||||
afterSingletonCreation(beanName);
|
||||
}
|
||||
}
|
||||
beforeSingletonCreation(beanName);
|
||||
try {
|
||||
object = postProcessObjectFromFactoryBean(object, beanName);
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new BeanCreationException(beanName,
|
||||
"Post-processing of FactoryBean's singleton object failed", ex);
|
||||
}
|
||||
finally {
|
||||
afterSingletonCreation(beanName);
|
||||
if (containsSingleton(beanName)) {
|
||||
this.factoryBeanObjectCache.put(beanName, object);
|
||||
}
|
||||
}
|
||||
if (containsSingleton(beanName)) {
|
||||
this.factoryBeanObjectCache.put(beanName, object);
|
||||
}
|
||||
}
|
||||
return object;
|
||||
}
|
||||
finally {
|
||||
this.singletonLock.unlock();
|
||||
}
|
||||
return object;
|
||||
}
|
||||
else {
|
||||
Object object = doGetObjectFromFactoryBean(factory, beanName);
|
||||
|
||||
+5
@@ -870,10 +870,15 @@ class DefaultListableBeanFactoryTests {
|
||||
void beanDefinitionOverriding() {
|
||||
lbf.setAllowBeanDefinitionOverriding(true);
|
||||
lbf.registerBeanDefinition("test", new RootBeanDefinition(TestBean.class));
|
||||
// Override "test" bean definition.
|
||||
lbf.registerBeanDefinition("test", new RootBeanDefinition(NestedTestBean.class));
|
||||
// Temporary "test2" alias for nonexistent bean.
|
||||
lbf.registerAlias("otherTest", "test2");
|
||||
// Reassign "test2" alias to "test".
|
||||
lbf.registerAlias("test", "test2");
|
||||
// Assign "testX" alias to "test" as well.
|
||||
lbf.registerAlias("test", "testX");
|
||||
// Register new "testX" bean definition which also removes the "testX" alias for "test".
|
||||
lbf.registerBeanDefinition("testX", new RootBeanDefinition(TestBean.class));
|
||||
|
||||
assertThat(lbf.getBean("test")).isInstanceOf(NestedTestBean.class);
|
||||
|
||||
+17
-7
@@ -36,10 +36,10 @@ import org.springframework.beans.factory.groovy.GroovyBeanDefinitionReader;
|
||||
import org.springframework.beans.factory.parsing.SourceExtractor;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinitionReader;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReader;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanNameGenerator;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
|
||||
import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase;
|
||||
@@ -297,13 +297,21 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
return false;
|
||||
}
|
||||
BeanDefinition existingBeanDef = this.registry.getBeanDefinition(beanName);
|
||||
ConfigurationClass configClass = beanMethod.getConfigurationClass();
|
||||
|
||||
// If the bean method is an overloaded case on the same configuration class,
|
||||
// preserve the existing bean definition and mark it as overloaded.
|
||||
if (existingBeanDef instanceof ConfigurationClassBeanDefinition ccbd) {
|
||||
if (ccbd.getMetadata().getClassName().equals(beanMethod.getConfigurationClass().getMetadata().getClassName()) &&
|
||||
ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) {
|
||||
ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName());
|
||||
if (ccbd.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) {
|
||||
if (ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) {
|
||||
ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName());
|
||||
}
|
||||
else if (!this.registry.isBeanDefinitionOverridable(beanName)) {
|
||||
throw new BeanDefinitionOverrideException(beanName,
|
||||
new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName),
|
||||
existingBeanDef,
|
||||
"@Bean method override with same bean name but different method name: " + existingBeanDef);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
@@ -329,9 +337,11 @@ class ConfigurationClassBeanDefinitionReader {
|
||||
|
||||
// At this point, it's a top-level override (probably XML), just having been parsed
|
||||
// before configuration class processing kicks in...
|
||||
if (this.registry instanceof DefaultListableBeanFactory dlbf && !dlbf.isBeanDefinitionOverridable(beanName)) {
|
||||
throw new BeanDefinitionStoreException(beanMethod.getConfigurationClass().getResource().getDescription(),
|
||||
beanName, "@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
|
||||
if (!this.registry.isBeanDefinitionOverridable(beanName)) {
|
||||
throw new BeanDefinitionOverrideException(beanName,
|
||||
new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName),
|
||||
existingBeanDef,
|
||||
"@Bean definition illegally overridden by existing bean definition: " + existingBeanDef);
|
||||
}
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug(String.format("Skipping bean definition for %s: a definition for bean '%s' " +
|
||||
|
||||
+26
-25
@@ -29,7 +29,8 @@ import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Declares that a field or method parameter should be formatted as a
|
||||
* {@link java.time.Duration}, according to the specified {@link #style style}.
|
||||
* {@link java.time.Duration}, according to the specified {@link #style Style}
|
||||
* and {@link #defaultUnit Unit}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 6.2
|
||||
@@ -40,20 +41,20 @@ import org.springframework.lang.Nullable;
|
||||
public @interface DurationFormat {
|
||||
|
||||
/**
|
||||
* The {@code Style} to use for parsing and printing a {@code Duration}.
|
||||
* The {@link Style} to use for parsing and printing a {@link Duration}.
|
||||
* <p>Defaults to the JDK style ({@link Style#ISO8601}).
|
||||
*/
|
||||
Style style() default Style.ISO8601;
|
||||
|
||||
/**
|
||||
* The {@link Unit} to fall back to in case the {@code style()} needs a unit
|
||||
* The {@link Unit} to fall back to in case the {@link #style Style} needs a unit
|
||||
* for either parsing or printing, and none is explicitly provided in the input.
|
||||
* <p>Defaults to {@link Unit#MILLIS} if unspecified.
|
||||
*/
|
||||
Unit defaultUnit() default Unit.MILLIS;
|
||||
|
||||
/**
|
||||
* Duration format styles.
|
||||
* {@link Duration} format styles.
|
||||
*/
|
||||
enum Style {
|
||||
|
||||
@@ -62,7 +63,7 @@ public @interface DurationFormat {
|
||||
* <p>Supported unit suffixes include: {@code ns, us, ms, s, m, h, d}.
|
||||
* Those correspond to nanoseconds, microseconds, milliseconds, seconds,
|
||||
* minutes, hours, and days, respectively.
|
||||
* <p>Note that when printing a {@code Duration}, this style can be
|
||||
* <p>Note that when printing a {@link Duration}, this style can be
|
||||
* lossy if the selected unit is bigger than the resolution of the
|
||||
* duration. For example, {@code Duration.ofMillis(5).plusNanos(1234)}
|
||||
* would get truncated to {@code "5ms"} when printing using
|
||||
@@ -73,7 +74,7 @@ public @interface DurationFormat {
|
||||
|
||||
/**
|
||||
* ISO-8601 formatting.
|
||||
* <p>This is what the JDK uses in {@link java.time.Duration#parse(CharSequence)}
|
||||
* <p>This is what the JDK uses in {@link Duration#parse(CharSequence)}
|
||||
* and {@link Duration#toString()}.
|
||||
*/
|
||||
ISO8601,
|
||||
@@ -90,11 +91,11 @@ public @interface DurationFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Duration format unit, which mirrors a subset of {@link ChronoUnit} and
|
||||
* {@link Duration} format unit, which mirrors a subset of {@link ChronoUnit} and
|
||||
* allows conversion to and from a supported {@code ChronoUnit} as well as
|
||||
* conversion from durations to longs.
|
||||
*
|
||||
* <p>The enum includes its corresponding suffix in the {@link Style#SIMPLE simple}
|
||||
* <p>The enum includes its corresponding suffix in the {@link Style#SIMPLE SIMPLE}
|
||||
* {@code Duration} format style.
|
||||
*/
|
||||
enum Unit {
|
||||
@@ -147,25 +148,24 @@ public @interface DurationFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this {@code DurationFormat.Unit} to its {@link ChronoUnit}
|
||||
* equivalent.
|
||||
* Convert this {@code Unit} to its {@link ChronoUnit} equivalent.
|
||||
*/
|
||||
public ChronoUnit asChronoUnit() {
|
||||
return this.chronoUnit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert this {@code DurationFormat.Unit} to a simple {@code String}
|
||||
* suffix, suitable for the {@link Style#SIMPLE SIMPLE} style.
|
||||
* Convert this {@code Unit} to a simple {@code String} suffix, suitable
|
||||
* for the {@link Style#SIMPLE SIMPLE} style.
|
||||
*/
|
||||
public String asSuffix() {
|
||||
return this.suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a {@code long} from a {@code String} and interpret it to be a
|
||||
* {@code Duration} in the current unit.
|
||||
* @param value the String representation of the long
|
||||
* Parse a {@code long} from the given {@link String} and interpret it to be a
|
||||
* {@link Duration} in the current unit.
|
||||
* @param value the {@code String} representation of the long
|
||||
* @return the corresponding {@code Duration}
|
||||
*/
|
||||
public Duration parse(String value) {
|
||||
@@ -173,11 +173,11 @@ public @interface DurationFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Print a {@code Duration} as a {@code String}, converting it to a long
|
||||
* Print the given {@link Duration} as a {@link String}, converting it to a long
|
||||
* value using this unit's precision via {@link #longValue(Duration)}
|
||||
* and appending this unit's simple {@link #asSuffix() suffix}.
|
||||
* @param value the {@code Duration} to convert to a String
|
||||
* @return the String representation of the {@code Duration} in the
|
||||
* @param value the {@code Duration} to convert to a {@code String}
|
||||
* @return the {@code String} representation of the {@code Duration} in the
|
||||
* {@link Style#SIMPLE SIMPLE} style
|
||||
*/
|
||||
public String print(Duration value) {
|
||||
@@ -185,11 +185,12 @@ public @interface DurationFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given {@code Duration} to a long value in the resolution
|
||||
* of this unit. Note that this can be lossy if the current unit is
|
||||
* bigger than the actual resolution of the duration.
|
||||
* <p>For example, {@code Duration.ofMillis(5).plusNanos(1234)} would
|
||||
* get truncated to {@code 5} for unit {@code MILLIS}.
|
||||
* Convert the given {@link Duration} to a long value in the resolution
|
||||
* of this unit.
|
||||
* <p>Note that this can be lossy if the current unit is bigger than the
|
||||
* actual resolution of the duration. For example,
|
||||
* {@code Duration.ofMillis(5).plusNanos(1234)} would get truncated to
|
||||
* {@code 5} for unit {@code MILLIS}.
|
||||
* @param value the {@code Duration} to convert to a long
|
||||
* @return the long value for the {@code Duration} in this {@code Unit}
|
||||
*/
|
||||
@@ -198,7 +199,7 @@ public @interface DurationFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code Unit} corresponding to the given {@code ChronoUnit}.
|
||||
* Get the {@link Unit} corresponding to the given {@link ChronoUnit}.
|
||||
* @throws IllegalArgumentException if the given {@code ChronoUnit} is
|
||||
* not supported
|
||||
*/
|
||||
@@ -215,7 +216,7 @@ public @interface DurationFormat {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@code Unit} corresponding to the given {@code String} suffix.
|
||||
* Get the {@link Unit} corresponding to the given {@link String} suffix.
|
||||
* @throws IllegalArgumentException if the given suffix is not supported
|
||||
*/
|
||||
public static Unit fromSuffix(String suffix) {
|
||||
|
||||
+13
-4
@@ -123,8 +123,9 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||
Publisher<?> publisher = getPublisherFor(method, targetBean);
|
||||
Supplier<ScheduledTaskObservationContext> contextSupplier =
|
||||
() -> new ScheduledTaskObservationContext(targetBean, method);
|
||||
String displayName = targetBean.getClass().getName() + "." + method.getName();
|
||||
return new SubscribingRunnable(publisher, shouldBlock, scheduled.scheduler(),
|
||||
subscriptionTrackerRegistry, observationRegistrySupplier, contextSupplier);
|
||||
subscriptionTrackerRegistry, displayName, observationRegistrySupplier, contextSupplier);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -192,6 +193,8 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||
|
||||
final boolean shouldBlock;
|
||||
|
||||
final String displayName;
|
||||
|
||||
@Nullable
|
||||
private final String qualifier;
|
||||
|
||||
@@ -202,12 +205,13 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||
final Supplier<ScheduledTaskObservationContext> contextSupplier;
|
||||
|
||||
SubscribingRunnable(Publisher<?> publisher, boolean shouldBlock,
|
||||
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
|
||||
Supplier<ObservationRegistry> observationRegistrySupplier,
|
||||
Supplier<ScheduledTaskObservationContext> contextSupplier) {
|
||||
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
|
||||
String displayName, Supplier<ObservationRegistry> observationRegistrySupplier,
|
||||
Supplier<ScheduledTaskObservationContext> contextSupplier) {
|
||||
|
||||
this.publisher = publisher;
|
||||
this.shouldBlock = shouldBlock;
|
||||
this.displayName = displayName;
|
||||
this.qualifier = qualifier;
|
||||
this.subscriptionTrackerRegistry = subscriptionTrackerRegistry;
|
||||
this.observationRegistrySupplier = observationRegistrySupplier;
|
||||
@@ -253,6 +257,11 @@ abstract class ScheduledAnnotationReactiveSupport {
|
||||
this.publisher.subscribe(subscriber);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ package org.springframework.scheduling.config;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.scheduling.SchedulingAwareRunnable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -68,7 +70,7 @@ public class Task {
|
||||
}
|
||||
|
||||
|
||||
private class OutcomeTrackingRunnable implements Runnable {
|
||||
private class OutcomeTrackingRunnable implements SchedulingAwareRunnable {
|
||||
|
||||
private final Runnable runnable;
|
||||
|
||||
@@ -89,6 +91,23 @@ public class Task {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLongLived() {
|
||||
if (this.runnable instanceof SchedulingAwareRunnable sar) {
|
||||
return sar.isLongLived();
|
||||
}
|
||||
return SchedulingAwareRunnable.super.isLongLived();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getQualifier() {
|
||||
if (this.runnable instanceof SchedulingAwareRunnable sar) {
|
||||
return sar.getQualifier();
|
||||
}
|
||||
return SchedulingAwareRunnable.super.getQualifier();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.runnable.toString();
|
||||
|
||||
+27
-17
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -18,7 +18,6 @@ package org.springframework.validation.beanvalidation;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -107,7 +106,7 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
|
||||
Set<Class<?>> validatedClasses = new HashSet<>();
|
||||
Set<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses = new HashSet<>();
|
||||
|
||||
processAheadOfTime(beanClass, validatedClasses, constraintValidatorClasses);
|
||||
processAheadOfTime(beanClass, new HashSet<>(), validatedClasses, constraintValidatorClasses);
|
||||
|
||||
if (!validatedClasses.isEmpty() || !constraintValidatorClasses.isEmpty()) {
|
||||
return new AotContribution(validatedClasses, constraintValidatorClasses);
|
||||
@@ -115,27 +114,38 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void processAheadOfTime(Class<?> clazz, Collection<Class<?>> validatedClasses,
|
||||
Collection<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
|
||||
private static void processAheadOfTime(Class<?> clazz, Set<Class<?>> visitedClasses, Set<Class<?>> validatedClasses,
|
||||
Set<Class<? extends ConstraintValidator<?, ?>>> constraintValidatorClasses) {
|
||||
|
||||
Assert.notNull(validator, "Validator can't be null");
|
||||
Assert.notNull(validator, "Validator cannot be null");
|
||||
|
||||
if (!visitedClasses.add(clazz)) {
|
||||
return;
|
||||
}
|
||||
|
||||
BeanDescriptor descriptor;
|
||||
try {
|
||||
descriptor = validator.getConstraintsForClass(clazz);
|
||||
}
|
||||
catch (RuntimeException ex) {
|
||||
catch (RuntimeException | LinkageError ex) {
|
||||
String className = clazz.getName();
|
||||
if (KotlinDetector.isKotlinType(clazz) && ex instanceof ArrayIndexOutOfBoundsException) {
|
||||
// See https://hibernate.atlassian.net/browse/HV-1796 and https://youtrack.jetbrains.com/issue/KT-40857
|
||||
logger.warn("Skipping validation constraint hint inference for class " + clazz +
|
||||
" due to an ArrayIndexOutOfBoundsException at validator level");
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Skipping validation constraint hint inference for class " + className +
|
||||
" due to an ArrayIndexOutOfBoundsException at validator level");
|
||||
}
|
||||
}
|
||||
else if (ex instanceof TypeNotPresentException) {
|
||||
logger.debug("Skipping validation constraint hint inference for class " +
|
||||
clazz + " due to a TypeNotPresentException at validator level: " + ex.getMessage());
|
||||
else if (ex instanceof TypeNotPresentException || ex instanceof NoClassDefFoundError) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping validation constraint hint inference for class %s due to a %s for %s"
|
||||
.formatted(className, ex.getClass().getSimpleName(), ex.getMessage()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
logger.warn("Skipping validation constraint hint inference for class " + clazz, ex);
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Skipping validation constraint hint inference for class " + className, ex);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -149,12 +159,12 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
|
||||
|
||||
ReflectionUtils.doWithFields(clazz, field -> {
|
||||
Class<?> type = field.getType();
|
||||
if (Iterable.class.isAssignableFrom(type) || List.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) {
|
||||
if (Iterable.class.isAssignableFrom(type) || Optional.class.isAssignableFrom(type)) {
|
||||
ResolvableType resolvableType = ResolvableType.forField(field);
|
||||
Class<?> genericType = resolvableType.getGeneric(0).toClass();
|
||||
if (shouldProcess(genericType)) {
|
||||
validatedClasses.add(clazz);
|
||||
processAheadOfTime(genericType, validatedClasses, constraintValidatorClasses);
|
||||
processAheadOfTime(genericType, visitedClasses, validatedClasses, constraintValidatorClasses);
|
||||
}
|
||||
}
|
||||
if (Map.class.isAssignableFrom(type)) {
|
||||
@@ -163,11 +173,11 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP
|
||||
Class<?> valueGenericType = resolvableType.getGeneric(1).toClass();
|
||||
if (shouldProcess(keyGenericType)) {
|
||||
validatedClasses.add(clazz);
|
||||
processAheadOfTime(keyGenericType, validatedClasses, constraintValidatorClasses);
|
||||
processAheadOfTime(keyGenericType, visitedClasses, validatedClasses, constraintValidatorClasses);
|
||||
}
|
||||
if (shouldProcess(valueGenericType)) {
|
||||
validatedClasses.add(clazz);
|
||||
processAheadOfTime(valueGenericType, validatedClasses, constraintValidatorClasses);
|
||||
processAheadOfTime(valueGenericType, visitedClasses, validatedClasses, constraintValidatorClasses);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
+9
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.validation.beanvalidation;
|
||||
|
||||
import jakarta.validation.ValidationException;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
/**
|
||||
@@ -39,7 +40,13 @@ public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {
|
||||
super.afterPropertiesSet();
|
||||
}
|
||||
catch (ValidationException ex) {
|
||||
LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
|
||||
Log logger = LogFactory.getLog(getClass());
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Failed to set up a Bean Validation provider", ex);
|
||||
}
|
||||
else if (logger.isInfoEnabled()) {
|
||||
logger.info("Failed to set up a Bean Validation provider: " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+31
-30
@@ -85,7 +85,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void aspectsAreApplied() {
|
||||
ClassPathXmlApplicationContext bf = newContext("aspects.xml");
|
||||
|
||||
ITestBean tb = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean tb = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(tb.getAge()).isEqualTo(68);
|
||||
MethodInvokingFactoryBean factoryBean = (MethodInvokingFactoryBean) bf.getBean("&factoryBean");
|
||||
assertThat(AopUtils.isAopProxy(factoryBean.getTargetObject())).isTrue();
|
||||
@@ -96,7 +96,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void multipleAspectsWithParameterApplied() {
|
||||
ClassPathXmlApplicationContext bf = newContext("aspects.xml");
|
||||
|
||||
ITestBean tb = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean tb = bf.getBean("adrian", ITestBean.class);
|
||||
tb.setAge(10);
|
||||
assertThat(tb.getAge()).isEqualTo(20);
|
||||
}
|
||||
@@ -105,7 +105,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void aspectsAreAppliedInDefinedOrder() {
|
||||
ClassPathXmlApplicationContext bf = newContext("aspectsWithOrdering.xml");
|
||||
|
||||
ITestBean tb = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean tb = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(tb.getAge()).isEqualTo(71);
|
||||
}
|
||||
|
||||
@@ -113,8 +113,8 @@ class AspectJAutoProxyCreatorTests {
|
||||
void aspectsAndAdvisorAreApplied() {
|
||||
ClassPathXmlApplicationContext ac = newContext("aspectsPlusAdvisor.xml");
|
||||
|
||||
ITestBean shouldBeWeaved = (ITestBean) ac.getBean("adrian");
|
||||
doTestAspectsAndAdvisorAreApplied(ac, shouldBeWeaved);
|
||||
ITestBean shouldBeWoven = ac.getBean("adrian", ITestBean.class);
|
||||
assertAspectsAndAdvisorAreApplied(ac, shouldBeWoven);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -124,20 +124,22 @@ class AspectJAutoProxyCreatorTests {
|
||||
GenericApplicationContext childAc = new GenericApplicationContext(ac);
|
||||
// Create a child factory with a bean that should be woven
|
||||
RootBeanDefinition bd = new RootBeanDefinition(TestBean.class);
|
||||
bd.getPropertyValues().addPropertyValue(new PropertyValue("name", "Adrian"))
|
||||
bd.getPropertyValues()
|
||||
.addPropertyValue(new PropertyValue("name", "Adrian"))
|
||||
.addPropertyValue(new PropertyValue("age", 34));
|
||||
childAc.registerBeanDefinition("adrian2", bd);
|
||||
// Register the advisor auto proxy creator with subclass
|
||||
childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(), new RootBeanDefinition(
|
||||
AnnotationAwareAspectJAutoProxyCreator.class));
|
||||
childAc.registerBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class.getName(),
|
||||
new RootBeanDefinition(AnnotationAwareAspectJAutoProxyCreator.class));
|
||||
childAc.refresh();
|
||||
|
||||
ITestBean beanFromChildContextThatShouldBeWeaved = (ITestBean) childAc.getBean("adrian2");
|
||||
//testAspectsAndAdvisorAreApplied(childAc, (ITestBean) ac.getBean("adrian"));
|
||||
doTestAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWeaved);
|
||||
ITestBean beanFromParentContextThatShouldBeWoven = ac.getBean("adrian", ITestBean.class);
|
||||
ITestBean beanFromChildContextThatShouldBeWoven = childAc.getBean("adrian2", ITestBean.class);
|
||||
assertAspectsAndAdvisorAreApplied(childAc, beanFromParentContextThatShouldBeWoven);
|
||||
assertAspectsAndAdvisorAreApplied(childAc, beanFromChildContextThatShouldBeWoven);
|
||||
}
|
||||
|
||||
protected void doTestAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWeaved) {
|
||||
protected void assertAspectsAndAdvisorAreApplied(ApplicationContext ac, ITestBean shouldBeWoven) {
|
||||
TestBeanAdvisor tba = (TestBeanAdvisor) ac.getBean("advisor");
|
||||
|
||||
MultiplyReturnValue mrv = (MultiplyReturnValue) ac.getBean("aspect");
|
||||
@@ -146,10 +148,10 @@ class AspectJAutoProxyCreatorTests {
|
||||
tba.count = 0;
|
||||
mrv.invocations = 0;
|
||||
|
||||
assertThat(AopUtils.isAopProxy(shouldBeWeaved)).as("Autoproxying must apply from @AspectJ aspect").isTrue();
|
||||
assertThat(shouldBeWeaved.getName()).isEqualTo("Adrian");
|
||||
assertThat(AopUtils.isAopProxy(shouldBeWoven)).as("Autoproxying must apply from @AspectJ aspect").isTrue();
|
||||
assertThat(shouldBeWoven.getName()).isEqualTo("Adrian");
|
||||
assertThat(mrv.invocations).isEqualTo(0);
|
||||
assertThat(shouldBeWeaved.getAge()).isEqualTo((34 * mrv.getMultiple()));
|
||||
assertThat(shouldBeWoven.getAge()).isEqualTo((34 * mrv.getMultiple()));
|
||||
assertThat(tba.count).as("Spring advisor must be invoked").isEqualTo(2);
|
||||
assertThat(mrv.invocations).as("Must be able to hold state in aspect").isEqualTo(1);
|
||||
}
|
||||
@@ -158,13 +160,13 @@ class AspectJAutoProxyCreatorTests {
|
||||
void perThisAspect() {
|
||||
ClassPathXmlApplicationContext bf = newContext("perthis.xml");
|
||||
|
||||
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
|
||||
|
||||
assertThat(adrian1.getAge()).isEqualTo(0);
|
||||
assertThat(adrian1.getAge()).isEqualTo(1);
|
||||
|
||||
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(adrian2).isNotSameAs(adrian1);
|
||||
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
|
||||
assertThat(adrian2.getAge()).isEqualTo(0);
|
||||
@@ -178,7 +180,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void perTargetAspect() throws SecurityException, NoSuchMethodException {
|
||||
ClassPathXmlApplicationContext bf = newContext("pertarget.xml");
|
||||
|
||||
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
|
||||
|
||||
// Does not trigger advice or count
|
||||
@@ -199,7 +201,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
adrian1.setName("Adrian");
|
||||
//assertEquals("Any other setter does not increment", 2, adrian1.getAge());
|
||||
|
||||
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(adrian2).isNotSameAs(adrian1);
|
||||
assertThat(AopUtils.isAopProxy(adrian1)).isTrue();
|
||||
assertThat(adrian2.getAge()).isEqualTo(34);
|
||||
@@ -239,7 +241,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void twoAdviceAspect() {
|
||||
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspect.xml");
|
||||
|
||||
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
|
||||
testAgeAspect(adrian1, 0, 2);
|
||||
}
|
||||
|
||||
@@ -247,9 +249,9 @@ class AspectJAutoProxyCreatorTests {
|
||||
void twoAdviceAspectSingleton() {
|
||||
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectSingleton.xml");
|
||||
|
||||
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
|
||||
testAgeAspect(adrian1, 0, 1);
|
||||
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(adrian2).isNotSameAs(adrian1);
|
||||
testAgeAspect(adrian2, 2, 1);
|
||||
}
|
||||
@@ -258,9 +260,9 @@ class AspectJAutoProxyCreatorTests {
|
||||
void twoAdviceAspectPrototype() {
|
||||
ClassPathXmlApplicationContext bf = newContext("twoAdviceAspectPrototype.xml");
|
||||
|
||||
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
|
||||
testAgeAspect(adrian1, 0, 1);
|
||||
ITestBean adrian2 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian2 = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(adrian2).isNotSameAs(adrian1);
|
||||
testAgeAspect(adrian2, 0, 1);
|
||||
}
|
||||
@@ -280,7 +282,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void adviceUsingJoinPoint() {
|
||||
ClassPathXmlApplicationContext bf = newContext("usesJoinPointAspect.xml");
|
||||
|
||||
ITestBean adrian1 = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian1 = bf.getBean("adrian", ITestBean.class);
|
||||
adrian1.getAge();
|
||||
AdviceUsingThisJoinPoint aspectInstance = (AdviceUsingThisJoinPoint) bf.getBean("aspect");
|
||||
//(AdviceUsingThisJoinPoint) Aspects.aspectOf(AdviceUsingThisJoinPoint.class);
|
||||
@@ -292,7 +294,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void includeMechanism() {
|
||||
ClassPathXmlApplicationContext bf = newContext("usesInclude.xml");
|
||||
|
||||
ITestBean adrian = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(AopUtils.isAopProxy(adrian)).isTrue();
|
||||
assertThat(adrian.getAge()).isEqualTo(68);
|
||||
}
|
||||
@@ -310,7 +312,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void withAbstractFactoryBeanAreApplied() {
|
||||
ClassPathXmlApplicationContext bf = newContext("aspectsWithAbstractBean.xml");
|
||||
|
||||
ITestBean adrian = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean adrian = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(AopUtils.isAopProxy(adrian)).isTrue();
|
||||
assertThat(adrian.getAge()).isEqualTo(68);
|
||||
}
|
||||
@@ -321,8 +323,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
|
||||
UnreliableBean bean = (UnreliableBean) bf.getBean("unreliableBean");
|
||||
RetryAspect aspect = (RetryAspect) bf.getBean("retryAspect");
|
||||
int attempts = bean.unreliable();
|
||||
assertThat(attempts).isEqualTo(2);
|
||||
assertThat(bean.unreliable()).isEqualTo(2);
|
||||
assertThat(aspect.getBeginCalls()).isEqualTo(2);
|
||||
assertThat(aspect.getRollbackCalls()).isEqualTo(1);
|
||||
assertThat(aspect.getCommitCalls()).isEqualTo(1);
|
||||
@@ -332,7 +333,7 @@ class AspectJAutoProxyCreatorTests {
|
||||
void withBeanNameAutoProxyCreator() {
|
||||
ClassPathXmlApplicationContext bf = newContext("withBeanNameAutoProxyCreator.xml");
|
||||
|
||||
ITestBean tb = (ITestBean) bf.getBean("adrian");
|
||||
ITestBean tb = bf.getBean("adrian", ITestBean.class);
|
||||
assertThat(tb.getAge()).isEqualTo(68);
|
||||
}
|
||||
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.aop.framework.autoproxy;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import org.aopalliance.aop.Advice;
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.aop.Pointcut;
|
||||
import org.springframework.aop.support.AbstractPointcutAdvisor;
|
||||
import org.springframework.aop.support.RootClassFilter;
|
||||
import org.springframework.aop.support.StaticMethodMatcherPointcut;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link DefaultAdvisorAutoProxyCreator}.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 6.2.1
|
||||
*/
|
||||
class DefaultAdvisorAutoProxyCreatorTests {
|
||||
|
||||
/**
|
||||
* Indirectly tests behavior of {@link org.springframework.aop.framework.AdvisedSupport.MethodCacheKey}.
|
||||
* @see StaticMethodMatcherPointcut#matches(Method, Class)
|
||||
*/
|
||||
@Test // gh-33915
|
||||
void staticMethodMatcherPointcutMatchesMethodIsNotInvokedAgainForActualMethodInvocation() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
DemoBean.class, DemoPointcutAdvisor.class, DefaultAdvisorAutoProxyCreator.class);
|
||||
DemoPointcutAdvisor demoPointcutAdvisor = context.getBean(DemoPointcutAdvisor.class);
|
||||
DemoBean demoBean = context.getBean(DemoBean.class);
|
||||
|
||||
assertThat(demoPointcutAdvisor.matchesInvocationCount).as("matches() invocations before").isEqualTo(2);
|
||||
// Invoke multiple times to ensure additional invocations don't affect the outcome.
|
||||
assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!");
|
||||
assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!");
|
||||
assertThat(demoBean.sayHello()).isEqualTo("Advised: Hello!");
|
||||
assertThat(demoPointcutAdvisor.matchesInvocationCount).as("matches() invocations after").isEqualTo(2);
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
|
||||
static class DemoBean {
|
||||
|
||||
public String sayHello() {
|
||||
return "Hello!";
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class DemoPointcutAdvisor extends AbstractPointcutAdvisor {
|
||||
|
||||
int matchesInvocationCount = 0;
|
||||
|
||||
@Override
|
||||
public Pointcut getPointcut() {
|
||||
StaticMethodMatcherPointcut pointcut = new StaticMethodMatcherPointcut() {
|
||||
|
||||
@Override
|
||||
public boolean matches(Method method, Class<?> targetClass) {
|
||||
if (method.getName().equals("sayHello")) {
|
||||
matchesInvocationCount++;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
pointcut.setClassFilter(new RootClassFilter(DemoBean.class));
|
||||
return pointcut;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Advice getAdvice() {
|
||||
return (MethodInterceptor) invocation -> "Advised: " + invocation.proceed();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+24
-16
@@ -110,7 +110,7 @@ class ConfigurationClassProcessingTests {
|
||||
|
||||
private void aliasesAreRespected(Class<?> testClass, Supplier<TestBean> testBeanSupplier, String beanName) {
|
||||
TestBean testBean = testBeanSupplier.get();
|
||||
BeanFactory factory = initBeanFactory(testClass);
|
||||
BeanFactory factory = initBeanFactory(false, testClass);
|
||||
|
||||
assertThat(factory.getBean(beanName)).isSameAs(testBean);
|
||||
Arrays.stream(factory.getAliases(beanName)).map(factory::getBean).forEach(alias -> assertThat(alias).isSameAs(testBean));
|
||||
@@ -141,30 +141,30 @@ class ConfigurationClassProcessingTests {
|
||||
@Test
|
||||
void finalBeanMethod() {
|
||||
assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() ->
|
||||
initBeanFactory(ConfigWithFinalBean.class));
|
||||
initBeanFactory(false, ConfigWithFinalBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void finalBeanMethodWithoutProxy() {
|
||||
initBeanFactory(ConfigWithFinalBeanWithoutProxy.class);
|
||||
initBeanFactory(false, ConfigWithFinalBeanWithoutProxy.class);
|
||||
}
|
||||
|
||||
@Test // gh-31007
|
||||
void voidBeanMethod() {
|
||||
assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() ->
|
||||
initBeanFactory(ConfigWithVoidBean.class));
|
||||
initBeanFactory(false, ConfigWithVoidBean.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void simplestPossibleConfig() {
|
||||
BeanFactory factory = initBeanFactory(SimplestPossibleConfig.class);
|
||||
BeanFactory factory = initBeanFactory(false, SimplestPossibleConfig.class);
|
||||
String stringBean = factory.getBean("stringBean", String.class);
|
||||
assertThat(stringBean).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void configWithObjectReturnType() {
|
||||
BeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class);
|
||||
BeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class);
|
||||
assertThat(factory.getType("stringBean")).isEqualTo(Object.class);
|
||||
assertThat(factory.isTypeMatch("stringBean", String.class)).isFalse();
|
||||
String stringBean = factory.getBean("stringBean", String.class);
|
||||
@@ -173,7 +173,7 @@ class ConfigurationClassProcessingTests {
|
||||
|
||||
@Test
|
||||
void configWithFactoryBeanReturnType() {
|
||||
ListableBeanFactory factory = initBeanFactory(ConfigWithNonSpecificReturnTypes.class);
|
||||
ListableBeanFactory factory = initBeanFactory(false, ConfigWithNonSpecificReturnTypes.class);
|
||||
assertThat(factory.getType("factoryBean")).isEqualTo(List.class);
|
||||
assertThat(factory.isTypeMatch("factoryBean", List.class)).isTrue();
|
||||
assertThat(factory.getType("&factoryBean")).isEqualTo(FactoryBean.class);
|
||||
@@ -201,7 +201,7 @@ class ConfigurationClassProcessingTests {
|
||||
|
||||
@Test
|
||||
void configurationWithPrototypeScopedBeans() {
|
||||
BeanFactory factory = initBeanFactory(ConfigWithPrototypeBean.class);
|
||||
BeanFactory factory = initBeanFactory(false, ConfigWithPrototypeBean.class);
|
||||
|
||||
TestBean foo = factory.getBean("foo", TestBean.class);
|
||||
ITestBean bar = factory.getBean("bar", ITestBean.class);
|
||||
@@ -213,7 +213,7 @@ class ConfigurationClassProcessingTests {
|
||||
|
||||
@Test
|
||||
void configurationWithNullReference() {
|
||||
BeanFactory factory = initBeanFactory(ConfigWithNullReference.class);
|
||||
BeanFactory factory = initBeanFactory(false, ConfigWithNullReference.class);
|
||||
|
||||
TestBean foo = factory.getBean("foo", TestBean.class);
|
||||
assertThat(factory.getBean("bar")).isEqualTo(null);
|
||||
@@ -223,7 +223,15 @@ class ConfigurationClassProcessingTests {
|
||||
@Test // gh-33330
|
||||
void configurationWithMethodNameMismatch() {
|
||||
assertThatExceptionOfType(BeanDefinitionOverrideException.class)
|
||||
.isThrownBy(() -> initBeanFactory(ConfigWithMethodNameMismatch.class));
|
||||
.isThrownBy(() -> initBeanFactory(false, ConfigWithMethodNameMismatch.class));
|
||||
}
|
||||
|
||||
@Test // gh-33920
|
||||
void configurationWithMethodNameMismatchAndOverridingAllowed() {
|
||||
BeanFactory factory = initBeanFactory(true, ConfigWithMethodNameMismatch.class);
|
||||
|
||||
SpousyTestBean foo = factory.getBean("foo", SpousyTestBean.class);
|
||||
assertThat(foo.getName()).isEqualTo("foo1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -353,13 +361,13 @@ class ConfigurationClassProcessingTests {
|
||||
* When complete, the factory is ready to service requests for any {@link Bean} methods
|
||||
* declared by {@code configClasses}.
|
||||
*/
|
||||
private DefaultListableBeanFactory initBeanFactory(Class<?>... configClasses) {
|
||||
private DefaultListableBeanFactory initBeanFactory(boolean allowOverriding, Class<?>... configClasses) {
|
||||
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
|
||||
for (Class<?> configClass : configClasses) {
|
||||
String configBeanName = configClass.getName();
|
||||
factory.registerBeanDefinition(configBeanName, new RootBeanDefinition(configClass));
|
||||
}
|
||||
factory.setAllowBeanDefinitionOverriding(false);
|
||||
factory.setAllowBeanDefinitionOverriding(allowOverriding);
|
||||
ConfigurationClassPostProcessor ccpp = new ConfigurationClassPostProcessor();
|
||||
ccpp.postProcessBeanDefinitionRegistry(factory);
|
||||
ccpp.postProcessBeanFactory(factory);
|
||||
@@ -537,12 +545,12 @@ class ConfigurationClassProcessingTests {
|
||||
@Configuration
|
||||
static class ConfigWithMethodNameMismatch {
|
||||
|
||||
@Bean(name = "foo") public TestBean foo() {
|
||||
return new SpousyTestBean("foo");
|
||||
@Bean(name = "foo") public TestBean foo1() {
|
||||
return new SpousyTestBean("foo1");
|
||||
}
|
||||
|
||||
@Bean(name = "foo") public TestBean fooX() {
|
||||
return new SpousyTestBean("fooX");
|
||||
@Bean(name = "foo") public TestBean foo2() {
|
||||
return new SpousyTestBean("foo2");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
-1
@@ -66,8 +66,10 @@ import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor;
|
||||
import org.springframework.context.annotation.ContextAnnotationAutowireCandidateResolver;
|
||||
import org.springframework.context.support.GenericApplicationContext;
|
||||
import org.springframework.context.support.GenericXmlApplicationContext;
|
||||
import org.springframework.context.testfixture.context.annotation.AutowiredCglibConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.AutowiredComponent;
|
||||
import org.springframework.context.testfixture.context.annotation.AutowiredGenericTemplate;
|
||||
import org.springframework.context.testfixture.context.annotation.AutowiredMixedCglibConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.CglibConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.ConfigurableCglibConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.GenericTemplateConfiguration;
|
||||
@@ -82,6 +84,7 @@ import org.springframework.context.testfixture.context.annotation.LazyResourceMe
|
||||
import org.springframework.context.testfixture.context.annotation.PropertySourceConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.QualifierConfiguration;
|
||||
import org.springframework.context.testfixture.context.annotation.ResourceComponent;
|
||||
import org.springframework.context.testfixture.context.annotation.ValueCglibConfiguration;
|
||||
import org.springframework.context.testfixture.context.generator.SimpleComponent;
|
||||
import org.springframework.core.env.ConfigurableEnvironment;
|
||||
import org.springframework.core.env.Environment;
|
||||
@@ -436,12 +439,14 @@ class ApplicationContextAotGeneratorTests {
|
||||
@CompileWithForkedClassLoader
|
||||
class ConfigurationClassCglibProxy {
|
||||
|
||||
private static final String CGLIB_CONFIGURATION_CLASS_SUFFIX = "$$SpringCGLIB$$0";
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeWhenHasCglibProxyWriteProxyAndGenerateReflectionHints() throws IOException {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.registerBean(CglibConfiguration.class);
|
||||
TestGenerationContext context = processAheadOfTime(applicationContext);
|
||||
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$0");
|
||||
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + CGLIB_CONFIGURATION_CLASS_SUFFIX);
|
||||
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$0");
|
||||
isRegisteredCglibClass(context, CglibConfiguration.class.getName() + "$$SpringCGLIB$$FastClass$$1");
|
||||
}
|
||||
@@ -453,6 +458,43 @@ class ApplicationContextAotGeneratorTests {
|
||||
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(context.getRuntimeHints());
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeExposeUserClassForCglibProxy() {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.registerBean("config", ValueCglibConfiguration.class);
|
||||
|
||||
testCompiledResult(applicationContext, (initializer, compiled) -> {
|
||||
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
|
||||
assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", ValueCglibConfiguration.class));
|
||||
assertThat(compiled.getSourceFile(".*ValueCglibConfiguration__BeanDefinitions"))
|
||||
.contains("new RootBeanDefinition(ValueCglibConfiguration.class)")
|
||||
.contains("new %s(".formatted(toCglibClassSimpleName(ValueCglibConfiguration.class)));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeUsesCglibClassForFactoryMethod() {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.registerBean("config", CglibConfiguration.class);
|
||||
|
||||
testCompiledResult(applicationContext, (initializer, compiled) -> {
|
||||
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(initializer);
|
||||
assertThat(freshApplicationContext).satisfies(hasBeanDefinitionOfBeanClass("config", CglibConfiguration.class));
|
||||
assertThat(compiled.getSourceFile(".*CglibConfiguration__BeanDefinitions"))
|
||||
.contains("new RootBeanDefinition(CglibConfiguration.class)")
|
||||
.contains(">forFactoryMethod(%s.class,".formatted(toCglibClassSimpleName(CglibConfiguration.class)))
|
||||
.doesNotContain(">forFactoryMethod(%s.class,".formatted(CglibConfiguration.class));
|
||||
});
|
||||
}
|
||||
|
||||
private Consumer<GenericApplicationContext> hasBeanDefinitionOfBeanClass(String name, Class<?> beanClass) {
|
||||
return context -> {
|
||||
assertThat(context.containsBean(name)).isTrue();
|
||||
assertThat(context.getBeanDefinition(name)).isInstanceOfSatisfying(RootBeanDefinition.class,
|
||||
rbd -> assertThat(rbd.getBeanClass()).isEqualTo(beanClass));
|
||||
};
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeWhenHasCglibProxyUseProxy() {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
@@ -464,6 +506,47 @@ class ApplicationContextAotGeneratorTests {
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeWhenHasCglibProxyAndAutowiring() {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.registerBean(AutowiredCglibConfiguration.class);
|
||||
testCompiledResult(applicationContext, (initializer, compiled) -> {
|
||||
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> {
|
||||
context.setEnvironment(new MockEnvironment().withProperty("hello", "Hi"));
|
||||
initializer.initialize(context);
|
||||
});
|
||||
assertThat(freshApplicationContext.getBean("text", String.class)).isEqualTo("Hi World");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeWhenHasCglibProxyAndMixedAutowiring() {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.registerBean(AutowiredMixedCglibConfiguration.class);
|
||||
testCompiledResult(applicationContext, (initializer, compiled) -> {
|
||||
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> {
|
||||
context.setEnvironment(new MockEnvironment().withProperty("hello", "Hi")
|
||||
.withProperty("world", "AOT World"));
|
||||
initializer.initialize(context);
|
||||
});
|
||||
assertThat(freshApplicationContext.getBean("text", String.class)).isEqualTo("Hi AOT World");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeWhenHasCglibProxyWithAnnotationsOnTheUserClasConstructor() {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
applicationContext.registerBean("config", ValueCglibConfiguration.class);
|
||||
testCompiledResult(applicationContext, (initializer, compiled) -> {
|
||||
GenericApplicationContext freshApplicationContext = toFreshApplicationContext(context -> {
|
||||
context.setEnvironment(new MockEnvironment().withProperty("name", "AOT World"));
|
||||
initializer.initialize(context);
|
||||
});
|
||||
assertThat(freshApplicationContext.getBean(ValueCglibConfiguration.class)
|
||||
.getName()).isEqualTo("AOT World");
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void processAheadOfTimeWhenHasCglibProxyWithArgumentsUseProxy() {
|
||||
GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext();
|
||||
@@ -487,6 +570,10 @@ class ApplicationContextAotGeneratorTests {
|
||||
.accepts(generationContext.getRuntimeHints());
|
||||
}
|
||||
|
||||
private String toCglibClassSimpleName(Class<?> configClass) {
|
||||
return configClass.getSimpleName() + CGLIB_CONFIGURATION_CLASS_SUFFIX;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -47,7 +47,7 @@ public class EventCollector {
|
||||
|
||||
/**
|
||||
* Return the events that the specified listener has received. The list of events
|
||||
* is ordered according to their reception order.
|
||||
* is ordered according to the order in which they were received.
|
||||
*/
|
||||
public List<Object> getEvents(Identifiable listener) {
|
||||
return this.content.get(listener.getId());
|
||||
|
||||
+14
-13
@@ -42,7 +42,7 @@ import static org.springframework.format.annotation.DurationFormat.Style.SIMPLE;
|
||||
class DurationFormatterUtilsTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(DurationFormat.Style.class)
|
||||
@EnumSource
|
||||
void parseEmptyStringFailsWithDedicatedException(DurationFormat.Style style) {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> DurationFormatterUtils.parse("", style))
|
||||
@@ -50,7 +50,7 @@ class DurationFormatterUtilsTests {
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@EnumSource(DurationFormat.Style.class)
|
||||
@EnumSource
|
||||
void parseNullStringFailsWithDedicatedException(DurationFormat.Style style) {
|
||||
assertThatIllegalArgumentException()
|
||||
.isThrownBy(() -> DurationFormatterUtils.parse(null, style))
|
||||
@@ -113,29 +113,30 @@ class DurationFormatterUtilsTests {
|
||||
|
||||
@Test
|
||||
void parseIsoNoChronoUnit() {
|
||||
//these are based on the examples given in Duration.parse
|
||||
// "PT20.345S" -- parses as "20.345 seconds"
|
||||
// These are based on the examples given in Duration.parse.
|
||||
|
||||
// "PT20.345S" -- parses as "20.345 seconds"
|
||||
assertThat(DurationFormatterUtils.parse("PT20.345S", ISO8601))
|
||||
.hasMillis(20345);
|
||||
// "PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
|
||||
// "PT15M" -- parses as "15 minutes" (where a minute is 60 seconds)
|
||||
assertThat(DurationFormatterUtils.parse("PT15M", ISO8601))
|
||||
.hasSeconds(15*60);
|
||||
// "PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
|
||||
// "PT10H" -- parses as "10 hours" (where an hour is 3600 seconds)
|
||||
assertThat(DurationFormatterUtils.parse("PT10H", ISO8601))
|
||||
.hasHours(10);
|
||||
// "P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
|
||||
// "P2D" -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
|
||||
assertThat(DurationFormatterUtils.parse("P2D", ISO8601))
|
||||
.hasDays(2);
|
||||
// "P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes"
|
||||
// "P2DT3H4M" -- parses as "2 days, 3 hours and 4 minutes"
|
||||
assertThat(DurationFormatterUtils.parse("P2DT3H4M", ISO8601))
|
||||
.isEqualTo(Duration.ofDays(2).plusHours(3).plusMinutes(4));
|
||||
// "PT-6H3M" -- parses as "-6 hours and +3 minutes"
|
||||
// "PT-6H3M" -- parses as "-6 hours and +3 minutes"
|
||||
assertThat(DurationFormatterUtils.parse("PT-6H3M", ISO8601))
|
||||
.isEqualTo(Duration.ofHours(-6).plusMinutes(3));
|
||||
// "-PT6H3M" -- parses as "-6 hours and -3 minutes"
|
||||
// "-PT6H3M" -- parses as "-6 hours and -3 minutes"
|
||||
assertThat(DurationFormatterUtils.parse("-PT6H3M", ISO8601))
|
||||
.isEqualTo(Duration.ofHours(-6).plusMinutes(-3));
|
||||
// "-PT-6H+3M" -- parses as "+6 hours and -3 minutes"
|
||||
// "-PT-6H+3M" -- parses as "+6 hours and -3 minutes"
|
||||
assertThat(DurationFormatterUtils.parse("-PT-6H+3M", ISO8601))
|
||||
.isEqualTo(Duration.ofHours(6).plusMinutes(-3));
|
||||
}
|
||||
@@ -189,7 +190,7 @@ class DurationFormatterUtilsTests {
|
||||
.isEqualTo(Duration.ofMinutes(34).plusSeconds(57));
|
||||
}
|
||||
|
||||
@Test //Kotlin style compatibility
|
||||
@Test // Kotlin style compatibility
|
||||
void parseCompositeNegativeWithSpacesAndParenthesis() {
|
||||
assertThat(DurationFormatterUtils.parse("-(34m 57s)", COMPOSITE))
|
||||
.isEqualTo(Duration.ofMinutes(-34).plusSeconds(-57));
|
||||
@@ -315,7 +316,7 @@ class DurationFormatterUtilsTests {
|
||||
|
||||
assertThat(DurationFormatterUtils.detect("-(1d 2h 34m 2ns)"))
|
||||
.as("COMPOSITE")
|
||||
.isEqualTo(COMPOSITE);
|
||||
.isEqualTo(COMPOSITE);
|
||||
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> DurationFormatterUtils.detect("WPT2H-4M"))
|
||||
.withMessage("'WPT2H-4M' is not a valid duration, cannot detect any known style")
|
||||
|
||||
+1
-2
@@ -175,8 +175,7 @@ class ScheduledAnnotationBeanPostProcessorObservabilityTests {
|
||||
}
|
||||
|
||||
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatTaskObservation() {
|
||||
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
|
||||
.hasObservationWithNameEqualTo("tasks.scheduled.execution").that();
|
||||
return assertThat(this.observationRegistry).hasObservationWithNameEqualTo("tasks.scheduled.execution").that();
|
||||
}
|
||||
|
||||
|
||||
|
||||
+12
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -173,6 +173,17 @@ class ScheduledAnnotationReactiveSupportTests {
|
||||
assertThat(p).hasToString("checkpoint(\"@Scheduled 'mono()' in 'org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupportTests$ReactiveMethods'\")");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProvideToString() {
|
||||
ReactiveMethods target = new ReactiveMethods();
|
||||
Method m = ReflectionUtils.findMethod(ReactiveMethods.class, "mono");
|
||||
Scheduled cron = AnnotationUtils.synthesizeAnnotation(Map.of("cron", "-"), Scheduled.class, null);
|
||||
List<Runnable> tracker = new ArrayList<>();
|
||||
|
||||
assertThat(createSubscriptionRunnable(m, target, cron, () -> ObservationRegistry.NOOP, tracker))
|
||||
.hasToString("org.springframework.scheduling.annotation.ScheduledAnnotationReactiveSupportTests$ReactiveMethods.mono");
|
||||
}
|
||||
|
||||
|
||||
static class ReactiveMethods {
|
||||
|
||||
|
||||
+9
-17
@@ -125,7 +125,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
@SuppressWarnings("removal")
|
||||
void submitListenableRunnable() {
|
||||
TestTask task = new TestTask(this.testName, 1);
|
||||
// Act
|
||||
@@ -156,7 +156,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
@SuppressWarnings("removal")
|
||||
void submitFailingListenableRunnable() {
|
||||
TestTask task = new TestTask(this.testName, 0);
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future = executor.submitListenable(task);
|
||||
@@ -185,7 +185,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
@SuppressWarnings("removal")
|
||||
void submitListenableRunnableWithGetAfterShutdown() throws Exception {
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future1 = executor.submitListenable(new TestTask(this.testName, -1));
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future2 = executor.submitListenable(new TestTask(this.testName, -1));
|
||||
@@ -209,18 +209,10 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
CompletableFuture<?> future1 = executor.submitCompletable(new TestTask(this.testName, -1));
|
||||
CompletableFuture<?> future2 = executor.submitCompletable(new TestTask(this.testName, -1));
|
||||
shutdownExecutor();
|
||||
|
||||
try {
|
||||
assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> {
|
||||
future1.get(1000, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// ignore
|
||||
}
|
||||
Awaitility.await()
|
||||
.atMost(5, TimeUnit.SECONDS)
|
||||
.pollInterval(10, TimeUnit.MILLISECONDS)
|
||||
.untilAsserted(() -> assertThatExceptionOfType(TimeoutException.class)
|
||||
.isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS)));
|
||||
future2.get(1000, TimeUnit.MILLISECONDS);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -260,7 +252,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
@SuppressWarnings("removal")
|
||||
void submitListenableCallable() {
|
||||
TestCallable task = new TestCallable(this.testName, 1);
|
||||
// Act
|
||||
@@ -275,7 +267,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
@SuppressWarnings("removal")
|
||||
void submitFailingListenableCallable() {
|
||||
TestCallable task = new TestCallable(this.testName, 0);
|
||||
// Act
|
||||
@@ -291,7 +283,7 @@ abstract class AbstractSchedulingTaskExecutorTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
@SuppressWarnings("removal")
|
||||
void submitListenableCallableWithGetAfterShutdown() throws Exception {
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future1 = executor.submitListenable(new TestCallable(this.testName, -1));
|
||||
org.springframework.util.concurrent.ListenableFuture<?> future2 = executor.submitListenable(new TestCallable(this.testName, -1));
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
|
||||
package org.springframework.scheduling.config;
|
||||
|
||||
import io.micrometer.observation.tck.TestObservationRegistry;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.scheduling.SchedulingAwareRunnable;
|
||||
import org.springframework.scheduling.support.ScheduledMethodRunnable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
@@ -72,6 +76,18 @@ class TaskTests {
|
||||
assertThat(executionOutcome.throwable()).isInstanceOf(IllegalStateException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDelegateToSchedulingAwareRunnable() throws Exception {
|
||||
ScheduledMethodRunnable methodRunnable = new ScheduledMethodRunnable(new TestRunnable(),
|
||||
TestRunnable.class.getMethod("run"), "myScheduler", TestObservationRegistry::create);
|
||||
Task task = new Task(methodRunnable);
|
||||
|
||||
assertThat(task.getRunnable()).isInstanceOf(SchedulingAwareRunnable.class);
|
||||
SchedulingAwareRunnable actual = (SchedulingAwareRunnable) task.getRunnable();
|
||||
assertThat(actual.getQualifier()).isEqualTo(methodRunnable.getQualifier());
|
||||
assertThat(actual.isLongLived()).isEqualTo(methodRunnable.isLongLived());
|
||||
}
|
||||
|
||||
|
||||
static class TestRunnable implements Runnable {
|
||||
|
||||
|
||||
+62
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,8 @@ import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
@@ -31,6 +33,8 @@ import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import org.hibernate.validator.internal.constraintvalidators.bv.PatternValidator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.aot.generate.GenerationContext;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
@@ -40,6 +44,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationAotContribution;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.RegisteredBean;
|
||||
import org.springframework.beans.factory.support.RootBeanDefinition;
|
||||
import org.springframework.core.OverridingClassLoader;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
|
||||
@@ -121,6 +126,23 @@ class BeanValidationBeanRegistrationAotProcessorTests {
|
||||
.withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints());
|
||||
}
|
||||
|
||||
@ParameterizedTest // gh-33936
|
||||
@ValueSource(classes = {BeanWithRecursiveIterable.class, BeanWithRecursiveMap.class, BeanWithRecursiveOptional.class})
|
||||
void shouldProcessRecursiveGenericsWithoutInfiniteRecursion(Class<?> beanClass) {
|
||||
process(beanClass);
|
||||
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(1);
|
||||
assertThat(RuntimeHintsPredicates.reflection().onType(beanClass)
|
||||
.withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints());
|
||||
}
|
||||
|
||||
@Test // gh-33940
|
||||
void shouldSkipConstraintWithMissingDependency() throws Exception {
|
||||
MissingDependencyClassLoader classLoader = new MissingDependencyClassLoader(getClass().getClassLoader());
|
||||
Class<?> beanClass = classLoader.loadClass(ConstraintWithMissingDependency.class.getName());
|
||||
process(beanClass);
|
||||
assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).isEmpty();
|
||||
}
|
||||
|
||||
private void process(Class<?> beanClass) {
|
||||
BeanRegistrationAotContribution contribution = createContribution(beanClass);
|
||||
if (contribution != null) {
|
||||
@@ -244,4 +266,43 @@ class BeanValidationBeanRegistrationAotProcessorTests {
|
||||
}
|
||||
}
|
||||
|
||||
static class BeanWithRecursiveIterable {
|
||||
Iterable<BeanWithRecursiveIterable> iterable;
|
||||
}
|
||||
|
||||
static class BeanWithRecursiveMap {
|
||||
Map<BeanWithRecursiveMap, BeanWithRecursiveMap> map;
|
||||
}
|
||||
|
||||
static class BeanWithRecursiveOptional {
|
||||
Optional<BeanWithRecursiveOptional> optional;
|
||||
}
|
||||
|
||||
static class ConstraintWithMissingDependency {
|
||||
|
||||
MissingType missingType;
|
||||
}
|
||||
|
||||
static class MissingType {}
|
||||
|
||||
static class MissingDependencyClassLoader extends OverridingClassLoader {
|
||||
|
||||
MissingDependencyClassLoader(ClassLoader parent) {
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isEligibleForOverriding(String className) {
|
||||
return className.startsWith(BeanValidationBeanRegistrationAotProcessorTests.class.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClassForOverriding(String name) throws ClassNotFoundException {
|
||||
if (name.contains("MissingType")) {
|
||||
throw new NoClassDefFoundError(name);
|
||||
}
|
||||
return super.loadClassForOverriding(name);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.context.annotation;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
@Configuration
|
||||
public class AutowiredCglibConfiguration {
|
||||
|
||||
@Autowired
|
||||
private Environment environment;
|
||||
|
||||
@Bean
|
||||
public String text() {
|
||||
return this.environment.getProperty("hello") + " World";
|
||||
}
|
||||
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.context.annotation;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
@Configuration
|
||||
public class AutowiredMixedCglibConfiguration {
|
||||
|
||||
@Value("${world:World}")
|
||||
private String world;
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
public AutowiredMixedCglibConfiguration(Environment environment) {
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public String text() {
|
||||
return this.environment.getProperty("hello") + " " + this.world;
|
||||
}
|
||||
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.context.testfixture.context.annotation;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ValueCglibConfiguration {
|
||||
|
||||
private final String name;
|
||||
|
||||
public ValueCglibConfiguration(@Value("${name:World}") String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -124,6 +124,14 @@ public enum MemberCategory {
|
||||
* reflection for inner classes but rather makes sure they are available
|
||||
* via a call to {@link Class#getDeclaredClasses}.
|
||||
*/
|
||||
DECLARED_CLASSES
|
||||
DECLARED_CLASSES,
|
||||
|
||||
/**
|
||||
* A category that represents the need for
|
||||
* {@link sun.misc.Unsafe#allocateInstance(Class) unsafe allocation}
|
||||
* for this type.
|
||||
* @since 6.2.1
|
||||
*/
|
||||
UNSAFE_ALLOCATED
|
||||
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ class ReflectionHintsWriter {
|
||||
attributes.put("allDeclaredMethods", true);
|
||||
case PUBLIC_CLASSES -> attributes.put("allPublicClasses", true);
|
||||
case DECLARED_CLASSES -> attributes.put("allDeclaredClasses", true);
|
||||
case UNSAFE_ALLOCATED -> attributes.put("unsafeAllocated", true);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -44,7 +44,6 @@ import kotlinx.coroutines.reactor.ReactorFlowKt;
|
||||
import org.reactivestreams.Publisher;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.core.publisher.SynchronousSink;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
@@ -109,7 +108,7 @@ public abstract class CoroutinesUtils {
|
||||
* @throws IllegalArgumentException if {@code method} is not a suspending function
|
||||
* @since 6.0
|
||||
*/
|
||||
@SuppressWarnings({"deprecation", "DataFlowIssue", "NullAway"})
|
||||
@SuppressWarnings({"DataFlowIssue", "NullAway"})
|
||||
public static Publisher<?> invokeSuspendingFunction(
|
||||
CoroutineContext context, Method method, @Nullable Object target, @Nullable Object... args) {
|
||||
|
||||
@@ -146,7 +145,7 @@ public abstract class CoroutinesUtils {
|
||||
}
|
||||
return KCallables.callSuspendBy(function, argMap, continuation);
|
||||
})
|
||||
.handle(CoroutinesUtils::handleResult)
|
||||
.filter(result -> result != Unit.INSTANCE)
|
||||
.onErrorMap(InvocationTargetException.class, InvocationTargetException::getTargetException);
|
||||
|
||||
KType returnType = function.getReturnType();
|
||||
@@ -166,22 +165,4 @@ public abstract class CoroutinesUtils {
|
||||
return ReactorFlowKt.asFlux(((Flow<?>) flow));
|
||||
}
|
||||
|
||||
private static void handleResult(Object result, SynchronousSink<Object> sink) {
|
||||
if (result == Unit.INSTANCE) {
|
||||
sink.complete();
|
||||
}
|
||||
else if (KotlinDetector.isInlineClass(result.getClass())) {
|
||||
try {
|
||||
sink.next(result.getClass().getDeclaredMethod("unbox-impl").invoke(result));
|
||||
sink.complete();
|
||||
}
|
||||
catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException ex) {
|
||||
sink.error(ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
sink.next(result);
|
||||
sink.complete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,19 +334,19 @@ public class ResolvableType implements Serializable {
|
||||
|
||||
// Deal with wildcard bounds
|
||||
WildcardBounds ourBounds = WildcardBounds.get(this);
|
||||
WildcardBounds typeBounds = WildcardBounds.get(other);
|
||||
WildcardBounds otherBounds = WildcardBounds.get(other);
|
||||
|
||||
// In the form X is assignable to <? extends Number>
|
||||
if (typeBounds != null) {
|
||||
if (otherBounds != null) {
|
||||
if (ourBounds != null) {
|
||||
return (ourBounds.isSameKind(typeBounds) &&
|
||||
ourBounds.isAssignableFrom(typeBounds.getBounds(), matchedBefore));
|
||||
return (ourBounds.isSameKind(otherBounds) &&
|
||||
ourBounds.isAssignableFrom(otherBounds.getBounds(), matchedBefore));
|
||||
}
|
||||
else if (upUntilUnresolvable) {
|
||||
return typeBounds.isAssignableFrom(this, matchedBefore);
|
||||
return otherBounds.isAssignableFrom(this, matchedBefore);
|
||||
}
|
||||
else if (!exactMatch) {
|
||||
return typeBounds.isAssignableTo(this, matchedBefore);
|
||||
return otherBounds.isAssignableTo(this, matchedBefore);
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
@@ -400,8 +400,8 @@ public class ResolvableType implements Serializable {
|
||||
if (checkGenerics) {
|
||||
// Recursively check each generic
|
||||
ResolvableType[] ourGenerics = getGenerics();
|
||||
ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics();
|
||||
if (ourGenerics.length != typeGenerics.length) {
|
||||
ResolvableType[] otherGenerics = other.as(ourResolved).getGenerics();
|
||||
if (ourGenerics.length != otherGenerics.length) {
|
||||
return false;
|
||||
}
|
||||
if (ourGenerics.length > 0) {
|
||||
@@ -410,7 +410,8 @@ public class ResolvableType implements Serializable {
|
||||
}
|
||||
matchedBefore.put(this.type, other.type);
|
||||
for (int i = 0; i < ourGenerics.length; i++) {
|
||||
if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], true, matchedBefore, upUntilUnresolvable)) {
|
||||
if (!ourGenerics[i].isAssignableFrom(otherGenerics[i],
|
||||
!other.hasUnresolvableGenerics(), matchedBefore, upUntilUnresolvable)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ import org.springframework.lang.Contract;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
/**
|
||||
* Contextual descriptor about a type to convert from or to.
|
||||
@@ -501,16 +500,7 @@ public class TypeDescriptor implements Serializable {
|
||||
if (!annotationsMatch(otherDesc)) {
|
||||
return false;
|
||||
}
|
||||
if (isCollection() || isArray()) {
|
||||
return ObjectUtils.nullSafeEquals(getElementTypeDescriptor(), otherDesc.getElementTypeDescriptor());
|
||||
}
|
||||
else if (isMap()) {
|
||||
return (ObjectUtils.nullSafeEquals(getMapKeyTypeDescriptor(), otherDesc.getMapKeyTypeDescriptor()) &&
|
||||
ObjectUtils.nullSafeEquals(getMapValueTypeDescriptor(), otherDesc.getMapValueTypeDescriptor()));
|
||||
}
|
||||
else {
|
||||
return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics());
|
||||
}
|
||||
return Arrays.equals(getResolvableType().getGenerics(), otherDesc.getResolvableType().getGenerics());
|
||||
}
|
||||
|
||||
private boolean annotationsMatch(TypeDescriptor otherDesc) {
|
||||
|
||||
+2
-2
@@ -978,8 +978,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
|
||||
}
|
||||
|
||||
if (!Files.exists(rootPath)) {
|
||||
if (logger.isInfoEnabled()) {
|
||||
logger.info("Skipping search for files matching pattern [%s]: directory [%s] does not exist"
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Skipping search for files matching pattern [%s]: directory [%s] does not exist"
|
||||
.formatted(subPattern, rootPath.toAbsolutePath()));
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -26,7 +26,9 @@ import java.util.function.Consumer;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Composite map that combines two other maps. This type is created via
|
||||
* Composite map that combines two other maps.
|
||||
*
|
||||
* <p>This type is created via
|
||||
* {@link CollectionUtils#compositeMap(Map, Map, BiFunction, Consumer)}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -98,13 +98,11 @@ public class FastByteArrayOutputStream extends OutputStream {
|
||||
if (this.closed) {
|
||||
throw new IOException("Stream closed");
|
||||
}
|
||||
else {
|
||||
if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
|
||||
addBuffer(1);
|
||||
}
|
||||
// store the byte
|
||||
this.buffers.getLast()[this.index++] = (byte) datum;
|
||||
if (this.buffers.peekLast() == null || this.buffers.getLast().length == this.index) {
|
||||
addBuffer(1);
|
||||
}
|
||||
// store the byte
|
||||
this.buffers.getLast()[this.index++] = (byte) datum;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -384,22 +382,20 @@ public class FastByteArrayOutputStream extends OutputStream {
|
||||
// This stream doesn't have any data in it...
|
||||
return -1;
|
||||
}
|
||||
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
|
||||
this.totalBytesRead++;
|
||||
return this.currentBuffer[this.nextIndexInCurrentBuffer++] & 0xFF;
|
||||
}
|
||||
else {
|
||||
if (this.nextIndexInCurrentBuffer < this.currentBufferLength) {
|
||||
this.totalBytesRead++;
|
||||
return this.currentBuffer[this.nextIndexInCurrentBuffer++] & 0xFF;
|
||||
if (this.buffersIterator.hasNext()) {
|
||||
this.currentBuffer = this.buffersIterator.next();
|
||||
updateCurrentBufferLength();
|
||||
this.nextIndexInCurrentBuffer = 0;
|
||||
}
|
||||
else {
|
||||
if (this.buffersIterator.hasNext()) {
|
||||
this.currentBuffer = this.buffersIterator.next();
|
||||
updateCurrentBufferLength();
|
||||
this.nextIndexInCurrentBuffer = 0;
|
||||
}
|
||||
else {
|
||||
this.currentBuffer = null;
|
||||
}
|
||||
return read();
|
||||
this.currentBuffer = null;
|
||||
}
|
||||
return read();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,8 +23,9 @@ import java.util.function.Predicate;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* Iterator that filters out values that do not match a predicate.
|
||||
* This type is used by {@link CompositeMap}.
|
||||
* {@link Iterator} that filters out values that do not match a predicate.
|
||||
*
|
||||
* <p>This type is used by {@link CompositeMap}.
|
||||
*
|
||||
* @author Arjen Poutsma
|
||||
* @since 6.2
|
||||
@@ -39,7 +40,7 @@ final class FilteredIterator<E> implements Iterator<E> {
|
||||
@Nullable
|
||||
private E next;
|
||||
|
||||
private boolean nextSet;
|
||||
private boolean hasNext;
|
||||
|
||||
|
||||
public FilteredIterator(Iterator<E> delegate, Predicate<E> filter) {
|
||||
@@ -52,22 +53,15 @@ final class FilteredIterator<E> implements Iterator<E> {
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
if (this.nextSet) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return setNext();
|
||||
}
|
||||
return (this.hasNext || setNext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public E next() {
|
||||
if (!this.nextSet) {
|
||||
if (!setNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
if (!this.hasNext && !setNext()) {
|
||||
throw new NoSuchElementException();
|
||||
}
|
||||
this.nextSet = false;
|
||||
this.hasNext = false;
|
||||
Assert.state(this.next != null, "Next should not be null");
|
||||
return this.next;
|
||||
}
|
||||
@@ -77,7 +71,7 @@ final class FilteredIterator<E> implements Iterator<E> {
|
||||
E next = this.delegate.next();
|
||||
if (this.filter.test(next)) {
|
||||
this.next = next;
|
||||
this.nextSet = true;
|
||||
this.hasNext = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -226,14 +226,12 @@ public class MethodInvoker {
|
||||
Method matchingMethod = null;
|
||||
|
||||
for (Method candidate : candidates) {
|
||||
if (candidate.getName().equals(targetMethod)) {
|
||||
if (candidate.getParameterCount() == argCount) {
|
||||
Class<?>[] paramTypes = candidate.getParameterTypes();
|
||||
int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments);
|
||||
if (typeDiffWeight < minTypeDiffWeight) {
|
||||
minTypeDiffWeight = typeDiffWeight;
|
||||
matchingMethod = candidate;
|
||||
}
|
||||
if (candidate.getName().equals(targetMethod) && candidate.getParameterCount() == argCount) {
|
||||
Class<?>[] paramTypes = candidate.getParameterTypes();
|
||||
int typeDiffWeight = getTypeDifferenceWeight(paramTypes, arguments);
|
||||
if (typeDiffWeight < minTypeDiffWeight) {
|
||||
minTypeDiffWeight = typeDiffWeight;
|
||||
matchingMethod = candidate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,9 +243,7 @@ public class MimeType implements Comparable<MimeType>, Serializable {
|
||||
if (s.length() < 2) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));
|
||||
}
|
||||
return ((s.startsWith("\"") && s.endsWith("\"")) || (s.startsWith("'") && s.endsWith("'")));
|
||||
}
|
||||
|
||||
protected String unquote(String s) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -110,10 +110,8 @@ public abstract class NumberUtils {
|
||||
// do not lose precision - use BigDecimal's own conversion
|
||||
return (T) bigDecimal.toBigInteger();
|
||||
}
|
||||
else {
|
||||
// original value is not a Big* number - use standard long conversion
|
||||
return (T) BigInteger.valueOf(number.longValue());
|
||||
}
|
||||
// original value is not a Big* number - use standard long conversion
|
||||
return (T) BigInteger.valueOf(number.longValue());
|
||||
}
|
||||
else if (Float.class == targetClass) {
|
||||
return (T) Float.valueOf(number.floatValue());
|
||||
|
||||
@@ -138,6 +138,7 @@ public abstract class ObjectUtils {
|
||||
* @see CollectionUtils#isEmpty(java.util.Collection)
|
||||
* @see CollectionUtils#isEmpty(java.util.Map)
|
||||
*/
|
||||
@Contract("null -> true")
|
||||
public static boolean isEmpty(@Nullable Object obj) {
|
||||
if (obj == null) {
|
||||
return true;
|
||||
@@ -702,8 +703,7 @@ public abstract class ObjectUtils {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
if (array.length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
@@ -726,8 +726,7 @@ public abstract class ObjectUtils {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
if (array.length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
@@ -750,8 +749,7 @@ public abstract class ObjectUtils {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
if (array.length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
@@ -774,8 +772,7 @@ public abstract class ObjectUtils {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
if (array.length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
@@ -798,8 +795,7 @@ public abstract class ObjectUtils {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
if (array.length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
@@ -846,8 +842,7 @@ public abstract class ObjectUtils {
|
||||
if (array == null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
int length = array.length;
|
||||
if (length == 0) {
|
||||
if (array.length == 0) {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
StringJoiner stringJoiner = new StringJoiner(ARRAY_ELEMENT_SEPARATOR, ARRAY_START, ARRAY_END);
|
||||
|
||||
@@ -247,7 +247,7 @@ final class PlaceholderParser {
|
||||
if (!parts.isEmpty()) {
|
||||
Part current = parts.removeLast();
|
||||
if (current instanceof TextPart textPart) {
|
||||
parts.add(new TextPart(textPart.text + text));
|
||||
parts.add(new TextPart(textPart.text() + text));
|
||||
}
|
||||
else {
|
||||
parts.add(current);
|
||||
@@ -420,51 +420,42 @@ final class PlaceholderParser {
|
||||
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that does not contain a valid placeholder.
|
||||
* @param text the raw (and resolved) text
|
||||
* A base {@link Part} implementation.
|
||||
*/
|
||||
record TextPart(String text) implements Part {
|
||||
abstract static class AbstractPart implements Part {
|
||||
|
||||
private final String text;
|
||||
|
||||
protected AbstractPart(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
public String text() {
|
||||
return this.text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that represents a single placeholder with
|
||||
* a hard-coded fallback.
|
||||
* @param text the raw text
|
||||
* @param key the key of the placeholder
|
||||
* @param fallback the fallback to use, if any
|
||||
*/
|
||||
record SimplePlaceholderPart(String text, String key, @Nullable String fallback) implements Part {
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
String resolvedValue = resolveToText(resolutionContext, this.key);
|
||||
if (resolvedValue != null) {
|
||||
return resolvedValue;
|
||||
}
|
||||
else if (this.fallback != null) {
|
||||
return this.fallback;
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, this.text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the placeholder with the given {@code key}. If the result of such
|
||||
* resolution return other placeholders, those are resolved as well until the
|
||||
* resolution no longer contains any placeholders.
|
||||
* @param resolutionContext the resolution context to use
|
||||
* @param key the initial placeholder
|
||||
* @return the full resolution of the given {@code key} or {@code null} if
|
||||
* the placeholder has no value to begin with
|
||||
*/
|
||||
@Nullable
|
||||
private String resolveToText(PartResolutionContext resolutionContext, String text) {
|
||||
String resolvedValue = resolutionContext.resolvePlaceholder(text);
|
||||
protected String resolveRecursively(PartResolutionContext resolutionContext, String key) {
|
||||
String resolvedValue = resolutionContext.resolvePlaceholder(key);
|
||||
if (resolvedValue != null) {
|
||||
resolutionContext.flagPlaceholderAsVisited(text);
|
||||
resolutionContext.flagPlaceholderAsVisited(key);
|
||||
// Let's check if we need to recursively resolve that value
|
||||
List<Part> nestedParts = resolutionContext.parse(resolvedValue);
|
||||
String value = toText(nestedParts);
|
||||
if (!isTextOnly(nestedParts)) {
|
||||
value = new ParsedValue(resolvedValue, nestedParts).resolve(resolutionContext);
|
||||
}
|
||||
resolutionContext.removePlaceholder(text);
|
||||
resolutionContext.removePlaceholder(key);
|
||||
return value;
|
||||
}
|
||||
// Not found
|
||||
@@ -483,26 +474,97 @@ final class PlaceholderParser {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that does not contain a valid placeholder.
|
||||
*/
|
||||
static class TextPart extends AbstractPart {
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
* @param text the raw (and resolved) text
|
||||
*/
|
||||
public TextPart(String text) {
|
||||
super(text);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
return text();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that represents a single placeholder with
|
||||
* a hard-coded fallback.
|
||||
*/
|
||||
static class SimplePlaceholderPart extends AbstractPart {
|
||||
|
||||
private final String key;
|
||||
|
||||
@Nullable
|
||||
private final String fallback;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
* @param text the raw text
|
||||
* @param key the key of the placeholder
|
||||
* @param fallback the fallback to use, if any
|
||||
*/
|
||||
public SimplePlaceholderPart(String text,String key, @Nullable String fallback) {
|
||||
super(text);
|
||||
this.key = key;
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
String value = resolveRecursively(resolutionContext, this.key);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
else if (this.fallback != null) {
|
||||
return this.fallback;
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(this.key, text());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A {@link Part} implementation that represents a single placeholder
|
||||
* containing nested placeholders.
|
||||
* @param text the raw text of the root placeholder
|
||||
* @param keyParts the parts of the key
|
||||
* @param defaultParts the parts of the fallback, if any
|
||||
*/
|
||||
record NestedPlaceholderPart(String text, List<Part> keyParts, @Nullable List<Part> defaultParts) implements Part {
|
||||
static class NestedPlaceholderPart extends AbstractPart {
|
||||
|
||||
private final List<Part> keyParts;
|
||||
|
||||
@Nullable
|
||||
private final List<Part> defaultParts;
|
||||
|
||||
/**
|
||||
* Create a new instance.
|
||||
* @param text the raw text of the root placeholder
|
||||
* @param keyParts the parts of the key
|
||||
* @param defaultParts the parts of the fallback, if any
|
||||
*/
|
||||
NestedPlaceholderPart(String text, List<Part> keyParts, @Nullable List<Part> defaultParts) {
|
||||
super(text);
|
||||
this.keyParts = keyParts;
|
||||
this.defaultParts = defaultParts;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String resolve(PartResolutionContext resolutionContext) {
|
||||
String resolvedKey = Part.resolveAll(this.keyParts, resolutionContext);
|
||||
String value = resolutionContext.resolvePlaceholder(resolvedKey);
|
||||
String value = resolveRecursively(resolutionContext, resolvedKey);
|
||||
if (value != null) {
|
||||
return value;
|
||||
}
|
||||
else if (this.defaultParts != null) {
|
||||
return Part.resolveAll(this.defaultParts, resolutionContext);
|
||||
}
|
||||
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, this.text);
|
||||
return resolutionContext.handleUnresolvablePlaceholder(resolvedKey, text());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -59,7 +59,7 @@ class ReflectionHintsWriterTests {
|
||||
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
|
||||
MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS,
|
||||
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS,
|
||||
MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES)
|
||||
MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES, MemberCategory.UNSAFE_ALLOCATED)
|
||||
.withField("DEFAULT_CHARSET")
|
||||
.withField("defaultCharset")
|
||||
.withField("aScore")
|
||||
@@ -83,6 +83,7 @@ class ReflectionHintsWriterTests {
|
||||
"allDeclaredMethods": true,
|
||||
"allPublicClasses": true,
|
||||
"allDeclaredClasses": true,
|
||||
"unsafeAllocated": true,
|
||||
"fields": [
|
||||
{ "name": "aScore" },
|
||||
{ "name": "DEFAULT_CHARSET" },
|
||||
|
||||
@@ -1188,6 +1188,26 @@ class ResolvableTypeTests {
|
||||
assertThatResolvableType(complex4).isNotAssignableFrom(complex3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isAssignableFromForUnresolvedWildcards() {
|
||||
ResolvableType wildcard = ResolvableType.forInstance(new Wildcard<>());
|
||||
ResolvableType wildcardFixed = ResolvableType.forInstance(new WildcardFixed());
|
||||
ResolvableType wildcardConcrete = ResolvableType.forClassWithGenerics(Wildcard.class, Number.class);
|
||||
|
||||
assertThat(wildcard.isAssignableFrom(wildcardFixed)).isTrue();
|
||||
assertThat(wildcard.isAssignableFromResolvedPart(wildcardFixed)).isTrue();
|
||||
assertThat(wildcard.isAssignableFrom(wildcardConcrete)).isTrue();
|
||||
assertThat(wildcard.isAssignableFromResolvedPart(wildcardConcrete)).isTrue();
|
||||
assertThat(wildcardFixed.isAssignableFrom(wildcard)).isFalse();
|
||||
assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcard)).isFalse();
|
||||
assertThat(wildcardFixed.isAssignableFrom(wildcardConcrete)).isFalse();
|
||||
assertThat(wildcardFixed.isAssignableFromResolvedPart(wildcardConcrete)).isFalse();
|
||||
assertThat(wildcardConcrete.isAssignableFrom(wildcard)).isTrue();
|
||||
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcard)).isTrue();
|
||||
assertThat(wildcardConcrete.isAssignableFrom(wildcardFixed)).isFalse();
|
||||
assertThat(wildcardConcrete.isAssignableFromResolvedPart(wildcardFixed)).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void identifyTypeVariable() throws Exception {
|
||||
Method method = ClassArguments.class.getMethod("typedArgumentFirst", Class.class, Class.class, Class.class);
|
||||
@@ -1367,6 +1387,30 @@ class ResolvableTypeTests {
|
||||
assertThat(type.hasUnresolvableGenerics()).isFalse();
|
||||
}
|
||||
|
||||
@Test // gh-33932
|
||||
void recursiveType() {
|
||||
assertThat(ResolvableType.forClass(RecursiveMap.class)).isEqualTo(
|
||||
ResolvableType.forClass(RecursiveMap.class));
|
||||
|
||||
ResolvableType resolvableType1 = ResolvableType.forClassWithGenerics(Map.class,
|
||||
String.class, RecursiveMap.class);
|
||||
ResolvableType resolvableType2 = ResolvableType.forClassWithGenerics(Map.class,
|
||||
String.class, RecursiveMap.class);
|
||||
assertThat(resolvableType1).isEqualTo(resolvableType2);
|
||||
}
|
||||
|
||||
@Test // gh-33932
|
||||
void recursiveTypeWithInterface() {
|
||||
assertThat(ResolvableType.forClass(RecursiveMapWithInterface.class)).isEqualTo(
|
||||
ResolvableType.forClass(RecursiveMapWithInterface.class));
|
||||
|
||||
ResolvableType resolvableType1 = ResolvableType.forClassWithGenerics(Map.class,
|
||||
String.class, RecursiveMapWithInterface.class);
|
||||
ResolvableType resolvableType2 = ResolvableType.forClassWithGenerics(Map.class,
|
||||
String.class, RecursiveMapWithInterface.class);
|
||||
assertThat(resolvableType1).isEqualTo(resolvableType2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void spr11219() throws Exception {
|
||||
ResolvableType type = ResolvableType.forField(BaseProvider.class.getField("stuff"), BaseProvider.class);
|
||||
@@ -1685,7 +1729,6 @@ class ResolvableTypeTests {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class MySimpleInterfaceType implements MyInterfaceType<String> {
|
||||
}
|
||||
|
||||
@@ -1695,7 +1738,6 @@ class ResolvableTypeTests {
|
||||
public abstract class ExtendsMySimpleInterfaceTypeWithImplementsRaw extends MySimpleInterfaceTypeWithImplementsRaw {
|
||||
}
|
||||
|
||||
|
||||
public class MyCollectionInterfaceType implements MyInterfaceType<Collection<String>> {
|
||||
}
|
||||
|
||||
@@ -1703,20 +1745,17 @@ class ResolvableTypeTests {
|
||||
public abstract class MySuperclassType<T> {
|
||||
}
|
||||
|
||||
|
||||
public class MySimpleSuperclassType extends MySuperclassType<String> {
|
||||
}
|
||||
|
||||
|
||||
public class MyCollectionSuperclassType extends MySuperclassType<Collection<String>> {
|
||||
}
|
||||
|
||||
|
||||
interface Wildcard<T extends Number> extends List<T> {
|
||||
public class Wildcard<T extends Number> {
|
||||
}
|
||||
|
||||
|
||||
interface RawExtendsWildcard extends Wildcard {
|
||||
public class WildcardFixed extends Wildcard<Integer> {
|
||||
}
|
||||
|
||||
|
||||
@@ -1821,6 +1860,16 @@ class ResolvableTypeTests {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class RecursiveMap extends HashMap<String, RecursiveMap> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class RecursiveMapWithInterface extends HashMap<String, RecursiveMapWithInterface>
|
||||
implements Map<String, RecursiveMapWithInterface> {
|
||||
}
|
||||
|
||||
|
||||
private static class ResolvableTypeAssert extends AbstractAssert<ResolvableTypeAssert, ResolvableType>{
|
||||
|
||||
public ResolvableTypeAssert(ResolvableType actual) {
|
||||
|
||||
@@ -770,6 +770,30 @@ class TypeDescriptorTests {
|
||||
assertThat(td1).isNotEqualTo(td2);
|
||||
}
|
||||
|
||||
@Test // gh-33932
|
||||
void recursiveType() {
|
||||
assertThat(TypeDescriptor.valueOf(RecursiveMap.class)).isEqualTo(
|
||||
TypeDescriptor.valueOf(RecursiveMap.class));
|
||||
|
||||
TypeDescriptor typeDescriptor1 = TypeDescriptor.map(Map.class,
|
||||
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMap.class));
|
||||
TypeDescriptor typeDescriptor2 = TypeDescriptor.map(Map.class,
|
||||
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMap.class));
|
||||
assertThat(typeDescriptor1).isEqualTo(typeDescriptor2);
|
||||
}
|
||||
|
||||
@Test // gh-33932
|
||||
void recursiveTypeWithInterface() {
|
||||
assertThat(TypeDescriptor.valueOf(RecursiveMapWithInterface.class)).isEqualTo(
|
||||
TypeDescriptor.valueOf(RecursiveMapWithInterface.class));
|
||||
|
||||
TypeDescriptor typeDescriptor1 = TypeDescriptor.map(Map.class,
|
||||
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMapWithInterface.class));
|
||||
TypeDescriptor typeDescriptor2 = TypeDescriptor.map(Map.class,
|
||||
TypeDescriptor.valueOf(String.class), TypeDescriptor.valueOf(RecursiveMapWithInterface.class));
|
||||
assertThat(typeDescriptor1).isEqualTo(typeDescriptor2);
|
||||
}
|
||||
|
||||
|
||||
// Methods designed for test introspection
|
||||
|
||||
@@ -987,6 +1011,16 @@ class TypeDescriptorTests {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class RecursiveMap extends HashMap<String, RecursiveMap> {
|
||||
}
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
static class RecursiveMapWithInterface extends HashMap<String, RecursiveMapWithInterface>
|
||||
implements Map<String, RecursiveMapWithInterface> {
|
||||
}
|
||||
|
||||
|
||||
// Annotations used on tested elements
|
||||
|
||||
@Target({ElementType.PARAMETER})
|
||||
|
||||
@@ -210,6 +210,8 @@ class PlaceholderParserTests {
|
||||
static Stream<Arguments> nestedPlaceholders() {
|
||||
return Stream.of(
|
||||
Arguments.of("${p6}", "v1:v2:def"),
|
||||
Arguments.of("${p6:not-used}", "v1:v2:def"),
|
||||
Arguments.of("${p6:${invalid}}", "v1:v2:def"),
|
||||
Arguments.of("${invalid:${p1}:${p2}}", "v1:v2"),
|
||||
Arguments.of("${invalid:${p3}}", "v1:v2"),
|
||||
Arguments.of("${invalid:${p4}}", "v1:v2"),
|
||||
|
||||
@@ -192,7 +192,7 @@ class CoroutinesUtilsTests {
|
||||
|
||||
@Test
|
||||
fun invokeSuspendingFunctionWithValueClassParameter() {
|
||||
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClass") }
|
||||
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassParameter") }
|
||||
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, "foo", null) as Mono
|
||||
runBlocking {
|
||||
Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo")
|
||||
@@ -204,7 +204,16 @@ class CoroutinesUtilsTests {
|
||||
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithValueClassReturnValue") }
|
||||
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono
|
||||
runBlocking {
|
||||
Assertions.assertThat(mono.awaitSingle()).isEqualTo("foo")
|
||||
Assertions.assertThat(mono.awaitSingle()).isEqualTo(ValueClass("foo"))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun invokeSuspendingFunctionWithResultOfUnitReturnValue() {
|
||||
val method = CoroutinesUtilsTests::class.java.declaredMethods.first { it.name.startsWith("suspendingFunctionWithResultOfUnitReturnValue") }
|
||||
val mono = CoroutinesUtils.invokeSuspendingFunction(method, this, null) as Mono
|
||||
runBlocking {
|
||||
Assertions.assertThat(mono.awaitSingle()).isEqualTo(Result.success(Unit))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +323,7 @@ class CoroutinesUtilsTests {
|
||||
return null
|
||||
}
|
||||
|
||||
suspend fun suspendingFunctionWithValueClass(value: ValueClass): String {
|
||||
suspend fun suspendingFunctionWithValueClassParameter(value: ValueClass): String {
|
||||
delay(1)
|
||||
return value.value
|
||||
}
|
||||
@@ -324,6 +333,11 @@ class CoroutinesUtilsTests {
|
||||
return ValueClass("foo")
|
||||
}
|
||||
|
||||
suspend fun suspendingFunctionWithResultOfUnitReturnValue(): Result<Unit> {
|
||||
delay(1)
|
||||
return Result.success(Unit)
|
||||
}
|
||||
|
||||
suspend fun suspendingFunctionWithValueClassWithInit(value: ValueClassWithInit): String {
|
||||
delay(1)
|
||||
return value.value
|
||||
|
||||
+2
@@ -36,6 +36,8 @@ import static org.assertj.core.api.Assertions.assertThatException;
|
||||
* Tests invocation of constructors.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @see MethodInvocationTests
|
||||
* @see VariableAndFunctionTests
|
||||
*/
|
||||
class ConstructorInvocationTests extends AbstractExpressionTests {
|
||||
|
||||
|
||||
+2
@@ -48,6 +48,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
* @author Andy Clement
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @see ConstructorInvocationTests
|
||||
* @see VariableAndFunctionTests
|
||||
*/
|
||||
class MethodInvocationTests extends AbstractExpressionTests {
|
||||
|
||||
|
||||
+64
-1
@@ -21,10 +21,13 @@ import java.lang.invoke.MethodHandles;
|
||||
import java.lang.invoke.MethodType;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import example.Color;
|
||||
import example.FruitMap;
|
||||
@@ -658,7 +661,8 @@ class SpelDocumentationTests extends AbstractExpressionTests {
|
||||
MethodHandle methodHandle = MethodHandles.lookup().findVirtual(String.class, "formatted",
|
||||
MethodType.methodType(String.class, Object[].class))
|
||||
.bindTo(template)
|
||||
.bindTo(varargs); // here we have to provide arguments in a single array binding
|
||||
// Here we have to provide the arguments in a single array binding:
|
||||
.bindTo(varargs);
|
||||
context.registerFunction("message", methodHandle);
|
||||
|
||||
String message = parser.parseExpression("#message()").getValue(context, String.class);
|
||||
@@ -666,6 +670,59 @@ class SpelDocumentationTests extends AbstractExpressionTests {
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
class Varargs {
|
||||
|
||||
@Test
|
||||
void varargsMethodInvocationWithIndividualArguments() {
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted('blue', 1)";
|
||||
String message = parser.parseExpression(expression)
|
||||
.getValue(String.class);
|
||||
assertThat(message).isEqualTo("blue is color #1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void varargsMethodInvocationWithArgumentsAsObjectArray() {
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted(new Object[] {'blue', 1})";
|
||||
String message = parser.parseExpression(expression)
|
||||
.getValue(String.class);
|
||||
assertThat(message).isEqualTo("blue is color #1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void varargsMethodInvocationWithArgumentsAsInlineList() {
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%d'.formatted({'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
assertThat(message).isEqualTo("blue is color #1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void varargsMethodInvocationWithTypeConversion() {
|
||||
Method reverseStringsMethod = ReflectionUtils.findMethod(StringUtils.class, "reverseStrings", String[].class);
|
||||
SimpleEvaluationContext evaluationContext = SimpleEvaluationContext.forReadOnlyDataBinding().build();
|
||||
evaluationContext.setVariable("reverseStrings", reverseStringsMethod);
|
||||
|
||||
// String reverseStrings(String... strings)
|
||||
// evaluates to "3.0, 2.0, 1.0, SpEL"
|
||||
String expression = "#reverseStrings('SpEL', 1, 10F / 5, 3.0000)";
|
||||
String message = parser.parseExpression(expression)
|
||||
.getValue(evaluationContext, String.class);
|
||||
assertThat(message).isEqualTo("3.0, 2.0, 1, SpEL");
|
||||
}
|
||||
|
||||
@Test
|
||||
void varargsMethodInvocationWithArgumentsAsStringArray() {
|
||||
// evaluates to "blue is color #1"
|
||||
String expression = "'%s is color #%s'.formatted(new String[] {'blue', 1})";
|
||||
String message = parser.parseExpression(expression).getValue(String.class);
|
||||
assertThat(message).isEqualTo("blue is color #1");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Nested
|
||||
class TernaryOperator {
|
||||
|
||||
@@ -905,6 +962,12 @@ class SpelDocumentationTests extends AbstractExpressionTests {
|
||||
public static String reverseString(String input) {
|
||||
return new StringBuilder(input).reverse().toString();
|
||||
}
|
||||
|
||||
public static String reverseStrings(String... strings) {
|
||||
List<String> list = Arrays.asList(strings);
|
||||
Collections.reverse(list);
|
||||
return list.stream().collect(Collectors.joining(", "));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ListConcatenation implements OperatorOverloader {
|
||||
|
||||
+2
@@ -32,6 +32,8 @@ import static org.springframework.expression.spel.SpelMessage.INCORRECT_NUMBER_O
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Sam Brannen
|
||||
* @see ConstructorInvocationTests
|
||||
* @see MethodInvocationTests
|
||||
*/
|
||||
class VariableAndFunctionTests extends AbstractExpressionTests {
|
||||
|
||||
|
||||
+4
@@ -17,6 +17,7 @@
|
||||
package org.springframework.jdbc.datasource;
|
||||
|
||||
import java.sql.SQLException;
|
||||
import java.sql.SQLFeatureNotSupportedException;
|
||||
import java.sql.Savepoint;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -179,6 +180,9 @@ public abstract class JdbcTransactionObjectSupport implements SavepointManager,
|
||||
try {
|
||||
conHolder.getConnection().releaseSavepoint((Savepoint) savepoint);
|
||||
}
|
||||
catch (SQLFeatureNotSupportedException ex) {
|
||||
// typically on Oracle - ignore
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
throw new TransactionSystemException("Could not explicitly release JDBC savepoint", ex);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ import static org.mockito.Mockito.verify;
|
||||
*/
|
||||
class SqlQueryTests {
|
||||
|
||||
//FIXME inline?
|
||||
// FIXME inline?
|
||||
private static final String SELECT_ID =
|
||||
"select id from custmr";
|
||||
private static final String SELECT_ID_WHERE =
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ class H2SequenceMaxValueIncrementerTests {
|
||||
* Tests that the incrementer works when using all supported H2 <em>compatibility modes</em>.
|
||||
*/
|
||||
@ParameterizedTest
|
||||
@EnumSource(ModeEnum.class)
|
||||
@EnumSource
|
||||
void incrementsSequenceWithExplicitH2CompatibilityMode(ModeEnum mode) {
|
||||
String connectionUrl = String.format("jdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=%s", UUID.randomUUID(), mode);
|
||||
DataSource dataSource = new SimpleDriverDataSource(new org.h2.Driver(), connectionUrl, "sa", "");
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -102,7 +102,7 @@ import org.springframework.util.ErrorHandler;
|
||||
* (i.e. after your business logic executed but before the JMS part got committed),
|
||||
* so duplicate message detection is just there to cover a corner case.
|
||||
* <li>Or wrap your <i>entire processing with an XA transaction</i>, covering the
|
||||
* reception of the JMS message as well as the execution of the business logic in
|
||||
* receipt of the JMS message as well as the execution of the business logic in
|
||||
* your message listener (including database operations etc). This is only
|
||||
* supported by {@link DefaultMessageListenerContainer}, through specifying
|
||||
* an external "transactionManager" (typically a
|
||||
|
||||
+4
-4
@@ -45,7 +45,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* <p>This listener container variant is built for repeated polling attempts,
|
||||
* each invoking the {@link #receiveAndExecute} method. The MessageConsumer used
|
||||
* may be reobtained fo reach attempt or cached in between attempts; this is up
|
||||
* may be reobtained for each attempt or cached in between attempts; this is up
|
||||
* to the concrete implementation. The receive timeout for each attempt can be
|
||||
* configured through the {@link #setReceiveTimeout "receiveTimeout"} property.
|
||||
*
|
||||
@@ -56,7 +56,7 @@ import org.springframework.util.Assert;
|
||||
* full control over the listening process, allowing for custom scaling and throttling
|
||||
* and of concurrent message processing (which is up to concrete subclasses).
|
||||
*
|
||||
* <p>Message reception and listener execution can automatically be wrapped
|
||||
* <p>Message receipt and listener execution can automatically be wrapped
|
||||
* in transactions through passing a Spring
|
||||
* {@link org.springframework.transaction.PlatformTransactionManager} into the
|
||||
* {@link #setTransactionManager "transactionManager"} property. This will usually
|
||||
@@ -105,7 +105,7 @@ public abstract class AbstractPollingMessageListenerContainer extends AbstractMe
|
||||
|
||||
/**
|
||||
* Specify the Spring {@link org.springframework.transaction.PlatformTransactionManager}
|
||||
* to use for transactional wrapping of message reception plus listener execution.
|
||||
* to use for transactional wrapping of message receipt plus listener execution.
|
||||
* <p>Default is none, not performing any transactional wrapping.
|
||||
* If specified, this will usually be a Spring
|
||||
* {@link org.springframework.transaction.jta.JtaTransactionManager} or one
|
||||
@@ -131,7 +131,7 @@ public abstract class AbstractPollingMessageListenerContainer extends AbstractMe
|
||||
|
||||
/**
|
||||
* Return the Spring PlatformTransactionManager to use for transactional
|
||||
* wrapping of message reception plus listener execution.
|
||||
* wrapping of message receipt plus listener execution.
|
||||
*/
|
||||
@Nullable
|
||||
protected final PlatformTransactionManager getTransactionManager() {
|
||||
|
||||
+4
-4
@@ -47,7 +47,7 @@ import org.springframework.util.backoff.FixedBackOff;
|
||||
/**
|
||||
* Message listener container variant that uses plain JMS client APIs, specifically
|
||||
* a loop of {@code MessageConsumer.receive()} calls that also allow for
|
||||
* transactional reception of messages (registering them with XA transactions).
|
||||
* transactional receipt of messages (registering them with XA transactions).
|
||||
* Designed to work in a native JMS environment as well as in a Jakarta EE environment,
|
||||
* with only minimal differences in configuration.
|
||||
*
|
||||
@@ -70,7 +70,7 @@ import org.springframework.util.backoff.FixedBackOff;
|
||||
* {@code MessageConsumer} (only refreshed in case of failure), using the JMS provider's
|
||||
* resources as efficiently as possible.
|
||||
*
|
||||
* <p>Message reception and listener execution can automatically be wrapped
|
||||
* <p>Message receipt and listener execution can automatically be wrapped
|
||||
* in transactions by passing a Spring
|
||||
* {@link org.springframework.transaction.PlatformTransactionManager} into the
|
||||
* {@link #setTransactionManager "transactionManager"} property. This will usually
|
||||
@@ -474,7 +474,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
|
||||
|
||||
/**
|
||||
* Specify the maximum number of messages to process in one task.
|
||||
* More concretely, this limits the number of message reception attempts
|
||||
* More concretely, this limits the number of message receipt attempts
|
||||
* per task, which includes receive iterations that did not actually
|
||||
* pick up a message until they hit their timeout (see the
|
||||
* {@link #setReceiveTimeout "receiveTimeout"} property).
|
||||
@@ -562,7 +562,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
|
||||
* The minimum number of consumers
|
||||
* (see {@link #setConcurrentConsumers "concurrentConsumers"})
|
||||
* will be kept around until shutdown in any case.
|
||||
* <p>Within each task execution, a number of message reception attempts
|
||||
* <p>Within each task execution, a number of message receipt attempts
|
||||
* (according to the "maxMessagesPerTask" setting) will each wait for an incoming
|
||||
* message (according to the "receiveTimeout" setting). If all of those receive
|
||||
* attempts in a given task return without a message, the task is considered
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ import org.springframework.util.Assert;
|
||||
*
|
||||
* <p>For a different style of MessageListener handling, through looped
|
||||
* {@code MessageConsumer.receive()} calls that also allow for
|
||||
* transactional reception of messages (registering them with XA transactions),
|
||||
* transactional receipt of messages (registering them with XA transactions),
|
||||
* see {@link DefaultMessageListenerContainer}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -89,7 +89,7 @@ public interface JmsHeaders {
|
||||
|
||||
/**
|
||||
* Specify if the message was resent. This occurs when a message
|
||||
* consumer fails to acknowledge the message reception.
|
||||
* consumer fails to acknowledge receipt of the message.
|
||||
* <p>Read-only value.
|
||||
* @see jakarta.jms.Message#getJMSRedelivered()
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2013 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -36,9 +36,9 @@ public interface PollableChannel extends MessageChannel {
|
||||
/**
|
||||
* Receive a message from this channel, blocking until either a message is available
|
||||
* or the specified timeout period elapses.
|
||||
* @param timeout the timeout in milliseconds or {@link MessageChannel#INDEFINITE_TIMEOUT}.
|
||||
* @param timeout the timeout in milliseconds or {@link MessageChannel#INDEFINITE_TIMEOUT}
|
||||
* @return the next available {@link Message} or {@code null} if the specified timeout
|
||||
* period elapses or the message reception is interrupted
|
||||
* period elapses or the message receipt is interrupted
|
||||
*/
|
||||
@Nullable
|
||||
Message<?> receive(long timeout);
|
||||
|
||||
+2
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2017 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,9 +22,7 @@ import org.springframework.messaging.Message;
|
||||
|
||||
/**
|
||||
* Convenient base class for {@link AsyncHandlerMethodReturnValueHandler}
|
||||
* implementations that support only asynchronous (Future-like) return values
|
||||
* and merely serve as adapters of such types to Spring's
|
||||
* {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}.
|
||||
* implementations that support only asynchronous (Future-like) return values.
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @since 4.2
|
||||
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -573,7 +573,7 @@ public abstract class AbstractMethodMessageHandler<T>
|
||||
if (returnValue != null && this.returnValueHandlers.isAsyncReturnValue(returnValue, returnType)) {
|
||||
CompletableFuture<?> future = this.returnValueHandlers.toCompletableFuture(returnValue, returnType);
|
||||
if (future != null) {
|
||||
future.whenComplete(new ReturnValueListenableFutureCallback(invocable, message));
|
||||
future.whenComplete(new ReturnValueCallback(invocable, message));
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -704,13 +704,13 @@ public abstract class AbstractMethodMessageHandler<T>
|
||||
}
|
||||
|
||||
|
||||
private class ReturnValueListenableFutureCallback implements BiConsumer<Object, Throwable> {
|
||||
private class ReturnValueCallback implements BiConsumer<Object, Throwable> {
|
||||
|
||||
private final InvocableHandlerMethod handlerMethod;
|
||||
|
||||
private final Message<?> message;
|
||||
|
||||
public ReturnValueListenableFutureCallback(InvocableHandlerMethod handlerMethod, Message<?> message) {
|
||||
public ReturnValueCallback(InvocableHandlerMethod handlerMethod, Message<?> message) {
|
||||
this.handlerMethod = handlerMethod;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
-2
@@ -24,8 +24,6 @@ import org.springframework.lang.Nullable;
|
||||
/**
|
||||
* An extension of {@link HandlerMethodReturnValueHandler} for handling async,
|
||||
* Future-like return value types that support success and error callbacks.
|
||||
* Essentially anything that can be adapted to a
|
||||
* {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}.
|
||||
*
|
||||
* <p>Implementations should consider extending the convenient base class
|
||||
* {@link AbstractAsyncReturnValueHandler}.
|
||||
|
||||
+1
-1
@@ -181,7 +181,7 @@ class ChannelSendOperator<T> extends Mono<Void> implements Scannable {
|
||||
requiredWriteSubscriber().onNext(item);
|
||||
return;
|
||||
}
|
||||
//FIXME revisit in case of reentrant sync deadlock
|
||||
// FIXME revisit in case of reentrant sync deadlock
|
||||
synchronized (this) {
|
||||
if (this.state == State.READY_TO_WRITE) {
|
||||
requiredWriteSubscriber().onNext(item);
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ import org.springframework.aot.hint.annotation.Reflective;
|
||||
* <p>Specifying this annotation registers the configured {@link BeanOverrideProcessor}
|
||||
* which must be capable of handling the composed annotation and its attributes.
|
||||
*
|
||||
* <p>Since the composed annotation should only be applied to fields, it is
|
||||
* <p>Since the composed annotation should only be applied to non-static fields, it is
|
||||
* expected that it is meta-annotated with {@link Target @Target(ElementType.FIELD)}.
|
||||
*
|
||||
* <p>For concrete examples, see
|
||||
|
||||
+92
-39
@@ -18,12 +18,12 @@ package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryUtils;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
|
||||
@@ -44,7 +44,7 @@ import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A {@link BeanFactoryPostProcessor} implementation that processes identified
|
||||
* use of {@link BeanOverride @BeanOverride} and adapts the {@link BeanFactory}
|
||||
* use of {@link BeanOverride @BeanOverride} and adapts the {@code BeanFactory}
|
||||
* accordingly.
|
||||
*
|
||||
* <p>For each override, the bean factory is prepared according to the chosen
|
||||
@@ -94,12 +94,15 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
|
||||
Set<String> generatedBeanNames = new HashSet<>();
|
||||
for (BeanOverrideHandler handler : this.beanOverrideHandlers) {
|
||||
registerBeanOverride(beanFactory, handler);
|
||||
registerBeanOverride(beanFactory, handler, generatedBeanNames);
|
||||
}
|
||||
}
|
||||
|
||||
private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) {
|
||||
private void registerBeanOverride(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
|
||||
Set<String> generatedBeanNames) {
|
||||
|
||||
String beanName = handler.getBeanName();
|
||||
Field field = handler.getField();
|
||||
Assert.state(!BeanFactoryUtils.isFactoryDereference(beanName),() -> """
|
||||
@@ -108,27 +111,43 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
beanName, field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
|
||||
switch (handler.getStrategy()) {
|
||||
case REPLACE -> replaceOrCreateBean(beanFactory, handler, true);
|
||||
case REPLACE_OR_CREATE -> replaceOrCreateBean(beanFactory, handler, false);
|
||||
case REPLACE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, true);
|
||||
case REPLACE_OR_CREATE -> replaceOrCreateBean(beanFactory, handler, generatedBeanNames, false);
|
||||
case WRAP -> wrapBean(beanFactory, handler);
|
||||
}
|
||||
}
|
||||
|
||||
private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler,
|
||||
boolean requireExistingBean) {
|
||||
Set<String> generatedBeanNames, boolean requireExistingBean) {
|
||||
|
||||
// NOTE: This method supports 3 distinct scenarios which must be accounted for.
|
||||
//
|
||||
// 1) JVM runtime
|
||||
// 2) AOT processing
|
||||
// 3) AOT runtime
|
||||
// - JVM runtime
|
||||
// - AOT processing
|
||||
// - AOT runtime
|
||||
//
|
||||
// In addition, this method supports 4 distinct use cases.
|
||||
//
|
||||
// 1) Override existing bean by-type
|
||||
// 2) Create bean by-type, with a generated name
|
||||
// 3) Override existing bean by-name
|
||||
// 4) Create bean by-name, with a provided name
|
||||
|
||||
String beanName = handler.getBeanName();
|
||||
Field field = handler.getField();
|
||||
BeanDefinition existingBeanDefinition = null;
|
||||
if (beanName == null) {
|
||||
beanName = getBeanNameForType(beanFactory, handler, requireExistingBean);
|
||||
if (beanName != null) {
|
||||
// We are overriding an existing bean by-type.
|
||||
// If the generatedBeanNames set already contains the beanName that we
|
||||
// just found by-type, that means we are experiencing a "phantom read"
|
||||
// (i.e., we found a bean that was not previously there). Consequently,
|
||||
// we cannot "override the override", because we would lose one of the
|
||||
// overrides. Instead, we must create a new override for the current
|
||||
// handler. For example, if one handler creates an override for a SubType
|
||||
// and a subsequent handler creates an override for a SuperType of that
|
||||
// SubType, we must end up with overrides for both SuperType and SubType.
|
||||
if (beanName != null && !generatedBeanNames.contains(beanName)) {
|
||||
// 1) We are overriding an existing bean by-type.
|
||||
beanName = BeanFactoryUtils.transformedBeanName(beanName);
|
||||
// If we are overriding a manually registered singleton, we won't find
|
||||
// an existing bean definition.
|
||||
@@ -137,23 +156,26 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
}
|
||||
else {
|
||||
// We will later generate a name for the nonexistent bean, but since NullAway
|
||||
// will reject leaving the beanName set to null, we set it to a placeholder.
|
||||
// 2) We are creating a bean by-type, with a generated name.
|
||||
// Since NullAway will reject leaving the beanName set to null,
|
||||
// we set it to a placeholder that will be replaced later.
|
||||
beanName = PSEUDO_BEAN_NAME_PLACEHOLDER;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
|
||||
if (candidates.contains(beanName)) {
|
||||
// We are overriding an existing bean by-name.
|
||||
// 3) We are overriding an existing bean by-name.
|
||||
existingBeanDefinition = beanFactory.getBeanDefinition(beanName);
|
||||
}
|
||||
else if (requireExistingBean) {
|
||||
throw new IllegalStateException("""
|
||||
Unable to override bean: there is no bean to replace \
|
||||
with name [%s] and type [%s]."""
|
||||
.formatted(beanName, handler.getBeanType()));
|
||||
Unable to replace bean: there is no bean with name '%s' and type %s \
|
||||
(as required by field '%s.%s')."""
|
||||
.formatted(beanName, handler.getBeanType(),
|
||||
field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
}
|
||||
// 4) We are creating a bean by-name with the provided beanName.
|
||||
}
|
||||
|
||||
if (existingBeanDefinition != null) {
|
||||
@@ -179,7 +201,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
|
||||
if (!(beanFactory instanceof BeanDefinitionRegistry registry)) {
|
||||
throw new IllegalStateException("Cannot process bean override with a BeanFactory " +
|
||||
"that doesn't implement BeanDefinitionRegistry: " + beanFactory.getClass().getName());
|
||||
"that does not implement BeanDefinitionRegistry: " + beanFactory.getClass().getName());
|
||||
}
|
||||
|
||||
RootBeanDefinition pseudoBeanDefinition = createPseudoBeanDefinition(handler);
|
||||
@@ -187,6 +209,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
// Generate a name for the nonexistent bean.
|
||||
if (PSEUDO_BEAN_NAME_PLACEHOLDER.equals(beanName)) {
|
||||
beanName = beanNameGenerator.generateBeanName(pseudoBeanDefinition, registry);
|
||||
generatedBeanNames.add(beanName);
|
||||
}
|
||||
|
||||
registry.registerBeanDefinition(beanName, pseudoBeanDefinition);
|
||||
@@ -212,14 +235,18 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
|
||||
/**
|
||||
* Check that a bean with the specified {@link BeanOverrideHandler#getBeanName() name}
|
||||
* and {@link BeanOverrideHandler#getBeanType() type} is registered.
|
||||
* <p>If so, put the {@link BeanOverrideHandler} in the early tracking map.
|
||||
* <p>The map will later be checked to see if a given bean should be wrapped
|
||||
* upon creation, during the {@link WrapEarlyBeanPostProcessor#getEarlyBeanReference}
|
||||
* phase.
|
||||
* or {@link BeanOverrideHandler#getBeanType() type} has already been registered
|
||||
* in the {@code BeanFactory}.
|
||||
* <p>If so, register the {@link BeanOverrideHandler} and the corresponding bean
|
||||
* name in the {@link BeanOverrideRegistry}.
|
||||
* <p>The registry will later be checked to see if a given bean should be wrapped
|
||||
* upon creation, during the early bean post-processing phase.
|
||||
* @see BeanOverrideRegistry#registerBeanOverrideHandler(BeanOverrideHandler, String)
|
||||
* @see WrapEarlyBeanPostProcessor#getEarlyBeanReference(Object, String)
|
||||
*/
|
||||
private void wrapBean(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler) {
|
||||
String beanName = handler.getBeanName();
|
||||
Field field = handler.getField();
|
||||
ResolvableType beanType = handler.getBeanType();
|
||||
|
||||
if (beanName == null) {
|
||||
@@ -235,13 +262,17 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
beanName = primaryCandidate;
|
||||
}
|
||||
else {
|
||||
Field field = handler.getField();
|
||||
throw new IllegalStateException("""
|
||||
Unable to select a bean to override by wrapping: found %d bean instances of type %s \
|
||||
(as required by annotated field '%s.%s')%s"""
|
||||
String message = "Unable to select a bean to wrap: ";
|
||||
if (candidateCount == 0) {
|
||||
message += "there are no beans of type %s (as required by field '%s.%s')."
|
||||
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName());
|
||||
}
|
||||
else {
|
||||
message += "found %d beans of type %s (as required by field '%s.%s'): %s"
|
||||
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName(), (candidateCount > 0 ? ": " + candidateNames : "")));
|
||||
|
||||
field.getName(), candidateNames);
|
||||
}
|
||||
throw new IllegalStateException(message);
|
||||
}
|
||||
}
|
||||
beanName = BeanFactoryUtils.transformedBeanName(beanName);
|
||||
@@ -251,9 +282,10 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
Set<String> candidates = getExistingBeanNamesByType(beanFactory, handler, false);
|
||||
if (!candidates.contains(beanName)) {
|
||||
throw new IllegalStateException("""
|
||||
Unable to override bean by wrapping: there is no existing bean \
|
||||
with name [%s] and type [%s]."""
|
||||
.formatted(beanName, beanType));
|
||||
Unable to wrap bean: there is no bean with name '%s' and type %s \
|
||||
(as required by field '%s.%s')."""
|
||||
.formatted(beanName, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,7 +308,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
else if (candidateCount == 0) {
|
||||
if (requireExistingBean) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to override bean: no beans of type %s (as required by annotated field '%s.%s')"
|
||||
"Unable to override bean: there are no beans of type %s (as required by field '%s.%s')."
|
||||
.formatted(beanType, field.getDeclaringClass().getSimpleName(), field.getName()));
|
||||
}
|
||||
return null;
|
||||
@@ -287,9 +319,8 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
return primaryCandidate;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("""
|
||||
Unable to select a bean to override: found %s beans of type %s \
|
||||
(as required by annotated field '%s.%s'): %s"""
|
||||
throw new IllegalStateException(
|
||||
"Unable to select a bean to override: found %d beans of type %s (as required by field '%s.%s'): %s"
|
||||
.formatted(candidateCount, beanType, field.getDeclaringClass().getSimpleName(),
|
||||
field.getName(), candidateNames));
|
||||
}
|
||||
@@ -331,6 +362,12 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
return beanNames;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the primary candidate in the given set of bean names.
|
||||
* <p>Honors both <em>primary</em> and <em>fallback</em> semantics.
|
||||
* @return the name of the primary candidate, or {@code null} if none found
|
||||
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determinePrimaryCandidate(Map, Class)
|
||||
*/
|
||||
@Nullable
|
||||
private static String determinePrimaryCandidate(
|
||||
ConfigurableListableBeanFactory beanFactory, Set<String> candidateBeanNames, Class<?> beanType) {
|
||||
@@ -340,6 +377,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
|
||||
String primaryBeanName = null;
|
||||
// First pass: identify unique primary candidate
|
||||
for (String candidateBeanName : candidateBeanNames) {
|
||||
if (beanFactory.containsBeanDefinition(candidateBeanName)) {
|
||||
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName);
|
||||
@@ -352,6 +390,21 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
}
|
||||
}
|
||||
}
|
||||
// Second pass: identify unique non-fallback candidate
|
||||
if (primaryBeanName == null) {
|
||||
for (String candidateBeanName : candidateBeanNames) {
|
||||
if (beanFactory.containsBeanDefinition(candidateBeanName)) {
|
||||
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(candidateBeanName);
|
||||
if (!beanDefinition.isFallback()) {
|
||||
if (primaryBeanName != null) {
|
||||
// More than one non-fallback bean found among candidates.
|
||||
return null;
|
||||
}
|
||||
primaryBeanName = candidateBeanName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return primaryBeanName;
|
||||
}
|
||||
|
||||
@@ -364,7 +417,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
* respectively.
|
||||
* <p>The returned bean definition should <strong>not</strong> be used to create
|
||||
* a bean instance but rather only for the purpose of having suitable bean
|
||||
* definition metadata available in the {@link BeanFactory} — for example,
|
||||
* definition metadata available in the {@code BeanFactory} — for example,
|
||||
* for autowiring candidate resolution.
|
||||
*/
|
||||
private static RootBeanDefinition createPseudoBeanDefinition(BeanOverrideHandler handler) {
|
||||
@@ -394,7 +447,7 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor,
|
||||
private static void destroySingleton(ConfigurableListableBeanFactory beanFactory, String beanName) {
|
||||
if (!(beanFactory instanceof DefaultListableBeanFactory dlbf)) {
|
||||
throw new IllegalStateException("Cannot process bean override with a BeanFactory " +
|
||||
"that doesn't implement DefaultListableBeanFactory: " + beanFactory.getClass().getName());
|
||||
"that does not implement DefaultListableBeanFactory: " + beanFactory.getClass().getName());
|
||||
}
|
||||
dlbf.destroySingleton(beanName);
|
||||
}
|
||||
|
||||
+7
-3
@@ -16,7 +16,7 @@
|
||||
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.ContextConfigurationAttributes;
|
||||
import org.springframework.test.context.ContextCustomizerFactory;
|
||||
import org.springframework.test.context.TestContextAnnotationUtils;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link ContextCustomizerFactory} implementation that provides support for
|
||||
@@ -42,7 +43,7 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
public BeanOverrideContextCustomizer createContextCustomizer(Class<?> testClass,
|
||||
List<ContextConfigurationAttributes> configAttributes) {
|
||||
|
||||
Set<BeanOverrideHandler> handlers = new HashSet<>();
|
||||
Set<BeanOverrideHandler> handlers = new LinkedHashSet<>();
|
||||
findBeanOverrideHandler(testClass, handlers);
|
||||
if (handlers.isEmpty()) {
|
||||
return null;
|
||||
@@ -51,10 +52,13 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory {
|
||||
}
|
||||
|
||||
private void findBeanOverrideHandler(Class<?> testClass, Set<BeanOverrideHandler> handlers) {
|
||||
handlers.addAll(BeanOverrideHandler.forTestClass(testClass));
|
||||
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
|
||||
findBeanOverrideHandler(testClass.getEnclosingClass(), handlers);
|
||||
}
|
||||
BeanOverrideHandler.forTestClass(testClass).forEach(handler ->
|
||||
Assert.state(handlers.add(handler), () ->
|
||||
"Duplicate BeanOverrideHandler discovered in test class %s: %s"
|
||||
.formatted(testClass.getName(), handler)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Modifier;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -105,6 +106,8 @@ public abstract class BeanOverrideHandler {
|
||||
private static void processField(Field field, Class<?> testClass, List<BeanOverrideHandler> handlers) {
|
||||
AtomicBoolean overrideAnnotationFound = new AtomicBoolean();
|
||||
MergedAnnotations.from(field, DIRECT).stream(BeanOverride.class).forEach(mergedAnnotation -> {
|
||||
Assert.state(!Modifier.isStatic(field.getModifiers()),
|
||||
() -> "@BeanOverride field must not be static: " + field);
|
||||
MergedAnnotation<?> metaSource = mergedAnnotation.getMetaSource();
|
||||
Assert.state(metaSource != null, "@BeanOverride annotation must be meta-present");
|
||||
|
||||
|
||||
+29
-3
@@ -17,8 +17,13 @@
|
||||
package org.springframework.test.context.bean.override;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.factory.BeanCreationException;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
@@ -37,9 +42,12 @@ import org.springframework.util.StringUtils;
|
||||
*/
|
||||
class BeanOverrideRegistry {
|
||||
|
||||
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new HashMap<>();
|
||||
private static final Log logger = LogFactory.getLog(BeanOverrideRegistry.class);
|
||||
|
||||
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new HashMap<>();
|
||||
|
||||
private final Map<BeanOverrideHandler, String> handlerToBeanNameMap = new LinkedHashMap<>();
|
||||
|
||||
private final Map<String, BeanOverrideHandler> wrappingBeanOverrideHandlers = new LinkedHashMap<>();
|
||||
|
||||
private final ConfigurableBeanFactory beanFactory;
|
||||
|
||||
@@ -57,7 +65,25 @@ class BeanOverrideRegistry {
|
||||
* bean via {@link #wrapBeanIfNecessary(Object, String)}.
|
||||
*/
|
||||
void registerBeanOverrideHandler(BeanOverrideHandler handler, String beanName) {
|
||||
Assert.state(!this.handlerToBeanNameMap.containsKey(handler), () ->
|
||||
"Cannot register BeanOverrideHandler for bean with name '%s'; detected multiple registrations for %s"
|
||||
.formatted(beanName, handler));
|
||||
|
||||
// Check if beanName was already registered, before adding the new mapping.
|
||||
boolean beanNameAlreadyRegistered = this.handlerToBeanNameMap.containsValue(beanName);
|
||||
// Add new mapping before potentially logging a warning, to ensure that
|
||||
// the current handler is logged as well.
|
||||
this.handlerToBeanNameMap.put(handler, beanName);
|
||||
|
||||
if (beanNameAlreadyRegistered && logger.isWarnEnabled()) {
|
||||
List<BeanOverrideHandler> competingHandlers = this.handlerToBeanNameMap.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().equals(beanName))
|
||||
.map(Entry::getKey)
|
||||
.toList();
|
||||
logger.warn("Bean with name '%s' was overridden by multiple handlers: %s"
|
||||
.formatted(beanName, competingHandlers));
|
||||
}
|
||||
|
||||
if (handler.getStrategy() == BeanOverrideStrategy.WRAP) {
|
||||
this.wrappingBeanOverrideHandlers.put(beanName, handler);
|
||||
}
|
||||
|
||||
+7
-2
@@ -27,8 +27,8 @@ import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.test.context.bean.override.BeanOverride;
|
||||
|
||||
/**
|
||||
* {@code @TestBean} is an annotation that can be applied to a field in a test
|
||||
* class to override a bean in the test's
|
||||
* {@code @TestBean} is an annotation that can be applied to a non-static field
|
||||
* in a test class to override a bean in the test's
|
||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
||||
* using a static factory method.
|
||||
*
|
||||
@@ -105,6 +105,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton bean
|
||||
* corresponding to the value returned from the {@code @TestBean} factory method.
|
||||
*
|
||||
* <p>There are no restrictions on the visibility of {@code @TestBean} fields or
|
||||
* factory methods. Such fields and methods can therefore be {@code public},
|
||||
* {@code protected}, package-private (default visibility), or {@code private}
|
||||
* depending on the needs or coding practices of the project.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
|
||||
+12
@@ -23,6 +23,7 @@ import java.util.Objects;
|
||||
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.core.ResolvableType;
|
||||
import org.springframework.core.style.ToStringCreator;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideHandler;
|
||||
import org.springframework.test.context.bean.override.BeanOverrideStrategy;
|
||||
@@ -83,4 +84,15 @@ final class TestBeanOverrideHandler extends BeanOverrideHandler {
|
||||
return this.factoryMethod.hashCode() * 29 + super.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new ToStringCreator(this)
|
||||
.append("field", getField())
|
||||
.append("beanType", getBeanType())
|
||||
.append("beanName", getBeanName())
|
||||
.append("strategy", getStrategy())
|
||||
.append("factoryMethod", this.factoryMethod)
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+4
-3
@@ -99,9 +99,10 @@ public enum MockReset {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the {@link MockReset} associated with the given mock.
|
||||
* @param mock the source mock
|
||||
* @return the reset type (never {@code null})
|
||||
* Get the {@link MockReset} strategy associated with the given mock.
|
||||
* @param mock the mock
|
||||
* @return the reset strategy for the given mock, or {@link MockReset#NONE}
|
||||
* if no strategy is associated with the given mock
|
||||
*/
|
||||
static MockReset get(Object mock) {
|
||||
MockingDetails mockingDetails = Mockito.mockingDetails(mock);
|
||||
|
||||
+7
-2
@@ -29,8 +29,8 @@ import org.springframework.core.annotation.AliasFor;
|
||||
import org.springframework.test.context.bean.override.BeanOverride;
|
||||
|
||||
/**
|
||||
* {@code @MockitoBean} is an annotation that can be applied to a field in a test
|
||||
* class to override a bean in the test's
|
||||
* {@code @MockitoBean} is an annotation that can be applied to a non-static field
|
||||
* in a test class to override a bean in the test's
|
||||
* {@link org.springframework.context.ApplicationContext ApplicationContext}
|
||||
* using a Mockito mock.
|
||||
*
|
||||
@@ -58,6 +58,11 @@ import org.springframework.test.context.bean.override.BeanOverride;
|
||||
* FactoryBean}, the {@code FactoryBean} will be replaced with a singleton mock
|
||||
* of the type of object created by the {@code FactoryBean}.
|
||||
*
|
||||
* <p>There are no restrictions on the visibility of a {@code @MockitoBean} field.
|
||||
* Such fields can therefore be {@code public}, {@code protected}, package-private
|
||||
* (default visibility), or {@code private} depending on the needs or coding
|
||||
* practices of the project.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @author Sam Brannen
|
||||
* @since 6.2
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user