Compare commits

...

79 Commits

Author SHA1 Message Date
Brian Clozel ba590ac9e4 Release v6.2.7 2025-05-15 10:35:00 +02:00
rstoyanchev ee62701f56 Make use of PatternMatchUtils ignoreCase option
Closes gh-34801
2025-05-15 08:18:56 +01:00
Juergen Hoeller fa168ca78a Revise FactoryBean locking behavior for strict/lenient consistency
After the bootstrap phase (and with spring.locking.strict=true during the bootstrap phase), getSingletonFactoryBeanForTypeCheck always locks. In a background bootstrap thread, it never locks. Otherwise, it tries locking and explicitly resolves the bean class for subsequent type-based resolution (even for a component-scanned class) when it fails to acquire the lock. Furthermore, getObjectFromFactoryBean follows the same locking algorithm for post-processing.

Closes gh-34902
2025-05-15 01:45:09 +02:00
rstoyanchev 3c228a5c1d Add missing @since tags in PatternMatchUtils
See: gh-34801
2025-05-14 15:15:50 +01:00
Juergen Hoeller 9bf6b8cddf Upgrade to Reactor 2024.0.6
Closes gh-34898
2025-05-14 15:03:28 +02:00
Patrick Strawderman 37ecdd1437 Forward more methods to underlying InputStream in NonClosingInputStream
NonClosingInputStream extends FilterInputStream, which does not forward some
newer InputStream methods such as transferTo and readAllBytes. Specific InputStream
implementations may have more optimized methods (e.g., FileInputStream).

Closes gh-34893

Signed-off-by: Patrick Strawderman <pstrawderman@netflix.com>
2025-05-14 09:58:23 +02:00
Juergen Hoeller 73f1c5a189 Polishing 2025-05-13 16:08:57 +02:00
Juergen Hoeller 4d296fb4ca Upgrade to Micrometer 1.14.7
Includes Jackson 2.18.4, Jetty 12.0.21, Netty 4.1.121, Apache HttpClient 5.4.4, Checkstyle 10.23.1

