Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Clozel 90f9c0929b Release v6.2.6 2025-04-17 09:18:15 +02:00
259 changed files with 2986 additions and 4233 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
org.gradle.caching=true
javaFormatVersion=0.0.43
javaFormatVersion=0.0.42
@@ -50,7 +50,7 @@ public class CheckstyleConventions {
project.getPlugins().apply(CheckstylePlugin.class);
project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g"));
CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class);
checkstyle.setToolVersion("10.23.1");
checkstyle.setToolVersion("10.23.0");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,8 +21,6 @@ import java.util.Map;
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;
@@ -36,7 +34,6 @@ import org.gradle.testretry.TestRetryTaskExtension;
*
* @author Brian Clozel
* @author Andy Wilkinson
* @author Sam Brannen
*/
class TestConventions {
@@ -53,12 +50,7 @@ class TestConventions {
}
private void configureTests(Project project, Test test) {
TestFrameworkOptions existingOptions = test.getOptions();
test.useJUnitPlatform(options -> {
if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) {
options.copyFrom(junitPlatformOptions);
}
});
test.useJUnitPlatform();
test.include("**/*Tests.class", "**/*Test.class");
test.setSystemProperties(Map.of(
"java.awt.headless", "true",
+1 -2
View File
@@ -1,6 +1,6 @@
plugins {
id 'java-platform'
id 'io.freefair.aggregate-javadoc' version '8.13.1'
id 'io.freefair.aggregate-javadoc' version '8.3'
}
description = "Spring Framework API Docs"
@@ -21,7 +21,6 @@ dependencies {
javadoc {
title = "${rootProject.description} ${version} API"
failOnError = true
options {
encoding = "UTF-8"
memberLevel = JavadocMemberLevel.PROTECTED
@@ -103,14 +103,6 @@ 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.
@@ -101,11 +101,8 @@ 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 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).
`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or
`setEscapeCharacter` to customize placeholders.
NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that
will get properties from `application.properties` and `application.yml` files.
@@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the
[[beans-factory-placeholderconfigurer]]
=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer`
=== Example: The Class Name Substitution `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, 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 and log4j and JSP EL style.
The actual values come from another file in the standard Java `Properties` format:
@@ -355,15 +355,11 @@ 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,
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).
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, you can configure property placeholders
With the `context` namespace introduced in Spring 2.5, 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:
@@ -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 `PropertySourcesPlaceholderConfigurer` bean:
following configuration beans:
[source,kotlin,indent=0]
----
@@ -200,10 +200,8 @@ following `PropertySourcesPlaceholderConfigurer` bean:
}
----
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:
You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`)
that uses the `${...}` syntax, with configuration beans, as the following example shows:
[source,kotlin,indent=0]
----
@@ -217,9 +215,6 @@ shows:
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]]
@@ -2,9 +2,8 @@
= 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`]
@@ -1,9 +1,166 @@
[[testcontext-support-classes]]
= TestContext Framework Support Classes
This section describes the various classes that support the Spring TestContext Framework
in JUnit and TestNG.
This section describes the various classes that support the Spring TestContext Framework.
[[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
@@ -20,17 +177,14 @@ 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`:
@@ -153,8 +307,7 @@ 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`
@@ -165,9 +318,10 @@ 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]]
@@ -187,9 +341,8 @@ 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.
@@ -254,9 +407,8 @@ 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]
======
@@ -401,19 +553,17 @@ 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]
======
@@ -476,174 +626,8 @@ 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
== TestNG Support Classes
The `org.springframework.test.context.testng` package provides the following support
classes for TestNG based test cases:
@@ -666,8 +650,7 @@ 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`.
@@ -234,8 +234,8 @@ Kotlin::
--
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on
through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this to, for example, parameterize a base URL based on
some external configuration.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
+7 -7
View File
@@ -7,17 +7,17 @@ javaPlatform {
}
dependencies {
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("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("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.6"))
api(platform("io.projectreactor:reactor-bom:2024.0.4"))
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.21"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21"))
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.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.4")
api("org.apache.httpcomponents.client5:httpclient5:5.4.3")
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")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.7
version=6.2.6
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+1 -1
View File
@@ -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
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
Vendored
+2 -2
View File
@@ -114,7 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# 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" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
Vendored
+2 -2
View File
@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%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" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,10 +20,8 @@ import org.springframework.beans.factory.BeanDefinitionStoreException;
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
@@ -39,16 +37,16 @@ import org.springframework.util.SystemPropertyUtils;
*
* <pre class="code">
* &lt;bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"&gt;
* &lt;property name="driverClassName" value="${jdbc.driver}" /&gt;
* &lt;property name="url" value="jdbc:${jdbc.dbname}" /&gt;
* &lt;property name="driverClassName" value="${driver}" /&gt;
* &lt;property name="url" value="jdbc:${dbname}" /&gt;
* &lt;/bean&gt;
* </pre>
*
* Example properties file:
*
* <pre class="code">
* jdbc.driver=com.mysql.jdbc.Driver
* jdbc.dbname=mysql:mydb</pre>
* driver=com.mysql.jdbc.Driver
* dbname=mysql:mydb</pre>
*
* Annotated bean definitions may take advantage of property replacement using
* the {@link org.springframework.beans.factory.annotation.Value @Value} annotation:
@@ -81,12 +79,11 @@ import org.springframework.util.SystemPropertyUtils;
* <p>Example XML property with default value:
*
* <pre class="code">
* &lt;property name="url" value="jdbc:${jdbc.dbname:defaultdb}" /&gt;
* &lt;property name="url" value="jdbc:${dbname:defaultdb}" /&gt;
* </pre>
*
* @author Chris Beams
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.1
* @see PropertyPlaceholderConfigurer
* @see org.springframework.context.support.PropertySourcesPlaceholderConfigurer
@@ -95,21 +92,16 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
implements BeanNameAware, BeanFactoryAware {
/** Default placeholder prefix: {@value}. */
public static final String DEFAULT_PLACEHOLDER_PREFIX = SystemPropertyUtils.PLACEHOLDER_PREFIX;
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
/** Default placeholder suffix: {@value}. */
public static final String DEFAULT_PLACEHOLDER_SUFFIX = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
/** Default value separator: {@value}. */
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;
public static final String DEFAULT_VALUE_SEPARATOR = ":";
/** Default escape character: {@code '\'}. */
public static final Character DEFAULT_ESCAPE_CHARACTER = '\\';
/** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */
protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;
@@ -121,11 +113,9 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
@Nullable
protected String valueSeparator = DEFAULT_VALUE_SEPARATOR;
/**
* The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
*/
/** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */
@Nullable
protected Character escapeCharacter = AbstractPropertyResolver.getDefaultEscapeCharacter();
protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER;
protected boolean trimValues = false;
@@ -143,7 +133,7 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
/**
* Set the prefix that a placeholder string starts with.
* <p>The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}.
* The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}.
*/
public void setPlaceholderPrefix(String placeholderPrefix) {
this.placeholderPrefix = placeholderPrefix;
@@ -151,32 +141,31 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
/**
* Set the suffix that a placeholder string ends with.
* <p>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.
* <p>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;
}
/**
* 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.
* <p>The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
* Specify the escape character to use to ignore placeholder prefix
* or value separator, or {@code null} if no escaping should take
* place.
* <p>Default is {@link #DEFAULT_ESCAPE_CHARACTER}.
* @since 6.2
*/
public void setEscapeCharacter(@Nullable Character escapeCharacter) {
this.escapeCharacter = escapeCharacter;
public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) {
this.escapeCharacter = escsEscapeCharacter;
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,14 +35,12 @@ import org.springframework.beans.factory.BeanInitializationException;
*
* Example properties file:
*
* <pre class="code">
* dataSource.driverClassName=com.mysql.jdbc.Driver
* <pre class="code">dataSource.driverClassName=com.mysql.jdbc.Driver
* dataSource.url=jdbc:mysql:mydb</pre>
*
* <p>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.
* 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.
*
* <p>Note that the context definition <i>is not</i> aware of being overridden;
* so this is not immediately obvious when looking at the XML definition file.
@@ -997,17 +997,9 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
*/
@Nullable
private FactoryBean<?> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
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;
}
boolean locked = this.singletonLock.tryLock();
if (!locked) {
return null;
}
try {
@@ -1066,9 +1066,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
@Nullable
protected Boolean isCurrentThreadAllowedToHoldSingletonLock() {
String mainThreadPrefix = this.mainThreadPrefix;
if (mainThreadPrefix != null) {
// We only differentiate in the preInstantiateSingletons phase, using
// the volatile mainThreadPrefix field as an indicator for that phase.
if (this.mainThreadPrefix != null) {
// We only differentiate in the preInstantiateSingletons phase.
PreInstantiation preInstantiation = this.preInstantiationThread.get();
if (preInstantiation != null) {
@@ -1088,7 +1087,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
else if (this.strictLocking == null) {
// No explicit locking configuration -> infer appropriate locking.
if (!getThreadNamePrefix().equals(mainThreadPrefix)) {
if (mainThreadPrefix != null && !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).
@@ -271,15 +271,13 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
// 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<String> 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 {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -118,15 +118,7 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
*/
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
if (factory.isSingleton() && containsSingleton(beanName)) {
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
boolean locked;
if (lockFlag == null) {
this.singletonLock.lock();
locked = true;
}
else {
locked = (lockFlag && this.singletonLock.tryLock());
}
this.singletonLock.lock();
try {
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
@@ -139,13 +131,11 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
}
else {
if (shouldPostProcess) {
if (locked) {
if (isSingletonCurrentlyInCreation(beanName)) {
// Temporarily return non-post-processed object, not storing it yet
return object;
}
beforeSingletonCreation(beanName);
if (isSingletonCurrentlyInCreation(beanName)) {
// Temporarily return non-post-processed object, not storing it yet
return object;
}
beforeSingletonCreation(beanName);
try {
object = postProcessObjectFromFactoryBean(object, beanName);
}
@@ -154,9 +144,7 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
"Post-processing of FactoryBean's singleton object failed", ex);
}
finally {
if (locked) {
afterSingletonCreation(beanName);
}
afterSingletonCreation(beanName);
}
}
if (containsSingleton(beanName)) {
@@ -167,9 +155,7 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
return object;
}
finally {
if (locked) {
this.singletonLock.unlock();
}
this.singletonLock.unlock();
}
}
else {
-7
View File
@@ -59,10 +59,3 @@ 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"
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 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,7 +50,6 @@ public class ApplicationContextAotGenerator {
*/
public ClassName processAheadOfTime(GenericApplicationContext applicationContext,
GenerationContext generationContext) {
return withCglibClassHandler(new CglibClassHandler(generationContext), () -> {
applicationContext.refreshForAotProcessing(generationContext.getRuntimeHints());
ApplicationContextInitializationCodeGenerator codeGenerator =
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2022 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,9 +80,8 @@ public abstract class ContextAotProcessor extends AbstractAotProcessor<ClassName
@Override
protected ClassName doProcess() {
deleteExistingOutput();
try (GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass())) {
return performAotProcessing(applicationContext);
}
GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass());
return performAotProcessing(applicationContext);
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -100,7 +100,6 @@ public class ReflectiveProcessorAotContributionBuilder {
return (!this.classes.isEmpty() ? new AotContribution(this.classes) : null);
}
private static class AotContribution implements BeanFactoryInitializationAotContribution {
private final Class<?>[] classes;
@@ -114,8 +113,8 @@ public class ReflectiveProcessorAotContributionBuilder {
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
registrar.registerRuntimeHints(runtimeHints, this.classes);
}
}
}
private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,6 +29,7 @@ import org.springframework.core.env.ConfigurablePropertyResolver;
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;
@@ -48,7 +49,7 @@ import org.springframework.util.StringValueResolver;
* XSD documentation for complete details.
*
* <p>Any local properties (for example, those added via {@link #setProperties}, {@link #setLocations}
* et al.) are added as a single {@link PropertySource}. Search precedence of local properties is
* et al.) are added as a {@code 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.
@@ -100,9 +101,8 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
}
/**
* {@inheritDoc}
* <p>{@code PropertySources} from the given {@link Environment} will be searched
* when replacing ${...} placeholders.
* {@code PropertySources} from the given {@link Environment}
* will be searched when replacing ${...} placeholders.
* @see #setPropertySources
* @see #postProcessBeanFactory
*/
@@ -132,11 +132,28 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
if (this.propertySources == null) {
this.propertySources = new MutablePropertySources();
if (this.environment != null) {
PropertySource<?> environmentPropertySource =
(this.environment instanceof ConfigurableEnvironment configurableEnvironment ?
new ConfigurableEnvironmentPropertySource(configurableEnvironment) :
new FallbackEnvironmentPropertySource(this.environment));
this.propertySources.addLast(environmentPropertySource);
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);
}
}
);
}
try {
PropertySource<?> localPropertySource =
@@ -159,7 +176,6 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
/**
* Create a {@link ConfigurablePropertyResolver} for the specified property sources.
* <p>The default implementation creates a {@link PropertySourcesPropertyResolver}.
* @param propertySources the property sources to use
* @since 6.0.12
*/
@@ -172,7 +188,7 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
* placeholders with values from the given properties.
*/
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
ConfigurablePropertyResolver propertyResolver) throws BeansException {
final ConfigurablePropertyResolver propertyResolver) throws BeansException {
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
@@ -218,75 +234,4 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
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<ConfigurableEnvironment> {
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}.
* <p>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<Environment> {
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 + "}";
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2021 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,12 +146,6 @@ import org.springframework.core.Ordered;
* 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.
*
* <p><b>Note: {@code @EnableAsync} applies to its local application context only,
* allowing for selective activation at different levels.</b> 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
@@ -27,6 +27,7 @@ import java.util.Collections;
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;
@@ -549,13 +550,14 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* <p>Mark fields as disallowed, for example to avoid unwanted
* modifications by malicious users when binding HTTP request parameters.
* <p>Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts),
* as well as direct equality.
* <p>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.
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality.
* <p>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.
* <p>More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
* <p>Alternatively, specify a list of <i>allowed</i> field patterns.
@@ -573,7 +575,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
else {
String[] fieldPatterns = new String[disallowedFields.length];
for (int i = 0; i < fieldPatterns.length; i++) {
fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
String field = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
fieldPatterns[i] = field.toLowerCase(Locale.ROOT);
}
this.disallowedFields = fieldPatterns;
}
@@ -1299,9 +1302,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
* Determine if the given field is allowed for binding.
* <p>Invoked for each passed-in property value.
* <p>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.
* <p>Matching against allowed field patterns is case-sensitive; whereas,
* matching against disallowed field patterns is case-insensitive.
* <p>A field matching a disallowed pattern will not be accepted even if it
@@ -1317,13 +1320,8 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
protected boolean isAllowed(String field) {
String[] allowed = getAllowedFields();
String[] disallowed = getDisallowedFields();
if (!ObjectUtils.isEmpty(allowed) && !PatternMatchUtils.simpleMatch(allowed, field)) {
return false;
}
if (!ObjectUtils.isEmpty(disallowed)) {
return !PatternMatchUtils.simpleMatchIgnoreCase(disallowed, field);
}
return true;
return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) &&
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase(Locale.ROOT))));
}
/**
@@ -0,0 +1,11 @@
<?xml version='1.0' encoding='UTF-8'?>
<!DOCTYPE rmi PUBLIC "-//BEA Systems, Inc.//RMI Runtime DTD 1.0//EN" "rmi.dtd">
<!--
- Special WebLogic deployment descriptor for Spring's RMI invoker.
- Only applied by WebLogic Server, ignored on other platforms.
-->
<rmi name="org.springframework.remoting.rmi.RmiInvocationWrapper">
<cluster clusterable="true"/>
<method name="getTargetInterfaceName" idempotent="true"/>
</rmi>
@@ -24,7 +24,6 @@ import org.junit.jupiter.api.Timeout;
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;
@@ -244,24 +243,14 @@ class BackgroundBootstrapTests {
}
@Bean
public FactoryBean<TestBean> testBean4() {
public TestBean testBean4() {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
TestBean testBean = new TestBean();
return new FactoryBean<>() {
@Override
public TestBean getObject() {
return testBean;
}
@Override
public Class<?> getObjectType() {
return testBean.getClass();
}
};
return new TestBean();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,8 +38,7 @@ import org.springframework.context.support.GenericApplicationContext;
* @author Juergen Hoeller
* @since 3.0
*/
// WARNING: This class MUST be public, since it is based on JUnit 3.
public class SpringAtInjectTckTests {
class SpringAtInjectTckTests {
@SuppressWarnings("unchecked")
public static Test suite() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2022 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,9 +45,8 @@ class ContextAotProcessorTests {
void processGeneratesAssets(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory);
ContextAotProcessor 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());
@@ -62,10 +61,9 @@ class ContextAotProcessorTests {
Path existingSourceOutput = createExisting(sourceOutput);
Path existingResourceOutput = createExisting(resourceOutput);
Path existingClassOutput = createExisting(classOutput);
DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class,
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class,
sourceOutput, resourceOutput, classOutput);
processor.process();
assertThat(processor.context.isClosed()).isTrue();
assertThat(existingSourceOutput).doesNotExist();
assertThat(existingResourceOutput).doesNotExist();
assertThat(existingClassOutput).doesNotExist();
@@ -75,14 +73,13 @@ class ContextAotProcessorTests {
void processWithEmptyNativeImageArgumentsDoesNotCreateNativeImageProperties(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) {
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) {
@Override
protected List<String> 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();
@@ -121,8 +118,6 @@ class ContextAotProcessorTests {
private static class DemoContextAotProcessor extends ContextAotProcessor {
AnnotationConfigApplicationContext context;
DemoContextAotProcessor(Class<?> application, Path rootPath) {
this(application, rootPath.resolve("source"), rootPath.resolve("resource"), rootPath.resolve("class"));
}
@@ -146,11 +141,10 @@ class ContextAotProcessorTests {
protected GenericApplicationContext prepareApplicationContext(Class<?> application) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(application);
this.context = context;
return context;
}
}
}
@Configuration(proxyBeanMethods = false)
static class SampleApplication {
@@ -159,6 +153,7 @@ class ContextAotProcessorTests {
public String testBean() {
return "Hello";
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,34 +16,20 @@
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;
@@ -52,15 +38,12 @@ import org.springframework.core.io.Resource;
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}.
@@ -90,43 +73,6 @@ class PropertySourcesPlaceholderConfigurerTests {
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();
@@ -142,29 +88,14 @@ class PropertySourcesPlaceholderConfigurerTests {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo");
}
@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 localPropertiesOverrideFalse() {
localPropertiesOverride(false);
}
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
void localPropertiesOverrideTrue() {
localPropertiesOverride(true);
}
@Test
@@ -350,58 +281,28 @@ class PropertySourcesPlaceholderConfigurerTests {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("bar");
}
@Test // gh-34861
void withEnumerableAndNonEnumerablePropertySourcesInTheEnvironmentAndLocalProperties() {
@SuppressWarnings("serial")
private void localPropertiesOverride(boolean override) {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("testBean",
genericBeanDefinition(TestBean.class)
.addPropertyValue("name", "${foo:bogus}")
.addPropertyValue("jedi", "${local:false}")
.addPropertyValue("name", "${foo}")
.getBeanDefinition());
// 1) MockPropertySource is an EnumerablePropertySource.
MockPropertySource mockPropertySource = new MockPropertySource("mockPropertySource")
.withProperty("foo", "${bar}");
// 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.setLocalOverride(override);
ppc.setProperties(new Properties() {{
setProperty("local", "true");
setProperty("foo", "local");
}});
ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing"));
ppc.postProcessBeanFactory(bf);
// 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<String> propertyNames = new ArrayList<>();
for (PropertySource<?> propertySource : ppc.getAppliedPropertySources()) {
if (propertySource instanceof EnumerablePropertySource<?> enumerablePropertySource) {
Collections.addAll(propertyNames, enumerablePropertySource.getPropertyNames());
}
if (override) {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local");
}
else {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing");
}
// Should not contain "foo" or "bar" from the Environment.
assertThat(propertyNames).containsOnly("local");
}
@Test
@@ -531,252 +432,6 @@ class PropertySourcesPlaceholderConfigurerTests {
}
/**
* 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<String> name;
@@ -817,23 +472,4 @@ class PropertySourcesPlaceholderConfigurerTests {
}
}
@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;
}
}
}
@@ -463,21 +463,10 @@ public class ReflectUtils {
c = lookup.defineClass(b);
}
catch (LinkageError | IllegalArgumentException 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;
}
// 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);
@@ -26,20 +26,18 @@ import org.springframework.lang.Nullable;
/**
* Static holder for local Spring properties, i.e. defined at the Spring library level.
*
* <p>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}.
* <p>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.
*
* <p>This is an alternative way to set Spring-related system properties such as
* {@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"}.
* "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".
*
* @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
@@ -361,12 +361,16 @@ abstract class AnnotationsScanner {
}
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(
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,7 +42,7 @@ import org.springframework.util.StringUtils;
* 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 work against the {@link MutablePropertySources} API.
* and working 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 specify active profiles: {@value}.
* Name of the property to set to specify active profiles: {@value}.
* <p>The value may be comma delimited.
* <p>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 specify profiles that are active by default: {@value}.
* Name of the property to set to specify profiles that are active by default: {@value}.
* <p>The value may be comma delimited.
* <p>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 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
/**
* Factory method used to create the {@link ConfigurablePropertyResolver}
* used by this {@code Environment}.
* instance used by the Environment.
* @since 5.3.4
* @see #getPropertyResolver()
*/
@@ -150,7 +150,8 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
}
/**
* Return the {@link ConfigurablePropertyResolver} used by the {@code Environment}.
* Return the {@link ConfigurablePropertyResolver} being used by the
* {@link Environment}.
* @since 5.3.4
* @see #createPropertyResolver(MutablePropertySources)
*/
@@ -319,6 +320,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
}
}
@Override
public String[] getDefaultProfiles() {
return StringUtils.toStringArray(doGetDefaultProfiles());
@@ -326,7 +328,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
/**
* 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.
@@ -418,7 +420,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
* active or default profiles.
* <p>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
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import java.util.Set;
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;
@@ -38,52 +37,10 @@ import org.springframework.util.SystemPropertyUtils;
*
* @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 <em>default</em> escape character
* for property placeholder support: {@value}.
* <p>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 '@'}.
* <pre style="code">-Dspring.placeholder.escapeCharacter.default=@</pre>
* <p>To disable escape character support, set the value to an empty string
* &mdash; for example, by supplying the following JVM system property via
* the command line.
* <pre style="code">-Dspring.placeholder.escapeCharacter.default=</pre>
* <p>If the property is not set, {@code '\'} will be used as the default
* escape character.
* <p>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
@@ -105,7 +62,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
@Nullable
private Character escapeCharacter = getDefaultEscapeCharacter();
private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER;
private final Set<String> requiredProperties = new LinkedHashSet<>();
@@ -134,9 +91,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* {@inheritDoc}
* <p>The default is <code>"${"</code>.
* @see SystemPropertyUtils#PLACEHOLDER_PREFIX
* Set the prefix that placeholders replaced by this resolver must begin with.
* <p>The default is "${".
* @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_PREFIX
*/
@Override
public void setPlaceholderPrefix(String placeholderPrefix) {
@@ -145,9 +102,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* {@inheritDoc}
* <p>The default is <code>"}"</code>.
* @see SystemPropertyUtils#PLACEHOLDER_SUFFIX
* Set the suffix that placeholders replaced by this resolver must end with.
* <p>The default is "}".
* @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_SUFFIX
*/
@Override
public void setPlaceholderSuffix(String placeholderSuffix) {
@@ -156,9 +113,11 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* {@inheritDoc}
* <p>The default is {@code ":"}.
* @see SystemPropertyUtils#VALUE_SEPARATOR
* 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.
* <p>The default is ":".
* @see org.springframework.util.SystemPropertyUtils#VALUE_SEPARATOR
*/
@Override
public void setValueSeparator(@Nullable String valueSeparator) {
@@ -166,9 +125,12 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* {@inheritDoc}
* <p>The default is determined by {@link #getDefaultEscapeCharacter()}.
* Specify the escape character to use to ignore placeholder prefix
* or value separator, or {@code null} if no escaping should take
* place.
* <p>The default is "\".
* @since 6.2
* @see org.springframework.util.SystemPropertyUtils#ESCAPE_CHARACTER
*/
@Override
public void setEscapeCharacter(@Nullable Character escapeCharacter) {
@@ -329,60 +291,4 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
@Nullable
protected abstract String getPropertyAsRawString(String key);
/**
* Get the default {@linkplain #setEscapeCharacter(Character) escape character}
* to use when parsing strings for property placeholder resolution.
* <p>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.
* <p>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;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,10 +34,7 @@ import org.springframework.util.StringUtils;
*
* <p>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 - and failing with an
* {@code IllegalStateException} against any non-{@code EnumerablePropertySource}.
* <b>When used through the {@code EnumerablePropertySource} contract, all contained
* sources are expected to be of type {@code EnumerablePropertySource} as well.</b>
* accumulated property names from all contained sources (as far as possible).
*
* @author Chris Beams
* @author Juergen Hoeller
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2022 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<String, Object> 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.
* <p>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
* &mdash; 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.
* <p>Active and default profile names are also filtered for duplicates, to avoid
* confusion and redundant storage.
* <p>The parent environment remains unmodified in any case. Note that any changes to
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -69,23 +69,21 @@ public interface ConfigurablePropertyResolver extends PropertyResolver {
void setPlaceholderSuffix(String placeholderSuffix);
/**
* Set the separating character to be honored between placeholders replaced by
* this resolver and their associated default values, or {@code null} if no such
* 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.
*/
void setValueSeparator(@Nullable String valueSeparator);
/**
* 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.
* Specify the escape character to use to ignore placeholder prefix or
* value separator, or {@code null} if no escaping should take place.
* @since 6.2
*/
void setEscapeCharacter(@Nullable Character escapeCharacter);
/**
* Specify whether to throw an exception when encountering an unresolvable placeholder
* Set 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
@@ -108,7 +106,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;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,13 +30,13 @@ import org.springframework.lang.Nullable;
public interface PropertyResolver {
/**
* Determine whether the given property key is available for resolution
* &mdash; for example, if the value for the given key is not {@code null}.
* Return whether the given property key is available for resolution,
* i.e. if the value for the given key is not {@code null}.
*/
boolean containsProperty(String key);
/**
* Resolve the property value associated with the given key,
* Return 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);
/**
* Resolve the property value associated with the given key, or
* Return 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);
/**
* Resolve the property value associated with the given key,
* Return 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> T getProperty(String key, Class<T> targetType);
/**
* Resolve the property value associated with the given key,
* Return 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> T getProperty(String key, Class<T> targetType, T defaultValue);
/**
* Resolve the property value associated with the given key (never {@code null}).
* Return 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;
/**
* Resolve the property value associated with the given key, converted to the given
* Return 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
*/
@@ -29,7 +29,6 @@ import java.nio.channels.ReadableByteChannel;
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;
@@ -45,7 +44,6 @@ import org.springframework.util.ResourceUtils;
*/
public abstract class AbstractFileResolvingResource extends AbstractResource {
@SuppressWarnings("try")
@Override
public boolean exists() {
try {
@@ -88,11 +86,7 @@ public abstract class AbstractFileResolvingResource extends AbstractResource {
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).
// 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);
}
return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null);
}
else if (con.getContentLengthLong() > 0) {
return true;
@@ -36,7 +36,6 @@ import java.nio.file.FileSystemNotFoundException;
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;
@@ -875,9 +874,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
closeJarFile = !jarCon.getUseCaches();
}
catch (ZipException | FileNotFoundException | NoSuchFileException ex) {
catch (ZipException | FileNotFoundException ex) {
// Happens in case of a non-jar file or in case of a cached root directory
// without the specific subdirectory present, respectively.
// without specific subdirectory present, respectively.
return Collections.emptySet();
}
}
@@ -1276,7 +1275,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
/**
* Return an alternative form of the resource, i.e. with or without a leading slash.
* Return a 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}
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -141,8 +141,8 @@ public abstract class PropertiesLoaderSupport {
/**
* Return a merged {@link Properties} instance containing both the
* loaded properties and properties set on this component.
* Return a merged Properties instance containing both the
* loaded properties and properties set on this FactoryBean.
*/
protected Properties mergeProperties() throws IOException {
Properties result = new Properties();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 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,25 +37,13 @@ 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 (ignoreCase ? pattern.equalsIgnoreCase(str) : pattern.equals(str));
return pattern.equals(str);
}
if (firstIndex == 0) {
@@ -64,43 +52,25 @@ public abstract class PatternMatchUtils {
}
int nextIndex = pattern.indexOf('*', 1);
if (nextIndex == -1) {
String part = pattern.substring(1);
return (ignoreCase ? StringUtils.endsWithIgnoreCase(str, part) : str.endsWith(part));
return str.endsWith(pattern.substring(1));
}
String part = pattern.substring(1, nextIndex);
if (part.isEmpty()) {
return simpleMatch(pattern.substring(nextIndex), str, ignoreCase);
return simpleMatch(pattern.substring(nextIndex), str);
}
int partIndex = indexOf(str, part, 0, ignoreCase);
int partIndex = str.indexOf(part);
while (partIndex != -1) {
if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()), ignoreCase)) {
if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) {
return true;
}
partIndex = indexOf(str, part, partIndex + 1, ignoreCase);
partIndex = str.indexOf(part, partIndex + 1);
}
return false;
}
return (str.length() >= 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;
pattern.startsWith(str.substring(0, firstIndex)) &&
simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex)));
}
/**
@@ -124,19 +94,4 @@ public abstract class PatternMatchUtils {
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;
}
}
@@ -239,7 +239,7 @@ public abstract class StreamUtils {
}
private static final class NonClosingInputStream extends FilterInputStream {
private static class NonClosingInputStream extends FilterInputStream {
public NonClosingInputStream(InputStream in) {
super(in);
@@ -248,30 +248,10 @@ public abstract class StreamUtils {
@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 final class NonClosingOutputStream extends FilterOutputStream {
private static class NonClosingOutputStream extends FilterOutputStream {
public NonClosingOutputStream(OutputStream out) {
super(out);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,19 +35,16 @@ import org.springframework.lang.Nullable;
*/
public abstract class SystemPropertyUtils {
/** Prefix for property placeholders: {@value}. */
/** Prefix for system property placeholders: {@value}. */
public static final String PLACEHOLDER_PREFIX = "${";
/** Suffix for property placeholders: {@value}. */
/** Suffix for system property placeholders: {@value}. */
public static final String PLACEHOLDER_SUFFIX = "}";
/** Value separator for property placeholders: {@value}. */
/** Value separator for system property placeholders: {@value}. */
public static final String VALUE_SEPARATOR = ":";
/**
* Escape character for property placeholders: {@code '\'}.
* @since 6.2
*/
/** Default escape character: {@code '\'}. */
public static final Character ESCAPE_CHARACTER = '\\';
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -85,7 +85,6 @@ 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;
@@ -205,7 +204,6 @@ public class ExponentialBackOff implements BackOff {
return this.maxAttempts;
}
@Override
public BackOffExecution start() {
return new ExponentialBackOffExecution();
@@ -227,7 +225,6 @@ public class ExponentialBackOff implements BackOff {
.toString();
}
private class ExponentialBackOffExecution implements BackOffExecution {
private long currentInterval = -1;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2018 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,7 +35,6 @@ public class FixedBackOff implements BackOff {
*/
public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE;
private long interval = DEFAULT_INTERVAL;
private long maxAttempts = UNLIMITED_ATTEMPTS;
@@ -87,7 +86,6 @@ public class FixedBackOff implements BackOff {
return this.maxAttempts;
}
@Override
public BackOffExecution start() {
return new FixedBackOffExecution();
@@ -1,120 +0,0 @@
/*
* 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);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@ import java.util.Map;
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;
@@ -39,15 +38,18 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
class PropertySourcesPropertyResolverTests {
private final Properties testProperties = new Properties();
private Properties testProperties;
private final MutablePropertySources propertySources = new MutablePropertySources();
private MutablePropertySources propertySources;
private final PropertySourcesPropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);
private ConfigurablePropertyResolver propertyResolver;
@BeforeEach
void setUp() {
propertySources = new MutablePropertySources();
propertyResolver = new PropertySourcesPropertyResolver(propertySources);
testProperties = new Properties();
propertySources.addFirst(new PropertiesPropertySource("testProperties", testProperties));
}
@@ -75,12 +77,14 @@ class PropertySourcesPropertyResolverTests {
@Test
void getProperty_propertySourceSearchOrderIsFIFO() {
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");
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");
}
@Test
@@ -111,8 +115,8 @@ class PropertySourcesPropertyResolverTests {
class TestType { }
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> propertyResolver.getProperty("foo", TestType.class));
assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() ->
propertyResolver.getProperty("foo", TestType.class));
}
@Test
@@ -123,6 +127,7 @@ class PropertySourcesPropertyResolverTests {
HashMap<String, Object> 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);
@@ -133,6 +138,7 @@ class PropertySourcesPropertyResolverTests {
@Test
void getProperty_doesNotCache_addNewKeyPostConstruction() {
HashMap<String, Object> map = new HashMap<>();
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MapPropertySource("testProperties", map));
PropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(propertyResolver.getProperty("foo")).isNull();
@@ -142,9 +148,10 @@ class PropertySourcesPropertyResolverTests {
@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");
@@ -155,7 +162,7 @@ class PropertySourcesPropertyResolverTests {
// 'system' now has precedence
assertThat(propertyResolver.getProperty("foo")).isEqualTo("newValue");
assertThat(propertySources).hasSize(3);
assertThat(propertySources).hasSize(2);
}
@Test
@@ -163,65 +170,81 @@ class PropertySourcesPropertyResolverTests {
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)).containsExactly("abc", "123");
assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).isEqualTo(new String[] { "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"));
assertThat(propertyResolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
}
@Test
void resolvePlaceholders_withUnresolvable() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown}"))
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.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"));
assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
.isEqualTo("Replace this value plus defaultValue");
}
@Test
void resolvePlaceholders_withNullInput() {
assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolvePlaceholders(null));
assertThatIllegalArgumentException().isThrownBy(() ->
new PropertySourcesPropertyResolver(new MutablePropertySources()).resolvePlaceholders(null));
}
@Test
void resolveRequiredPlaceholders() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
}
@Test
void resolveRequiredPlaceholders_withUnresolvable() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}"));
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}"));
}
@Test
void resolveRequiredPlaceholders_withDefaultValue() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
.isEqualTo("Replace this value plus defaultValue");
}
@Test
void resolveRequiredPlaceholders_withNullInput() {
assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders(null));
assertThatIllegalArgumentException().isThrownBy(() ->
new PropertySourcesPropertyResolver(new MutablePropertySources()).resolveRequiredPlaceholders(null));
}
@Test
@@ -233,17 +256,17 @@ class PropertySourcesPropertyResolverTests {
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", "");
@@ -268,13 +291,13 @@ class PropertySourcesPropertyResolverTests {
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
@@ -326,9 +349,9 @@ class PropertySourcesPropertyResolverTests {
// 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);
@@ -338,58 +361,9 @@ class PropertySourcesPropertyResolverTests {
// 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}\"");
}
@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");
}
assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
pr.resolveRequiredPlaceholders("${p1}:${p2}:${bogus}"))
.withMessageContaining("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\"");
}
}
@@ -335,11 +335,6 @@ class PathMatchingResourcePatternResolverTests {
}
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 {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -53,22 +53,18 @@ class PatternMatchUtilsTests {
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
@@ -78,10 +74,6 @@ class PatternMatchUtilsTests {
assertMatches("*stuff*", "stuffTest");
assertMatches("*stuff*", "getstuff");
assertMatches("*stuff*", "stuff");
testMixedCaseMatch("*stuff*", "getStuffTest");
testMixedCaseMatch("*stuff*", "StuffTest");
testMixedCaseMatch("*stuff*", "getStuff");
testMixedCaseMatch("*stuff*", "Stuff");
}
@Test
@@ -90,8 +82,6 @@ class PatternMatchUtilsTests {
assertMatches("on*Event", "onEvent");
assertDoesNotMatch("3*3", "3");
assertMatches("3*3", "33");
testMixedCaseMatch("on*Event", "OnMyEvenT");
testMixedCaseMatch("on*Event", "OnEvenT");
}
@Test
@@ -132,27 +122,18 @@ class PatternMatchUtilsTests {
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();
}
}
@@ -16,7 +16,7 @@
package org.springframework.util;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
@@ -36,13 +36,13 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
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 PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("placeholders")
void placeholderIsReplaced(String text, String expected) {
Map<String, String> properties = Map.of(
"firstName", "John",
"nested0", "first",
"nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(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);
}
static Stream<Arguments> placeholders() {
@@ -79,13 +79,13 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
Map<String, String> 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);
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);
}
static Stream<Arguments> nestedPlaceholders() {
@@ -101,15 +101,19 @@ class PlaceholderParserTests {
@Test
void parseWithSinglePlaceholder() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "firstName");
assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
.isEqualTo("John");
verify(resolver).resolvePlaceholder("firstName");
verifyNoMoreInteractions(resolver);
}
@Test
void parseWithPlaceholderAndPrefixText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)).isEqualTo("This is John");
verifyPlaceholderResolutions(resolver, "firstName");
assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver))
.isEqualTo("This is John");
verify(resolver).resolvePlaceholder("firstName");
verifyNoMoreInteractions(resolver);
}
@Test
@@ -117,25 +121,31 @@ class PlaceholderParserTests {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith");
assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver))
.isEqualTo("User: John - Smith.");
verifyPlaceholderResolutions(resolver, "firstName", "lastName");
verify(resolver).resolvePlaceholder("firstName");
verify(resolver).resolvePlaceholder("lastName");
verifyNoMoreInteractions(resolver);
}
@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 placeholderValueContainingSeparatorIsHandledAsIs() {
void placeholdersWithSeparatorAreHandledAsIs() {
PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value");
assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value");
verifyPlaceholderResolutions(resolver, "my:test");
@@ -143,20 +153,17 @@ class PlaceholderParserTests {
@Test
void placeholdersWithoutEscapeCharAreNotEscaped() {
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");
PlaceholderResolver resolver = mockPlaceholderResolver("test", "value");
assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value");
verifyPlaceholderResolutions(resolver, "test");
}
@Test
void textWithInvalidPlaceholderSyntaxIsMerged() {
void textWithInvalidPlaceholderIsMerged() {
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));
}
}
@@ -169,11 +176,11 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("placeholders")
void placeholderIsReplaced(String text, String expected) {
Map<String, String> properties = Map.of(
"firstName", "John",
"nested0", "first",
"nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(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);
}
static Stream<Arguments> placeholders() {
@@ -192,14 +199,14 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
Map<String, String> 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);
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);
}
static Stream<Arguments> nestedPlaceholders() {
@@ -218,11 +225,11 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("exactMatchPlaceholders")
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
Map<String, String> properties = Map.of(
"prefix://my-service", "example-service",
"px", "prefix",
"p1", "${prefix://my-service}");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(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);
}
static Stream<Arguments> exactMatchPlaceholders() {
@@ -235,55 +242,74 @@ class PlaceholderParserTests {
@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");
}
}
/**
* Tests that use the escape character.
*/
@Nested
@Nested // Tests with the use of the escape character
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<Arguments> 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) {
Map<String, String> properties = Map.of(
"firstName", "John",
PlaceholderResolver resolver = mockPlaceholderResolver(
"firstName", "John", "nested0", "first", "nested1", "Name",
"${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, properties::get)).isEqualTo(expected);
assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected);
}
static Stream<Arguments> escapedPlaceholders() {
@@ -298,21 +324,18 @@ class PlaceholderParserTests {
Arguments.of("${p4}", "adc${p1}"),
Arguments.of("${p5}", "adcv1"),
Arguments.of("${p6}", "adcdef${p1}"),
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")
);
Arguments.of("${p7}", "adc\\${"));
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedSeparators")
void escapedSeparatorIsNotReplaced(String text, String expected) {
Map<String, String> properties = Map.of("first:Name", "John");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(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);
}
static Stream<Arguments> escapedSeparators() {
@@ -322,26 +345,6 @@ class PlaceholderParserTests {
);
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedSeparatorsInNestedPlaceholders")
void escapedSeparatorInNestedPlaceholderIsNotReplaced(String text, String expected) {
Map<String, String> 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<Arguments> 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
@@ -351,38 +354,34 @@ class PlaceholderParserTests {
@Test
void textWithCircularReference() {
Map<String, String> properties = Map.of(
"pL", "${pR}",
"pR", "${pL}");
assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", properties::get))
PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}");
assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver))
.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("X${bogus}Z", placeholderName -> null))
.withMessage("Could not resolve placeholder 'bogus' in value \"X${bogus}Z\"")
.isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver))
.withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"")
.withNoCause();
}
@Test
void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() {
Map<String, String> properties = Map.of(
"p1", "v1",
"p2", "v2",
PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2",
"p3", "${p1}:${p2}:${bogus}");
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> this.parser.replacePlaceholders("${p3}", properties::get))
.isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver))
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
.withNoCause();
}
}
private static PlaceholderResolver mockPlaceholderResolver(String... pairs) {
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
if (pairs.length % 2 == 1) {
throw new IllegalArgumentException("size must be even, it is a set of key=value pairs");
}
@@ -395,7 +394,7 @@ class PlaceholderParserTests {
return resolver;
}
private static void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
InOrder ordered = inOrder(mock);
for (String placeholder : placeholders) {
ordered.verify(mock).resolvePlaceholder(placeholder);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,11 +16,8 @@
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;
@@ -147,86 +144,6 @@ class JdbcClientIntegrationTests {
}
@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<User> users = jdbcClient.sql(QUERY1)
.param("name", "John")
.query(User.class)
.list();
assertResults(users);
}
@Test
void selectWithReusedNamedParameterFromBeanProperties() {
List<User> users = jdbcClient.sql(QUERY1)
.paramSource(new Name("John"))
.query(User.class)
.list();
assertResults(users);
}
@Test
void selectWithReusedNamedParameterList() {
List<User> users = jdbcClient.sql(QUERY2)
.param("names", List.of("John", "Bogus"))
.query(User.class)
.list();
assertResults(users);
}
@Test
void selectWithReusedNamedParameterListFromBeanProperties() {
List<User> users = jdbcClient.sql(QUERY2)
.paramSource(new Names(List.of("John", "Bogus")))
.query(User.class)
.list();
assertResults(users);
}
private static void assertResults(List<User> users) {
assertThat(users).containsExactly(new User(2, "John", "John"), new User(3, "John", "Smith"));
}
record Name(String name) {}
record Names(List<String> names) {}
}
private void assertNumUsers(long count) {
long numUsers = this.jdbcClient.sql("select count(id) from users").query(Long.class).single();
assertThat(numUsers).isEqualTo(count);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,7 +49,6 @@ import org.springframework.util.Assert;
* @author Juergen Hoeller
* @author Mark Paluch
* @author Anton Naydenov
* @author Sam Brannen
* @since 5.3
*/
abstract class NamedParameterUtils {
@@ -514,17 +513,70 @@ abstract class NamedParameterUtils {
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<BindMarker> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
if (parameter.getValue() instanceof Collection collection) {
Iterator<Object> iterator = collection.iterator();
Iterator<BindMarker> markers = bindMarkers.iterator();
while (iterator.hasNext()) {
Object valueToBind = iterator.next();
if (valueToBind instanceof Object[] objects) {
for (Object object : objects) {
bind(target, markers, object);
}
}
else {
bind(target, markers, valueToBind);
}
}
}
else {
for (BindMarker bindMarker : bindMarkers) {
bindMarker.bind(target, parameter);
}
}
}
@Override
public String toQuery() {
return this.expandedSql;
private void bind(BindTarget target, Iterator<BindMarker> markers, Object valueToBind) {
Assert.isTrue(markers.hasNext(), () -> String.format(
"No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.",
valueToBind, toQuery()));
markers.next().bind(target, valueToBind);
}
public void bindNull(BindTarget target, String identifier, Parameter parameter) {
List<BindMarker> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
for (BindMarker bindMarker : bindMarkers) {
bindMarker.bind(target, parameter);
}
}
@Nullable
List<BindMarker> getBindMarkers(String identifier) {
List<NamedParameters.NamedParameter> parameters = this.parameters.getMarker(identifier);
if (parameters == null) {
return null;
}
List<BindMarker> markers = new ArrayList<>();
for (NamedParameters.NamedParameter parameter : parameters) {
markers.addAll(parameter.placeholders);
}
return markers;
}
@Override
@@ -545,68 +597,10 @@ abstract class NamedParameterUtils {
}
}
private void bindNull(BindTarget target, String identifier, Parameter parameter) {
List<List<BindMarker>> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
for (List<BindMarker> outer : bindMarkers) {
for (BindMarker bindMarker : outer) {
bindMarker.bind(target, parameter);
}
}
@Override
public String toQuery() {
return this.expandedSql;
}
private void bind(BindTarget target, String identifier, Parameter parameter) {
List<List<BindMarker>> bindMarkers = getBindMarkers(identifier);
if (bindMarkers == null) {
target.bind(identifier, parameter);
return;
}
for (List<BindMarker> outer : bindMarkers) {
if (parameter.getValue() instanceof Collection<?> collection) {
Iterator<BindMarker> markers = outer.iterator();
for (Object valueToBind : collection) {
if (valueToBind instanceof Object[] objects) {
for (Object object : objects) {
bind(target, markers, object);
}
}
else {
bind(target, markers, valueToBind);
}
}
}
else {
for (BindMarker bindMarker : outer) {
bindMarker.bind(target, parameter);
}
}
}
}
private void bind(BindTarget target, Iterator<BindMarker> markers, Object valueToBind) {
Assert.isTrue(markers.hasNext(), () -> String.format(
"No bind marker for value [%s] in SQL [%s]. Check that the query was expanded using the same arguments.",
valueToBind, toQuery()));
markers.next().bind(target, valueToBind);
}
@Nullable
private List<List<BindMarker>> getBindMarkers(String identifier) {
List<NamedParameters.NamedParameter> parameters = this.parameters.getMarker(identifier);
if (parameters == null) {
return null;
}
List<List<BindMarker>> markers = new ArrayList<>();
for (NamedParameters.NamedParameter parameter : parameters) {
markers.add(new ArrayList<>(parameter.placeholders));
}
return markers;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,9 +19,8 @@ package org.springframework.r2dbc.core.binding;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* Anonymous, index-based bind markers that use a static placeholder.
*
* <p>Instances are bound by the ordinal position ordered by the appearance of
* Anonymous, index-based bind marker using a static placeholder.
* Instances are bound by the ordinal position ordered by the appearance of
* the placeholder. This implementation creates indexed bind markers using
* an anonymous placeholder that correlates with an index.
*
@@ -47,7 +46,7 @@ class AnonymousBindMarkers implements BindMarkers {
/**
* Create a new {@link AnonymousBindMarkers} instance for the given {@code placeholder}.
* Create a new {@link AnonymousBindMarkers} instance given {@code placeholder}.
* @param placeholder parameter bind marker
*/
AnonymousBindMarkers(String placeholder) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,7 @@ import io.r2dbc.spi.Statement;
/**
* A bind marker represents a single bindable parameter within a query.
*
* <p>Bind markers are dialect-specific and provide a
* Bind markers are dialect-specific and provide a
* {@link #getPlaceholder() placeholder} that is used in the actual query.
*
* @author Mark Paluch
@@ -38,8 +37,7 @@ public interface BindMarker {
String getPlaceholder();
/**
* Bind the given {@code value} to the {@link Statement} using the underlying
* binding strategy.
* Bind the given {@code value} to the {@link Statement} using the underlying binding strategy.
* @param bindTarget the target to bind the value to
* @param value the actual value (must not be {@code null};
* use {@link #bindNull(BindTarget, Class)} for {@code null} values)
@@ -48,8 +46,7 @@ public interface BindMarker {
void bind(BindTarget bindTarget, Object value);
/**
* Bind a {@code null} value to the {@link Statement} using the underlying
* binding strategy.
* Bind a {@code null} value to the {@link Statement} using the underlying binding strategy.
* @param bindTarget the target to bind the value to
* @param valueType the value type (must not be {@code null})
* @see Statement#bindNull
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,10 +20,10 @@ package org.springframework.r2dbc.core.binding;
* Bind markers represent placeholders in SQL queries for substitution
* for an actual parameter. Using bind markers allows creating safe queries
* so query strings are not required to contain escaped values but rather
* the driver encodes the parameter in the appropriate representation.
* the driver encodes parameter in the appropriate representation.
*
* <p>{@link BindMarkers} is stateful and can be only used for a single binding
* pass of one or more parameters. It maintains bind indexes or bind parameter names.
* pass of one or more parameters. It maintains bind indexes/bind parameter names.
*
* @author Mark Paluch
* @since 5.3
@@ -41,7 +41,7 @@ public interface BindMarkers {
/**
* Create a new {@link BindMarker} that accepts a {@code hint}.
* <p>Implementations are allowed to consider/ignore/filter
* Implementations are allowed to consider/ignore/filter
* the name hint to create more expressive bind markers.
* @param hint an optional name hint that can be used as part of the bind marker
* @return a new {@link BindMarker}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,8 +30,8 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
/**
* Resolves a {@link BindMarkersFactory} from a {@link ConnectionFactory} using
* a {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's
* {@link SpringFactoriesLoader spring.factories} file to determine available extensions.
* {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's
* {@link SpringFactoriesLoader spring.factories} to determine available extensions.
*
* @author Mark Paluch
* @since 5.3
@@ -45,8 +45,8 @@ public final class BindMarkersFactoryResolver {
/**
* Retrieve a {@link BindMarkersFactory} by inspecting the supplied
* {@link ConnectionFactory} and its metadata.
* Retrieve a {@link BindMarkersFactory} by inspecting {@link ConnectionFactory}
* and its metadata.
* @param connectionFactory the connection factory to inspect
* @return the resolved {@link BindMarkersFactory}
* @throws NoBindMarkersFactoryException if no {@link BindMarkersFactory} can be resolved
@@ -69,21 +69,18 @@ public final class BindMarkersFactoryResolver {
/**
* SPI to extend Spring's default R2DBC {@link BindMarkersFactory} discovery
* mechanism.
*
* <p>Implementations of this interface are discovered through Spring's
* SPI to extend Spring's default R2DBC BindMarkersFactory discovery mechanism.
* Implementations of this interface are discovered through Spring's
* {@link SpringFactoriesLoader} mechanism.
*
* @see SpringFactoriesLoader
*/
@FunctionalInterface
public interface BindMarkerFactoryProvider {
/**
* Return a {@link BindMarkersFactory} for the given {@link ConnectionFactory}.
* @param connectionFactory the connection factory to be used with the {@code BindMarkersFactory}
* @return the {@code BindMarkersFactory} if this {@code BindMarkerFactoryProvider}
* Return a {@link BindMarkersFactory} for a {@link ConnectionFactory}.
* @param connectionFactory the connection factory to be used with the {@link BindMarkersFactory}
* @return the {@link BindMarkersFactory} if the {@link BindMarkerFactoryProvider}
* can provide a bind marker factory object, otherwise {@code null}
*/
@Nullable
@@ -92,7 +89,7 @@ public final class BindMarkersFactoryResolver {
/**
* Exception thrown when a {@link BindMarkersFactoryResolver} cannot resolve a
* Exception thrown when {@link BindMarkersFactoryResolver} cannot resolve a
* {@link BindMarkersFactory}.
*/
@SuppressWarnings("serial")
@@ -109,11 +106,8 @@ public final class BindMarkersFactoryResolver {
/**
* Built-in bind marker factories.
*
* <p>Typically used as the last {@link BindMarkerFactoryProvider} when other
* providers are registered with a higher precedence.
*
* Built-in bind maker factories. Used typically as last {@link BindMarkerFactoryProvider}
* when other providers register with a higher precedence.
* @see org.springframework.core.Ordered
* @see org.springframework.core.annotation.AnnotationAwareOrderComparator
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@ package org.springframework.r2dbc.core.binding;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* Index-based bind markers. This implementation creates indexed bind
* Index-based bind marker. This implementation creates indexed bind
* markers using a numeric index and an optional prefix for bind markers
* to be represented within the query string.
*
@@ -43,15 +43,14 @@ class IndexedBindMarkers implements BindMarkers {
/**
* Create a new {@link IndexedBindMarker} instance for the given {@code prefix}
* and {@code beginWith} value.
* @param prefix the bind parameter prefix
* @param beginIndex the first index to use
* Create a new {@link IndexedBindMarker} instance given {@code prefix} and {@code beginWith}.
* @param prefix bind parameter prefix
* @param beginWith the first index to use
*/
IndexedBindMarkers(String prefix, int beginIndex) {
IndexedBindMarkers(String prefix, int beginWith) {
this.counter = 0;
this.prefix = prefix;
this.offset = beginIndex;
this.offset = beginWith;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,7 +23,6 @@ import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.Parameters;
import io.r2dbc.spi.Result;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -39,7 +38,6 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Mark Paluch
* @author Mingyuan Wu
* @author Juergen Hoeller
* @author Sam Brannen
*/
abstract class AbstractDatabaseClientIntegrationTests {
@@ -123,8 +121,7 @@ abstract class AbstractDatabaseClientIntegrationTests {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
databaseClient.sql("INSERT INTO legoset (id, name, manual) VALUES(:id, :name, :manual)")
.bindValues(Map.of(
"id", 42055,
.bindValues(Map.of("id", 42055,
"name", Parameters.in("SCHAUFELRADBAGGER"),
"manual", Parameters.in(Integer.class)))
.fetch().rowsUpdated()
@@ -202,7 +199,8 @@ abstract class AbstractDatabaseClientIntegrationTests {
void shouldEmitGeneratedKey() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
databaseClient.sql("INSERT INTO legoset ( name, manual) VALUES(:name, :manual)")
databaseClient.sql(
"INSERT INTO legoset ( name, manual) VALUES(:name, :manual)")
.bind("name","SCHAUFELRADBAGGER")
.bindNull("manual", Integer.class)
.filter(statement -> statement.returnGeneratedValues("id"))
@@ -214,129 +212,6 @@ abstract class AbstractDatabaseClientIntegrationTests {
}
@Nested
class ReusedNamedParameterTests {
@Test // gh-34768
void executeInsertWithReusedNamedParameter() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
Lego lego = new Lego(1, 42, "Star Wars", 42);
// ":number" is reused.
databaseClient.sql("INSERT INTO legoset (id, version, name, manual) VALUES(:id, :number, :name, :number)")
.bind("id", lego.id)
.bind("name", lego.name)
.bind("number", lego.version)
.fetch().rowsUpdated()
.as(StepVerifier::create)
.expectNext(1L)
.verifyComplete();
databaseClient.sql("SELECT * FROM legoset")
.mapProperties(Lego.class)
.first()
.as(StepVerifier::create)
.assertNext(actual -> assertThat(actual).isEqualTo(lego))
.verifyComplete();
}
@Test // gh-34768
void executeSelectWithReusedNamedParameterList() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)";
// ":numbers" is reused.
String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)";
Lego lego = new Lego(1, 42, "Star Wars", 99);
databaseClient.sql(insertSql)
.bind("id", lego.id)
.bind("version", lego.version)
.bind("name", lego.name)
.bind("manual", lego.manual)
.fetch().rowsUpdated()
.as(StepVerifier::create)
.expectNext(1L)
.verifyComplete();
databaseClient.sql(selectSql)
// match version
.bind("numbers", List.of(2, 3, lego.version, 4))
.mapProperties(Lego.class)
.first()
.as(StepVerifier::create)
.assertNext(actual -> assertThat(actual).isEqualTo(lego))
.verifyComplete();
databaseClient.sql(selectSql)
// match manual
.bind("numbers", List.of(2, 3, lego.manual, 4))
.mapProperties(Lego.class)
.first()
.as(StepVerifier::create)
.assertNext(actual -> assertThat(actual).isEqualTo(lego))
.verifyComplete();
}
@Test // gh-34768
void executeSelectWithReusedNamedParameterListFromBeanProperties() {
DatabaseClient databaseClient = DatabaseClient.create(connectionFactory);
String insertSql = "INSERT INTO legoset (id, version, name, manual) VALUES(:id, :version, :name, :manual)";
// ":numbers" is reused.
String selectSql = "SELECT * FROM legoset WHERE version IN (:numbers) OR manual IN (:numbers)";
Lego lego = new Lego(1, 42, "Star Wars", 99);
databaseClient.sql(insertSql)
.bind("id", lego.id)
.bind("version", lego.version)
.bind("name", lego.name)
.bind("manual", lego.manual)
.fetch().rowsUpdated()
.as(StepVerifier::create)
.expectNext(1L)
.verifyComplete();
databaseClient.sql(selectSql)
// match version
.bindProperties(new LegoRequest(List.of(lego.version)))
.mapProperties(Lego.class)
.first()
.as(StepVerifier::create)
.assertNext(actual -> assertThat(actual).isEqualTo(lego))
.verifyComplete();
databaseClient.sql(selectSql)
// match manual
.bindProperties(new LegoRequest(List.of(lego.manual)))
.mapProperties(Lego.class)
.first()
.as(StepVerifier::create)
.assertNext(actual -> assertThat(actual).isEqualTo(lego))
.verifyComplete();
}
record Lego(int id, Integer version, String name, Integer manual) {
}
static class LegoRequest {
private final List<Integer> numbers;
LegoRequest(List<Integer> numbers) {
this.numbers = numbers;
}
public List<Integer> getNumbers() {
return numbers;
}
}
}
record ParameterRecord(int id, String name, Integer manual) {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,10 @@
package org.springframework.r2dbc.core;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.LinkedHashMap;
import java.util.Map;
import io.r2dbc.spi.Parameters;
@@ -27,6 +29,8 @@ import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.r2dbc.core.binding.BindMarkersFactory;
import org.springframework.r2dbc.core.binding.BindTarget;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
@@ -38,13 +42,10 @@ import static org.mockito.Mockito.verify;
* @author Mark Paluch
* @author Jens Schauder
* @author Anton Naydenov
* @author Sam Brannen
*/
class NamedParameterUtilsTests {
private static final BindMarkersFactory INDEXED_MARKERS = BindMarkersFactory.indexed("$", 1);
private static final BindMarkersFactory ANONYMOUS_MARKERS = BindMarkersFactory.anonymous("?");
private final BindMarkersFactory BIND_MARKERS = BindMarkersFactory.indexed("$", 1);
@Test
@@ -72,7 +73,7 @@ class NamedParameterUtilsTests {
namedParams.addValue("a", "a").addValue("b", "b").addValue("c", "c");
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a :b :c", INDEXED_MARKERS, namedParams);
"xxx :a :b :c", BIND_MARKERS, namedParams);
assertThat(operation.toQuery()).isEqualTo("xxx $1 $2 $3");
@@ -86,11 +87,11 @@ class NamedParameterUtilsTests {
void substituteObjectArray() {
MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
namedParams.addValue("a",
List.of(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
Arrays.asList(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a", INDEXED_MARKERS, namedParams);
"xxx :a", BIND_MARKERS, namedParams);
assertThat(operation.toQuery()).isEqualTo("xxx ($1, $2), ($3, $4)");
}
@@ -99,13 +100,13 @@ class NamedParameterUtilsTests {
void shouldBindObjectArray() {
MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
namedParams.addValue("a",
List.of(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
Arrays.asList(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
BindTarget bindTarget = mock();
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a", INDEXED_MARKERS, namedParams);
"xxx :a", BIND_MARKERS, namedParams);
operation.bindTo(bindTarget);
verify(bindTarget).bind(0, "Walter");
@@ -140,7 +141,7 @@ class NamedParameterUtilsTests {
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
parsedSql, INDEXED_MARKERS, new MapBindParameterSource());
parsedSql, BIND_MARKERS, new MapBindParameterSource());
assertThat(operation.toQuery()).isEqualTo(expectedSql);
}
@@ -311,139 +312,157 @@ class NamedParameterUtilsTests {
void multipleEqualParameterReferencesBindsValueOnce() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo")));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source);
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = $1 or lastname = $1");
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameters.in("foo"))));
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = $0 or lastname = $0");
operation.bindTo(trackingBindTarget);
assertThat(trackingBindTarget.bindings)
.hasSize(1)
.containsEntry(0, Parameters.in("foo"));
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
assertThat(index).isEqualTo(0);
assertThat(value).isEqualTo(Parameters.in("foo"));
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
}
@Test
void multipleEqualCollectionParameterReferencesForIndexedMarkersBindsValuesOnce() {
void multipleEqualCollectionParameterReferencesBindsValueOnce() {
String sql = "SELECT * FROM person where name IN (:ids) or lastname IN (:ids)";
MapBindParameterSource source = new MapBindParameterSource(Map.of("ids",
Parameters.in(List.of("foo", "bar", "baz"))));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source);
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name IN ($1, $2, $3) or lastname IN ($1, $2, $3)");
MultiValueMap<Integer, Object> bindings = new LinkedMultiValueMap<>();
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(Collections.singletonMap("ids",
Parameters.in(Arrays.asList("foo", "bar", "baz")))));
operation.bindTo(trackingBindTarget);
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name IN ($0, $1, $2) or lastname IN ($0, $1, $2)");
assertThat(trackingBindTarget.bindings)
.hasSize(3)
.containsEntry(0, "foo")
.containsEntry(1, "bar")
.containsEntry(2, "baz");
}
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
assertThat(index).isIn(0, 1, 2);
assertThat(value).isIn("foo", "bar", "baz");
bindings.add(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
@Test // gh-34768
void multipleEqualCollectionParameterReferencesForAnonymousMarkersBindsValuesTwice() {
String sql = "SELECT * FROM fund_info WHERE fund_code IN (:fundCodes) OR fund_code IN (:fundCodes)";
MapBindParameterSource source = new MapBindParameterSource(Map.of("fundCodes", Parameters.in(List.of("foo", "bar", "baz"))));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source);
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM fund_info WHERE fund_code IN (?, ?, ?) OR fund_code IN (?, ?, ?)");
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
operation.bindTo(trackingBindTarget);
assertThat(trackingBindTarget.bindings)
.hasSize(6)
.containsEntry(0, "foo")
.containsEntry(1, "bar")
.containsEntry(2, "baz")
.containsEntry(3, "foo")
.containsEntry(4, "bar")
.containsEntry(5, "baz");
assertThat(bindings).containsEntry(0, Collections.singletonList("foo")) //
.containsEntry(1, Collections.singletonList("bar")) //
.containsEntry(2, Collections.singletonList("baz"));
}
@Test
void multipleEqualParameterReferencesForAnonymousMarkersBindsValueTwice() {
void multipleEqualParameterReferencesForAnonymousMarkersBindsValueMultipleTimes() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo")));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source);
BindMarkersFactory factory = BindMarkersFactory.anonymous("?");
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = ? or lastname = ?");
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameters.in("foo"))));
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = ? or lastname = ?");
operation.bindTo(trackingBindTarget);
Map<Integer, Object> bindValues = new LinkedHashMap<>();
assertThat(trackingBindTarget.bindings)
.hasSize(2)
.containsEntry(0, Parameters.in("foo"))
.containsEntry(1, Parameters.in("foo"));
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
bindValues.put(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
assertThat(bindValues).hasSize(2).containsEntry(0, Parameters.in("foo")).containsEntry(1, Parameters.in("foo"));
}
@Test
void multipleEqualParameterReferencesBindsNullOnce() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in(String.class)));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source);
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = $1 or lastname = $1");
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameters.in(String.class))));
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = $0 or lastname = $0");
operation.bindTo(trackingBindTarget);
assertThat(trackingBindTarget.bindings)
.hasSize(1)
.containsEntry(0, Parameters.in(String.class));
operation.bindTo(new BindTarget() {
@Override
public void bind(String identifier, Object value) {
throw new UnsupportedOperationException();
}
@Override
public void bind(int index, Object value) {
assertThat(index).isEqualTo(0);
assertThat(value).isEqualTo(Parameters.in(String.class));
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
});
}
private static String expand(ParsedSql sql) {
return NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS,
private String expand(ParsedSql sql) {
return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS,
new MapBindParameterSource()).toQuery();
}
private static String expand(String sql) {
return NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS,
private String expand(String sql) {
return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS,
new MapBindParameterSource()).toQuery();
}
private static class TrackingBindTarget implements BindTarget {
final Map<Integer, Object> bindings = new HashMap<>();
@Override
public void bind(String identifier, Object value) {}
@Override
public void bind(int index, Object value) {
this.bindings.put(index, value);
}
@Override
public void bindNull(String identifier, Class<?> type) {
throw new UnsupportedOperationException();
}
@Override
public void bindNull(int index, Class<?> type) {
throw new UnsupportedOperationException();
}
}
}
+7 -3
View File
@@ -105,10 +105,14 @@ test {
description = "Runs JUnit 4, JUnit Jupiter, and TestNG tests."
useJUnitPlatform {
includeEngines "junit-vintage", "junit-jupiter", "testng"
excludeTags "failing-test-case"
}
// `include` test filters and system properties are configured in
// org.springframework.build.TestConventions in buildSrc.
// We use `include` instead of `filter.includeTestsMatching`, since
// the latter results in some tests being executed/reported
// multiple times.
include(["**/*Tests.class", "**/*Test.class"])
filter.excludeTestsMatching("*TestCase")
// Optionally configure Java Util Logging for the JUnit Platform.
systemProperty("testGroups", project.properties.get("testGroups"))
// Java Util Logging for the JUnit Platform.
// systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
}
@@ -16,8 +16,6 @@
package org.springframework.mock.web.server;
import java.security.Principal;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
@@ -42,19 +40,15 @@ import org.springframework.web.server.session.WebSessionManager;
*/
public final class MockServerWebExchange extends DefaultServerWebExchange {
private final Mono<Principal> principalMono;
private MockServerWebExchange(
MockServerHttpRequest request, @Nullable WebSessionManager sessionManager,
@Nullable ApplicationContext applicationContext, @Nullable Principal principal) {
@Nullable ApplicationContext applicationContext) {
super(request, new MockServerHttpResponse(),
sessionManager != null ? sessionManager : new DefaultWebSessionManager(),
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(),
applicationContext);
this.principalMono = (principal != null) ? Mono.just(principal) : Mono.empty();
}
@@ -63,16 +57,6 @@ public final class MockServerWebExchange extends DefaultServerWebExchange {
return (MockServerHttpResponse) super.getResponse();
}
/**
* Return the user set via {@link Builder#principal(Principal)}.
* @since 6.2.7
*/
@SuppressWarnings("unchecked")
@Override
public <T extends Principal> Mono<T> getPrincipal() {
return (Mono<T>) this.principalMono;
}
/**
* Create a {@link MockServerWebExchange} from the given mock request.
@@ -127,9 +111,6 @@ public final class MockServerWebExchange extends DefaultServerWebExchange {
@Nullable
private ApplicationContext applicationContext;
@Nullable
private Principal principal;
public Builder(MockServerHttpRequest request) {
this.request = request;
}
@@ -166,22 +147,11 @@ public final class MockServerWebExchange extends DefaultServerWebExchange {
return this;
}
/**
* Provide a user to associate with the exchange.
* @param principal the principal to use
* @since 6.2.7
*/
public Builder principal(@Nullable Principal principal) {
this.principal = principal;
return this;
}
/**
* Build the {@code MockServerWebExchange} instance.
*/
public MockServerWebExchange build() {
return new MockServerWebExchange(
this.request, this.sessionManager, this.applicationContext, this.principal);
return new MockServerWebExchange(this.request, this.sessionManager, this.applicationContext);
}
}
@@ -314,7 +314,8 @@ public class TestContextAotGenerator {
ClassName processAheadOfTime(MergedContextConfiguration mergedConfig,
GenerationContext generationContext) throws TestContextAotException {
try (GenericApplicationContext gac = loadContextForAotProcessing(mergedConfig)) {
GenericApplicationContext gac = loadContextForAotProcessing(mergedConfig);
try {
return this.aotGenerator.processAheadOfTime(gac, generationContext);
}
catch (Throwable ex) {
@@ -332,7 +333,7 @@ public class TestContextAotGenerator {
* context or if one of the prerequisites is not met
* @see AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration, RuntimeHints)
*/
GenericApplicationContext loadContextForAotProcessing(
private GenericApplicationContext loadContextForAotProcessing(
MergedContextConfiguration mergedConfig) throws TestContextAotException {
Class<?> testClass = mergedConfig.getTestClass();
@@ -184,32 +184,30 @@ public abstract class BeanOverrideHandler {
* @param testClass the original test class
* @param handlers the list of handlers found
* @param localFieldsOnly whether to search only on local fields within the type hierarchy
* @param visitedTypes the set of types already visited
* @param visitedEnclosingClasses the set of enclosing classes already visited
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly, Set<Class<?>> visitedTypes) {
// 0) Ensure that we do not process the same class or interface multiple times.
if (!visitedTypes.add(clazz)) {
return;
}
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, visitedTypes);
Class<?> enclosingClass = clazz.getEnclosingClass();
if (visitedEnclosingClasses.add(enclosingClass)) {
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedTypes);
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedTypes);
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
// 4) Process current class.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 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,9 @@ import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
@@ -31,10 +32,9 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit4.SpringRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
/**
* Integration tests that verify proper behavior of {@link DirtiesContext @DirtiesContext}
@@ -87,11 +87,9 @@ class ContextHierarchyDirtiesContextTests {
private void runTestAndVerifyHierarchies(Class<? extends FooTestCase> testClass, boolean isFooContextActive,
boolean isBarContextActive, boolean isBazContextActive) {
EngineTestKit.engine("junit-jupiter")
.selectors(selectClass(testClass))
.execute()
.testEvents()
.assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
JUnitCore jUnitCore = new JUnitCore();
Result result = jUnitCore.run(testClass);
assertThat(result.wasSuccessful()).as("all tests passed").isTrue();
assertThat(ContextHierarchyDirtiesContextTests.context).isNotNull();
@@ -113,7 +111,7 @@ class ContextHierarchyDirtiesContextTests {
// -------------------------------------------------------------------------
@ExtendWith(SpringExtension.class)
@RunWith(SpringRunner.class)
@ContextHierarchy(@ContextConfiguration(name = "foo"))
abstract static class FooTestCase implements ApplicationContextAware {
@@ -172,10 +170,10 @@ class ContextHierarchyDirtiesContextTests {
* context.
*/
@DirtiesContext
static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
public static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
@Test
void test() {
@org.junit.Test
public void test() {
}
}
@@ -186,10 +184,10 @@ class ContextHierarchyDirtiesContextTests {
* beginning from the current context hierarchy and down through all subhierarchies.
*/
@DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL)
static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
public static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
@Test
void test() {
@org.junit.Test
public void test() {
}
}
@@ -201,11 +199,11 @@ class ContextHierarchyDirtiesContextTests {
* parent context, and then back down through all subhierarchies of the parent
* context.
*/
static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
public static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
@Test
@org.junit.Test
@DirtiesContext
void test() {
public void test() {
}
}
@@ -215,11 +213,11 @@ class ContextHierarchyDirtiesContextTests {
* <p>After running this test class, the context cache should be cleared
* beginning from the current context hierarchy and down through all subhierarchies.
*/
static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
public static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
@Test
@org.junit.Test
@DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL)
void test() {
public void test() {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2021 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,6 @@
package org.springframework.test.context;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.ExcludeTags;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.SelectPackages;
@@ -46,9 +44,5 @@ import org.junit.platform.suite.api.Suite;
@SelectPackages("org.springframework.test.context")
@IncludeClassNamePatterns(".*Tests?$")
@ExcludeTags("failing-test-case")
@ConfigurationParameter(
key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME,
value = "org.junit.jupiter.api.ClassOrderer$ClassName"
)
class SpringTestContextFrameworkTestSuite {
}
@@ -167,6 +167,7 @@ class AotIntegrationTests extends AbstractAotTests {
void endToEndTestsForSelectedTestClasses() {
List<Class<?>> testClasses = List.of(
org.springframework.test.context.bean.override.easymock.EasyMockBeanIntegrationTests.class,
org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests.class,
org.springframework.test.context.junit4.ParameterizedDependencyInjectionTests.class
);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,23 +16,13 @@
package org.springframework.test.context.aot;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.aot.generate.GeneratedFiles;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.SpringProperties;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.context.aot.TestContextAotGenerator.FAIL_ON_ERROR_PROPERTY_NAME;
@@ -70,55 +60,9 @@ class TestContextAotGeneratorTests {
assertThat(createGenerator().failOnError).isFalse();
}
@Test // gh-34841
void contextIsClosedAfterAotProcessing() {
DemoTestContextAotGenerator generator = createGenerator();
generator.processAheadOfTime(Stream.of(TestCase1.class, TestCase2.class));
assertThat(generator.contexts)
.allSatisfy(context -> assertThat(context.isClosed()).as("context is closed").isTrue());
}
private static DemoTestContextAotGenerator createGenerator() {
return new DemoTestContextAotGenerator(new InMemoryGeneratedFiles());
}
private static class DemoTestContextAotGenerator extends TestContextAotGenerator {
List<GenericApplicationContext> contexts = new ArrayList<>();
DemoTestContextAotGenerator(GeneratedFiles generatedFiles) {
super(generatedFiles);
}
@Override
GenericApplicationContext loadContextForAotProcessing(
MergedContextConfiguration mergedConfig) throws TestContextAotException {
GenericApplicationContext context = super.loadContextForAotProcessing(mergedConfig);
this.contexts.add(context);
return context;
}
}
@SpringJUnitConfig
private static class TestCase1 {
@Configuration(proxyBeanMethods = false)
static class Config {
// no beans
}
}
@SpringJUnitConfig
private static class TestCase2 {
@Configuration(proxyBeanMethods = false)
static class Config {
// no beans
}
private static TestContextAotGenerator createGenerator() {
return new TestContextAotGenerator(null);
}
}
@@ -1,69 +0,0 @@
/*
* 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.test.context.bean.override.mockito;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Abstract top-level class and abstract inner class for integration tests for
* {@link MockitoBean @MockitoBean} which verify that {@code @MockitoBean} fields
* are not discovered more than once when searching intertwined enclosing class
* hierarchies and type hierarchies, when a superclass is <em>present</em> twice
* in the intertwined hierarchies.
*
* @author Sam Brannen
* @since 6.2.7
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
*/
@ExtendWith(SpringExtension.class)
abstract class AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests {
@Autowired
ApplicationContext enclosingContext;
@MockitoBean
ExampleService service;
@Test
void topLevelTest() {
assertIsMock(service);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
}
abstract class AbstractBaseClassForNestedTests {
@Test
void nestedTest(ApplicationContext nestedContext) {
assertIsMock(service);
assertThat(enclosingContext).isSameAs(nestedContext);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
}
}
}
@@ -31,17 +31,14 @@ import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Integration tests for {@link MockitoBean @MockitoBean} which verify that
* {@code @MockitoBean} fields are not discovered more than once when searching
* intertwined enclosing class hierarchies and type hierarchies, when an enclosing
* class is <em>present</em> twice in the intertwined hierarchies.
* intertwined enclosing class hierarchies and type hierarchies.
*
* @author Sam Brannen
* @since 6.2.3
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
* @see MockitoBeanWithInterfacePresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34324">gh-34324</a>
*/
@ExtendWith(SpringExtension.class)
class MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests {
class MockitoBeanNestedAndTypeHierarchiesTests {
@Autowired
ApplicationContext enclosingContext;
@@ -53,7 +50,6 @@ class MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests {
@Test
void topLevelTest() {
assertIsMock(service);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
// The following are prerequisites for the reported regression.
assertThat(NestedTests.class.getSuperclass())
@@ -70,7 +66,6 @@ class MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests {
void nestedTest(ApplicationContext nestedContext) {
assertIsMock(service);
assertThat(enclosingContext).isSameAs(nestedContext);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
}
}
@@ -1,59 +0,0 @@
/*
* 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.test.context.bean.override.mockito;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link MockitoBean @MockitoBean} which verify that
* {@code @MockitoBean} fields are not discovered more than once when searching
* intertwined enclosing class hierarchies and type hierarchies, when a superclass
* is <em>present</em> twice in the intertwined hierarchies.
*
* @author Sam Brannen
* @since 6.2.7
* @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests
* @see MockitoBeanWithInterfacePresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
*/
class MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
extends AbstractMockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests {
@Test
@Override
void topLevelTest() {
super.topLevelTest();
// The following are prerequisites for the reported regression.
assertThat(NestedTests.class.getSuperclass())
.isEqualTo(AbstractBaseClassForNestedTests.class);
assertThat(NestedTests.class.getEnclosingClass())
.isEqualTo(getClass());
assertThat(NestedTests.class.getEnclosingClass().getSuperclass())
.isEqualTo(AbstractBaseClassForNestedTests.class.getEnclosingClass())
.isEqualTo(getClass().getSuperclass());
}
@Nested
class NestedTests extends AbstractBaseClassForNestedTests {
}
}
@@ -1,66 +0,0 @@
/*
* 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.test.context.bean.override.mockito;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.bean.override.example.ExampleService;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.mockito.MockitoAssertions.assertIsMock;
/**
* Integration tests for {@link MockitoBean @MockitoBean} which verify that type-level
* {@code @MockitoBean} declarations are not discovered more than once when searching
* a type hierarchy, when an interface is <em>present</em> twice in the hierarchy.
*
* @author Sam Brannen
* @since 6.2.7
* @see MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests
* @see MockitoBeanNestedAndTypeHierarchiesWithSuperclassPresentTwiceTests
* @see <a href="https://github.com/spring-projects/spring-framework/issues/34844">gh-34844</a>
*/
class MockitoBeanWithInterfacePresentTwiceTests extends AbstractMockitoBeanWithInterfacePresentTwiceTests
implements MockConfigInterface {
@Test
void test(ApplicationContext context) {
assertIsMock(service);
assertThat(context.getBeanNamesForType(ExampleService.class)).hasSize(1);
// The following are prerequisites for the tested scenario.
assertThat(getClass().getInterfaces()).containsExactly(MockConfigInterface.class);
assertThat(getClass().getSuperclass().getInterfaces()).containsExactly(MockConfigInterface.class);
}
}
@MockitoBean(types = ExampleService.class)
interface MockConfigInterface {
}
@ExtendWith(SpringExtension.class)
abstract class AbstractMockitoBeanWithInterfacePresentTwiceTests implements MockConfigInterface {
@Autowired
ExampleService service;
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.rules;
package org.springframework.test.context.bean.override.mockito.integration;
import org.junit.AfterClass;
import org.junit.BeforeClass;
@@ -23,6 +23,7 @@ import org.junit.Test;
import org.springframework.test.annotation.Repeat;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.context.junit4.rules.SpringMethodRule;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.when;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,7 +31,6 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.testng.AbstractTestNGSpringContextTests;
import org.springframework.test.context.testng.TrackingTestNGTestListener;
@@ -163,18 +162,12 @@ class ClassLevelDirtiesContextTestNGTests {
// -------------------------------------------------------------------
@TestExecutionListeners(listeners = { DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class }, inheritListeners = false)
@ContextConfiguration
// Ensure that we do not include the EventPublishingTestExecutionListener
// since it will access the ApplicationContext for each method in the
// TestExecutionListener API, thus distorting our cache hit/miss results.
@TestExecutionListeners({
DirtiesContextBeforeModesTestExecutionListener.class,
DependencyInjectionTestExecutionListener.class,
DirtiesContextTestExecutionListener.class
})
abstract static class BaseTestCase extends AbstractTestNGSpringContextTests {
@Configuration(proxyBeanMethods = false)
@Configuration
static class Config {
/* no beans */
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 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,8 +21,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
@@ -32,14 +31,15 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.springframework.test.context.cache.ContextCacheTestUtils.assertContextCacheStatistics;
import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache;
import static org.springframework.test.context.junit4.JUnitTestingUtils.runTestsAndAssertCounters;
/**
* JUnit based integration test which verifies correct {@linkplain ContextCache
@@ -131,24 +131,15 @@ class ClassLevelDirtiesContextTests {
0, cacheHits.incrementAndGet(), cacheMisses.get());
}
private void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) throws Exception {
runTestsAndAssertCounters(testClass, expectedTestCount, 0, expectedTestCount, 0, 0);
}
private void assertBehaviorForCleanTestCase() throws Exception {
runTestClassAndAssertStats(CleanTestCase.class, 1);
assertContextCacheStatistics("after clean test class", 1, cacheHits.get(), cacheMisses.incrementAndGet());
}
private void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) throws Exception {
EngineTestKit.engine("junit-jupiter")
.selectors(selectClass(testClass))
.execute()
.testEvents()
.assertStatistics(stats -> stats
.started(expectedTestCount)
.finished(expectedTestCount)
.succeeded(expectedTestCount)
.failed(0)
.aborted(0));
}
@AfterAll
static void verifyFinalCacheState() {
assertContextCacheStatistics("AfterClass", 0, cacheHits.get(), cacheMisses.get());
@@ -157,7 +148,7 @@ class ClassLevelDirtiesContextTests {
// -------------------------------------------------------------------
@ExtendWith(SpringExtension.class)
@RunWith(SpringRunner.class)
@ContextConfiguration
// Ensure that we do not include the EventPublishingTestExecutionListener
// since it will access the ApplicationContext for each method in the
@@ -169,7 +160,7 @@ class ClassLevelDirtiesContextTests {
})
abstract static class BaseTestCase {
@Configuration(proxyBeanMethods = false)
@Configuration
static class Config {
/* no beans */
}
@@ -184,75 +175,75 @@ class ClassLevelDirtiesContextTests {
}
}
static final class CleanTestCase extends BaseTestCase {
public static final class CleanTestCase extends BaseTestCase {
@Test
void verifyContextWasAutowired() {
@org.junit.Test
public void verifyContextWasAutowired() {
assertApplicationContextWasAutowired();
}
}
@DirtiesContext
static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase {
public static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase {
@Test
void verifyContextWasAutowired() {
@org.junit.Test
public void verifyContextWasAutowired() {
assertApplicationContextWasAutowired();
}
}
static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends
public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends
ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase {
}
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase {
public static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase {
@Test
void verifyContextWasAutowired() {
@org.junit.Test
public void verifyContextWasAutowired() {
assertApplicationContextWasAutowired();
}
}
static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends
public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends
ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase {
}
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase {
public static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase {
@Test
void verifyContextWasAutowired1() {
@org.junit.Test
public void verifyContextWasAutowired1() {
assertApplicationContextWasAutowired();
}
@Test
void verifyContextWasAutowired2() {
@org.junit.Test
public void verifyContextWasAutowired2() {
assertApplicationContextWasAutowired();
}
@Test
void verifyContextWasAutowired3() {
@org.junit.Test
public void verifyContextWasAutowired3() {
assertApplicationContextWasAutowired();
}
}
static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends
public static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends
ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase {
}
@DirtiesContext
static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase {
public static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase {
@Test
@org.junit.Test
@DirtiesContext
void dirtyContext() {
public void dirtyContext() {
assertApplicationContextWasAutowired();
}
}
static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends
public static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends
ClassLevelDirtiesContextWithDirtyMethodsTestCase {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.annotation.DirtiesContext.MethodMode.BEFORE_METHOD;
/**
* Integration tests which verify correct interaction between the
* Integration test which verifies correct interaction between the
* {@link DirtiesContextBeforeModesTestExecutionListener},
* {@link DependencyInjectionTestExecutionListener}, and
* {@link DirtiesContextTestExecutionListener} when
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,9 +37,10 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.asser
import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache;
/**
* JUnit based integration test which verifies correct {@linkplain ContextCache
* application context caching} in conjunction with the {@link SpringExtension} and
* {@link DirtiesContext @DirtiesContext} at the method level.
* Unit tests which verify correct {@link ContextCache
* application context caching} in conjunction with the
* {@link SpringExtension} and the {@link DirtiesContext
* &#064;DirtiesContext} annotation at the method level.
*
* @author Sam Brannen
* @author Juergen Hoeller
@@ -47,7 +48,7 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.reset
* @see ContextCacheTests
* @see LruContextCacheTests
*/
@SpringJUnitConfig(locations = "../config/CoreContextConfigurationAppCtxTests-context.xml")
@SpringJUnitConfig(locations = "../junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml")
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class })
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SpringExtensionContextCacheTests {
@@ -1,54 +0,0 @@
/*
* 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.test.context.config;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
/**
* JUnit Platform based test suite annotation-driven <em>configuration class</em>
* support in the Spring TestContext Framework.
*
* <p><strong>This suite is only intended to be used manually within an IDE.</strong>
*
* <h3>Logging Configuration</h3>
*
* <p>In order for our log4j2 configuration to be used in an IDE, you must
* set the following system property before running any tests &mdash; for
* example, in <em>Run Configurations</em> in Eclipse.
*
* <pre style="code">
* -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
* </pre>
*
* @author Sam Brannen
* @since 3.1
*/
@Suite
@IncludeEngines("junit-jupiter")
@SelectPackages("org.springframework.test.context.config")
@IncludeClassNamePatterns(".*Tests$")
@ConfigurationParameter(
key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME,
value = "org.junit.jupiter.api.ClassOrderer$ClassName"
)
public class ContextConfigTestSuite {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,22 +21,21 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
import static org.springframework.test.context.cache.ContextCacheTestUtils.assertContextCacheStatistics;
import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache;
import static org.springframework.test.context.junit4.JUnitTestingUtils.runTestsAndAssertCounters;
/**
* @author Sam Brannen
@@ -71,14 +70,11 @@ class DirtiesContextInterfaceTests {
}
private void runTestClassAndAssertStats(Class<?> testClass, int expectedTestCount) throws Exception {
EngineTestKit.engine("junit-jupiter")
.selectors(selectClass(testClass))
.execute()
.testEvents()
.assertStatistics(stats -> stats.started(expectedTestCount).succeeded(expectedTestCount).failed(0));
runTestsAndAssertCounters(testClass, expectedTestCount, 0, expectedTestCount, 0, 0);
}
@ExtendWith(SpringExtension.class)
@RunWith(SpringRunner.class)
// Ensure that we do not include the EventPublishingTestExecutionListener
// since it will access the ApplicationContext for each method in the
// TestExecutionListener API, thus distorting our cache hit/miss results.
@@ -94,13 +90,13 @@ class DirtiesContextInterfaceTests {
ApplicationContext applicationContext;
@Test
@org.junit.Test
public void verifyContextWasAutowired() {
assertThat(this.applicationContext).as("The application context should have been autowired.").isNotNull();
}
@Configuration(proxyBeanMethods = false)
@Configuration
static class Config {
/* no beans */
}
@@ -1,62 +0,0 @@
/*
* 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.test.context.initializers;
import org.junit.jupiter.api.ClassOrderer;
import org.junit.platform.suite.api.ConfigurationParameter;
import org.junit.platform.suite.api.ExcludeTags;
import org.junit.platform.suite.api.IncludeClassNamePatterns;
import org.junit.platform.suite.api.IncludeEngines;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
import org.springframework.context.ApplicationContextInitializer;
/**
* JUnit Platform based test suite for tests that involve
* {@link ApplicationContextInitializer ApplicationContextInitializers} (ACIs)
* in the TestContext framework.
*
* <p><strong>This suite is only intended to be used manually within an IDE.</strong>
*
* <h3>Logging Configuration</h3>
*
* <p>In order for our log4j2 configuration to be used in an IDE, you must
* set the following system property before running any tests &mdash; for
* example, in <em>Run Configurations</em> in Eclipse.
*
* <pre style="code">
* -Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager
* </pre>
*
* @author Sam Brannen
* @since 3.2
*/
@Suite
@IncludeEngines("junit-jupiter")
@SelectPackages({
"org.springframework.test.context.initializers.annotation",
"org.springframework.test.context.initializers.xml"
})
@IncludeClassNamePatterns(".*Tests$")
@ExcludeTags("failing-test-case")
@ConfigurationParameter(
key = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME,
value = "org.junit.jupiter.api.ClassOrderer$ClassName"
)
public class AciTestSuite {
}
@@ -1,135 +0,0 @@
/*
* 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.test.context.jdbc;
import javax.sql.DataSource;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
/**
* Test suite to investigate claims raised in
* <a href="https://github.com/spring-projects/spring-framework/issues/13491">gh-13491</a>.
*
* <h3>Work Around</h3>
* <p>By using a SpEL expression to generate a random {@code database-name}
* for the embedded database (see {@code datasource-config.xml}), we ensure
* that each {@code ApplicationContext} that imports the common configuration
* will create an embedded database with a unique name.
*
* <p>To reproduce the problem mentioned in gh-13491, delete the declaration
* of the {@code database-name} attribute of the embedded database in
* {@code datasource-config.xml} and run this <em>suite</em>.
*
* <h3>Solution</h3>
* <p>As of Spring 4.2, a proper solution is possible thanks to gh-13491.
* {@link TestClass2A} and {@link TestClass2B} both import
* {@code datasource-config-with-auto-generated-db-name.xml} which makes
* use of the new {@code generate-name} attribute of {@code <jdbc:embedded-database>}.
*
* @author Sam Brannen
* @author Mickael Leduque
*/
class GeneratedDatabaseNamesTests {
private static final String DATASOURCE_CONFIG_XML =
"classpath:/org/springframework/test/context/jdbc/datasource-config.xml";
private static final String DATASOURCE_CONFIG_WITH_AUTO_GENERATED_DB_NAME_XML =
"classpath:/org/springframework/test/context/jdbc/datasource-config-with-auto-generated-db-name.xml";
@Test
void runTestsWithGeneratedDatabaseNames() {
EngineTestKit.engine("junit-jupiter")
.selectors(
selectClass(TestClass1A.class),
selectClass(TestClass1B.class),
selectClass(TestClass2A.class),
selectClass(TestClass2B.class)
)
.execute()
.testEvents()
.assertStatistics(stats -> stats.started(4).succeeded(4).failed(0));
}
@ExtendWith(SpringExtension.class)
abstract static class AbstractTestCase {
@Resource
DataSource dataSource;
@Test
void test() {
assertThat(dataSource).isNotNull();
}
}
@ContextConfiguration
static class TestClass1A extends AbstractTestCase {
@Configuration
@ImportResource(DATASOURCE_CONFIG_XML)
static class Config {
}
}
@ContextConfiguration
static class TestClass1B extends AbstractTestCase {
@Configuration
@ImportResource(DATASOURCE_CONFIG_XML)
static class Config {
}
}
/**
* @since 4.2
*/
@ContextConfiguration
static class TestClass2A extends AbstractTestCase {
@Configuration
@ImportResource(DATASOURCE_CONFIG_WITH_AUTO_GENERATED_DB_NAME_XML)
static class Config {
}
}
/**
* @since 4.2
*/
@ContextConfiguration
static class TestClass2B extends AbstractTestCase {
@Configuration
@ImportResource(DATASOURCE_CONFIG_WITH_AUTO_GENERATED_DB_NAME_XML)
static class Config {
}
}
}
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.test.context.orm.jpa;
package org.springframework.test.context.junit.jupiter.orm;
import java.util.List;
@@ -37,10 +37,10 @@ import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.test.context.jdbc.Sql;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.orm.jpa.domain.JpaPersonRepository;
import org.springframework.test.context.orm.jpa.domain.Person;
import org.springframework.test.context.orm.jpa.domain.PersonListener;
import org.springframework.test.context.orm.jpa.domain.PersonRepository;
import org.springframework.test.context.junit.jupiter.orm.domain.JpaPersonRepository;
import org.springframework.test.context.junit.jupiter.orm.domain.Person;
import org.springframework.test.context.junit.jupiter.orm.domain.PersonListener;
import org.springframework.test.context.junit.jupiter.orm.domain.PersonRepository;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.annotation.Transactional;
@@ -53,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Sam Brannen
* @since 5.3.18
* @see <a href="https://github.com/spring-projects/spring-framework/issues/28228">issue gh-28228</a>
* @see org.springframework.test.context.orm.hibernate.HibernateSessionFlushingTests
* @see org.springframework.test.context.junit4.orm.HibernateSessionFlushingTests
*/
@SpringJUnitConfig
@Transactional
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.test.context.orm.jpa.domain;
package org.springframework.test.context.junit.jupiter.orm.domain;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.test.context.orm.jpa.domain;
package org.springframework.test.context.junit.jupiter.orm.domain;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityListeners;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.test.context.orm.jpa.domain;
package org.springframework.test.context.junit.jupiter.orm.domain;
import java.util.ArrayList;
import java.util.List;
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.test.context.orm.jpa.domain;
package org.springframework.test.context.junit.jupiter.orm.domain;
/**
* Person repository API.
@@ -14,22 +14,22 @@
* limitations under the License.
*/
package org.springframework.test.context.config;
package org.springframework.test.context.junit4;
import org.springframework.test.context.ContextConfiguration;
/**
* Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that
* Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that
* we can specify an explicit, <em>absolute path</em> location for our
* application context.
*
* @author Sam Brannen
* @since 2.5
* @see CoreContextConfigurationAppCtxTests
* @see ClassPathResourceContextConfigurationAppCtxTests
* @see RelativePathContextConfigurationAppCtxTests
* @see SpringJUnit4ClassRunnerAppCtxTests
* @see ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests
* @see RelativePathSpringJUnit4ClassRunnerAppCtxTests
*/
@ContextConfiguration(locations = CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH, inheritLocations = false)
class AbsolutePathContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests {
@ContextConfiguration(locations = { SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH }, inheritLocations = false)
public class AbsolutePathSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests {
/* all tests are in the parent class. */
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2016 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.
@@ -14,24 +14,27 @@
* limitations under the License.
*/
package org.springframework.test.context.transaction;
package org.springframework.test.context.junit4;
import org.junit.runner.RunWith;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;
/**
* Abstract base class for verifying support of Spring's
* {@link Transactional @Transactional} annotation.
* Abstract base class for verifying support of Spring's {@link Transactional
* &#64;Transactional} annotation.
*
* @author Sam Brannen
* @since 2.5
* @see ClassLevelTransactionalSpringTests
* @see MethodLevelTransactionalSpringTests
* @see ClassLevelTransactionalSpringRunnerTests
* @see MethodLevelTransactionalSpringRunnerTests
* @see Transactional
*/
@SpringJUnitConfig(locations = "transactionalTests-context.xml")
abstract class AbstractTransactionalSpringTests {
@RunWith(SpringRunner.class)
@ContextConfiguration("transactionalTests-context.xml")
public abstract class AbstractTransactionalSpringRunnerTests {
protected static final String BOB = "bob";
protected static final String JANE = "jane";
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 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.
@@ -14,21 +14,22 @@
* limitations under the License.
*/
package org.springframework.test.context.transaction;
package org.springframework.test.context.junit4;
import javax.sql.DataSource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TestName;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.transaction.AfterTransaction;
import org.springframework.test.context.transaction.BeforeTransaction;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@@ -36,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.transaction.TransactionAssert.assertThatTransaction;
/**
* JUnit based integration test which verifies
* JUnit 4 based integration test which verifies
* {@link BeforeTransaction @BeforeTransaction} and
* {@link AfterTransaction @AfterTransaction} behavior.
*
@@ -44,31 +45,33 @@ import static org.springframework.test.transaction.TransactionAssert.assertThatT
* @since 2.5
*/
@Transactional
@TestInstance(Lifecycle.PER_CLASS)
class BeforeAndAfterTransactionAnnotationTests extends AbstractTransactionalSpringTests {
public class BeforeAndAfterTransactionAnnotationTests extends AbstractTransactionalSpringRunnerTests {
private static int numBeforeTransactionCalls = 0;
private static int numAfterTransactionCalls = 0;
protected static JdbcTemplate jdbcTemplate;
JdbcTemplate jdbcTemplate;
protected static int numBeforeTransactionCalls = 0;
protected static int numAfterTransactionCalls = 0;
boolean inTransaction = false;
protected boolean inTransaction = false;
@Rule
public final TestName testName = new TestName();
@Autowired
void setDataSource(DataSource dataSource) {
public void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@BeforeAll
void beforeClass() {
@BeforeClass
public static void beforeClass() {
BeforeAndAfterTransactionAnnotationTests.numBeforeTransactionCalls = 0;
BeforeAndAfterTransactionAnnotationTests.numAfterTransactionCalls = 0;
}
@AfterAll
void afterClass() {
@AfterClass
public static void afterClass() {
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the final number of rows in the person table after all tests.").isEqualTo(3);
assertThat(BeforeAndAfterTransactionAnnotationTests.numBeforeTransactionCalls).as("Verifying the total number of calls to beforeTransaction().").isEqualTo(2);
assertThat(BeforeAndAfterTransactionAnnotationTests.numAfterTransactionCalls).as("Verifying the total number of calls to afterTransaction().").isEqualTo(2);
@@ -92,27 +95,37 @@ class BeforeAndAfterTransactionAnnotationTests extends AbstractTransactionalSpri
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the number of rows in the person table after a transactional test method.").isEqualTo(0);
}
@BeforeEach
void before(TestInfo testInfo) {
assertShouldBeInTransaction(testInfo);
@Before
public void before() {
assertShouldBeInTransaction();
long expected = (this.inTransaction ? 1 : 0);
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the number of rows in the person table before a test method.").isEqualTo(expected);
}
@AfterEach
void after(TestInfo testInfo) {
assertShouldBeInTransaction(testInfo);
private void assertShouldBeInTransaction() {
boolean shouldBeInTransaction = !testName.getMethodName().equals("nonTransactionalMethod");
if (shouldBeInTransaction) {
assertThatTransaction().isActive();
}
else {
assertThatTransaction().isNotActive();
}
}
@After
public void after() {
assertShouldBeInTransaction();
}
@Test
void transactionalMethod1() {
public void transactionalMethod1() {
assertThatTransaction().isActive();
assertThat(addPerson(jdbcTemplate, JANE)).as("Adding jane").isEqualTo(1);
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the number of rows in the person table within transactionalMethod1().").isEqualTo(2);
}
@Test
void transactionalMethod2() {
public void transactionalMethod2() {
assertThatTransaction().isActive();
assertThat(addPerson(jdbcTemplate, JANE)).as("Adding jane").isEqualTo(1);
assertThat(addPerson(jdbcTemplate, SUE)).as("Adding sue").isEqualTo(1);
@@ -121,7 +134,7 @@ class BeforeAndAfterTransactionAnnotationTests extends AbstractTransactionalSpri
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
void nonTransactionalMethod() {
public void nonTransactionalMethod() {
assertThatTransaction().isNotActive();
assertThat(addPerson(jdbcTemplate, LUKE)).as("Adding luke").isEqualTo(1);
assertThat(addPerson(jdbcTemplate, LEIA)).as("Adding leia").isEqualTo(1);
@@ -129,14 +142,4 @@ class BeforeAndAfterTransactionAnnotationTests extends AbstractTransactionalSpri
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the number of rows in the person table without a transaction.").isEqualTo(3);
}
private static void assertShouldBeInTransaction(TestInfo testInfo) {
if (!testInfo.getTestMethod().get().getName().equals("nonTransactionalMethod")) {
assertThatTransaction().isActive();
}
else {
assertThatTransaction().isNotActive();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,13 @@
* limitations under the License.
*/
package org.springframework.test.context.transaction;
package org.springframework.test.context.junit4;
import javax.sql.DataSource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
@@ -31,6 +29,7 @@ import org.springframework.test.context.TestExecutionListener;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.DirtiesContextTestExecutionListener;
import org.springframework.test.context.transaction.TransactionalTestExecutionListener;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@@ -38,11 +37,12 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.transaction.TransactionAssert.assertThatTransaction;
/**
* JUnit based integration test which verifies support of Spring's
* {@link Transactional @Transactional},
* {@link TestExecutionListeners @TestExecutionListeners}, and
* {@link ContextConfiguration @ContextConfiguration} annotations in conjunction
* with the following {@link TestExecutionListener TestExecutionListeners}:
* JUnit 4 based integration test which verifies support of Spring's
* {@link Transactional &#64;Transactional}, {@link TestExecutionListeners
* &#64;TestExecutionListeners}, and {@link ContextConfiguration
* &#64;ContextConfiguration} annotations in conjunction with the
* {@link SpringRunner} and the following
* {@link TestExecutionListener TestExecutionListeners}:
*
* <ul>
* <li>{@link DependencyInjectionTestExecutionListener}</li>
@@ -55,29 +55,33 @@ import static org.springframework.test.transaction.TransactionAssert.assertThatT
*
* @author Sam Brannen
* @since 2.5
* @see MethodLevelTransactionalSpringTests
* @see MethodLevelTransactionalSpringRunnerTests
*/
@Transactional
@TestInstance(Lifecycle.PER_CLASS)
class ClassLevelTransactionalSpringTests extends AbstractTransactionalSpringTests {
public class ClassLevelTransactionalSpringRunnerTests extends AbstractTransactionalSpringRunnerTests {
private JdbcTemplate jdbcTemplate;
protected static JdbcTemplate jdbcTemplate;
@Autowired
void setDataSource(DataSource dataSource) {
public void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@BeforeEach
void verifyInitialTestData() {
@AfterClass
public static void verifyFinalTestData() {
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the final number of rows in the person table after all tests.").isEqualTo(4);
}
@Before
public void verifyInitialTestData() {
clearPersonTable(jdbcTemplate);
assertThat(addPerson(jdbcTemplate, BOB)).as("Adding bob").isEqualTo(1);
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the initial number of rows in the person table.").isEqualTo(1);
}
@Test
void modifyTestDataWithinTransaction() {
public void modifyTestDataWithinTransaction() {
assertThatTransaction().isActive();
assertThat(deletePerson(jdbcTemplate, BOB)).as("Deleting bob").isEqualTo(1);
assertThat(addPerson(jdbcTemplate, JANE)).as("Adding jane").isEqualTo(1);
@@ -87,7 +91,7 @@ class ClassLevelTransactionalSpringTests extends AbstractTransactionalSpringTest
@Test
@Transactional(propagation = Propagation.NOT_SUPPORTED)
void modifyTestDataWithoutTransaction() {
public void modifyTestDataWithoutTransaction() {
assertThatTransaction().isNotActive();
assertThat(addPerson(jdbcTemplate, LUKE)).as("Adding luke").isEqualTo(1);
assertThat(addPerson(jdbcTemplate, LEIA)).as("Adding leia").isEqualTo(1);
@@ -95,10 +99,4 @@ class ClassLevelTransactionalSpringTests extends AbstractTransactionalSpringTest
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the number of rows in the person table without a transaction.").isEqualTo(4);
}
@AfterAll
void verifyFinalTestData() {
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the final number of rows in the person table after all tests.").isEqualTo(4);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,35 +14,36 @@
* limitations under the License.
*/
package org.springframework.test.context.config;
package org.springframework.test.context.junit4;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.ResourceUtils;
/**
* Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that
* Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that
* we can specify an explicit, <em>classpath</em> location for our application
* context.
*
* @author Sam Brannen
* @since 2.5
* @see CoreContextConfigurationAppCtxTests
* @see SpringJUnit4ClassRunnerAppCtxTests
* @see #CLASSPATH_CONTEXT_RESOURCE_PATH
* @see AbsolutePathContextConfigurationAppCtxTests
* @see RelativePathContextConfigurationAppCtxTests
* @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests
* @see RelativePathSpringJUnit4ClassRunnerAppCtxTests
*/
@ContextConfiguration(locations = { ClassPathResourceContextConfigurationAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false)
class ClassPathResourceContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests {
@ContextConfiguration(locations = { ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false)
public class ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests {
/**
* Classpath-based resource path for the application context configuration
* for {@link CoreContextConfigurationAppCtxTests}: {@value}
* for {@link SpringJUnit4ClassRunnerAppCtxTests}:
* {@code &quot;classpath:/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml&quot;}
*
* @see CoreContextConfigurationAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH
* @see SpringJUnit4ClassRunnerAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH
* @see ResourceUtils#CLASSPATH_URL_PREFIX
*/
public static final String CLASSPATH_CONTEXT_RESOURCE_PATH = ResourceUtils.CLASSPATH_URL_PREFIX +
CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH;
SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH;
/* all tests are in the parent class. */
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,21 +14,19 @@
* limitations under the License.
*/
package org.springframework.test.context.transaction;
package org.springframework.test.context.junit4;
import javax.sql.DataSource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,32 +43,32 @@ import static org.springframework.test.transaction.TransactionAssert.assertThatT
* @since 4.2
* @see Rollback
* @see Transactional#transactionManager
* @see DefaultRollbackTrueRollbackAnnotationTransactionalTests
* @see DefaultRollbackFalseTransactionalTests
*/
@SpringJUnitConfig(classes = EmbeddedPersonDatabaseTestsConfig.class, inheritLocations = false)
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = EmbeddedPersonDatabaseTestsConfig.class, inheritLocations = false)
@Transactional("txMgr")
@Rollback(false)
@TestInstance(Lifecycle.PER_CLASS)
class DefaultRollbackFalseRollbackAnnotationTransactionalTests extends AbstractTransactionalSpringTests {
public class DefaultRollbackFalseRollbackAnnotationTransactionalTests extends AbstractTransactionalSpringRunnerTests {
JdbcTemplate jdbcTemplate;
private static JdbcTemplate jdbcTemplate;
@Autowired
void setDataSource(DataSource dataSource) {
public void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@BeforeEach
void verifyInitialTestData() {
@Before
public void verifyInitialTestData() {
clearPersonTable(jdbcTemplate);
assertThat(addPerson(jdbcTemplate, BOB)).as("Adding bob").isEqualTo(1);
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the initial number of rows in the person table.").isEqualTo(1);
}
@Test
void modifyTestDataWithinTransaction() {
public void modifyTestDataWithinTransaction() {
assertThatTransaction().isActive();
assertThat(deletePerson(jdbcTemplate, BOB)).as("Deleting bob").isEqualTo(1);
assertThat(addPerson(jdbcTemplate, JANE)).as("Adding jane").isEqualTo(1);
@@ -78,8 +76,8 @@ class DefaultRollbackFalseRollbackAnnotationTransactionalTests extends AbstractT
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the number of rows in the person table within a transaction.").isEqualTo(2);
}
@AfterAll
void verifyFinalTestData() {
@AfterClass
public static void verifyFinalTestData() {
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the final number of rows in the person table after all tests.").isEqualTo(2);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,21 +14,19 @@
* limitations under the License.
*/
package org.springframework.test.context.transaction;
package org.springframework.test.context.junit4;
import javax.sql.DataSource;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.Timeout;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,43 +43,42 @@ import static org.springframework.test.transaction.TransactionAssert.assertThatT
* @since 4.2
* @see Rollback
* @see Transactional#transactionManager
* @see DefaultRollbackFalseRollbackAnnotationTransactionalTests
* @see DefaultRollbackTrueTransactionalTests
*/
@SpringJUnitConfig(classes = EmbeddedPersonDatabaseTestsConfig.class, inheritLocations = false)
@RunWith(SpringRunner.class)
@ContextConfiguration(classes = EmbeddedPersonDatabaseTestsConfig.class, inheritLocations = false)
@Transactional("txMgr")
@Rollback(true)
@TestInstance(Lifecycle.PER_CLASS)
class DefaultRollbackTrueRollbackAnnotationTransactionalTests extends AbstractTransactionalSpringTests {
public class DefaultRollbackTrueRollbackAnnotationTransactionalTests extends AbstractTransactionalSpringRunnerTests {
private static int originalNumRows;
JdbcTemplate jdbcTemplate;
private static JdbcTemplate jdbcTemplate;
@Autowired
void setDataSource(DataSource dataSource) {
public void setDataSource(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@BeforeEach
void verifyInitialTestData() {
@Before
public void verifyInitialTestData() {
originalNumRows = clearPersonTable(jdbcTemplate);
assertThat(addPerson(jdbcTemplate, BOB)).as("Adding bob").isEqualTo(1);
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the initial number of rows in the person table.").isEqualTo(1);
}
@Test
@Timeout(1)
void modifyTestDataWithinTransaction() {
@Test(timeout = 1000)
public void modifyTestDataWithinTransaction() {
assertThatTransaction().isActive();
assertThat(addPerson(jdbcTemplate, JANE)).as("Adding jane").isEqualTo(1);
assertThat(addPerson(jdbcTemplate, SUE)).as("Adding sue").isEqualTo(1);
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the number of rows in the person table within a transaction.").isEqualTo(3);
}
@AfterAll
void verifyFinalTestData() {
@AfterClass
public static void verifyFinalTestData() {
assertThat(countRowsInPersonTable(jdbcTemplate)).as("Verifying the final number of rows in the person table after all tests.").isEqualTo(originalNumRows);
}

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