diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties
index eacaf4f5b871..eced14624837 100644
--- a/buildSrc/gradle.properties
+++ b/buildSrc/gradle.properties
@@ -1,2 +1,2 @@
org.gradle.caching=true
-javaFormatVersion=0.0.42
+javaFormatVersion=0.0.43
diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java
index 4216ae6fa21e..b35b3e3b5df6 100644
--- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java
+++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java
@@ -50,7 +50,7 @@ public void apply(Project project) {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
- checkstyle.setToolVersion("10.23.0");
+ checkstyle.setToolVersion("10.23.1");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
diff --git a/buildSrc/src/main/java/org/springframework/build/TestConventions.java b/buildSrc/src/main/java/org/springframework/build/TestConventions.java
index 1283d233765d..bb8f507efac4 100644
--- a/buildSrc/src/main/java/org/springframework/build/TestConventions.java
+++ b/buildSrc/src/main/java/org/springframework/build/TestConventions.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaBasePlugin;
import org.gradle.api.tasks.testing.Test;
+import org.gradle.api.tasks.testing.TestFrameworkOptions;
+import org.gradle.api.tasks.testing.junitplatform.JUnitPlatformOptions;
import org.gradle.testretry.TestRetryPlugin;
import org.gradle.testretry.TestRetryTaskExtension;
@@ -34,6 +36,7 @@
*
* @author Brian Clozel
* @author Andy Wilkinson
+ * @author Sam Brannen
*/
class TestConventions {
@@ -50,7 +53,12 @@ private void configureTestConventions(Project project) {
}
private void configureTests(Project project, Test test) {
- test.useJUnitPlatform();
+ TestFrameworkOptions existingOptions = test.getOptions();
+ test.useJUnitPlatform(options -> {
+ if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) {
+ options.copyFrom(junitPlatformOptions);
+ }
+ });
test.include("**/*Tests.class", "**/*Test.class");
test.setSystemProperties(Map.of(
"java.awt.headless", "true",
diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle
index df7f3bbd57da..5526a79ba53c 100644
--- a/framework-api/framework-api.gradle
+++ b/framework-api/framework-api.gradle
@@ -1,6 +1,6 @@
plugins {
id 'java-platform'
- id 'io.freefair.aggregate-javadoc' version '8.3'
+ id 'io.freefair.aggregate-javadoc' version '8.13.1'
}
description = "Spring Framework API Docs"
@@ -21,6 +21,7 @@ dependencies {
javadoc {
title = "${rootProject.description} ${version} API"
+ failOnError = true
options {
encoding = "UTF-8"
memberLevel = JavadocMemberLevel.PROTECTED
diff --git a/framework-docs/modules/ROOT/pages/appendix.adoc b/framework-docs/modules/ROOT/pages/appendix.adoc
index 9a8c9048c051..6e7e5cecd0e0 100644
--- a/framework-docs/modules/ROOT/pages/appendix.adoc
+++ b/framework-docs/modules/ROOT/pages/appendix.adoc
@@ -103,6 +103,14 @@ for details.
{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
for details.
+| `spring.placeholder.escapeCharacter.default`
+| The default escape character for property placeholder support. If not set, `'\'` will
+be used. Can be set to a custom escape character or an empty string to disable support
+for an escape character. The default escape character be explicitly overridden in
+`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See
+{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`]
+for details.
+
| `spring.test.aot.processing.failOnError`
| A boolean flag that controls whether errors encountered during AOT processing in the
_Spring TestContext Framework_ should result in an exception that fails the overall process.
diff --git a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc
index 72e70005d0cd..d91aaafb19b4 100644
--- a/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc
+++ b/framework-docs/modules/ROOT/pages/core/beans/annotation-config/value-annotations.adoc
@@ -101,8 +101,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig
Using the above configuration ensures Spring initialization failure if any `${}`
placeholder could not be resolved. It is also possible to use methods like
-`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or
-`setEscapeCharacter` to customize placeholders.
+`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or
+`setEscapeCharacter()` to customize the placeholder syntax. In addition, the default
+escape character can be changed or disabled globally by setting the
+`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
+the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that
will get properties from `application.properties` and `application.yml` files.
diff --git a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc
index bd4f6da550e1..56641fd847eb 100644
--- a/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc
+++ b/framework-docs/modules/ROOT/pages/core/beans/factory-extension.adoc
@@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the
[[beans-factory-placeholderconfigurer]]
-=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer`
+=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer`
You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values
from a bean definition in a separate file by using the standard Java `Properties` format.
@@ -341,8 +341,8 @@ with placeholder values is defined:
The example shows properties configured from an external `Properties` file. At runtime,
a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some
-properties of the DataSource. The values to replace are specified as placeholders of the
-form pass:q[`${property-name}`], which follows the Ant and log4j and JSP EL style.
+properties of the `DataSource`. The values to replace are specified as placeholders of the
+form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style.
The actual values come from another file in the standard Java `Properties` format:
@@ -355,11 +355,15 @@ jdbc.password=root
----
Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and
-the same applies for other placeholder values that match keys in the properties file.
-The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
-attributes of a bean definition. Furthermore, you can customize the placeholder prefix and suffix.
-
-With the `context` namespace introduced in Spring 2.5, you can configure property placeholders
+the same applies for other placeholder values that match keys in the properties file. The
+`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
+attributes of a bean definition. Furthermore, you can customize the placeholder prefix,
+suffix, default value separator, and escape character. In addition, the default escape
+character can be changed or disabled globally by setting the
+`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
+the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
+
+With the `context` namespace, you can configure property placeholders
with a dedicated configuration element. You can provide one or more locations as a
comma-separated list in the `location` attribute, as the following example shows:
diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc
index 3fa561bf51c5..2a8670de0740 100644
--- a/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc
+++ b/framework-docs/modules/ROOT/pages/languages/kotlin/spring-projects-in.adoc
@@ -190,7 +190,7 @@ NOTE: If you use Spring Boot, you should probably use
instead of `@Value` annotations.
As an alternative, you can customize the property placeholder prefix by declaring the
-following configuration beans:
+following `PropertySourcesPlaceholderConfigurer` bean:
[source,kotlin,indent=0]
----
@@ -200,8 +200,10 @@ following configuration beans:
}
----
-You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`)
-that uses the `${...}` syntax, with configuration beans, as the following example shows:
+You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use
+the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by
+declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example
+shows:
[source,kotlin,indent=0]
----
@@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
----
+In addition, the default escape character can be changed or disabled globally by setting
+the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or
+via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
[[checked-exceptions]]
diff --git a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc
index 17fdf5c9bef0..fdc6470f9682 100644
--- a/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/annotations/integration-junit4.adoc
@@ -2,8 +2,9 @@
= Spring JUnit 4 Testing Annotations
The following annotations are supported only when used in conjunction with the
-xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules]
-, or xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]:
+xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner],
+xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules], or
+xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]:
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-ifprofilevalue[`@IfProfileValue`]
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`]
diff --git a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc
index 1ee5856ecfd0..a77980058736 100644
--- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc
+++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/support-classes.adoc
@@ -1,166 +1,9 @@
[[testcontext-support-classes]]
= TestContext Framework Support Classes
-This section describes the various classes that support the Spring TestContext Framework.
+This section describes the various classes that support the Spring TestContext Framework
+in JUnit and TestNG.
-[[testcontext-junit4-runner]]
-== Spring JUnit 4 Runner
-
-The Spring TestContext Framework offers full integration with JUnit 4 through a custom
-runner (supported on JUnit 4.12 or higher). By annotating test classes with
-`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
-variant, developers can implement standard JUnit 4-based unit and integration tests and
-simultaneously reap the benefits of the TestContext framework, such as support for
-loading application contexts, dependency injection of test instances, transactional test
-method execution, and so on. If you want to use the Spring TestContext Framework with an
-alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
-(such as the `MockitoJUnitRunner`), you can, optionally, use
-xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] instead.
-
-The following code listing shows the minimal requirements for configuring a test class to
-run with the custom Spring `Runner`:
-
-[tabs]
-======
-Java::
-+
-[source,java,indent=0,subs="verbatim,quotes"]
-----
- @RunWith(SpringRunner.class)
- @TestExecutionListeners({})
- public class SimpleTest {
-
- @Test
- public void testMethod() {
- // test logic...
- }
- }
-----
-
-Kotlin::
-+
-[source,kotlin,indent=0,subs="verbatim,quotes"]
-----
- @RunWith(SpringRunner::class)
- @TestExecutionListeners
- class SimpleTest {
-
- @Test
- fun testMethod() {
- // test logic...
- }
- }
-----
-======
-
-In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
-disable the default listeners, which otherwise would require an `ApplicationContext` to
-be configured through `@ContextConfiguration`.
-
-[[testcontext-junit4-rules]]
-== Spring JUnit 4 Rules
-
-The `org.springframework.test.context.junit4.rules` package provides the following JUnit
-4 rules (supported on JUnit 4.12 or higher):
-
-* `SpringClassRule`
-* `SpringMethodRule`
-
-`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
-TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
-instance-level and method-level features of the Spring TestContext Framework.
-
-In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
-being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
-combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
-third-party runners (such as the `MockitoJUnitRunner`).
-
-To support the full functionality of the TestContext framework, you must combine a
-`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
-to declare these rules in an integration test:
-
-[tabs]
-======
-Java::
-+
-[source,java,indent=0,subs="verbatim,quotes"]
-----
- // Optionally specify a non-Spring Runner via @RunWith(...)
- @ContextConfiguration
- public class IntegrationTest {
-
- @ClassRule
- public static final SpringClassRule springClassRule = new SpringClassRule();
-
- @Rule
- public final SpringMethodRule springMethodRule = new SpringMethodRule();
-
- @Test
- public void testMethod() {
- // test logic...
- }
- }
-----
-
-Kotlin::
-+
-[source,kotlin,indent=0,subs="verbatim,quotes"]
-----
- // Optionally specify a non-Spring Runner via @RunWith(...)
- @ContextConfiguration
- class IntegrationTest {
-
- @Rule
- val springMethodRule = SpringMethodRule()
-
- @Test
- fun testMethod() {
- // test logic...
- }
-
- companion object {
- @ClassRule
- val springClassRule = SpringClassRule()
- }
- }
-----
-======
-
-[[testcontext-support-classes-junit4]]
-== JUnit 4 Support Classes
-
-The `org.springframework.test.context.junit4` package provides the following support
-classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
-
-* `AbstractJUnit4SpringContextTests`
-* `AbstractTransactionalJUnit4SpringContextTests`
-
-`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
-Spring TestContext Framework with explicit `ApplicationContext` testing support in a
-JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
-`protected` `applicationContext` instance variable that you can use to perform explicit
-bean lookups or to test the state of the context as a whole.
-
-`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
-`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
-access. This class expects a `javax.sql.DataSource` bean and a
-`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
-extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
-`jdbcTemplate` instance variable that you can use to run SQL statements to query the
-database. You can use such queries to confirm database state both before and after
-running database-related application code, and Spring ensures that such queries run in
-the scope of the same transaction as the application code. When used in conjunction with
-an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
-As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
-`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
-delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
-Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
-`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
-
-TIP: These classes are a convenience for extension. If you do not want your test classes
-to be tied to a Spring-specific class hierarchy, you can configure your own custom test
-classes by using `@RunWith(SpringRunner.class)` or xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules]
-.
[[testcontext-junit-jupiter-extension]]
== SpringExtension for JUnit Jupiter
@@ -177,14 +20,17 @@ following features above and beyond the feature set that Spring supports for JUn
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.
+ 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://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] for further details and examples.
+ xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
+ for further details and examples.
* Custom composed annotations that combine annotations from Spring and JUnit Jupiter. See
the `@TransactionalDevTestConfig` and `@TransactionalIntegrationTest` examples in
- xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for further details.
+ xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for
+ further details.
The following code listing shows how to configure a test class to use the
`SpringExtension` in conjunction with `@ContextConfiguration`:
@@ -307,7 +153,8 @@ Kotlin::
======
See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in
-xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details.
+xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
+for further details.
[[testcontext-junit-jupiter-di]]
=== Dependency Injection with the `SpringExtension`
@@ -318,10 +165,9 @@ extension API from JUnit Jupiter, which lets Spring provide dependency injection
constructors, test methods, and test lifecycle callback methods.
Specifically, the `SpringExtension` can inject dependencies from the test's
-`ApplicationContext` into test constructors and methods that are annotated with
-Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`,
-`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`,
-and others.
+`ApplicationContext` into test constructors and methods that are annotated with Spring's
+`@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@AfterAll`,
+`@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and others.
[[testcontext-junit-jupiter-di-constructor]]
@@ -341,8 +187,9 @@ autowirable if one of the following conditions is met (in order of precedence).
attribute set to `ALL`.
* The default _test constructor autowire mode_ has been changed to `ALL`.
-See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] for details on the use of
-`@TestConstructor` and how to change the global _test constructor autowire mode_.
+See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]
+for details on the use of `@TestConstructor` and how to change the global _test
+constructor autowire mode_.
WARNING: If the constructor for a test class is considered to be _autowirable_, Spring
assumes the responsibility for resolving arguments for all parameters in the constructor.
@@ -407,8 +254,9 @@ Kotlin::
Note that this feature lets test dependencies be `final` and therefore immutable.
If the `spring.test.constructor.autowire.mode` property is to `all` (see
-xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), we can omit the declaration of
-`@Autowired` on the constructor in the previous example, resulting in the following.
+xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]),
+we can omit the declaration of `@Autowired` on the constructor in the previous example,
+resulting in the following.
[tabs]
======
@@ -553,17 +401,19 @@ honor `@NestedTestConfiguration` semantics.
In order to allow development teams to change the default to `OVERRIDE` – for example,
for compatibility with Spring Framework 5.0 through 5.2 – the default mode can be changed
globally via a JVM system property or a `spring.properties` file in the root of the
-classpath. See the xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"]
- note for details.
+classpath. See the
+xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"]
+note for details.
Although the following "Hello World" example is very simplistic, it shows how to declare
common configuration on a top-level class that is inherited by its `@Nested` test
classes. In this particular example, only the `TestConfig` configuration class is
inherited. Each nested test class provides its own set of active profiles, resulting in a
distinct `ApplicationContext` for each nested test class (see
-xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). Consult the list of
-xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] to see
-which annotations can be inherited in `@Nested` test classes.
+xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details).
+Consult the list of
+xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations]
+to see which annotations can be inherited in `@Nested` test classes.
[tabs]
======
@@ -626,8 +476,174 @@ Kotlin::
----
======
+
+[[testcontext-junit4-support]]
+== JUnit 4 Support
+
+[[testcontext-junit4-runner]]
+=== Spring JUnit 4 Runner
+
+The Spring TestContext Framework offers full integration with JUnit 4 through a custom
+runner (supported on JUnit 4.12 or higher). By annotating test classes with
+`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
+variant, developers can implement standard JUnit 4-based unit and integration tests and
+simultaneously reap the benefits of the TestContext framework, such as support for
+loading application contexts, dependency injection of test instances, transactional test
+method execution, and so on. If you want to use the Spring TestContext Framework with an
+alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
+(such as the `MockitoJUnitRunner`), you can, optionally, use
+xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules]
+instead.
+
+The following code listing shows the minimal requirements for configuring a test class to
+run with the custom Spring `Runner`:
+
+[tabs]
+======
+Java::
++
+[source,java,indent=0,subs="verbatim,quotes"]
+----
+ @RunWith(SpringRunner.class)
+ @TestExecutionListeners({})
+ public class SimpleTest {
+
+ @Test
+ public void testMethod() {
+ // test logic...
+ }
+ }
+----
+
+Kotlin::
++
+[source,kotlin,indent=0,subs="verbatim,quotes"]
+----
+ @RunWith(SpringRunner::class)
+ @TestExecutionListeners
+ class SimpleTest {
+
+ @Test
+ fun testMethod() {
+ // test logic...
+ }
+ }
+----
+======
+
+In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
+disable the default listeners, which otherwise would require an `ApplicationContext` to
+be configured through `@ContextConfiguration`.
+
+[[testcontext-junit4-rules]]
+=== Spring JUnit 4 Rules
+
+The `org.springframework.test.context.junit4.rules` package provides the following JUnit
+4 rules (supported on JUnit 4.12 or higher):
+
+* `SpringClassRule`
+* `SpringMethodRule`
+
+`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
+TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
+instance-level and method-level features of the Spring TestContext Framework.
+
+In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
+being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
+combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
+third-party runners (such as the `MockitoJUnitRunner`).
+
+To support the full functionality of the TestContext framework, you must combine a
+`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
+to declare these rules in an integration test:
+
+[tabs]
+======
+Java::
++
+[source,java,indent=0,subs="verbatim,quotes"]
+----
+ // Optionally specify a non-Spring Runner via @RunWith(...)
+ @ContextConfiguration
+ public class IntegrationTest {
+
+ @ClassRule
+ public static final SpringClassRule springClassRule = new SpringClassRule();
+
+ @Rule
+ public final SpringMethodRule springMethodRule = new SpringMethodRule();
+
+ @Test
+ public void testMethod() {
+ // test logic...
+ }
+ }
+----
+
+Kotlin::
++
+[source,kotlin,indent=0,subs="verbatim,quotes"]
+----
+ // Optionally specify a non-Spring Runner via @RunWith(...)
+ @ContextConfiguration
+ class IntegrationTest {
+
+ @Rule
+ val springMethodRule = SpringMethodRule()
+
+ @Test
+ fun testMethod() {
+ // test logic...
+ }
+
+ companion object {
+ @ClassRule
+ val springClassRule = SpringClassRule()
+ }
+ }
+----
+======
+
+[[testcontext-support-classes-junit4]]
+=== JUnit 4 Base Classes
+
+The `org.springframework.test.context.junit4` package provides the following support
+classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
+
+* `AbstractJUnit4SpringContextTests`
+* `AbstractTransactionalJUnit4SpringContextTests`
+
+`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
+Spring TestContext Framework with explicit `ApplicationContext` testing support in a
+JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
+`protected` `applicationContext` instance variable that you can use to perform explicit
+bean lookups or to test the state of the context as a whole.
+
+`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
+`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
+access. This class expects a `javax.sql.DataSource` bean and a
+`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
+extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
+`jdbcTemplate` instance variable that you can use to run SQL statements to query the
+database. You can use such queries to confirm database state both before and after
+running database-related application code, and Spring ensures that such queries run in
+the scope of the same transaction as the application code. When used in conjunction with
+an ORM tool, be sure to avoid
+xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
+As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
+`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
+delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
+Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
+`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
+
+TIP: These classes are a convenience for extension. If you do not want your test classes
+to be tied to a Spring-specific class hierarchy, you can configure your own custom test
+classes by using `@RunWith(SpringRunner.class)` or
+xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules].
+
+
[[testcontext-support-classes-testng]]
-== TestNG Support Classes
+== TestNG Support
The `org.springframework.test.context.testng` package provides the following support
classes for TestNG based test cases:
@@ -650,7 +666,8 @@ extend `AbstractTransactionalTestNGSpringContextTests`, you can access a `protec
database. You can use such queries to confirm database state both before and after
running database-related application code, and Spring ensures that such queries run in
the scope of the same transaction as the application code. When used in conjunction with
-an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
+an ORM tool, be sure to avoid
+xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
`AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
diff --git a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc
index cbfcad7a7cec..ba618c0c8d86 100644
--- a/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc
+++ b/framework-docs/modules/ROOT/pages/web/webflux/controller/ann-requestmapping.adoc
@@ -234,8 +234,8 @@ Kotlin::
--
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
-through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
-other property sources. You can use this to, for example, parameterize a base URL based on
+by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
+other property sources. You can use this, for example, to parameterize a base URL based on
some external configuration.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle
index a057d3beb163..169d653da8f6 100644
--- a/framework-platform/framework-platform.gradle
+++ b/framework-platform/framework-platform.gradle
@@ -7,17 +7,17 @@ javaPlatform {
}
dependencies {
- api(platform("com.fasterxml.jackson:jackson-bom:2.18.3"))
- api(platform("io.micrometer:micrometer-bom:1.14.5"))
- api(platform("io.netty:netty-bom:4.1.119.Final"))
+ api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
+ api(platform("io.micrometer:micrometer-bom:1.14.7"))
+ api(platform("io.netty:netty-bom:4.1.121.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
- api(platform("io.projectreactor:reactor-bom:2024.0.4"))
+ api(platform("io.projectreactor:reactor-bom:2024.0.6"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.26"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.3"))
- api(platform("org.eclipse.jetty:jetty-bom:12.0.18"))
- api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18"))
+ api(platform("org.eclipse.jetty:jetty-bom:12.0.21"))
+ api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21"))
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.12.2"))
@@ -100,7 +100,7 @@ dependencies {
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
- api("org.apache.httpcomponents.client5:httpclient5:5.4.3")
+ api("org.apache.httpcomponents.client5:httpclient5:5.4.4")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
diff --git a/gradle.properties b/gradle.properties
index edd7222db737..266782ebfa4e 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,4 +1,4 @@
-version=6.2.6-SNAPSHOT
+version=6.2.7
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle
index 0fb2cfe2fefe..e6378c0739b5 100644
--- a/gradle/spring-module.gradle
+++ b/gradle/spring-module.gradle
@@ -69,7 +69,7 @@ normalization {
javadoc {
description = "Generates project-level javadoc for use in -javadoc jar"
-
+ failOnError = true
options {
encoding = "UTF-8"
memberLevel = JavadocMemberLevel.PROTECTED
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 9bbc975c742b..1b33c55baabb 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 37f853b1c84d..ca025c83a7cc 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index faf93008b77e..23d15a936707 100755
--- a/gradlew
+++ b/gradlew
@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/gradlew.bat b/gradlew.bat
index 9b42019c7915..5eed7ee84528 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+set CLASSPATH=
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java
index 6e37fc17fb46..49f21f9044ba 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PlaceholderConfigurerSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,10 @@
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.BeanNameAware;
+import org.springframework.core.env.AbstractPropertyResolver;
import org.springframework.lang.Nullable;
import org.springframework.util.StringValueResolver;
+import org.springframework.util.SystemPropertyUtils;
/**
* Abstract base class for property resource configurers that resolve placeholders
@@ -37,16 +39,16 @@
*
*
*
* @author Chris Beams
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.1
* @see PropertyPlaceholderConfigurer
* @see org.springframework.context.support.PropertySourcesPlaceholderConfigurer
@@ -92,16 +95,21 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
implements BeanNameAware, BeanFactoryAware {
/** Default placeholder prefix: {@value}. */
- public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
+ public static final String DEFAULT_PLACEHOLDER_PREFIX = SystemPropertyUtils.PLACEHOLDER_PREFIX;
/** Default placeholder suffix: {@value}. */
- public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
+ public static final String DEFAULT_PLACEHOLDER_SUFFIX = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
/** Default value separator: {@value}. */
- public static final String DEFAULT_VALUE_SEPARATOR = ":";
+ public static final String DEFAULT_VALUE_SEPARATOR = SystemPropertyUtils.VALUE_SEPARATOR;
+
+ /**
+ * Default escape character: {@code '\'}.
+ * @since 6.2
+ * @see AbstractPropertyResolver#getDefaultEscapeCharacter()
+ */
+ public static final Character DEFAULT_ESCAPE_CHARACTER = SystemPropertyUtils.ESCAPE_CHARACTER;
- /** Default escape character: {@code '\'}. */
- public static final Character DEFAULT_ESCAPE_CHARACTER = '\\';
/** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */
protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;
@@ -113,9 +121,11 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
@Nullable
protected String valueSeparator = DEFAULT_VALUE_SEPARATOR;
- /** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */
+ /**
+ * The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
+ */
@Nullable
- protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER;
+ protected Character escapeCharacter = AbstractPropertyResolver.getDefaultEscapeCharacter();
protected boolean trimValues = false;
@@ -133,7 +143,7 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
/**
* Set the prefix that a placeholder string starts with.
- * The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}.
+ *
The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}.
*/
public void setPlaceholderPrefix(String placeholderPrefix) {
this.placeholderPrefix = placeholderPrefix;
@@ -141,31 +151,32 @@ public void setPlaceholderPrefix(String placeholderPrefix) {
/**
* Set the suffix that a placeholder string ends with.
- * The default is {@value #DEFAULT_PLACEHOLDER_SUFFIX}.
+ *
The default is {@value #DEFAULT_PLACEHOLDER_SUFFIX}.
*/
public void setPlaceholderSuffix(String placeholderSuffix) {
this.placeholderSuffix = placeholderSuffix;
}
/**
- * Specify the separating character between the placeholder variable
- * and the associated default value, or {@code null} if no such
- * special character should be processed as a value separator.
- * The default is {@value #DEFAULT_VALUE_SEPARATOR}.
+ * Specify the separating character between the placeholder variable and the
+ * associated default value, or {@code null} if no such special character
+ * should be processed as a value separator.
+ *
The default is {@value #DEFAULT_VALUE_SEPARATOR}.
*/
public void setValueSeparator(@Nullable String valueSeparator) {
this.valueSeparator = valueSeparator;
}
/**
- * Specify the escape character to use to ignore placeholder prefix
- * or value separator, or {@code null} if no escaping should take
- * place.
- *
Default is {@link #DEFAULT_ESCAPE_CHARACTER}.
+ * Set the escape character to use to ignore the
+ * {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the
+ * {@linkplain #setValueSeparator(String) value separator}, or {@code null}
+ * if no escaping should take place.
+ *
The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
* @since 6.2
*/
- public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) {
- this.escapeCharacter = escsEscapeCharacter;
+ public void setEscapeCharacter(@Nullable Character escapeCharacter) {
+ this.escapeCharacter = escapeCharacter;
}
/**
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java
index 840a34e76234..0a26f2041965 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyOverrideConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -35,12 +35,14 @@
*
* Example properties file:
*
- *
*
- * In contrast to PropertyPlaceholderConfigurer, the original definition can have default
- * values or no values at all for such bean properties. If an overriding properties file does
- * not have an entry for a certain bean property, the default context definition is used.
+ *
In contrast to {@link PropertyPlaceholderConfigurer}, the original definition
+ * can have default values or no values at all for such bean properties. If an
+ * overriding properties file does not have an entry for a certain bean property,
+ * the default context definition is used.
*
*
Note that the context definition is not aware of being overridden;
* so this is not immediately obvious when looking at the XML definition file.
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java
index a46a36c66d39..38887a61a91a 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractAutowireCapableBeanFactory.java
@@ -997,9 +997,17 @@ protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd,
*/
@Nullable
private FactoryBean> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
- boolean locked = this.singletonLock.tryLock();
- if (!locked) {
- return null;
+ Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
+ if (lockFlag == null) {
+ this.singletonLock.lock();
+ }
+ else {
+ boolean locked = (lockFlag && this.singletonLock.tryLock());
+ if (!locked) {
+ // Avoid shortcut FactoryBean instance but allow for subsequent type-based resolution.
+ resolveBeanClass(mbd, beanName);
+ return null;
+ }
}
try {
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java
index 44f24cc912dc..29271d5b0111 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java
@@ -1066,8 +1066,9 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName
@Nullable
protected Boolean isCurrentThreadAllowedToHoldSingletonLock() {
String mainThreadPrefix = this.mainThreadPrefix;
- if (this.mainThreadPrefix != null) {
- // We only differentiate in the preInstantiateSingletons phase.
+ if (mainThreadPrefix != null) {
+ // We only differentiate in the preInstantiateSingletons phase, using
+ // the volatile mainThreadPrefix field as an indicator for that phase.
PreInstantiation preInstantiation = this.preInstantiationThread.get();
if (preInstantiation != null) {
@@ -1087,7 +1088,7 @@ protected Boolean isCurrentThreadAllowedToHoldSingletonLock() {
}
else if (this.strictLocking == null) {
// No explicit locking configuration -> infer appropriate locking.
- if (mainThreadPrefix != null && !getThreadNamePrefix().equals(mainThreadPrefix)) {
+ if (!getThreadNamePrefix().equals(mainThreadPrefix)) {
// An unmanaged thread (assumed to be application-internal) with lenient locking,
// and not part of the same thread pool that provided the main bootstrap thread
// (excluding scenarios where we are hit by multiple external bootstrap threads).
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
index ad3ec147bd5f..a5f8585bc89b 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java
@@ -271,13 +271,15 @@ public Object getSingleton(String beanName, ObjectFactory> singletonFactory) {
// Fallback as of 6.2: process given singleton bean outside of singleton lock.
// Thread-safe exposure is still guaranteed, there is just a risk of collisions
// when triggering creation of other beans as dependencies of the current bean.
- if (logger.isInfoEnabled()) {
- logger.info("Obtaining singleton bean '" + beanName + "' in thread \"" +
- Thread.currentThread().getName() + "\" while other thread holds " +
- "singleton lock for other beans " + this.singletonsCurrentlyInCreation);
- }
this.lenientCreationLock.lock();
try {
+ if (logger.isInfoEnabled()) {
+ Set lockedBeans = new HashSet<>(this.singletonsCurrentlyInCreation);
+ lockedBeans.removeAll(this.singletonsInLenientCreation);
+ logger.info("Obtaining singleton bean '" + beanName + "' in thread \"" +
+ currentThread.getName() + "\" while other thread holds singleton " +
+ "lock for other beans " + lockedBeans);
+ }
this.singletonsInLenientCreation.add(beanName);
}
finally {
diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java
index ffcd87bbfbc1..2ba14e3484db 100644
--- a/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java
+++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/FactoryBeanRegistrySupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -118,7 +118,15 @@ protected Object getCachedObjectForFactoryBean(String beanName) {
*/
protected Object getObjectFromFactoryBean(FactoryBean> factory, String beanName, boolean shouldPostProcess) {
if (factory.isSingleton() && containsSingleton(beanName)) {
- this.singletonLock.lock();
+ Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
+ boolean locked;
+ if (lockFlag == null) {
+ this.singletonLock.lock();
+ locked = true;
+ }
+ else {
+ locked = (lockFlag && this.singletonLock.tryLock());
+ }
try {
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
@@ -131,11 +139,13 @@ protected Object getObjectFromFactoryBean(FactoryBean> factory, String beanNam
}
else {
if (shouldPostProcess) {
- if (isSingletonCurrentlyInCreation(beanName)) {
- // Temporarily return non-post-processed object, not storing it yet
- return object;
+ if (locked) {
+ if (isSingletonCurrentlyInCreation(beanName)) {
+ // Temporarily return non-post-processed object, not storing it yet
+ return object;
+ }
+ beforeSingletonCreation(beanName);
}
- beforeSingletonCreation(beanName);
try {
object = postProcessObjectFromFactoryBean(object, beanName);
}
@@ -144,7 +154,9 @@ protected Object getObjectFromFactoryBean(FactoryBean> factory, String beanNam
"Post-processing of FactoryBean's singleton object failed", ex);
}
finally {
- afterSingletonCreation(beanName);
+ if (locked) {
+ afterSingletonCreation(beanName);
+ }
}
}
if (containsSingleton(beanName)) {
@@ -155,7 +167,9 @@ protected Object getObjectFromFactoryBean(FactoryBean> factory, String beanNam
return object;
}
finally {
- this.singletonLock.unlock();
+ if (locked) {
+ this.singletonLock.unlock();
+ }
}
}
else {
diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle
index af48a0fa2070..e4795ee61bb9 100644
--- a/spring-context/spring-context.gradle
+++ b/spring-context/spring-context.gradle
@@ -59,3 +59,10 @@ dependencies {
testRuntimeOnly("org.javamoney:moneta")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine") // for @Inject TCK
}
+
+test {
+ description = "Runs JUnit Jupiter tests and the @Inject TCK via JUnit Vintage."
+ useJUnitPlatform {
+ includeEngines "junit-jupiter", "junit-vintage"
+ }
+}
diff --git a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java
index 0331b145c10e..eb5c736fafb9 100644
--- a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java
+++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextAotGenerator.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -50,6 +50,7 @@ public class ApplicationContextAotGenerator {
*/
public ClassName processAheadOfTime(GenericApplicationContext applicationContext,
GenerationContext generationContext) {
+
return withCglibClassHandler(new CglibClassHandler(generationContext), () -> {
applicationContext.refreshForAotProcessing(generationContext.getRuntimeHints());
ApplicationContextInitializationCodeGenerator codeGenerator =
diff --git a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java
index 16fbee9bb3a1..746e23cfd6f9 100644
--- a/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java
+++ b/spring-context/src/main/java/org/springframework/context/aot/ContextAotProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -80,8 +80,9 @@ protected Class> getApplicationClass() {
@Override
protected ClassName doProcess() {
deleteExistingOutput();
- GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass());
- return performAotProcessing(applicationContext);
+ try (GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass())) {
+ return performAotProcessing(applicationContext);
+ }
}
/**
diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java
index 4b6a7b79182a..d538a56ede36 100644
--- a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java
+++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -100,6 +100,7 @@ public BeanFactoryInitializationAotContribution build() {
return (!this.classes.isEmpty() ? new AotContribution(this.classes) : null);
}
+
private static class AotContribution implements BeanFactoryInitializationAotContribution {
private final Class>[] classes;
@@ -113,9 +114,9 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
registrar.registerRuntimeHints(runtimeHints, this.classes);
}
-
}
+
private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider {
@Nullable
diff --git a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java
index 31d1652542b8..7df8bf086ae5 100644
--- a/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java
+++ b/spring-context/src/main/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,6 @@
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertiesPropertySource;
-import org.springframework.core.env.PropertyResolver;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.PropertySources;
import org.springframework.core.env.PropertySourcesPropertyResolver;
@@ -49,7 +48,7 @@
* XSD documentation for complete details.
*
*
Any local properties (for example, those added via {@link #setProperties}, {@link #setLocations}
- * et al.) are added as a {@code PropertySource}. Search precedence of local properties is
+ * et al.) are added as a single {@link PropertySource}. Search precedence of local properties is
* based on the value of the {@link #setLocalOverride localOverride} property, which is by
* default {@code false} meaning that local properties are to be searched last, after all
* environment property sources.
@@ -101,8 +100,9 @@ public void setPropertySources(PropertySources propertySources) {
}
/**
- * {@code PropertySources} from the given {@link Environment}
- * will be searched when replacing ${...} placeholders.
+ * {@inheritDoc}
+ *
{@code PropertySources} from the given {@link Environment} will be searched
+ * when replacing ${...} placeholders.
* @see #setPropertySources
* @see #postProcessBeanFactory
*/
@@ -132,28 +132,11 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
if (this.propertySources == null) {
this.propertySources = new MutablePropertySources();
if (this.environment != null) {
- PropertyResolver propertyResolver = this.environment;
- // If the ignoreUnresolvablePlaceholders flag is set to true, we have to create a
- // local PropertyResolver to enforce that setting, since the Environment is most
- // likely not configured with ignoreUnresolvablePlaceholders set to true.
- // See https://github.com/spring-projects/spring-framework/issues/27947
- if (this.ignoreUnresolvablePlaceholders &&
- (this.environment instanceof ConfigurableEnvironment configurableEnvironment)) {
- PropertySourcesPropertyResolver resolver =
- new PropertySourcesPropertyResolver(configurableEnvironment.getPropertySources());
- resolver.setIgnoreUnresolvableNestedPlaceholders(true);
- propertyResolver = resolver;
- }
- PropertyResolver propertyResolverToUse = propertyResolver;
- this.propertySources.addLast(
- new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
- @Override
- @Nullable
- public String getProperty(String key) {
- return propertyResolverToUse.getProperty(key);
- }
- }
- );
+ PropertySource> environmentPropertySource =
+ (this.environment instanceof ConfigurableEnvironment configurableEnvironment ?
+ new ConfigurableEnvironmentPropertySource(configurableEnvironment) :
+ new FallbackEnvironmentPropertySource(this.environment));
+ this.propertySources.addLast(environmentPropertySource);
}
try {
PropertySource> localPropertySource =
@@ -176,6 +159,7 @@ public String getProperty(String key) {
/**
* Create a {@link ConfigurablePropertyResolver} for the specified property sources.
+ *
The default implementation creates a {@link PropertySourcesPropertyResolver}.
* @param propertySources the property sources to use
* @since 6.0.12
*/
@@ -188,7 +172,7 @@ protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySou
* placeholders with values from the given properties.
*/
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
- final ConfigurablePropertyResolver propertyResolver) throws BeansException {
+ ConfigurablePropertyResolver propertyResolver) throws BeansException {
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
@@ -234,4 +218,75 @@ public PropertySources getAppliedPropertySources() throws IllegalStateException
return this.appliedPropertySources;
}
+
+ /**
+ * Custom {@link PropertySource} that delegates to the
+ * {@link ConfigurableEnvironment#getPropertySources() PropertySources} in a
+ * {@link ConfigurableEnvironment}.
+ * @since 6.2.7
+ */
+ private static class ConfigurableEnvironmentPropertySource extends PropertySource {
+
+ ConfigurableEnvironmentPropertySource(ConfigurableEnvironment environment) {
+ super(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, environment);
+ }
+
+ @Override
+ public boolean containsProperty(String name) {
+ for (PropertySource> propertySource : super.source.getPropertySources()) {
+ if (propertySource.containsProperty(name)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ @Nullable
+ public Object getProperty(String name) {
+ for (PropertySource> propertySource : super.source.getPropertySources()) {
+ Object candidate = propertySource.getProperty(name);
+ if (candidate != null) {
+ return candidate;
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public String toString() {
+ return "ConfigurableEnvironmentPropertySource {propertySources=" + super.source.getPropertySources() + "}";
+ }
+ }
+
+
+ /**
+ * Fallback {@link PropertySource} that delegates to a raw {@link Environment}.
+ *
Should never apply in a regular scenario, since the {@code Environment}
+ * in an {@code ApplicationContext} should always be a {@link ConfigurableEnvironment}.
+ * @since 6.2.7
+ */
+ private static class FallbackEnvironmentPropertySource extends PropertySource {
+
+ FallbackEnvironmentPropertySource(Environment environment) {
+ super(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, environment);
+ }
+
+ @Override
+ public boolean containsProperty(String name) {
+ return super.source.containsProperty(name);
+ }
+
+ @Override
+ @Nullable
+ public Object getProperty(String name) {
+ return super.source.getProperty(name);
+ }
+
+ @Override
+ public String toString() {
+ return "FallbackEnvironmentPropertySource {environment=" + super.source + "}";
+ }
+ }
+
}
diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java
index 38f5c2f410cb..cced791405ec 100644
--- a/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java
+++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/EnableAsync.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -146,6 +146,12 @@
* compile-time weaving or load-time weaving applying the aspect to the affected classes.
* There is no proxy involved in such a scenario; local calls will be intercepted as well.
*
+ *
Note: {@code @EnableAsync} applies to its local application context only,
+ * allowing for selective activation at different levels. Please redeclare
+ * {@code @EnableAsync} in each individual context, for example, the common root web
+ * application context and any separate {@code DispatcherServlet} application contexts,
+ * if you need to apply its behavior at multiple levels.
+ *
* @author Chris Beams
* @author Juergen Hoeller
* @author Stephane Nicoll
diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java
index 3433af095bd1..ab134ce3b165 100644
--- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java
+++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java
@@ -27,7 +27,6 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
-import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
@@ -550,14 +549,13 @@ public String[] getAllowedFields() {
*
Mark fields as disallowed, for example to avoid unwanted
* modifications by malicious users when binding HTTP request parameters.
*
Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
- * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
- * well as direct equality.
- *
The default implementation of this method stores disallowed field patterns
- * in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical}
- * form and also transforms disallowed field patterns to
- * {@linkplain String#toLowerCase() lowercase} to support case-insensitive
- * pattern matching in {@link #isAllowed}. Subclasses which override this
- * method must therefore take both of these transformations into account.
+ * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts),
+ * as well as direct equality.
+ *
The default implementation of this method stores disallowed field
+ * patterns in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String)
+ * canonical} form, and subsequently pattern matching in {@link #isAllowed}
+ * is case-insensitive. Subclasses that override this method must therefore
+ * take this transformation into account.
*
More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
*
Alternatively, specify a list of allowed field patterns.
@@ -575,8 +573,7 @@ public void setDisallowedFields(@Nullable String... disallowedFields) {
else {
String[] fieldPatterns = new String[disallowedFields.length];
for (int i = 0; i < fieldPatterns.length; i++) {
- String field = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
- fieldPatterns[i] = field.toLowerCase(Locale.ROOT);
+ fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
}
this.disallowedFields = fieldPatterns;
}
@@ -1302,9 +1299,9 @@ protected void checkAllowedFields(MutablePropertyValues mpvs) {
* Determine if the given field is allowed for binding.
*
Invoked for each passed-in property value.
*
Checks for {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
- * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
- * well as direct equality, in the configured lists of allowed field patterns
- * and disallowed field patterns.
+ * {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts),
+ * as well as direct equality, in the configured lists of allowed field
+ * patterns and disallowed field patterns.
*
Matching against allowed field patterns is case-sensitive; whereas,
* matching against disallowed field patterns is case-insensitive.
*
A field matching a disallowed pattern will not be accepted even if it
@@ -1320,8 +1317,13 @@ protected void checkAllowedFields(MutablePropertyValues mpvs) {
protected boolean isAllowed(String field) {
String[] allowed = getAllowedFields();
String[] disallowed = getDisallowedFields();
- return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) &&
- (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase(Locale.ROOT))));
+ if (!ObjectUtils.isEmpty(allowed) && !PatternMatchUtils.simpleMatch(allowed, field)) {
+ return false;
+ }
+ if (!ObjectUtils.isEmpty(disallowed)) {
+ return !PatternMatchUtils.simpleMatchIgnoreCase(disallowed, field);
+ }
+ return true;
}
/**
diff --git a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml b/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml
deleted file mode 100644
index 3ea5d627e90c..000000000000
--- a/spring-context/src/main/resources/org/springframework/remoting/rmi/RmiInvocationWrapperRTD.xml
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
-
-
-
-
-
-
diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java
index 75f446f6ad3e..ed5e45b85b85 100644
--- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java
+++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java
@@ -24,6 +24,7 @@
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
+import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
@@ -243,14 +244,24 @@ public TestBean testBean3(TestBean testBean4) {
}
@Bean
- public TestBean testBean4() {
+ public FactoryBean testBean4() {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
- return new TestBean();
+ TestBean testBean = new TestBean();
+ return new FactoryBean<>() {
+ @Override
+ public TestBean getObject() {
+ return testBean;
+ }
+ @Override
+ public Class> getObjectType() {
+ return testBean.getClass();
+ }
+ };
}
}
diff --git a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java
index f9d0574d552a..bc36d8fd46f6 100644
--- a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java
+++ b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -38,7 +38,8 @@
* @author Juergen Hoeller
* @since 3.0
*/
-class SpringAtInjectTckTests {
+// WARNING: This class MUST be public, since it is based on JUnit 3.
+public class SpringAtInjectTckTests {
@SuppressWarnings("unchecked")
public static Test suite() {
diff --git a/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java
index 384f54d59fcc..f41711797fa1 100644
--- a/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java
+++ b/spring-context/src/test/java/org/springframework/context/aot/ContextAotProcessorTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,8 +45,9 @@ class ContextAotProcessorTests {
void processGeneratesAssets(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
- ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory);
+ DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory);
ClassName className = processor.process();
+ assertThat(processor.context.isClosed()).isTrue();
assertThat(className).isEqualTo(ClassName.get(SampleApplication.class.getPackageName(),
"ContextAotProcessorTests_SampleApplication__ApplicationContextInitializer"));
assertThat(directory).satisfies(hasGeneratedAssetsForSampleApplication());
@@ -61,9 +62,10 @@ void processingDeletesExistingOutput(@TempDir Path directory) throws IOException
Path existingSourceOutput = createExisting(sourceOutput);
Path existingResourceOutput = createExisting(resourceOutput);
Path existingClassOutput = createExisting(classOutput);
- ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class,
+ DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class,
sourceOutput, resourceOutput, classOutput);
processor.process();
+ assertThat(processor.context.isClosed()).isTrue();
assertThat(existingSourceOutput).doesNotExist();
assertThat(existingResourceOutput).doesNotExist();
assertThat(existingClassOutput).doesNotExist();
@@ -73,13 +75,14 @@ void processingDeletesExistingOutput(@TempDir Path directory) throws IOException
void processWithEmptyNativeImageArgumentsDoesNotCreateNativeImageProperties(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
- ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) {
+ DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) {
@Override
protected List getDefaultNativeImageArguments(String application) {
return Collections.emptyList();
}
};
processor.process();
+ assertThat(processor.context.isClosed()).isTrue();
assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/native-image.properties"))
.doesNotExist();
context.close();
@@ -118,6 +121,8 @@ private Consumer hasGeneratedAssetsForSampleApplication() {
private static class DemoContextAotProcessor extends ContextAotProcessor {
+ AnnotationConfigApplicationContext context;
+
DemoContextAotProcessor(Class> application, Path rootPath) {
this(application, rootPath.resolve("source"), rootPath.resolve("resource"), rootPath.resolve("class"));
}
@@ -141,11 +146,12 @@ private static Settings createSettings(Path sourceOutput, Path resourceOutput,
protected GenericApplicationContext prepareApplicationContext(Class> application) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(application);
+ this.context = context;
return context;
}
-
}
+
@Configuration(proxyBeanMethods = false)
static class SampleApplication {
@@ -153,7 +159,6 @@ static class SampleApplication {
public String testBean() {
return "Hello";
}
-
}
}
diff --git a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java
index c8bb5d5ef708..d78bb08406a4 100644
--- a/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java
+++ b/spring-context/src/test/java/org/springframework/context/support/PropertySourcesPlaceholderConfigurerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,34 @@
package org.springframework.context.support;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
import java.util.Optional;
import java.util.Properties;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+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.factory.BeanCreationException;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.annotation.Value;
+import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Scope;
+import org.springframework.core.SpringProperties;
import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.core.env.AbstractPropertyResolver;
+import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
@@ -38,12 +52,15 @@
import org.springframework.core.testfixture.env.MockPropertySource;
import org.springframework.mock.env.MockEnvironment;
import org.springframework.util.PlaceholderResolutionException;
+import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.rootBeanDefinition;
+import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME;
/**
* Tests for {@link PropertySourcesPlaceholderConfigurer}.
@@ -73,6 +90,43 @@ void replacementFromEnvironmentProperties() {
assertThat(ppc.getAppliedPropertySources()).isNotNull();
}
+ /**
+ * Ensure that a {@link PropertySource} added to the {@code Environment} after context
+ * refresh (i.e., after {@link PropertySourcesPlaceholderConfigurer#postProcessBeanFactory()}
+ * has been invoked) can still contribute properties in late-binding scenarios.
+ */
+ @Test // gh-34861
+ void replacementFromEnvironmentPropertiesWithLateBinding() {
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ MutablePropertySources propertySources = context.getEnvironment().getPropertySources();
+ propertySources.addFirst(new MockPropertySource("early properties").withProperty("foo", "bar"));
+
+ context.register(PropertySourcesPlaceholderConfigurer.class);
+ context.register(PrototypeBean.class);
+ context.refresh();
+
+ // Verify that placeholder resolution works for early binding.
+ PrototypeBean prototypeBean = context.getBean(PrototypeBean.class);
+ assertThat(prototypeBean.getName()).isEqualTo("bar");
+ assertThat(prototypeBean.isJedi()).isFalse();
+
+ // Add new PropertySource after context refresh.
+ propertySources.addFirst(new MockPropertySource("late properties").withProperty("jedi", "true"));
+
+ // Verify that placeholder resolution works for late binding: isJedi() switches to true.
+ prototypeBean = context.getBean(PrototypeBean.class);
+ assertThat(prototypeBean.getName()).isEqualTo("bar");
+ assertThat(prototypeBean.isJedi()).isTrue();
+
+ // Add yet another PropertySource after context refresh.
+ propertySources.addFirst(new MockPropertySource("even later properties").withProperty("foo", "enigma"));
+
+ // Verify that placeholder resolution works for even later binding: getName() switches to enigma.
+ prototypeBean = context.getBean(PrototypeBean.class);
+ assertThat(prototypeBean.getName()).isEqualTo("enigma");
+ assertThat(prototypeBean.isJedi()).isTrue();
+ }
+
@Test
void localPropertiesViaResource() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
@@ -88,14 +142,29 @@ void localPropertiesViaResource() {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo");
}
- @Test
- void localPropertiesOverrideFalse() {
- localPropertiesOverride(false);
- }
+ @ParameterizedTest
+ @ValueSource(booleans = {true, false})
+ void localPropertiesOverride(boolean override) {
+ DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
+ bf.registerBeanDefinition("testBean",
+ genericBeanDefinition(TestBean.class)
+ .addPropertyValue("name", "${foo}")
+ .getBeanDefinition());
- @Test
- void localPropertiesOverrideTrue() {
- localPropertiesOverride(true);
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+
+ ppc.setLocalOverride(override);
+ ppc.setProperties(new Properties() {{
+ setProperty("foo", "local");
+ }});
+ ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing"));
+ ppc.postProcessBeanFactory(bf);
+ if (override) {
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local");
+ }
+ else {
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing");
+ }
}
@Test
@@ -281,28 +350,58 @@ public Object getProperty(String key) {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("bar");
}
- @SuppressWarnings("serial")
- private void localPropertiesOverride(boolean override) {
+ @Test // gh-34861
+ void withEnumerableAndNonEnumerablePropertySourcesInTheEnvironmentAndLocalProperties() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("testBean",
genericBeanDefinition(TestBean.class)
- .addPropertyValue("name", "${foo}")
+ .addPropertyValue("name", "${foo:bogus}")
+ .addPropertyValue("jedi", "${local:false}")
.getBeanDefinition());
- PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ // 1) MockPropertySource is an EnumerablePropertySource.
+ MockPropertySource mockPropertySource = new MockPropertySource("mockPropertySource")
+ .withProperty("foo", "${bar}");
- ppc.setLocalOverride(override);
+ // 2) PropertySource is not an EnumerablePropertySource.
+ PropertySource> rawPropertySource = new PropertySource<>("rawPropertySource", new Object()) {
+ @Override
+ public Object getProperty(String key) {
+ return ("bar".equals(key) ? "quux" : null);
+ }
+ };
+
+ MockEnvironment env = new MockEnvironment();
+ env.getPropertySources().addFirst(mockPropertySource);
+ env.getPropertySources().addLast(rawPropertySource);
+
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ // 3) Local properties are stored in a PropertiesPropertySource which is an EnumerablePropertySource.
ppc.setProperties(new Properties() {{
- setProperty("foo", "local");
+ setProperty("local", "true");
}});
- ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing"));
ppc.postProcessBeanFactory(bf);
- if (override) {
- assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local");
- }
- else {
- assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing");
+
+ // Verify all properties can be resolved via the Environment.
+ assertThat(env.getProperty("foo")).isEqualTo("quux");
+ assertThat(env.getProperty("bar")).isEqualTo("quux");
+
+ // Verify that placeholder resolution works.
+ TestBean testBean = bf.getBean(TestBean.class);
+ assertThat(testBean.getName()).isEqualTo("quux");
+ assertThat(testBean.isJedi()).isTrue();
+
+ // Verify that the presence of a non-EnumerablePropertySource does not prevent
+ // accessing EnumerablePropertySources via getAppliedPropertySources().
+ List propertyNames = new ArrayList<>();
+ for (PropertySource> propertySource : ppc.getAppliedPropertySources()) {
+ if (propertySource instanceof EnumerablePropertySource> enumerablePropertySource) {
+ Collections.addAll(propertyNames, enumerablePropertySource.getPropertyNames());
+ }
}
+ // Should not contain "foo" or "bar" from the Environment.
+ assertThat(propertyNames).containsOnly("local");
}
@Test
@@ -432,6 +531,252 @@ void optionalPropertyWithoutValue() {
}
+ /**
+ * Tests that use the escape character (or disable it) with nested placeholder
+ * resolution.
+ */
+ @Nested
+ class EscapedNestedPlaceholdersTests {
+
+ @Test // gh-34861
+ void singleEscapeWithDefaultEscapeCharacter() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin")
+ .withProperty("my.property", "\\DOMAIN\\${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ ppc.postProcessBeanFactory(bf);
+
+ // \DOMAIN\${user.home} resolves to \DOMAIN${user.home} instead of \DOMAIN\admin
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN${user.home}");
+ }
+
+ @Test // gh-34861
+ void singleEscapeWithCustomEscapeCharacter() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin\\~${nested}")
+ .withProperty("my.property", "DOMAIN\\${user.home}\\~${enigma}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ // Set custom escape character.
+ ppc.setEscapeCharacter('~');
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}");
+ }
+
+ @Test // gh-34861
+ void singleEscapeWithEscapeCharacterDisabled() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin\\")
+ .withProperty("my.property", "\\DOMAIN\\${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ // Disable escape character.
+ ppc.setEscapeCharacter(null);
+ ppc.postProcessBeanFactory(bf);
+
+ // \DOMAIN\${user.home} resolves to \DOMAIN\admin
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin\\");
+ }
+
+ @Test // gh-34861
+ void tripleEscapeWithDefaultEscapeCharacter() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin\\\\\\")
+ .withProperty("my.property", "DOMAIN\\\\\\${user.home}#${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\\\${user.home}#admin\\\\\\");
+ }
+
+ @Test // gh-34861
+ void tripleEscapeWithCustomEscapeCharacter() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin\\~${enigma}")
+ .withProperty("my.property", "DOMAIN~~~${user.home}#${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ // Set custom escape character.
+ ppc.setEscapeCharacter('~');
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN~~${user.home}#admin\\${enigma}");
+ }
+
+ @Test // gh-34861
+ void singleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "${enigma}")
+ .withProperty("my.property", "\\${DOMAIN}${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ ppc.setIgnoreUnresolvablePlaceholders(true);
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}${enigma}");
+ }
+
+ @Test // gh-34861
+ void singleEscapeWithCustomEscapeCharacterAndIgnoreUnresolvablePlaceholders() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "${enigma}")
+ .withProperty("my.property", "~${DOMAIN}\\${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ // Set custom escape character.
+ ppc.setEscapeCharacter('~');
+ ppc.setIgnoreUnresolvablePlaceholders(true);
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("${DOMAIN}\\${enigma}");
+ }
+
+ @Test // gh-34861
+ void tripleEscapeWithDefaultEscapeCharacterAndIgnoreUnresolvablePlaceholders() {
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "${enigma}")
+ .withProperty("my.property", "X:\\\\\\${DOMAIN}${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ ppc.setIgnoreUnresolvablePlaceholders(true);
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("X:\\\\${DOMAIN}${enigma}");
+ }
+
+ private static DefaultListableBeanFactory createBeanFactory() {
+ BeanDefinition beanDefinition = genericBeanDefinition(TestBean.class)
+ .addPropertyValue("name", "${my.property}")
+ .getBeanDefinition();
+ DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
+ bf.registerBeanDefinition("testBean",beanDefinition);
+ return bf;
+ }
+
+ }
+
+
+ /**
+ * Tests that globally set the default escape character (or disable it) and
+ * rely on nested placeholder resolution.
+ */
+ @Nested
+ class GlobalDefaultEscapeCharacterTests {
+
+ private static final Field defaultEscapeCharacterField =
+ ReflectionUtils.findField(AbstractPropertyResolver.class, "defaultEscapeCharacter");
+
+ static {
+ ReflectionUtils.makeAccessible(defaultEscapeCharacterField);
+ }
+
+
+ @BeforeEach
+ void resetStateBeforeEachTest() {
+ resetState();
+ }
+
+ @AfterAll
+ static void resetState() {
+ ReflectionUtils.setField(defaultEscapeCharacterField, null, Character.MIN_VALUE);
+ setSpringProperty(null);
+ }
+
+
+ @Test // gh-34865
+ void defaultEscapeCharacterSetToXyz() {
+ setSpringProperty("XYZ");
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(PropertySourcesPlaceholderConfigurer::new)
+ .withMessage("Value [XYZ] for property [%s] must be a single character or an empty string",
+ DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME);
+ }
+
+ @Test // gh-34865
+ void defaultEscapeCharacterDisabled() {
+ setSpringProperty("");
+
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin")
+ .withProperty("my.property", "\\DOMAIN\\${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN\\admin");
+ }
+
+ @Test // gh-34865
+ void defaultEscapeCharacterSetToBackslash() {
+ setSpringProperty("\\");
+
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin")
+ .withProperty("my.property", "\\DOMAIN\\${user.home}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ ppc.postProcessBeanFactory(bf);
+
+ // \DOMAIN\${user.home} resolves to \DOMAIN${user.home} instead of \DOMAIN\admin
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("\\DOMAIN${user.home}");
+ }
+
+ @Test // gh-34865
+ void defaultEscapeCharacterSetToTilde() {
+ setSpringProperty("~");
+
+ MockEnvironment env = new MockEnvironment()
+ .withProperty("user.home", "admin\\~${nested}")
+ .withProperty("my.property", "DOMAIN\\${user.home}\\~${enigma}");
+
+ DefaultListableBeanFactory bf = createBeanFactory();
+ PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+ ppc.setEnvironment(env);
+ ppc.postProcessBeanFactory(bf);
+
+ assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("DOMAIN\\admin\\${nested}\\${enigma}");
+ }
+
+ private static void setSpringProperty(String value) {
+ SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value);
+ }
+
+ private static DefaultListableBeanFactory createBeanFactory() {
+ BeanDefinition beanDefinition = genericBeanDefinition(TestBean.class)
+ .addPropertyValue("name", "${my.property}")
+ .getBeanDefinition();
+ DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
+ bf.registerBeanDefinition("testBean",beanDefinition);
+ return bf;
+ }
+
+ }
+
+
private static class OptionalTestBean {
private Optional name;
@@ -472,4 +817,23 @@ static PropertySourcesPlaceholderConfigurer pspc() {
}
}
+ @Scope(BeanDefinition.SCOPE_PROTOTYPE)
+ static class PrototypeBean {
+
+ @Value("${foo:bogus}")
+ private String name;
+
+ @Value("${jedi:false}")
+ private boolean jedi;
+
+
+ public String getName() {
+ return this.name;
+ }
+
+ public boolean isJedi() {
+ return this.jedi;
+ }
+ }
+
}
diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java
index fd4077b78b19..bbbdcbaeee32 100644
--- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java
+++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java
@@ -463,10 +463,21 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader,
c = lookup.defineClass(b);
}
catch (LinkageError | IllegalArgumentException ex) {
- // in case of plain LinkageError (class already defined)
- // or IllegalArgumentException (class in different package):
- // fall through to traditional ClassLoader.defineClass below
- t = ex;
+ if (ex instanceof LinkageError) {
+ // Could be a ClassLoader mismatch with the class pre-existing in a
+ // parent ClassLoader -> try loadClass before giving up completely.
+ try {
+ c = contextClass.getClassLoader().loadClass(className);
+ }
+ catch (ClassNotFoundException cnfe) {
+ }
+ }
+ if (c == null) {
+ // in case of plain LinkageError (class already defined)
+ // or IllegalArgumentException (class in different package):
+ // fall through to traditional ClassLoader.defineClass below
+ t = ex;
+ }
}
catch (Throwable ex) {
throw new CodeGenerationException(ex);
diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java
index 1bb44d6cd264..209956fe72b2 100644
--- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java
+++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java
@@ -26,18 +26,20 @@
/**
* Static holder for local Spring properties, i.e. defined at the Spring library level.
*
- *
Reads a {@code spring.properties} file from the root of the Spring library classpath,
- * and also allows for programmatically setting properties through {@link #setProperty}.
- * When checking a property, local entries are being checked first, then falling back
- * to JVM-level system properties through a {@link System#getProperty} check.
+ *
Reads a {@code spring.properties} file from the root of the classpath and
+ * also allows for programmatically setting properties via {@link #setProperty}.
+ * When retrieving properties, local entries are checked first, with JVM-level
+ * system properties checked next as a fallback via {@link System#getProperty}.
*
*
This is an alternative way to set Spring-related system properties such as
- * "spring.getenv.ignore" and "spring.beaninfo.ignore", in particular for scenarios
- * where JVM system properties are locked on the target platform (for example, WebSphere).
- * See {@link #setFlag} for a convenient way to locally set such flags to "true".
+ * {@code spring.getenv.ignore} and {@code spring.beaninfo.ignore}, in particular
+ * for scenarios where JVM system properties are locked on the target platform
+ * (for example, WebSphere). See {@link #setFlag} for a convenient way to locally
+ * set such flags to {@code "true"}.
*
* @author Juergen Hoeller
* @since 3.2.7
+ * @see org.springframework.aot.AotDetector#AOT_ENABLED
* @see org.springframework.beans.StandardBeanInfoFactory#IGNORE_BEANINFO_PROPERTY_NAME
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java
index 918a63ee5554..fa92e4ef98de 100644
--- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java
+++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsScanner.java
@@ -361,16 +361,12 @@ private static boolean isOverride(Method rootMethod, Method candidateMethod) {
}
private static boolean hasSameParameterTypes(Method rootMethod, Method candidateMethod) {
- if (candidateMethod.getParameterCount() != rootMethod.getParameterCount()) {
- return false;
- }
Class>[] rootParameterTypes = rootMethod.getParameterTypes();
Class>[] candidateParameterTypes = candidateMethod.getParameterTypes();
if (Arrays.equals(candidateParameterTypes, rootParameterTypes)) {
return true;
}
- return hasSameGenericTypeParameters(rootMethod, candidateMethod,
- rootParameterTypes);
+ return hasSameGenericTypeParameters(rootMethod, candidateMethod, rootParameterTypes);
}
private static boolean hasSameGenericTypeParameters(
diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java
index de1e84c535b3..f45241726d65 100644
--- a/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java
+++ b/spring-core/src/main/java/org/springframework/core/env/AbstractEnvironment.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -42,7 +42,7 @@
* add by default. {@code AbstractEnvironment} adds none. Subclasses should contribute
* property sources through the protected {@link #customizePropertySources(MutablePropertySources)}
* hook, while clients should customize using {@link ConfigurableEnvironment#getPropertySources()}
- * and working against the {@link MutablePropertySources} API.
+ * and work against the {@link MutablePropertySources} API.
* See {@link ConfigurableEnvironment} javadoc for usage examples.
*
* @author Chris Beams
@@ -66,7 +66,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";
/**
- * Name of the property to set to specify active profiles: {@value}.
+ * Name of the property to specify active profiles: {@value}.
*
The value may be comma delimited.
*
Note that certain shell environments such as Bash disallow the use of the period
* character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource}
@@ -77,7 +77,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
public static final String ACTIVE_PROFILES_PROPERTY_NAME = "spring.profiles.active";
/**
- * Name of the property to set to specify profiles that are active by default: {@value}.
+ * Name of the property to specify profiles that are active by default: {@value}.
*
The value may be comma delimited.
*
Note that certain shell environments such as Bash disallow the use of the period
* character in variable names. Assuming that Spring's {@link SystemEnvironmentPropertySource}
@@ -141,7 +141,7 @@ protected AbstractEnvironment(MutablePropertySources propertySources) {
/**
* Factory method used to create the {@link ConfigurablePropertyResolver}
- * instance used by the Environment.
+ * used by this {@code Environment}.
* @since 5.3.4
* @see #getPropertyResolver()
*/
@@ -150,8 +150,7 @@ protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySou
}
/**
- * Return the {@link ConfigurablePropertyResolver} being used by the
- * {@link Environment}.
+ * Return the {@link ConfigurablePropertyResolver} used by the {@code Environment}.
* @since 5.3.4
* @see #createPropertyResolver(MutablePropertySources)
*/
@@ -320,7 +319,6 @@ public void addActiveProfile(String profile) {
}
}
-
@Override
public String[] getDefaultProfiles() {
return StringUtils.toStringArray(doGetDefaultProfiles());
@@ -328,7 +326,7 @@ public String[] getDefaultProfiles() {
/**
* Return the set of default profiles explicitly set via
- * {@link #setDefaultProfiles(String...)} or if the current set of default profiles
+ * {@link #setDefaultProfiles(String...)}, or if the current set of default profiles
* consists only of {@linkplain #getReservedDefaultProfiles() reserved default
* profiles}, then check for the presence of {@link #doGetActiveProfilesProperty()}
* and assign its value (if any) to the set of default profiles.
@@ -420,7 +418,7 @@ protected boolean isProfileActive(String profile) {
* active or default profiles.
*
Subclasses may override to impose further restrictions on profile syntax.
* @throws IllegalArgumentException if the profile is null, empty, whitespace-only or
- * begins with the profile NOT operator (!).
+ * begins with the profile NOT operator (!)
* @see #acceptsProfiles
* @see #addActiveProfile
* @see #setDefaultProfiles
diff --git a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java
index 151c482b7ed9..a13c40a74fba 100644
--- a/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java
+++ b/spring-core/src/main/java/org/springframework/core/env/AbstractPropertyResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -23,6 +23,7 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
+import org.springframework.core.SpringProperties;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
@@ -37,10 +38,52 @@
*
* @author Chris Beams
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 3.1
*/
public abstract class AbstractPropertyResolver implements ConfigurablePropertyResolver {
+ /**
+ * JVM system property used to change the default escape character
+ * for property placeholder support: {@value}.
+ *
To configure a custom escape character, supply a string containing a
+ * single character (other than {@link Character#MIN_VALUE}). For example,
+ * supplying the following JVM system property via the command line sets the
+ * default escape character to {@code '@'}.
+ *
-Dspring.placeholder.escapeCharacter.default=@
+ *
To disable escape character support, set the value to an empty string
+ * — for example, by supplying the following JVM system property via
+ * the command line.
+ *
-Dspring.placeholder.escapeCharacter.default=
+ *
If the property is not set, {@code '\'} will be used as the default
+ * escape character.
+ *
May alternatively be configured via a
+ * {@link org.springframework.core.SpringProperties spring.properties} file
+ * in the root of the classpath.
+ * @since 6.2.7
+ * @see #getDefaultEscapeCharacter()
+ */
+ public static final String DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME =
+ "spring.placeholder.escapeCharacter.default";
+
+ /**
+ * Since {@code null} is a valid value for {@link #defaultEscapeCharacter},
+ * this constant provides a way to represent an undefined (or not yet set)
+ * value. Consequently, {@link #getDefaultEscapeCharacter()} prevents the use
+ * of {@link Character#MIN_VALUE} as the actual escape character.
+ * @since 6.2.7
+ */
+ static final Character UNDEFINED_ESCAPE_CHARACTER = Character.MIN_VALUE;
+
+
+ /**
+ * Cached value for the default escape character.
+ * @since 6.2.7
+ */
+ @Nullable
+ static volatile Character defaultEscapeCharacter = UNDEFINED_ESCAPE_CHARACTER;
+
+
protected final Log logger = LogFactory.getLog(getClass());
@Nullable
@@ -62,7 +105,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
@Nullable
- private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER;
+ private Character escapeCharacter = getDefaultEscapeCharacter();
private final Set requiredProperties = new LinkedHashSet<>();
@@ -91,9 +134,9 @@ public void setConversionService(ConfigurableConversionService conversionService
}
/**
- * Set the prefix that placeholders replaced by this resolver must begin with.
- *
The default is "${".
- * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_PREFIX
+ * {@inheritDoc}
+ *
The default is "${".
+ * @see SystemPropertyUtils#PLACEHOLDER_PREFIX
*/
@Override
public void setPlaceholderPrefix(String placeholderPrefix) {
@@ -102,9 +145,9 @@ public void setPlaceholderPrefix(String placeholderPrefix) {
}
/**
- * Set the suffix that placeholders replaced by this resolver must end with.
- *
The default is "}".
- * @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_SUFFIX
+ * {@inheritDoc}
+ *
The default is "}".
+ * @see SystemPropertyUtils#PLACEHOLDER_SUFFIX
*/
@Override
public void setPlaceholderSuffix(String placeholderSuffix) {
@@ -113,11 +156,9 @@ public void setPlaceholderSuffix(String placeholderSuffix) {
}
/**
- * Specify the separating character between the placeholders replaced by this
- * resolver and their associated default value, or {@code null} if no such
- * special character should be processed as a value separator.
- *
The default is ":".
- * @see org.springframework.util.SystemPropertyUtils#VALUE_SEPARATOR
+ * {@inheritDoc}
+ *
The default is {@code ":"}.
+ * @see SystemPropertyUtils#VALUE_SEPARATOR
*/
@Override
public void setValueSeparator(@Nullable String valueSeparator) {
@@ -125,12 +166,9 @@ public void setValueSeparator(@Nullable String valueSeparator) {
}
/**
- * Specify the escape character to use to ignore placeholder prefix
- * or value separator, or {@code null} if no escaping should take
- * place.
- *
The default is "\".
+ * {@inheritDoc}
+ *
The default is determined by {@link #getDefaultEscapeCharacter()}.
* @since 6.2
- * @see org.springframework.util.SystemPropertyUtils#ESCAPE_CHARACTER
*/
@Override
public void setEscapeCharacter(@Nullable Character escapeCharacter) {
@@ -291,4 +329,60 @@ protected T convertValueIfNecessary(Object value, @Nullable Class targetT
@Nullable
protected abstract String getPropertyAsRawString(String key);
+
+ /**
+ * Get the default {@linkplain #setEscapeCharacter(Character) escape character}
+ * to use when parsing strings for property placeholder resolution.
+ *
This method attempts to retrieve the default escape character configured
+ * via the {@value #DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME} JVM system
+ * property or Spring property.
+ *
Falls back to {@code '\'} if the property has not been set.
+ * @return the configured default escape character, {@code null} if escape character
+ * support has been disabled, or {@code '\'} if the property has not been set
+ * @throws IllegalArgumentException if the property is configured with an
+ * invalid value, such as {@link Character#MIN_VALUE} or a string containing
+ * more than one character
+ * @since 6.2.7
+ * @see #DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME
+ * @see SystemPropertyUtils#ESCAPE_CHARACTER
+ * @see SpringProperties
+ */
+ @Nullable
+ public static Character getDefaultEscapeCharacter() throws IllegalArgumentException {
+ Character escapeCharacter = defaultEscapeCharacter;
+ if (UNDEFINED_ESCAPE_CHARACTER.equals(escapeCharacter)) {
+ String value = SpringProperties.getProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME);
+ if (value != null) {
+ if (value.isEmpty()) {
+ // Disable escape character support by default.
+ escapeCharacter = null;
+ }
+ else if (value.length() == 1) {
+ try {
+ // Use custom default escape character.
+ escapeCharacter = value.charAt(0);
+ }
+ catch (Exception ex) {
+ throw new IllegalArgumentException("Failed to process value [%s] for property [%s]: %s"
+ .formatted(value, DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, ex.getMessage()), ex);
+ }
+ Assert.isTrue(!escapeCharacter.equals(Character.MIN_VALUE),
+ () -> "Value for property [%s] must not be Character.MIN_VALUE"
+ .formatted(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME));
+ }
+ else {
+ throw new IllegalArgumentException(
+ "Value [%s] for property [%s] must be a single character or an empty string"
+ .formatted(value, DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME));
+ }
+ }
+ else {
+ // Use standard default value for the escape character.
+ escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER;
+ }
+ defaultEscapeCharacter = escapeCharacter;
+ }
+ return escapeCharacter;
+ }
+
}
diff --git a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java
index 5fe8115842c1..56f47247c926 100644
--- a/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java
+++ b/spring-core/src/main/java/org/springframework/core/env/CompositePropertySource.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -34,7 +34,10 @@
*
*
As of Spring 4.1.2, this class extends {@link EnumerablePropertySource} instead
* of plain {@link PropertySource}, exposing {@link #getPropertyNames()} based on the
- * accumulated property names from all contained sources (as far as possible).
+ * accumulated property names from all contained sources - and failing with an
+ * {@code IllegalStateException} against any non-{@code EnumerablePropertySource}.
+ * When used through the {@code EnumerablePropertySource} contract, all contained
+ * sources are expected to be of type {@code EnumerablePropertySource} as well.
*
* @author Chris Beams
* @author Juergen Hoeller
diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java
index 9de866854fd0..34ecf6623400 100644
--- a/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java
+++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurableEnvironment.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2022 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -137,13 +137,13 @@ public interface ConfigurableEnvironment extends Environment, ConfigurableProper
Map getSystemEnvironment();
/**
- * Append the given parent environment's active profiles, default profiles and
+ * Append the given parent environment's active profiles, default profiles, and
* property sources to this (child) environment's respective collections of each.
*
For any identically-named {@code PropertySource} instance existing in both
* parent and child, the child instance is to be preserved and the parent instance
* discarded. This has the effect of allowing overriding of property sources by the
- * child as well as avoiding redundant searches through common property source types,
- * for example, system environment and system properties.
+ * child as well as avoiding redundant searches through common property source types
+ * — for example, system environment and system properties.
*
Active and default profile names are also filtered for duplicates, to avoid
* confusion and redundant storage.
*
The parent environment remains unmodified in any case. Note that any changes to
diff --git a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java
index 01f47dae1f62..d440b78135b1 100644
--- a/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java
+++ b/spring-core/src/main/java/org/springframework/core/env/ConfigurablePropertyResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -69,21 +69,23 @@ public interface ConfigurablePropertyResolver extends PropertyResolver {
void setPlaceholderSuffix(String placeholderSuffix);
/**
- * Specify the separating character between the placeholders replaced by this
- * resolver and their associated default value, or {@code null} if no such
+ * Set the separating character to be honored between placeholders replaced by
+ * this resolver and their associated default values, or {@code null} if no such
* special character should be processed as a value separator.
*/
void setValueSeparator(@Nullable String valueSeparator);
/**
- * Specify the escape character to use to ignore placeholder prefix or
- * value separator, or {@code null} if no escaping should take place.
+ * Set the escape character to use to ignore the
+ * {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the
+ * {@linkplain #setValueSeparator(String) value separator}, or {@code null}
+ * if no escaping should take place.
* @since 6.2
*/
void setEscapeCharacter(@Nullable Character escapeCharacter);
/**
- * Set whether to throw an exception when encountering an unresolvable placeholder
+ * Specify whether to throw an exception when encountering an unresolvable placeholder
* nested within the value of a given property. A {@code false} value indicates strict
* resolution, i.e. that an exception will be thrown. A {@code true} value indicates
* that unresolvable nested placeholders should be passed through in their unresolved
@@ -106,7 +108,7 @@ public interface ConfigurablePropertyResolver extends PropertyResolver {
* {@link #setRequiredProperties} is present and resolves to a
* non-{@code null} value.
* @throws MissingRequiredPropertiesException if any of the required
- * properties are not resolvable.
+ * properties are not resolvable
*/
void validateRequiredProperties() throws MissingRequiredPropertiesException;
diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java
index 173a1a33784d..55a7e84968d5 100644
--- a/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java
+++ b/spring-core/src/main/java/org/springframework/core/env/PropertyResolver.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,13 +30,13 @@
public interface PropertyResolver {
/**
- * Return whether the given property key is available for resolution,
- * i.e. if the value for the given key is not {@code null}.
+ * Determine whether the given property key is available for resolution
+ * — for example, if the value for the given key is not {@code null}.
*/
boolean containsProperty(String key);
/**
- * Return the property value associated with the given key,
+ * Resolve the property value associated with the given key,
* or {@code null} if the key cannot be resolved.
* @param key the property name to resolve
* @see #getProperty(String, String)
@@ -47,7 +47,7 @@ public interface PropertyResolver {
String getProperty(String key);
/**
- * Return the property value associated with the given key, or
+ * Resolve the property value associated with the given key, or
* {@code defaultValue} if the key cannot be resolved.
* @param key the property name to resolve
* @param defaultValue the default value to return if no value is found
@@ -57,7 +57,7 @@ public interface PropertyResolver {
String getProperty(String key, String defaultValue);
/**
- * Return the property value associated with the given key,
+ * Resolve the property value associated with the given key,
* or {@code null} if the key cannot be resolved.
* @param key the property name to resolve
* @param targetType the expected type of the property value
@@ -67,7 +67,7 @@ public interface PropertyResolver {
T getProperty(String key, Class targetType);
/**
- * Return the property value associated with the given key,
+ * Resolve the property value associated with the given key,
* or {@code defaultValue} if the key cannot be resolved.
* @param key the property name to resolve
* @param targetType the expected type of the property value
@@ -77,14 +77,14 @@ public interface PropertyResolver {
T getProperty(String key, Class targetType, T defaultValue);
/**
- * Return the property value associated with the given key (never {@code null}).
+ * Resolve the property value associated with the given key (never {@code null}).
* @throws IllegalStateException if the key cannot be resolved
* @see #getRequiredProperty(String, Class)
*/
String getRequiredProperty(String key) throws IllegalStateException;
/**
- * Return the property value associated with the given key, converted to the given
+ * Resolve the property value associated with the given key, converted to the given
* targetType (never {@code null}).
* @throws IllegalStateException if the given key cannot be resolved
*/
diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java
index 2dcfb4f322dc..13d4f2cddb2a 100644
--- a/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java
+++ b/spring-core/src/main/java/org/springframework/core/io/AbstractFileResolvingResource.java
@@ -29,6 +29,7 @@
import java.nio.file.NoSuchFileException;
import java.nio.file.StandardOpenOption;
import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
import org.springframework.util.ResourceUtils;
@@ -44,6 +45,7 @@
*/
public abstract class AbstractFileResolvingResource extends AbstractResource {
+ @SuppressWarnings("try")
@Override
public boolean exists() {
try {
@@ -86,7 +88,11 @@ else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
if (con instanceof JarURLConnection jarCon) {
// For JarURLConnection, do not check content-length but rather the
// existence of the entry (or the jar root in case of no entryName).
- return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null);
+ // getJarFile() called for enforced presence check of the jar file,
+ // throwing a NoSuchFileException otherwise (turned to false below).
+ try (JarFile jarFile = jarCon.getJarFile()) {
+ return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null);
+ }
}
else if (con.getContentLengthLong() > 0) {
return true;
diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java
index 7fe7c54b082f..4a73678fd86d 100644
--- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java
+++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java
@@ -36,6 +36,7 @@
import java.nio.file.FileSystems;
import java.nio.file.FileVisitOption;
import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Enumeration;
@@ -874,9 +875,9 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource,
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
closeJarFile = !jarCon.getUseCaches();
}
- catch (ZipException | FileNotFoundException ex) {
+ catch (ZipException | FileNotFoundException | NoSuchFileException ex) {
// Happens in case of a non-jar file or in case of a cached root directory
- // without specific subdirectory present, respectively.
+ // without the specific subdirectory present, respectively.
return Collections.emptySet();
}
}
@@ -1275,7 +1276,7 @@ private static String fixPath(String path) {
}
/**
- * Return a alternative form of the resource, i.e. with or without a leading slash.
+ * Return an alternative form of the resource, i.e. with or without a leading slash.
* @param path the file path (with or without a leading slash)
* @return the alternative form or {@code null}
*/
diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java
index 85d53d40475a..12cf39559cae 100644
--- a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java
+++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -141,8 +141,8 @@ public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersi
/**
- * Return a merged Properties instance containing both the
- * loaded properties and properties set on this FactoryBean.
+ * Return a merged {@link Properties} instance containing both the
+ * loaded properties and properties set on this component.
*/
protected Properties mergeProperties() throws IOException {
Properties result = new Properties();
diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java
index 9f050351f0b6..f0f0070567d0 100644
--- a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2023 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -37,13 +37,25 @@ public abstract class PatternMatchUtils {
* @return whether the String matches the given pattern
*/
public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) {
+ return simpleMatch(pattern, str, false);
+ }
+
+ /**
+ * Variant of {@link #simpleMatch(String, String)} that ignores upper/lower case.
+ * @since 6.1.20
+ */
+ public static boolean simpleMatchIgnoreCase(@Nullable String pattern, @Nullable String str) {
+ return simpleMatch(pattern, str, true);
+ }
+
+ private static boolean simpleMatch(@Nullable String pattern, @Nullable String str, boolean ignoreCase) {
if (pattern == null || str == null) {
return false;
}
int firstIndex = pattern.indexOf('*');
if (firstIndex == -1) {
- return pattern.equals(str);
+ return (ignoreCase ? pattern.equalsIgnoreCase(str) : pattern.equals(str));
}
if (firstIndex == 0) {
@@ -52,25 +64,43 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str
}
int nextIndex = pattern.indexOf('*', 1);
if (nextIndex == -1) {
- return str.endsWith(pattern.substring(1));
+ String part = pattern.substring(1);
+ return (ignoreCase ? StringUtils.endsWithIgnoreCase(str, part) : str.endsWith(part));
}
String part = pattern.substring(1, nextIndex);
if (part.isEmpty()) {
- return simpleMatch(pattern.substring(nextIndex), str);
+ return simpleMatch(pattern.substring(nextIndex), str, ignoreCase);
}
- int partIndex = str.indexOf(part);
+ int partIndex = indexOf(str, part, 0, ignoreCase);
while (partIndex != -1) {
- if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) {
+ if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()), ignoreCase)) {
return true;
}
- partIndex = str.indexOf(part, partIndex + 1);
+ partIndex = indexOf(str, part, partIndex + 1, ignoreCase);
}
return false;
}
return (str.length() >= firstIndex &&
- pattern.startsWith(str.substring(0, firstIndex)) &&
- simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex)));
+ checkStartsWith(pattern, str, firstIndex, ignoreCase) &&
+ simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex), ignoreCase));
+ }
+
+ private static boolean checkStartsWith(String pattern, String str, int index, boolean ignoreCase) {
+ String part = str.substring(0, index);
+ return (ignoreCase ? StringUtils.startsWithIgnoreCase(pattern, part) : pattern.startsWith(part));
+ }
+
+ private static int indexOf(String str, String otherStr, int startIndex, boolean ignoreCase) {
+ if (!ignoreCase) {
+ return str.indexOf(otherStr, startIndex);
+ }
+ for (int i = startIndex; i <= (str.length() - otherStr.length()); i++) {
+ if (str.regionMatches(true, i, otherStr, 0, otherStr.length())) {
+ return i;
+ }
+ }
+ return -1;
}
/**
@@ -94,4 +124,19 @@ public static boolean simpleMatch(@Nullable String[] patterns, @Nullable String
return false;
}
+ /**
+ * Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case.
+ * @since 6.1.20
+ */
+ public static boolean simpleMatchIgnoreCase(@Nullable String[] patterns, @Nullable String str) {
+ if (patterns != null) {
+ for (String pattern : patterns) {
+ if (simpleMatch(pattern, str, true)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
}
diff --git a/spring-core/src/main/java/org/springframework/util/StreamUtils.java b/spring-core/src/main/java/org/springframework/util/StreamUtils.java
index 99b44e03f29e..354e7b2cf20f 100644
--- a/spring-core/src/main/java/org/springframework/util/StreamUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/StreamUtils.java
@@ -239,7 +239,7 @@ public static OutputStream nonClosing(OutputStream out) {
}
- private static class NonClosingInputStream extends FilterInputStream {
+ private static final class NonClosingInputStream extends FilterInputStream {
public NonClosingInputStream(InputStream in) {
super(in);
@@ -248,10 +248,30 @@ public NonClosingInputStream(InputStream in) {
@Override
public void close() throws IOException {
}
+
+ @Override
+ public byte[] readAllBytes() throws IOException {
+ return in.readAllBytes();
+ }
+
+ @Override
+ public byte[] readNBytes(int len) throws IOException {
+ return in.readNBytes(len);
+ }
+
+ @Override
+ public int readNBytes(byte[] b, int off, int len) throws IOException {
+ return in.readNBytes(b, off, len);
+ }
+
+ @Override
+ public long transferTo(OutputStream out) throws IOException {
+ return in.transferTo(out);
+ }
}
- private static class NonClosingOutputStream extends FilterOutputStream {
+ private static final class NonClosingOutputStream extends FilterOutputStream {
public NonClosingOutputStream(OutputStream out) {
super(out);
diff --git a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java
index eb962efc5bb3..fae3b69934b4 100644
--- a/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/SystemPropertyUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -35,16 +35,19 @@
*/
public abstract class SystemPropertyUtils {
- /** Prefix for system property placeholders: {@value}. */
+ /** Prefix for property placeholders: {@value}. */
public static final String PLACEHOLDER_PREFIX = "${";
- /** Suffix for system property placeholders: {@value}. */
+ /** Suffix for property placeholders: {@value}. */
public static final String PLACEHOLDER_SUFFIX = "}";
- /** Value separator for system property placeholders: {@value}. */
+ /** Value separator for property placeholders: {@value}. */
public static final String VALUE_SEPARATOR = ":";
- /** Default escape character: {@code '\'}. */
+ /**
+ * Escape character for property placeholders: {@code '\'}.
+ * @since 6.2
+ */
public static final Character ESCAPE_CHARACTER = '\\';
diff --git a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java
index 5cd39685fa28..79836518a165 100644
--- a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java
+++ b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -85,6 +85,7 @@ public class ExponentialBackOff implements BackOff {
*/
public static final int DEFAULT_MAX_ATTEMPTS = Integer.MAX_VALUE;
+
private long initialInterval = DEFAULT_INITIAL_INTERVAL;
private double multiplier = DEFAULT_MULTIPLIER;
@@ -204,6 +205,7 @@ public int getMaxAttempts() {
return this.maxAttempts;
}
+
@Override
public BackOffExecution start() {
return new ExponentialBackOffExecution();
@@ -225,6 +227,7 @@ public String toString() {
.toString();
}
+
private class ExponentialBackOffExecution implements BackOffExecution {
private long currentInterval = -1;
diff --git a/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java
index b4d80c481227..9695077362b1 100644
--- a/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java
+++ b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -35,6 +35,7 @@ public class FixedBackOff implements BackOff {
*/
public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE;
+
private long interval = DEFAULT_INTERVAL;
private long maxAttempts = UNLIMITED_ATTEMPTS;
@@ -86,6 +87,7 @@ public long getMaxAttempts() {
return this.maxAttempts;
}
+
@Override
public BackOffExecution start() {
return new FixedBackOffExecution();
diff --git a/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java
new file mode 100644
index 000000000000..ca536d83078c
--- /dev/null
+++ b/spring-core/src/test/java/org/springframework/core/env/AbstractPropertyResolverTests.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2002-2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.core.env;
+
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.core.SpringProperties;
+import org.springframework.lang.Nullable;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
+import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME;
+import static org.springframework.core.env.AbstractPropertyResolver.UNDEFINED_ESCAPE_CHARACTER;
+
+/**
+ * Unit tests for {@link AbstractPropertyResolver}.
+ *
+ * @author Sam Brannen
+ * @since 6.2.7
+ */
+class AbstractPropertyResolverTests {
+
+ @BeforeEach
+ void resetStateBeforeEachTest() {
+ resetState();
+ }
+
+ @AfterAll
+ static void resetState() {
+ AbstractPropertyResolver.defaultEscapeCharacter = UNDEFINED_ESCAPE_CHARACTER;
+ setSpringProperty(null);
+ }
+
+
+ @Test
+ void getDefaultEscapeCharacterWithSpringPropertySetToCharacterMinValue() {
+ setSpringProperty("" + Character.MIN_VALUE);
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter)
+ .withMessage("Value for property [%s] must not be Character.MIN_VALUE",
+ DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME);
+
+ assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER);
+ }
+
+ @Test
+ void getDefaultEscapeCharacterWithSpringPropertySetToXyz() {
+ setSpringProperty("XYZ");
+
+ assertThatIllegalArgumentException()
+ .isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter)
+ .withMessage("Value [XYZ] for property [%s] must be a single character or an empty string",
+ DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME);
+
+ assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER);
+ }
+
+ @Test
+ void getDefaultEscapeCharacterWithSpringPropertySetToEmptyString() {
+ setSpringProperty("");
+ assertEscapeCharacter(null);
+ }
+
+ @Test
+ void getDefaultEscapeCharacterWithoutSpringPropertySet() {
+ assertEscapeCharacter('\\');
+ }
+
+ @Test
+ void getDefaultEscapeCharacterWithSpringPropertySetToBackslash() {
+ setSpringProperty("\\");
+ assertEscapeCharacter('\\');
+ }
+
+ @Test
+ void getDefaultEscapeCharacterWithSpringPropertySetToTilde() {
+ setSpringProperty("~");
+ assertEscapeCharacter('~');
+ }
+
+ @Test
+ void getDefaultEscapeCharacterFromMultipleThreads() {
+ setSpringProperty("~");
+
+ IntStream.range(1, 32).parallel().forEach(__ ->
+ assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo('~'));
+
+ assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo('~');
+ }
+
+
+ private static void setSpringProperty(String value) {
+ SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value);
+ }
+
+ private static void assertEscapeCharacter(@Nullable Character expected) {
+ assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo(expected);
+ assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(expected);
+ }
+
+}
diff --git a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java
index 23654dfe10e5..514052e47c6b 100644
--- a/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java
+++ b/spring-core/src/test/java/org/springframework/core/env/PropertySourcesPropertyResolverTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@
import java.util.Properties;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.ConverterNotFoundException;
@@ -38,18 +39,15 @@
*/
class PropertySourcesPropertyResolverTests {
- private Properties testProperties;
+ private final Properties testProperties = new Properties();
- private MutablePropertySources propertySources;
+ private final MutablePropertySources propertySources = new MutablePropertySources();
- private ConfigurablePropertyResolver propertyResolver;
+ private final PropertySourcesPropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);
@BeforeEach
void setUp() {
- propertySources = new MutablePropertySources();
- propertyResolver = new PropertySourcesPropertyResolver(propertySources);
- testProperties = new Properties();
propertySources.addFirst(new PropertiesPropertySource("testProperties", testProperties));
}
@@ -77,14 +75,12 @@ void getProperty_withDefaultValue() {
@Test
void getProperty_propertySourceSearchOrderIsFIFO() {
- MutablePropertySources sources = new MutablePropertySources();
- PropertyResolver resolver = new PropertySourcesPropertyResolver(sources);
- sources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value"));
- assertThat(resolver.getProperty("pName")).isEqualTo("ps1Value");
- sources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value"));
- assertThat(resolver.getProperty("pName")).isEqualTo("ps2Value");
- sources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value"));
- assertThat(resolver.getProperty("pName")).isEqualTo("ps3Value");
+ propertySources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value"));
+ assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps1Value");
+ propertySources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value"));
+ assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps2Value");
+ propertySources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value"));
+ assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps3Value");
}
@Test
@@ -115,8 +111,8 @@ void getProperty_withNonConvertibleTargetType() {
class TestType { }
- assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() ->
- propertyResolver.getProperty("foo", TestType.class));
+ assertThatExceptionOfType(ConverterNotFoundException.class)
+ .isThrownBy(() -> propertyResolver.getProperty("foo", TestType.class));
}
@Test
@@ -127,7 +123,6 @@ void getProperty_doesNotCache_replaceExistingKeyPostConstruction() {
HashMap map = new HashMap<>();
map.put(key, value1); // before construction
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MapPropertySource("testProperties", map));
PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(propertyResolver.getProperty(key)).isEqualTo(value1);
@@ -138,7 +133,6 @@ void getProperty_doesNotCache_replaceExistingKeyPostConstruction() {
@Test
void getProperty_doesNotCache_addNewKeyPostConstruction() {
HashMap map = new HashMap<>();
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MapPropertySource("testProperties", map));
PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(propertyResolver.getProperty("foo")).isNull();
@@ -148,10 +142,9 @@ void getProperty_doesNotCache_addNewKeyPostConstruction() {
@Test
void getPropertySources_replacePropertySource() {
- propertySources = new MutablePropertySources();
- propertyResolver = new PropertySourcesPropertyResolver(propertySources);
propertySources.addLast(new MockPropertySource("local").withProperty("foo", "localValue"));
propertySources.addLast(new MockPropertySource("system").withProperty("foo", "systemValue"));
+ assertThat(propertySources).hasSize(3);
// 'local' was added first so has precedence
assertThat(propertyResolver.getProperty("foo")).isEqualTo("localValue");
@@ -162,7 +155,7 @@ void getPropertySources_replacePropertySource() {
// 'system' now has precedence
assertThat(propertyResolver.getProperty("foo")).isEqualTo("newValue");
- assertThat(propertySources).hasSize(2);
+ assertThat(propertySources).hasSize(3);
}
@Test
@@ -170,81 +163,65 @@ void getRequiredProperty() {
testProperties.put("exists", "xyz");
assertThat(propertyResolver.getRequiredProperty("exists")).isEqualTo("xyz");
- assertThatIllegalStateException().isThrownBy(() ->
- propertyResolver.getRequiredProperty("bogus"));
+ assertThatIllegalStateException().isThrownBy(() -> propertyResolver.getRequiredProperty("bogus"));
}
@Test
void getRequiredProperty_withStringArrayConversion() {
testProperties.put("exists", "abc,123");
- assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).isEqualTo(new String[] { "abc", "123" });
+ assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).containsExactly("abc", "123");
- assertThatIllegalStateException().isThrownBy(() ->
- propertyResolver.getRequiredProperty("bogus", String[].class));
+ assertThatIllegalStateException().isThrownBy(() -> propertyResolver.getRequiredProperty("bogus", String[].class));
}
@Test
void resolvePlaceholders() {
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
- PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
- assertThat(resolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
+ assertThat(propertyResolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
}
@Test
void resolvePlaceholders_withUnresolvable() {
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
- PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
- assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown}"))
+ assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown}"))
.isEqualTo("Replace this value plus ${unknown}");
}
@Test
void resolvePlaceholders_withDefaultValue() {
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
- PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
- assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
+ assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
.isEqualTo("Replace this value plus defaultValue");
}
@Test
void resolvePlaceholders_withNullInput() {
- assertThatIllegalArgumentException().isThrownBy(() ->
- new PropertySourcesPropertyResolver(new MutablePropertySources()).resolvePlaceholders(null));
+ assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolvePlaceholders(null));
}
@Test
void resolveRequiredPlaceholders() {
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
- PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
- assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
+ assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
}
@Test
void resolveRequiredPlaceholders_withUnresolvable() {
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
- PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
- assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
- resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}"));
+ assertThatExceptionOfType(PlaceholderResolutionException.class)
+ .isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}"));
}
@Test
void resolveRequiredPlaceholders_withDefaultValue() {
- MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
- PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
- assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
+ assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
.isEqualTo("Replace this value plus defaultValue");
}
@Test
void resolveRequiredPlaceholders_withNullInput() {
- assertThatIllegalArgumentException().isThrownBy(() ->
- new PropertySourcesPropertyResolver(new MutablePropertySources()).resolveRequiredPlaceholders(null));
+ assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders(null));
}
@Test
@@ -256,17 +233,17 @@ void setRequiredProperties_andValidateRequiredProperties() {
propertyResolver.setRequiredProperties("foo", "bar");
// neither foo nor bar properties are present -> validating should throw
- assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(
- propertyResolver::validateRequiredProperties)
- .withMessage("The following properties were declared as required " +
- "but could not be resolved: [foo, bar]");
+ assertThatExceptionOfType(MissingRequiredPropertiesException.class)
+ .isThrownBy(propertyResolver::validateRequiredProperties)
+ .withMessage("The following properties were declared as required " +
+ "but could not be resolved: [foo, bar]");
// add foo property -> validation should fail only on missing 'bar' property
testProperties.put("foo", "fooValue");
- assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(
- propertyResolver::validateRequiredProperties)
- .withMessage("The following properties were declared as required " +
- "but could not be resolved: [bar]");
+ assertThatExceptionOfType(MissingRequiredPropertiesException.class)
+ .isThrownBy(propertyResolver::validateRequiredProperties)
+ .withMessage("The following properties were declared as required " +
+ "but could not be resolved: [bar]");
// add bar property -> validation should pass, even with an empty string value
testProperties.put("bar", "");
@@ -291,13 +268,13 @@ void resolveNestedPropertyPlaceholders() {
assertThat(pr.getProperty("p2")).isEqualTo("v2");
assertThat(pr.getProperty("p3")).isEqualTo("v1:v2");
assertThat(pr.getProperty("p4")).isEqualTo("v1:v2");
- assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
- pr.getProperty("p5"))
- .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\"");
+ assertThatExceptionOfType(PlaceholderResolutionException.class)
+ .isThrownBy(() -> pr.getProperty("p5"))
+ .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\"");
assertThat(pr.getProperty("p6")).isEqualTo("v1:v2:def");
- assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
- pr.getProperty("pL"))
- .withMessageContaining("Circular");
+ assertThatExceptionOfType(PlaceholderResolutionException.class)
+ .isThrownBy(() -> pr.getProperty("pL"))
+ .withMessageContaining("Circular");
}
@Test
@@ -349,9 +326,9 @@ void ignoreUnresolvableNestedPlaceholdersIsConfigurable() {
// placeholders nested within the value of "p4" are unresolvable and cause an
// exception by default
- assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
- pr.getProperty("p4"))
- .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\"");
+ assertThatExceptionOfType(PlaceholderResolutionException.class)
+ .isThrownBy(() -> pr.getProperty("p4"))
+ .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\"");
// relax the treatment of unresolvable nested placeholders
pr.setIgnoreUnresolvableNestedPlaceholders(true);
@@ -361,9 +338,58 @@ void ignoreUnresolvableNestedPlaceholdersIsConfigurable() {
// resolve[Nested]Placeholders methods behave as usual regardless the value of
// ignoreUnresolvableNestedPlaceholders
assertThat(pr.resolvePlaceholders("${p1}:${p2}:${bogus}")).isEqualTo("v1:v2:${bogus}");
- assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
- pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}"))
- .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\"");
+ assertThatExceptionOfType(PlaceholderResolutionException.class)
+ .isThrownBy(() -> pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}"))
+ .withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\"");
+ }
+
+
+ @Nested
+ class EscapedPlaceholderTests {
+
+ @Test // gh-34720
+ void escapedPlaceholdersAreNotEvaluated() {
+ testProperties.put("prop1", "value1");
+ testProperties.put("prop2", "value2\\${prop1}");
+
+ assertThat(propertyResolver.getProperty("prop2")).isEqualTo("value2${prop1}");
+ }
+
+ @Test // gh-34720
+ void escapedPlaceholdersAreNotEvaluatedWithCharSequenceValues() {
+ testProperties.put("prop1", "value1");
+ testProperties.put("prop2", new StringBuilder("value2\\${prop1}"));
+
+ assertThat(propertyResolver.getProperty("prop2")).isEqualTo("value2${prop1}");
+ }
+
+ @Test // gh-34720
+ void multipleEscapedPlaceholdersArePreserved() {
+ testProperties.put("prop1", "value1");
+ testProperties.put("prop2", "value2");
+ testProperties.put("complex", "start\\${prop1}middle\\${prop2}end");
+
+ assertThat(propertyResolver.getProperty("complex")).isEqualTo("start${prop1}middle${prop2}end");
+ }
+
+ @Test // gh-34720
+ void doubleBackslashesAreProcessedCorrectly() {
+ testProperties.put("prop1", "value1");
+ testProperties.put("doubleEscaped", "value2\\\\${prop1}");
+
+ assertThat(propertyResolver.getProperty("doubleEscaped")).isEqualTo("value2\\${prop1}");
+ }
+
+ @Test // gh-34720
+ void escapedPlaceholdersInNestedPropertiesAreNotEvaluated() {
+ testProperties.put("p1", "v1");
+ testProperties.put("p2", "v2");
+ testProperties.put("escaped", "prefix-\\${p1}");
+ testProperties.put("nested", "${escaped}-${p2}");
+
+ assertThat(propertyResolver.getProperty("nested")).isEqualTo("prefix-${p1}-v2");
+ }
+
}
}
diff --git a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java
index 780fa2331699..5ce4c7764e78 100644
--- a/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java
+++ b/spring-core/src/test/java/org/springframework/core/io/support/PathMatchingResourcePatternResolverTests.java
@@ -335,6 +335,11 @@ private void writeAssetJar(Path path) throws Exception {
}
assertThat(new FileSystemResource(path).exists()).isTrue();
assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue();
+ assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isTrue();
+ assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse();
+ assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isFalse();
+ assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/file.txt").exists()).isFalse();
+ assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + "X" + path + ResourceUtils.JAR_URL_SEPARATOR + "assets/none.txt").exists()).isFalse();
}
private void writeApplicationJar(Path path) throws Exception {
diff --git a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java
index b4618c090d78..d2ef171a30f5 100644
--- a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java
+++ b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -53,18 +53,22 @@ void trivial() {
assertMatches(new String[] { null, "" }, "");
assertMatches(new String[] { null, "123" }, "123");
assertMatches(new String[] { null, "*" }, "123");
+
+ testMixedCaseMatch("abC", "Abc");
}
@Test
void startsWith() {
assertMatches("get*", "getMe");
assertDoesNotMatch("get*", "setMe");
+ testMixedCaseMatch("geT*", "GetMe");
}
@Test
void endsWith() {
assertMatches("*Test", "getMeTest");
assertDoesNotMatch("*Test", "setMe");
+ testMixedCaseMatch("*TeSt", "getMeTesT");
}
@Test
@@ -74,6 +78,10 @@ void between() {
assertMatches("*stuff*", "stuffTest");
assertMatches("*stuff*", "getstuff");
assertMatches("*stuff*", "stuff");
+ testMixedCaseMatch("*stuff*", "getStuffTest");
+ testMixedCaseMatch("*stuff*", "StuffTest");
+ testMixedCaseMatch("*stuff*", "getStuff");
+ testMixedCaseMatch("*stuff*", "Stuff");
}
@Test
@@ -82,6 +90,8 @@ void startsEnds() {
assertMatches("on*Event", "onEvent");
assertDoesNotMatch("3*3", "3");
assertMatches("3*3", "33");
+ testMixedCaseMatch("on*Event", "OnMyEvenT");
+ testMixedCaseMatch("on*Event", "OnEvenT");
}
@Test
@@ -122,18 +132,27 @@ void patternVariants() {
private void assertMatches(String pattern, String str) {
assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isTrue();
+ assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue();
}
private void assertDoesNotMatch(String pattern, String str) {
assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse();
+ assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isFalse();
+ }
+
+ private void testMixedCaseMatch(String pattern, String str) {
+ assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse();
+ assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue();
}
private void assertMatches(String[] patterns, String str) {
assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isTrue();
+ assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isTrue();
}
private void assertDoesNotMatch(String[] patterns, String str) {
assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isFalse();
+ assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isFalse();
}
}
diff --git a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java
index 53182acf882e..b16f741517aa 100644
--- a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java
+++ b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java
@@ -16,7 +16,7 @@
package org.springframework.util;
-import java.util.Properties;
+import java.util.Map;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
@@ -36,13 +36,13 @@
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
/**
* Tests for {@link PlaceholderParser}.
*
* @author Stephane Nicoll
+ * @author Sam Brannen
*/
class PlaceholderParserTests {
@@ -54,11 +54,11 @@ class OnlyPlaceholderTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("placeholders")
void placeholderIsReplaced(String text, String expected) {
- Properties properties = new Properties();
- properties.setProperty("firstName", "John");
- properties.setProperty("nested0", "first");
- properties.setProperty("nested1", "Name");
- assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
+ Map properties = Map.of(
+ "firstName", "John",
+ "nested0", "first",
+ "nested1", "Name");
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream placeholders() {
@@ -79,13 +79,13 @@ static Stream placeholders() {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
- Properties properties = new Properties();
- properties.setProperty("p1", "v1");
- properties.setProperty("p2", "v2");
- properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
- properties.setProperty("p4", "${p3}"); // deeply nested placeholders
- properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
- assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
+ Map properties = Map.of(
+ "p1", "v1",
+ "p2", "v2",
+ "p3", "${p1}:${p2}", // nested placeholders
+ "p4", "${p3}", // deeply nested placeholders
+ "p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream nestedPlaceholders() {
@@ -101,19 +101,15 @@ static Stream nestedPlaceholders() {
@Test
void parseWithSinglePlaceholder() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
- assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
- .isEqualTo("John");
- verify(resolver).resolvePlaceholder("firstName");
- verifyNoMoreInteractions(resolver);
+ assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("John");
+ verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
void parseWithPlaceholderAndPrefixText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
- assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver))
- .isEqualTo("This is John");
- verify(resolver).resolvePlaceholder("firstName");
- verifyNoMoreInteractions(resolver);
+ assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)).isEqualTo("This is John");
+ verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
@@ -121,31 +117,25 @@ void parseWithMultiplePlaceholdersAndText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith");
assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver))
.isEqualTo("User: John - Smith.");
- verify(resolver).resolvePlaceholder("firstName");
- verify(resolver).resolvePlaceholder("lastName");
- verifyNoMoreInteractions(resolver);
+ verifyPlaceholderResolutions(resolver, "firstName", "lastName");
}
@Test
void parseWithNestedPlaceholderInKey() {
- PlaceholderResolver resolver = mockPlaceholderResolver(
- "nested", "Name", "firstName", "John");
- assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver))
- .isEqualTo("John");
+ PlaceholderResolver resolver = mockPlaceholderResolver("nested", "Name", "firstName", "John");
+ assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "nested", "firstName");
}
@Test
void parseWithMultipleNestedPlaceholdersInKey() {
- PlaceholderResolver resolver = mockPlaceholderResolver(
- "nested0", "first", "nested1", "Name", "firstName", "John");
- assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver))
- .isEqualTo("John");
+ PlaceholderResolver resolver = mockPlaceholderResolver("nested0", "first", "nested1", "Name", "firstName", "John");
+ assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName");
}
@Test
- void placeholdersWithSeparatorAreHandledAsIs() {
+ void placeholderValueContainingSeparatorIsHandledAsIs() {
PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value");
assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value");
verifyPlaceholderResolutions(resolver, "my:test");
@@ -153,17 +143,20 @@ void placeholdersWithSeparatorAreHandledAsIs() {
@Test
void placeholdersWithoutEscapeCharAreNotEscaped() {
- PlaceholderResolver resolver = mockPlaceholderResolver("test", "value");
- assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value");
- verifyPlaceholderResolutions(resolver, "test");
+ PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", "p3", "v3", "p4", "v4");
+ assertThat(this.parser.replacePlaceholders("\\${p1}", resolver)).isEqualTo("\\v1");
+ assertThat(this.parser.replacePlaceholders("\\\\${p2}", resolver)).isEqualTo("\\\\v2");
+ assertThat(this.parser.replacePlaceholders("\\${p3}\\", resolver)).isEqualTo("\\v3\\");
+ assertThat(this.parser.replacePlaceholders("a\\${p4}\\z", resolver)).isEqualTo("a\\v4\\z");
+ verifyPlaceholderResolutions(resolver, "p1", "p2", "p3", "p4");
}
@Test
- void textWithInvalidPlaceholderIsMerged() {
+ void textWithInvalidPlaceholderSyntaxIsMerged() {
String text = "test${of${with${and${";
ParsedValue parsedValue = this.parser.parse(text);
- assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(
- TextPart.class, textPart -> assertThat(textPart.text()).isEqualTo(text));
+ assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class,
+ textPart -> assertThat(textPart.text()).isEqualTo(text));
}
}
@@ -176,11 +169,11 @@ class DefaultValueTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("placeholders")
void placeholderIsReplaced(String text, String expected) {
- Properties properties = new Properties();
- properties.setProperty("firstName", "John");
- properties.setProperty("nested0", "first");
- properties.setProperty("nested1", "Name");
- assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
+ Map properties = Map.of(
+ "firstName", "John",
+ "nested0", "first",
+ "nested1", "Name");
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream placeholders() {
@@ -199,14 +192,14 @@ static Stream placeholders() {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
- Properties properties = new Properties();
- properties.setProperty("p1", "v1");
- properties.setProperty("p2", "v2");
- properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
- properties.setProperty("p4", "${p3}"); // deeply nested placeholders
- properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
- properties.setProperty("p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
- assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
+ Map properties = Map.of(
+ "p1", "v1",
+ "p2", "v2",
+ "p3", "${p1}:${p2}", // nested placeholders
+ "p4", "${p3}", // deeply nested placeholders
+ "p5", "${p1}:${p2}:${bogus}", // unresolvable placeholder
+ "p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream nestedPlaceholders() {
@@ -225,11 +218,11 @@ static Stream nestedPlaceholders() {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("exactMatchPlaceholders")
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
- Properties properties = new Properties();
- properties.setProperty("prefix://my-service", "example-service");
- properties.setProperty("px", "prefix");
- properties.setProperty("p1", "${prefix://my-service}");
- assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
+ Map properties = Map.of(
+ "prefix://my-service", "example-service",
+ "px", "prefix",
+ "p1", "${prefix://my-service}");
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream exactMatchPlaceholders() {
@@ -242,74 +235,55 @@ static Stream exactMatchPlaceholders() {
@Test
void parseWithKeyEqualsToText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "Steve");
- assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
- .isEqualTo("Steve");
+ assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("Steve");
verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
void parseWithHardcodedFallback() {
PlaceholderResolver resolver = mockPlaceholderResolver();
- assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver))
- .isEqualTo("Steve");
+ assertThat(this.parser.replacePlaceholders("${firstName:Steve}", resolver)).isEqualTo("Steve");
verifyPlaceholderResolutions(resolver, "firstName:Steve", "firstName");
}
@Test
void parseWithNestedPlaceholderInKeyUsingFallback() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
- assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver))
- .isEqualTo("John");
+ assertThat(this.parser.replacePlaceholders("${first${invalid:Name}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "invalid:Name", "invalid", "firstName");
}
@Test
void parseWithFallbackUsingPlaceholder() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
- assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver))
- .isEqualTo("John");
+ assertThat(this.parser.replacePlaceholders("${invalid:${firstName}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "invalid", "firstName");
}
}
- @Nested // Tests with the use of the escape character
+ /**
+ * Tests that use the escape character.
+ */
+ @Nested
class EscapedTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true);
- @ParameterizedTest(name = "{0} -> {1}")
- @MethodSource("escapedInNestedPlaceholders")
- void escapedSeparatorInNestedPlaceholder(String text, String expected) {
- Properties properties = new Properties();
- properties.setProperty("app.environment", "qa");
- properties.setProperty("app.service", "protocol");
- properties.setProperty("protocol://host/qa/name", "protocol://example.com/qa/name");
- properties.setProperty("service/host/qa/name", "https://example.com/qa/name");
- properties.setProperty("service/host/qa/name:value", "https://example.com/qa/name-value");
- assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
- }
-
- static Stream escapedInNestedPlaceholders() {
- return Stream.of(
- Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
- Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
- Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
- Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
- }
-
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedPlaceholders")
void escapedPlaceholderIsNotReplaced(String text, String expected) {
- PlaceholderResolver resolver = mockPlaceholderResolver(
- "firstName", "John", "nested0", "first", "nested1", "Name",
+ Map properties = Map.of(
+ "firstName", "John",
"${test}", "John",
- "p1", "v1", "p2", "\\${p1:default}", "p3", "${p2}",
+ "p1", "v1",
+ "p2", "\\${p1:default}",
+ "p3", "${p2}",
"p4", "adc${p0:\\${p1}}",
"p5", "adc${\\${p0}:${p1}}",
"p6", "adc${p0:def\\${p1}}",
"p7", "adc\\${");
- assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected);
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream escapedPlaceholders() {
@@ -324,18 +298,21 @@ static Stream escapedPlaceholders() {
Arguments.of("${p4}", "adc${p1}"),
Arguments.of("${p5}", "adcv1"),
Arguments.of("${p6}", "adcdef${p1}"),
- Arguments.of("${p7}", "adc\\${"));
-
+ Arguments.of("${p7}", "adc\\${"),
+ // Double backslash
+ Arguments.of("DOMAIN\\\\${user.name}", "DOMAIN\\${user.name}"),
+ // Triple backslash
+ Arguments.of("triple\\\\\\${backslash}", "triple\\\\${backslash}"),
+ // Multiple escaped placeholders
+ Arguments.of("start\\${prop1}middle\\${prop2}end", "start${prop1}middle${prop2}end")
+ );
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedSeparators")
void escapedSeparatorIsNotReplaced(String text, String expected) {
- Properties properties = new Properties();
- properties.setProperty("first:Name", "John");
- properties.setProperty("nested0", "first");
- properties.setProperty("nested1", "Name");
- assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
+ Map properties = Map.of("first:Name", "John");
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream escapedSeparators() {
@@ -345,6 +322,26 @@ static Stream escapedSeparators() {
);
}
+ @ParameterizedTest(name = "{0} -> {1}")
+ @MethodSource("escapedSeparatorsInNestedPlaceholders")
+ void escapedSeparatorInNestedPlaceholderIsNotReplaced(String text, String expected) {
+ Map properties = Map.of(
+ "app.environment", "qa",
+ "app.service", "protocol",
+ "protocol://host/qa/name", "protocol://example.com/qa/name",
+ "service/host/qa/name", "https://example.com/qa/name",
+ "service/host/qa/name:value", "https://example.com/qa/name-value");
+ assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
+ }
+
+ static Stream escapedSeparatorsInNestedPlaceholders() {
+ return Stream.of(
+ Arguments.of("${protocol\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
+ Arguments.of("${${app.service}\\://host/${app.environment}/name}", "protocol://example.com/qa/name"),
+ Arguments.of("${service/host/${app.environment}/name:\\value}", "https://example.com/qa/name"),
+ Arguments.of("${service/host/${name\\:value}/}", "${service/host/${name:value}/}"));
+ }
+
}
@Nested
@@ -354,34 +351,38 @@ class ExceptionTests {
@Test
void textWithCircularReference() {
- PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}");
- assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver))
+ Map properties = Map.of(
+ "pL", "${pR}",
+ "pR", "${pL}");
+ assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", properties::get))
.isInstanceOf(PlaceholderResolutionException.class)
.hasMessage("Circular placeholder reference 'pL' in value \"${pL}\" <-- \"${pR}\" <-- \"${pL}\"");
}
@Test
void unresolvablePlaceholderIsReported() {
- PlaceholderResolver resolver = mockPlaceholderResolver();
assertThatExceptionOfType(PlaceholderResolutionException.class)
- .isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver))
- .withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"")
+ .isThrownBy(() -> this.parser.replacePlaceholders("X${bogus}Z", placeholderName -> null))
+ .withMessage("Could not resolve placeholder 'bogus' in value \"X${bogus}Z\"")
.withNoCause();
}
@Test
void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() {
- PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2",
+ Map properties = Map.of(
+ "p1", "v1",
+ "p2", "v2",
"p3", "${p1}:${p2}:${bogus}");
assertThatExceptionOfType(PlaceholderResolutionException.class)
- .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver))
+ .isThrownBy(() -> this.parser.replacePlaceholders("${p3}", properties::get))
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
.withNoCause();
}
}
- PlaceholderResolver mockPlaceholderResolver(String... pairs) {
+
+ private static PlaceholderResolver mockPlaceholderResolver(String... pairs) {
if (pairs.length % 2 == 1) {
throw new IllegalArgumentException("size must be even, it is a set of key=value pairs");
}
@@ -394,7 +395,7 @@ PlaceholderResolver mockPlaceholderResolver(String... pairs) {
return resolver;
}
- void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
+ private static void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
InOrder ordered = inOrder(mock);
for (String placeholder : placeholders) {
ordered.verify(mock).resolvePlaceholder(placeholder);
diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java
index 99daa91494a2..84c2f7981f98 100644
--- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java
+++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,11 @@
package org.springframework.jdbc.core.simple;
+import java.util.List;
+
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassRelativeResourceLoader;
@@ -144,6 +147,86 @@ void updateWithGeneratedKeysAndKeyColumnNamesUsingNamedParameters() {
}
+ @Nested // gh-34768
+ class ReusedNamedParameterTests {
+
+ private static final String QUERY1 = """
+ select * from users
+ where
+ first_name in ('Bogus', :name) or
+ last_name in (:name, 'Bogus')
+ order by last_name
+ """;
+
+ private static final String QUERY2 = """
+ select * from users
+ where
+ first_name in (:names) or
+ last_name in (:names)
+ order by last_name
+ """;
+
+
+ @BeforeEach
+ void insertTestUsers() {
+ jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "John").update();
+ jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("John", "Smith").update();
+ jdbcClient.sql(INSERT_WITH_JDBC_PARAMS).params("Smith", "Smith").update();
+ assertNumUsers(4);
+ }
+
+ @Test
+ void selectWithReusedNamedParameter() {
+ List users = jdbcClient.sql(QUERY1)
+ .param("name", "John")
+ .query(User.class)
+ .list();
+
+ assertResults(users);
+ }
+
+ @Test
+ void selectWithReusedNamedParameterFromBeanProperties() {
+ List users = jdbcClient.sql(QUERY1)
+ .paramSource(new Name("John"))
+ .query(User.class)
+ .list();
+
+ assertResults(users);
+ }
+
+ @Test
+ void selectWithReusedNamedParameterList() {
+ List users = jdbcClient.sql(QUERY2)
+ .param("names", List.of("John", "Bogus"))
+ .query(User.class)
+ .list();
+
+ assertResults(users);
+ }
+
+ @Test
+ void selectWithReusedNamedParameterListFromBeanProperties() {
+ List users = jdbcClient.sql(QUERY2)
+ .paramSource(new Names(List.of("John", "Bogus")))
+ .query(User.class)
+ .list();
+
+ assertResults(users);
+ }
+
+
+ private static void assertResults(List users) {
+ assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith"));
+ }
+
+ record Name(String name) {}
+
+ record Names(List names) {}
+
+ }
+
+
private void assertNumUsers(long count) {
long numUsers = this.jdbcClient.sql("select count(id) from users").query(Long.class).single();
assertThat(numUsers).isEqualTo(count);
diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java
index f6696e8d9cd9..76b7adfcb879 100644
--- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java
+++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/NamedParameterUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 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.
@@ -49,6 +49,7 @@
* @author Juergen Hoeller
* @author Mark Paluch
* @author Anton Naydenov
+ * @author Sam Brannen
* @since 5.3
*/
abstract class NamedParameterUtils {
@@ -513,37 +514,75 @@ private static class ExpandedQuery implements PreparedOperation {
private final BindParameterSource parameterSource;
+
ExpandedQuery(String expandedSql, NamedParameters parameters, BindParameterSource parameterSource) {
this.expandedSql = expandedSql;
this.parameters = parameters;
this.parameterSource = parameterSource;
}
- @SuppressWarnings({"rawtypes", "unchecked"})
- public void bind(BindTarget target, String identifier, Parameter parameter) {
- List bindMarkers = getBindMarkers(identifier);
+
+ @Override
+ public String toQuery() {
+ return this.expandedSql;
+ }
+
+ @Override
+ public String getSource() {
+ return this.expandedSql;
+ }
+
+ @Override
+ public void bindTo(BindTarget target) {
+ for (String namedParameter : this.parameterSource.getParameterNames()) {
+ Parameter parameter = this.parameterSource.getValue(namedParameter);
+ if (parameter.getValue() == null) {
+ bindNull(target, namedParameter, parameter);
+ }
+ else {
+ bind(target, namedParameter, parameter);
+ }
+ }
+ }
+
+ private void bindNull(BindTarget target, String identifier, Parameter parameter) {
+ List> bindMarkers = getBindMarkers(identifier);
+ if (bindMarkers == null) {
+ target.bind(identifier, parameter);
+ return;
+ }
+ for (List outer : bindMarkers) {
+ for (BindMarker bindMarker : outer) {
+ bindMarker.bind(target, parameter);
+ }
+ }
+ }
+
+ private void bind(BindTarget target, String identifier, Parameter parameter) {
+ List> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
- if (parameter.getValue() instanceof Collection collection) {
- Iterator