Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Clozel e354390837 Release v6.2.12 2025-10-16 08:56:30 +02:00
435 changed files with 4253 additions and 10363 deletions
@@ -24,14 +24,14 @@ runs:
using: composite
steps:
- name: Set Up Java
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: ${{ inputs.java-early-access == 'true' && 'temurin' || (inputs.java-distribution || 'liberica') }}
java-version: |
${{ inputs.java-early-access == 'true' && format('{0}-ea', inputs.java-version) || inputs.java-version }}
${{ inputs.java-toolchain == 'true' && '17' || '' }}
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
cache-read-only: false
develocity-access-key: ${{ inputs.develocity-access-key }}
@@ -17,14 +17,14 @@ runs:
using: composite
steps:
- name: Set Up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
uses: jfrog/setup-jfrog-cli@f748a0599171a192a2668afee8d0497f7c1069df # v4.5.6
env:
JF_ENV_SPRING: ${{ inputs.jfrog-cli-config-token }}
- name: Download Release Artifacts
shell: bash
run: jf rt download --spec ${{ format('{0}/artifacts.spec', github.action_path) }} --spec-vars 'buildName=${{ format('spring-framework-{0}', inputs.spring-framework-version) }};buildNumber=${{ github.run_number }}'
- name: Sync
uses: spring-io/central-publish-action@0c03960e9b16fdfe70e2443e1d5393cbc3a35622 # v0.3.0
uses: spring-io/central-publish-action@0cdd90d12e6876341e82860d951e1bcddc1e51b6 # v0.2.0
with:
token: ${{ inputs.central-token-password }}
token-name: ${{ inputs.central-token-username }}
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set up Java
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'liberica'
java-version: 17
@@ -13,7 +13,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -21,7 +21,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Deploy
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
+2 -2
View File
@@ -10,7 +10,7 @@ jobs:
timeout-minutes: 60
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build
id: build
uses: ./.github/actions/build
@@ -19,7 +19,7 @@ jobs:
uses: ./.github/actions/print-jvm-thread-dumps
- name: Upload Build Reports
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: build-reports
path: '**/build/reports/'
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
git config --global core.longPaths true
Stop-Service -name Docker
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build
id: build
uses: ./.github/actions/build
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
fetch-depth: 1
ref: docs-build
+5 -5
View File
@@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Build and Publish
id: build-and-publish
uses: ./.github/actions/build
@@ -20,7 +20,7 @@ jobs:
develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }}
publish: true
- name: Stage Release
uses: spring-io/artifactory-deploy-action@926d7f7cc810569395346bf3a4d91b380b3e355b # v0.0.4
uses: spring-io/artifactory-deploy-action@dc1913008c0599f0c4b1fdafb6ff3c502b3565ea # v0.0.2
with:
artifact-properties: |
/**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false
@@ -56,7 +56,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Sync to Maven Central
uses: ./.github/actions/sync-to-maven-central
with:
@@ -72,7 +72,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Set up JFrog CLI
uses: jfrog/setup-jfrog-cli@5b06f730cc5a6f55d78b30753f8583454b08c0aa # v4.8.1
uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2
env:
JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }}
- name: Promote build
@@ -85,7 +85,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Code
uses: actions/checkout@v6
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Create GitHub Release
uses: ./.github/actions/create-github-release
with:
+5 -5
View File
@@ -30,23 +30,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check Out Release Verification Tests
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
ref: 'v0.0.2'
repository: spring-projects/spring-framework-release-verification
token: ${{ secrets.token }}
- name: Check Out Send Notification Action
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
path: send-notification
sparse-checkout: .github/actions/send-notification
- name: Set Up Java
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
distribution: 'liberica'
java-version: 17
- name: Set Up Gradle
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
uses: gradle/actions/setup-gradle@ac638b010cf58a27ee6c972d7336334ccaf61c96 # v4.4.1
with:
cache-read-only: false
- name: Configure Gradle Properties
@@ -64,7 +64,7 @@ jobs:
run: ./gradlew spring-framework-release-verification-tests:test
- name: Upload Build Reports on Failure
if: failure()
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: build-reports
path: '**/build/reports/'
+1 -1
View File
@@ -27,7 +27,7 @@ See the [Build from Source](https://github.com/spring-projects/spring-framework/
## Continuous Integration Builds
CI builds are defined with [GitHub Actions workflows](.github/workflows).
Information regarding CI builds can be found in the [Spring Framework Concourse pipeline](ci/README.adoc) documentation.
## Stay in Touch
+6 -2
View File
@@ -91,10 +91,14 @@ configure([rootProject] + javaProjects) { project ->
"https://fasterxml.github.io/jackson-core/javadoc/2.14/",
"https://fasterxml.github.io/jackson-databind/javadoc/2.14/",
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.14/",
//"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"https://hc.apache.org/httpcomponents-client-5.5.x/current/httpclient5/apidocs/",
"https://projectreactor.io/docs/test/release/api/",
"https://junit.org/junit4/javadoc/4.13.2/",
"https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.14.0/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
// Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages,
+2 -4
View File
@@ -13,9 +13,7 @@ content:
- url: https://github.com/spring-projects/spring-framework
# Refname matching:
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
# branches: We include snapshots for main, 6.2.x, and 7.0.x to 9.*.x.
branches: ['main', '6.2.x', '{7..9}.+({0..9}).x']
# tags: We effectively include all releases from 6.0.9 to 9.*.*.
branches: ['main', '{6..9}.+({1..9}).x']
tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})']
start_path: framework-docs
asciidoc:
@@ -38,4 +36,4 @@ runtime:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.25/ui-bundle.zip
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.18/ui-bundle.zip
+1 -1
View File
@@ -326,9 +326,9 @@
*** xref:testing/testcontext-framework/application-events.adoc[]
*** xref:testing/testcontext-framework/test-execution-events.adoc[]
*** xref:testing/testcontext-framework/ctx-management.adoc[]
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
**** xref:testing/testcontext-framework/ctx-management/xml.adoc[]
**** xref:testing/testcontext-framework/ctx-management/groovy.adoc[]
**** xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[]
**** xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[]
**** xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[]
**** xref:testing/testcontext-framework/ctx-management/initializers.adoc[]
@@ -104,7 +104,7 @@ Note that pointcut definitions are generally matched against any intercepted met
If a pointcut is strictly meant to be public-only, even in a CGLIB proxy scenario with
potential non-public interactions through proxies, it needs to be defined accordingly.
If your interception needs to include method calls or even constructors within the target
If your interception needs include method calls or even constructors within the target
class, consider the use of Spring-driven xref:core/aop/using-aspectj.adoc#aop-aj-ltw[native AspectJ weaving] instead
of Spring's proxy-based AOP framework. This constitutes a different mode of AOP usage
with different characteristics, so be sure to make yourself familiar with weaving
@@ -151,17 +151,17 @@ injected into a `Set<MovieCatalog>` annotated with `@Qualifier("action")`.
[TIP]
====
Letting qualifier values select against target bean names, within the type-matching
candidates, does not require a `@Qualifier` annotation at the injection point. If there
is no other resolution indicator (such as a qualifier, a primary marker, or a fallback
marker), for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses the
same-named candidate, if any (either by bean name or by associated alias).
candidates, does not require a `@Qualifier` annotation at the injection point.
If there is no other resolution indicator (such as a qualifier or a primary marker),
for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses
the same-named candidate, if any (either by bean name or by associated alias).
Since version 6.1, this requires the `-parameters` Java compiler flag to be present. As
of 6.2, the container applies fast shortcut resolution for bean name matches, bypassing
the full type matching algorithm when the parameter name matches the bean name and no
type, qualifier, primary, or fallback conditions override the match. It is therefore
recommendable for your parameter names to match the target bean names.
Since version 6.1, this requires the `-parameters` Java compiler flag to be present.
As of 6.2, the container applies fast shortcut resolution for bean name matches,
bypassing the full type matching algorithm when the parameter name matches the
bean name and no type, qualifier or primary conditions override the match. It is
therefore recommendable for your parameter names to match the target bean names.
====
As an alternative for injection by name, consider the JSR-250 `@Resource` annotation
@@ -308,8 +308,7 @@ set of multiple matches for the specific bean type (as returned by the factory m
Note that the standard `jakarta.annotation.Priority` annotation is not available at the
`@Bean` level, since it cannot be declared on methods. Its semantics can be modeled
through `@Order` values in combination with `@Primary` or `@Fallback` on a single bean
for each type.
through `@Order` values in combination with `@Primary` on a single bean for each type.
====
Even typed `Map` instances can be autowired as long as the expected key type is `String`.
@@ -27,5 +27,4 @@ with the `CustomAutowireConfigurer`
When multiple beans qualify as autowire candidates, the determination of a "`primary`" is
as follows: If exactly one bean definition among the candidates has a `primary`
attribute set to `true`, it is selected. For annotation-based configuration, see
xref:core/beans/annotation-config/autowired-primary.adoc[Fine-tuning with `@Primary` or `@Fallback`].
attribute set to `true`, it is selected.
@@ -50,8 +50,9 @@ XML configuration:
The preceding XML is more succinct. However, typos are discovered at runtime rather than
design time, unless you use an IDE (such as https://www.jetbrains.com/idea/[IntelliJ
IDEA] or the {spring-site-tools}[Spring Tools]) that supports automatic property
completion when you create bean definitions. Such IDE assistance is highly recommended.
IDEA] or the {spring-site-tools}[Spring Tools for Eclipse])
that supports automatic property completion when you create bean definitions. Such IDE
assistance is highly recommended.
You can also configure a `java.util.Properties` instance, as follows:
@@ -67,13 +67,6 @@ interface, clearly indicating the post-processor nature of that bean. Otherwise,
Since a `BeanPostProcessor` needs to be instantiated early in order to apply to the
initialization of other beans in the context, this early type detection is critical.
Furthermore, when registering a `BeanPostProcessor` via an `@Bean` factory method,
declare the method as `static` and ideally with no dependencies. Doing so avoids eager
initialization of the configuration class and other beans, which would make them
ineligible for full post-processing (such as auto-proxying). See the
"BeanPostProcessor-returning `@Bean` methods" section in the
{spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc for details.
[[beans-factory-programmatically-registering-beanpostprocessors]]
.Programmatically registering `BeanPostProcessor` instances
NOTE: While the recommended approach for `BeanPostProcessor` registration is through
@@ -87,7 +80,7 @@ of execution. Note also that `BeanPostProcessor` instances registered programmat
are always processed before those registered through auto-detection, regardless of any
explicit ordering.
.`BeanPostProcessor` instances and early initialization
.`BeanPostProcessor` instances and AOP auto-proxying
[NOTE]
====
Classes that implement the `BeanPostProcessor` interface are special and are treated
@@ -97,23 +90,17 @@ of the `ApplicationContext`. Next, all `BeanPostProcessor` instances are registe
in a sorted fashion and applied to all further beans in the container. Because AOP
auto-proxying is implemented as a `BeanPostProcessor` itself, neither `BeanPostProcessor`
instances nor the beans they directly reference are eligible for auto-proxying and,
thus, do not have aspects woven into them. More generally, any bean that is instantiated
during this early phase is not eligible for full post-processing by all
`BeanPostProcessor` instances.
thus, do not have aspects woven into them.
For any such bean, you should see a WARN-level log message similar to the following.
For any such bean, you should see an informational log message: `Bean someBean is not
eligible for getting processed by all BeanPostProcessor interfaces (for example: not
eligible for auto-proxying)`.
[quote]
Bean 'someBean' of type [org.example.SomeType] is not eligible for getting processed by
all BeanPostProcessors (for example: not eligible for auto-proxying).
To minimize the number of beans affected, register a `BeanPostProcessor` with a
`static` `@Bean` method that has no dependencies (see the note above). If you have
beans wired into your `BeanPostProcessor` by using autowiring or `@Resource` (which
may fall back to autowiring), Spring might access unexpected beans when searching
for type-matching dependency candidates and, therefore, make them ineligible for
auto-proxying or other kinds of bean post-processing. For example, if you have a
dependency annotated with `@Resource` where the field or setter name does not
If you have beans wired into your `BeanPostProcessor` by using autowiring or
`@Resource` (which may fall back to autowiring), Spring might access unexpected beans
when searching for type-matching dependency candidates and, therefore, make them
ineligible for auto-proxying or other kinds of bean post-processing. For example, if you
have a dependency annotated with `@Resource` where the field or setter name does not
directly correspond to the declared name of a bean and no name attribute is used,
Spring accesses other beans for matching them by type.
====
@@ -148,7 +135,7 @@ Java::
}
public Object postProcessAfterInitialization(Object bean, String beanName) {
System.out.println("Bean '" + beanName + "' created : " + bean);
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
@@ -177,48 +164,7 @@ Kotlin::
----
======
You can register the `InstantiationTracingBeanPostProcessor` with Java configuration
by using a `static` `@Bean` method (recommended to avoid eager initialization of the
configuration class and other beans):
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes",chomp="-packages"]
----
@Configuration
public class AppConfig {
@Bean
public static InstantiationTracingBeanPostProcessor instantiationTracingBeanPostProcessor() {
return new InstantiationTracingBeanPostProcessor();
}
// ... other bean definitions
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes",chomp="-packages"]
----
@Configuration
class AppConfig {
@Bean
companion object {
@JvmStatic
fun instantiationTracingBeanPostProcessor() = InstantiationTracingBeanPostProcessor()
}
// ... other bean definitions
}
----
======
Alternatively, the `InstantiationTracingBeanPostProcessor` can be registered via the
`bean` element with XML configuration:
The following `beans` element uses the `InstantiationTracingBeanPostProcessor`:
[source,xml,indent=0,subs="verbatim,quotes"]
----
@@ -246,7 +192,7 @@ Alternatively, the `InstantiationTracingBeanPostProcessor` can be registered via
----
Notice how the `InstantiationTracingBeanPostProcessor` is merely defined. It does not
even have a name, and, because it is a bean, it can be dependency-injected as with any
even have a name, and, because it is a bean, it can be dependency-injected as you would any
other bean. (The preceding configuration also defines a bean that is backed by a Groovy
script. The Spring dynamic language support is detailed in the chapter entitled
xref:languages/dynamic.adoc[Dynamic Language Support].)
@@ -355,23 +301,6 @@ implement the `BeanFactoryPostProcessor` interface. It uses these beans as bean
post-processors, at the appropriate time. You can deploy these post-processor beans as
you would any other bean.
When registering a `BeanFactoryPostProcessor` via an `@Bean` factory method in a
`@Configuration` class, declare the method as `static` to avoid lifecycle conflicts
with annotation processing (such as `@Autowired`, `@Value`, and `@PostConstruct`) in the
configuration class. See the "BeanFactoryPostProcessor-returning `@Bean` methods"
section in the {spring-framework-api}/context/annotation/Bean.html[`@Bean`] javadoc
for details and an example.
For any non-static `@Bean` factory method with a `BeanFactoryPostProcessor` return type,
you should see an INFO-level log message similar to the following.
[quote]
@Bean method MyConfig.myBfpp is non-static and returns an object assignable to Spring's
BeanFactoryPostProcessor interface. This will result in a failure to process annotations
such as @Autowired, @Resource, and @PostConstruct within the method's declaring
@Configuration class. Add the 'static' modifier to this method to avoid these container
lifecycle issues; see @Bean javadoc for complete details.
NOTE: As with ``BeanPostProcessor``s , you typically do not want to configure
``BeanFactoryPostProcessor``s for lazy initialization. If no other bean references a
`Bean(Factory)PostProcessor`, that post-processor will not get instantiated at all.
@@ -339,11 +339,11 @@ In the preceding scenario, using `@Autowired` works well and provides the desire
modularity, but determining exactly where the autowired bean definitions are declared is
still somewhat ambiguous. For example, as a developer looking at `ServiceConfig`, how do
you know exactly where the `@Autowired AccountRepository` bean is declared? It is not
explicit in the code, and this may be just fine. Note that the
{spring-site-tools}[Spring Tools] IDE support provides tooling that can render graphs
showing how everything is wired, which may be all you need. Also, your Java IDE can
easily find all declarations and uses of the `AccountRepository` type and quickly show
you the location of `@Bean` methods that return that type.
explicit in the code, and this may be just fine. Remember that the
{spring-site-tools}[Spring Tools for Eclipse] provides tooling that
can render graphs showing how everything is wired, which may be all you need. Also,
your Java IDE can easily find all declarations and uses of the `AccountRepository` type
and quickly show you the location of `@Bean` methods that return that type.
In cases where this ambiguity is not acceptable and you wish to have direct navigation
from within your IDE from one `@Configuration` class to another, consider autowiring the
@@ -7,14 +7,14 @@ similar to the https://jakarta.ee/specifications/expression-language/[Jakarta Ex
Language] but offers additional features, most notably method invocation and basic string
templating functionality.
While there are several other Java expression languages available -- OGNL, MVEL, and
JBoss EL, to name a few -- the Spring Expression Language was created to provide the
Spring community with a single well supported expression language that can be used across
all the products in the Spring portfolio. Its language features are driven by the
requirements of the projects in the Spring portfolio, including tooling requirements for
code completion within the {spring-site-tools}[Spring Tools] IDE support. That said, SpEL
is based on a technology-agnostic API that lets other expression language implementations
be integrated, should the need arise.
While there are several other Java expression languages available -- OGNL, MVEL, and JBoss
EL, to name a few -- the Spring Expression Language was created to provide the Spring
community with a single well supported expression language that can be used across all
the products in the Spring portfolio. Its language features are driven by the
requirements of the projects in the Spring portfolio, including tooling requirements
for code completion support within the {spring-site-tools}[Spring Tools for Eclipse].
That said, SpEL is based on a technology-agnostic API that lets other expression language
implementations be integrated, should the need arise.
While SpEL serves as the foundation for expression evaluation within the Spring
portfolio, it is not directly tied to Spring and can be used independently. To
@@ -53,7 +53,7 @@ Kotlin::
val trueValue = parser.parseExpression("'black' < 'block'").getValue(Boolean::class.java)
// uses CustomValue:::compareTo
val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java)
val trueValue = parser.parseExpression("new CustomValue(1) < new CustomValue(2)").getValue(Boolean::class.java);
----
======
@@ -167,7 +167,7 @@ Kotlin::
[CAUTION]
====
The syntax for the `between` operator is `<input> between {<range_begin>, <range_end>}`,
which is effectively a shortcut for `<input> >= <range_begin> && <input> \<= <range_end>`.
which is effectively a shortcut for `<input> >= <range_begin> && <input> \<= <range_end>}`.
Consequently, `1 between {1, 5}` evaluates to `true`, while `1 between {5, 1}` evaluates
to `false`.
@@ -312,13 +312,13 @@ Kotlin::
// evaluates to 'a'
val ch = parser.parseExpression("'d' - 3")
.getValue(Char::class.java)
.getValue(Character::class.java);
// -- Repeat --
// evaluates to "abcabc"
val repeated = parser.parseExpression("'abc' * 2")
.getValue(String::class.java)
.getValue(String::class.java);
----
======
@@ -485,7 +485,7 @@ Kotlin::
// -- Operator precedence --
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
val minusTwentyOne = parser.parseExpression("1+2-3*8").getValue(Int::class.java) // -21
----
======
@@ -541,7 +541,32 @@ For example, if we want to overload the `ADD` operator to allow two lists to be
concatenated using the `+` sign, we can implement a custom `OperatorOverloader` as
follows.
include-code::./ListConcatenation[]
[source,java,indent=0,subs="verbatim,quotes"]
----
pubic class ListConcatenation implements OperatorOverloader {
@Override
public boolean overridesOperation(Operation operation, Object left, Object right) {
return (operation == Operation.ADD &&
left instanceof List && right instanceof List);
}
@Override
@SuppressWarnings("unchecked")
public Object operate(Operation operation, Object left, Object right) {
if (operation == Operation.ADD &&
left instanceof List list1 && right instanceof List list2) {
List result = new ArrayList(list1);
result.addAll(list2);
return result;
}
throw new UnsupportedOperationException(
"No overload for operation %s and operands [%s] and [%s]"
.formatted(operation, left, right));
}
}
----
If we register `ListConcatenation` as the `OperatorOverloader` in a
`StandardEvaluationContext`, we can then evaluate expressions like `{1, 2, 3} + {4, 5}`
@@ -564,8 +589,8 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val context = StandardEvaluationContext()
context.operatorOverloader = ListConcatenation()
StandardEvaluationContext context = StandardEvaluationContext()
context.setOperatorOverloader(ListConcatenation())
// evaluates to a new list: [1, 2, 3, 4, 5]
parser.parseExpression("{1, 2, 3} + {2 + 2, 5}").getValue(context, List::class.java)
@@ -6,7 +6,7 @@ are set by using the `setVariable()` method in `EvaluationContext` implementatio
[NOTE]
====
Variable names must begin with a letter (as defined below), an underscore, or a dollar
Variable names must be begin with a letter (as defined below), an underscore, or a dollar
sign.
Variable names must be composed of one or more of the following supported types of
@@ -676,7 +676,7 @@ provides `firstName` and `lastName` properties, such as the `Actor` class from a
[source,java,indent=0,subs="verbatim,quotes"]
----
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.paramSource(new Actor("Leonor", "Watling"))
.paramSource(new Actor("Leonor", "Watling")
.update();
----
@@ -15,7 +15,7 @@ configuration options. You should instantiate the `SimpleJdbcInsert` in the data
layer's initialization method. For this example, the initializing method is the
`setDataSource` method. You do not need to subclass the `SimpleJdbcInsert` class. Instead,
you can create a new instance and set the table name by using the `withTableName` method.
Configuration methods for this class follow the `fluent` style that returns the instance
Configuration methods for this class follow the `fluid` style that returns the instance
of the `SimpleJdbcInsert`, which lets you chain all configuration methods. The following
example uses only one configuration method (we show examples of multiple methods later):
@@ -349,11 +349,11 @@ parameters return the data read from the table.
You can declare `SimpleJdbcCall` in a manner similar to declaring `SimpleJdbcInsert`. You
should instantiate and configure the class in the initialization method of your data-access
layer. In contrast to the `StoredProcedure` class, you do not need to create a subclass,
and you do not need to declare parameters that can be looked up in the database metadata.
The following `SimpleJdbcCall` configuration example uses the preceding stored procedure.
The only configuration option (other than the `DataSource`) is the name of the stored
procedure.
layer. Compared to the `StoredProcedure` class, you need not create a subclass
and you need not to declare parameters that can be looked up in the database metadata.
The following example of a `SimpleJdbcCall` configuration uses the preceding stored
procedure (the only configuration option, in addition to the `DataSource`, is the name
of the stored procedure):
[tabs]
======
@@ -1,11 +1,16 @@
[[appendix]]
= Appendix
[[appendix.xsd-schemas]]
== XML Schemas
This part of the appendix lists XML schemas related to integration technologies.
[[appendix.xsd-schemas-jee]]
=== The `jee` Schema
@@ -167,7 +172,7 @@ different properties with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -179,7 +184,7 @@ without `jee`:
</bean>
----
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -195,7 +200,7 @@ with `jee`:
The `<jee:local-slsb/>` element configures a reference to a local EJB Stateless Session Bean.
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
and a number of properties without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -210,7 +215,7 @@ and a number of properties without `jee`:
</bean>
----
The following example shows how to configure a reference to a local EJB Stateless Session Bean
The following example shows how to configures a reference to a local EJB Stateless Session Bean
and a number of properties with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -229,7 +234,7 @@ and a number of properties with `jee`:
The `<jee:remote-slsb/>` element configures a reference to a `remote` EJB Stateless Session Bean.
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
without `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -246,7 +251,7 @@ without `jee`:
</bean>
----
The following example shows how to configure a reference to a remote EJB Stateless Session Bean
The following example shows how to configures a reference to a remote EJB Stateless Session Bean
with `jee`:
[source,xml,indent=0,subs="verbatim,quotes"]
@@ -308,7 +313,7 @@ xref:integration/jmx/naming.adoc#jmx-context-mbeanexport[Configuring Annotation-
=== The `cache` Schema
You can use the `cache` elements to enable support for Spring's `@CacheEvict`, `@CachePut`,
and `@Caching` annotations. The `cache` schema also supports declarative XML-based caching. See
and `@Caching` annotations. It it also supports declarative XML-based caching. See
xref:integration/cache/annotations.adoc#cache-annotation-enable[Enabling Caching Annotations] and
xref:integration/cache/declarative-xml.adoc[Declarative XML-based Caching] for details.
@@ -13,4 +13,4 @@ running the Kotlin compiler with its `-java-parameters` flag for standard Java p
You can declare configuration classes as
{kotlin-docs}/nested-classes.html[top level or nested but not inner],
since the latter requires a reference to the outer class.
since the later requires a reference to the outer class.
@@ -319,17 +319,9 @@ progresses.
== Testing
This section addresses testing with the combination of Kotlin and Spring Framework.
The recommended testing framework is https://junit.org/[JUnit Jupiter] along with
The recommended testing framework is https://junit.org/junit5/[JUnit 5] along with
https://mockk.io/[Mockk] for mocking.
[TIP]
====
Kotlin lets you specify meaningful test function names between backticks (```).
For a concrete example, see the `+++`Find all users on HTML page`()+++` test function later
in this section.
====
NOTE: If you are using Spring Boot, see
{spring-boot-docs-ref}/features/kotlin.html#features.kotlin.testing[this related documentation].
@@ -360,6 +352,7 @@ file with a `spring.test.constructor.autowire.mode = all` property.
[[per_class-lifecycle]]
=== `PER_CLASS` Lifecycle
Kotlin lets you specify meaningful test function names between backticks (```).
With JUnit Jupiter (JUnit 5), Kotlin test classes can use the `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`
annotation to enable single instantiation of test classes, which allows the use of `@BeforeAll`
and `@AfterAll` annotations on non-static methods, which is a good fit for Kotlin.
@@ -187,7 +187,7 @@ default mode may be set via the
xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism.
The default mode may also be configured as a
https://docs.junit.org/current/running-tests/configuration-parameters.html[JUnit Platform configuration parameter].
https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params[JUnit Platform configuration parameter].
If the `spring.test.constructor.autowire.mode` property is not set, test class
constructors will not be automatically autowired.
@@ -5,25 +5,24 @@ It is important to be able to perform some integration testing without requiring
deployment to your application server or connecting to other enterprise infrastructure.
Doing so lets you test things such as:
* The correct wiring of your Spring components.
* Data access using JDBC or an ORM tool.
** This can include such things as the correctness of SQL statements, Hibernate queries,
JPA entity mappings, and so forth.
* The correct wiring of your Spring IoC container contexts.
* Data access using JDBC or an ORM tool. This can include such things as the correctness
of SQL statements, Hibernate queries, JPA entity mappings, and so forth.
The Spring Framework provides first-class support for integration testing in the
`spring-test` module. The name of the actual JAR file might include the release version,
depending on where you get it from (see the
{spring-framework-wiki}/Spring-Framework-Artifacts[Spring Framework Artifacts] wiki page
for details). This library includes the `org.springframework.test` package, which
`spring-test` module. The name of the actual JAR file might include the release version
and might also be in the long `org.springframework.test` form, depending on where you get
it from (see the xref:core/beans/dependencies.adoc[section on Dependency Management]
for an explanation). This library includes the `org.springframework.test` package, which
contains valuable classes for integration testing with a Spring container. This testing
does not rely on an application server or other deployment environment. Such tests are
slower to run than unit tests but much faster than the equivalent Selenium tests or
remote tests that rely on deployment to an application server.
Unit and integration testing support is provided in the form of the annotation-driven
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext
framework is agnostic of the actual testing framework in use, which allows
instrumentation of tests in various environments, including JUnit, TestNG, and others.
xref:testing/testcontext-framework.adoc[Spring TestContext Framework]. The TestContext framework is
agnostic of the actual testing framework in use, which allows instrumentation of tests
in various environments, including JUnit, TestNG, and others.
The following section provides an overview of the high-level goals of Spring's
integration support, and the rest of this chapter then focuses on dedicated topics:
@@ -8,9 +8,9 @@ use https://www.gebish.org/[Geb] to make our tests even Groovy-er.
== Why Geb and MockMvc?
Geb is backed by WebDriver, so it offers many of the
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[same benefits]
that we get from WebDriver. However, Geb makes things even easier by taking care of some
of the boilerplate code for us.
xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-why[same benefits] that we get from
WebDriver. However, Geb makes things even easier by taking care of some of the
boilerplate code for us.
[[mockmvc-server-htmlunit-geb-setup]]
== MockMvc and Geb Setup
@@ -28,8 +28,7 @@ def setup() {
----
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
usage, see
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
This ensures that any URL referencing `localhost` as the server is directed to our
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
@@ -63,10 +62,10 @@ forwarded to the current page object. This removes a lot of the boilerplate code
needed when using WebDriver directly.
As with direct WebDriver usage, this improves on the design of our
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
by using the Page Object Pattern. As mentioned previously, we can use the Page Object
Pattern with HtmlUnit and WebDriver, but it is even easier with Geb. Consider our new
Groovy-based `CreateMessagePage` implementation:
xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test] by using the Page Object
Pattern. As mentioned previously, we can use the Page Object Pattern with HtmlUnit and
WebDriver, but it is even easier with Geb. Consider our new Groovy-based
`CreateMessagePage` implementation:
[source,groovy]
----
@@ -7,7 +7,8 @@ to use the raw HtmlUnit libraries.
[[mockmvc-server-htmlunit-mah-setup]]
== MockMvc and HtmlUnit Setup
First, make sure that you have included a test dependency on `org.htmlunit:htmlunit`.
First, make sure that you have included a test dependency on
`org.htmlunit:htmlunit`.
We can easily create an HtmlUnit `WebClient` that integrates with MockMvc by using the
`MockMvcWebClientBuilder`, as follows:
@@ -44,7 +45,7 @@ Kotlin::
======
NOTE: This is a simple example of using `MockMvcWebClientBuilder`. For advanced usage,
see <<mockmvc-server-htmlunit-mah-advanced-builder>>.
see xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
This ensures that any URL that references `localhost` as the server is directed to our
`MockMvc` instance without the need for a real HTTP connection. Any other URL is
@@ -76,7 +77,7 @@ Kotlin::
======
NOTE: The default context path is `""`. Alternatively, we can specify the context path,
as described in <<mockmvc-server-htmlunit-mah-advanced-builder>>.
as described in xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-advanced-builder[Advanced `MockMvcWebClientBuilder`].
Once we have a reference to the `HtmlPage`, we can then fill out the form and submit it
to create a message, as the following example shows:
@@ -143,10 +144,10 @@ Kotlin::
======
The preceding code improves on our
xref:testing/mockmvc/htmlunit/why.adoc#mockmvc-server-htmlunit-why[MockMvc test] in a
number of ways. First, we no longer have to explicitly verify our form and then create a
request that looks like the form. Instead, we request the form, fill it out, and submit
it, thereby significantly reducing the overhead.
xref:testing/mockmvc/htmlunit/why.adoc#spring-mvc-test-server-htmlunit-mock-mvc-test[MockMvc test] in a number of ways.
First, we no longer have to explicitly verify our form and then create a request that
looks like the form. Instead, we request the form, fill it out, and submit it, thereby
significantly reducing the overhead.
Another important factor is that https://htmlunit.sourceforge.io/javascript.html[HtmlUnit
uses the Mozilla Rhino engine] to evaluate JavaScript. This means that we can also test
@@ -203,7 +203,7 @@ Kotlin::
======
NOTE: This is a simple example of using `MockMvcHtmlUnitDriverBuilder`. For more advanced
usage, see <<mockmvc-server-htmlunit-webdriver-advanced-builder>>.
usage, see xref:testing/mockmvc/htmlunit/webdriver.adoc#spring-mvc-test-server-htmlunit-webdriver-advanced-builder[Advanced `MockMvcHtmlUnitDriverBuilder`].
The preceding example ensures that any URL that references `localhost` as the server is
directed to our `MockMvc` instance without the need for a real HTTP connection. Any other
@@ -259,11 +259,10 @@ Kotlin::
======
--
This improves on the design of our
xref:testing/mockmvc/htmlunit/mah.adoc#mockmvc-server-htmlunit-mah-usage[HtmlUnit test]
This improves on the design of our xref:testing/mockmvc/htmlunit/mah.adoc#spring-mvc-test-server-htmlunit-mah-usage[HtmlUnit test]
by leveraging the Page Object Pattern. As we mentioned in
<<mockmvc-server-htmlunit-webdriver-why>>, we can use the Page Object Pattern with
HtmlUnit, but it is much easier with WebDriver. Consider the following
xref:testing/mockmvc/htmlunit/webdriver.adoc#mockmvc-server-htmlunit-webdriver-why[Why WebDriver and MockMvc?], we can use the Page Object Pattern
with HtmlUnit, but it is much easier with WebDriver. Consider the following
`CreateMessagePage` implementation:
--
@@ -60,7 +60,7 @@ assume our form looks like the following snippet:
</form>
----
How do we ensure that our form produces the correct request to create a new message? A
How do we ensure that our form produce the correct request to create a new message? A
naive attempt might resemble the following:
[tabs]
@@ -154,7 +154,7 @@ validation.
[[mockmvc-server-htmlunit-why-integration]]
== Integration Testing to the Rescue?
To resolve the issues mentioned above, we could perform end-to-end integration testing,
To resolve the issues mentioned earlier, we could perform end-to-end integration testing,
but this has some drawbacks. Consider testing the view that lets us page through the
messages. We might need the following tests:
@@ -171,7 +171,7 @@ leads to a number of additional challenges:
* Testing can become slow, since each test would need to ensure that the database is in
the correct state.
* Since our database needs to be in a specific state, we cannot run tests in parallel.
* Performing assertions on items such as auto-generated IDs, timestamps, and others can
* Performing assertions on such items as auto-generated IDs, timestamps, and others can
be difficult.
These challenges do not mean that we should abandon end-to-end integration testing
@@ -49,6 +49,7 @@ Kotlin::
<1> Injecting the `ApplicationContext`.
======
Similarly, if your test is configured to load a `WebApplicationContext`, you can inject
the web application context into your test, as follows:
@@ -86,6 +87,7 @@ Kotlin::
<2> Injecting the `WebApplicationContext`.
======
Dependency injection by using `@Autowired` is provided by the
`DependencyInjectionTestExecutionListener`, which is configured by default
(see xref:testing/testcontext-framework/fixture-di.adoc[Dependency Injection of Test Fixtures]).
@@ -94,23 +96,22 @@ Dependency injection by using `@Autowired` is provided by the
Test classes that use the TestContext framework do not need to extend any particular
class or implement a specific interface to configure their application context. Instead,
configuration is achieved by declaring the `@ContextConfiguration` annotation at the
class level. If your test class does not explicitly declare component classes or resource
locations, the configured `ContextLoader` determines how to load a context from _default_
configuration classes or a _default_ location. In addition to component classes and
context resource locations, an application context can also be configured through
xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[context customizers]
or xref:testing/testcontext-framework/ctx-management/initializers.adoc[context initializers].
class level. If your test class does not explicitly declare application context resource
locations or component classes, the configured `ContextLoader` determines how to load a
context from a default location or default configuration classes. In addition to context
resource locations and component classes, an application context can also be configured
through application context initializers.
The following sections explain how to use `@ContextConfiguration` and related annotations
to configure a test `ApplicationContext` by using component classes (typically
`@Configuration` classes), XML configuration files, Groovy scripts, context customizers,
or context initializers. Alternatively, you can implement and configure your own custom
`SmartContextLoader` for advanced use cases.
The following sections explain how to use Spring's `@ContextConfiguration` annotation to
configure a test `ApplicationContext` by using XML configuration files, Groovy scripts,
component classes (typically `@Configuration` classes), or context initializers.
Alternatively, you can implement and configure your own custom `SmartContextLoader` for
advanced use cases.
* xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes]
* xref:testing/testcontext-framework/ctx-management/xml.adoc[Context Configuration with XML resources]
* xref:testing/testcontext-framework/ctx-management/groovy.adoc[Context Configuration with Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing Component Classes, XML, and Groovy Scripts]
* xref:testing/testcontext-framework/ctx-management/javaconfig.adoc[Context Configuration with Component Classes]
* xref:testing/testcontext-framework/ctx-management/mixed-config.adoc[Mixing XML, Groovy Scripts, and Component Classes]
* xref:testing/testcontext-framework/ctx-management/context-customizers.adoc[Context Configuration with Context Customizers]
* xref:testing/testcontext-framework/ctx-management/initializers.adoc[Context Configuration with Context Initializers]
* xref:testing/testcontext-framework/ctx-management/inheritance.adoc[Context Configuration Inheritance]
@@ -1,32 +1,33 @@
[[testcontext-ctx-management-mixed-config]]
= Mixing Component Classes, XML, and Groovy Scripts
= Mixing XML, Groovy Scripts, and Component Classes
It may sometimes be desirable to mix component classes (typically `@Configuration`
classes), XML configuration files, or Groovy scripts to configure an `ApplicationContext`
for your tests. For example, if you use XML configuration in production for legacy
reasons, you may decide that you want to use `@Configuration` classes to configure
It may sometimes be desirable to mix XML configuration files, Groovy scripts, and
component classes (typically `@Configuration` classes) to configure an
`ApplicationContext` for your tests. For example, if you use XML configuration in
production, you may decide that you want to use `@Configuration` classes to configure
specific Spring-managed components for your tests, or vice versa.
Furthermore, some third-party frameworks (such as Spring Boot) provide first-class
support for loading an `ApplicationContext` from different types of resources
simultaneously (for example, `@Configuration` classes, XML configuration files, and
Groovy scripts). The Spring Framework, historically, has not supported this for standard
deployments. Consequently, most of the `SmartContextLoader` implementations that the
Spring Framework delivers in the `spring-test` module support only one resource type for
each test context. However, this does not mean that you cannot use a mixture of resource
types. One exception to the general rule is that the `GenericGroovyXmlContextLoader` and
simultaneously (for example, XML configuration files, Groovy scripts, and
`@Configuration` classes). The Spring Framework, historically, has not supported this for
standard deployments. Consequently, most of the `SmartContextLoader` implementations that
the Spring Framework delivers in the `spring-test` module support only one resource type
for each test context. However, this does not mean that you cannot use both. One
exception to the general rule is that the `GenericGroovyXmlContextLoader` and
`GenericGroovyXmlWebContextLoader` support both XML configuration files and Groovy
scripts simultaneously. Furthermore, third-party frameworks may choose to support the
declaration of both `classes` and `locations` through `@ContextConfiguration`, and, with
declaration of both `locations` and `classes` through `@ContextConfiguration`, and, with
the standard testing support in the TestContext framework, you have the following options.
If you want to use `@Configuration` classes and resource locations (for example, XML or
Groovy) to configure your tests, you must pick one as the entry point, and that one must
import or include the other. For example, in a `@Configuration` class, you can use
`@ImportResource` to import XML configuration files or Groovy scripts; whereas, in XML or
Groovy scripts, you can include `@Configuration` classes by using component scanning or
defining them as normal Spring beans. Note that this behavior is semantically equivalent
If you want to use resource locations (for example, XML or Groovy) and `@Configuration`
classes to configure your tests, you must pick one as the entry point, and that one must
include or import the other. For example, in XML or Groovy scripts, you can include
`@Configuration` classes by using component scanning or defining them as normal Spring
beans, whereas, in a `@Configuration` class, you can use `@ImportResource` to import XML
configuration files or Groovy scripts. Note that this behavior is semantically equivalent
to how you configure your application in production: In production configuration, you
define either a set of `@Configuration` classes or a set of XML or Groovy resource
locations from which your production `ApplicationContext` is loaded, but you still have
the freedom to import or include the other type of configuration.
define either a set of XML or Groovy resource locations or a set of `@Configuration`
classes from which your production `ApplicationContext` is loaded, but you still have the
freedom to include or import the other type of configuration.
@@ -22,7 +22,7 @@ TestNG:
* Dependency injection for test constructors, test methods, and test lifecycle callback
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
Injection with the `SpringExtension`] for further details.
* Powerful support for link:https://docs.junit.org/current/extensions/conditional-test-execution.html[conditional
* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
test execution] based on SpEL expressions, environment variables, system properties,
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
@@ -160,7 +160,7 @@ for further details.
=== Dependency Injection with the `SpringExtension`
The `SpringExtension` implements the
link:https://docs.junit.org/current/extensions/parameter-resolution.html[`ParameterResolver`]
link:https://junit.org/junit5/docs/current/user-guide/#extensions-parameter-resolution[`ParameterResolver`]
extension API from JUnit Jupiter, which lets Spring provide dependency injection for test
constructors, test methods, and test lifecycle callback methods.
@@ -362,7 +362,7 @@ of `PlatformTransactionManager` within the test's `ApplicationContext`, you can
qualifier by using `@Transactional("myTxMgr")` or `@Transactional(transactionManager =
"myTxMgr")`, or `TransactionManagementConfigurer` can be implemented by an
`@Configuration` class. Consult the
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager(org.springframework.test.context.TestContext,java.lang.String)[javadoc
{spring-framework-api}/test/context/transaction/TestContextTransactionUtils.html#retrieveTransactionManager-org.springframework.test.context.TestContext-java.lang.String-[javadoc
for `TestContextTransactionUtils.retrieveTransactionManager()`] for details on the
algorithm used to look up a transaction manager in the test's `ApplicationContext`.
@@ -75,7 +75,7 @@ infrastructure and controller declarations and use it to handle requests via moc
and response objects, without a running server.
For WebFlux, use the following where the Spring `ApplicationContext` is passed to
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext(org.springframework.context.ApplicationContext)[WebHttpHandlerBuilder]
{spring-framework-api}/web/server/adapter/WebHttpHandlerBuilder.html#applicationContext-org.springframework.context.ApplicationContext-[WebHttpHandlerBuilder]
to create the xref:web/webflux/reactive-spring.adoc#webflux-web-handler-api[WebHandler chain] to handle
requests:
@@ -44,7 +44,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
and, consequently, browsers reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
{spring-framework-api}/web/reactive/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
use the WebFlux Java configuration to declare such mappings, which results in a single,
global map passed to all `HandlerMapping` implementations.
@@ -57,7 +57,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
The rules for combining global and local configuration are generally additive -- for example,
all global and all local origins. For those attributes where only a single value can be
accepted, such as `allowCredentials` and `maxAge`, the local overrides the global value. See
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
for more details.
[TIP]
@@ -448,7 +448,7 @@ Java::
----
@GetMapping
FragmentsRendering handle() {
return FragmentsRendering.fragment("posts").fragment("comments").build();
return FragmentsRendering.with("posts").fragment("comments").build();
}
----
@@ -458,7 +458,7 @@ Kotlin::
----
@GetMapping
fun handle(): FragmentsRendering {
return FragmentsRendering.fragment("posts").fragment("comments").build()
return FragmentsRendering.with("posts").fragment("comments").build()
}
----
======
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- The Jackson JSON and XML message converters use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
To enable RFC 9457 responses for Spring WebFlux exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
@@ -251,8 +251,7 @@ Kotlin::
======
TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and
mark it with `@Primary`, or mark the one declared in the MVC config with `@Fallback`, in
order to avoid conflict.
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC config.
[[webflux-config-content-negotiation]]
@@ -114,6 +114,6 @@ and others) and is equivalent to `required=false`.
| Any other argument
| If a method argument is not matched to any of the above, it is, by default, resolved as
a `@RequestParam` if it is a simple type, as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
or as a `@ModelAttribute`, otherwise.
|===
@@ -205,7 +205,7 @@ controller method xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation]
TIP: Using `@ModelAttribute` is optional. By default, any argument that is not a simple
value type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
@@ -74,6 +74,6 @@ When a `@RequestParam` annotation is declared on a `Map<String, String>` or
Note that use of `@RequestParam` is optional -- for example, to set its attributes. By
default, any argument that is a simple value type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
and is not resolved by any other argument resolver is treated as if it were annotated
with `@RequestParam`.
@@ -87,6 +87,6 @@ Reactor provides a dedicated operator for that, `Flux#collectList()`.
| Other return values
| If a return value remains unresolved in any other way, it is treated as a model
attribute, unless it is a simple type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
in which case it remains unresolved.
|===
@@ -89,7 +89,40 @@ Kotlin::
You can map requests by using glob patterns and wildcards:
include::partial$web/uri-patterns.adoc[leveloffset=+1]
[cols="2,3,5"]
|===
|Pattern |Description |Example
| `+?+`
| Matches one character
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
| `+*+`
| Matches zero or more characters within a path segment
| `+"/resources/*.png"+` matches `+"/resources/file.png"+`
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`
| `+**+`
| Matches zero or more path segments until the end of the path
| `+"/resources/**"+` matches `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is only allowed at the end of the path.
| `+{name}+`
| Matches a path segment and captures it as a variable named "name"
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
| `{name:[a-z]+}`
| Matches the regexp `[a-z]+` as a path variable named "name"
| `/projects/{project:[a-z]+}/versions` matches `/projects/spring/versions` but not `/projects/spring1/versions`
| `+{*path}+`
| Matches zero or more path segments until the end of the path and captures it as a variable named "path"
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
|===
Captured URI variables can be accessed with `@PathVariable`, as the following example shows:
@@ -7,23 +7,19 @@ Spring WebFlux has built-in xref:core/validation/validator.adoc[Validation] for
`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation].
Validation may be applied at one of two levels:
1. Java Bean Validation is applied individually to an
xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
1. xref:web/webflux/controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
xref:web/webflux/controller/ann-methods/requestbody.adoc[@RequestBody], and
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter
annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as
it is a command object rather than a container such as `Map` or `Collection`, it does not
have `Errors` or `BindingResult` immediately after in the method signature, and does not
otherwise require method validation (see next). `WebExchangeBindException` is the
exception raised when validating a method parameter individually.
xref:web/webflux/controller/ann-methods/multipart-forms.adoc[@RequestPart] argument
resolvers validate a method argument individually if the method parameter is annotated
with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or
`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be
discussed next). The exception raised in this case is `WebExchangeBindException`.
2. Java Bean Validation is applied to the method when `@Constraint` annotations such as
`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the
method for the return value, and it supersedes any validation that would be applied
otherwise to a method parameter individually because method validation covers both
method parameter constraints and nested constraints via `@Valid`.
`HandlerMethodValidationException` is the exception raised validation is applied
to the method.
2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared
directly on method parameters, or on the method (for the return value), then method
validation must be applied, and that supersedes validation at the method argument level
because method validation covers both method parameter constraints and nested constraints
via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`.
Applications must handle both `WebExchangeBindException` and
`HandlerMethodValidationException` as either may be raised depending on the controller
@@ -207,7 +207,7 @@ was not provided (for example, model attribute was returned) or an async return
view resolution scenarios. Explore the options in your IDE with code completion.
* `Model`, `Map`: Extra model attributes to be added to the model for the request.
* Any other: Any other return value (except for simple types, as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
is treated as a model attribute to be added to the model. The attribute name is derived
from the class name by using {spring-framework-api}/core/Conventions.html[conventions],
unless a handler method `@ModelAttribute` annotation is present.
@@ -418,23 +418,16 @@ See the section on xref:web/webflux-cors.adoc[CORS] and the xref:web/webflux-cor
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
Adding trailing slash variants to all mapping declarations is not the best way to handle this use case.
The `UrlHandlerFilter` web filter has been designed for this purpose. It can be configured to:
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* mutate the request to remove the trailing slash.
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* mutate the request to act as if the request was sent without a trailing slash and continue the processing of the request.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
[[webflux-exception-handler]]
== Exceptions
@@ -71,7 +71,7 @@ rejected. No CORS headers are added to the responses of simple and actual CORS r
and, consequently, browsers reject them.
Each `HandlerMapping` can be
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations(java.util.Map)[configured]
{spring-framework-api}/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations-java.util.Map-[configured]
individually with URL pattern-based `CorsConfiguration` mappings. In most cases, applications
use the MVC Java configuration or the XML namespace to declare such mappings, which results
in a single global map being passed to all `HandlerMapping` instances.
@@ -84,7 +84,7 @@ class- or method-level `@CrossOrigin` annotations (other handlers can implement
The rules for combining global and local configuration are generally additive -- for example,
all global and all local origins. For those attributes where only a single value can be
accepted, for example, `allowCredentials` and `maxAge`, the local overrides the global value. See
{spring-framework-api}/web/cors/CorsConfiguration.html#combine(org.springframework.web.cors.CorsConfiguration)[`CorsConfiguration#combine(CorsConfiguration)`]
{spring-framework-api}/web/cors/CorsConfiguration.html#combine-org.springframework.web.cors.CorsConfiguration-[`CorsConfiguration#combine(CorsConfiguration)`]
for more details.
[TIP]
@@ -48,7 +48,7 @@ Java::
----
@GetMapping
FragmentsRendering handle() {
return FragmentsRendering.fragment("posts").fragment("comments").build();
return FragmentsRendering.with("posts").fragment("comments").build();
}
----
@@ -58,7 +58,7 @@ Kotlin::
----
@GetMapping
fun handle(): FragmentsRendering {
return FragmentsRendering.fragment("posts").fragment("comments").build()
return FragmentsRendering.with("posts").fragment("comments").build()
}
----
======
@@ -3,10 +3,7 @@
[.small]#xref:web/webflux/reactive-spring.adoc#webflux-filters[See equivalent in the Reactive stack]#
In the Servlet API, you can add a `jakarta.servlet.Filter` to apply interception-style logic
before and after the rest of the processing chain of filters and the target `Servlet`.
The `spring-web` module has a number of built-in `Filter` implementations:
The `spring-web` module provides some useful filters:
* xref:web/webmvc/filters.adoc#filters-http-put[Form Data]
* xref:web/webmvc/filters.adoc#filters-forwarded-headers[Forwarded Headers]
@@ -14,19 +11,9 @@ The `spring-web` module has a number of built-in `Filter` implementations:
* xref:web/webmvc/filters.adoc#filters-cors[CORS]
* xref:web/webmvc/filters.adoc#filters.url-handler[URL Handler]
There are also base class implementations for use in Spring applications:
* `GenericFilterBean` -- base class for a `Filter` configured as a Spring bean;
integrates with the Spring `ApplicationContext` lifecycle.
* `OncePerRequestFilter` -- extension of `GenericFilterBean` that supports a single
invocation at the start of a request, i.e. during the `REQUEST` dispatch phase, and
ignoring further handling via `FORWARD` dispatches. The filter also provides control
over whether the `Filter` gets involved in `ASYNC` and `ERROR` dispatches.
Servlet filters can be configured in `web.xml` or via Servlet annotations.
In a Spring Boot application , you can
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare Filter's as beans]
and Boot will have them configured.
Servlet filters can be configured in the `web.xml` configuration file or using Servlet annotations.
If you are using Spring Boot, you can
{spring-boot-docs}/how-to/webserver.html#howto.webserver.add-servlet-filter-listener.spring-bean[declare them as beans and configure them as part of your application].
[[filters-http-put]]
@@ -120,26 +107,17 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors
== URL Handler
[.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]#
You may want your controller endpoints to match routes with or without a trailing slash in the URL path.
For example, both "GET /home" and "GET /home/" should be handled by a controller method annotated with `@GetMapping("/home")`.
In previous Spring Framework versions, Spring MVC could be configured to ignore trailing slashes in URL paths
when mapping incoming requests on controller methods. This could be done by enabling the `setUseTrailingSlashMatch`
option on the `PathMatchConfigurer`. This means that sending a "GET /home/" request would be handled by a controller
method annotated with `@GetMapping("/home")`.
Spring provides `UrlHandlerFilter` that removes the trailing slash from URL paths to ensure a consistent view of paths with or without a trailing slash.
This is important to avoid a mismatch between URL-based authorization decisions and web framework request mappings.
The filter can remove the trailing slash in one of a couple of ways:
This option has been retired, but applications are still expected to handle such requests in a safe way.
The `UrlHandlerFilter` Servlet filter has been designed for this purpose. It can be configured to:
* respond with an HTTP redirect status that sends clients to the same path without a trailing slash.
* wrap the request to remove the trailing slash.
NOTE: Historically Spring MVC supported trailing slash matching of URL paths.
This capability was deprecated in 6.0 for security reasons and removed in 7.0 with
`UrlHandlerFilter` providing a safer alternative.
* respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant.
* wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request.
Here is how you can instantiate and configure a `UrlHandlerFilter` for a blog application:
include-code::./UrlHandlerFilterConfiguration[tag=config,indent=0]
Keep in mind the following:
- the root path `"/"` is excluded from trailing slash handling.
- `@RequestMapping("/")` adds a trailing slash to a type-level mapping, and therefore will
not map when trailing slash handling applies; use `@RequestMapping` (no path attribute) instead.
@@ -422,7 +422,7 @@ reactive types from the controller method.
Reactive return values are handled as follows:
* A single-value promise is adapted to, similar to using `DeferredResult`. Examples
include `CompletionStage` (JDK), `Mono` (Reactor), and `Single` (RxJava).
include `CompletionStage` (JDK), Mono` (Reactor), and `Single` (RxJava).
* A multi-value stream with a streaming media type (such as `application/x-ndjson`
or `text/event-stream`) is adapted to, similar to using `ResponseBodyEmitter` or
`SseEmitter`. Examples include `Flux` (Reactor) or `Observable` (RxJava).
@@ -32,9 +32,9 @@ any `@RequestMapping` method to render an RFC 9457 response. This is processed a
- The `status` property of `ProblemDetail` determines the HTTP status.
- The `instance` property of `ProblemDetail` is set from the current URL path, if not
already set.
- The Jackson JSON and XML codecs use "application/problem+json" or
"application/problem+xml" respectively as the producible media types for `ProblemDetail`
to ensure they are favored for content negotiation.
- For content negotiation, the Jackson `HttpMessageConverter` prefers
"application/problem+json" over "application/json" when rendering a `ProblemDetail`,
and also falls back on it if no compatible media type is found.
To enable RFC 9457 responses for Spring MVC exceptions and for any
`ErrorResponseException`, extend `ResponseEntityExceptionHandler` and declare it as an
@@ -175,7 +175,7 @@ Message codes and arguments for each error are also resolved via `MessageSource`
| `TypeMismatchException`
| (default)
| `+{0}+` property name, `+{1}+` property value, `+{2}+` simple name of required type
| `+{0}+` property name, `+{1}+` property value
| `UnsatisfiedServletRequestParameterException`
| (default)
@@ -19,5 +19,4 @@ example shows:
include-code::./MyController[tag=snippet,indent=0]
TIP: If you need to have a `LocalValidatorFactoryBean` injected somewhere, create a bean and
mark it with `@Primary`, or mark the one declared in the MVC configuration with
`@Fallback`, in order to avoid conflict.
mark it with `@Primary` in order to avoid conflict with the one declared in the MVC configuration.
@@ -215,7 +215,7 @@ the content negotiation during the error handling phase will decide which conten
| Any other return value
| If a return value is not matched to any of the above and is not a simple type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
by default, it is treated as a model attribute to be added to the model. If it is a simple type,
it remains unresolved.
|===
@@ -135,6 +135,6 @@ and others) and is equivalent to `required=false`.
| Any other argument
| If a method argument is not matched to any of the earlier values in this table and it is
a simple type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]),
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]),
it is resolved as a `@RequestParam`. Otherwise, it is resolved as a `@ModelAttribute`.
|===
@@ -250,7 +250,7 @@ xref:web/webmvc/mvc-controller/ann-validation.adoc[Validation].
TIP: Using `@ModelAttribute` is optional. By default, any parameter that is not a simple
value type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty]
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty]
_AND_ that is not resolved by any other argument resolver is treated as an implicit `@ModelAttribute`.
WARNING: When compiling to a native image with GraalVM, the implicit `@ModelAttribute`
@@ -117,6 +117,6 @@ Kotlin::
Note that use of `@RequestParam` is optional (for example, to set its attributes).
By default, any argument that is a simple value type (as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty])
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty])
and is not resolved by any other argument resolver, is treated as if it were annotated
with `@RequestParam`.
@@ -98,6 +98,6 @@ supported for all return values.
| Other return values
| If a return value remains unresolved in any other way, it is treated as a model
attribute, unless it is a simple type as determined by
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty(java.lang.Class)[BeanUtils#isSimpleProperty],
{spring-framework-api}/beans/BeanUtils.html#isSimpleProperty-java.lang.Class-[BeanUtils#isSimpleProperty],
in which case it remains unresolved.
|===
@@ -103,9 +103,22 @@ Spring WebFlux. It was enabled for use in Spring MVC from version 5.3 and is ena
default from version 6.0. See xref:web/webmvc/mvc-config/path-matching.adoc[MVC config] for
customizations of path matching options.
You can map requests by using glob patterns and wildcards:
`PathPattern` supports the same pattern syntax as `AntPathMatcher`. In addition, it also
supports the capturing pattern, for example, `+{*spring}+`, for matching 0 or more path segments
at the end of a path. `PathPattern` also restricts the use of `+**+` for matching multiple
path segments such that it's only allowed at the end of a pattern. This eliminates many
cases of ambiguity when choosing the best matching pattern for a given request.
For full pattern syntax please refer to
{spring-framework-api}/web/util/pattern/PathPattern.html[PathPattern] and
{spring-framework-api}/util/AntPathMatcher.html[AntPathMatcher].
include::partial$web/uri-patterns.adoc[leveloffset=+1]
Some example patterns:
* `+"/resources/ima?e.png"+` - match one character in a path segment
* `+"/resources/*.png"+` - match zero or more characters in a path segment
* `+"/resources/**"+` - match multiple path segments
* `+"/projects/{project}/versions"+` - match a path segment and capture it as a variable
* `++"/projects/{project:[a-z]+}/versions"++` - match and capture a variable with a regex
Captured URI variables can be accessed with `@PathVariable`. For example:
@@ -221,7 +234,7 @@ When multiple patterns match a URL, the best match must be selected. This is don
one of the following depending on whether use of parsed `PathPattern` is enabled for use or not:
* {spring-framework-api}/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR[`PathPattern.SPECIFICITY_COMPARATOR`]
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator(java.lang.String)[`AntPathMatcher.getPatternComparator(String path)`]
* {spring-framework-api}/util/AntPathMatcher.html#getPatternComparator-java.lang.String-[`AntPathMatcher.getPatternComparator(String path)`]
Both help to sort patterns with more specific ones on top. A pattern is more specific if
it has a lower count of URI variables (counted as 1), single wildcards (counted as 1),
@@ -7,26 +7,22 @@ Spring MVC has built-in xref:core/validation/validator.adoc[validation] for
`@RequestMapping` methods, including xref:core/validation/beanvalidation.adoc[Java Bean Validation].
Validation may be applied at one of two levels:
1. Java Bean Validation is applied individually to an
xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
1. xref:web/webmvc/mvc-controller/ann-methods/modelattrib-method-args.adoc[@ModelAttribute],
xref:web/webmvc/mvc-controller/ann-methods/requestbody.adoc[@RequestBody], and
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] method parameter
annotated with `@jakarta.validation.Valid` or Spring's `@Validated` so long as
it is a command object rather than a container such as `Map` or `Collection`, it does not
have `Errors` or `BindingResult` immediately after in the method signature, and does not
otherwise require method validation (see next). `MethodArgumentNotValidException` is the
exception raised when validating a method parameter individually.
xref:web/webmvc/mvc-controller/ann-methods/multipart-forms.adoc[@RequestPart] argument
resolvers validate a method argument individually if the method parameter is annotated
with Jakarta `@Valid` or Spring's `@Validated`, _AND_ there is no `Errors` or
`BindingResult` parameter immediately after, _AND_ method validation is not needed (to be
discussed next). The exception raised in this case is `MethodArgumentNotValidException`.
2. Java Bean Validation is applied to the method when `@Constraint` annotations such as
`@Min`, `@NotBlank` and others are declared directly on method parameters, or on the
method for the return value, and it supersedes any validation that would be applied
otherwise to a method parameter individually because method validation covers both
method parameter constraints and nested constraints via `@Valid`.
`HandlerMethodValidationException` is the exception raised validation is applied
to the method.
2. When `@Constraint` annotations such as `@Min`, `@NotBlank` and others are declared
directly on method parameters, or on the method (for the return value), then method
validation must be applied, and that supersedes validation at the method argument level
because method validation covers both method parameter constraints and nested constraints
via `@Valid`. The exception raised in this case is `HandlerMethodValidationException`.
Applications should handle both `MethodArgumentNotValidException` and
`HandlerMethodValidationException` since either may be raised depending on the controller
Applications must handle both `MethodArgumentNotValidException` and
`HandlerMethodValidationException` as either may be raised depending on the controller
method signature. The two exceptions, however are designed to be very similar, and can be
handled with almost identical code. The main difference is that the former is for a single
object while the latter is for a list of method parameters.
@@ -3,7 +3,7 @@
[.small]#xref:web/webflux/uri-building.adoc[See equivalent in the Reactive stack]#
This section describes various options available in the Spring Framework to work with URIs.
This section describes various options available in the Spring Framework to work with URI's.
include::partial$web/web-uris.adoc[leveloffset=+1]
@@ -1,51 +0,0 @@
[cols="2,3,5"]
|===
|Pattern |Description |Example
| `spring`
| Literal pattern
| `+"/spring"+` matches `+"/spring"+`
| `+?+`
| Matches one character
| `+"/pages/t?st.html"+` matches `+"/pages/test.html"+` and `+"/pages/t3st.html"+`
| `+*+`
| Matches zero or more characters within a path segment
| `+"/resources/*.png"+` matches `+"/resources/file.png"+`
`+"/projects/*/versions"+` matches `+"/projects/spring/versions"+` but does not match `+"/projects/spring/boot/versions"+`.
`+"/projects/*"+` matches `+"/projects/spring"+` but does not match `+"/projects"+` as the path segment is not present.
| `+**+`
| Matches zero or more path segments
| `+"/resources/**"+` matches `+"/resources"+`, `+"/resources/file.png"+` and `+"/resources/images/file.png"+`
`+"/**/info"+` matches `+"/info"+`, `+"/spring/info"+` and `+"/spring/framework/info"+`
`+"/resources/**/file.png"+` is invalid as `+**+` is not allowed in the middle of the path.
`+"/**/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
| `+{name}+`
| Similar to `+*+`, but also captures the path segment as a variable named "name"
| `+"/projects/{project}/versions"+` matches `+"/projects/spring/versions"+` and captures `+project=spring+`
`+"/projects/{project}/versions"+` does not match `+"/projects/spring/framework/versions"+` as it captures a single path segment.
| `{name:[a-z]+}`
| Matches the regexp `"[a-z]+"` as a path variable named "name"
| `"/projects/{project:[a-z]+}/versions"` matches `"/projects/spring/versions"` but not `"/projects/spring1/versions"`
| `+{*path}+`
| Similar to `+**+`, but also captures the path segments as a variable named "path"
| `+"/resources/{*file}"+` matches `+"/resources/images/file.png"+` and captures `+file=/images/file.png+`
`+"{*path}/resources"+` matches `+"/spring/framework/resources"+` and captures `+path=/spring/framework+`
`+"/resources/{*path}/file.png"+` is invalid as `{*path}` is not allowed in the middle of the path.
`+"/{*path}/spring/**"+` is not allowed, as only a single `+**+`/`+{*path}+` instance is allowed per pattern.
|===
@@ -2,7 +2,7 @@
= UriComponents
[.small]#Spring MVC and Spring WebFlux#
`UriComponentsBuilder` helps to build URIs from URI templates with variables, as the following example shows:
`UriComponentsBuilder` helps to build URI's from URI templates with variables, as the following example shows:
[tabs]
======
@@ -128,7 +128,7 @@ Kotlin::
= UriBuilder
[.small]#Spring MVC and Spring WebFlux#
<<uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
<<web-uricomponents, `UriComponentsBuilder`>> implements `UriBuilder`. You can create a
`UriBuilder`, in turn, with a `UriBuilderFactory`. Together, `UriBuilderFactory` and
`UriBuilder` provide a pluggable mechanism to build URIs from URI templates, based on
shared configuration, such as a base URL, encoding preferences, and other details.
@@ -247,7 +247,7 @@ and treats deviations from the syntax as illegal.
https://github.com/web-platform-tests/wpt/tree/master/url[URL parsing algorithm] in the
https://url.spec.whatwg.org[WhatWG URL Living standard]. It provides lenient handling of
a wide range of cases of unexpected input. Browsers implement this in order to handle
leniently user typed URLs. For more details, see the URL Living Standard and URL parsing
leniently user typed URL's. For more details, see the URL Living Standard and URL parsing
https://github.com/web-platform-tests/wpt/tree/master/url[test cases].
By default, `RestClient`, `WebClient`, and `RestTemplate` use the RFC parser type, and
@@ -255,10 +255,10 @@ expect applications to provide with URL templates that conform to RFC syntax. To
that you can customize the `UriBuilderFactory` on any of the clients.
Applications and frameworks may further rely on `UriComponentsBuilder` for their own needs
to parse user provided URLs in order to inspect and possibly validated URI components
to parse user provided URL's in order to inspect and possibly validated URI components
such as the scheme, host, port, path, and query. Such components can decide to use the
WhatWG parser type in order to handle URLs more leniently, and to align with the way
browsers parse URIs, in case of a redirect to the input URL or if it is included in a
WhatWG parser type in order to handle URL's more leniently, and to align with the way
browsers parse URI's, in case of a redirect to the input URL or if it is included in a
response to a browser.
@@ -373,14 +373,14 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
String baseUrl = "https://example.com";
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl)
factory.setEncodingMode(EncodingMode.TEMPLATE_AND_VALUES);
// Customize the RestTemplate.
// Customize the RestTemplate..
RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(factory);
// Customize the WebClient.
// Customize the WebClient..
WebClient client = WebClient.builder().uriBuilderFactory(factory).build();
----
@@ -393,12 +393,12 @@ Kotlin::
encodingMode = EncodingMode.TEMPLATE_AND_VALUES
}
// Customize the RestTemplate.
// Customize the RestTemplate..
val restTemplate = RestTemplate().apply {
uriTemplateHandler = factory
}
// Customize the WebClient.
// Customize the WebClient..
val client = WebClient.builder().uriBuilderFactory(factory).build()
----
======
+6 -6
View File
@@ -1,11 +1,11 @@
{
"dependencies": {
"antora": "3.2.0-alpha.11",
"@antora/atlas-extension": "1.0.0-alpha.5",
"@antora/collector-extension": "1.0.2",
"antora": "3.2.0-alpha.4",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-alpha.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.14.7",
"fast-xml-parser": "5.3.8",
"@springio/asciidoctor-extensions": "1.0.0-alpha.17"
"@springio/antora-extensions": "1.14.2",
"fast-xml-parser": "4.5.2",
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
}
}
+10 -9
View File
@@ -200,7 +200,6 @@
See the License for the specific language governing permissions and
limitations under the License.
=======================================================================
SPRING FRAMEWORK ${version} SUBCOMPONENTS:
@@ -213,7 +212,7 @@ code for these subcomponents is subject to the terms and
conditions of the following licenses.
>>> ASM 9.9.1 (org.ow2.asm:asm:9.9.1):
>>> ASM 9.1 (org.ow2.asm:asm:9.1, org.ow2.asm:asm-commons:9.1):
Copyright (c) 2000-2011 INRIA, France Telecom
All rights reserved.
@@ -250,8 +249,10 @@ Copyright (c) 1999-2009, OW2 Consortium <https://www.ow2.org/>
>>> CGLIB 3.3 (cglib:cglib:3.3):
Per the LICENSE file in the CGLIB distribution, CGLIB 3.3 is licensed
under the Apache License, version 2.0, the text of which is included above.
Per the LICENSE file in the CGLIB JAR distribution downloaded from
https://github.com/cglib/cglib/releases/download/RELEASE_3_3_0/cglib-3.3.0.jar,
CGLIB 3.3 is licensed under the Apache License, version 2.0, the text of which
is included above.
>>> JavaPoet 1.13.0 (com.squareup:javapoet:1.13.0):
@@ -262,18 +263,18 @@ JavaPoet 1.13.0 is licensed under the Apache License, version 2.0, the text of
which is included above.
>>> Objenesis 3.5 (org.objenesis:objenesis:3.5):
>>> Objenesis 3.4 (org.objenesis:objenesis:3.4):
Per the LICENSE file in the Objenesis distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.5 is licensed under the
Per the LICENSE file in the Objenesis ZIP distribution downloaded from
http://objenesis.org/download.html, Objenesis 3.4 is licensed under the
Apache License, version 2.0, the text of which is included above.
Per the NOTICE file in the Objenesis distribution downloaded from
Per the NOTICE file in the Objenesis ZIP distribution downloaded from
http://objenesis.org/download.html and corresponding to section 4d of the
Apache License, Version 2.0, in this case for Objenesis:
Objenesis
Copyright 2006-2026 Joe Walnes, Henri Tremblay, Leonardo Mesquita
Copyright 2006-2019 Joe Walnes, Henri Tremblay, Leonardo Mesquita
===============================================================================
@@ -1,47 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.expressions.languageref.expressionsoperatorsoverloaded;
import org.springframework.expression.Operation;
import org.springframework.expression.OperatorOverloader;
import java.util.ArrayList;
import java.util.List;
public class ListConcatenation implements OperatorOverloader {
@Override
public boolean overridesOperation(Operation operation, Object left, Object right) {
return (operation == Operation.ADD && left instanceof List && right instanceof List);
}
@Override
@SuppressWarnings({"rawtypes", "unchecked"})
public Object operate(Operation operation, Object left, Object right) {
if (operation == Operation.ADD &&
left instanceof List list1 && right instanceof List list2) {
List result = new ArrayList(list1);
result.addAll(list2);
return result;
}
throw new UnsupportedOperationException(
"No overload for operation %s and operands [%s] and [%s]"
.formatted(operation, left, right));
}
}
@@ -1,37 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.docs.core.expressions.languageref.expressionsoperatorsoverloaded
import org.springframework.expression.Operation
import org.springframework.expression.OperatorOverloader
class ListConcatenation: OperatorOverloader {
override fun overridesOperation(operation: Operation, left: Any?, right: Any?): Boolean {
return operation == Operation.ADD && left is List<*> && right is List<*>
}
override fun operate(operation: Operation, left: Any?, right: Any?): Any {
if (operation == Operation.ADD && left is List<*> && right is List<*>) {
return left + right
}
throw UnsupportedOperationException(
"No overload for operation $operation and operands [$left] and [$right]")
}
}
+24 -24
View File
@@ -7,33 +7,33 @@ javaPlatform {
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.5"))
api(platform("io.micrometer:micrometer-bom:1.15.10"))
api(platform("io.netty:netty-bom:4.1.130.Final"))
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4.1"))
api(platform("io.micrometer:micrometer-bom:1.14.12"))
api(platform("io.netty:netty-bom:4.1.128.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.16"))
api(platform("io.projectreactor:reactor-bom:2024.0.11"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.30"))
api(platform("org.apache.groovy:groovy-bom:4.0.28"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.7"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.33"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.33"))
api(platform("org.assertj:assertj-bom:3.27.6"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.28"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.28"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.14.3"))
api(platform("org.mockito:mockito-bom:5.22.0"))
api(platform("org.junit:junit-bom:5.14.0"))
api(platform("org.mockito:mockito-bom:5.20.0"))
constraints {
api("com.fasterxml:aalto-xml:1.3.4")
api("com.fasterxml.woodstox:woodstox-core:6.7.0")
api("com.github.ben-manes.caffeine:caffeine:3.2.3")
api("com.github.ben-manes.caffeine:caffeine:3.2.2")
api("com.github.librepdf:openpdf:1.3.43")
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.13.2")
api("com.google.protobuf:protobuf-java-util:4.34.0")
api("com.h2database:h2:2.4.240")
api("com.jayway.jsonpath:json-path:2.10.0")
api("com.google.protobuf:protobuf-java-util:4.32.1")
api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
api("com.rometools:rome:1.19.0")
api("com.squareup.okhttp3:mockwebserver:3.14.9")
@@ -50,7 +50,7 @@ dependencies {
api("io.mockk:mockk:1.13.17")
api("io.projectreactor.netty:reactor-netty5-http:2.0.0-M3")
api("io.projectreactor.tools:blockhound:1.0.8.RELEASE")
api("io.r2dbc:r2dbc-h2:1.1.0.RELEASE")
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.12")
@@ -90,11 +90,11 @@ dependencies {
api("junit:junit:4.13.2")
api("net.sf.jopt-simple:jopt-simple:5.0.4")
api("org.apache-extras.beanshell:bsh:2.0b6")
api("org.apache.activemq:activemq-broker:5.17.7")
api("org.apache.activemq:activemq-kahadb-store:5.17.7")
api("org.apache.activemq:activemq-stomp:5.17.7")
api("org.apache.activemq:artemis-jakarta-client:2.42.0")
api("org.apache.activemq:artemis-junit-5:2.42.0")
api("org.apache.activemq:activemq-broker:5.17.7")
api("org.apache.activemq:activemq-kahadb-store:5.17.7")
api("org.apache.activemq:activemq-stomp:5.17.7")
api("org.apache.activemq:artemis-jakarta-client:2.42.0")
api("org.apache.activemq:artemis-junit-5:2.42.0")
api("org.apache.commons:commons-pool2:2.9.0")
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
@@ -129,7 +129,7 @@ dependencies {
api("org.hibernate:hibernate-core-jakarta:5.6.15.Final")
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.4")
api("org.htmlunit:htmlunit:4.21.0")
api("org.htmlunit:htmlunit:4.17.0")
api("org.javamoney:moneta:1.4.4")
api("org.jruby:jruby:9.4.13.0")
api("org.junit.support:testng-engine:1.0.5")
@@ -137,8 +137,8 @@ dependencies {
api("org.ogce:xpp3:1.1.6")
api("org.python:jython-standalone:2.7.4")
api("org.quartz-scheduler:quartz:2.3.2")
api("org.seleniumhq.selenium:htmlunit3-driver:4.41.0")
api("org.seleniumhq.selenium:selenium-java:4.41.0")
api("org.seleniumhq.selenium:htmlunit3-driver:4.36.1")
api("org.seleniumhq.selenium:selenium-java:4.36.0")
api("org.skyscreamer:jsonassert:1.5.3")
api("org.slf4j:slf4j-api:2.0.17")
api("org.testng:testng:7.11.0")
@@ -147,6 +147,6 @@ dependencies {
api("org.webjars:webjars-locator-lite:1.1.0")
api("org.xmlunit:xmlunit-assertj:2.10.4")
api("org.xmlunit:xmlunit-matchers:2.10.4")
api("org.yaml:snakeyaml:2.6")
api("org.yaml:snakeyaml:2.5")
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.17
version=6.2.12
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -62,7 +62,7 @@ public interface ProxyMethodInvocation extends MethodInvocation {
MethodInvocation invocableClone(Object... arguments);
/**
* Set the arguments to be used on subsequent invocations in any advice
* Set the arguments to be used on subsequent invocations in the any advice
* in this chain.
* @param arguments the argument array
*/
@@ -41,8 +41,8 @@ abstract class CoroutinesUtils {
}
}
@SuppressWarnings({"rawtypes", "unchecked"})
@Nullable
@SuppressWarnings({"unchecked", "rawtypes"})
static Object awaitSingleOrNull(@Nullable Object value, Object continuation) {
return MonoKt.awaitSingleOrNull(value instanceof Mono mono ? mono : Mono.justOrEmpty(value),
(Continuation<Object>) continuation);
@@ -489,9 +489,6 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
@Override
@Nullable
public Class<?> getPropertyType(String propertyName) throws BeansException {
if (this.wrappedObject == null) {
return null;
}
try {
PropertyHandler ph = getPropertyHandler(propertyName);
if (ph != null) {
@@ -192,7 +192,7 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements
@Override
@Nullable
protected PropertyHandler getLocalPropertyHandler(String propertyName) {
protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) {
PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName);
return (pd != null ? new BeanPropertyHandler((GenericTypeAwarePropertyDescriptor) pd) : null);
}
@@ -73,7 +73,7 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
@Override
@Nullable
protected PropertyHandler getLocalPropertyHandler(String propertyName) {
protected FieldPropertyHandler getLocalPropertyHandler(String propertyName) {
FieldPropertyHandler propertyHandler = this.fieldMap.get(propertyName);
if (propertyHandler == null) {
Field field = ReflectionUtils.findField(getWrappedClass(), propertyName);
@@ -26,7 +26,6 @@ import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
@@ -36,7 +35,6 @@ import org.springframework.util.StringUtils;
*
* @author Chris Beams
* @author Juergen Hoeller
* @author Sam Brannen
*/
abstract class PropertyDescriptorUtils {
@@ -101,13 +99,14 @@ abstract class PropertyDescriptorUtils {
}
else {
Method readMethod = pd.getReadMethod();
if (readMethod == null || readMethod.getReturnType().isAssignableFrom(method.getReturnType())) {
if (readMethod == null ||
(readMethod.getReturnType() == method.getReturnType() && method.getName().startsWith("is"))) {
pd.setReadMethod(method);
}
}
}
else {
pd = new BasicPropertyDescriptor(propertyName, beanClass, (!setter ? method : null), (setter ? method : null));
pd = new BasicPropertyDescriptor(propertyName, (!setter ? method : null), (setter ? method : null));
pdMap.put(propertyName, pd);
}
}
@@ -265,8 +264,6 @@ abstract class PropertyDescriptorUtils {
*/
private static class BasicPropertyDescriptor extends PropertyDescriptor {
private final Class<?> beanClass;
@Nullable
private Method readMethod;
@@ -275,11 +272,10 @@ abstract class PropertyDescriptorUtils {
private final List<Method> alternativeWriteMethods = new ArrayList<>();
public BasicPropertyDescriptor(String propertyName, Class<?> beanClass, @Nullable Method readMethod, @Nullable Method writeMethod)
public BasicPropertyDescriptor(String propertyName, @Nullable Method readMethod, @Nullable Method writeMethod)
throws IntrospectionException {
super(propertyName, readMethod, writeMethod);
this.beanClass = beanClass;
}
@Override
@@ -311,24 +307,14 @@ abstract class PropertyDescriptorUtils {
public Method getWriteMethod() {
if (this.writeMethod == null && !this.alternativeWriteMethods.isEmpty()) {
if (this.readMethod == null) {
this.writeMethod = this.alternativeWriteMethods.get(0);
return this.alternativeWriteMethods.get(0);
}
else {
for (Method method : this.alternativeWriteMethods) {
// Check subtype match first.
if (this.readMethod.getReturnType().isAssignableFrom(method.getParameterTypes()[0])) {
this.writeMethod = method;
break;
}
// Check exact match against resolved generic parameter type as a fallback.
if (!(method.getGenericParameterTypes()[0] instanceof Class<?>)) {
Class<?> resolvedParameterType =
ResolvableType.forMethodParameter(method, 0, this.beanClass).toClass();
if (this.readMethod.getReturnType().equals(resolvedParameterType)) {
this.writeMethod = method;
break;
}
}
}
}
}
@@ -336,4 +322,5 @@ abstract class PropertyDescriptorUtils {
}
}
}
@@ -566,7 +566,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
}
final List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = ClassUtils.getUserClass(clazz);
Class<?> targetClass = clazz;
do {
final List<InjectionMetadata.InjectedElement> fieldElements = new ArrayList<>();
@@ -586,11 +586,12 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
final List<InjectionMetadata.InjectedElement> methodElements = new ArrayList<>();
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
if (method.isBridge()) {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
MergedAnnotation<?> ann = findAutowiredAnnotation(method);
if (ann != null && method.equals(BridgeMethodResolver.getMostSpecificMethod(method, clazz))) {
MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
if (logger.isInfoEnabled()) {
logger.info("Autowired annotation is not supported on static methods: " + method);
@@ -608,7 +609,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
}
}
boolean required = determineRequiredStatus(ann);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method, clazz);
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
methodElements.add(new AutowiredMethodElement(method, required, pd));
}
});
@@ -180,6 +180,7 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
* {@code true} if a qualifier has been found and matched,
* {@code null} if no qualifier has been found at all
*/
@Nullable
protected Boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
boolean qualifierFound = false;
@@ -374,17 +375,6 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
return true;
}
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
for (Annotation annotation : methodParam.getMethodAnnotations()) {
if (isQualifier(annotation.annotationType())) {
return true;
}
}
}
}
return false;
}
@@ -29,7 +29,6 @@ import org.springframework.aot.generate.ValueCodeGenerator.Delegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.CollectionDelegate;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates.MapDelegate;
import org.springframework.beans.factory.config.AutowiredPropertyMarker;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.config.TypedStringValue;
@@ -58,7 +57,6 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
* <li>{@link LinkedHashMap}</li>
* <li>{@link BeanReference}</li>
* <li>{@link TypedStringValue}</li>
* <li>{@link AutowiredPropertyMarker}</li>
* </ul>
* When combined with {@linkplain ValueCodeGeneratorDelegates#INSTANCES the
* delegates for common value types}, this should be added first as they have
@@ -70,8 +68,7 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
new ManagedMapDelegate(),
new LinkedHashMapDelegate(),
new BeanReferenceDelegate(),
new TypedStringValueDelegate(),
new AutowiredPropertyMarkerDelegate()
new TypedStringValueDelegate()
);
@@ -219,20 +216,4 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
return valueCodeGenerator.generateCode(value);
}
}
/**
* {@link Delegate} for {@link AutowiredPropertyMarker} types.
*/
private static class AutowiredPropertyMarkerDelegate implements Delegate {
@Override
@Nullable
public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) {
if (value instanceof AutowiredPropertyMarker) {
return CodeBlock.of("$T.INSTANCE", AutowiredPropertyMarker.class);
}
return null;
}
}
}
@@ -40,13 +40,6 @@ import org.springframework.beans.BeansException;
* A {@code BeanFactoryPostProcessor} may also be registered programmatically
* with a {@code ConfigurableApplicationContext}.
*
* <p>When registering a {@code BeanFactoryPostProcessor} via an {@code @Bean} method
* in a {@code @Configuration} class, use a {@code static} method to avoid eager
* initialization of other beans in the configuration class. See the
* "BeanFactoryPostProcessor-returning {@code @Bean} methods" section in
* {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details
* and an example.
*
* <h3>Ordering</h3>
* <p>{@code BeanFactoryPostProcessor} beans that are autodetected in an
* {@code ApplicationContext} will be ordered according to
@@ -34,14 +34,6 @@ import org.springframework.lang.Nullable;
* created. A plain {@code BeanFactory} allows for programmatic registration of
* post-processors, applying them to all beans created through the bean factory.
*
* <p>When registering a {@code BeanPostProcessor} via an {@code @Bean} method in
* a {@code @Configuration} class, use a {@code static} method with ideally no
* dependencies in order to avoid eager initialization that can make other beans
* ineligible for full post-processing. See the "BeanPostProcessor-returning
* {@code @Bean} methods" section in
* {@link org.springframework.context.annotation.Bean @Bean}'s javadoc for details
* and an example.
*
* <h3>Ordering</h3>
* <p>{@code BeanPostProcessor} beans that are autodetected in an
* {@code ApplicationContext} will be ordered according to
@@ -32,6 +32,7 @@ import org.springframework.util.StringUtils;
* @author Juergen Hoeller
* @author Rob Harrop
* @since 1.1
* @see PropertiesBeanDefinitionReader
* @see org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader
*/
public abstract class BeanDefinitionReaderUtils {
@@ -43,6 +43,7 @@ import org.springframework.core.AliasRegistry;
* @see DefaultListableBeanFactory
* @see org.springframework.context.support.GenericApplicationContext
* @see org.springframework.beans.factory.xml.XmlBeanDefinitionReader
* @see PropertiesBeanDefinitionReader
*/
public interface BeanDefinitionRegistry extends AliasRegistry {
@@ -1653,8 +1653,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
return doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
@SuppressWarnings("NullAway") // Dataflow analysis limitation
@Nullable
@SuppressWarnings("NullAway")
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
@@ -1991,8 +1991,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
for (String candidate : candidateNames) {
if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) &&
(!multiple || matchesBeanName(candidate, descriptor.getDependencyName()) ||
getAutowireCandidateResolver().hasQualifier(descriptor))) {
(!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) {
addCandidateEntry(result, candidate, descriptor, requiredType);
}
}
@@ -2263,12 +2262,12 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
/**
* Determine whether the given dependency name matches the bean name or the aliases
* Determine whether the given candidate name matches the bean name or the aliases
* stored in this bean definition.
*/
protected boolean matchesBeanName(String beanName, @Nullable String dependencyName) {
return (dependencyName != null &&
(dependencyName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), dependencyName)));
protected boolean matchesBeanName(String beanName, @Nullable String candidateName) {
return (candidateName != null &&
(candidateName.equals(beanName) || ObjectUtils.containsElement(getAliases(beanName), candidateName)));
}
/**
@@ -585,7 +585,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
public void registerContainedBean(String containedBeanName, String containingBeanName) {
synchronized (this.containedBeanMap) {
Set<String> containedBeans =
this.containedBeanMap.computeIfAbsent(containingBeanName, key -> new LinkedHashSet<>(8));
this.containedBeanMap.computeIfAbsent(containingBeanName, k -> new LinkedHashSet<>(8));
if (!containedBeans.add(containedBeanName)) {
return;
}
@@ -604,7 +604,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
synchronized (this.dependentBeanMap) {
Set<String> dependentBeans =
this.dependentBeanMap.computeIfAbsent(canonicalName, key -> new LinkedHashSet<>(8));
this.dependentBeanMap.computeIfAbsent(canonicalName, k -> new LinkedHashSet<>(8));
if (!dependentBeans.add(dependentBeanName)) {
return;
}
@@ -612,7 +612,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
synchronized (this.dependenciesForBeanMap) {
Set<String> dependenciesForBean =
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, key -> new LinkedHashSet<>(8));
this.dependenciesForBeanMap.computeIfAbsent(dependentBeanName, k -> new LinkedHashSet<>(8));
dependenciesForBean.add(canonicalName);
}
}
@@ -74,10 +74,10 @@ import org.springframework.util.StringUtils;
* @author Rob Harrop
* @since 26.11.2003
* @see DefaultListableBeanFactory
* @deprecated in favor of Spring's common bean definition formats and/or
* custom BeanDefinitionReader implementations
* @deprecated as of 5.3, in favor of Spring's common bean definition formats
* and/or custom reader implementations
*/
@Deprecated(since = "5.3")
@Deprecated
public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader {
/**
@@ -472,7 +472,7 @@ abstract class AbstractPropertyAccessorTests {
assertThat(target.getAge()).as("age is OK").isEqualTo(age);
assertThat(name).as("name is OK").isEqualTo(target.getName());
accessor.setPropertyValues(new MutablePropertyValues());
// Check it's unchanged
// Check its unchanged
assertThat(target.getAge()).as("age is OK").isEqualTo(age);
assertThat(name).as("name is OK").isEqualTo(target.getName());
}
@@ -33,12 +33,10 @@ import java.util.List;
import java.util.Locale;
import java.util.UUID;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.PropertyDescriptorUtilsPropertyResolutionTests.ServiceWithOverriddenGetterAndOverloadedSetter;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.testfixture.beans.DerivedTestBean;
@@ -69,14 +67,14 @@ class BeanUtilsTests {
@Test
void instantiateClassGivenInterface() {
assertThatExceptionOfType(FatalBeanException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(List.class));
assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() ->
BeanUtils.instantiateClass(List.class));
}
@Test
void instantiateClassGivenClassWithoutDefaultConstructor() {
assertThatExceptionOfType(FatalBeanException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(CustomDateEditor.class));
assertThatExceptionOfType(FatalBeanException.class).isThrownBy(() ->
BeanUtils.instantiateClass(CustomDateEditor.class));
}
@Test // gh-22531
@@ -93,16 +91,16 @@ class BeanUtilsTests {
void instantiateClassWithFewerArgsThanParameters() throws NoSuchMethodException {
Constructor<BeanWithPrimitiveTypes> constructor = getBeanWithPrimitiveTypesConstructor();
assertThatExceptionOfType(BeanInstantiationException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(constructor, null, null, "foo"));
assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() ->
BeanUtils.instantiateClass(constructor, null, null, "foo"));
}
@Test // gh-22531
void instantiateClassWithMoreArgsThanParameters() throws NoSuchMethodException {
Constructor<BeanWithPrimitiveTypes> constructor = getBeanWithPrimitiveTypesConstructor();
assertThatExceptionOfType(BeanInstantiationException.class)
.isThrownBy(() -> BeanUtils.instantiateClass(constructor, null, null, null, null, null, null, null, null, "foo", null));
assertThatExceptionOfType(BeanInstantiationException.class).isThrownBy(() ->
BeanUtils.instantiateClass(constructor, null, null, null, null, null, null, null, null, "foo", null));
}
@Test // gh-22531, gh-27390
@@ -159,6 +157,267 @@ class BeanUtilsTests {
assertThat(BeanUtils.findEditorByConvention(Resource.class).getClass()).isEqualTo(ResourceEditor.class);
}
@Test
void copyProperties() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes1() throws Exception {
DerivedTestBean tb = new DerivedTestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes2() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
DerivedTestBean tb2 = new DerivedTestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
/**
* {@code Integer} can be copied to {@code Number}.
*/
@Test
void copyPropertiesFromSubTypeToSuperType() {
IntegerHolder integerHolder = new IntegerHolder();
integerHolder.setNumber(42);
NumberHolder numberHolder = new NumberHolder();
BeanUtils.copyProperties(integerHolder, numberHolder);
assertThat(integerHolder.getNumber()).isEqualTo(42);
assertThat(numberHolder.getNumber()).isEqualTo(42);
}
/**
* {@code List<Integer>} can be copied to {@code List<Integer>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
BeanUtils.copyProperties(integerListHolder1, integerListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(integerListHolder2.getList()).containsExactly(42);
}
/**
* {@code List<?>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromWildcardToWildcard() {
List<?> list = List.of("foo", 42);
WildcardListHolder1 wildcardListHolder1 = new WildcardListHolder1();
wildcardListHolder1.setList(list);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
assertThat(wildcardListHolder2.getList()).isEmpty();
BeanUtils.copyProperties(wildcardListHolder1, wildcardListHolder2);
assertThat(wildcardListHolder1.getList()).isEqualTo(list);
assertThat(wildcardListHolder2.getList()).isEqualTo(list);
}
/**
* {@code List<Integer>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(wildcardListHolder2.getList()).isEqualTo(List.of(42));
}
/**
* {@code List<Integer>} can be copied to {@code List<? extends Number>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder();
BeanUtils.copyProperties(integerListHolder1, numberListHolder);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEqualTo(List.of(42));
}
/**
* {@code Number} can NOT be copied to {@code Integer}.
*/
@Test
void copyPropertiesDoesNotCopyFromSuperTypeToSubType() {
NumberHolder numberHolder = new NumberHolder();
numberHolder.setNumber(42);
IntegerHolder integerHolder = new IntegerHolder();
BeanUtils.copyProperties(numberHolder, integerHolder);
assertThat(numberHolder.getNumber()).isEqualTo(42);
assertThat(integerHolder.getNumber()).isNull();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Long>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatches() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
LongListHolder longListHolder = new LongListHolder();
BeanUtils.copyProperties(integerListHolder, longListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(longListHolder.getList()).isEmpty();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Number>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
NumberListHolder numberListHolder = new NumberListHolder();
BeanUtils.copyProperties(integerListHolder, numberListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEmpty();
}
@Test // gh-26531
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
Order original = new Order("test", List.of("foo", "bar"));
// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
assertThat(proxy.getLineItems()).containsExactly("foo", "bar");
// Copy from proxy to target.
Order target = new Order();
BeanUtils.copyProperties(proxy, target);
assertThat(target.getId()).isEqualTo("test");
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}
@Test // gh-32888
public void copyPropertiesWithGenericCglibClass() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
User user = (User) enhancer.create();
user.setId(1);
user.setName("proxy");
user.setAddress("addr");
User target = new User();
BeanUtils.copyProperties(user, target);
assertThat(target.getId()).isEqualTo(user.getId());
assertThat(target.getName()).isEqualTo(user.getName());
assertThat(target.getAddress()).isEqualTo(user.getAddress());
}
@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "touchy" should not be copied: it's not defined in ITestBean
BeanUtils.copyProperties(tb, tb2, ITestBean.class);
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age copied").isEqualTo(32);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnore() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "spouse", "touchy", "age" should not be copied
BeanUtils.copyProperties(tb, tb2, "spouse", "touchy", "age");
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age still empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnoredNonExistingProperty() {
NameAndSpecialProperty source = new NameAndSpecialProperty();
source.setName("name");
TestBean target = new TestBean();
BeanUtils.copyProperties(source, target, "specialProperty");
assertThat(target.getName()).isEqualTo("name");
}
@Test
void copyPropertiesWithInvalidProperty() {
InvalidProperty source = new InvalidProperty();
source.setName("name");
source.setFlag1(true);
source.setFlag2(true);
InvalidProperty target = new InvalidProperty();
BeanUtils.copyProperties(source, target);
assertThat(target.getName()).isEqualTo("name");
assertThat((boolean) target.getFlag1()).isTrue();
assertThat(target.getFlag2()).isTrue();
}
@Test
void resolveSimpleSignature() throws Exception {
Method desiredMethod = MethodSignatureBean.class.getMethod("doSomething");
@@ -168,14 +427,14 @@ class BeanUtilsTests {
@Test
void resolveInvalidSignatureEndParen() {
assertThatIllegalArgumentException()
.isThrownBy(() -> BeanUtils.resolveSignature("doSomething(", MethodSignatureBean.class));
assertThatIllegalArgumentException().isThrownBy(() ->
BeanUtils.resolveSignature("doSomething(", MethodSignatureBean.class));
}
@Test
void resolveInvalidSignatureStartParen() {
assertThatIllegalArgumentException()
.isThrownBy(() -> BeanUtils.resolveSignature("doSomething)", MethodSignatureBean.class));
assertThatIllegalArgumentException().isThrownBy(() ->
BeanUtils.resolveSignature("doSomething)", MethodSignatureBean.class));
}
@Test
@@ -279,283 +538,6 @@ class BeanUtilsTests {
}
@Nested
class CopyPropertiesTests {
@Test
void copyProperties() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes1() throws Exception {
DerivedTestBean tb = new DerivedTestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
TestBean tb2 = new TestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
@Test
void copyPropertiesWithDifferentTypes2() throws Exception {
TestBean tb = new TestBean();
tb.setName("rod");
tb.setAge(32);
tb.setTouchy("touchy");
DerivedTestBean tb2 = new DerivedTestBean();
assertThat(tb2.getName()).as("Name empty").isNull();
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
BeanUtils.copyProperties(tb, tb2);
assertThat(tb2.getName()).as("Name copied").isEqualTo(tb.getName());
assertThat(tb2.getAge()).as("Age copied").isEqualTo(tb.getAge());
assertThat(tb2.getTouchy()).as("Touchy copied").isEqualTo(tb.getTouchy());
}
/**
* {@code Integer} can be copied to {@code Number}.
*/
@Test
void copyPropertiesFromSubTypeToSuperType() {
IntegerHolder integerHolder = new IntegerHolder();
integerHolder.setNumber(42);
NumberHolder numberHolder = new NumberHolder();
BeanUtils.copyProperties(integerHolder, numberHolder);
assertThat(integerHolder.getNumber()).isEqualTo(42);
assertThat(numberHolder.getNumber()).isEqualTo(42);
}
/**
* {@code List<Integer>} can be copied to {@code List<Integer>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToInteger() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
IntegerListHolder2 integerListHolder2 = new IntegerListHolder2();
BeanUtils.copyProperties(integerListHolder1, integerListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(integerListHolder2.getList()).containsExactly(42);
}
/**
* {@code List<?>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromWildcardToWildcard() {
List<?> list = List.of("foo", 42);
WildcardListHolder1 wildcardListHolder1 = new WildcardListHolder1();
wildcardListHolder1.setList(list);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
assertThat(wildcardListHolder2.getList()).isEmpty();
BeanUtils.copyProperties(wildcardListHolder1, wildcardListHolder2);
assertThat(wildcardListHolder1.getList()).isEqualTo(list);
assertThat(wildcardListHolder2.getList()).isEqualTo(list);
}
/**
* {@code List<Integer>} can be copied to {@code List<?>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesFromIntegerToWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
WildcardListHolder2 wildcardListHolder2 = new WildcardListHolder2();
BeanUtils.copyProperties(integerListHolder1, wildcardListHolder2);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(wildcardListHolder2.getList()).isEqualTo(List.of(42));
}
/**
* {@code List<Integer>} can be copied to {@code List<? extends Number>}.
*/
@Test
void copyPropertiesHonorsGenericTypeMatchesForUpperBoundedWildcard() {
IntegerListHolder1 integerListHolder1 = new IntegerListHolder1();
integerListHolder1.getList().add(42);
NumberUpperBoundedWildcardListHolder numberListHolder = new NumberUpperBoundedWildcardListHolder();
BeanUtils.copyProperties(integerListHolder1, numberListHolder);
assertThat(integerListHolder1.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEqualTo(List.of(42));
}
/**
* {@code Number} can NOT be copied to {@code Integer}.
*/
@Test
void copyPropertiesDoesNotCopyFromSuperTypeToSubType() {
NumberHolder numberHolder = new NumberHolder();
numberHolder.setNumber(42);
IntegerHolder integerHolder = new IntegerHolder();
BeanUtils.copyProperties(numberHolder, integerHolder);
assertThat(numberHolder.getNumber()).isEqualTo(42);
assertThat(integerHolder.getNumber()).isNull();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Long>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatches() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
LongListHolder longListHolder = new LongListHolder();
BeanUtils.copyProperties(integerListHolder, longListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(longListHolder.getList()).isEmpty();
}
/**
* {@code List<Integer>} can NOT be copied to {@code List<Number>}.
*/
@Test
void copyPropertiesDoesNotHonorGenericTypeMismatchesFromSubTypeToSuperType() {
IntegerListHolder1 integerListHolder = new IntegerListHolder1();
integerListHolder.getList().add(42);
NumberListHolder numberListHolder = new NumberListHolder();
BeanUtils.copyProperties(integerListHolder, numberListHolder);
assertThat(integerListHolder.getList()).containsExactly(42);
assertThat(numberListHolder.getList()).isEmpty();
}
@Test // gh-26531
void copyPropertiesIgnoresGenericsIfSourceOrTargetHasUnresolvableGenerics() throws Exception {
Order original = new Order("test", List.of("foo", "bar"));
// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
assertThat(proxy.getLineItems()).containsExactly("foo", "bar");
// Copy from proxy to target.
Order target = new Order();
BeanUtils.copyProperties(proxy, target);
assertThat(target.getId()).isEqualTo("test");
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}
@Test // gh-32888
void copyPropertiesWithGenericCglibClass() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
User user = (User) enhancer.create();
user.setId(1);
user.setName("proxy");
user.setAddress("addr");
User target = new User();
BeanUtils.copyProperties(user, target);
assertThat(target.getId()).isEqualTo(user.getId());
assertThat(target.getName()).isEqualTo(user.getName());
assertThat(target.getAddress()).isEqualTo(user.getAddress());
}
@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "touchy" should not be copied: it's not defined in ITestBean
BeanUtils.copyProperties(tb, tb2, ITestBean.class);
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age copied").isEqualTo(32);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnore() throws Exception {
TestBean tb = new TestBean();
assertThat(tb.getName()).as("Name empty").isNull();
tb.setAge(32);
tb.setTouchy("bla");
TestBean tb2 = new TestBean();
tb2.setName("rod");
assertThat(tb2.getAge()).as("Age empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy empty").isNull();
// "spouse", "touchy", "age" should not be copied
BeanUtils.copyProperties(tb, tb2, "spouse", "touchy", "age");
assertThat(tb2.getName()).as("Name copied").isNull();
assertThat(tb2.getAge()).as("Age still empty").isEqualTo(0);
assertThat(tb2.getTouchy()).as("Touchy still empty").isNull();
}
@Test
void copyPropertiesWithIgnoredNonExistingProperty() {
NameAndSpecialProperty source = new NameAndSpecialProperty();
source.setName("name");
TestBean target = new TestBean();
BeanUtils.copyProperties(source, target, "specialProperty");
assertThat(target.getName()).isEqualTo("name");
}
@Test
void copyPropertiesWithInvalidProperty() {
InvalidProperty source = new InvalidProperty();
source.setName("name");
source.setFlag1(true);
source.setFlag2(true);
InvalidProperty target = new InvalidProperty();
BeanUtils.copyProperties(source, target);
assertThat(target.getName()).isEqualTo("name");
assertThat((boolean) target.getFlag1()).isTrue();
assertThat(target.getFlag2()).isTrue();
}
@Test // gh-36019
void copyPropertiesHonorsGenericsInTypeHieararchyAndIgnoresOverloadedSetterMethod() {
var source = new ServiceWithOverriddenGetterAndOverloadedSetter();
var target = new ServiceWithOverriddenGetterAndOverloadedSetter();
source.setId(1);
BeanUtils.copyProperties(source, target);
assertThat(target.getId()).isEqualTo(source.getId()).isEqualTo("1");
}
}
public record RecordWithMultiplePublicConstructors(String value, String name) {
@SuppressWarnings("unused")
public RecordWithMultiplePublicConstructors(String value) {
@@ -1,265 +0,0 @@
/*
* Copyright 2002-present the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Named;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.Parameter;
import org.junit.jupiter.params.ParameterizedClass;
import org.junit.jupiter.params.provider.FieldSource;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assumptions.assumeThat;
import static org.assertj.core.api.SoftAssertions.assertSoftly;
import static org.junit.jupiter.api.Named.named;
/**
* Unit tests for property descriptor resolution via
* {@link PropertyDescriptorUtils#determineBasicProperties(Class)}.
*
* <p>Results are compared to the behavior of the standard {@link Introspector}.
*
* @author Sam Brannen
* @since 6.2.16
*/
@ParameterizedClass(name = "{0}")
@FieldSource("resolvers")
class PropertyDescriptorUtilsPropertyResolutionTests {
static final List<Named<PropertiesResolver>> resolvers = List.of(
named("Basic Properties", new BasicPropertiesResolver()),
named("Standard Properties", new StandardPropertiesResolver()));
@Parameter
PropertiesResolver resolver;
@Test
void determineBasicPropertiesWithUnresolvedGenericsInInterface() {
var pdMap = resolver.resolve(GenericService.class);
assertThat(pdMap).containsOnlyKeys("id");
assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class);
}
@Test
void determineBasicPropertiesWithUnresolvedGenericsInSubInterface() {
// FYI: java.beans.Introspector does not resolve properties for sub-interfaces.
assumeThat(resolver).isNotInstanceOf(StandardPropertiesResolver.class);
var pdMap = resolver.resolve(SubGenericService.class);
assertThat(pdMap).containsOnlyKeys("id");
assertReadAndWriteMethodsForId(pdMap.get("id"), Object.class, Object.class);
}
@Test
void resolvePropertiesWithUnresolvedGenericsInClass() {
var pdMap = resolver.resolve(BaseService.class);
assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class);
}
@Test // gh-36019
void resolvePropertiesInSubclassWithOverriddenGetterAndSetter() {
var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndSetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, String.class, String.class);
}
@Test // gh-36019
void resolvePropertiesWithUnresolvedGenericsInSubclassWithOverloadedSetter() {
var pdMap = resolver.resolve(ServiceWithOverloadedSetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, Object.class, Object.class);
}
@Test // gh-36019
void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetter() {
var pdMap = resolver.resolve(ServiceWithOverriddenGetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class);
}
@Test // gh-36019
void resolvePropertiesWithPartiallyUnresolvedGenericsInSubclassWithOverriddenGetterAndOverloadedSetter() {
var pdMap = resolver.resolve(ServiceWithOverriddenGetterAndOverloadedSetter.class);
assertReadAndWriteMethodsForClassAndId(pdMap, String.class, Object.class);
}
private static void assertReadAndWriteMethodsForClassAndId(Map<String, List<PropertyDescriptor>> pdMap,
Class<?> readType, Class<?> writeType) {
assertThat(pdMap).containsOnlyKeys("class", "id");
assertReadAndWriteMethodsForClass(pdMap.get("class"));
assertReadAndWriteMethodsForId(pdMap.get("id"), readType, writeType);
}
private static void assertReadAndWriteMethodsForClass(List<PropertyDescriptor> pds) {
assertThat(pds).hasSize(1);
var pd = pds.get(0);
assertThat(pd.getName()).isEqualTo("class");
var readMethod = pd.getReadMethod();
assertThat(readMethod.getName()).isEqualTo("getClass");
assertThat(readMethod.getReturnType()).as("read type").isEqualTo(Class.class);
assertThat(readMethod.getParameterCount()).isZero();
assertThat(pd.getWriteMethod()).as("write method").isNull();
}
private static void assertReadAndWriteMethodsForId(List<PropertyDescriptor> pds, Class<?> readType, Class<?> writeType) {
assertThat(pds).hasSize(1);
var pd = pds.get(0);
assertThat(pd.getName()).isEqualTo("id");
var readMethod = pd.getReadMethod();
var writeMethod = pd.getWriteMethod();
assertSoftly(softly -> {
softly.assertThat(readMethod.getName()).isEqualTo("getId");
softly.assertThat(readMethod.getReturnType()).as("read type").isEqualTo(readType);
softly.assertThat(readMethod.getParameterCount()).isZero();
softly.assertThat(writeMethod).as("write method").isNotNull();
if (writeMethod != null) {
softly.assertThat(writeMethod.getName()).isEqualTo("setId");
softly.assertThat(writeMethod.getReturnType()).isEqualTo(void.class);
softly.assertThat(writeMethod.getParameterCount()).isEqualTo(1);
softly.assertThat(writeMethod.getParameterTypes()[0]).as("write type").isEqualTo(writeType);
}
});
}
private static Map<String, List<PropertyDescriptor>> toMap(Stream<? extends PropertyDescriptor> stream) {
return stream.collect(groupingBy(PropertyDescriptor::getName, toList()));
}
interface PropertiesResolver {
Map<String, List<PropertyDescriptor>> resolve(Class<?> beanClass);
}
static class BasicPropertiesResolver implements PropertiesResolver {
@Override
public Map<String, List<PropertyDescriptor>> resolve(Class<?> beanClass) {
try {
var pds = PropertyDescriptorUtils.determineBasicProperties(beanClass);
return toMap(pds.stream());
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
static class StandardPropertiesResolver implements PropertiesResolver {
@Override
public Map<String, List<PropertyDescriptor>> resolve(Class<?> beanClass) {
try {
var beanInfo = Introspector.getBeanInfo(beanClass);
return toMap(Arrays.stream(beanInfo.getPropertyDescriptors()));
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
}
interface GenericService<T> {
void setId(T id);
T getId();
}
interface SubGenericService<T> extends GenericService<T> {
}
static class BaseService<T> {
private T id;
public T getId() {
return id;
}
public void setId(T id) {
this.id = id;
}
}
static class ServiceWithOverriddenGetterAndSetter extends BaseService<String>
implements SubGenericService<String> {
@Override
public String getId() {
return super.getId();
}
@Override
public void setId(String id) {
super.setId(id);
}
}
static class ServiceWithOverloadedSetter extends BaseService<String>
implements SubGenericService<String> {
public void setId(int id) {
setId(String.valueOf(id));
}
}
static class ServiceWithOverriddenGetter extends BaseService<String>
implements SubGenericService<String> {
@Override
public String getId() {
return super.getId();
}
}
static class ServiceWithOverriddenGetterAndOverloadedSetter extends BaseService<String>
implements SubGenericService<String> {
@Override
public String getId() {
return super.getId();
}
public void setId(int id) {
setId(String.valueOf(id));
}
}
}
@@ -1275,6 +1275,88 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(bean.getTestBean().get("testBean2")).isNull();
}
@Test
void fieldInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb1 = new TestBean("tb1");
TestBean tb2 = new TestBean("tb2");
bf.registerSingleton("testBean1", tb1);
bf.registerSingleton("testBean2", tb2);
bf.registerAlias("testBean1", "testBean");
MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
}
@Test
void methodInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb = new TestBean();
bf.registerSingleton("testBean", tb);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndMultipleMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class));
assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type")
.isThrownBy(() -> bf.getBean("annotatedBean"))
.satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class));
}
@Test
void methodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class);
rbd2.setAutowireCandidate(false);
bf.registerBeanDefinition("testBean2", rbd2);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
TestBean tb = bf.getBean("testBean1", TestBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndNoMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isNull();
assertThat(bean.getTestBean()).isNull();
}
@Test
void constructorInjectionWithTypedMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(MapConstructorInjectionBean.class);
@@ -1327,19 +1409,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
@Test
void constructorInjectionWithPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(NamedMapConstructorInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
bf.registerBeanDefinition("testBeanMap", new RootBeanDefinition(HashMap.class));
NamedMapConstructorInjectionBean bean = bf.getBean("annotatedBean", NamedMapConstructorInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
bean = bf.getBean("annotatedBean", NamedMapConstructorInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
}
@Test
void constructorInjectionWithQualifiedPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(QualifiedMapConstructorInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
@@ -1428,114 +1497,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
assertThat(bean.getTestBeanSet()).contains(tb1, tb2);
}
@Test
void fieldInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapFieldInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb1 = new TestBean("tb1");
TestBean tb2 = new TestBean("tb2");
bf.registerSingleton("testBean1", tb1);
bf.registerSingleton("testBean2", tb2);
bf.registerAlias("testBean1", "testBean");
MapFieldInjectionBean bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
bean = bf.getBean("annotatedBean", MapFieldInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(2);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsKey("testBean2");
assertThat(bean.getTestBeanMap()).containsValue(tb1);
assertThat(bean.getTestBeanMap()).containsValue(tb2);
}
@Test
void methodInjectionWithMap() {
RootBeanDefinition bd = new RootBeanDefinition(MapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
TestBean tb = new TestBean();
bf.registerSingleton("testBean", tb);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndMultipleMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
bf.registerBeanDefinition("testBean2", new RootBeanDefinition(TestBean.class));
assertThatExceptionOfType(UnsatisfiedDependencyException.class).as("should have failed, more than one bean of type")
.isThrownBy(() -> bf.getBean("annotatedBean"))
.satisfies(methodParameterDeclaredOn(MapMethodInjectionBean.class));
}
@Test
void methodInjectionWithMapAndMultipleMatchesButOnlyOneAutowireCandidate() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
bf.registerBeanDefinition("testBean1", new RootBeanDefinition(TestBean.class));
RootBeanDefinition rbd2 = new RootBeanDefinition(TestBean.class);
rbd2.setAutowireCandidate(false);
bf.registerBeanDefinition("testBean2", rbd2);
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
TestBean tb = bf.getBean("testBean1", TestBean.class);
assertThat(bean.getTestBeanMap()).hasSize(1);
assertThat(bean.getTestBeanMap()).containsKey("testBean1");
assertThat(bean.getTestBeanMap()).containsValue(tb);
assertThat(bean.getTestBean()).isSameAs(tb);
}
@Test
void methodInjectionWithMapAndNoMatches() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(MapMethodInjectionBean.class));
MapMethodInjectionBean bean = bf.getBean("annotatedBean", MapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isNull();
assertThat(bean.getTestBean()).isNull();
}
@Test
void methodInjectionWithPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(NamedMapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
bf.registerBeanDefinition("testBeanMap", new RootBeanDefinition(HashMap.class));
NamedMapMethodInjectionBean bean = bf.getBean("annotatedBean", NamedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
bean = bf.getBean("annotatedBean", NamedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("testBeanMap"));
}
@Test
void methodInjectionWithQualifiedPlainHashMapAsBean() {
RootBeanDefinition bd = new RootBeanDefinition(QualifiedMapMethodInjectionBean.class);
bd.setScope(BeanDefinition.SCOPE_PROTOTYPE);
bf.registerBeanDefinition("annotatedBean", bd);
bf.registerBeanDefinition("myTestBeanMap", new RootBeanDefinition(HashMap.class));
QualifiedMapMethodInjectionBean bean = bf.getBean("annotatedBean", QualifiedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap"));
bean = bf.getBean("annotatedBean", QualifiedMapMethodInjectionBean.class);
assertThat(bean.getTestBeanMap()).isSameAs(bf.getBean("myTestBeanMap"));
}
@Test
void selfReference() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SelfInjectionBean.class));
@@ -2112,8 +2073,8 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
@@ -2724,7 +2685,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerSingleton("nonNullBean", "Test");
bf.registerBeanDefinition("mixedNullableInjectionBean",
new RootBeanDefinition(MixedNullableInjectionBean.class));
MixedNullableInjectionBean mixedNullableInjectionBean = bf.getBean(MixedNullableInjectionBean.class);
assertThat(mixedNullableInjectionBean.nonNullBean).isNotNull();
assertThat(mixedNullableInjectionBean.nullableBean).isNull();
@@ -2735,7 +2695,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
bf.registerSingleton("nonNullBean", "Test");
bf.registerBeanDefinition("mixedOptionalInjectionBean",
new RootBeanDefinition(MixedOptionalInjectionBean.class));
MixedOptionalInjectionBean mixedOptionalInjectionBean = bf.getBean(MixedOptionalInjectionBean.class);
assertThat(mixedOptionalInjectionBean.nonNullBean).isNotNull();
assertThat(mixedOptionalInjectionBean.nullableBean).isNull();
@@ -3299,21 +3258,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
}
public static class NamedMapConstructorInjectionBean {
private Map<String, TestBean> testBeanMap;
@Autowired
public NamedMapConstructorInjectionBean(Map<String, TestBean> testBeanMap) {
this.testBeanMap = testBeanMap;
}
public Map<String, TestBean> getTestBeanMap() {
return this.testBeanMap;
}
}
public static class QualifiedMapConstructorInjectionBean {
private Map<String, TestBean> testBeanMap;
@@ -3413,37 +3357,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
}
public static class NamedMapMethodInjectionBean {
private Map<String, TestBean> testBeanMap;
@Autowired
public void setTestBeanMap(Map<String, TestBean> testBeanMap) {
this.testBeanMap = testBeanMap;
}
public Map<String, TestBean> getTestBeanMap() {
return this.testBeanMap;
}
}
public static class QualifiedMapMethodInjectionBean {
private Map<String, TestBean> testBeanMap;
@Autowired
@Qualifier("myTestBeanMap")
public void setTestBeanMap(Map<String, TestBean> testBeanMap) {
this.testBeanMap = testBeanMap;
}
public Map<String, TestBean> getTestBeanMap() {
return this.testBeanMap;
}
}
@SuppressWarnings("serial")
public static class ObjectFactoryFieldInjectionBean implements Serializable {
@@ -444,8 +444,8 @@ class InjectAnnotationBeanPostProcessorTests {
bf.registerBeanDefinition("factoryBeanDependentBean", new RootBeanDefinition(FactoryBeanDependentBean.class));
bf.registerSingleton("stringFactoryBean", new StringFactoryBean());
StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
final StringFactoryBean factoryBean = (StringFactoryBean) bf.getBean("&stringFactoryBean");
final FactoryBeanDependentBean bean = (FactoryBeanDependentBean) bf.getBean("factoryBeanDependentBean");
assertThat(factoryBean).as("The singleton StringFactoryBean should have been registered.").isNotNull();
assertThat(bean).as("The factoryBeanDependentBean should have been registered.").isNotNull();
@@ -788,6 +788,7 @@ class InjectAnnotationBeanPostProcessorTests {
private ConfigurableListableBeanFactory beanFactory;
public ConstructorResourceInjectionBean() {
throw new UnsupportedOperationException();
}
@@ -16,7 +16,6 @@
package org.springframework.beans.factory.aot;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -35,7 +34,6 @@ import org.springframework.aot.generate.GenerationContext;
import org.springframework.aot.generate.MethodReference;
import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.AutowiredPropertyMarker;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -628,22 +626,6 @@ class BeanDefinitionMethodGeneratorTests {
-> assertThat(customPropertyValue.value()).isEqualTo("test")));
}
@Test
void generateBeanDefinitionMethodWhenHasAutowiredPropertyGeneratesMethod() {
RootBeanDefinition beanDefinition = (RootBeanDefinition) BeanDefinitionBuilder
.rootBeanDefinition(CustomBean.class).addAutowiredProperty("innerBean")
.getBeanDefinition();
RegisteredBean registeredBean = registerBean(beanDefinition);
BeanDefinitionMethodGenerator generator = new BeanDefinitionMethodGenerator(
this.methodGeneratorFactory, registeredBean, null,
Collections.emptyList());
MethodReference method = generator.generateBeanDefinitionMethod(
this.generationContext, this.beanRegistrationsCode);
compile(method, (actual, compiled) ->
assertThat(actual.getPropertyValues().get("innerBean"))
.isSameAs(AutowiredPropertyMarker.INSTANCE));
}
@Test
void generateBeanDefinitionMethodWhenHasAotContributionsAppliesContributions() {
RegisteredBean registeredBean = registerBean(
@@ -39,7 +39,6 @@ import org.springframework.aot.generate.GeneratedClass;
import org.springframework.aot.generate.ValueCodeGenerator;
import org.springframework.aot.generate.ValueCodeGeneratorDelegates;
import org.springframework.aot.test.generate.TestGenerationContext;
import org.springframework.beans.factory.config.AutowiredPropertyMarker;
import org.springframework.beans.factory.config.BeanReference;
import org.springframework.beans.factory.config.RuntimeBeanNameReference;
import org.springframework.beans.factory.config.RuntimeBeanReference;
@@ -487,16 +486,4 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
}
@Nested
class AutowiredPropertyMarkerTests {
@Test
void generateWhenAutowiredPropertyMarker() {
compile(AutowiredPropertyMarker.INSTANCE, (instance, compiler) ->
assertThat(instance).isInstanceOf(AutowiredPropertyMarker.class)
.isSameAs(AutowiredPropertyMarker.INSTANCE));
}
}
}
@@ -28,7 +28,6 @@ import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.config.FieldRetrievingFactoryBean;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.beans.factory.parsing.ComponentDefinition;
@@ -47,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Juergen Hoeller
* @author Mark Fisher
*/
@SuppressWarnings("rawtypes")
class UtilNamespaceHandlerTests {
private DefaultListableBeanFactory beanFactory;
@@ -55,7 +55,7 @@ class UtilNamespaceHandlerTests {
@BeforeEach
void setup() {
void setUp() {
this.beanFactory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory);
reader.setEventListener(this.listener);
@@ -109,17 +109,17 @@ class UtilNamespaceHandlerTests {
@Test
void testSimpleMap() {
Map<?, ?> map = (Map<?, ?>) this.beanFactory.getBean("simpleMap");
Map<?, ?> map = (Map) this.beanFactory.getBean("simpleMap");
assertThat(map.get("foo")).isEqualTo("bar");
Map<?, ?> map2 = (Map<?, ?>) this.beanFactory.getBean("simpleMap");
Map<?, ?> map2 = (Map) this.beanFactory.getBean("simpleMap");
assertThat(map).isSameAs(map2);
}
@Test
void testScopedMap() {
Map<?, ?> map = (Map<?, ?>) this.beanFactory.getBean("scopedMap");
Map<?, ?> map = (Map) this.beanFactory.getBean("scopedMap");
assertThat(map.get("foo")).isEqualTo("bar");
Map<?, ?> map2 = (Map<?, ?>) this.beanFactory.getBean("scopedMap");
Map<?, ?> map2 = (Map) this.beanFactory.getBean("scopedMap");
assertThat(map2.get("foo")).isEqualTo("bar");
assertThat(map).isNotSameAs(map2);
}
@@ -164,23 +164,17 @@ class UtilNamespaceHandlerTests {
}
@Test
void testMapWithRef() throws Exception {
Map<?, ?> map = (Map<?, ?>) this.beanFactory.getBean("mapWithRef");
void testMapWithRef() {
Map<?, ?> map = (Map) this.beanFactory.getBean("mapWithRef");
assertThat(map).isInstanceOf(TreeMap.class);
assertThat(map.get("bean")).isEqualTo(this.beanFactory.getBean("testBean"));
assertThat(this.beanFactory.resolveDependency(
new DependencyDescriptor(getClass().getDeclaredField("mapWithRef"), true), null))
.isSameAs(map);
}
@Test
void testMapWithTypes() throws Exception {
Map<?, ?> map = (Map<?, ?>) this.beanFactory.getBean("mapWithTypes");
void testMapWithTypes() {
Map<?, ?> map = (Map) this.beanFactory.getBean("mapWithTypes");
assertThat(map).isInstanceOf(LinkedCaseInsensitiveMap.class);
assertThat(map.get("bean")).isEqualTo(this.beanFactory.getBean("testBean"));
assertThat(this.beanFactory.resolveDependency(
new DependencyDescriptor(getClass().getDeclaredField("mapWithTypes"), true), null))
.isSameAs(map);
}
@Test
@@ -246,11 +240,11 @@ class UtilNamespaceHandlerTests {
void testCircularCollections() {
TestBean bean = (TestBean) this.beanFactory.getBean("circularCollectionsBean");
assertThat(bean.getSomeList()).singleElement().isSameAs(bean);
assertThat(bean.getSomeSet()).singleElement().isSameAs(bean);
assertThat(bean.getSomeList()).singleElement().isEqualTo(bean);
assertThat(bean.getSomeSet()).singleElement().isEqualTo(bean);
assertThat(bean.getSomeMap()).hasSize(1).allSatisfy((key, value) -> {
assertThat(key).isEqualTo("foo");
assertThat(value).isSameAs(bean);
assertThat(value).isEqualTo(bean);
});
}
@@ -261,17 +255,17 @@ class UtilNamespaceHandlerTests {
List<?> list = bean.getSomeList();
assertThat(Proxy.isProxyClass(list.getClass())).isTrue();
assertThat(list).singleElement().isSameAs(bean);
assertThat(list).singleElement().isEqualTo(bean);
Set<?> set = bean.getSomeSet();
assertThat(Proxy.isProxyClass(set.getClass())).isFalse();
assertThat(set).singleElement().isSameAs(bean);
assertThat(set).singleElement().isEqualTo(bean);
Map<?, ?> map = bean.getSomeMap();
assertThat(Proxy.isProxyClass(map.getClass())).isFalse();
assertThat(map).hasSize(1).allSatisfy((key, value) -> {
assertThat(key).isEqualTo("foo");
assertThat(value).isSameAs(bean);
assertThat(value).isEqualTo(bean);
});
}
@@ -282,17 +276,17 @@ class UtilNamespaceHandlerTests {
List<?> list = bean.getSomeList();
assertThat(Proxy.isProxyClass(list.getClass())).isFalse();
assertThat(list).singleElement().isSameAs(bean);
assertThat(list).singleElement().isEqualTo(bean);
Set<?> set = bean.getSomeSet();
assertThat(Proxy.isProxyClass(set.getClass())).isTrue();
assertThat(set).singleElement().isSameAs(bean);
assertThat(set).singleElement().isEqualTo(bean);
Map<?, ?> map = bean.getSomeMap();
assertThat(Proxy.isProxyClass(map.getClass())).isFalse();
assertThat(map).hasSize(1).allSatisfy((key, value) -> {
assertThat(key).isEqualTo("foo");
assertThat(value).isSameAs(bean);
assertThat(value).isEqualTo(bean);
});
}
@@ -303,17 +297,17 @@ class UtilNamespaceHandlerTests {
List<?> list = bean.getSomeList();
assertThat(Proxy.isProxyClass(list.getClass())).isFalse();
assertThat(list).singleElement().isSameAs(bean);
assertThat(list).singleElement().isEqualTo(bean);
Set<?> set = bean.getSomeSet();
assertThat(Proxy.isProxyClass(set.getClass())).isFalse();
assertThat(set).singleElement().isSameAs(bean);
assertThat(set).singleElement().isEqualTo(bean);
Map<?, ?> map = bean.getSomeMap();
assertThat(Proxy.isProxyClass(map.getClass())).isTrue();
assertThat(map).hasSize(1).allSatisfy((key, value) -> {
assertThat(key).isEqualTo("foo");
assertThat(value).isSameAs(bean);
assertThat(value).isEqualTo(bean);
});
}
@@ -378,9 +372,4 @@ class UtilNamespaceHandlerTests {
assertThat(props).as("Incorrect property value").containsEntry("foo2", "local2");
}
// For DependencyDescriptor resolution
Map<String, TestBean> mapWithRef;
Map<String, TestBean> mapWithTypes;
}
@@ -59,8 +59,6 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
private boolean jedi;
private String favoriteCafé;
private ITestBean spouse;
private String touchy;
@@ -211,14 +209,6 @@ public class TestBean implements BeanNameAware, BeanFactoryAware, ITestBean, IOt
this.jedi = jedi;
}
public String getFavoriteCafé() {
return this.favoriteCafé;
}
public void setFavoriteCafé(String favoriteCafé) {
this.favoriteCafé = favoriteCafé;
}
@Override
public ITestBean getSpouse() {
return this.spouse;
@@ -77,7 +77,7 @@ public class CaffeineCacheManager implements CacheManager {
private boolean allowNullValues = true;
private volatile boolean dynamic = true;
private boolean dynamic = true;
private final Map<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
@@ -102,15 +102,10 @@ public class CaffeineCacheManager implements CacheManager {
/**
* Specify the set of cache names for this CacheManager's 'static' mode.
* <p>The number of caches and their names will be fixed after a call
* to this method, with no creation of further cache regions at runtime.
* <p>Note that this method replaces existing caches of the given names
* and prevents the creation of further cache regions from here on - but
* does <i>not</i> remove unrelated existing caches. For a full reset,
* consider calling {@link #resetCaches()} before calling this method.
* <p>Calling this method with a {@code null} collection argument resets
* the mode to 'dynamic', allowing for further creation of caches again.
* @see #resetCaches()
* <p>The number of caches and their names will be fixed after a call to this method,
* with no creation of further cache regions at runtime.
* <p>Calling this with a {@code null} collection argument resets the
* mode to 'dynamic', allowing for further creation of caches again.
*/
public void setCacheNames(@Nullable Collection<String> cacheNames) {
if (cacheNames != null) {
@@ -250,6 +245,11 @@ public class CaffeineCacheManager implements CacheManager {
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
@Override
@Nullable
public Cache getCache(String name) {
@@ -260,33 +260,6 @@ public class CaffeineCacheManager implements CacheManager {
return cache;
}
@Override
public Collection<String> getCacheNames() {
return Collections.unmodifiableSet(this.cacheMap.keySet());
}
/**
* Reset this cache manager's caches, removing them completely for on-demand
* re-creation in 'dynamic' mode, or simply clearing their entries otherwise.
* @since 6.2.14
*/
public void resetCaches() {
this.cacheMap.values().forEach(Cache::clear);
if (this.dynamic) {
this.cacheMap.keySet().retainAll(this.customCacheNames);
}
}
/**
* Remove the specified cache from this cache manager, applying to
* custom caches as well as dynamically registered caches at runtime.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.customCacheNames.remove(name);
this.cacheMap.remove(name);
}
/**
* Register the given native Caffeine Cache instance with this cache manager,
@@ -330,6 +303,16 @@ public class CaffeineCacheManager implements CacheManager {
this.cacheMap.put(name, adaptCaffeineCache(name, cache));
}
/**
* Remove the specified cache from this cache manager, applying to
* custom caches as well as dynamically registered caches at runtime.
* @param name the name of the cache
* @since 6.1.15
*/
public void removeCache(String name) {
this.customCacheNames.remove(name);
this.cacheMap.remove(name);
}
/**
* Adapt the given new native Caffeine Cache instance to Spring's {@link Cache}
@@ -25,7 +25,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.Nullable;
import org.springframework.util.ReflectionUtils;
@@ -98,10 +97,6 @@ public abstract class AbstractFallbackJCacheOperationSource implements JCacheOpe
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Skip setBeanFactory method on BeanFactoryAware.
if (method.getDeclaringClass() == BeanFactoryAware.class) {
return null;
}
// The method may be on an interface, but we need metadata from the target class.
// If the target class is null, the method will be unchanged.

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