Closes gh-34889
2025-05-13 16:08:31 +02:00
Juergen Hoeller 6a9444473f Clarify CompositePropertySource behavior for EnumerablePropertySource contract
Closes gh-34886
2025-05-13 16:08:03 +02:00
Sam Brannen 03ae97b2eb Introduce Spring property for default escape character for placeholders
Spring Framework 6.2 introduced support for an escape character for
property placeholders (by default '\'). However, as of Spring Framework
6.2.6, there was no way to either escape the escape character or disable
escape character support.

For example, given a `username` property configured with the value of
`Jane.Smith` and a `DOMAIN\${username}` configuration string, property
placeholder replacement used to result in `DOMAIN\Jane.Smith` prior to
6.2 but now results in `DOMAIN${username}`. Similarly, an attempt to
escape the escape character via `DOMAIN\\${username}` results in
`DOMAIN\${username}`.

In theory, one should be able to disable use of an escape character
altogether, and that is currently possible by invoking
setEscapeCharacter(null) on AbstractPropertyResolver and
PlaceholderConfigurerSupport (the superclass of
PropertySourcesPlaceholderConfigurer).

However, in reality, there are two hurdles.

- As of 6.2.6, an invocation of setEscapeCharacter(null) on a
  PropertySourcesPlaceholderConfigurer applied to its internal
  top-level PropertySourcesPropertyResolver but not to any nested
  PropertySourcesPropertyResolver, which means that the `null` escape
  character could not be effectively applied.

- Users may not have an easy way to explicitly set the escape character
  to `null` for a PropertyResolver or
  PropertySourcesPlaceholderConfigurer. For example, Spring Boot
  auto-configures a PropertySourcesPlaceholderConfigurer with the
  default escape character enabled.

This first issue above has recently been addressed by gh-34861.

This commit therefore addresses the second issue as follows.

- To allow developers to easily revert to the pre-6.2 behavior without
  changes to code or configuration strings, this commit introduces a
  `spring.placeholder.escapeCharacter.default` property for use with
  SpringProperties which globally sets the default escape character that
  is automatically configured in AbstractPropertyResolver and
  PlaceholderConfigurerSupport.

- Setting the property to an empty string sets the default escape
  character to `null`, effectively disabling the default support for
  escape characters.

    spring.placeholder.escapeCharacter.default =

- Setting the property to any other character sets the default escape
  character to that specific character.

    spring.placeholder.escapeCharacter.default = ~

- Setting the property to a string containing more than one character
  results in an exception.

- Developers are still able to configure an explicit escape character
  in AbstractPropertyResolver and PlaceholderConfigurerSupport if they
  choose to do so.

- Third-party components that wish to rely on the same feature can
  invoke AbstractPropertyResolver.getDefaultEscapeCharacter() to obtain
  the globally configured default escape character.

See gh-9628
See gh-34315
See gh-34861
Closes gh-34865
2025-05-13 13:37:30 +02:00
Sam Brannen e34cdc2a55 Improve Javadoc for property placeholder support 2025-05-12 17:25:49 +02:00
Sam Brannen 8b1b9ef9e5 Stop redefining placeholder syntax constants in PlaceholderConfigurerSupport 2025-05-12 17:21:37 +02:00
Sam Brannen efdaae02e0 Add test for late binding from Environment property sources
This new test serves as a "regression test" for behavior tested in
Spring Boot.

See gh-34861
2025-05-11 16:41:19 +02:00
Sam Brannen 49ffb833a3 Sort methods to align with PropertySource declaration order
See gh-34861
2025-05-11 15:48:00 +02:00
Sam Brannen 065e50a444 Override containsProperty() in FallbackEnvironmentPropertySource
This commit overrides containsProperty() in
FallbackEnvironmentPropertySource for consistency with the
implementation of ConfigurableEnvironmentPropertySource.

See gh-34861
2025-05-11 15:44:51 +02:00
Sam Brannen 7135527765 Always obtain fresh PropertySources in ConfigurableEnvironmentPropertySource
Although it's unlikely that the implementation of getPropertySources()
in a ConfigurableEnvironment would be overridden to return a different
MutablePropertySources instance than the one that the
ConfigurableEnvironment typically acts on, it is in fact possible.

In light of that possibility, this commit refactors
ConfigurableEnvironmentPropertySource so that it always obtains a fresh
PropertySources reference.

See gh-34861
2025-05-11 15:30:09 +02:00
Sam Brannen 65e30132f3 Revert "Introduce CompositePropertySource constructor that accepts Iterable<PropertySource>"
This reverts commit 0867dfca33.
2025-05-11 14:10:54 +02:00
Sam Brannen ebb44a8368 Restore support for non-EnumerablePropertySource in PropertySourcesPlaceholderConfigurer
Commit 3295289e17 fixed a number issues with placeholder resolution in
PropertySourcesPlaceholderConfigurer. However, in doing so, it replaced
a raw PropertySource with a CompositePropertySource which implements
EnumerablePropertySource.

Consequently, all property sources registered in the Environment must
now implement EnumerablePropertySource (which is not an actual
requirement). Otherwise, invocations of getPropertyNames() on the
CompositePropertySource result in an IllegalStateException, and that is
a breaking change which resulted in numerous build failures within the
Spring portfolio.

To address that regression, this commit introduces a private
ConfigurableEnvironmentPropertySource in
PropertySourcesPlaceholderConfigurer which is a "raw" PropertySource
that delegates directly to the PropertySources in a
ConfigurableEnvironment.

This commit also extracts the raw PropertySource for direct Environment
delegation into a new FallbackEnvironmentPropertySource.

See gh-17385
Closes gh-34861
2025-05-11 13:46:11 +02:00
Sam Brannen 3096bb6d0c Polishing 2025-05-11 13:24:40 +02:00
Sam Brannen 2b2a57b4a3 Polish Javadoc 2025-05-10 15:44:08 +02:00
Sam Brannen 3295289e17 Fix placeholder resolution in PropertySourcesPlaceholderConfigurer
Currently, the placeholder resolution algorithm in
PropertySourcesPlaceholderConfigurer fails in several scenarios, and
the root cause for this category of failures has actually existed since
PropertySourcesPlaceholderConfigurer was introduced in Spring Framework
3.1.

Specifically, PropertySourcesPlaceholderConfigurer creates its own
PropertySourcesPropertyResolver that indirectly delegates to another
"nested" PropertySourcesPropertyResolver to interact with
PropertySources from the Environment, which results in double
placeholder parsing and resolution attempts, and that behavior leads to
a whole category of bugs.

For example, #27947 was addressed in Spring Framework 5.3.16, and due
to #34315 and #34326 we have recently realized that additional bugs
exist with placeholder resolution: nested placeholder resolution can
fail when escape characters are used, and it is currently impossible
to disable the escape character support for nested resolution.

To address this category of bugs, we no longer indirectly use or
directly create a "nested" PropertySourcesPropertyResolver in
PropertySourcesPlaceholderConfigurer. Instead, properties from property
sources from the Environment are now accessed directly without
duplicate/nested placeholder resolution.

See gh-27947
See gh-34326
See gh-34862
Closes gh-34861
2025-05-10 15:14:50 +02:00
Sam Brannen 457e876303 Polish reference manual regarding placeholders 2025-05-10 15:10:29 +02:00
Sam Brannen 021bf6e77d Add more tests for escape character support in PlaceholderParser 2025-05-10 14:19:41 +02:00
Sam Brannen 90453643cc Simplify and revise PlaceholderParserTests for consistency 2025-05-10 14:16:49 +02:00
Sam Brannen 5a2cbc1ab3 Polish PropertySourcesPropertyResolverTests 2025-05-09 16:43:04 +02:00
Sam Brannen 348b4cd067 Polish contribution
See gh-34720
2025-05-09 16:32:13 +02:00
Fawzi Essam 9b52cfd7d5 Test escape character support in PropertySourcesPropertyResolver
See gh-34326
See gh-34720
See gh-34861

Signed-off-by: Fawzi Essam <iifawzie@gmail.com>
2025-05-09 16:31:27 +02:00
Sam Brannen dd6eede243 Properly expand reused collection parameters in R2DBC NamedParameterUtils
Prior to this commit, NamedParameterUtils in spring-r2dbc did not
properly expand reused collection parameters. Specifically, values in a
supplied collection were only expanded in the resulting query once, for
the first occurrence of the named parameter.

To address that, this commit effectively reinstates the original logic
for ExpandedQuery from NamedParameterUtils in the Spring Data R2DBC
project.

https://github.com/spring-projects/spring-data-relational/blob/94958f5eb66cbe2e8e025155cd99abf36f6f91f4/spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/core/NamedParameterUtils.java#L486

Closes gh-34768
2025-05-09 14:27:19 +02:00
Sam Brannen de97e35189 Polish Javadoc and internals of R2DBC support 2025-05-09 14:04:10 +02:00
Sam Brannen 45a2c51fe1 Polish Javadoc for Environment-related components 2025-05-08 10:49:44 +02:00
Sam Brannen b98c3257af Introduce @⁠Disabled failing test for R2DBC NamedParameterUtils
The last assertion of the new test method currently fails since "foo"
is only bound once.

java.lang.AssertionError:
Expected size: 2 but was: 1 in:
{0="foo"}

See gh-34768
2025-05-07 17:30:07 +02:00
Sam Brannen ea8ae09cb7 Revise NamedParameterUtilsTests for consistency 2025-05-07 17:24:41 +02:00
Sam Brannen 0abfad870c Polish class-level Javadoc for SpringProperties 2025-05-07 16:14:23 +02:00
Sam Brannen 5a9af9e024 Document AotDetector.AOT_ENABLED flag in SpringProperties
Prior to this commit, the AotDetector.AOT_ENABLED flag was documented
in the reference manual not in the Javadoc for SpringProperties.
2025-05-07 16:14:07 +02:00
Sam Brannen 0867dfca33 Introduce CompositePropertySource constructor that accepts Iterable<PropertySource>
This commit introduces a new constructor for CompositePropertySource
that accepts a `String name` and an Iterable<PropertySource<?>>, which
allows a CompositePropertySource to be constructed from existing
property sources, such as an instance of MutablePropertySources.

Closes gh-34862
2025-05-07 14:25:13 +02:00
Sam Brannen b4355dc955 Polishing 2025-05-07 12:42:40 +02:00
Sam Brannen bc91e0ea96 Revise PropertyResolver Javadoc to highlight resolution semantics 2025-05-06 18:50:33 +02:00
Sam Brannen 8599ee6c2b Polish Javadoc 2025-05-06 16:07:46 +02:00
Sam Brannen bc466022b1 Fix typo in parameter name 2025-05-06 16:07:46 +02:00
Brian Clozel 6f11711e27 Fix HttpClient 5.3.x request config compatibility
As of gh-33806, the HttpComponents client request factory is forward
compatible with the 5.4+ versions of that library for configuring HTTP
request configuration.

This change would not tkae into account configuration set at the Spring
level because it would consider the default `RequestConfig` instance as
a custom one. This commit ensures that Spring sets its own configuration
if no custom configuration was set for all supported HttpComponents
generations.

Fixes gh-34851
2025-05-05 14:38:30 +02:00
rstoyanchev c067919173 Ensure Fragment can merge attributes
Use a new map when merging as the original may be immutable.

Closes gh-34848
2025-05-02 15:58:51 +01:00
rstoyanchev ac773d97e9 Polishing contribution
Closes gh-34828
2025-05-02 15:58:51 +01:00
Artur bd7007227c Provide a working example instead of unclear placeholders
See gh-34828

Signed-off-by: Artur <artur@vaadin.com>
2025-05-02 15:58:51 +01:00
Sam Brannen e8f873a349 Ensure Bean Overrides are discovered once in hierarchies
Prior to this commit, bean overrides (such as @⁠MockitoBean, etc.) were
discovered multiple times if they were declared:

- at the type-level on an interface that is implemented at more than
  one level in the type hierarchy, the enclosing class hierarchy, or a
  combination of the type and enclosing class hierarchies.

or

- on a field declared in a class which can be reached multiple times
  while traversing the type and enclosing class hierarchies in
  scenarios such as the following: the class (X) in which the field is
  declared is a supertype of an enclosing type of the test class, and X
  is also an enclosing type of a supertype of the test class.

Such scenarios resulted in an IllegalStateException stating that a
duplicate BeanOverrideHandler was discovered.

To address that, this commit revises the search algorithm in
BeanOverrideHandler so that all types (superclasses, enclosing classes,
and implemented interfaces) are only visited once while traversing the
type and enclosing class hierarchies in search of bean override
handlers.

See gh-33925
See gh-34324
Closes gh-34844
2025-05-02 11:26:33 +02:00
Sam Brannen b943817f3e Close ApplicationContext after test AOT processing
See commit 1c108054ee
Closes gh-34841
2025-05-01 10:50:38 +02:00
Juergen Hoeller 4466548f53 Align parameter javadoc with nullable signature
Closes gh-34845
2025-04-30 17:55:39 +02:00
Juergen Hoeller 9c183f9e77 Add explicit note on redeclaring in each application context
Closes gh-34843
2025-04-30 17:55:18 +02:00
Juergen Hoeller 03620fc530 Polishing 2025-04-29 11:47:47 +02:00
Juergen Hoeller 1c108054ee Close ApplicationContext after AOT processing
Closes gh-34841
2025-04-29 11:47:09 +02:00
Juergen Hoeller d0b186a1c7 Polishing 2025-04-28 16:13:04 +02:00
Juergen Hoeller 4172581f1b Try loadClass on LinkageError in case of same ClassLoader as well
Closes gh-34824
2025-04-28 16:12:45 +02:00
rstoyanchev c88ba6c90e Polishing contribution
Closes gh-34812
2025-04-28 14:23:30 +01:00
Yanming Zhou d7c13d6518 HttpEntity.EMPTY should be immutable
See gh-34812

Signed-off-by: Yanming Zhou <zhouyanming@gmail.com>
2025-04-28 14:22:59 +01:00
Sam Brannen 5013d6d771 Fail Gradle build whenever a javadoc task fails
To ensure that failures in javadoc tasks do not result in documentation
silently not being generated/published, this commit sets
`failOnError = true` for all javadoc tasks.

See gh-27497
See gh-34774
Closes gh-34837
2025-04-27 17:59:41 +02:00
Sam Brannen 9cd7b6a91e Upgrade spring-javaformat-gradle-plugin to version 0.0.43 2025-04-27 16:51:47 +02:00
Sam Brannen e9dcd64068 Upgrade to Gradle 8.14
Closes gh-34836
2025-04-27 16:29:46 +02:00
Sam Brannen ce7f47c962 Upgrade io.freefair.aggregate-javadoc plugin to version 8.13.1 2025-04-27 16:23:22 +02:00
Sam Brannen ef34464c94 Restructure TestContext framework support sections
This commit moves the JUnit Jupiter section above the JUnit 4 section
and groups all JUnit 4 sections under a new "JUnit 4 Support" heading.
2025-04-26 09:38:10 +02:00
Sam Brannen ef11a00c0b Polishing 2025-04-26 09:37:34 +02:00
Sam Brannen 176b0b09bf Migrate remaining JUnit 4 tests to JUnit Jupiter where feasible
In 49e5c84928 I unfortunately overlooked
several JUnit 4 based tests in the `junit4` package that should be
migrated to JUnit Jupiter.

This commit address those remaining test classes.

See gh-23451
See gh-34794
Closes gh-34813
2025-04-26 07:51:24 +02:00
rstoyanchev c48ff357dc HTTP Service proxy sets body type
Closes gh-34793
2025-04-25 21:03:00 +01:00
rstoyanchev 190dabb8e1 Polishing contribution
Closes gh-34789
2025-04-25 21:03:00 +01:00
blake_bauman d15abd58b4 Add option to set Principal in MockServerWebExchange
See gh-34789

Signed-off-by: blake_bauman <blake_bauman@apple.com>
2025-04-25 21:03:00 +01:00
Sam Brannen 44500cf868 Revise TestConventions to retain existing JUnit Platform options
This commit revises the implementation of TestConventions so that
existing JUnit Platform options from a pre-configured `test` task are
copied instead of overridden.

Closes gh-34827
2025-04-25 13:38:18 +02:00
Sam Brannen e384389790 Reinstate the @⁠Inject Technology Compatibility Kit (TCK)
In commit 05ebca8677, the `public` modifier was removed from the
SpringAtInjectTckTests class, which prevents it from being run as a
JUnit 3 test class.

To address that, this commit adds the missing `public` modifier as well
as a a code comment to help prevent this from happening again.

In addition, this commit updates spring-context.gradle to ensure that
the JUnit Vintage test engine is always applied. However, that Gradle
configuration is unfortunately ignored due to how our TestConventions
class has been implemented. Thus, that issue will have to be addressed
separately.

Closes gh-34800
2025-04-25 12:08:39 +02:00
Sam Brannen 49e5c84928 Migrate remaining JUnit 4 tests to JUnit Jupiter where feasible
In Spring Framework 5.2, we migrated most of the test suite from JUnit
4 to JUnit Jupiter; however, prior to this commit, several tests in the
spring-test module were still based on JUnit 4 unnecessarily.

Since we are now planning to deprecate our JUnit 4 support in 7.0, this
commit migrates our remaining JUnit 4 based tests to JUnit Jupiter
whenever feasible. In the process, test classes that previously resided
under the "junit4" package have been moved to new packages directly
under the "org.springframework.text.context" package, and several
classes have been renamed for greater clarity of purpose.

Consequently, the only remaining tests based on JUnit 4 are those tests
that are required to run with JUnit 4 in order to test our JUnit 4
support.

This commit also greatly simplifies exclusions for Checkstyle rules
pertaining to JUnit usage.

See gh-23451
See gh-34794
Closes gh-34813
2025-04-24 16:20:39 +02:00
Sébastien Deleuze 56eb135608 Fix AbstractJackson2HttpMessageConverter nullness
This commit makes AbstractJackson2HttpMessageConverter#getObjectMappersForType
return value non nullable as an empty map is returned in case of no
registrations.

Closes gh-34811
2025-04-24 10:37:30 +02:00
rstoyanchev 5c5cf73e11 Add ignoreCase variants to PatternMatchUtils
See gh-34801
2025-04-23 12:10:55 +01:00
rstoyanchev 858c2bd270 Polishing contribution
Closes gh-34783
2025-04-23 10:54:52 +01:00
whl 124582d910 Fix expansion of query param with same name
See gh-34783

Signed-off-by: whl <whlit.cola@gmail.com>
2025-04-23 10:54:52 +01:00
Juergen Hoeller 253f321e8b Early getJarFile() call for consistent jar file existence check
See gh-34796
2025-04-23 10:16:12 +02:00
Juergen Hoeller 0252e39409 Check for the existence of any actual jar entries in case of jar root
Closes gh-34796
2025-04-22 23:18:56 +02:00
Sam Brannen 2f60083cd5 Add integration tests for reused named parameters from bean properties
See gh-34768
2025-04-22 12:45:35 +02:00
Juergen Hoeller b83e07ff8c Ignore NoSuchFileException from getJarFile() as well
Closes gh-34764
2025-04-22 12:17:18 +02:00
Juergen Hoeller 1841ad3472 Consistently use local copy of volatile mainThreadPrefix field
Closes gh-34746
2025-04-22 12:17:03 +02:00
Sam Brannen 0477ba4de4 Remove obsolete RmiInvocationWrapperRTD.xml file
Closes gh-34779

(cherry picked from commit e7402bc365)
2025-04-22 12:10:18 +02:00
Sam Brannen 8c376e9cc5 Remove redundant parameter count check in AnnotationsScanner.hasSameParameterTypes()
The redundancy was reported by @TAKETODAY.

See gh-34717
2025-04-22 11:53:40 +02:00
Sam Brannen 018d3c9ef2 Add integration tests for reused named parameters
See gh-34768
2025-04-17 15:48:24 +02:00
Brian Clozel c70741f60d Next development version (v6.2.7-SNAPSHOT) 2025-04-17 09:50:20 +02:00
260 changed files with 4265 additions and 3018 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
org.gradle.caching=true
javaFormatVersion=0.0.42
javaFormatVersion=0.0.43
@@ -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.0");
checkstyle.setToolVersion("10.23.1");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,8 @@ import 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;
@@ -34,6 +36,7 @@ import org.gradle.testretry.TestRetryTaskExtension;
*
* @author Brian Clozel
* @author Andy Wilkinson
* @author Sam Brannen
*/
class TestConventions {
@@ -50,7 +53,12 @@ class TestConventions {
}
private void configureTests(Project project, Test test) {
test.useJUnitPlatform();
TestFrameworkOptions existingOptions = test.getOptions();
test.useJUnitPlatform(options -> {
if (existingOptions instanceof JUnitPlatformOptions junitPlatformOptions) {
options.copyFrom(junitPlatformOptions);
}
});
test.include("**/*Tests.class", "**/*Test.class");
test.setSystemProperties(Map.of(
"java.awt.headless", "true",
+2 -1
View File
@@ -1,6 +1,6 @@
plugins {
id 'java-platform'
id 'io.freefair.aggregate-javadoc' version '8.3'
id 'io.freefair.aggregate-javadoc' version '8.13.1'
}
description = "Spring Framework API Docs"
@@ -21,6 +21,7 @@ dependencies {
javadoc {
title = "${rootProject.description} ${version} API"
failOnError = true
options {
encoding = "UTF-8"
memberLevel = JavadocMemberLevel.PROTECTED
@@ -103,6 +103,14 @@ for details.
{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
for details.
| `spring.placeholder.escapeCharacter.default`
| The default escape character for property placeholder support. If not set, `'\'` will
be used. Can be set to a custom escape character or an empty string to disable support
for an escape character. The default escape character be explicitly overridden in
`PropertySourcesPlaceholderConfigurer` and subclasses of `AbstractPropertyResolver`. See
{spring-framework-api}++/core/env/AbstractPropertyResolver.html#DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME++[`AbstractPropertyResolver`]
for details.
| `spring.test.aot.processing.failOnError`
| A boolean flag that controls whether errors encountered during AOT processing in the
_Spring TestContext Framework_ should result in an exception that fails the overall process.
@@ -101,8 +101,11 @@ NOTE: When configuring a `PropertySourcesPlaceholderConfigurer` using JavaConfig
Using the above configuration ensures Spring initialization failure if any `${}`
placeholder could not be resolved. It is also possible to use methods like
`setPlaceholderPrefix`, `setPlaceholderSuffix`, `setValueSeparator`, or
`setEscapeCharacter` to customize placeholders.
`setPlaceholderPrefix()`, `setPlaceholderSuffix()`, `setValueSeparator()`, or
`setEscapeCharacter()` to customize the placeholder syntax. In addition, the default
escape character can be changed or disabled globally by setting the
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
NOTE: Spring Boot configures by default a `PropertySourcesPlaceholderConfigurer` bean that
will get properties from `application.properties` and `application.yml` files.
@@ -314,7 +314,7 @@ Thus, marking it for lazy initialization will be ignored, and the
[[beans-factory-placeholderconfigurer]]
=== Example: The Class Name Substitution `PropertySourcesPlaceholderConfigurer`
=== Example: Property Placeholder Substitution with `PropertySourcesPlaceholderConfigurer`
You can use the `PropertySourcesPlaceholderConfigurer` to externalize property values
from a bean definition in a separate file by using the standard Java `Properties` format.
@@ -341,8 +341,8 @@ with placeholder values is defined:
The example shows properties configured from an external `Properties` file. At runtime,
a `PropertySourcesPlaceholderConfigurer` is applied to the metadata that replaces some
properties of the DataSource. The values to replace are specified as placeholders of the
form pass:q[`${property-name}`], which follows the Ant and log4j and JSP EL style.
properties of the `DataSource`. The values to replace are specified as placeholders of the
form pass:q[`${property-name}`], which follows the Ant, log4j, and JSP EL style.
The actual values come from another file in the standard Java `Properties` format:
@@ -355,11 +355,15 @@ jdbc.password=root
----
Therefore, the `${jdbc.username}` string is replaced at runtime with the value, 'sa', and
the same applies for other placeholder values that match keys in the properties file.
The `PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
attributes of a bean definition. Furthermore, you can customize the placeholder prefix and suffix.
the same applies for other placeholder values that match keys in the properties file. The
`PropertySourcesPlaceholderConfigurer` checks for placeholders in most properties and
attributes of a bean definition. Furthermore, you can customize the placeholder prefix,
suffix, default value separator, and escape character. In addition, the default escape
character can be changed or disabled globally by setting the
`spring.placeholder.escapeCharacter.default` property via a JVM system property (or via
the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
With the `context` namespace introduced in Spring 2.5, you can configure property placeholders
With the `context` namespace, you can configure property placeholders
with a dedicated configuration element. You can provide one or more locations as a
comma-separated list in the `location` attribute, as the following example shows:
@@ -190,7 +190,7 @@ NOTE: If you use Spring Boot, you should probably use
instead of `@Value` annotations.
As an alternative, you can customize the property placeholder prefix by declaring the
following configuration beans:
following `PropertySourcesPlaceholderConfigurer` bean:
[source,kotlin,indent=0]
----
@@ -200,8 +200,10 @@ following configuration beans:
}
----
You can customize existing code (such as Spring Boot actuators or `@LocalServerPort`)
that uses the `${...}` syntax, with configuration beans, as the following example shows:
You can support components (such as Spring Boot actuators or `@LocalServerPort`) that use
the standard `${...}` syntax alongside components that use the custom `%{...}` syntax by
declaring multiple `PropertySourcesPlaceholderConfigurer` beans, as the following example
shows:
[source,kotlin,indent=0]
----
@@ -215,6 +217,9 @@ that uses the `${...}` syntax, with configuration beans, as the following exampl
fun defaultPropertyConfigurer() = PropertySourcesPlaceholderConfigurer()
----
In addition, the default escape character can be changed or disabled globally by setting
the `spring.placeholder.escapeCharacter.default` property via a JVM system property (or
via the xref:appendix.adoc#appendix-spring-properties[`SpringProperties`] mechanism).
[[checked-exceptions]]
@@ -2,8 +2,9 @@
= Spring JUnit 4 Testing Annotations
The following annotations are supported only when used in conjunction with the
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner], xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules]
, or xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]:
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-runner[SpringRunner],
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit 4 rules], or
xref:testing/testcontext-framework/support-classes.adoc#testcontext-support-classes-junit4[Spring's JUnit 4 support classes]:
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-ifprofilevalue[`@IfProfileValue`]
* xref:testing/annotations/integration-junit4.adoc#integration-testing-annotations-junit4-profilevaluesourceconfiguration[`@ProfileValueSourceConfiguration`]
@@ -1,166 +1,9 @@
[[testcontext-support-classes]]
= TestContext Framework Support Classes
This section describes the various classes that support the Spring TestContext Framework.
This section describes the various classes that support the Spring TestContext Framework
in JUnit and TestNG.
[[testcontext-junit4-runner]]
== Spring JUnit 4 Runner
The Spring TestContext Framework offers full integration with JUnit 4 through a custom
runner (supported on JUnit 4.12 or higher). By annotating test classes with
`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
variant, developers can implement standard JUnit 4-based unit and integration tests and
simultaneously reap the benefits of the TestContext framework, such as support for
loading application contexts, dependency injection of test instances, transactional test
method execution, and so on. If you want to use the Spring TestContext Framework with an
alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
(such as the `MockitoJUnitRunner`), you can, optionally, use
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules] instead.
The following code listing shows the minimal requirements for configuring a test class to
run with the custom Spring `Runner`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {
@Test
fun testMethod() {
// test logic...
}
}
----
======
In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
disable the default listeners, which otherwise would require an `ApplicationContext` to
be configured through `@ContextConfiguration`.
[[testcontext-junit4-rules]]
== Spring JUnit 4 Rules
The `org.springframework.test.context.junit4.rules` package provides the following JUnit
4 rules (supported on JUnit 4.12 or higher):
* `SpringClassRule`
* `SpringMethodRule`
`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
instance-level and method-level features of the Spring TestContext Framework.
In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
third-party runners (such as the `MockitoJUnitRunner`).
To support the full functionality of the TestContext framework, you must combine a
`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
to declare these rules in an integration test:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {
@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
class IntegrationTest {
@Rule
val springMethodRule = SpringMethodRule()
@Test
fun testMethod() {
// test logic...
}
companion object {
@ClassRule
val springClassRule = SpringClassRule()
}
}
----
======
[[testcontext-support-classes-junit4]]
== JUnit 4 Support Classes
The `org.springframework.test.context.junit4` package provides the following support
classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
* `AbstractJUnit4SpringContextTests`
* `AbstractTransactionalJUnit4SpringContextTests`
`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
Spring TestContext Framework with explicit `ApplicationContext` testing support in a
JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
`protected` `applicationContext` instance variable that you can use to perform explicit
bean lookups or to test the state of the context as a whole.
`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
access. This class expects a `javax.sql.DataSource` bean and a
`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
`jdbcTemplate` instance variable that you can use to run SQL statements to query the
database. You can use such queries to confirm database state both before and after
running database-related application code, and Spring ensures that such queries run in
the scope of the same transaction as the application code. When used in conjunction with
an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
TIP: These classes are a convenience for extension. If you do not want your test classes
to be tied to a Spring-specific class hierarchy, you can configure your own custom test
classes by using `@RunWith(SpringRunner.class)` or xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules]
.
[[testcontext-junit-jupiter-extension]]
== SpringExtension for JUnit Jupiter
@@ -177,14 +20,17 @@ following features above and beyond the feature set that Spring supports for JUn
TestNG:
* Dependency injection for test constructors, test methods, and test lifecycle callback
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency Injection with the `SpringExtension`] for further details.
methods. See xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit-jupiter-di[Dependency
Injection with the `SpringExtension`] for further details.
* Powerful support for link:https://junit.org/junit5/docs/current/user-guide/#extensions-conditions[conditional
test execution] based on SpEL expressions, environment variables, system properties,
and so on. See the documentation for `@EnabledIf` and `@DisabledIf` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details and examples.
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
for further details and examples.
* Custom composed annotations that combine annotations from Spring and JUnit Jupiter. See
the `@TransactionalDevTestConfig` and `@TransactionalIntegrationTest` examples in
xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for further details.
xref:testing/annotations/integration-meta.adoc[Meta-Annotation Support for Testing] for
further details.
The following code listing shows how to configure a test class to use the
`SpringExtension` in conjunction with `@ContextConfiguration`:
@@ -307,7 +153,8 @@ Kotlin::
======
See the documentation for `@SpringJUnitConfig` and `@SpringJUnitWebConfig` in
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations] for further details.
xref:testing/annotations/integration-junit-jupiter.adoc[Spring JUnit Jupiter Testing Annotations]
for further details.
[[testcontext-junit-jupiter-di]]
=== Dependency Injection with the `SpringExtension`
@@ -318,10 +165,9 @@ extension API from JUnit Jupiter, which lets Spring provide dependency injection
constructors, test methods, and test lifecycle callback methods.
Specifically, the `SpringExtension` can inject dependencies from the test's
`ApplicationContext` into test constructors and methods that are annotated with
Spring's `@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`,
`@AfterAll`, `@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`,
and others.
`ApplicationContext` into test constructors and methods that are annotated with Spring's
`@BeforeTransaction` and `@AfterTransaction` or JUnit's `@BeforeAll`, `@AfterAll`,
`@BeforeEach`, `@AfterEach`, `@Test`, `@RepeatedTest`, `@ParameterizedTest`, and others.
[[testcontext-junit-jupiter-di-constructor]]
@@ -341,8 +187,9 @@ autowirable if one of the following conditions is met (in order of precedence).
attribute set to `ALL`.
* The default _test constructor autowire mode_ has been changed to `ALL`.
See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`] for details on the use of
`@TestConstructor` and how to change the global _test constructor autowire mode_.
See xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]
for details on the use of `@TestConstructor` and how to change the global _test
constructor autowire mode_.
WARNING: If the constructor for a test class is considered to be _autowirable_, Spring
assumes the responsibility for resolving arguments for all parameters in the constructor.
@@ -407,8 +254,9 @@ Kotlin::
Note that this feature lets test dependencies be `final` and therefore immutable.
If the `spring.test.constructor.autowire.mode` property is to `all` (see
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]), we can omit the declaration of
`@Autowired` on the constructor in the previous example, resulting in the following.
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-testconstructor[`@TestConstructor`]),
we can omit the declaration of `@Autowired` on the constructor in the previous example,
resulting in the following.
[tabs]
======
@@ -553,17 +401,19 @@ honor `@NestedTestConfiguration` semantics.
In order to allow development teams to change the default to `OVERRIDE` for example,
for compatibility with Spring Framework 5.0 through 5.2 the default mode can be changed
globally via a JVM system property or a `spring.properties` file in the root of the
classpath. See the xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"]
note for details.
classpath. See the
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration["Changing the default enclosing configuration inheritance mode"]
note for details.
Although the following "Hello World" example is very simplistic, it shows how to declare
common configuration on a top-level class that is inherited by its `@Nested` test
classes. In this particular example, only the `TestConfig` configuration class is
inherited. Each nested test class provides its own set of active profiles, resulting in a
distinct `ApplicationContext` for each nested test class (see
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details). Consult the list of
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations] to see
which annotations can be inherited in `@Nested` test classes.
xref:testing/testcontext-framework/ctx-management/caching.adoc[Context Caching] for details).
Consult the list of
xref:testing/annotations/integration-junit-jupiter.adoc#integration-testing-annotations-nestedtestconfiguration[supported annotations]
to see which annotations can be inherited in `@Nested` test classes.
[tabs]
======
@@ -626,8 +476,174 @@ Kotlin::
----
======
[[testcontext-junit4-support]]
== JUnit 4 Support
[[testcontext-junit4-runner]]
=== Spring JUnit 4 Runner
The Spring TestContext Framework offers full integration with JUnit 4 through a custom
runner (supported on JUnit 4.12 or higher). By annotating test classes with
`@RunWith(SpringJUnit4ClassRunner.class)` or the shorter `@RunWith(SpringRunner.class)`
variant, developers can implement standard JUnit 4-based unit and integration tests and
simultaneously reap the benefits of the TestContext framework, such as support for
loading application contexts, dependency injection of test instances, transactional test
method execution, and so on. If you want to use the Spring TestContext Framework with an
alternative runner (such as JUnit 4's `Parameterized` runner) or third-party runners
(such as the `MockitoJUnitRunner`), you can, optionally, use
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's support for JUnit rules]
instead.
The following code listing shows the minimal requirements for configuring a test class to
run with the custom Spring `Runner`:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner.class)
@TestExecutionListeners({})
public class SimpleTest {
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@RunWith(SpringRunner::class)
@TestExecutionListeners
class SimpleTest {
@Test
fun testMethod() {
// test logic...
}
}
----
======
In the preceding example, `@TestExecutionListeners` is configured with an empty list, to
disable the default listeners, which otherwise would require an `ApplicationContext` to
be configured through `@ContextConfiguration`.
[[testcontext-junit4-rules]]
=== Spring JUnit 4 Rules
The `org.springframework.test.context.junit4.rules` package provides the following JUnit
4 rules (supported on JUnit 4.12 or higher):
* `SpringClassRule`
* `SpringMethodRule`
`SpringClassRule` is a JUnit `TestRule` that supports class-level features of the Spring
TestContext Framework, whereas `SpringMethodRule` is a JUnit `MethodRule` that supports
instance-level and method-level features of the Spring TestContext Framework.
In contrast to the `SpringRunner`, Spring's rule-based JUnit support has the advantage of
being independent of any `org.junit.runner.Runner` implementation and can, therefore, be
combined with existing alternative runners (such as JUnit 4's `Parameterized`) or
third-party runners (such as the `MockitoJUnitRunner`).
To support the full functionality of the TestContext framework, you must combine a
`SpringClassRule` with a `SpringMethodRule`. The following example shows the proper way
to declare these rules in an integration test:
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
public class IntegrationTest {
@ClassRule
public static final SpringClassRule springClassRule = new SpringClassRule();
@Rule
public final SpringMethodRule springMethodRule = new SpringMethodRule();
@Test
public void testMethod() {
// test logic...
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
// Optionally specify a non-Spring Runner via @RunWith(...)
@ContextConfiguration
class IntegrationTest {
@Rule
val springMethodRule = SpringMethodRule()
@Test
fun testMethod() {
// test logic...
}
companion object {
@ClassRule
val springClassRule = SpringClassRule()
}
}
----
======
[[testcontext-support-classes-junit4]]
=== JUnit 4 Base Classes
The `org.springframework.test.context.junit4` package provides the following support
classes for JUnit 4-based test cases (supported on JUnit 4.12 or higher):
* `AbstractJUnit4SpringContextTests`
* `AbstractTransactionalJUnit4SpringContextTests`
`AbstractJUnit4SpringContextTests` is an abstract base test class that integrates the
Spring TestContext Framework with explicit `ApplicationContext` testing support in a
JUnit 4 environment. When you extend `AbstractJUnit4SpringContextTests`, you can access a
`protected` `applicationContext` instance variable that you can use to perform explicit
bean lookups or to test the state of the context as a whole.
`AbstractTransactionalJUnit4SpringContextTests` is an abstract transactional extension of
`AbstractJUnit4SpringContextTests` that adds some convenience functionality for JDBC
access. This class expects a `javax.sql.DataSource` bean and a
`PlatformTransactionManager` bean to be defined in the `ApplicationContext`. When you
extend `AbstractTransactionalJUnit4SpringContextTests`, you can access a `protected`
`jdbcTemplate` instance variable that you can use to run SQL statements to query the
database. You can use such queries to confirm database state both before and after
running database-related application code, and Spring ensures that such queries run in
the scope of the same transaction as the application code. When used in conjunction with
an ORM tool, be sure to avoid
xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
`AbstractTransactionalJUnit4SpringContextTests` also provides convenience methods that
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
Furthermore, `AbstractTransactionalJUnit4SpringContextTests` provides an
`executeSqlScript(..)` method for running SQL scripts against the configured `DataSource`.
TIP: These classes are a convenience for extension. If you do not want your test classes
to be tied to a Spring-specific class hierarchy, you can configure your own custom test
classes by using `@RunWith(SpringRunner.class)` or
xref:testing/testcontext-framework/support-classes.adoc#testcontext-junit4-rules[Spring's JUnit rules].
[[testcontext-support-classes-testng]]
== TestNG Support Classes
== TestNG Support
The `org.springframework.test.context.testng` package provides the following support
classes for TestNG based test cases:
@@ -650,7 +666,8 @@ extend `AbstractTransactionalTestNGSpringContextTests`, you can access a `protec
database. You can use such queries to confirm database state both before and after
running database-related application code, and Spring ensures that such queries run in
the scope of the same transaction as the application code. When used in conjunction with
an ORM tool, be sure to avoid xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
an ORM tool, be sure to avoid
xref:testing/testcontext-framework/tx.adoc#testcontext-tx-false-positives[false positives].
As mentioned in xref:testing/support-jdbc.adoc[JDBC Testing Support],
`AbstractTransactionalTestNGSpringContextTests` also provides convenience methods that
delegate to methods in `JdbcTestUtils` by using the aforementioned `jdbcTemplate`.
@@ -234,8 +234,8 @@ Kotlin::
--
URI path patterns can also have embedded `${...}` placeholders that are resolved on startup
through `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this to, for example, parameterize a base URL based on
by using `PropertySourcesPlaceholderConfigurer` against local, system, environment, and
other property sources. You can use this, for example, to parameterize a base URL based on
some external configuration.
NOTE: Spring WebFlux uses `PathPattern` and the `PathPatternParser` for URI path matching support.
+7 -7
View File
@@ -7,17 +7,17 @@ javaPlatform {
}
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.18.3"))
api(platform("io.micrometer:micrometer-bom:1.14.5"))
api(platform("io.netty:netty-bom:4.1.119.Final"))
api(platform("com.fasterxml.jackson:jackson-bom:2.18.4"))
api(platform("io.micrometer:micrometer-bom:1.14.7"))
api(platform("io.netty:netty-bom:4.1.121.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2024.0.4"))
api(platform("io.projectreactor:reactor-bom:2024.0.6"))
api(platform("io.rsocket:rsocket-bom:1.1.5"))
api(platform("org.apache.groovy:groovy-bom:4.0.26"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
api(platform("org.assertj:assertj-bom:3.27.3"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.18"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.18"))
api(platform("org.eclipse.jetty:jetty-bom:12.0.21"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.21"))
api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1"))
api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3"))
api(platform("org.junit:junit-bom:5.12.2"))
@@ -100,7 +100,7 @@ dependencies {
api("org.apache.derby:derby:10.16.1.1")
api("org.apache.derby:derbyclient:10.16.1.1")
api("org.apache.derby:derbytools:10.16.1.1")
api("org.apache.httpcomponents.client5:httpclient5:5.4.3")
api("org.apache.httpcomponents.client5:httpclient5:5.4.4")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.4")
api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.6-SNAPSHOT
version=6.2.7
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.13-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
CLASSPATH="\\\"\\\""
# Determine the Java command to use to start the JVM.
@@ -213,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
Vendored
+2 -2
View File
@@ -70,11 +70,11 @@ goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
set CLASSPATH=
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,8 +20,10 @@ import org.springframework.beans.factory.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
@@ -37,16 +39,16 @@ import org.springframework.util.StringValueResolver;
*
* <pre class="code">
* &lt;bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"&gt;
* &lt;property name="driverClassName" value="${driver}" /&gt;
* &lt;property name="url" value="jdbc:${dbname}" /&gt;
* &lt;property name="driverClassName" value="${jdbc.driver}" /&gt;
* &lt;property name="url" value="jdbc:${jdbc.dbname}" /&gt;
* &lt;/bean&gt;
* </pre>
*
* Example properties file:
*
* <pre class="code">
* driver=com.mysql.jdbc.Driver
* dbname=mysql:mydb</pre>
* jdbc.driver=com.mysql.jdbc.Driver
* jdbc.dbname=mysql:mydb</pre>
*
* Annotated bean definitions may take advantage of property replacement using
* the {@link org.springframework.beans.factory.annotation.Value @Value} annotation:
@@ -79,11 +81,12 @@ import org.springframework.util.StringValueResolver;
* <p>Example XML property with default value:
*
* <pre class="code">
* &lt;property name="url" value="jdbc:${dbname:defaultdb}" /&gt;
* &lt;property name="url" value="jdbc:${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
@@ -92,16 +95,21 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
implements BeanNameAware, BeanFactoryAware {
/** Default placeholder prefix: {@value}. */
public static final String DEFAULT_PLACEHOLDER_PREFIX = "${";
public static final String DEFAULT_PLACEHOLDER_PREFIX = SystemPropertyUtils.PLACEHOLDER_PREFIX;
/** Default placeholder suffix: {@value}. */
public static final String DEFAULT_PLACEHOLDER_SUFFIX = "}";
public static final String DEFAULT_PLACEHOLDER_SUFFIX = SystemPropertyUtils.PLACEHOLDER_SUFFIX;
/** Default value separator: {@value}. */
public static final String DEFAULT_VALUE_SEPARATOR = ":";
public static final String DEFAULT_VALUE_SEPARATOR = SystemPropertyUtils.VALUE_SEPARATOR;
/**
* Default escape character: {@code '\'}.
* @since 6.2
* @see AbstractPropertyResolver#getDefaultEscapeCharacter()
*/
public static final Character DEFAULT_ESCAPE_CHARACTER = SystemPropertyUtils.ESCAPE_CHARACTER;
/** Default escape character: {@code '\'}. */
public static final Character DEFAULT_ESCAPE_CHARACTER = '\\';
/** Defaults to {@value #DEFAULT_PLACEHOLDER_PREFIX}. */
protected String placeholderPrefix = DEFAULT_PLACEHOLDER_PREFIX;
@@ -113,9 +121,11 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
@Nullable
protected String valueSeparator = DEFAULT_VALUE_SEPARATOR;
/** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */
/**
* The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
*/
@Nullable
protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER;
protected Character escapeCharacter = AbstractPropertyResolver.getDefaultEscapeCharacter();
protected boolean trimValues = false;
@@ -133,7 +143,7 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
/**
* Set the prefix that a placeholder string starts with.
* The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}.
* <p>The default is {@value #DEFAULT_PLACEHOLDER_PREFIX}.
*/
public void setPlaceholderPrefix(String placeholderPrefix) {
this.placeholderPrefix = placeholderPrefix;
@@ -141,31 +151,32 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi
/**
* Set the suffix that a placeholder string ends with.
* The default is {@value #DEFAULT_PLACEHOLDER_SUFFIX}.
* <p>The default is {@value #DEFAULT_PLACEHOLDER_SUFFIX}.
*/
public void setPlaceholderSuffix(String placeholderSuffix) {
this.placeholderSuffix = placeholderSuffix;
}
/**
* Specify the separating character between the placeholder variable
* and the associated default value, or {@code null} if no such
* special character should be processed as a value separator.
* The default is {@value #DEFAULT_VALUE_SEPARATOR}.
* Specify the separating character between the placeholder variable and the
* associated default value, or {@code null} if no such special character
* should be processed as a value separator.
* <p>The default is {@value #DEFAULT_VALUE_SEPARATOR}.
*/
public void setValueSeparator(@Nullable String valueSeparator) {
this.valueSeparator = valueSeparator;
}
/**
* Specify the escape character to use to ignore placeholder prefix
* or value separator, or {@code null} if no escaping should take
* place.
* <p>Default is {@link #DEFAULT_ESCAPE_CHARACTER}.
* Set the escape character to use to ignore the
* {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the
* {@linkplain #setValueSeparator(String) value separator}, or {@code null}
* if no escaping should take place.
* <p>The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}.
* @since 6.2
*/
public void setEscapeCharacter(@Nullable Character escsEscapeCharacter) {
this.escapeCharacter = escsEscapeCharacter;
public void setEscapeCharacter(@Nullable Character escapeCharacter) {
this.escapeCharacter = escapeCharacter;
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,12 +35,14 @@ 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>
*
* 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>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.
*
* <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,9 +997,17 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
*/
@Nullable
private FactoryBean<?> getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) {
boolean locked = this.singletonLock.tryLock();
if (!locked) {
return null;
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
if (lockFlag == null) {
this.singletonLock.lock();
}
else {
boolean locked = (lockFlag && this.singletonLock.tryLock());
if (!locked) {
// Avoid shortcut FactoryBean instance but allow for subsequent type-based resolution.
resolveBeanClass(mbd, beanName);
return null;
}
}
try {
@@ -1066,8 +1066,9 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
@Nullable
protected Boolean isCurrentThreadAllowedToHoldSingletonLock() {
String mainThreadPrefix = this.mainThreadPrefix;
if (this.mainThreadPrefix != null) {
// We only differentiate in the preInstantiateSingletons phase.
if (mainThreadPrefix != null) {
// We only differentiate in the preInstantiateSingletons phase, using
// the volatile mainThreadPrefix field as an indicator for that phase.
PreInstantiation preInstantiation = this.preInstantiationThread.get();
if (preInstantiation != null) {
@@ -1087,7 +1088,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
else if (this.strictLocking == null) {
// No explicit locking configuration -> infer appropriate locking.
if (mainThreadPrefix != null && !getThreadNamePrefix().equals(mainThreadPrefix)) {
if (!getThreadNamePrefix().equals(mainThreadPrefix)) {
// An unmanaged thread (assumed to be application-internal) with lenient locking,
// and not part of the same thread pool that provided the main bootstrap thread
// (excluding scenarios where we are hit by multiple external bootstrap threads).
@@ -271,13 +271,15 @@ 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -118,7 +118,15 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
*/
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
if (factory.isSingleton() && containsSingleton(beanName)) {
this.singletonLock.lock();
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
boolean locked;
if (lockFlag == null) {
this.singletonLock.lock();
locked = true;
}
else {
locked = (lockFlag && this.singletonLock.tryLock());
}
try {
Object object = this.factoryBeanObjectCache.get(beanName);
if (object == null) {
@@ -131,11 +139,13 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
}
else {
if (shouldPostProcess) {
if (isSingletonCurrentlyInCreation(beanName)) {
// Temporarily return non-post-processed object, not storing it yet
return object;
if (locked) {
if (isSingletonCurrentlyInCreation(beanName)) {
// Temporarily return non-post-processed object, not storing it yet
return object;
}
beforeSingletonCreation(beanName);
}
beforeSingletonCreation(beanName);
try {
object = postProcessObjectFromFactoryBean(object, beanName);
}
@@ -144,7 +154,9 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
"Post-processing of FactoryBean's singleton object failed", ex);
}
finally {
afterSingletonCreation(beanName);
if (locked) {
afterSingletonCreation(beanName);
}
}
}
if (containsSingleton(beanName)) {
@@ -155,7 +167,9 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg
return object;
}
finally {
this.singletonLock.unlock();
if (locked) {
this.singletonLock.unlock();
}
}
}
else {
+7
View File
@@ -59,3 +59,10 @@ dependencies {
testRuntimeOnly("org.javamoney:moneta")
testRuntimeOnly("org.junit.vintage:junit-vintage-engine") // for @Inject TCK
}
test {
description = "Runs JUnit Jupiter tests and the @Inject TCK via JUnit Vintage."
useJUnitPlatform {
includeEngines "junit-jupiter", "junit-vintage"
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,6 +50,7 @@ public class ApplicationContextAotGenerator {
*/
public ClassName processAheadOfTime(GenericApplicationContext applicationContext,
GenerationContext generationContext) {
return withCglibClassHandler(new CglibClassHandler(generationContext), () -> {
applicationContext.refreshForAotProcessing(generationContext.getRuntimeHints());
ApplicationContextInitializationCodeGenerator codeGenerator =
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -80,8 +80,9 @@ public abstract class ContextAotProcessor extends AbstractAotProcessor<ClassName
@Override
protected ClassName doProcess() {
deleteExistingOutput();
GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass());
return performAotProcessing(applicationContext);
try (GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass())) {
return performAotProcessing(applicationContext);
}
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -100,6 +100,7 @@ public class ReflectiveProcessorAotContributionBuilder {
return (!this.classes.isEmpty() ? new AotContribution(this.classes) : null);
}
private static class AotContribution implements BeanFactoryInitializationAotContribution {
private final Class<?>[] classes;
@@ -113,9 +114,9 @@ public class ReflectiveProcessorAotContributionBuilder {
RuntimeHints runtimeHints = generationContext.getRuntimeHints();
registrar.registerRuntimeHints(runtimeHints, this.classes);
}
}
private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider {
@Nullable
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -29,7 +29,6 @@ import org.springframework.core.env.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;
@@ -49,7 +48,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 {@code PropertySource}. Search precedence of local properties is
* et al.) are added as a single {@link PropertySource}. Search precedence of local properties is
* based on the value of the {@link #setLocalOverride localOverride} property, which is by
* default {@code false} meaning that local properties are to be searched last, after all
* environment property sources.
@@ -101,8 +100,9 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
}
/**
* {@code PropertySources} from the given {@link Environment}
* will be searched when replacing ${...} placeholders.
* {@inheritDoc}
* <p>{@code PropertySources} from the given {@link Environment} will be searched
* when replacing ${...} placeholders.
* @see #setPropertySources
* @see #postProcessBeanFactory
*/
@@ -132,28 +132,11 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
if (this.propertySources == null) {
this.propertySources = new MutablePropertySources();
if (this.environment != null) {
PropertyResolver propertyResolver = this.environment;
// If the ignoreUnresolvablePlaceholders flag is set to true, we have to create a
// local PropertyResolver to enforce that setting, since the Environment is most
// likely not configured with ignoreUnresolvablePlaceholders set to true.
// See https://github.com/spring-projects/spring-framework/issues/27947
if (this.ignoreUnresolvablePlaceholders &&
(this.environment instanceof ConfigurableEnvironment configurableEnvironment)) {
PropertySourcesPropertyResolver resolver =
new PropertySourcesPropertyResolver(configurableEnvironment.getPropertySources());
resolver.setIgnoreUnresolvableNestedPlaceholders(true);
propertyResolver = resolver;
}
PropertyResolver propertyResolverToUse = propertyResolver;
this.propertySources.addLast(
new PropertySource<>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, this.environment) {
@Override
@Nullable
public String getProperty(String key) {
return propertyResolverToUse.getProperty(key);
}
}
);
PropertySource<?> environmentPropertySource =
(this.environment instanceof ConfigurableEnvironment configurableEnvironment ?
new ConfigurableEnvironmentPropertySource(configurableEnvironment) :
new FallbackEnvironmentPropertySource(this.environment));
this.propertySources.addLast(environmentPropertySource);
}
try {
PropertySource<?> localPropertySource =
@@ -176,6 +159,7 @@ public 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
*/
@@ -188,7 +172,7 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS
* placeholders with values from the given properties.
*/
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
final ConfigurablePropertyResolver propertyResolver) throws BeansException {
ConfigurablePropertyResolver propertyResolver) throws BeansException {
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
@@ -234,4 +218,75 @@ public 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-2021 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -146,6 +146,12 @@ 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,7 +27,6 @@ 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;
@@ -550,14 +549,13 @@ 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 also transforms disallowed field patterns to
* {@linkplain String#toLowerCase() lowercase} to support case-insensitive
* pattern matching in {@link #isAllowed}. Subclasses which override this
* method must therefore take both of these transformations into account.
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts),
* as well as direct equality.
* <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.
* <p>More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
* <p>Alternatively, specify a list of <i>allowed</i> field patterns.
@@ -575,8 +573,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
else {
String[] fieldPatterns = new String[disallowedFields.length];
for (int i = 0; i < fieldPatterns.length; i++) {
String field = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
fieldPatterns[i] = field.toLowerCase(Locale.ROOT);
fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]);
}
this.disallowedFields = fieldPatterns;
}
@@ -1302,9 +1299,9 @@ 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
@@ -1320,8 +1317,13 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
protected boolean isAllowed(String field) {
String[] allowed = getAllowedFields();
String[] disallowed = getDisallowedFields();
return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) &&
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase(Locale.ROOT))));
if (!ObjectUtils.isEmpty(allowed) && !PatternMatchUtils.simpleMatch(allowed, field)) {
return false;
}
if (!ObjectUtils.isEmpty(disallowed)) {
return !PatternMatchUtils.simpleMatchIgnoreCase(disallowed, field);
}
return true;
}
/**
@@ -1,11 +0,0 @@
<?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,6 +24,7 @@ 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;
@@ -243,14 +244,24 @@ class BackgroundBootstrapTests {
}
@Bean
public TestBean testBean4() {
public FactoryBean<TestBean> testBean4() {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean();
TestBean testBean = new TestBean();
return new FactoryBean<>() {
@Override
public TestBean getObject() {
return testBean;
}
@Override
public Class<?> getObjectType() {
return testBean.getClass();
}
};
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +38,8 @@ import org.springframework.context.support.GenericApplicationContext;
* @author Juergen Hoeller
* @since 3.0
*/
class SpringAtInjectTckTests {
// WARNING: This class MUST be public, since it is based on JUnit 3.
public class SpringAtInjectTckTests {
@SuppressWarnings("unchecked")
public static Test suite() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -45,8 +45,9 @@ class ContextAotProcessorTests {
void processGeneratesAssets(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory);
DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory);
ClassName className = processor.process();
assertThat(processor.context.isClosed()).isTrue();
assertThat(className).isEqualTo(ClassName.get(SampleApplication.class.getPackageName(),
"ContextAotProcessorTests_SampleApplication__ApplicationContextInitializer"));
assertThat(directory).satisfies(hasGeneratedAssetsForSampleApplication());
@@ -61,9 +62,10 @@ class ContextAotProcessorTests {
Path existingSourceOutput = createExisting(sourceOutput);
Path existingResourceOutput = createExisting(resourceOutput);
Path existingClassOutput = createExisting(classOutput);
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class,
DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class,
sourceOutput, resourceOutput, classOutput);
processor.process();
assertThat(processor.context.isClosed()).isTrue();
assertThat(existingSourceOutput).doesNotExist();
assertThat(existingResourceOutput).doesNotExist();
assertThat(existingClassOutput).doesNotExist();
@@ -73,13 +75,14 @@ class ContextAotProcessorTests {
void processWithEmptyNativeImageArgumentsDoesNotCreateNativeImageProperties(@TempDir Path directory) {
GenericApplicationContext context = new AnnotationConfigApplicationContext();
context.registerBean(SampleApplication.class);
ContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) {
DemoContextAotProcessor processor = new DemoContextAotProcessor(SampleApplication.class, directory) {
@Override
protected List<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();
@@ -118,6 +121,8 @@ 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"));
}
@@ -141,11 +146,12 @@ 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 {
@@ -153,7 +159,6 @@ class ContextAotProcessorTests {
public String testBean() {
return "Hello";
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,20 +16,34 @@
package org.springframework.context.support;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Properties;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;
import org.springframework.core.SpringProperties;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.env.AbstractPropertyResolver;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.StandardEnvironment;
@@ -38,12 +52,15 @@ import org.springframework.core.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}.
@@ -73,6 +90,43 @@ 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();
@@ -88,14 +142,29 @@ class PropertySourcesPlaceholderConfigurerTests {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("foo");
}
@Test
void localPropertiesOverrideFalse() {
localPropertiesOverride(false);
}
@ParameterizedTest
@ValueSource(booleans = {true, false})
void localPropertiesOverride(boolean override) {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("testBean",
genericBeanDefinition(TestBean.class)
.addPropertyValue("name", "${foo}")
.getBeanDefinition());
@Test
void localPropertiesOverrideTrue() {
localPropertiesOverride(true);
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
ppc.setLocalOverride(override);
ppc.setProperties(new Properties() {{
setProperty("foo", "local");
}});
ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing"));
ppc.postProcessBeanFactory(bf);
if (override) {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local");
}
else {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing");
}
}
@Test
@@ -281,28 +350,58 @@ class PropertySourcesPlaceholderConfigurerTests {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("bar");
}
@SuppressWarnings("serial")
private void localPropertiesOverride(boolean override) {
@Test // gh-34861
void withEnumerableAndNonEnumerablePropertySourcesInTheEnvironmentAndLocalProperties() {
DefaultListableBeanFactory bf = new DefaultListableBeanFactory();
bf.registerBeanDefinition("testBean",
genericBeanDefinition(TestBean.class)
.addPropertyValue("name", "${foo}")
.addPropertyValue("name", "${foo:bogus}")
.addPropertyValue("jedi", "${local:false}")
.getBeanDefinition());
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
// 1) MockPropertySource is an EnumerablePropertySource.
MockPropertySource mockPropertySource = new MockPropertySource("mockPropertySource")
.withProperty("foo", "${bar}");
ppc.setLocalOverride(override);
// 2) PropertySource is not an EnumerablePropertySource.
PropertySource<?> rawPropertySource = new PropertySource<>("rawPropertySource", new Object()) {
@Override
public Object getProperty(String key) {
return ("bar".equals(key) ? "quux" : null);
}
};
MockEnvironment env = new MockEnvironment();
env.getPropertySources().addFirst(mockPropertySource);
env.getPropertySources().addLast(rawPropertySource);
PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
ppc.setEnvironment(env);
// 3) Local properties are stored in a PropertiesPropertySource which is an EnumerablePropertySource.
ppc.setProperties(new Properties() {{
setProperty("foo", "local");
setProperty("local", "true");
}});
ppc.setEnvironment(new MockEnvironment().withProperty("foo", "enclosing"));
ppc.postProcessBeanFactory(bf);
if (override) {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("local");
}
else {
assertThat(bf.getBean(TestBean.class).getName()).isEqualTo("enclosing");
// Verify all properties can be resolved via the Environment.
assertThat(env.getProperty("foo")).isEqualTo("quux");
assertThat(env.getProperty("bar")).isEqualTo("quux");
// Verify that placeholder resolution works.
TestBean testBean = bf.getBean(TestBean.class);
assertThat(testBean.getName()).isEqualTo("quux");
assertThat(testBean.isJedi()).isTrue();
// Verify that the presence of a non-EnumerablePropertySource does not prevent
// accessing EnumerablePropertySources via getAppliedPropertySources().
List<String> propertyNames = new ArrayList<>();
for (PropertySource<?> propertySource : ppc.getAppliedPropertySources()) {
if (propertySource instanceof EnumerablePropertySource<?> enumerablePropertySource) {
Collections.addAll(propertyNames, enumerablePropertySource.getPropertyNames());
}
}
// Should not contain "foo" or "bar" from the Environment.
assertThat(propertyNames).containsOnly("local");
}
@Test
@@ -432,6 +531,252 @@ 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;
@@ -472,4 +817,23 @@ 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,10 +463,21 @@ public class ReflectUtils {
c = lookup.defineClass(b);
}
catch (LinkageError | IllegalArgumentException ex) {
// in case of plain LinkageError (class already defined)
// or IllegalArgumentException (class in different package):
// fall through to traditional ClassLoader.defineClass below
t = ex;
if (ex instanceof LinkageError) {
// Could be a ClassLoader mismatch with the class pre-existing in a
// parent ClassLoader -> try loadClass before giving up completely.
try {
c = contextClass.getClassLoader().loadClass(className);
}
catch (ClassNotFoundException cnfe) {
}
}
if (c == null) {
// in case of plain LinkageError (class already defined)
// or IllegalArgumentException (class in different package):
// fall through to traditional ClassLoader.defineClass below
t = ex;
}
}
catch (Throwable ex) {
throw new CodeGenerationException(ex);
@@ -26,18 +26,20 @@ 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 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>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>This is an alternative way to set Spring-related system properties such as
* "spring.getenv.ignore" and "spring.beaninfo.ignore", in particular for scenarios
* where JVM system properties are locked on the target platform (for example, WebSphere).
* See {@link #setFlag} for a convenient way to locally set such flags to "true".
* {@code spring.getenv.ignore} and {@code spring.beaninfo.ignore}, in particular
* for scenarios where JVM system properties are locked on the target platform
* (for example, WebSphere). See {@link #setFlag} for a convenient way to locally
* set such flags to {@code "true"}.
*
* @author Juergen Hoeller
* @since 3.2.7
* @see org.springframework.aot.AotDetector#AOT_ENABLED
* @see org.springframework.beans.StandardBeanInfoFactory#IGNORE_BEANINFO_PROPERTY_NAME
* @see org.springframework.beans.factory.support.DefaultListableBeanFactory#STRICT_LOCKING_PROPERTY_NAME
* @see org.springframework.core.env.AbstractEnvironment#IGNORE_GETENV_PROPERTY_NAME
@@ -361,16 +361,12 @@ 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,7 +42,7 @@ 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 working against the {@link MutablePropertySources} API.
* and work against the {@link MutablePropertySources} API.
* See {@link ConfigurableEnvironment} javadoc for usage examples.
*
* @author Chris Beams
@@ -66,7 +66,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
public static final String IGNORE_GETENV_PROPERTY_NAME = "spring.getenv.ignore";
/**
* Name of the property to set to specify active profiles: {@value}.
* Name of the property to specify active profiles: {@value}.
* <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 set to specify profiles that are active by default: {@value}.
* Name of the property 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}
* instance used by the Environment.
* used by this {@code Environment}.
* @since 5.3.4
* @see #getPropertyResolver()
*/
@@ -150,8 +150,7 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
}
/**
* Return the {@link ConfigurablePropertyResolver} being used by the
* {@link Environment}.
* Return the {@link ConfigurablePropertyResolver} used by the {@code Environment}.
* @since 5.3.4
* @see #createPropertyResolver(MutablePropertySources)
*/
@@ -320,7 +319,6 @@ public abstract class AbstractEnvironment implements ConfigurableEnvironment {
}
}
@Override
public String[] getDefaultProfiles() {
return StringUtils.toStringArray(doGetDefaultProfiles());
@@ -328,7 +326,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.
@@ -420,7 +418,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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import 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;
@@ -37,10 +38,52 @@ 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
@@ -62,7 +105,7 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR;
@Nullable
private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER;
private Character escapeCharacter = getDefaultEscapeCharacter();
private final Set<String> requiredProperties = new LinkedHashSet<>();
@@ -91,9 +134,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* Set the prefix that placeholders replaced by this resolver must begin with.
* <p>The default is "${".
* @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_PREFIX
* {@inheritDoc}
* <p>The default is <code>"${"</code>.
* @see SystemPropertyUtils#PLACEHOLDER_PREFIX
*/
@Override
public void setPlaceholderPrefix(String placeholderPrefix) {
@@ -102,9 +145,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* Set the suffix that placeholders replaced by this resolver must end with.
* <p>The default is "}".
* @see org.springframework.util.SystemPropertyUtils#PLACEHOLDER_SUFFIX
* {@inheritDoc}
* <p>The default is <code>"}"</code>.
* @see SystemPropertyUtils#PLACEHOLDER_SUFFIX
*/
@Override
public void setPlaceholderSuffix(String placeholderSuffix) {
@@ -113,11 +156,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* 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
* {@inheritDoc}
* <p>The default is {@code ":"}.
* @see SystemPropertyUtils#VALUE_SEPARATOR
*/
@Override
public void setValueSeparator(@Nullable String valueSeparator) {
@@ -125,12 +166,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe
}
/**
* 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 "\".
* {@inheritDoc}
* <p>The default is determined by {@link #getDefaultEscapeCharacter()}.
* @since 6.2
* @see org.springframework.util.SystemPropertyUtils#ESCAPE_CHARACTER
*/
@Override
public void setEscapeCharacter(@Nullable Character escapeCharacter) {
@@ -291,4 +329,60 @@ 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -34,7 +34,10 @@ 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 (as far as possible).
* 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>
*
* @author Chris Beams
* @author Juergen Hoeller
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -137,13 +137,13 @@ public interface ConfigurableEnvironment extends Environment, ConfigurableProper
Map<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,
* for example, system environment and system properties.
* child as well as avoiding redundant searches through common property source types
* &mdash; 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -69,21 +69,23 @@ public interface ConfigurablePropertyResolver extends PropertyResolver {
void setPlaceholderSuffix(String placeholderSuffix);
/**
* Specify the separating character between the placeholders replaced by this
* resolver and their associated default value, or {@code null} if no such
* Set the separating character to be honored between placeholders replaced by
* this resolver and their associated default values, or {@code null} if no such
* special character should be processed as a value separator.
*/
void setValueSeparator(@Nullable String valueSeparator);
/**
* Specify the escape character to use to ignore placeholder prefix or
* value separator, or {@code null} if no escaping should take place.
* Set the escape character to use to ignore the
* {@linkplain #setPlaceholderPrefix(String) placeholder prefix} and the
* {@linkplain #setValueSeparator(String) value separator}, or {@code null}
* if no escaping should take place.
* @since 6.2
*/
void setEscapeCharacter(@Nullable Character escapeCharacter);
/**
* Set whether to throw an exception when encountering an unresolvable placeholder
* Specify whether to throw an exception when encountering an unresolvable placeholder
* nested within the value of a given property. A {@code false} value indicates strict
* resolution, i.e. that an exception will be thrown. A {@code true} value indicates
* that unresolvable nested placeholders should be passed through in their unresolved
@@ -106,7 +108,7 @@ public interface ConfigurablePropertyResolver extends PropertyResolver {
* {@link #setRequiredProperties} is present and resolves to a
* non-{@code null} value.
* @throws MissingRequiredPropertiesException if any of the required
* properties are not resolvable.
* properties are not resolvable
*/
void validateRequiredProperties() throws MissingRequiredPropertiesException;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,13 +30,13 @@ import org.springframework.lang.Nullable;
public interface PropertyResolver {
/**
* Return whether the given property key is available for resolution,
* i.e. if the value for the given key is not {@code null}.
* Determine whether the given property key is available for resolution
* &mdash; for example, if the value for the given key is not {@code null}.
*/
boolean containsProperty(String key);
/**
* Return the property value associated with the given key,
* Resolve the property value associated with the given key,
* or {@code null} if the key cannot be resolved.
* @param key the property name to resolve
* @see #getProperty(String, String)
@@ -47,7 +47,7 @@ public interface PropertyResolver {
String getProperty(String key);
/**
* Return the property value associated with the given key, or
* Resolve the property value associated with the given key, or
* {@code defaultValue} if the key cannot be resolved.
* @param key the property name to resolve
* @param defaultValue the default value to return if no value is found
@@ -57,7 +57,7 @@ public interface PropertyResolver {
String getProperty(String key, String defaultValue);
/**
* Return the property value associated with the given key,
* Resolve the property value associated with the given key,
* or {@code null} if the key cannot be resolved.
* @param key the property name to resolve
* @param targetType the expected type of the property value
@@ -67,7 +67,7 @@ public interface PropertyResolver {
<T> T getProperty(String key, Class<T> targetType);
/**
* Return the property value associated with the given key,
* Resolve the property value associated with the given key,
* or {@code defaultValue} if the key cannot be resolved.
* @param key the property name to resolve
* @param targetType the expected type of the property value
@@ -77,14 +77,14 @@ public interface PropertyResolver {
<T> T getProperty(String key, Class<T> targetType, T defaultValue);
/**
* Return the property value associated with the given key (never {@code null}).
* Resolve the property value associated with the given key (never {@code null}).
* @throws IllegalStateException if the key cannot be resolved
* @see #getRequiredProperty(String, Class)
*/
String getRequiredProperty(String key) throws IllegalStateException;
/**
* Return the property value associated with the given key, converted to the given
* Resolve the property value associated with the given key, converted to the given
* targetType (never {@code null}).
* @throws IllegalStateException if the given key cannot be resolved
*/
@@ -29,6 +29,7 @@ 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;
@@ -44,6 +45,7 @@ import org.springframework.util.ResourceUtils;
*/
public abstract class AbstractFileResolvingResource extends AbstractResource {
@SuppressWarnings("try")
@Override
public boolean exists() {
try {
@@ -86,7 +88,11 @@ 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).
return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null);
// getJarFile() called for enforced presence check of the jar file,
// throwing a NoSuchFileException otherwise (turned to false below).
try (JarFile jarFile = jarCon.getJarFile()) {
return (jarCon.getEntryName() == null || jarCon.getJarEntry() != null);
}
}
else if (con.getContentLengthLong() > 0) {
return true;
@@ -36,6 +36,7 @@ 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;
@@ -874,9 +875,9 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
rootEntryPath = (jarEntry != null ? jarEntry.getName() : "");
closeJarFile = !jarCon.getUseCaches();
}
catch (ZipException | FileNotFoundException ex) {
catch (ZipException | FileNotFoundException | NoSuchFileException ex) {
// Happens in case of a non-jar file or in case of a cached root directory
// without specific subdirectory present, respectively.
// without the specific subdirectory present, respectively.
return Collections.emptySet();
}
}
@@ -1275,7 +1276,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
/**
* Return a alternative form of the resource, i.e. with or without a leading slash.
* Return an alternative form of the resource, i.e. with or without a leading slash.
* @param path the file path (with or without a leading slash)
* @return the alternative form or {@code null}
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -141,8 +141,8 @@ public abstract class PropertiesLoaderSupport {
/**
* Return a merged Properties instance containing both the
* loaded properties and properties set on this FactoryBean.
* Return a merged {@link Properties} instance containing both the
* loaded properties and properties set on this component.
*/
protected Properties mergeProperties() throws IOException {
Properties result = new Properties();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,13 +37,25 @@ public abstract class PatternMatchUtils {
* @return whether the String matches the given pattern
*/
public static boolean simpleMatch(@Nullable String pattern, @Nullable String str) {
return simpleMatch(pattern, str, false);
}
/**
* Variant of {@link #simpleMatch(String, String)} that ignores upper/lower case.
* @since 6.1.20
*/
public static boolean simpleMatchIgnoreCase(@Nullable String pattern, @Nullable String str) {
return simpleMatch(pattern, str, true);
}
private static boolean simpleMatch(@Nullable String pattern, @Nullable String str, boolean ignoreCase) {
if (pattern == null || str == null) {
return false;
}
int firstIndex = pattern.indexOf('*');
if (firstIndex == -1) {
return pattern.equals(str);
return (ignoreCase ? pattern.equalsIgnoreCase(str) : pattern.equals(str));
}
if (firstIndex == 0) {
@@ -52,25 +64,43 @@ public abstract class PatternMatchUtils {
}
int nextIndex = pattern.indexOf('*', 1);
if (nextIndex == -1) {
return str.endsWith(pattern.substring(1));
String part = pattern.substring(1);
return (ignoreCase ? StringUtils.endsWithIgnoreCase(str, part) : str.endsWith(part));
}
String part = pattern.substring(1, nextIndex);
if (part.isEmpty()) {
return simpleMatch(pattern.substring(nextIndex), str);
return simpleMatch(pattern.substring(nextIndex), str, ignoreCase);
}
int partIndex = str.indexOf(part);
int partIndex = indexOf(str, part, 0, ignoreCase);
while (partIndex != -1) {
if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) {
if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()), ignoreCase)) {
return true;
}
partIndex = str.indexOf(part, partIndex + 1);
partIndex = indexOf(str, part, partIndex + 1, ignoreCase);
}
return false;
}
return (str.length() >= firstIndex &&
pattern.startsWith(str.substring(0, firstIndex)) &&
simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex)));
checkStartsWith(pattern, str, firstIndex, ignoreCase) &&
simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex), ignoreCase));
}
private static boolean checkStartsWith(String pattern, String str, int index, boolean ignoreCase) {
String part = str.substring(0, index);
return (ignoreCase ? StringUtils.startsWithIgnoreCase(pattern, part) : pattern.startsWith(part));
}
private static int indexOf(String str, String otherStr, int startIndex, boolean ignoreCase) {
if (!ignoreCase) {
return str.indexOf(otherStr, startIndex);
}
for (int i = startIndex; i <= (str.length() - otherStr.length()); i++) {
if (str.regionMatches(true, i, otherStr, 0, otherStr.length())) {
return i;
}
}
return -1;
}
/**
@@ -94,4 +124,19 @@ public 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 class NonClosingInputStream extends FilterInputStream {
private static final class NonClosingInputStream extends FilterInputStream {
public NonClosingInputStream(InputStream in) {
super(in);
@@ -248,10 +248,30 @@ 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 class NonClosingOutputStream extends FilterOutputStream {
private static final class NonClosingOutputStream extends FilterOutputStream {
public NonClosingOutputStream(OutputStream out) {
super(out);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,16 +35,19 @@ import org.springframework.lang.Nullable;
*/
public abstract class SystemPropertyUtils {
/** Prefix for system property placeholders: {@value}. */
/** Prefix for property placeholders: {@value}. */
public static final String PLACEHOLDER_PREFIX = "${";
/** Suffix for system property placeholders: {@value}. */
/** Suffix for property placeholders: {@value}. */
public static final String PLACEHOLDER_SUFFIX = "}";
/** Value separator for system property placeholders: {@value}. */
/** Value separator for property placeholders: {@value}. */
public static final String VALUE_SEPARATOR = ":";
/** Default escape character: {@code '\'}. */
/**
* Escape character for property placeholders: {@code '\'}.
* @since 6.2
*/
public static final Character ESCAPE_CHARACTER = '\\';
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -85,6 +85,7 @@ public class ExponentialBackOff implements BackOff {
*/
public static final int DEFAULT_MAX_ATTEMPTS = Integer.MAX_VALUE;
private long initialInterval = DEFAULT_INITIAL_INTERVAL;
private double multiplier = DEFAULT_MULTIPLIER;
@@ -204,6 +205,7 @@ public class ExponentialBackOff implements BackOff {
return this.maxAttempts;
}
@Override
public BackOffExecution start() {
return new ExponentialBackOffExecution();
@@ -225,6 +227,7 @@ public class ExponentialBackOff implements BackOff {
.toString();
}
private class ExponentialBackOffExecution implements BackOffExecution {
private long currentInterval = -1;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ public class FixedBackOff implements BackOff {
*/
public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE;
private long interval = DEFAULT_INTERVAL;
private long maxAttempts = UNLIMITED_ATTEMPTS;
@@ -86,6 +87,7 @@ public class FixedBackOff implements BackOff {
return this.maxAttempts;
}
@Override
public BackOffExecution start() {
return new FixedBackOffExecution();
@@ -0,0 +1,120 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.env;
import java.util.stream.IntStream;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.SpringProperties;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.core.env.AbstractPropertyResolver.DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME;
import static org.springframework.core.env.AbstractPropertyResolver.UNDEFINED_ESCAPE_CHARACTER;
/**
* Unit tests for {@link AbstractPropertyResolver}.
*
* @author Sam Brannen
* @since 6.2.7
*/
class AbstractPropertyResolverTests {
@BeforeEach
void resetStateBeforeEachTest() {
resetState();
}
@AfterAll
static void resetState() {
AbstractPropertyResolver.defaultEscapeCharacter = UNDEFINED_ESCAPE_CHARACTER;
setSpringProperty(null);
}
@Test
void getDefaultEscapeCharacterWithSpringPropertySetToCharacterMinValue() {
setSpringProperty("" + Character.MIN_VALUE);
assertThatIllegalArgumentException()
.isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter)
.withMessage("Value for property [%s] must not be Character.MIN_VALUE",
DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME);
assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER);
}
@Test
void getDefaultEscapeCharacterWithSpringPropertySetToXyz() {
setSpringProperty("XYZ");
assertThatIllegalArgumentException()
.isThrownBy(AbstractPropertyResolver::getDefaultEscapeCharacter)
.withMessage("Value [XYZ] for property [%s] must be a single character or an empty string",
DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME);
assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(UNDEFINED_ESCAPE_CHARACTER);
}
@Test
void getDefaultEscapeCharacterWithSpringPropertySetToEmptyString() {
setSpringProperty("");
assertEscapeCharacter(null);
}
@Test
void getDefaultEscapeCharacterWithoutSpringPropertySet() {
assertEscapeCharacter('\\');
}
@Test
void getDefaultEscapeCharacterWithSpringPropertySetToBackslash() {
setSpringProperty("\\");
assertEscapeCharacter('\\');
}
@Test
void getDefaultEscapeCharacterWithSpringPropertySetToTilde() {
setSpringProperty("~");
assertEscapeCharacter('~');
}
@Test
void getDefaultEscapeCharacterFromMultipleThreads() {
setSpringProperty("~");
IntStream.range(1, 32).parallel().forEach(__ ->
assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo('~'));
assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo('~');
}
private static void setSpringProperty(String value) {
SpringProperties.setProperty(DEFAULT_PLACEHOLDER_ESCAPE_CHARACTER_PROPERTY_NAME, value);
}
private static void assertEscapeCharacter(@Nullable Character expected) {
assertThat(AbstractPropertyResolver.getDefaultEscapeCharacter()).isEqualTo(expected);
assertThat(AbstractPropertyResolver.defaultEscapeCharacter).isEqualTo(expected);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.util.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;
@@ -38,18 +39,15 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
class PropertySourcesPropertyResolverTests {
private Properties testProperties;
private final Properties testProperties = new Properties();
private MutablePropertySources propertySources;
private final MutablePropertySources propertySources = new MutablePropertySources();
private ConfigurablePropertyResolver propertyResolver;
private final PropertySourcesPropertyResolver propertyResolver = new PropertySourcesPropertyResolver(propertySources);
@BeforeEach
void setUp() {
propertySources = new MutablePropertySources();
propertyResolver = new PropertySourcesPropertyResolver(propertySources);
testProperties = new Properties();
propertySources.addFirst(new PropertiesPropertySource("testProperties", testProperties));
}
@@ -77,14 +75,12 @@ class PropertySourcesPropertyResolverTests {
@Test
void getProperty_propertySourceSearchOrderIsFIFO() {
MutablePropertySources sources = new MutablePropertySources();
PropertyResolver resolver = new PropertySourcesPropertyResolver(sources);
sources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value"));
assertThat(resolver.getProperty("pName")).isEqualTo("ps1Value");
sources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value"));
assertThat(resolver.getProperty("pName")).isEqualTo("ps2Value");
sources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value"));
assertThat(resolver.getProperty("pName")).isEqualTo("ps3Value");
propertySources.addFirst(new MockPropertySource("ps1").withProperty("pName", "ps1Value"));
assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps1Value");
propertySources.addFirst(new MockPropertySource("ps2").withProperty("pName", "ps2Value"));
assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps2Value");
propertySources.addFirst(new MockPropertySource("ps3").withProperty("pName", "ps3Value"));
assertThat(propertyResolver.getProperty("pName")).isEqualTo("ps3Value");
}
@Test
@@ -115,8 +111,8 @@ class PropertySourcesPropertyResolverTests {
class TestType { }
assertThatExceptionOfType(ConverterNotFoundException.class).isThrownBy(() ->
propertyResolver.getProperty("foo", TestType.class));
assertThatExceptionOfType(ConverterNotFoundException.class)
.isThrownBy(() -> propertyResolver.getProperty("foo", TestType.class));
}
@Test
@@ -127,7 +123,6 @@ 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);
@@ -138,7 +133,6 @@ 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();
@@ -148,10 +142,9 @@ 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");
@@ -162,7 +155,7 @@ class PropertySourcesPropertyResolverTests {
// 'system' now has precedence
assertThat(propertyResolver.getProperty("foo")).isEqualTo("newValue");
assertThat(propertySources).hasSize(2);
assertThat(propertySources).hasSize(3);
}
@Test
@@ -170,81 +163,65 @@ 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)).isEqualTo(new String[] { "abc", "123" });
assertThat(propertyResolver.getRequiredProperty("exists", String[].class)).containsExactly("abc", "123");
assertThatIllegalStateException().isThrownBy(() ->
propertyResolver.getRequiredProperty("bogus", String[].class));
assertThatIllegalStateException().isThrownBy(() -> propertyResolver.getRequiredProperty("bogus", String[].class));
}
@Test
void resolvePlaceholders() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
assertThat(propertyResolver.resolvePlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
}
@Test
void resolvePlaceholders_withUnresolvable() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown}"))
assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown}"))
.isEqualTo("Replace this value plus ${unknown}");
}
@Test
void resolvePlaceholders_withDefaultValue() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
assertThat(propertyResolver.resolvePlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
.isEqualTo("Replace this value plus defaultValue");
}
@Test
void resolvePlaceholders_withNullInput() {
assertThatIllegalArgumentException().isThrownBy(() ->
new PropertySourcesPropertyResolver(new MutablePropertySources()).resolvePlaceholders(null));
assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolvePlaceholders(null));
}
@Test
void resolveRequiredPlaceholders() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key}")).isEqualTo("Replace this value");
}
@Test
void resolveRequiredPlaceholders_withUnresolvable() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThatExceptionOfType(PlaceholderResolutionException.class).isThrownBy(() ->
resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}"));
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown}"));
}
@Test
void resolveRequiredPlaceholders_withDefaultValue() {
MutablePropertySources propertySources = new MutablePropertySources();
propertySources.addFirst(new MockPropertySource().withProperty("key", "value"));
PropertyResolver resolver = new PropertySourcesPropertyResolver(propertySources);
assertThat(resolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
assertThat(propertyResolver.resolveRequiredPlaceholders("Replace this ${key} plus ${unknown:defaultValue}"))
.isEqualTo("Replace this value plus defaultValue");
}
@Test
void resolveRequiredPlaceholders_withNullInput() {
assertThatIllegalArgumentException().isThrownBy(() ->
new PropertySourcesPropertyResolver(new MutablePropertySources()).resolveRequiredPlaceholders(null));
assertThatIllegalArgumentException().isThrownBy(() -> propertyResolver.resolveRequiredPlaceholders(null));
}
@Test
@@ -256,17 +233,17 @@ 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", "");
@@ -291,13 +268,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
@@ -349,9 +326,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);
@@ -361,9 +338,58 @@ 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}\"");
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");
}
}
}
@@ -335,6 +335,11 @@ 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -53,18 +53,22 @@ 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
@@ -74,6 +78,10 @@ class PatternMatchUtilsTests {
assertMatches("*stuff*", "stuffTest");
assertMatches("*stuff*", "getstuff");
assertMatches("*stuff*", "stuff");
testMixedCaseMatch("*stuff*", "getStuffTest");
testMixedCaseMatch("*stuff*", "StuffTest");
testMixedCaseMatch("*stuff*", "getStuff");
testMixedCaseMatch("*stuff*", "Stuff");
}
@Test
@@ -82,6 +90,8 @@ class PatternMatchUtilsTests {
assertMatches("on*Event", "onEvent");
assertDoesNotMatch("3*3", "3");
assertMatches("3*3", "33");
testMixedCaseMatch("on*Event", "OnMyEvenT");
testMixedCaseMatch("on*Event", "OnEvenT");
}
@Test
@@ -122,18 +132,27 @@ 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.Properties;
import java.util.Map;
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) {
Properties properties = new Properties();
properties.setProperty("firstName", "John");
properties.setProperty("nested0", "first");
properties.setProperty("nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of(
"firstName", "John",
"nested0", "first",
"nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> placeholders() {
@@ -79,13 +79,13 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("p1", "v1");
properties.setProperty("p2", "v2");
properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
properties.setProperty("p4", "${p3}"); // deeply nested placeholders
properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<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);
}
static Stream<Arguments> nestedPlaceholders() {
@@ -101,19 +101,15 @@ class PlaceholderParserTests {
@Test
void parseWithSinglePlaceholder() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("${firstName}", resolver))
.isEqualTo("John");
verify(resolver).resolvePlaceholder("firstName");
verifyNoMoreInteractions(resolver);
assertThat(this.parser.replacePlaceholders("${firstName}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
void parseWithPlaceholderAndPrefixText() {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John");
assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver))
.isEqualTo("This is John");
verify(resolver).resolvePlaceholder("firstName");
verifyNoMoreInteractions(resolver);
assertThat(this.parser.replacePlaceholders("This is ${firstName}", resolver)).isEqualTo("This is John");
verifyPlaceholderResolutions(resolver, "firstName");
}
@Test
@@ -121,31 +117,25 @@ class PlaceholderParserTests {
PlaceholderResolver resolver = mockPlaceholderResolver("firstName", "John", "lastName", "Smith");
assertThat(this.parser.replacePlaceholders("User: ${firstName} - ${lastName}.", resolver))
.isEqualTo("User: John - Smith.");
verify(resolver).resolvePlaceholder("firstName");
verify(resolver).resolvePlaceholder("lastName");
verifyNoMoreInteractions(resolver);
verifyPlaceholderResolutions(resolver, "firstName", "lastName");
}
@Test
void parseWithNestedPlaceholderInKey() {
PlaceholderResolver resolver = mockPlaceholderResolver(
"nested", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver))
.isEqualTo("John");
PlaceholderResolver resolver = mockPlaceholderResolver("nested", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${first${nested}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "nested", "firstName");
}
@Test
void parseWithMultipleNestedPlaceholdersInKey() {
PlaceholderResolver resolver = mockPlaceholderResolver(
"nested0", "first", "nested1", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver))
.isEqualTo("John");
PlaceholderResolver resolver = mockPlaceholderResolver("nested0", "first", "nested1", "Name", "firstName", "John");
assertThat(this.parser.replacePlaceholders("${${nested0}${nested1}}", resolver)).isEqualTo("John");
verifyPlaceholderResolutions(resolver, "nested0", "nested1", "firstName");
}
@Test
void placeholdersWithSeparatorAreHandledAsIs() {
void placeholderValueContainingSeparatorIsHandledAsIs() {
PlaceholderResolver resolver = mockPlaceholderResolver("my:test", "value");
assertThat(this.parser.replacePlaceholders("${my:test}", resolver)).isEqualTo("value");
verifyPlaceholderResolutions(resolver, "my:test");
@@ -153,17 +143,20 @@ class PlaceholderParserTests {
@Test
void placeholdersWithoutEscapeCharAreNotEscaped() {
PlaceholderResolver resolver = mockPlaceholderResolver("test", "value");
assertThat(this.parser.replacePlaceholders("\\${test}", resolver)).isEqualTo("\\value");
verifyPlaceholderResolutions(resolver, "test");
PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2", "p3", "v3", "p4", "v4");
assertThat(this.parser.replacePlaceholders("\\${p1}", resolver)).isEqualTo("\\v1");
assertThat(this.parser.replacePlaceholders("\\\\${p2}", resolver)).isEqualTo("\\\\v2");
assertThat(this.parser.replacePlaceholders("\\${p3}\\", resolver)).isEqualTo("\\v3\\");
assertThat(this.parser.replacePlaceholders("a\\${p4}\\z", resolver)).isEqualTo("a\\v4\\z");
verifyPlaceholderResolutions(resolver, "p1", "p2", "p3", "p4");
}
@Test
void textWithInvalidPlaceholderIsMerged() {
void textWithInvalidPlaceholderSyntaxIsMerged() {
String text = "test${of${with${and${";
ParsedValue parsedValue = this.parser.parse(text);
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(
TextPart.class, textPart -> assertThat(textPart.text()).isEqualTo(text));
assertThat(parsedValue.parts()).singleElement().isInstanceOfSatisfying(TextPart.class,
textPart -> assertThat(textPart.text()).isEqualTo(text));
}
}
@@ -176,11 +169,11 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("placeholders")
void placeholderIsReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("firstName", "John");
properties.setProperty("nested0", "first");
properties.setProperty("nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of(
"firstName", "John",
"nested0", "first",
"nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> placeholders() {
@@ -199,14 +192,14 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("nestedPlaceholders")
void nestedPlaceholdersAreReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("p1", "v1");
properties.setProperty("p2", "v2");
properties.setProperty("p3", "${p1}:${p2}"); // nested placeholders
properties.setProperty("p4", "${p3}"); // deeply nested placeholders
properties.setProperty("p5", "${p1}:${p2}:${bogus}"); // unresolvable placeholder
properties.setProperty("p6", "${p1}:${p2}:${bogus:def}"); // unresolvable w/ default
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<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);
}
static Stream<Arguments> nestedPlaceholders() {
@@ -225,11 +218,11 @@ class PlaceholderParserTests {
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("exactMatchPlaceholders")
void placeholdersWithExactMatchAreConsidered(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("prefix://my-service", "example-service");
properties.setProperty("px", "prefix");
properties.setProperty("p1", "${prefix://my-service}");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<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);
}
static Stream<Arguments> exactMatchPlaceholders() {
@@ -242,74 +235,55 @@ 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");
}
}
@Nested // Tests with the use of the escape character
/**
* Tests that use the escape character.
*/
@Nested
class EscapedTests {
private final PlaceholderParser parser = new PlaceholderParser("${", "}", ":", '\\', true);
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedInNestedPlaceholders")
void escapedSeparatorInNestedPlaceholder(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("app.environment", "qa");
properties.setProperty("app.service", "protocol");
properties.setProperty("protocol://host/qa/name", "protocol://example.com/qa/name");
properties.setProperty("service/host/qa/name", "https://example.com/qa/name");
properties.setProperty("service/host/qa/name:value", "https://example.com/qa/name-value");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
}
static Stream<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) {
PlaceholderResolver resolver = mockPlaceholderResolver(
"firstName", "John", "nested0", "first", "nested1", "Name",
Map<String, String> properties = Map.of(
"firstName", "John",
"${test}", "John",
"p1", "v1", "p2", "\\${p1:default}", "p3", "${p2}",
"p1", "v1",
"p2", "\\${p1:default}",
"p3", "${p2}",
"p4", "adc${p0:\\${p1}}",
"p5", "adc${\\${p0}:${p1}}",
"p6", "adc${p0:def\\${p1}}",
"p7", "adc\\${");
assertThat(this.parser.replacePlaceholders(text, resolver)).isEqualTo(expected);
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> escapedPlaceholders() {
@@ -324,18 +298,21 @@ class PlaceholderParserTests {
Arguments.of("${p4}", "adc${p1}"),
Arguments.of("${p5}", "adcv1"),
Arguments.of("${p6}", "adcdef${p1}"),
Arguments.of("${p7}", "adc\\${"));
Arguments.of("${p7}", "adc\\${"),
// Double backslash
Arguments.of("DOMAIN\\\\${user.name}", "DOMAIN\\${user.name}"),
// Triple backslash
Arguments.of("triple\\\\\\${backslash}", "triple\\\\${backslash}"),
// Multiple escaped placeholders
Arguments.of("start\\${prop1}middle\\${prop2}end", "start${prop1}middle${prop2}end")
);
}
@ParameterizedTest(name = "{0} -> {1}")
@MethodSource("escapedSeparators")
void escapedSeparatorIsNotReplaced(String text, String expected) {
Properties properties = new Properties();
properties.setProperty("first:Name", "John");
properties.setProperty("nested0", "first");
properties.setProperty("nested1", "Name");
assertThat(this.parser.replacePlaceholders(text, properties::getProperty)).isEqualTo(expected);
Map<String, String> properties = Map.of("first:Name", "John");
assertThat(this.parser.replacePlaceholders(text, properties::get)).isEqualTo(expected);
}
static Stream<Arguments> escapedSeparators() {
@@ -345,6 +322,26 @@ 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
@@ -354,34 +351,38 @@ class PlaceholderParserTests {
@Test
void textWithCircularReference() {
PlaceholderResolver resolver = mockPlaceholderResolver("pL", "${pR}", "pR", "${pL}");
assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", resolver))
Map<String, String> properties = Map.of(
"pL", "${pR}",
"pR", "${pL}");
assertThatThrownBy(() -> this.parser.replacePlaceholders("${pL}", properties::get))
.isInstanceOf(PlaceholderResolutionException.class)
.hasMessage("Circular placeholder reference 'pL' in value \"${pL}\" <-- \"${pR}\" <-- \"${pL}\"");
}
@Test
void unresolvablePlaceholderIsReported() {
PlaceholderResolver resolver = mockPlaceholderResolver();
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> this.parser.replacePlaceholders("${bogus}", resolver))
.withMessage("Could not resolve placeholder 'bogus' in value \"${bogus}\"")
.isThrownBy(() -> this.parser.replacePlaceholders("X${bogus}Z", placeholderName -> null))
.withMessage("Could not resolve placeholder 'bogus' in value \"X${bogus}Z\"")
.withNoCause();
}
@Test
void unresolvablePlaceholderInNestedPlaceholderIsReportedWithChain() {
PlaceholderResolver resolver = mockPlaceholderResolver("p1", "v1", "p2", "v2",
Map<String, String> properties = Map.of(
"p1", "v1",
"p2", "v2",
"p3", "${p1}:${p2}:${bogus}");
assertThatExceptionOfType(PlaceholderResolutionException.class)
.isThrownBy(() -> this.parser.replacePlaceholders("${p3}", resolver))
.isThrownBy(() -> this.parser.replacePlaceholders("${p3}", properties::get))
.withMessage("Could not resolve placeholder 'bogus' in value \"${p1}:${p2}:${bogus}\" <-- \"${p3}\"")
.withNoCause();
}
}
PlaceholderResolver mockPlaceholderResolver(String... pairs) {
private static PlaceholderResolver mockPlaceholderResolver(String... pairs) {
if (pairs.length % 2 == 1) {
throw new IllegalArgumentException("size must be even, it is a set of key=value pairs");
}
@@ -394,7 +395,7 @@ class PlaceholderParserTests {
return resolver;
}
void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
private static void verifyPlaceholderResolutions(PlaceholderResolver mock, String... placeholders) {
InOrder ordered = inOrder(mock);
for (String placeholder : placeholders) {
ordered.verify(mock).resolvePlaceholder(placeholder);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,8 +16,11 @@
package org.springframework.jdbc.core.simple;
import java.util.List;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassRelativeResourceLoader;
@@ -144,6 +147,86 @@ 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,6 +49,7 @@ import org.springframework.util.Assert;
* @author Juergen Hoeller
* @author Mark Paluch
* @author Anton Naydenov
* @author Sam Brannen
* @since 5.3
*/
abstract class NamedParameterUtils {
@@ -513,70 +514,17 @@ 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);
}
}
}
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
public String toQuery() {
return this.expandedSql;
}
@Override
@@ -597,10 +545,68 @@ abstract class NamedParameterUtils {
}
}
@Override
public String toQuery() {
return this.expandedSql;
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);
}
}
}
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-2022 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,8 +19,9 @@ package org.springframework.r2dbc.core.binding;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* Anonymous, index-based bind marker using a static placeholder.
* Instances are bound by the ordinal position ordered by the appearance of
* Anonymous, index-based bind markers that use a static placeholder.
*
* <p>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.
*
@@ -46,7 +47,7 @@ class AnonymousBindMarkers implements BindMarkers {
/**
* Create a new {@link AnonymousBindMarkers} instance given {@code placeholder}.
* Create a new {@link AnonymousBindMarkers} instance for the given {@code placeholder}.
* @param placeholder parameter bind marker
*/
AnonymousBindMarkers(String placeholder) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,8 @@ import io.r2dbc.spi.Statement;
/**
* A bind marker represents a single bindable parameter within a query.
* Bind markers are dialect-specific and provide a
*
* <p>Bind markers are dialect-specific and provide a
* {@link #getPlaceholder() placeholder} that is used in the actual query.
*
* @author Mark Paluch
@@ -37,7 +38,8 @@ 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)
@@ -46,7 +48,8 @@ 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-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -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 parameter in the appropriate representation.
* the driver encodes the 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/bind parameter names.
* pass of one or more parameters. It maintains bind indexes or 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}.
* Implementations are allowed to consider/ignore/filter
* <p>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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,8 +30,8 @@ import org.springframework.util.LinkedCaseInsensitiveMap;
/**
* Resolves a {@link BindMarkersFactory} from a {@link ConnectionFactory} using
* {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's
* {@link SpringFactoriesLoader spring.factories} to determine available extensions.
* a {@link BindMarkerFactoryProvider}. Dialect resolution uses Spring's
* {@link SpringFactoriesLoader spring.factories} file to determine available extensions.
*
* @author Mark Paluch
* @since 5.3
@@ -45,8 +45,8 @@ public final class BindMarkersFactoryResolver {
/**
* Retrieve a {@link BindMarkersFactory} by inspecting {@link ConnectionFactory}
* and its metadata.
* Retrieve a {@link BindMarkersFactory} by inspecting the supplied
* {@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,18 +69,21 @@ public final class BindMarkersFactoryResolver {
/**
* SPI to extend Spring's default R2DBC BindMarkersFactory discovery mechanism.
* Implementations of this interface are discovered through Spring's
* SPI to extend Spring's default R2DBC {@link BindMarkersFactory} discovery
* mechanism.
*
* <p>Implementations of this interface are discovered through Spring's
* {@link SpringFactoriesLoader} mechanism.
*
* @see SpringFactoriesLoader
*/
@FunctionalInterface
public interface 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}
* 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}
* can provide a bind marker factory object, otherwise {@code null}
*/
@Nullable
@@ -89,7 +92,7 @@ public final class BindMarkersFactoryResolver {
/**
* Exception thrown when {@link BindMarkersFactoryResolver} cannot resolve a
* Exception thrown when a {@link BindMarkersFactoryResolver} cannot resolve a
* {@link BindMarkersFactory}.
*/
@SuppressWarnings("serial")
@@ -106,8 +109,11 @@ public final class BindMarkersFactoryResolver {
/**
* Built-in bind maker factories. Used typically as last {@link BindMarkerFactoryProvider}
* when other providers register with a higher precedence.
* Built-in bind marker factories.
*
* <p>Typically used as the last {@link BindMarkerFactoryProvider} when other
* providers are registered with a higher precedence.
*
* @see org.springframework.core.Ordered
* @see org.springframework.core.annotation.AnnotationAwareOrderComparator
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@ package org.springframework.r2dbc.core.binding;
import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;
/**
* Index-based bind marker. This implementation creates indexed bind
* Index-based bind markers. 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,14 +43,15 @@ class IndexedBindMarkers implements BindMarkers {
/**
* Create a new {@link IndexedBindMarker} instance given {@code prefix} and {@code beginWith}.
* @param prefix bind parameter prefix
* @param beginWith the first index to use
* 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
*/
IndexedBindMarkers(String prefix, int beginWith) {
IndexedBindMarkers(String prefix, int beginIndex) {
this.counter = 0;
this.prefix = prefix;
this.offset = beginWith;
this.offset = beginIndex;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import 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;
@@ -38,6 +39,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Mark Paluch
* @author Mingyuan Wu
* @author Juergen Hoeller
* @author Sam Brannen
*/
abstract class AbstractDatabaseClientIntegrationTests {
@@ -121,7 +123,8 @@ 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()
@@ -199,8 +202,7 @@ 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"))
@@ -212,6 +214,129 @@ 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,10 +16,8 @@
package org.springframework.r2dbc.core;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import io.r2dbc.spi.Parameters;
@@ -29,8 +27,6 @@ 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;
@@ -42,10 +38,13 @@ import static org.mockito.Mockito.verify;
* @author Mark Paluch
* @author Jens Schauder
* @author Anton Naydenov
* @author Sam Brannen
*/
class NamedParameterUtilsTests {
private final BindMarkersFactory BIND_MARKERS = BindMarkersFactory.indexed("$", 1);
private static final BindMarkersFactory INDEXED_MARKERS = BindMarkersFactory.indexed("$", 1);
private static final BindMarkersFactory ANONYMOUS_MARKERS = BindMarkersFactory.anonymous("?");
@Test
@@ -73,7 +72,7 @@ class NamedParameterUtilsTests {
namedParams.addValue("a", "a").addValue("b", "b").addValue("c", "c");
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a :b :c", BIND_MARKERS, namedParams);
"xxx :a :b :c", INDEXED_MARKERS, namedParams);
assertThat(operation.toQuery()).isEqualTo("xxx $1 $2 $3");
@@ -87,11 +86,11 @@ class NamedParameterUtilsTests {
void substituteObjectArray() {
MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
namedParams.addValue("a",
Arrays.asList(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
List.of(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a", BIND_MARKERS, namedParams);
"xxx :a", INDEXED_MARKERS, namedParams);
assertThat(operation.toQuery()).isEqualTo("xxx ($1, $2), ($3, $4)");
}
@@ -100,13 +99,13 @@ class NamedParameterUtilsTests {
void shouldBindObjectArray() {
MapBindParameterSource namedParams = new MapBindParameterSource(new HashMap<>());
namedParams.addValue("a",
Arrays.asList(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
List.of(new Object[] {"Walter", "Heisenberg"},
new Object[] {"Walt Jr.", "Flynn"}));
BindTarget bindTarget = mock();
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
"xxx :a", BIND_MARKERS, namedParams);
"xxx :a", INDEXED_MARKERS, namedParams);
operation.bindTo(bindTarget);
verify(bindTarget).bind(0, "Walter");
@@ -141,7 +140,7 @@ class NamedParameterUtilsTests {
ParsedSql parsedSql = NamedParameterUtils.parseSqlStatement(sql);
PreparedOperation<?> operation = NamedParameterUtils.substituteNamedParameters(
parsedSql, BIND_MARKERS, new MapBindParameterSource());
parsedSql, INDEXED_MARKERS, new MapBindParameterSource());
assertThat(operation.toQuery()).isEqualTo(expectedSql);
}
@@ -312,157 +311,139 @@ class NamedParameterUtilsTests {
void multipleEqualParameterReferencesBindsValueOnce() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo")));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source);
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameters.in("foo"))));
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = $1 or lastname = $1");
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = $0 or lastname = $0");
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
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();
}
});
operation.bindTo(trackingBindTarget);
assertThat(trackingBindTarget.bindings)
.hasSize(1)
.containsEntry(0, Parameters.in("foo"));
}
@Test
void multipleEqualCollectionParameterReferencesBindsValueOnce() {
void multipleEqualCollectionParameterReferencesForIndexedMarkersBindsValuesOnce() {
String sql = "SELECT * FROM person where name IN (:ids) or lastname IN (:ids)";
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
MapBindParameterSource source = new MapBindParameterSource(Map.of("ids",
Parameters.in(List.of("foo", "bar", "baz"))));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source);
MultiValueMap<Integer, Object> bindings = new LinkedMultiValueMap<>();
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name IN ($1, $2, $3) or lastname IN ($1, $2, $3)");
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(Collections.singletonMap("ids",
Parameters.in(Arrays.asList("foo", "bar", "baz")))));
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name IN ($0, $1, $2) or lastname IN ($0, $1, $2)");
operation.bindTo(trackingBindTarget);
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();
}
});
assertThat(trackingBindTarget.bindings)
.hasSize(3)
.containsEntry(0, "foo")
.containsEntry(1, "bar")
.containsEntry(2, "baz");
}
assertThat(bindings).containsEntry(0, Collections.singletonList("foo")) //
.containsEntry(1, Collections.singletonList("bar")) //
.containsEntry(2, Collections.singletonList("baz"));
@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");
}
@Test
void multipleEqualParameterReferencesForAnonymousMarkersBindsValueMultipleTimes() {
void multipleEqualParameterReferencesForAnonymousMarkersBindsValueTwice() {
String sql = "SELECT * FROM person where name = :id or lastname = :id";
BindMarkersFactory factory = BindMarkersFactory.anonymous("?");
MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo")));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source);
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameters.in("foo"))));
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = ? or lastname = ?");
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = ? or lastname = ?");
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
Map<Integer, Object> bindValues = new LinkedHashMap<>();
operation.bindTo(trackingBindTarget);
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"));
assertThat(trackingBindTarget.bindings)
.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";
BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0);
MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in(String.class)));
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source);
PreparedOperation<String> operation = NamedParameterUtils.substituteNamedParameters(
sql, factory, new MapBindParameterSource(
Collections.singletonMap("id", Parameters.in(String.class))));
assertThat(operation.toQuery())
.isEqualTo("SELECT * FROM person where name = $1 or lastname = $1");
assertThat(operation.toQuery()).isEqualTo(
"SELECT * FROM person where name = $0 or lastname = $0");
TrackingBindTarget trackingBindTarget = new TrackingBindTarget();
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();
}
});
operation.bindTo(trackingBindTarget);
assertThat(trackingBindTarget.bindings)
.hasSize(1)
.containsEntry(0, Parameters.in(String.class));
}
private String expand(ParsedSql sql) {
return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS,
private static String expand(ParsedSql sql) {
return NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS,
new MapBindParameterSource()).toQuery();
}
private String expand(String sql) {
return NamedParameterUtils.substituteNamedParameters(sql, BIND_MARKERS,
private static String expand(String sql) {
return NamedParameterUtils.substituteNamedParameters(sql, INDEXED_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();
}
}
}
+3 -7
View File
@@ -105,14 +105,10 @@ test {
description = "Runs JUnit 4, JUnit Jupiter, and TestNG tests."
useJUnitPlatform {
includeEngines "junit-vintage", "junit-jupiter", "testng"
excludeTags "failing-test-case"
}
// We use `include` instead of `filter.includeTestsMatching`, since
// the latter results in some tests being executed/reported
// multiple times.
include(["**/*Tests.class", "**/*Test.class"])
// `include` test filters and system properties are configured in
// org.springframework.build.TestConventions in buildSrc.
filter.excludeTestsMatching("*TestCase")
systemProperty("testGroups", project.properties.get("testGroups"))
// Java Util Logging for the JUnit Platform.
// Optionally configure Java Util Logging for the JUnit Platform.
// systemProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager")
}
@@ -16,6 +16,8 @@
package org.springframework.mock.web.server;
import java.security.Principal;
import reactor.core.publisher.Mono;
import org.springframework.context.ApplicationContext;
@@ -40,15 +42,19 @@ 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 ApplicationContext applicationContext, @Nullable Principal principal) {
super(request, new MockServerHttpResponse(),
sessionManager != null ? sessionManager : new DefaultWebSessionManager(),
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(),
applicationContext);
this.principalMono = (principal != null) ? Mono.just(principal) : Mono.empty();
}
@@ -57,6 +63,16 @@ 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.
@@ -111,6 +127,9 @@ public final class MockServerWebExchange extends DefaultServerWebExchange {
@Nullable
private ApplicationContext applicationContext;
@Nullable
private Principal principal;
public Builder(MockServerHttpRequest request) {
this.request = request;
}
@@ -147,11 +166,22 @@ 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);
return new MockServerWebExchange(
this.request, this.sessionManager, this.applicationContext, this.principal);
}
}
@@ -314,8 +314,7 @@ public class TestContextAotGenerator {
ClassName processAheadOfTime(MergedContextConfiguration mergedConfig,
GenerationContext generationContext) throws TestContextAotException {
GenericApplicationContext gac = loadContextForAotProcessing(mergedConfig);
try {
try (GenericApplicationContext gac = loadContextForAotProcessing(mergedConfig)) {
return this.aotGenerator.processAheadOfTime(gac, generationContext);
}
catch (Throwable ex) {
@@ -333,7 +332,7 @@ public class TestContextAotGenerator {
* context or if one of the prerequisites is not met
* @see AotContextLoader#loadContextForAotProcessing(MergedContextConfiguration, RuntimeHints)
*/
private GenericApplicationContext loadContextForAotProcessing(
GenericApplicationContext loadContextForAotProcessing(
MergedContextConfiguration mergedConfig) throws TestContextAotException {
Class<?> testClass = mergedConfig.getTestClass();
@@ -184,30 +184,32 @@ 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 visitedEnclosingClasses the set of enclosing classes already visited
* @param visitedTypes the set of types already visited
* @since 6.2.2
*/
private static void findHandlers(Class<?> clazz, Class<?> testClass, List<BeanOverrideHandler> handlers,
boolean localFieldsOnly, Set<Class<?>> visitedEnclosingClasses) {
boolean localFieldsOnly, Set<Class<?>> visitedTypes) {
// 0) Ensure that we do not process the same class or interface multiple times.
if (!visitedTypes.add(clazz)) {
return;
}
// 1) Search enclosing class hierarchy.
if (!localFieldsOnly && TestContextAnnotationUtils.searchEnclosingClass(clazz)) {
Class<?> enclosingClass = clazz.getEnclosingClass();
if (visitedEnclosingClasses.add(enclosingClass)) {
findHandlers(enclosingClass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
}
findHandlers(clazz.getEnclosingClass(), testClass, handlers, localFieldsOnly, visitedTypes);
}
// 2) Search class hierarchy.
Class<?> superclass = clazz.getSuperclass();
if (superclass != null && superclass != Object.class) {
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
findHandlers(superclass, testClass, handlers, localFieldsOnly, visitedTypes);
}
if (!localFieldsOnly) {
// 3) Search interfaces.
for (Class<?> ifc : clazz.getInterfaces()) {
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedEnclosingClasses);
findHandlers(ifc, testClass, handlers, localFieldsOnly, visitedTypes);
}
// 4) Process current class.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,9 +20,8 @@ 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.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
@@ -32,9 +31,10 @@ 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.junit4.SpringRunner;
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;
/**
* Integration tests that verify proper behavior of {@link DirtiesContext @DirtiesContext}
@@ -87,9 +87,11 @@ class ContextHierarchyDirtiesContextTests {
private void runTestAndVerifyHierarchies(Class<? extends FooTestCase> testClass, boolean isFooContextActive,
boolean isBarContextActive, boolean isBazContextActive) {
JUnitCore jUnitCore = new JUnitCore();
Result result = jUnitCore.run(testClass);
assertThat(result.wasSuccessful()).as("all tests passed").isTrue();
EngineTestKit.engine("junit-jupiter")
.selectors(selectClass(testClass))
.execute()
.testEvents()
.assertStatistics(stats -> stats.started(1).succeeded(1).failed(0));
assertThat(ContextHierarchyDirtiesContextTests.context).isNotNull();
@@ -111,7 +113,7 @@ class ContextHierarchyDirtiesContextTests {
// -------------------------------------------------------------------------
@RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@ContextHierarchy(@ContextConfiguration(name = "foo"))
abstract static class FooTestCase implements ApplicationContextAware {
@@ -170,10 +172,10 @@ class ContextHierarchyDirtiesContextTests {
* context.
*/
@DirtiesContext
public static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
static class ClassLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
@org.junit.Test
public void test() {
@Test
void test() {
}
}
@@ -184,10 +186,10 @@ class ContextHierarchyDirtiesContextTests {
* beginning from the current context hierarchy and down through all subhierarchies.
*/
@DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL)
public static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
static class ClassLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
@org.junit.Test
public void test() {
@Test
void test() {
}
}
@@ -199,11 +201,11 @@ class ContextHierarchyDirtiesContextTests {
* parent context, and then back down through all subhierarchies of the parent
* context.
*/
public static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
static class MethodLevelDirtiesContextWithExhaustiveModeTestCase extends BazTestCase {
@org.junit.Test
@Test
@DirtiesContext
public void test() {
void test() {
}
}
@@ -213,11 +215,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.
*/
public static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
static class MethodLevelDirtiesContextWithCurrentLevelModeTestCase extends BazTestCase {
@org.junit.Test
@Test
@DirtiesContext(hierarchyMode = HierarchyMode.CURRENT_LEVEL)
public void test() {
void test() {
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
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;
@@ -44,5 +46,9 @@ 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,7 +167,6 @@ 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +16,23 @@
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;
@@ -60,9 +70,55 @@ class TestContextAotGeneratorTests {
assertThat(createGenerator().failOnError).isFalse();
}
@Test // gh-34841
void contextIsClosedAfterAotProcessing() {
DemoTestContextAotGenerator generator = createGenerator();
generator.processAheadOfTime(Stream.of(TestCase1.class, TestCase2.class));
private static TestContextAotGenerator createGenerator() {
return new TestContextAotGenerator(null);
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
}
}
}
@@ -0,0 +1,69 @@
/*
* 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,14 +31,17 @@ 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.
* intertwined enclosing class hierarchies and type hierarchies, when an enclosing
* class is <em>present</em> twice in the intertwined 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 MockitoBeanNestedAndTypeHierarchiesTests {
class MockitoBeanNestedAndTypeHierarchiesWithEnclosingClassPresentTwiceTests {
@Autowired
ApplicationContext enclosingContext;
@@ -50,6 +53,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests {
@Test
void topLevelTest() {
assertIsMock(service);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
// The following are prerequisites for the reported regression.
assertThat(NestedTests.class.getSuperclass())
@@ -66,6 +70,7 @@ class MockitoBeanNestedAndTypeHierarchiesTests {
void nestedTest(ApplicationContext nestedContext) {
assertIsMock(service);
assertThat(enclosingContext).isSameAs(nestedContext);
assertThat(enclosingContext.getBeanNamesForType(ExampleService.class)).hasSize(1);
}
}
@@ -0,0 +1,59 @@
/*
* 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 {
}
}
@@ -0,0 +1,66 @@
/*
* 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;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@ 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;
@@ -162,12 +163,18 @@ 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
@Configuration(proxyBeanMethods = false)
static class Config {
/* no beans */
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,8 @@ 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.runner.RunWith;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.platform.testkit.engine.EngineTestKit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
@@ -31,15 +32,14 @@ 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,15 +131,24 @@ 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());
@@ -148,7 +157,7 @@ class ClassLevelDirtiesContextTests {
// -------------------------------------------------------------------
@RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration
// Ensure that we do not include the EventPublishingTestExecutionListener
// since it will access the ApplicationContext for each method in the
@@ -160,7 +169,7 @@ class ClassLevelDirtiesContextTests {
})
abstract static class BaseTestCase {
@Configuration
@Configuration(proxyBeanMethods = false)
static class Config {
/* no beans */
}
@@ -175,75 +184,75 @@ class ClassLevelDirtiesContextTests {
}
}
public static final class CleanTestCase extends BaseTestCase {
static final class CleanTestCase extends BaseTestCase {
@org.junit.Test
public void verifyContextWasAutowired() {
@Test
void verifyContextWasAutowired() {
assertApplicationContextWasAutowired();
}
}
@DirtiesContext
public static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase {
static class ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends BaseTestCase {
@org.junit.Test
public void verifyContextWasAutowired() {
@Test
void verifyContextWasAutowired() {
assertApplicationContextWasAutowired();
}
}
public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends
static class InheritedClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase extends
ClassLevelDirtiesContextWithCleanMethodsAndDefaultModeTestCase {
}
@DirtiesContext(classMode = ClassMode.AFTER_CLASS)
public static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase {
static class ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends BaseTestCase {
@org.junit.Test
public void verifyContextWasAutowired() {
@Test
void verifyContextWasAutowired() {
assertApplicationContextWasAutowired();
}
}
public static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends
static class InheritedClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase extends
ClassLevelDirtiesContextWithCleanMethodsAndAfterClassModeTestCase {
}
@DirtiesContext(classMode = ClassMode.AFTER_EACH_TEST_METHOD)
public static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase {
static class ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends BaseTestCase {
@org.junit.Test
public void verifyContextWasAutowired1() {
@Test
void verifyContextWasAutowired1() {
assertApplicationContextWasAutowired();
}
@org.junit.Test
public void verifyContextWasAutowired2() {
@Test
void verifyContextWasAutowired2() {
assertApplicationContextWasAutowired();
}
@org.junit.Test
public void verifyContextWasAutowired3() {
@Test
void verifyContextWasAutowired3() {
assertApplicationContextWasAutowired();
}
}
public static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends
static class InheritedClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase extends
ClassLevelDirtiesContextWithAfterEachTestMethodModeTestCase {
}
@DirtiesContext
public static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase {
static class ClassLevelDirtiesContextWithDirtyMethodsTestCase extends BaseTestCase {
@org.junit.Test
@Test
@DirtiesContext
public void dirtyContext() {
void dirtyContext() {
assertApplicationContextWasAutowired();
}
}
public static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends
static class InheritedClassLevelDirtiesContextWithDirtyMethodsTestCase extends
ClassLevelDirtiesContextWithDirtyMethodsTestCase {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,7 +37,7 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.annotation.DirtiesContext.MethodMode.BEFORE_METHOD;
/**
* Integration test which verifies correct interaction between the
* Integration tests which verify correct interaction between the
* {@link DirtiesContextBeforeModesTestExecutionListener},
* {@link DependencyInjectionTestExecutionListener}, and
* {@link DirtiesContextTestExecutionListener} when
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -37,10 +37,9 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.asser
import static org.springframework.test.context.cache.ContextCacheTestUtils.resetContextCache;
/**
* 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.
* 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.
*
* @author Sam Brannen
* @author Juergen Hoeller
@@ -48,7 +47,7 @@ import static org.springframework.test.context.cache.ContextCacheTestUtils.reset
* @see ContextCacheTests
* @see LruContextCacheTests
*/
@SpringJUnitConfig(locations = "../junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml")
@SpringJUnitConfig(locations = "../config/CoreContextConfigurationAppCtxTests-context.xml")
@TestExecutionListeners({ DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class })
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class SpringExtensionContextCacheTests {
@@ -14,22 +14,22 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4;
package org.springframework.test.context.config;
import org.springframework.test.context.ContextConfiguration;
/**
* Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that
* Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that
* we can specify an explicit, <em>absolute path</em> location for our
* application context.
*
* @author Sam Brannen
* @since 2.5
* @see SpringJUnit4ClassRunnerAppCtxTests
* @see ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests
* @see RelativePathSpringJUnit4ClassRunnerAppCtxTests
* @see CoreContextConfigurationAppCtxTests
* @see ClassPathResourceContextConfigurationAppCtxTests
* @see RelativePathContextConfigurationAppCtxTests
*/
@ContextConfiguration(locations = { SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH }, inheritLocations = false)
public class AbsolutePathSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests {
@ContextConfiguration(locations = CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH, inheritLocations = false)
class AbsolutePathContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests {
/* all tests are in the parent class. */
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2012 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,19 +14,18 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTests;
/**
* Integration tests that verify support for configuration classes in
* the Spring TestContext Framework.
*
* <p>Furthermore, by extending {@link SpringJUnit4ClassRunnerAppCtxTests},
* <p>Furthermore, by extending {@link CoreContextConfigurationAppCtxTests},
* this class also verifies support for several basic features of the
* Spring TestContext Framework. See JavaDoc in
* {@code SpringJUnit4ClassRunnerAppCtxTests} for details.
* {@link CoreContextConfigurationAppCtxTests} for details.
*
* <p>Configuration will be loaded from {@link PojoAndStringConfig}.
*
@@ -34,6 +33,6 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunnerAppCtxTest
* @since 3.1
*/
@ContextConfiguration(classes = PojoAndStringConfig.class, inheritLocations = false)
public class AnnotationConfigSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests {
class AnnotationConfigContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests {
/* all tests are in the parent class. */
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,43 +14,59 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.spr6128;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests to verify claims made in <a
* href="https://jira.springframework.org/browse/SPR-6128"
* target="_blank">SPR-6128</a>.
* Integration tests to verify claims made in
* <a href="https://github.com/spring-projects/spring-framework/issues/10796">gh-10796</a>.
*
* @author Sam Brannen
* @author Chris Beams
* @since 3.0
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@RunWith(SpringJUnit4ClassRunner.class)
public class AutowiredQualifierTests {
class AutowiredQualifierTests {
@Autowired
private String foo;
String foo;
@Autowired
@Qualifier("customFoo")
private String customFoo;
String customFoo;
@Test
public void test() {
void test() {
assertThat(foo).isEqualTo("normal");
assertThat(customFoo).isEqualTo("custom");
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
String foo() {
return "normal";
}
@Bean
String customFoo() {
return "custom";
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.testfixture.beans.Employee;
import org.springframework.context.annotation.Bean;
@@ -36,13 +36,21 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
*/
@ContextConfiguration
public class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests {
class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests {
@Configuration
@Test
@Override
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda");
}
@Configuration(proxyBeanMethods = false)
static class ContextConfiguration {
@Bean
public Employee employee() {
Employee employee() {
Employee employee = new Employee();
employee.setName("Yoda");
employee.setAge(900);
@@ -51,12 +59,4 @@ public class BeanOverridingDefaultConfigClassesInheritedTests extends DefaultCon
}
}
@Test
@Override
public void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ContextConfiguration;
@@ -33,11 +33,11 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
*/
@ContextConfiguration(classes = BeanOverridingDefaultConfigClassesInheritedTests.ContextConfiguration.class)
public class BeanOverridingExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests {
class BeanOverridingExplicitConfigClassesInheritedTests extends ExplicitConfigClassesBaseTests {
@Test
@Override
public void verifyEmployeeSetFromBaseContextConfig() {
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,36 +14,35 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4;
package org.springframework.test.context.config;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.util.ResourceUtils;
/**
* Extension of {@link SpringJUnit4ClassRunnerAppCtxTests}, which verifies that
* Extension of {@link CoreContextConfigurationAppCtxTests}, which verifies that
* we can specify an explicit, <em>classpath</em> location for our application
* context.
*
* @author Sam Brannen
* @since 2.5
* @see SpringJUnit4ClassRunnerAppCtxTests
* @see CoreContextConfigurationAppCtxTests
* @see #CLASSPATH_CONTEXT_RESOURCE_PATH
* @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests
* @see RelativePathSpringJUnit4ClassRunnerAppCtxTests
* @see AbsolutePathContextConfigurationAppCtxTests
* @see RelativePathContextConfigurationAppCtxTests
*/
@ContextConfiguration(locations = { ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false)
public class ClassPathResourceSpringJUnit4ClassRunnerAppCtxTests extends SpringJUnit4ClassRunnerAppCtxTests {
@ContextConfiguration(locations = { ClassPathResourceContextConfigurationAppCtxTests.CLASSPATH_CONTEXT_RESOURCE_PATH }, inheritLocations = false)
class ClassPathResourceContextConfigurationAppCtxTests extends CoreContextConfigurationAppCtxTests {
/**
* Classpath-based resource path for the application context configuration
* for {@link SpringJUnit4ClassRunnerAppCtxTests}:
* {@code &quot;classpath:/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml&quot;}
* for {@link CoreContextConfigurationAppCtxTests}: {@value}
*
* @see SpringJUnit4ClassRunnerAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH
* @see CoreContextConfigurationAppCtxTests#DEFAULT_CONTEXT_RESOURCE_PATH
* @see ResourceUtils#CLASSPATH_URL_PREFIX
*/
public static final String CLASSPATH_CONTEXT_RESOURCE_PATH = ResourceUtils.CLASSPATH_URL_PREFIX +
SpringJUnit4ClassRunnerAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH;
CoreContextConfigurationAppCtxTests.DEFAULT_CONTEXT_RESOURCE_PATH;
/* all tests are in the parent class. */
@@ -0,0 +1,54 @@
/*
* 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-2024 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,13 +14,13 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4;
package org.springframework.test.context.config;
import jakarta.annotation.Resource;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
@@ -33,18 +33,18 @@ import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
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.support.DependencyInjectionTestExecutionListener;
import org.springframework.test.context.support.GenericXmlContextLoader;
import static org.assertj.core.api.Assertions.assertThat;
/**
* SpringJUnit4ClassRunnerAppCtxTests serves as a <em>proof of concept</em>
* JUnit 4 based test class, which verifies the expected functionality of
* {@link SpringRunner} in conjunction with the following:
* {@code CoreContextConfigurationAppCtxTests} serves as a <em>core</em> test class, which
* verifies the expected functionality of {@link ContextConfiguration @ContextConfiguration}
* in conjunction with the following:
*
* <ul>
* <li>{@link ContextConfiguration @ContextConfiguration}</li>
* <li>{@link Autowired @Autowired}</li>
* <li>{@link Qualifier @Qualifier}</li>
* <li>{@link Resource @Resource}</li>
@@ -67,21 +67,24 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Sam Brannen
* @since 2.5
* @see AbsolutePathSpringJUnit4ClassRunnerAppCtxTests
* @see RelativePathSpringJUnit4ClassRunnerAppCtxTests
* @see InheritedConfigSpringJUnit4ClassRunnerAppCtxTests
* @see AbsolutePathContextConfigurationAppCtxTests
* @see AnnotationConfigContextConfigurationAppCtxTests
* @see ClassPathResourceContextConfigurationAppCtxTests
* @see InheritedConfigContextConfigurationAppCtxTests
* @see MultipleResourcesContextConfigurationAppCtxTests
* @see RelativePathContextConfigurationAppCtxTests
*/
@RunWith(SpringRunner.class)
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@TestExecutionListeners(DependencyInjectionTestExecutionListener.class)
public class SpringJUnit4ClassRunnerAppCtxTests implements ApplicationContextAware, BeanNameAware, InitializingBean {
class CoreContextConfigurationAppCtxTests implements ApplicationContextAware, BeanNameAware, InitializingBean {
/**
* Default resource path for the application context configuration for
* {@link SpringJUnit4ClassRunnerAppCtxTests}: {@value}
* {@link CoreContextConfigurationAppCtxTests}: {@value}
*/
public static final String DEFAULT_CONTEXT_RESOURCE_PATH =
"/org/springframework/test/context/junit4/SpringJUnit4ClassRunnerAppCtxTests-context.xml";
"/org/springframework/test/context/config/CoreContextConfigurationAppCtxTests-context.xml";
private Employee employee;
@@ -136,12 +139,12 @@ public class SpringJUnit4ClassRunnerAppCtxTests implements ApplicationContextAwa
}
@Autowired
public void setLiteralParameterValue(@Value("enigma") String literalParameterValue) {
void setLiteralParameterValue(@Value("enigma") String literalParameterValue) {
this.literalParameterValue = literalParameterValue;
}
@Autowired
public void setSpelParameterValue(@Value("#{2 == (1+1)}") Boolean spelParameterValue) {
void setSpelParameterValue(@Value("#{2 == (1+1)}") Boolean spelParameterValue) {
this.spelParameterValue = spelParameterValue;
}
@@ -162,23 +165,23 @@ public class SpringJUnit4ClassRunnerAppCtxTests implements ApplicationContextAwa
@Test
public void verifyBeanNameSet() {
void verifyBeanNameSet() {
assertThat(this.beanName).as("The bean name of this test instance should have been set due to BeanNameAware semantics.")
.startsWith(getClass().getName());
}
@Test
public void verifyApplicationContextSet() {
void verifyApplicationContextSet() {
assertThat(this.applicationContext).as("The application context should have been set due to ApplicationContextAware semantics.").isNotNull();
}
@Test
public void verifyBeanInitialized() {
void verifyBeanInitialized() {
assertThat(this.beanInitialized).as("This test bean should have been initialized due to InitializingBean semantics.").isTrue();
}
@Test
public void verifyAnnotationAutowiredAndInjectedFields() {
void verifyAnnotationAutowiredAndInjectedFields() {
assertThat(this.nonrequiredLong).as("The nonrequiredLong field should NOT have been autowired.").isNull();
assertThat(this.quux).as("The quux field should have been autowired via @Autowired and @Qualifier.").isEqualTo("Quux");
assertThat(this.namedQuux).as("The namedFoo field should have been injected via @Inject and @Named.").isEqualTo("Quux");
@@ -192,13 +195,13 @@ public class SpringJUnit4ClassRunnerAppCtxTests implements ApplicationContextAwa
}
@Test
public void verifyAnnotationAutowiredMethods() {
void verifyAnnotationAutowiredMethods() {
assertThat(this.employee).as("The employee setter method should have been autowired.").isNotNull();
assertThat(this.employee.getName()).isEqualTo("John Smith");
}
@Test
public void verifyAutowiredAtValueFields() {
void verifyAutowiredAtValueFields() {
assertThat(this.literalFieldValue).as("Literal @Value field should have been autowired").isNotNull();
assertThat(this.spelFieldValue).as("SpEL @Value field should have been autowired.").isNotNull();
assertThat(this.literalFieldValue).isEqualTo("enigma");
@@ -206,7 +209,7 @@ public class SpringJUnit4ClassRunnerAppCtxTests implements ApplicationContextAwa
}
@Test
public void verifyAutowiredAtValueMethods() {
void verifyAutowiredAtValueMethods() {
assertThat(this.literalParameterValue).as("Literal @Value method parameter should have been autowired.").isNotNull();
assertThat(this.spelParameterValue).as("SpEL @Value method parameter should have been autowired.").isNotNull();
assertThat(this.literalParameterValue).isEqualTo("enigma");
@@ -214,12 +217,12 @@ public class SpringJUnit4ClassRunnerAppCtxTests implements ApplicationContextAwa
}
@Test
public void verifyResourceAnnotationInjectedFields() {
void verifyResourceAnnotationInjectedFields() {
assertThat(this.foo).as("The foo field should have been injected via @Resource.").isEqualTo("Foo");
}
@Test
public void verifyResourceAnnotationInjectedMethods() {
void verifyResourceAnnotationInjectedMethods() {
assertThat(this.bar).as("The bar method should have been wired via @Resource.").isEqualTo("Bar");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,17 +14,15 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.testfixture.beans.Employee;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import static org.assertj.core.api.Assertions.assertThat;
@@ -39,15 +37,25 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
* @see DefaultLoaderDefaultConfigClassesBaseTests
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class)
public class DefaultConfigClassesBaseTests {
@SpringJUnitConfig(loader = AnnotationConfigContextLoader.class)
class DefaultConfigClassesBaseTests {
@Configuration
@Autowired
Employee employee;
@Test
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee field should have been autowired.").isNotNull();
assertThat(this.employee.getName()).isEqualTo("John Smith");
}
@Configuration(proxyBeanMethods = false)
static class ContextConfiguration {
@Bean
public Employee employee() {
Employee employee() {
Employee employee = new Employee();
employee.setName("John Smith");
employee.setAge(42);
@@ -56,15 +64,4 @@ public class DefaultConfigClassesBaseTests {
}
}
@Autowired
protected Employee employee;
@Test
public void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee field should have been autowired.").isNotNull();
assertThat(this.employee.getName()).isEqualTo("John Smith");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.testfixture.beans.Pet;
@@ -37,26 +37,26 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
*/
@ContextConfiguration
public class DefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests {
@Configuration
static class ContextConfiguration {
@Bean
public Pet pet() {
return new Pet("Fido");
}
}
class DefaultConfigClassesInheritedTests extends DefaultConfigClassesBaseTests {
@Autowired
private Pet pet;
Pet pet;
@Test
public void verifyPetSetFromExtendedContextConfig() {
void verifyPetSetFromExtendedContextConfig() {
assertThat(this.pet).as("The pet should have been autowired.").isNotNull();
assertThat(this.pet.getName()).isEqualTo("Fido");
}
@Configuration(proxyBeanMethods = false)
static class ContextConfiguration {
@Bean
Pet pet() {
return new Pet("Fido");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.testfixture.beans.Employee;
import org.springframework.context.annotation.Bean;
@@ -35,14 +35,22 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
*/
@ContextConfiguration
public class DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests extends
class DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests extends
DefaultLoaderDefaultConfigClassesBaseTests {
@Configuration
@Test
@Override
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda");
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
public Employee employee() {
Employee employee() {
Employee employee = new Employee();
employee.setName("Yoda");
employee.setAge(900);
@@ -51,12 +59,4 @@ public class DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests exten
}
}
@Test
@Override
public void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.support.DelegatingSmartContextLoader;
@@ -32,12 +32,12 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
*/
@ContextConfiguration(classes = DefaultLoaderBeanOverridingDefaultConfigClassesInheritedTests.Config.class)
public class DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests extends
class DefaultLoaderBeanOverridingExplicitConfigClassesInheritedTests extends
DefaultLoaderExplicitConfigClassesBaseTests {
@Test
@Override
public void verifyEmployeeSetFromBaseContextConfig() {
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).as("The employee bean should have been overridden.").isEqualTo("Yoda");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,17 +14,15 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.testfixture.beans.Employee;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.support.DelegatingSmartContextLoader;
import static org.assertj.core.api.Assertions.assertThat;
@@ -38,15 +36,25 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
* @see DefaultConfigClassesBaseTests
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class DefaultLoaderDefaultConfigClassesBaseTests {
@SpringJUnitConfig
class DefaultLoaderDefaultConfigClassesBaseTests {
@Configuration
@Autowired
Employee employee;
@Test
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee field should have been autowired.").isNotNull();
assertThat(this.employee.getName()).isEqualTo("John Smith");
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
public Employee employee() {
Employee employee() {
Employee employee = new Employee();
employee.setName("John Smith");
employee.setAge(42);
@@ -55,15 +63,4 @@ public class DefaultLoaderDefaultConfigClassesBaseTests {
}
}
@Autowired
protected Employee employee;
@Test
public void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee field should have been autowired.").isNotNull();
assertThat(this.employee.getName()).isEqualTo("John Smith");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,9 +14,9 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.testfixture.beans.Pet;
@@ -36,26 +36,26 @@ import static org.assertj.core.api.Assertions.assertThat;
* @since 3.1
*/
@ContextConfiguration
public class DefaultLoaderDefaultConfigClassesInheritedTests extends DefaultLoaderDefaultConfigClassesBaseTests {
@Configuration
static class Config {
@Bean
public Pet pet() {
return new Pet("Fido");
}
}
class DefaultLoaderDefaultConfigClassesInheritedTests extends DefaultLoaderDefaultConfigClassesBaseTests {
@Autowired
private Pet pet;
Pet pet;
@Test
public void verifyPetSetFromExtendedContextConfig() {
void verifyPetSetFromExtendedContextConfig() {
assertThat(this.pet).as("The pet should have been autowired.").isNotNull();
assertThat(this.pet.getName()).isEqualTo("Fido");
}
@Configuration(proxyBeanMethods = false)
static class Config {
@Bean
Pet pet() {
return new Pet("Fido");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,13 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.testfixture.beans.Employee;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.support.DelegatingSmartContextLoader;
import static org.assertj.core.api.Assertions.assertThat;
@@ -35,16 +33,15 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Sam Brannen
* @since 3.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DefaultLoaderDefaultConfigClassesBaseTests.Config.class)
public class DefaultLoaderExplicitConfigClassesBaseTests {
@SpringJUnitConfig(DefaultLoaderDefaultConfigClassesBaseTests.Config.class)
class DefaultLoaderExplicitConfigClassesBaseTests {
@Autowired
protected Employee employee;
Employee employee;
@Test
public void verifyEmployeeSetFromBaseContextConfig() {
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).isEqualTo("John Smith");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,13 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.testfixture.beans.Pet;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.support.DelegatingSmartContextLoader;
import static org.assertj.core.api.Assertions.assertThat;
@@ -35,16 +33,15 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Sam Brannen
* @since 3.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DefaultLoaderDefaultConfigClassesInheritedTests.Config.class)
public class DefaultLoaderExplicitConfigClassesInheritedTests extends DefaultLoaderExplicitConfigClassesBaseTests {
@SpringJUnitConfig(DefaultLoaderDefaultConfigClassesInheritedTests.Config.class)
class DefaultLoaderExplicitConfigClassesInheritedTests extends DefaultLoaderExplicitConfigClassesBaseTests {
@Autowired
private Pet pet;
Pet pet;
@Test
public void verifyPetSetFromExtendedContextConfig() {
void verifyPetSetFromExtendedContextConfig() {
assertThat(this.pet).as("The pet should have been autowired.").isNotNull();
assertThat(this.pet.getName()).isEqualTo("Fido");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,15 +14,13 @@
* limitations under the License.
*/
package org.springframework.test.context.junit4.annotation;
package org.springframework.test.context.config;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.testfixture.beans.Employee;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import static org.assertj.core.api.Assertions.assertThat;
@@ -36,16 +34,15 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Sam Brannen
* @since 3.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesBaseTests.ContextConfiguration.class)
public class ExplicitConfigClassesBaseTests {
@SpringJUnitConfig(loader = AnnotationConfigContextLoader.class, classes = DefaultConfigClassesBaseTests.ContextConfiguration.class)
class ExplicitConfigClassesBaseTests {
@Autowired
protected Employee employee;
Employee employee;
@Test
public void verifyEmployeeSetFromBaseContextConfig() {
void verifyEmployeeSetFromBaseContextConfig() {
assertThat(this.employee).as("The employee should have been autowired.").isNotNull();
assertThat(this.employee.getName()).isEqualTo("John Smith");
}

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