Compare commits

..

1 Commits

Author SHA1 Message Date
Brian Clozel 0f19fa5d8e Release v6.2.5 2025-03-19 18:03:14 +01:00
234 changed files with 1647 additions and 6256 deletions
@@ -15,7 +15,7 @@ runs:
using: composite
steps:
- name: Generate Changelog
uses: spring-io/github-changelog-generator@86958813a62af8fb223b3fd3b5152035504bcb83 #v0.0.12
uses: spring-io/github-changelog-generator@185319ad7eaa75b0e8e72e4b6db19c8b2cb8c4c1 #v0.0.11
with:
config-file: .github/actions/create-github-release/changelog-generator.yml
milestone: ${{ inputs.milestone }}
+1 -1
View File
@@ -102,7 +102,7 @@ configure([rootProject] + javaProjects) { project ->
// TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+.
// See https://github.com/spring-projects/spring-framework/issues/27497
//
// "https://junit.org/junit5/docs/5.12.2/api/",
// "https://junit.org/junit5/docs/5.12.1/api/",
"https://www.reactive-streams.org/reactive-streams-1.0.3-javadoc/",
//"https://javadoc.io/static/io.rsocket/rsocket-core/1.1.1/",
"https://r2dbc.io/spec/1.0.0.RELEASE/api/",
@@ -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.21.4");
checkstyle.getConfigDirectory().set(project.getRootProject().file("src/checkstyle"));
String version = SpringJavaFormatPlugin.class.getPackage().getImplementationVersion();
DependencySet checkstyleDependencies = project.getConfigurations().getByName("checkstyle").getDependencies();
+2 -3
View File
@@ -31,9 +31,8 @@ javadoc {
destinationDir = project.java.docsDir.dir("javadoc-api").get().asFile
splitIndex = true
links(rootProject.ext.javadocLinks)
// Check for 'syntax' and 'reference' during linting.
addBooleanOption('Xdoclint:syntax,reference', true)
addBooleanOption('Werror', true) // fail build on Javadoc warnings
addBooleanOption('Xdoclint:syntax,reference', true) // only check syntax and reference with doclint
addBooleanOption('Werror', true) // fail build on Javadoc warnings
}
maxMemory = "1024m"
doFirst {
+29 -29
View File
@@ -42,34 +42,34 @@ repositories {
}
dependencies {
implementation(project(":spring-aspects"))
implementation(project(":spring-context"))
implementation(project(":spring-context-support"))
implementation(project(":spring-core-test"))
implementation(project(":spring-jdbc"))
implementation(project(":spring-jms"))
implementation(project(":spring-test"))
implementation(project(":spring-web"))
implementation(project(":spring-webflux"))
implementation(project(":spring-webmvc"))
implementation(project(":spring-websocket"))
api(project(":spring-aspects"))
api(project(":spring-context"))
api(project(":spring-context-support"))
api(project(":spring-core-test"))
api(project(":spring-jdbc"))
api(project(":spring-jms"))
api(project(":spring-test"))
api(project(":spring-web"))
api(project(":spring-webflux"))
api(project(":spring-webmvc"))
api(project(":spring-websocket"))
api("com.fasterxml.jackson.core:jackson-databind")
api("com.fasterxml.jackson.module:jackson-module-parameter-names")
api("com.mchange:c3p0:0.9.5.5")
api("com.oracle.database.jdbc:ojdbc11")
api("io.projectreactor.netty:reactor-netty-http")
api("jakarta.jms:jakarta.jms-api")
api("jakarta.servlet:jakarta.servlet-api")
api("jakarta.resource:jakarta.resource-api")
api("jakarta.validation:jakarta.validation-api")
api("javax.cache:cache-api")
api("org.apache.activemq:activemq-ra:6.1.2")
api("org.apache.commons:commons-dbcp2:2.11.0")
api("org.aspectj:aspectjweaver")
api("org.assertj:assertj-core")
api("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
api("org.jetbrains.kotlin:kotlin-stdlib")
api("org.junit.jupiter:junit-jupiter-api")
implementation("com.fasterxml.jackson.core:jackson-databind")
implementation("com.fasterxml.jackson.module:jackson-module-parameter-names")
implementation("com.mchange:c3p0:0.9.5.5")
implementation("com.oracle.database.jdbc:ojdbc11")
implementation("io.projectreactor.netty:reactor-netty-http")
implementation("jakarta.jms:jakarta.jms-api")
implementation("jakarta.servlet:jakarta.servlet-api")
implementation("jakarta.resource:jakarta.resource-api")
implementation("jakarta.validation:jakarta.validation-api")
implementation("jakarta.websocket:jakarta.websocket-client-api")
implementation("javax.cache:cache-api")
implementation("org.apache.activemq:activemq-ra:6.1.2")
implementation("org.apache.commons:commons-dbcp2:2.11.0")
implementation("org.aspectj:aspectjweaver")
implementation("org.assertj:assertj-core")
implementation("org.eclipse.jetty.websocket:jetty-websocket-jetty-api")
implementation("org.jetbrains.kotlin:kotlin-stdlib")
implementation("org.junit.jupiter:junit-jupiter-api")
}
@@ -92,12 +92,6 @@ the repeated JNDI lookup overhead. See
{spring-framework-api}++/jndi/JndiLocatorDelegate.html#IGNORE_JNDI_PROPERTY_NAME++[`JndiLocatorDelegate`]
for details.
| `spring.locking.strict`
| Instructs Spring to enforce strict locking during bean creation, rather than the mix of
strict and lenient locking that 6.2 applies by default. See
{spring-framework-api}++/beans/factory/support/DefaultListableBeanFactory.html#STRICT_LOCKING_PROPERTY_NAME++[`DefaultListableBeanFactory`]
for details.
| `spring.objenesis.ignore`
| Instructs Spring to ignore Objenesis, not even attempting to use it. See
{spring-framework-api}++/objenesis/SpringObjenesis.html#IGNORE_OBJENESIS_PROPERTY_NAME++[`SpringObjenesis`]
@@ -469,20 +469,20 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);
RootBeanDefinition beanDefinition = new RootBeanDefinition(ClientFactoryBean.class);
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean.class, MyClient.class));
// ...
registry.registerBeanDefinition("myClient", beanDefinition);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java)
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java));
// ...
registry.registerBeanDefinition("myClient", beanDefinition)
val beanDefinition = RootBeanDefinition(ClientFactoryBean::class.java)
beanDefinition.setTargetType(ResolvableType.forClassWithGenerics(ClientFactoryBean::class.java, MyClient::class.java));
// ...
registry.registerBeanDefinition("myClient", beanDefinition)
----
======
@@ -9,15 +9,15 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Component
public class MovieRecommender {
@Component
public class MovieRecommender {
private final String catalog;
private final String catalog;
public MovieRecommender(@Value("${catalog.name}") String catalog) {
this.catalog = catalog;
}
}
public MovieRecommender(@Value("${catalog.name}") String catalog) {
this.catalog = catalog;
}
}
----
Kotlin::
@@ -37,9 +37,9 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }
@Configuration
@PropertySource("classpath:application.properties")
public class AppConfig { }
----
Kotlin::
@@ -56,7 +56,7 @@ And the following `application.properties` file:
[source,java,indent=0,subs="verbatim,quotes"]
----
catalog.name=MovieCatalog
catalog.name=MovieCatalog
----
In that case, the `catalog` parameter and field will be equal to the `MovieCatalog` value.
@@ -119,15 +119,15 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Component
public class MovieRecommender {
@Component
public class MovieRecommender {
private final String catalog;
private final String catalog;
public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
this.catalog = catalog;
}
}
public MovieRecommender(@Value("${catalog.name:defaultCatalog}") String catalog) {
this.catalog = catalog;
}
}
----
Kotlin::
@@ -150,16 +150,16 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class AppConfig {
@Configuration
public class AppConfig {
@Bean
public ConversionService conversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addConverter(new MyCustomConverter());
return conversionService;
}
}
@Bean
public ConversionService conversionService() {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
conversionService.addConverter(new MyCustomConverter());
return conversionService;
}
}
----
Kotlin::
@@ -188,15 +188,15 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Component
public class MovieRecommender {
@Component
public class MovieRecommender {
private final String catalog;
private final String catalog;
public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
this.catalog = catalog;
}
}
public MovieRecommender(@Value("#{systemProperties['user.catalog'] + 'Catalog' }") String catalog) {
this.catalog = catalog;
}
}
----
Kotlin::
@@ -217,16 +217,16 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Component
public class MovieRecommender {
@Component
public class MovieRecommender {
private final Map<String, Integer> countOfMoviesPerCatalog;
private final Map<String, Integer> countOfMoviesPerCatalog;
public MovieRecommender(
@Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
}
}
public MovieRecommender(
@Value("#{{'Thriller': 100, 'Comedy': 300}}") Map<String, Integer> countOfMoviesPerCatalog) {
this.countOfMoviesPerCatalog = countOfMoviesPerCatalog;
}
}
----
Kotlin::
@@ -119,7 +119,7 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
----
======
@@ -310,16 +310,16 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.springframework.beans.factory.getBean
import org.springframework.beans.factory.getBean
// create and configure beans
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
val context = ClassPathXmlApplicationContext("services.xml", "daos.xml")
// retrieve configured instance
val service = context.getBean<PetStoreService>("petStore")
// retrieve configured instance
val service = context.getBean<PetStoreService>("petStore")
// use configured instance
var userList = service.getUsernameList()
// use configured instance
var userList = service.getUsernameList()
----
======
@@ -513,7 +513,7 @@ the classes above:
<property name="notificationAddress" value="blockedlist@example.org"/>
</bean>
<!-- optional: a custom ApplicationEventMulticaster definition -->
<!-- optional: a custom ApplicationEventMulticaster definition -->
<bean id="applicationEventMulticaster" class="org.springframework.context.event.SimpleApplicationEventMulticaster">
<property name="taskExecutor" ref="..."/>
<property name="errorHandler" ref="..."/>
@@ -226,7 +226,7 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
import org.springframework.beans.factory.getBean
import org.springframework.beans.factory.getBean
fun main() {
val ctx = ClassPathXmlApplicationContext("scripting/beans.xml")
@@ -31,7 +31,7 @@ Java::
----
public class MyBean {
private final Log log = LogFactory.getLog(getClass());
// ...
// ...
}
----
@@ -40,8 +40,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MyBean {
private val log = LogFactory.getLog(javaClass)
// ...
private val log = LogFactory.getLog(javaClass)
// ...
}
----
======
@@ -399,7 +399,7 @@ A `ConstraintViolation` on the `degrees` method parameter is adapted to a
`MessageSourceResolvable` with the following:
- Error codes `"Max.myService#addStudent.degrees"`, `"Max.degrees"`, `"Max.int"`, `"Max"`
- Message arguments "degrees" and 2 (the field name and the constraint attribute)
- Message arguments "degrees2 and 2 (the field name and the constraint attribute)
- Default message "must be less than or equal to 2"
To customize the above default message, you can add a property such as:
@@ -78,27 +78,27 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@Configuration
public class DataSourceConfig {
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setDatabaseConfigurer(EmbeddedDatabaseConfigurers
.customizeConfigurer(H2, this::customize))
.addScript("schema.sql")
.build();
}
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setDatabaseConfigurer(EmbeddedDatabaseConfigurers
.customizeConfigurer(H2, this::customize))
.addScript("schema.sql")
.build();
}
private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) {
return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) {
@Override
public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {
super.configureConnectionProperties(properties, databaseName);
properties.setDriverClass(CustomDriver.class);
}
};
}
private EmbeddedDatabaseConfigurer customize(EmbeddedDatabaseConfigurer defaultConfigurer) {
return new EmbeddedDatabaseConfigurerDelegate(defaultConfigurer) {
@Override
public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {
super.configureConnectionProperties(properties, databaseName);
properties.setDriverClass(CustomDriver.class);
}
};
}
}
----
@@ -136,7 +136,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
Mono<Void> completion = client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.then();
.then();
----
Kotlin::
@@ -144,7 +144,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.sql("CREATE TABLE person (id VARCHAR(255) PRIMARY KEY, name VARCHAR(255), age INTEGER);")
.await()
.await()
----
======
@@ -173,7 +173,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person")
.fetch().first();
.fetch().first();
----
Kotlin::
@@ -181,7 +181,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val first = client.sql("SELECT id, name FROM person")
.fetch().awaitSingle()
.fetch().awaitSingle()
----
======
@@ -194,8 +194,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
Mono<Map<String, Object>> first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().first();
.bind("fn", "Joe")
.fetch().first();
----
Kotlin::
@@ -203,8 +203,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val first = client.sql("SELECT id, name FROM person WHERE first_name = :fn")
.bind("fn", "Joe")
.fetch().awaitSingle()
.bind("fn", "Joe")
.fetch().awaitSingle()
----
======
@@ -240,8 +240,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
Flux<String> names = client.sql("SELECT name FROM person")
.map(row -> row.get("name", String.class))
.all();
.map(row -> row.get("name", String.class))
.all();
----
Kotlin::
@@ -249,8 +249,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val names = client.sql("SELECT name FROM person")
.map{ row: Row -> row.get("name", String.class) }
.flow()
.map{ row: Row -> row.get("name", String.class) }
.flow()
----
======
@@ -301,8 +301,8 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
Mono<Integer> affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().rowsUpdated();
.bind("fn", "Joe")
.fetch().rowsUpdated();
----
Kotlin::
@@ -310,8 +310,8 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val affectedRows = client.sql("UPDATE person SET first_name = :fn")
.bind("fn", "Joe")
.fetch().awaitRowsUpdated()
.bind("fn", "Joe")
.fetch().awaitRowsUpdated()
----
======
@@ -337,9 +337,9 @@ The following example shows parameter binding for a query:
[source,java]
----
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind("id", "joe")
.bind("name", "Joe")
.bind("age", 34);
----
@@ -369,9 +369,9 @@ Indices are zero based.
[source,java]
----
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind(0, "joe")
.bind(1, "Joe")
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bind(0, "joe")
.bind(1, "Joe")
.bind(2, 34);
----
@@ -379,9 +379,9 @@ In case your application is binding to many parameters, the same can be achieved
[source,java]
----
List<?> values = List.of("joe", "Joe", 34);
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindValues(values);
List<?> values = List.of("joe", "Joe", 34);
db.sql("INSERT INTO person (id, name, age) VALUES(:id, :name, :age)")
.bindValues(values);
----
@@ -428,7 +428,7 @@ Java::
tuples.add(new Object[] {"Ann", 50});
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples);
.bind("tuples", tuples);
----
Kotlin::
@@ -440,7 +440,7 @@ Kotlin::
tuples.add(arrayOf("Ann", 50))
client.sql("SELECT id, name, state FROM table WHERE (name, age) IN (:tuples)")
.bind("tuples", tuples)
.bind("tuples", tuples)
----
======
@@ -455,7 +455,7 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", Arrays.asList(35, 50));
.bind("ages", Arrays.asList(35, 50));
----
Kotlin::
@@ -463,7 +463,7 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.sql("SELECT id, name, state FROM table WHERE age IN (:ages)")
.bind("ages", arrayOf(35, 50))
.bind("ages", arrayOf(35, 50))
----
======
@@ -490,9 +490,9 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
.filter((s, next) -> next.execute(s.returnGeneratedValues("id")))
.bind("name", …)
.bind("state", …);
----
Kotlin::
@@ -516,10 +516,10 @@ Java::
[source,java,indent=0,subs="verbatim,quotes"]
----
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter(statement -> s.returnGeneratedValues("id"));
.filter(statement -> s.returnGeneratedValues("id"));
client.sql("SELECT id, name, state FROM table")
.filter(statement -> s.fetchSize(25));
.filter(statement -> s.fetchSize(25));
----
Kotlin::
@@ -527,10 +527,10 @@ Kotlin::
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
client.sql("INSERT INTO table (name, state) VALUES(:name, :state)")
.filter { statement -> s.returnGeneratedValues("id") }
.filter { statement -> s.returnGeneratedValues("id") }
client.sql("SELECT id, name, state FROM table")
.filter { statement -> s.fetchSize(25) }
.filter { statement -> s.fetchSize(25) }
----
======
@@ -55,11 +55,11 @@ a "shared objects file" source, as shown in the following example:
[source,shell,indent=0,subs="verbatim"]
----
[0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top)
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
[0.064s][info][class,load] org.springframework.core.env.EnvironmentCapable source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.BeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.ListableBeanFactory source: shared objects file (top)
[0.064s][info][class,load] org.springframework.beans.factory.HierarchicalBeanFactory source: shared objects file (top)
[0.065s][info][class,load] org.springframework.context.MessageSource source: shared objects file (top)
----
If CDS can't be enabled or if you have a large number of classes that are not loaded from the cache, make sure that
@@ -30,36 +30,36 @@ Java::
+
[source,java,indent=0,subs="verbatim"]
----
RestClient defaultClient = RestClient.create();
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();
RestClient defaultClient = RestClient.create();
RestClient customClient = RestClient.builder()
.requestFactory(new HttpComponentsClientHttpRequestFactory())
.messageConverters(converters -> converters.add(new MyCustomMessageConverter()))
.baseUrl("https://example.com")
.defaultUriVariables(Map.of("variable", "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim"]
----
val defaultClient = RestClient.create()
val customClient = RestClient.builder()
.requestFactory(HttpComponentsClientHttpRequestFactory())
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
.baseUrl("https://example.com")
.defaultUriVariables(mapOf("variable" to "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build()
val defaultClient = RestClient.create()
val customClient = RestClient.builder()
.requestFactory(HttpComponentsClientHttpRequestFactory())
.messageConverters { converters -> converters.add(MyCustomMessageConverter()) }
.baseUrl("https://example.com")
.defaultUriVariables(mapOf("variable" to "foo"))
.defaultHeader("My-Header", "Foo")
.defaultCookie("My-Cookie", "Bar")
.requestInterceptor(myCustomInterceptor)
.requestInitializer(myCustomInitializer)
.build()
----
======
@@ -81,20 +81,20 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
int id = 42;
restClient.get()
.uri("https://example.com/orders/{id}", id)
// ...
int id = 42;
restClient.get()
.uri("https://example.com/orders/{id}", id)
....
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val id = 42
restClient.get()
.uri("https://example.com/orders/{id}", id)
// ...
val id = 42
restClient.get()
.uri("https://example.com/orders/{id}", id)
...
----
======
@@ -133,12 +133,12 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
String result = restClient.get() <1>
.uri("https://example.com") <2>
.retrieve() <3>
.body(String.class); <4>
System.out.println(result); <5>
String result = restClient.get() <1>
.uri("https://example.com") <2>
.retrieve() <3>
.body(String.class); <4>
System.out.println(result); <5>
----
<1> Set up a GET request
<2> Specify the URL to connect to
@@ -150,12 +150,12 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result= restClient.get() <1>
.uri("https://example.com") <2>
.retrieve() <3>
.body<String>() <4>
println(result) <5>
val result= restClient.get() <1>
.uri("https://example.com") <2>
.retrieve() <3>
.body<String>() <4>
println(result) <5>
----
<1> Set up a GET request
<2> Specify the URL to connect to
@@ -172,14 +172,14 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
ResponseEntity<String> result = restClient.get() <1>
.uri("https://example.com") <1>
.retrieve()
.toEntity(String.class); <2>
System.out.println("Response status: " + result.getStatusCode()); <3>
System.out.println("Response headers: " + result.getHeaders()); <3>
System.out.println("Contents: " + result.getBody()); <3>
ResponseEntity<String> result = restClient.get() <1>
.uri("https://example.com") <1>
.retrieve()
.toEntity(String.class); <2>
System.out.println("Response status: " + result.getStatusCode()); <3>
System.out.println("Response headers: " + result.getHeaders()); <3>
System.out.println("Contents: " + result.getBody()); <3>
----
<1> Set up a GET request for the specified URL
<2> Convert the response into a `ResponseEntity`
@@ -189,14 +189,14 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = restClient.get() <1>
.uri("https://example.com") <1>
.retrieve()
.toEntity<String>() <2>
println("Response status: " + result.statusCode) <3>
println("Response headers: " + result.headers) <3>
println("Contents: " + result.body) <3>
val result = restClient.get() <1>
.uri("https://example.com") <1>
.retrieve()
.toEntity<String>() <2>
println("Response status: " + result.statusCode) <3>
println("Response headers: " + result.headers) <3>
println("Contents: " + result.body) <3>
----
<1> Set up a GET request for the specified URL
<2> Convert the response into a `ResponseEntity`
@@ -212,12 +212,12 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
int id = ...;
Pet pet = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) <1>
.accept(APPLICATION_JSON) <2>
.retrieve()
.body(Pet.class); <3>
int id = ...;
Pet pet = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) <1>
.accept(APPLICATION_JSON) <2>
.retrieve()
.body(Pet.class); <3>
----
<1> Using URI variables
<2> Set the `Accept` header to `application/json`
@@ -227,12 +227,12 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val id = ...
val pet = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) <1>
.accept(APPLICATION_JSON) <2>
.retrieve()
.body<Pet>() <3>
val id = ...
val pet = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id) <1>
.accept(APPLICATION_JSON) <2>
.retrieve()
.body<Pet>() <3>
----
<1> Using URI variables
<2> Set the `Accept` header to `application/json`
@@ -247,13 +247,13 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
Pet pet = ... <1>
ResponseEntity<Void> response = restClient.post() <2>
.uri("https://petclinic.example.com/pets/new") <2>
.contentType(APPLICATION_JSON) <3>
.body(pet) <4>
.retrieve()
.toBodilessEntity(); <5>
Pet pet = ... <1>
ResponseEntity<Void> response = restClient.post() <2>
.uri("https://petclinic.example.com/pets/new") <2>
.contentType(APPLICATION_JSON) <3>
.body(pet) <4>
.retrieve()
.toBodilessEntity(); <5>
----
<1> Create a `Pet` domain object
<2> Set up a POST request, and the URL to connect to
@@ -265,13 +265,13 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val pet: Pet = ... <1>
val response = restClient.post() <2>
.uri("https://petclinic.example.com/pets/new") <2>
.contentType(APPLICATION_JSON) <3>
.body(pet) <4>
.retrieve()
.toBodilessEntity() <5>
val pet: Pet = ... <1>
val response = restClient.post() <2>
.uri("https://petclinic.example.com/pets/new") <2>
.contentType(APPLICATION_JSON) <3>
.body(pet) <4>
.retrieve()
.toBodilessEntity() <5>
----
<1> Create a `Pet` domain object
<2> Set up a POST request, and the URL to connect to
@@ -291,13 +291,13 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
String result = restClient.get() <1>
.uri("https://example.com/this-url-does-not-exist") <1>
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2>
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3>
})
.body(String.class);
String result = restClient.get() <1>
.uri("https://example.com/this-url-does-not-exist") <1>
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { <2>
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <3>
})
.body(String.class);
----
<1> Create a GET request for a URL that returns a 404 status code
<2> Set up a status handler for all 4xx status codes
@@ -307,12 +307,12 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = restClient.get() <1>
.uri("https://example.com/this-url-does-not-exist") <1>
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2>
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3>
.body<String>()
val result = restClient.get() <1>
.uri("https://example.com/this-url-does-not-exist") <1>
.retrieve()
.onStatus(HttpStatusCode::is4xxClientError) { _, response -> <2>
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) } <3>
.body<String>()
----
<1> Create a GET request for a URL that returns a 404 status code
<2> Set up a status handler for all 4xx status codes
@@ -330,18 +330,18 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
Pet result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(APPLICATION_JSON)
.exchange((request, response) -> { <1>
if (response.getStatusCode().is4xxClientError()) { <2>
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2>
}
else {
Pet pet = convertResponse(response); <3>
return pet;
}
});
Pet result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(APPLICATION_JSON)
.exchange((request, response) -> { <1>
if (response.getStatusCode().is4xxClientError()) { <2>
throw new MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()); <2>
}
else {
Pet pet = convertResponse(response); <3>
return pet;
}
});
----
<1> `exchange` provides the request and response
<2> Throw an exception when the response has a 4xx status code
@@ -351,17 +351,17 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.exchange { request, response -> <1>
if (response.getStatusCode().is4xxClientError()) { <2>
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2>
} else {
val pet: Pet = convertResponse(response) <3>
pet
}
}
val result = restClient.get()
.uri("https://petclinic.example.com/pets/{id}", id)
.accept(MediaType.APPLICATION_JSON)
.exchange { request, response -> <1>
if (response.getStatusCode().is4xxClientError()) { <2>
throw MyCustomRuntimeException(response.getStatusCode(), response.getHeaders()) <2>
} else {
val pet: Pet = convertResponse(response) <3>
pet
}
}
----
<1> `exchange` provides the request and response
<2> Throw an exception when the response has a 4xx status code
@@ -380,14 +380,15 @@ To serialize only a subset of the object properties, you can specify a {baeldung
[source,java,indent=0,subs="verbatim"]
----
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
.contentType(APPLICATION_JSON)
.body(value)
.retrieve()
.toBodilessEntity();
MappingJacksonValue value = new MappingJacksonValue(new User("eric", "7!jd#h23"));
value.setSerializationView(User.WithoutPasswordView.class);
ResponseEntity<Void> response = restClient.post() // or RestTemplate.postForEntity
.contentType(APPLICATION_JSON)
.body(value)
.retrieve()
.toBodilessEntity();
----
==== Multipart
@@ -397,24 +398,24 @@ For example:
[source,java,indent=0,subs="verbatim"]
----
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
// send using RestClient.post or RestTemplate.postForEntity
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>();
parts.add("fieldPart", "fieldValue");
parts.add("filePart", new FileSystemResource("...logo.png"));
parts.add("jsonPart", new Person("Jason"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
parts.add("xmlPart", new HttpEntity<>(myBean, headers));
// send using RestClient.post or RestTemplate.postForEntity
----
In most cases, you do not have to specify the `Content-Type` for each part.
The content type is determined automatically based on the `HttpMessageConverter` chosen to serialize it or, in the case of a `Resource`, based on the file extension.
If necessary, you can explicitly provide the `MediaType` with an `HttpEntity` wrapper.
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
Once the `MultiValueMap` is ready, you can use it as the body of a `POST` request, using `RestClient.post().body(parts)` (or `RestTemplate.postForObject`).
If the `MultiValueMap` contains at least one non-`String` value, the `Content-Type` is set to `multipart/form-data` by the `FormHttpMessageConverter`.
If the `MultiValueMap` has `String` values, the `Content-Type` defaults to `application/x-www-form-urlencoded`.
@@ -1136,11 +1137,11 @@ performed through the client:
[source,java,indent=0,subs="verbatim,quotes"]
----
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(myErrorHandler);
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(myErrorHandler);
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
----
For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and
@@ -215,43 +215,45 @@ For suspending functions, a `TransactionalOperator.executeAndAwait` extension is
[source,kotlin,indent=0]
----
import org.springframework.transaction.reactive.executeAndAwait
import org.springframework.transaction.reactive.executeAndAwait
class PersonRepository(private val operator: TransactionalOperator) {
class PersonRepository(private val operator: TransactionalOperator) {
suspend fun initDatabase() = operator.executeAndAwait {
insertPerson1()
insertPerson2()
}
suspend fun initDatabase() = operator.executeAndAwait {
insertPerson1()
insertPerson2()
}
private suspend fun insertPerson1() {
// INSERT SQL statement
}
private suspend fun insertPerson1() {
// INSERT SQL statement
}
private suspend fun insertPerson2() {
// INSERT SQL statement
}
}
private suspend fun insertPerson2() {
// INSERT SQL statement
}
}
----
For Kotlin `Flow`, a `Flow<T>.transactional` extension is provided.
[source,kotlin,indent=0]
----
import org.springframework.transaction.reactive.transactional
import org.springframework.transaction.reactive.transactional
class PersonRepository(private val operator: TransactionalOperator) {
class PersonRepository(private val operator: TransactionalOperator) {
fun updatePeople() = findPeople().map(::updatePerson).transactional(operator)
fun updatePeople() = findPeople().map(::updatePerson).transactional(operator)
private fun findPeople(): Flow<Person> {
// SELECT SQL statement
}
private fun findPeople(): Flow<Person> {
// SELECT SQL statement
}
private suspend fun updatePerson(person: Person): Person {
// UPDATE SQL statement
}
}
private suspend fun updatePerson(person: Person): Person {
// UPDATE SQL statement
}
}
----
@@ -296,17 +296,17 @@ for example when writing a `org.springframework.core.convert.converter.Converter
[source,kotlin,indent=0]
----
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
// ...
}
class ListOfFooConverter : Converter<List<Foo>, CustomJavaList<out Foo>> {
// ...
}
----
When converting any kind of objects, star projection with `*` can be used instead of `out Any`.
[source,kotlin,indent=0]
----
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
// ...
}
class ListOfAnyConverter : Converter<List<*>, CustomJavaList<*>> {
// ...
}
----
NOTE: Spring Framework does not leverage yet declaration-site variance type information for injecting beans,
@@ -340,14 +340,13 @@ file with a `spring.test.constructor.autowire.mode = all` property.
[source,kotlin,indent=0]
----
@SpringJUnitConfig(TestConfig::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
class OrderServiceIntegrationTests(
val orderService: OrderService,
val customerService: CustomerService) {
// tests that use the injected OrderService and CustomerService
}
@SpringJUnitConfig(TestConfig::class)
@TestConstructor(autowireMode = AutowireMode.ALL)
class OrderServiceIntegrationTests(val orderService: OrderService,
val customerService: CustomerService) {
// tests that use the injected OrderService and CustomerService
}
----
@@ -369,29 +368,29 @@ The following example demonstrates `@BeforeAll` and `@AfterAll` annotations on n
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class IntegrationTests {
val application = Application(8181)
val client = WebClient.create("http://localhost:8181")
val application = Application(8181)
val client = WebClient.create("http://localhost:8181")
@BeforeAll
fun beforeAll() {
application.start()
}
@BeforeAll
fun beforeAll() {
application.start()
}
@Test
fun `Find all users on HTML page`() {
client.get().uri("/users")
.accept(TEXT_HTML)
.retrieve()
.bodyToMono<String>()
.test()
.expectNextMatches { it.contains("Foo") }
.verifyComplete()
}
@Test
fun `Find all users on HTML page`() {
client.get().uri("/users")
.accept(TEXT_HTML)
.retrieve()
.bodyToMono<String>()
.test()
.expectNextMatches { it.contains("Foo") }
.verifyComplete()
}
@AfterAll
fun afterAll() {
application.stop()
}
@AfterAll
fun afterAll() {
application.stop()
}
}
----
@@ -404,27 +403,26 @@ The following example shows how to do so:
[source,kotlin,indent=0]
----
class SpecificationLikeTests {
@Nested
@DisplayName("a calculator")
inner class Calculator {
class SpecificationLikeTests {
val calculator = SampleCalculator()
@Test
fun `should return the result of adding the first number to the second number`() {
val sum = calculator.sum(2, 4)
assertEquals(6, sum)
}
@Test
fun `should return the result of subtracting the second number from the first number`() {
val subtract = calculator.subtract(4, 2)
assertEquals(2, subtract)
}
}
}
@Nested
@DisplayName("a calculator")
inner class Calculator {
val calculator = SampleCalculator()
@Test
fun `should return the result of adding the first number to the second number`() {
val sum = calculator.sum(2, 4)
assertEquals(6, sum)
}
@Test
fun `should return the result of subtracting the second number from the first number`() {
val subtract = calculator.subtract(4, 2)
assertEquals(2, subtract)
}
}
}
----
@@ -1,6 +1,8 @@
[[kotlin-web]]
= Web
[[router-dsl]]
== Router DSL
@@ -14,27 +16,27 @@ These DSL let you write clean and idiomatic Kotlin code to build a `RouterFuncti
[source,kotlin,indent=0]
----
@Configuration
class RouterRouterConfiguration {
@Bean
fun mainRouter(userHandler: UserHandler) = router {
accept(TEXT_HTML).nest {
GET("/") { ok().render("index") }
GET("/sse") { ok().render("sse") }
GET("/users", userHandler::findAllView)
}
"/api".nest {
accept(APPLICATION_JSON).nest {
GET("/users", userHandler::findAll)
}
accept(TEXT_EVENT_STREAM).nest {
GET("/users", userHandler::stream)
}
}
resources("/**", ClassPathResource("static/"))
@Configuration
class RouterRouterConfiguration {
@Bean
fun mainRouter(userHandler: UserHandler) = router {
accept(TEXT_HTML).nest {
GET("/") { ok().render("index") }
GET("/sse") { ok().render("sse") }
GET("/users", userHandler::findAllView)
}
"/api".nest {
accept(APPLICATION_JSON).nest {
GET("/users", userHandler::findAll)
}
accept(TEXT_EVENT_STREAM).nest {
GET("/users", userHandler::stream)
}
}
resources("/**", ClassPathResource("static/"))
}
}
----
NOTE: This DSL is programmatic, meaning that it allows custom registration logic of beans
@@ -53,22 +55,22 @@ idiomatic Kotlin API and to allow better discoverability (no usage of static met
[source,kotlin,indent=0]
----
val mockMvc: MockMvc = ...
mockMvc.get("/person/{name}", "Lee") {
secure = true
accept = APPLICATION_JSON
headers {
contentLanguage = Locale.FRANCE
}
principal = Principal { "foo" }
}.andExpect {
status { isOk }
content { contentType(APPLICATION_JSON) }
jsonPath("$.name") { value("Lee") }
content { json("""{"someBoolean": false}""", false) }
}.andDo {
print()
val mockMvc: MockMvc = ...
mockMvc.get("/person/{name}", "Lee") {
secure = true
accept = APPLICATION_JSON
headers {
contentLanguage = Locale.FRANCE
}
principal = Principal { "foo" }
}.andExpect {
status { isOk }
content { contentType(APPLICATION_JSON) }
jsonPath("$.name") { value("Lee") }
content { json("""{"someBoolean": false}""", false) }
}.andDo {
print()
}
----
@@ -87,9 +89,9 @@ is possible to use such feature to render Kotlin-based templates with
`build.gradle.kts`
[source,kotlin,indent=0]
----
dependencies {
runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}")
}
dependencies {
runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}")
}
----
Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplateViewResolver` beans.
@@ -97,23 +99,23 @@ Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplat
`KotlinScriptConfiguration.kt`
[source,kotlin,indent=0]
----
@Configuration
class KotlinScriptConfiguration {
@Bean
fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
engineName = "kotlin"
setScripts("scripts/render.kts")
renderFunction = "render"
isSharedEngine = false
}
@Bean
fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
setPrefix("templates/")
setSuffix(".kts")
}
@Configuration
class KotlinScriptConfiguration {
@Bean
fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply {
engineName = "kotlin"
setScripts("scripts/render.kts")
renderFunction = "render"
isSharedEngine = false
}
@Bean
fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply {
setPrefix("templates/")
setSuffix(".kts")
}
}
----
See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example
@@ -125,7 +127,7 @@ project for more details.
== Kotlin multiplatform serialization
{kotlin-github-org}/kotlinx.serialization[Kotlin multiplatform serialization] is
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The built-in support currently targets CBOR, JSON, and ProtoBuf formats.
supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, and ProtoBuf formats.
To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin.
With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since
@@ -133,3 +135,6 @@ Kotlin serialization is designed to serialize only Kotlin classes annotated with
With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration,
if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually.
@@ -47,21 +47,6 @@ the same bean in several test classes, make sure to name the fields consistently
creating unnecessary contexts.
====
[WARNING]
====
Using `@MockitoBean` or `@MockitoSpyBean` in conjunction with `@ContextHierarchy` can
lead to undesirable results since each `@MockitoBean` or `@MockitoSpyBean` will be
applied to all context hierarchy levels by default. To ensure that a particular
`@MockitoBean` or `@MockitoSpyBean` is applied to a single context hierarchy level, set
the `contextName` attribute to match a configured `@ContextConfiguration` name for
example, `@MockitoBean(contextName = "app-config")` or
`@MockitoSpyBean(contextName = "app-config")`.
See
xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
hierarchies with bean overrides] for further details and examples.
====
Each annotation also defines Mockito-specific attributes to fine-tune the mocking behavior.
The `@MockitoBean` annotation uses the `REPLACE_OR_CREATE`
@@ -31,19 +31,6 @@ same bean in several tests, make sure to name the field consistently to avoid cr
unnecessary contexts.
====
[WARNING]
====
Using `@TestBean` in conjunction with `@ContextHierarchy` can lead to undesirable results
since each `@TestBean` will be applied to all context hierarchy levels by default. To
ensure that a particular `@TestBean` is applied to a single context hierarchy level, set
the `contextName` attribute to match a configured `@ContextConfiguration` name for
example, `@TestBean(contextName = "app-config")`.
See
xref:testing/testcontext-framework/ctx-management/hierarchies.adoc#testcontext-ctx-management-ctx-hierarchies-with-bean-overrides[context
hierarchies with bean overrides] for further details and examples.
====
[NOTE]
====
There are no restrictions on the visibility of `@TestBean` fields or factory methods.
@@ -26,16 +26,16 @@ Java::
@Test
void test() throws Exception {
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
.andExpect(status().isOk()) <1>
.andExpect(request().asyncStarted()) <2>
.andExpect(request().asyncResult("body")) <3>
.andReturn();
MvcResult mvcResult = this.mockMvc.perform(get("/path"))
.andExpect(status().isOk()) <1>
.andExpect(request().asyncStarted()) <2>
.andExpect(request().asyncResult("body")) <3>
.andReturn();
this.mockMvc.perform(asyncDispatch(mvcResult)) <4>
.andExpect(status().isOk()) <5>
.andExpect(content().string("body"));
}
this.mockMvc.perform(asyncDispatch(mvcResult)) <4>
.andExpect(status().isOk()) <5>
.andExpect(content().string("body"));
}
----
<1> Check response status is still unchanged
<2> Async processing must have started
@@ -4,7 +4,7 @@
Once the TestContext framework loads an `ApplicationContext` (or `WebApplicationContext`)
for a test, that context is cached and reused for all subsequent tests that declare the
same unique context configuration within the same test suite. To understand how caching
works, it is important to understand what is meant by "unique" and "test suite."
works, it is important to understand what is meant by "`unique`" and "`test suite.`"
An `ApplicationContext` can be uniquely identified by the combination of configuration
parameters that is used to load it. Consequently, the unique combination of configuration
@@ -15,8 +15,8 @@ framework uses the following configuration parameters to build the context cache
* `classes` (from `@ContextConfiguration`)
* `contextInitializerClasses` (from `@ContextConfiguration`)
* `contextCustomizers` (from `ContextCustomizerFactory`) this includes
`@DynamicPropertySource` methods, bean overrides (such as `@TestBean`, `@MockitoBean`,
`@MockitoSpyBean` etc.), as well as various features from Spring Boot's testing support.
`@DynamicPropertySource` methods as well as various features from Spring Boot's
testing support such as `@MockBean` and `@SpyBean`.
* `contextLoader` (from `@ContextConfiguration`)
* `parent` (from `@ContextHierarchy`)
* `activeProfiles` (from `@ActiveProfiles`)
@@ -1,5 +1,5 @@
[[testcontext-context-customizers]]
= Context Configuration with Context Customizers
= Configuration Configuration with Context Customizers
A `ContextCustomizer` is responsible for customizing the supplied
`ConfigurableApplicationContext` after bean definitions have been loaded into the context
@@ -22,19 +22,8 @@ given level in the hierarchy, the configuration resource type (that is, XML conf
files or component classes) must be consistent. Otherwise, it is perfectly acceptable to
have different levels in a context hierarchy configured using different resource types.
[NOTE]
====
If you use `@DirtiesContext` in a test whose context is configured as part of a context
hierarchy, you can use the `hierarchyMode` flag to control how the context cache is
cleared.
For further details, see the discussion of `@DirtiesContext` in
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations]
and the {spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
====
The JUnit Jupiter based examples in this section show common configuration scenarios for
integration tests that require the use of context hierarchies.
The remaining JUnit Jupiter based examples in this section show common configuration
scenarios for integration tests that require the use of context hierarchies.
**Single test class with context hierarchy**
--
@@ -240,118 +229,12 @@ Kotlin::
class ExtendedTests : BaseTests() {}
----
======
.Dirtying a context within a context hierarchy
NOTE: If you use `@DirtiesContext` in a test whose context is configured as part of a
context hierarchy, you can use the `hierarchyMode` flag to control how the context cache
is cleared. For further details, see the discussion of `@DirtiesContext` in
xref:testing/annotations/integration-spring/annotation-dirtiescontext.adoc[Spring Testing Annotations] and the
{spring-framework-api}/test/annotation/DirtiesContext.html[`@DirtiesContext`] javadoc.
--
[[testcontext-ctx-management-ctx-hierarchies-with-bean-overrides]]
**Context hierarchies with bean overrides**
--
When `@ContextHierarchy` is used in conjunction with
xref:testing/testcontext-framework/bean-overriding.adoc[bean overrides] such as
`@TestBean`, `@MockitoBean`, or `@MockitoSpyBean`, it may be desirable or necessary to
have the override applied to a single level in the context hierarchy. To achieve that,
the bean override must specify a context name that matches a name configured via the
`name` attribute in `@ContextConfiguration`.
The following test class configures the name of the second hierarchy level to be
`"user-config"` and simultaneously specifies that the `UserService` should be wrapped in
a Mockito spy in the context named `"user-config"`. Consequently, Spring will only
attempt to create the spy in the `"user-config"` context and will not attempt to create
the spy in the parent context.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(classes = AppConfig.class),
@ContextConfiguration(classes = UserConfig.class, name = "user-config")
})
class IntegrationTests {
@MockitoSpyBean(contextName = "user-config")
UserService userService;
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
ContextConfiguration(classes = [AppConfig::class]),
ContextConfiguration(classes = [UserConfig::class], name = "user-config"))
class IntegrationTests {
@MockitoSpyBean(contextName = "user-config")
lateinit var userService: UserService
// ...
}
----
======
When applying bean overrides in different levels of the context hierarchy, you may need
to have all of the bean override instances injected into the test class in order to
interact with them — for example, to configure stubbing for mocks. However, `@Autowired`
will always inject a matching bean found in the lowest level of the context hierarchy.
Thus, to inject bean override instances from specific levels in the context hierarchy,
you need to annotate fields with appropriate bean override annotations and configure the
name of the context level.
The following test class configures the names of the hierarchy levels to be `"parent"`
and `"child"`. It also declares two `PropertyService` fields that are configured to
create or replace `PropertyService` beans with Mockito mocks in the respective contexts,
named `"parent"` and `"child"`. Consequently, the mock from the `"parent"` context will
be injected into the `propertyServiceInParent` field, and the mock from the `"child"`
context will be injected into the `propertyServiceInChild` field.
[tabs]
======
Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension.class)
@ContextHierarchy({
@ContextConfiguration(classes = ParentConfig.class, name = "parent"),
@ContextConfiguration(classes = ChildConfig.class, name = "child")
})
class IntegrationTests {
@MockitoBean(contextName = "parent")
PropertyService propertyServiceInParent;
@MockitoBean(contextName = "child")
PropertyService propertyServiceInChild;
// ...
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
@ExtendWith(SpringExtension::class)
@ContextHierarchy(
ContextConfiguration(classes = [ParentConfig::class], name = "parent"),
ContextConfiguration(classes = [ChildConfig::class], name = "child"))
class IntegrationTests {
@MockitoBean(contextName = "parent")
lateinit var propertyServiceInParent: PropertyService
@MockitoBean(contextName = "child")
lateinit var propertyServiceInChild: PropertyService
// ...
}
----
======
--
@@ -18,9 +18,9 @@ Do not run tests in parallel if the tests:
* Use Spring Framework's `@DirtiesContext` support.
* Use Spring Framework's `@MockitoBean` or `@MockitoSpyBean` support.
* Use Spring Boot's `@MockBean` or `@SpyBean` support.
* Use JUnit Jupiter's `@TestMethodOrder` support or any testing framework feature that is
designed to ensure that test methods run in a particular order. Note, however, that
this does not apply if entire test classes are run in parallel.
* Use JUnit 4's `@FixMethodOrder` support or any testing framework feature
that is designed to ensure that test methods run in a particular order. Note,
however, that this does not apply if entire test classes are run in parallel.
* Change the state of shared services or systems such as a database, message broker,
filesystem, and others. This applies to both embedded and external systems.
@@ -396,7 +396,7 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
import org.springframework.test.web.reactive.server.expectBody
import org.springframework.test.web.reactive.server.expectBody
client.get().uri("/persons/1")
.exchange()
@@ -306,34 +306,34 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
Resource resource = ...
Mono<String> result = webClient
.post()
.uri("https://example.com")
.body(Flux.concat(
FormPartEvent.create("field", "field value"),
FilePartEvent.create("file", resource)
), PartEvent.class)
.retrieve()
.bodyToMono(String.class);
Resource resource = ...
Mono<String> result = webClient
.post()
.uri("https://example.com")
.body(Flux.concat(
FormPartEvent.create("field", "field value"),
FilePartEvent.create("file", resource)
), PartEvent.class)
.retrieve()
.bodyToMono(String.class);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
var resource: Resource = ...
var result: Mono<String> = webClient
.post()
.uri("https://example.com")
.body(
Flux.concat(
FormPartEvent.create("field", "field value"),
FilePartEvent.create("file", resource)
)
var resource: Resource = ...
var result: Mono<String> = webClient
.post()
.uri("https://example.com")
.body(
Flux.concat(
FormPartEvent.create("field", "field value"),
FilePartEvent.create("file", resource)
)
.retrieve()
.bodyToMono()
)
.retrieve()
.bodyToMono()
----
======
@@ -390,29 +390,29 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
HttpClient httpClient = HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
HttpClient httpClient = HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build();
ClientHttpConnector connector =
new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory());
ClientHttpConnector connector =
new JdkClientHttpConnector(httpClient, new DefaultDataBufferFactory());
WebClient webClient = WebClient.builder().clientConnector(connector).build();
WebClient webClient = WebClient.builder().clientConnector(connector).build();
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val httpClient = HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build()
val httpClient = HttpClient.newBuilder()
.followRedirects(Redirect.NORMAL)
.connectTimeout(Duration.ofSeconds(20))
.build()
val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory())
val connector = JdkClientHttpConnector(httpClient, DefaultDataBufferFactory())
val webClient = WebClient.builder().clientConnector(connector).build()
val webClient = WebClient.builder().clientConnector(connector).build()
----
======
@@ -158,65 +158,65 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
return next.exchange(ClientRequest.from(request).body((outputMessage, context) ->
request.body().insert(new BufferingDecorator(outputMessage), context)).build()
);
} else {
return next.exchange(request);
}
}
private static final class BufferingDecorator extends ClientHttpRequestDecorator {
private BufferingDecorator(ClientHttpRequest delegate) {
super(delegate);
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
return DataBufferUtils.join(body).flatMap(buffer -> {
getHeaders().setContentLength(buffer.readableByteCount());
return super.writeWith(Mono.just(buffer));
});
}
}
}
public class MultipartExchangeFilterFunction implements ExchangeFilterFunction {
@Override
public Mono<ClientResponse> filter(ClientRequest request, ExchangeFunction next) {
if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
return next.exchange(ClientRequest.from(request).body((outputMessage, context) ->
request.body().insert(new BufferingDecorator(outputMessage), context)).build()
);
} else {
return next.exchange(request);
}
}
private static final class BufferingDecorator extends ClientHttpRequestDecorator {
private BufferingDecorator(ClientHttpRequest delegate) {
super(delegate);
}
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
return DataBufferUtils.join(body).flatMap(buffer -> {
getHeaders().setContentLength(buffer.readableByteCount());
return super.writeWith(Mono.just(buffer));
});
}
}
}
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
class MultipartExchangeFilterFunction : ExchangeFilterFunction {
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
next.exchange(ClientRequest.from(request)
.body { message, context -> request.body().insert(BufferingDecorator(message), context) }
.build())
}
else {
next.exchange(request)
}
}
private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) {
override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
return DataBufferUtils.join(body)
.flatMap {
headers.contentLength = it.readableByteCount().toLong()
super.writeWith(Mono.just(it))
}
}
}
}
class MultipartExchangeFilterFunction : ExchangeFilterFunction {
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
return if (MediaType.MULTIPART_FORM_DATA.includes(request.headers().getContentType())
&& (request.method() == HttpMethod.PUT || request.method() == HttpMethod.POST)) {
next.exchange(ClientRequest.from(request)
.body { message, context -> request.body().insert(BufferingDecorator(message), context) }
.build())
}
else {
next.exchange(request)
}
}
private class BufferingDecorator(delegate: ClientHttpRequest) : ClientHttpRequestDecorator(delegate) {
override fun writeWith(body: Publisher<out DataBuffer>): Mono<Void> {
return DataBufferUtils.join(body)
.flatMap {
headers.contentLength = it.readableByteCount().toLong()
super.writeWith(Mono.just(it))
}
}
}
}
----
======
======
@@ -207,14 +207,14 @@ Kotlin::
class ExampleHandler : WebSocketHandler {
override fun handle(session: WebSocketSession): Mono<Void> {
return session.receive() // <1>
return session.receive() // <1>
.doOnNext {
// ... // <2>
}
.concatMap {
// ... // <3>
}
.then() // <4>
.then() // <4>
}
}
----
@@ -268,16 +268,16 @@ Kotlin::
override fun handle(session: WebSocketSession): Mono<Void> {
val output = session.receive() // <1>
val output = session.receive() // <1>
.doOnNext {
// ...
}
.concatMap {
// ...
}
.map { session.textMessage("Echo $it") } // <2>
.map { session.textMessage("Echo $it") } // <2>
return session.send(output) // <3>
return session.send(output) // <3>
}
}
----
@@ -149,7 +149,7 @@ Java::
DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
registrar.setUseIsoFormat(true);
registrar.registerFormatters(registry);
}
}
}
----
@@ -62,7 +62,7 @@ Java::
----
class Account {
private final String firstName;
private final String firstName;
public Account(@BindParam("first-name") String firstName) {
this.firstName = firstName;
@@ -93,8 +93,8 @@ Java::
----
@ModelAttribute
public void addAccount(@RequestParam String number) {
Mono<Account> accountMono = accountRepository.findAccount(number);
model.addAttribute("account", accountMono);
Mono<Account> accountMono = accountRepository.findAccount(number);
model.addAttribute("account", accountMono);
}
@PostMapping("/accounts")
@@ -104,21 +104,21 @@ Kotlin::
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
// ...
}
}
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
// ...
}
}
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
// ...
}
}
// ...
override fun other(result: ParameterValidationResult) {
// ...
}
}
})
----
======
@@ -782,8 +782,8 @@ Java::
----
WebClient webClient = WebClient.builder()
.codecs(configurer -> {
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
CustomDecoder decoder = new CustomDecoder();
configurer.customCodecs().registerWithDefaultConfig(decoder);
})
.build();
----
@@ -794,8 +794,8 @@ Kotlin::
----
val webClient = WebClient.builder()
.codecs({ configurer ->
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
val decoder = CustomDecoder()
configurer.customCodecs().registerWithDefaultConfig(decoder)
})
.build()
----
@@ -276,7 +276,7 @@ ServerResponse.async(asyncResponse);
----
======
https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events] can be provided via the
https://www.w3.org/TR/eventsource/[Server-Sent Events] can be provided via the
static `sse` method on `ServerResponse`. The builder provided by that method
allows you to send Strings, or other objects as JSON. For example:
@@ -781,7 +781,7 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
ClassPathResource index = new ClassPathResource("static/index.html");
ClassPathResource index = new ClassPathResource("static/index.html");
List<String> extensions = List.of("js", "css", "ico", "png", "jpg", "gif");
RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate();
RouterFunction<ServerResponse> redirectToIndex = route()
@@ -793,7 +793,7 @@ Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val redirectToIndex = router {
val redirectToIndex = router {
val index = ClassPathResource("static/index.html")
val extensions = listOf("js", "css", "ico", "png", "jpg", "gif")
val spaPredicate = !(path("/api/**") or path("/error") or
@@ -814,16 +814,16 @@ Java::
+
[source,java,indent=0,subs="verbatim,quotes"]
----
Resource location = new FileUrlResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
Resource location = new FileUrlResource("public-resources/");
RouterFunction<ServerResponse> resources = RouterFunctions.resources("/resources/**", location);
----
Kotlin::
+
[source,kotlin,indent=0,subs="verbatim,quotes"]
----
val location = FileUrlResource("public-resources/")
val resources = router { resources("/resources/**", location) }
val location = FileUrlResource("public-resources/")
val resources = router { resources("/resources/**", location) }
----
======
@@ -281,7 +281,7 @@ invokes the configured exception resolvers and completes the request.
=== SSE
`SseEmitter` (a subclass of `ResponseBodyEmitter`) provides support for
https://html.spec.whatwg.org/multipage/server-sent-events.html[Server-Sent Events], where events sent from the server
https://www.w3.org/TR/eventsource/[Server-Sent Events], where events sent from the server
are formatted according to the W3C SSE specification. To produce an SSE
stream from a controller, return `SseEmitter`, as the following example shows:
@@ -97,7 +97,7 @@ Java::
----
class Account {
private final String firstName;
private final String firstName;
public Account(@BindParam("first-name") String firstName) {
this.firstName = firstName;
@@ -73,12 +73,12 @@ Java::
@Override
public void requestHeader(RequestHeader requestHeader, ParameterValidationResult result) {
// ...
// ...
}
@Override
public void requestParam(@Nullable RequestParam requestParam, ParameterValidationResult result) {
// ...
// ...
}
@Override
@@ -88,7 +88,7 @@ Java::
@Override
public void other(ParameterValidationResult result) {
// ...
// ...
}
});
----
@@ -103,22 +103,22 @@ Kotlin::
ex.visitResults(object : HandlerMethodValidationException.Visitor {
override fun requestHeader(requestHeader: RequestHeader, result: ParameterValidationResult) {
// ...
}
// ...
}
override fun requestParam(requestParam: RequestParam?, result: ParameterValidationResult) {
// ...
}
// ...
}
override fun modelAttribute(modelAttribute: ModelAttribute?, errors: ParameterErrors) {
// ...
}
// ...
}
// ...
override fun other(result: ParameterValidationResult) {
// ...
}
// ...
}
})
----
======
@@ -22,11 +22,11 @@ The following example code is based on it:
[source,javascript,indent=0,subs="verbatim,quotes"]
----
const stompClient = new StompJs.Client({
brokerURL: 'ws://domain.com/portfolio',
onConnect: () => {
// ...
}
});
brokerURL: 'ws://domain.com/portfolio',
onConnect: () => {
// ...
}
});
----
Alternatively, if you connect through SockJS, you can enable the
@@ -47,3 +47,5 @@ interactive web application] -- a getting started guide.
* https://github.com/rstoyanchev/spring-websocket-portfolio[Stock Portfolio] -- a sample
application.
+7 -7
View File
@@ -16,12 +16,12 @@ dependencies {
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.17"))
api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.17"))
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"))
api(platform("org.mockito:mockito-bom:5.17.0"))
api(platform("org.junit:junit-bom:5.12.1"))
api(platform("org.mockito:mockito-bom:5.16.1"))
constraints {
api("com.fasterxml:aalto-xml:1.3.2")
@@ -31,7 +31,7 @@ dependencies {
api("com.google.code.findbugs:findbugs:3.0.1")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.google.code.gson:gson:2.12.1")
api("com.google.protobuf:protobuf-java-util:4.30.2")
api("com.google.protobuf:protobuf-java-util:4.30.0")
api("com.h2database:h2:2.3.232")
api("com.jayway.jsonpath:json-path:2.9.0")
api("com.oracle.database.jdbc:ojdbc11:21.9.0.0")
@@ -100,8 +100,8 @@ 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.core5:httpcore5-reactive:5.3.4")
api("org.apache.httpcomponents.client5:httpclient5:5.4.2")
api("org.apache.httpcomponents.core5:httpcore5-reactive:5.3.3")
api("org.apache.poi:poi-ooxml:5.2.5")
api("org.apache.tomcat.embed:tomcat-embed-core:10.1.28")
api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.2.6
version=6.2.5
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
+2 -2
View File
@@ -98,7 +98,7 @@ if (project.name == "spring-oxm") {
}
// Include project specific settings
tasks.register('eclipseSettings', Copy) {
task eclipseSettings(type: Copy) {
from rootProject.files(
'src/eclipse/org.eclipse.core.resources.prefs',
'src/eclipse/org.eclipse.jdt.core.prefs',
@@ -107,7 +107,7 @@ tasks.register('eclipseSettings', Copy) {
outputs.upToDateWhen { false }
}
tasks.register('cleanEclipseSettings', Delete) {
task cleanEclipseSettings(type: Delete) {
delete project.file('.settings/org.eclipse.core.resources.prefs')
delete project.file('.settings/org.eclipse.jdt.core.prefs')
delete project.file('.settings/org.eclipse.jdt.ui.prefs')
+26 -25
View File
@@ -70,30 +70,31 @@ normalization {
javadoc {
description = "Generates project-level javadoc for use in -javadoc jar"
options {
encoding = "UTF-8"
memberLevel = JavadocMemberLevel.PROTECTED
author = true
header = project.name
use = true
links(project.ext.javadocLinks)
// Check for 'syntax' during linting. Note that the global
// 'framework-api:javadoc' task checks for 'reference' in addition
// to 'syntax'.
addBooleanOption("Xdoclint:syntax,-reference", true)
addBooleanOption('Werror', true) // fail build on Javadoc warnings
}
options.encoding = "UTF-8"
options.memberLevel = JavadocMemberLevel.PROTECTED
options.author = true
options.header = project.name
options.use = true
options.links(project.ext.javadocLinks)
// Check for syntax during linting. 'none' doesn't seem to work in suppressing
// all linting warnings all the time (see/link references most notably).
options.addStringOption("Xdoclint:syntax", "-quiet")
// Suppress warnings due to cross-module @see and @link references.
// Note that global 'api' task does display all warnings, and
// checks for 'reference' on top of 'syntax'.
logging.captureStandardError LogLevel.INFO
logging.captureStandardOutput LogLevel.INFO // suppress "## warnings" message
}
tasks.register('sourcesJar', Jar) {
dependsOn classes
task sourcesJar(type: Jar, dependsOn: classes) {
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
archiveClassifier.set("sources")
from sourceSets.main.allSource
// Don't include or exclude anything explicitly by default. See SPR-12085.
}
tasks.register('javadocJar', Jar) {
task javadocJar(type: Jar) {
archiveClassifier.set("javadoc")
from javadoc
}
@@ -113,16 +114,16 @@ components.java.withVariantsFromConfiguration(configurations.testFixturesApiElem
components.java.withVariantsFromConfiguration(configurations.testFixturesRuntimeElements) { skip() }
tasks.withType(JavaCompile).configureEach {
options.errorprone {
disableAllChecks = true
option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract")
option("NullAway:AnnotatedPackages", "org.springframework")
option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," +
options.errorprone {
disableAllChecks = true
option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract")
option("NullAway:AnnotatedPackages", "org.springframework")
option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," +
"org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," +
"org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature")
}
}
}
tasks.compileJava {
// The check defaults to a warning, bump it up to an error for the main sources
options.errorprone.error("NullAway")
}
// The check defaults to a warning, bump it up to an error for the main sources
options.errorprone.error("NullAway")
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -111,7 +111,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
* Create a new {@code ReflectiveAspectJAdvisorFactory}, propagating the given
* {@link BeanFactory} to the created {@link AspectJExpressionPointcut} instances,
* for bean pointcut handling as well as consistent {@link ClassLoader} resolution.
* @param beanFactory the BeanFactory to propagate (may be {@code null})
* @param beanFactory the BeanFactory to propagate (may be {@code null}}
* @since 4.3.6
* @see AspectJExpressionPointcut#setBeanFactory
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBeanClassLoader()
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,7 +35,6 @@ import org.springframework.aop.AopInvocationException;
import org.springframework.aop.RawTargetAccess;
import org.springframework.aop.TargetSource;
import org.springframework.aop.support.AopUtils;
import org.springframework.aot.AotDetector;
import org.springframework.cglib.core.ClassLoaderAwareGeneratorStrategy;
import org.springframework.cglib.core.CodeGenerationException;
import org.springframework.cglib.core.GeneratorStrategy;
@@ -206,7 +205,7 @@ class CglibAopProxy implements AopProxy, Serializable {
enhancer.setSuperclass(proxySuperClass);
enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised));
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts());
enhancer.setAttemptLoad(true);
enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ?
new ClassLoaderAwareGeneratorStrategy(classLoader) :
new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -114,10 +114,10 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator {
boolean isFactoryBean = FactoryBean.class.isAssignableFrom(beanClass);
for (String mappedName : this.beanNames) {
if (isFactoryBean) {
if (mappedName.isEmpty() || mappedName.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) {
if (!mappedName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
continue;
}
mappedName = mappedName.substring(1); // length of '&'
mappedName = mappedName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
}
if (isMatch(beanName, mappedName)) {
return true;
@@ -291,7 +291,7 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
String lastKey = tokens.keys[tokens.keys.length - 1];
if (propValue.getClass().isArray()) {
Class<?> componentType = propValue.getClass().componentType();
Class<?> requiredType = propValue.getClass().componentType();
int arrayIndex = Integer.parseInt(lastKey);
Object oldValue = null;
try {
@@ -299,9 +299,10 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
oldValue = Array.get(propValue, arrayIndex);
}
Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(),
componentType, ph.nested(tokens.keys.length));
requiredType, ph.nested(tokens.keys.length));
int length = Array.getLength(propValue);
if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) {
Class<?> componentType = propValue.getClass().componentType();
Object newArray = Array.newInstance(componentType, arrayIndex + 1);
System.arraycopy(propValue, 0, newArray, 0, length);
int lastKeyIndex = tokens.canonicalName.lastIndexOf('[');
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2018 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,7 +25,6 @@ import org.springframework.lang.Nullable;
* analogous to an InvocationTargetException.
*
* @author Rod Johnson
* @author Juergen Hoeller
*/
@SuppressWarnings("serial")
public class MethodInvocationException extends PropertyAccessException {
@@ -42,9 +41,7 @@ public class MethodInvocationException extends PropertyAccessException {
* @param cause the Throwable raised by the invoked method
*/
public MethodInvocationException(PropertyChangeEvent propertyChangeEvent, @Nullable Throwable cause) {
super(propertyChangeEvent,
"Property '" + propertyChangeEvent.getPropertyName() + "' threw exception: " + cause,
cause);
super(propertyChangeEvent, "Property '" + propertyChangeEvent.getPropertyName() + "' threw exception", cause);
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -124,16 +124,9 @@ public interface BeanFactory {
* beans <i>created</i> by the FactoryBean. For example, if the bean named
* {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject}
* will return the factory, not the instance returned by the factory.
* @see #FACTORY_BEAN_PREFIX_CHAR
*/
String FACTORY_BEAN_PREFIX = "&";
/**
* Character variant of {@link #FACTORY_BEAN_PREFIX}.
* @since 6.2.6
*/
char FACTORY_BEAN_PREFIX_CHAR = '&';
/**
* Return an instance, which may be shared or independent, of the specified bean.
@@ -72,7 +72,7 @@ public abstract class BeanFactoryUtils {
* @see BeanFactory#FACTORY_BEAN_PREFIX
*/
public static boolean isFactoryDereference(@Nullable String name) {
return (name != null && !name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR);
return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
}
/**
@@ -84,14 +84,14 @@ public abstract class BeanFactoryUtils {
*/
public static String transformedBeanName(String name) {
Assert.notNull(name, "'name' must not be null");
if (name.isEmpty() || name.charAt(0) != BeanFactory.FACTORY_BEAN_PREFIX_CHAR) {
if (!name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) {
return name;
}
return transformedBeanNameCache.computeIfAbsent(name, beanName -> {
do {
beanName = beanName.substring(1); // length of '&'
beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length());
}
while (beanName.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR);
while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
return beanName;
});
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -145,6 +145,8 @@ public interface ListableBeanFactory extends BeanFactory {
* <p>Does not consider any hierarchy this factory may participate in.
* Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors}
* to include beans in ancestor factories too.
* <p>Note: Does <i>not</i> ignore singleton beans that have been registered
* by other means than bean definitions.
* <p>This version of {@code getBeanNamesForType} matches all kinds of beans,
* be it singletons, prototypes, or FactoryBeans. In most implementations, the
* result will be the same as for {@code getBeanNamesForType(type, true, true)}.
@@ -174,6 +176,8 @@ public interface ListableBeanFactory extends BeanFactory {
* <p>Does not consider any hierarchy this factory may participate in.
* Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors}
* to include beans in ancestor factories too.
* <p>Note: Does <i>not</i> ignore singleton beans that have been registered
* by other means than bean definitions.
* <p>Bean names returned by this method should always return bean names <i>in the
* order of definition</i> in the backend configuration, as far as possible.
* @param type the generically typed class or interface to match
@@ -206,6 +210,8 @@ public interface ListableBeanFactory extends BeanFactory {
* <p>Does not consider any hierarchy this factory may participate in.
* Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors}
* to include beans in ancestor factories too.
* <p>Note: Does <i>not</i> ignore singleton beans that have been registered
* by other means than bean definitions.
* <p>This version of {@code getBeanNamesForType} matches all kinds of beans,
* be it singletons, prototypes, or FactoryBeans. In most implementations, the
* result will be the same as for {@code getBeanNamesForType(type, true, true)}.
@@ -233,6 +239,8 @@ public interface ListableBeanFactory extends BeanFactory {
* <p>Does not consider any hierarchy this factory may participate in.
* Use BeanFactoryUtils' {@code beanNamesForTypeIncludingAncestors}
* to include beans in ancestor factories too.
* <p>Note: Does <i>not</i> ignore singleton beans that have been registered
* by other means than bean definitions.
* <p>Bean names returned by this method should always return bean names <i>in the
* order of definition</i> in the backend configuration, as far as possible.
* @param type the class or interface to match, or {@code null} for all bean names
@@ -257,24 +265,21 @@ public interface ListableBeanFactory extends BeanFactory {
* subclasses), judging from either bean definitions or the value of
* {@code getObjectType} in the case of FactoryBeans.
* <p><b>NOTE: This method introspects top-level beans only.</b> It does <i>not</i>
* check nested beans which might match the specified type as well. Also, it
* <b>suppresses exceptions for beans that are currently in creation in a circular
* reference scenario:</b> typically, references back to the caller of this method.
* check nested beans which might match the specified type as well.
* <p>Does consider objects created by FactoryBeans, which means that FactoryBeans
* will get initialized. If the object created by the FactoryBean doesn't match,
* the raw FactoryBean itself will be matched against the type.
* <p>Does not consider any hierarchy this factory may participate in.
* Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors}
* to include beans in ancestor factories too.
* <p>Note: Does <i>not</i> ignore singleton beans that have been registered
* by other means than bean definitions.
* <p>This version of getBeansOfType matches all kinds of beans, be it
* singletons, prototypes, or FactoryBeans. In most implementations, the
* result will be the same as for {@code getBeansOfType(type, true, true)}.
* <p>The Map returned by this method should always return bean names and
* corresponding bean instances <i>in the order of definition</i> in the
* backend configuration, as far as possible.
* <p><b>Consider {@link #getBeanNamesForType(Class)} with selective {@link #getBean}
* calls for specific bean names in preference to this Map-based retrieval method.</b>
* Aside from lazy instantiation benefits, this also avoids any exception suppression.
* @param type the class or interface to match, or {@code null} for all concrete beans
* @return a Map with the matching beans, containing the bean names as
* keys and the corresponding bean instances as values
@@ -290,9 +295,7 @@ public interface ListableBeanFactory extends BeanFactory {
* subclasses), judging from either bean definitions or the value of
* {@code getObjectType} in the case of FactoryBeans.
* <p><b>NOTE: This method introspects top-level beans only.</b> It does <i>not</i>
* check nested beans which might match the specified type as well. Also, it
* <b>suppresses exceptions for beans that are currently in creation in a circular
* reference scenario:</b> typically, references back to the caller of this method.
* check nested beans which might match the specified type as well.
* <p>Does consider objects created by FactoryBeans if the "allowEagerInit" flag is set,
* which means that FactoryBeans will get initialized. If the object created by the
* FactoryBean doesn't match, the raw FactoryBean itself will be matched against the
@@ -301,12 +304,11 @@ public interface ListableBeanFactory extends BeanFactory {
* <p>Does not consider any hierarchy this factory may participate in.
* Use BeanFactoryUtils' {@code beansOfTypeIncludingAncestors}
* to include beans in ancestor factories too.
* <p>Note: Does <i>not</i> ignore singleton beans that have been registered
* by other means than bean definitions.
* <p>The Map returned by this method should always return bean names and
* corresponding bean instances <i>in the order of definition</i> in the
* backend configuration, as far as possible.
* <p><b>Consider {@link #getBeanNamesForType(Class)} with selective {@link #getBean}
* calls for specific bean names in preference to this Map-based retrieval method.</b>
* Aside from lazy instantiation benefits, this also avoids any exception suppression.
* @param type the class or interface to match, or {@code null} for all concrete beans
* @param includeNonSingletons whether to include prototype or scoped beans too
* or just singletons (also applies to FactoryBeans)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -156,8 +156,6 @@ abstract class BeanDefinitionPropertyValueCodeGeneratorDelegates {
.builder(SuppressWarnings.class)
.addMember("value", "{\"rawtypes\", \"unchecked\"}")
.build());
method.addModifiers(javax.lang.model.element.Modifier.PRIVATE,
javax.lang.model.element.Modifier.STATIC);
method.returns(Map.class);
method.addStatement("$T map = new $T($L)", Map.class,
LinkedHashMap.class, map.size());
@@ -144,10 +144,8 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
private final Set<Class<?>> ignoredDependencyTypes = new HashSet<>();
/**
* Dependency interfaces to ignore on dependency check and autowire, as a Set
* of Class objects.
* <p>By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and
* {@code BeanClassLoaderAware} interfaces are ignored.
* Dependency interfaces to ignore on dependency check and autowire, as Set of
* Class objects. By default, only the BeanFactory interface is ignored.
*/
private final Set<Class<?>> ignoredDependencyInterfaces = new HashSet<>();
@@ -287,15 +285,11 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac
/**
* Ignore the given dependency interface for autowiring.
* <p>This will typically be used by application contexts to register
* dependencies that are resolved in other ways, like {@code BeanFactory}
* through {@code BeanFactoryAware} or {@code ApplicationContext} through
* {@code ApplicationContextAware}.
* <p>By default, the {@code BeanNameAware}, {@code BeanFactoryAware}, and
* {@code BeanClassLoaderAware} interfaces are ignored.
* dependencies that are resolved in other ways, like BeanFactory through
* BeanFactoryAware or ApplicationContext through ApplicationContextAware.
* <p>By default, only the BeanFactoryAware interface is ignored.
* For further types to ignore, invoke this method for each type.
* @see org.springframework.beans.factory.BeanNameAware
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.beans.factory.BeanClassLoaderAware
* @see org.springframework.context.ApplicationContextAware
*/
public void ignoreDependencyInterface(Class<?> ifc) {
@@ -770,16 +770,16 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
public String[] getAliases(String name) {
String beanName = transformedBeanName(name);
List<String> aliases = new ArrayList<>();
boolean hasFactoryPrefix = (!name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR);
boolean factoryPrefix = name.startsWith(FACTORY_BEAN_PREFIX);
String fullBeanName = beanName;
if (hasFactoryPrefix) {
if (factoryPrefix) {
fullBeanName = FACTORY_BEAN_PREFIX + beanName;
}
if (!fullBeanName.equals(name)) {
aliases.add(fullBeanName);
}
String[] retrievedAliases = super.getAliases(beanName);
String prefix = (hasFactoryPrefix ? FACTORY_BEAN_PREFIX : "");
String prefix = (factoryPrefix ? FACTORY_BEAN_PREFIX : "");
for (String retrievedAlias : retrievedAliases) {
String alias = prefix + retrievedAlias;
if (!alias.equals(name)) {
@@ -1153,7 +1153,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
public BeanDefinition getMergedBeanDefinition(String name) throws BeansException {
String beanName = transformedBeanName(name);
// Efficiently check whether bean definition exists in this factory.
if (getParentBeanFactory() instanceof ConfigurableBeanFactory parent && !containsBeanDefinition(beanName)) {
if (!containsBeanDefinition(beanName) && getParentBeanFactory() instanceof ConfigurableBeanFactory parent) {
return parent.getMergedBeanDefinition(beanName);
}
// Resolve merged bean definition locally.
@@ -1292,7 +1292,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
*/
protected String originalBeanName(String name) {
String beanName = transformedBeanName(name);
if (!name.isEmpty() && name.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR) {
if (name.startsWith(FACTORY_BEAN_PREFIX)) {
beanName = FACTORY_BEAN_PREFIX + beanName;
}
return beanName;
@@ -1474,7 +1474,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
// Cache the merged bean definition for the time being
// (it might still get re-merged later on in order to pick up metadata changes)
if (containingBd == null && (isCacheBeanMetadata() || isBeanEligibleForMetadataCaching(beanName))) {
cacheMergedBeanDefinition(mbd, beanName);
this.mergedBeanDefinitions.put(beanName, mbd);
}
}
if (previous != null) {
@@ -1503,18 +1503,6 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
}
}
/**
* Cache the given merged bean definition.
* <p>Subclasses can override this to derive additional cached state
* from the final post-processed bean definition.
* @param mbd the merged bean definition to cache
* @param beanName the name of the bean
* @since 6.2.6
*/
protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) {
this.mergedBeanDefinitions.put(beanName, mbd);
}
/**
* Check the given merged bean definition,
* potentially throwing validation exceptions.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,7 +22,6 @@ import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.AotDetector;
import org.springframework.beans.BeanInstantiationException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory;
@@ -154,7 +153,7 @@ public class CglibSubclassingInstantiationStrategy extends SimpleInstantiationSt
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(beanDefinition.getBeanClass());
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts());
enhancer.setAttemptLoad(true);
if (this.owner instanceof ConfigurableBeanFactory cbf) {
ClassLoader cl = cbf.getBeanClassLoader();
enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl));
@@ -277,11 +276,12 @@ public class CglibSubclassingInstantiationStrategy extends SimpleInstantiationSt
this.owner = owner;
}
@Override
@Nullable
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
ReplaceOverride ro = (ReplaceOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
Assert.state(ro != null, "ReplaceOverride not found");
// TODO could cache if a singleton for minor performance optimization
MethodReplacer mr = this.owner.getBean(ro.getMethodReplacerBeanName(), MethodReplacer.class);
return processReturnType(method, mr.reimplement(obj, method, args));
}
@@ -76,7 +76,6 @@ import org.springframework.core.NamedThreadLocal;
import org.springframework.core.OrderComparator;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
import org.springframework.core.SpringProperties;
import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
@@ -129,20 +128,6 @@ import org.springframework.util.StringUtils;
public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory
implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable {
/**
* System property that instructs Spring to enforce strict locking during bean creation,
* rather than the mix of strict and lenient locking that 6.2 applies by default. Setting
* this flag to "true" restores 6.1.x style locking in the entire pre-instantiation phase.
* <p>By default, the factory infers strict locking from the encountered thread names:
* If additional threads have names that match the thread prefix of the main bootstrap thread,
* they are considered external (multiple external bootstrap threads calling into the factory)
* and therefore have strict locking applied to them. This inference can be turned off through
* explicitly setting this flag to "false" rather than leaving it unspecified.
* @since 6.2.6
* @see #preInstantiateSingletons()
*/
public static final String STRICT_LOCKING_PROPERTY_NAME = "spring.locking.strict";
@Nullable
private static Class<?> jakartaInjectProviderClass;
@@ -162,10 +147,6 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
private static final Map<String, Reference<DefaultListableBeanFactory>> serializableFactories =
new ConcurrentHashMap<>(8);
/** Whether strict locking is enforced or relaxed in this factory. */
@Nullable
private final Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME);
/** Optional id for this factory, for serialization purposes. */
@Nullable
private String serializationId;
@@ -218,9 +199,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
/** Whether bean definition metadata may be cached for all beans. */
private volatile boolean configurationFrozen;
/** Name prefix of main thread: only set during pre-instantiation phase. */
@Nullable
private volatile String mainThreadPrefix;
private volatile boolean preInstantiationPhase;
private final NamedThreadLocal<PreInstantiation> preInstantiationThread =
new NamedThreadLocal<>("Pre-instantiation thread marker");
@@ -647,15 +626,10 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
else {
if (includeNonSingletons || isNonLazyDecorated) {
if (includeNonSingletons || isNonLazyDecorated ||
(allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) {
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
}
else if (allowFactoryBeanInit) {
// Type check before singleton check, avoiding FactoryBean instantiation
// for early FactoryBean.isSingleton() calls on non-matching beans.
matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit) &&
isSingleton(beanName, mbd, dbd);
}
if (!matchFound) {
// In case of FactoryBean, try to match FactoryBean instance itself next.
beanName = FACTORY_BEAN_PREFIX + beanName;
@@ -921,7 +895,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
String beanName, DependencyDescriptor descriptor, AutowireCandidateResolver resolver)
throws NoSuchBeanDefinitionException {
String bdName = transformedBeanName(beanName);
String bdName = BeanFactoryUtils.transformedBeanName(beanName);
if (containsBeanDefinition(bdName)) {
return isAutowireCandidate(beanName, getMergedLocalBeanDefinition(bdName), descriptor, resolver);
}
@@ -955,7 +929,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
protected boolean isAutowireCandidate(String beanName, RootBeanDefinition mbd,
DependencyDescriptor descriptor, AutowireCandidateResolver resolver) {
String bdName = transformedBeanName(beanName);
String bdName = BeanFactoryUtils.transformedBeanName(beanName);
resolveBeanClass(mbd, bdName);
if (mbd.isFactoryMethodUnique && mbd.factoryMethodToIntrospect == null) {
new ConstructorResolver(this).resolveFactoryMethodIfPossible(mbd);
@@ -1033,14 +1007,6 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
return super.obtainInstanceFromSupplier(supplier, beanName, mbd);
}
@Override
protected void cacheMergedBeanDefinition(RootBeanDefinition mbd, String beanName) {
super.cacheMergedBeanDefinition(mbd, beanName);
if (mbd.isPrimary()) {
this.primaryBeanNames.add(beanName);
}
}
@Override
protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) {
super.checkMergedBeanDefinition(mbd, beanName, args);
@@ -1053,7 +1019,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
else {
// Bean intended to be initialized in main bootstrap thread.
// Bean intended to be initialized in main bootstrap thread
if (this.preInstantiationThread.get() == PreInstantiation.BACKGROUND) {
throw new BeanCurrentlyInCreationException(beanName, "Bean marked for mainline initialization " +
"but requested in background thread - enforce early instantiation in mainline thread " +
@@ -1065,39 +1031,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
@Override
@Nullable
protected Boolean isCurrentThreadAllowedToHoldSingletonLock() {
String mainThreadPrefix = this.mainThreadPrefix;
if (this.mainThreadPrefix != null) {
// We only differentiate in the preInstantiateSingletons phase.
PreInstantiation preInstantiation = this.preInstantiationThread.get();
if (preInstantiation != null) {
// A Spring-managed bootstrap thread:
// MAIN is allowed to lock (true) or even forced to lock (null),
// BACKGROUND is never allowed to lock (false).
return switch (preInstantiation) {
case MAIN -> (Boolean.TRUE.equals(this.strictLocking) ? null : true);
case BACKGROUND -> false;
};
}
// Not a Spring-managed bootstrap thread...
if (Boolean.FALSE.equals(this.strictLocking)) {
// Explicitly configured to use lenient locking wherever possible.
return true;
}
else if (this.strictLocking == null) {
// No explicit locking configuration -> infer appropriate locking.
if (mainThreadPrefix != null && !getThreadNamePrefix().equals(mainThreadPrefix)) {
// An unmanaged thread (assumed to be application-internal) with lenient locking,
// and not part of the same thread pool that provided the main bootstrap thread
// (excluding scenarios where we are hit by multiple external bootstrap threads).
return true;
}
}
}
// Traditional behavior: forced to always hold a full lock.
return null;
return (this.preInstantiationPhase ? this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null);
}
@Override
@@ -1113,8 +1047,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
// Trigger initialization of all non-lazy singleton beans...
List<CompletableFuture<?>> futures = new ArrayList<>();
this.preInstantiationPhase = true;
this.preInstantiationThread.set(PreInstantiation.MAIN);
this.mainThreadPrefix = getThreadNamePrefix();
try {
for (String beanName : beanNames) {
RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
@@ -1127,8 +1061,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
finally {
this.mainThreadPrefix = null;
this.preInstantiationThread.remove();
this.preInstantiationPhase = false;
}
if (!futures.isEmpty()) {
@@ -1222,12 +1156,6 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
}
}
private static String getThreadNamePrefix() {
String name = Thread.currentThread().getName();
int numberSeparator = name.lastIndexOf('-');
return (numberSeparator >= 0 ? name.substring(0, numberSeparator) : name);
}
//---------------------------------------------------------------------
// Implementation of BeanDefinitionRegistry interface
@@ -17,7 +17,6 @@
package org.springframework.beans.factory.support;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
@@ -111,12 +110,6 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
/** Names of beans that are currently in lenient creation. */
private final Set<String> singletonsInLenientCreation = new HashSet<>();
/** Map from one creation thread waiting on a lenient creation thread. */
private final Map<Thread, Thread> lenientWaitingThreads = new HashMap<>();
/** Map from bean name to actual creation thread for currently created beans. */
private final Map<String, Thread> currentCreationThreads = new ConcurrentHashMap<>();
/** Flag that indicates whether we're currently within destroySingletons. */
private volatile boolean singletonsCurrentlyInDestruction = false;
@@ -257,11 +250,9 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(beanName, "Bean name must not be null");
Thread currentThread = Thread.currentThread();
Boolean lockFlag = isCurrentThreadAllowedToHoldSingletonLock();
boolean acquireLock = !Boolean.FALSE.equals(lockFlag);
boolean locked = (acquireLock && this.singletonLock.tryLock());
try {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
@@ -272,7 +263,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
// 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 \"" +
logger.info("Creating singleton bean '" + beanName + "' in thread \"" +
Thread.currentThread().getName() + "\" while other thread holds " +
"singleton lock for other beans " + this.singletonsCurrentlyInCreation);
}
@@ -313,27 +304,14 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
this.lenientCreationLock.lock();
try {
while ((singletonObject = this.singletonObjects.get(beanName)) == null) {
Thread otherThread = this.currentCreationThreads.get(beanName);
if (otherThread != null && (otherThread == currentThread ||
checkDependentWaitingThreads(otherThread, currentThread))) {
throw ex;
}
if (!this.singletonsInLenientCreation.contains(beanName)) {
break;
}
if (otherThread != null) {
this.lenientWaitingThreads.put(currentThread, otherThread);
}
try {
this.lenientCreationFinished.await();
}
catch (InterruptedException ie) {
currentThread.interrupt();
}
finally {
if (otherThread != null) {
this.lenientWaitingThreads.remove(currentThread);
}
Thread.currentThread().interrupt();
}
}
}
@@ -366,13 +344,7 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
// Leniently created singleton object could have appeared in the meantime.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
this.currentCreationThreads.put(beanName, currentThread);
try {
singletonObject = singletonFactory.getObject();
}
finally {
this.currentCreationThreads.remove(beanName);
}
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
}
@@ -421,8 +393,6 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
this.lenientCreationLock.lock();
try {
this.singletonsInLenientCreation.remove(beanName);
this.lenientWaitingThreads.entrySet().removeIf(
entry -> entry.getValue() == currentThread);
this.lenientCreationFinished.signalAll();
}
finally {
@@ -431,28 +401,14 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
}
}
private boolean checkDependentWaitingThreads(Thread waitingThread, Thread candidateThread) {
Thread threadToCheck = waitingThread;
while ((threadToCheck = this.lenientWaitingThreads.get(threadToCheck)) != null) {
if (threadToCheck == candidateThread) {
return true;
}
}
return false;
}
/**
* Determine whether the current thread is allowed to hold the singleton lock.
* <p>By default, all threads are forced to hold a full lock through {@code null}.
* {@link DefaultListableBeanFactory} overrides this to specifically handle its
* threads during the pre-instantiation phase: {@code true} for the main thread,
* {@code false} for managed background threads, and configuration-dependent
* behavior for unmanaged threads.
* @return {@code true} if the current thread is explicitly allowed to hold the
* lock but also accepts lenient fallback behavior, {@code false} if it is
* explicitly not allowed to hold the lock and therefore forced to use lenient
* fallback behavior, or {@code null} if there is no specific indication
* (traditional behavior: forced to always hold a full lock)
* <p>By default, any thread may acquire and hold the singleton lock, except
* background threads from {@link DefaultListableBeanFactory#setBootstrapExecutor}.
* @return {@code false} if the current thread is explicitly not allowed to hold
* the lock, {@code true} if it is explicitly allowed to hold the lock but also
* accepts lenient fallback behavior, or {@code null} if there is no specific
* indication (traditional behavior: always holding a full lock)
* @since 6.2
*/
@Nullable
@@ -751,19 +707,12 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements
// For an individual destruction, remove the registered instance now.
// As of 6.2, this happens after the current bean's destruction step,
// allowing for late bean retrieval by on-demand suppliers etc.
if (this.currentCreationThreads.get(beanName) == Thread.currentThread()) {
// Local remove after failed creation step -> without singleton lock
// since bean creation may have happened leniently without any lock.
this.singletonLock.lock();
try {
removeSingleton(beanName);
}
else {
this.singletonLock.lock();
try {
removeSingleton(beanName);
}
finally {
this.singletonLock.unlock();
}
finally {
this.singletonLock.unlock();
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -87,7 +87,7 @@ public class MethodOverrides {
/**
* Return the override for the given method, if any.
* @param method the method to check for overrides for
* @param method method to check for overrides for
* @return the method override, or {@code null} if none
*/
@Nullable
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,7 +35,7 @@ public interface MethodReplacer {
* @param obj the instance we're reimplementing the method for
* @param method the method to reimplement
* @param args arguments to the method
* @return the return value for the method
* @return return value for the method
*/
Object reimplement(Object obj, Method method, Object[] args) throws Throwable;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -405,10 +405,10 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader
/**
* Get all property values, given a prefix (which will be stripped)
* and add the bean they define to the factory with the given name.
* @param beanName the name of the bean to define
* @param beanName name of the bean to define
* @param map a Map containing string pairs
* @param prefix the prefix of each entry, which will be stripped
* @param resourceDescription the description of the resource that the
* @param prefix prefix of each entry, which will be stripped
* @param resourceDescription description of the resource that the
* Map came from (for logging purposes)
* @throws BeansException if the bean definition could not be parsed or registered
*/
@@ -76,6 +76,7 @@ import org.springframework.beans.testfixture.beans.NestedTestBean;
import org.springframework.beans.testfixture.beans.SideEffectBean;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.beans.testfixture.beans.factory.DummyFactory;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.Ordered;
import org.springframework.core.ResolvableType;
@@ -262,32 +263,6 @@ class DefaultListableBeanFactoryTests {
assertThat(DummyFactory.wasPrototypeCreated()).as("prototype not instantiated").isFalse();
}
@Test
void nonInitializedFactoryBeanIgnoredByEagerTypeMatching() {
RootBeanDefinition bd = new RootBeanDefinition(DummyFactory.class);
bd.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, String.class);
lbf.registerBeanDefinition("x1", bd);
assertBeanNamesForType(TestBean.class, false, true);
assertThat(lbf.getBeanNamesForAnnotation(SuppressWarnings.class)).isEmpty();
assertThat(lbf.containsSingleton("x1")).isFalse();
assertThat(lbf.containsBean("x1")).isTrue();
assertThat(lbf.containsBean("&x1")).isTrue();
assertThat(lbf.isSingleton("x1")).isTrue();
assertThat(lbf.isSingleton("&x1")).isTrue();
assertThat(lbf.isPrototype("x1")).isFalse();
assertThat(lbf.isPrototype("&x1")).isFalse();
assertThat(lbf.isTypeMatch("x1", TestBean.class)).isTrue();
assertThat(lbf.isTypeMatch("&x1", TestBean.class)).isFalse();
assertThat(lbf.isTypeMatch("&x1", DummyFactory.class)).isTrue();
assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClass(DummyFactory.class))).isTrue();
assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, Object.class))).isTrue();
assertThat(lbf.isTypeMatch("&x1", ResolvableType.forClassWithGenerics(FactoryBean.class, String.class))).isFalse();
assertThat(lbf.getType("x1")).isEqualTo(TestBean.class);
assertThat(lbf.getType("&x1")).isEqualTo(DummyFactory.class);
}
@Test
void initializedFactoryBeanFoundByNonEagerTypeMatching() {
Properties p = new Properties();
@@ -1418,6 +1393,7 @@ class DefaultListableBeanFactoryTests {
lbf.registerBeanDefinition("rod", bd);
RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class);
lbf.registerBeanDefinition("rod2", bd2);
lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(() -> lbf.autowire(ConstructorDependency.class, AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, false))
@@ -1488,6 +1464,7 @@ class DefaultListableBeanFactoryTests {
RootBeanDefinition bd = new RootBeanDefinition(ConstructorDependenciesBean.class);
bd.setAutowireMode(RootBeanDefinition.AUTOWIRE_CONSTRUCTOR);
lbf.registerBeanDefinition("bean", bd);
lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class);
Object spouse1 = lbf.getBean("spouse1");
@@ -1505,6 +1482,7 @@ class DefaultListableBeanFactoryTests {
bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE,
ConstructorDependenciesBean.class.getConstructors());
lbf.registerBeanDefinition("bean", bd);
lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class);
Object spouse1 = lbf.getBean("spouse1");
@@ -1522,6 +1500,7 @@ class DefaultListableBeanFactoryTests {
bd.setAttribute(GenericBeanDefinition.PREFERRED_CONSTRUCTORS_ATTRIBUTE,
ConstructorDependenciesBean.class.getConstructor(TestBean.class));
lbf.registerBeanDefinition("bean", bd);
lbf.setParameterNameDiscoverer(new DefaultParameterNameDiscoverer());
ConstructorDependenciesBean bean = lbf.getBean(ConstructorDependenciesBean.class);
Object spouse = lbf.getBean("spouse1");
@@ -1727,40 +1727,6 @@ class AutowiredAnnotationBeanPostProcessorTests {
tb2.setFactoryMethodName("newTestBean2");
tb2.setLazyInit(true);
bf.registerBeanDefinition("testBean2", tb2);
bf.registerAlias("testBean2", "testBean");
ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class);
TestBean testBean1 = bf.getBean("testBean1", TestBean.class);
assertThat(bean.getTestBean()).isSameAs(testBean1);
assertThat(bean.getOptionalTestBean()).isSameAs(testBean1);
assertThat(bean.consumeOptionalTestBean()).isSameAs(testBean1);
assertThat(bean.getUniqueTestBean()).isSameAs(testBean1);
assertThat(bean.consumeUniqueTestBean()).isSameAs(testBean1);
assertThat(bf.containsSingleton("testBean2")).isFalse();
TestBean testBean2 = bf.getBean("testBean2", TestBean.class);
assertThat(bean.iterateTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.forEachTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.streamTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.streamTestBeansInOrder()).containsExactly(testBean2, testBean1);
assertThat(bean.allTestBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.allTestBeansInOrder()).containsExactly(testBean2, testBean1);
assertThat(bean.allSingletonBeans()).containsExactly(testBean1, testBean2);
assertThat(bean.allSingletonBeansInOrder()).containsExactly(testBean2, testBean1);
}
@Test
void objectProviderInjectionWithLateMarkedTargetPrimary() {
bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(ObjectProviderInjectionBean.class));
RootBeanDefinition tb1 = new RootBeanDefinition(TestBeanFactory.class);
tb1.setFactoryMethodName("newTestBean1");
bf.registerBeanDefinition("testBean1", tb1);
RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class);
tb2.setFactoryMethodName("newTestBean2");
tb2.setLazyInit(true);
bf.registerBeanDefinition("testBean2", tb2);
bf.registerAlias("testBean2", "testBean");
tb1.setPrimary(true);
ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class);
TestBean testBean1 = bf.getBean("testBean1", TestBean.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,7 +18,6 @@ package org.springframework.beans.factory.aot;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.time.temporal.ChronoUnit;
@@ -29,6 +28,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Supplier;
import javax.lang.model.element.Modifier;
@@ -54,7 +54,7 @@ import org.springframework.core.testfixture.aot.generate.value.ExampleClass;
import org.springframework.core.testfixture.aot.generate.value.ExampleClass$$GeneratedBy;
import org.springframework.javapoet.CodeBlock;
import org.springframework.javapoet.MethodSpec;
import org.springframework.util.ReflectionUtils;
import org.springframework.javapoet.ParameterizedTypeName;
import static org.assertj.core.api.Assertions.assertThat;
@@ -83,23 +83,14 @@ class BeanDefinitionPropertyValueCodeGeneratorDelegatesTests {
CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value);
typeBuilder.set(type -> {
type.addModifiers(Modifier.PUBLIC);
type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC, Modifier.STATIC)
type.addSuperinterface(
ParameterizedTypeName.get(Supplier.class, Object.class));
type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC)
.returns(Object.class).addStatement("return $L", generatedCode).build());
});
generationContext.writeGeneratedContent();
TestCompiler.forSystem().with(generationContext).compile(compiled ->
result.accept(getGeneratedCodeReturnValue(compiled, generatedClass), compiled));
}
private static Object getGeneratedCodeReturnValue(Compiled compiled, GeneratedClass generatedClass) {
try {
Object instance = compiled.getInstance(Object.class, generatedClass.getName().reflectionName());
Method get = ReflectionUtils.findMethod(instance.getClass(), "get");
return get.invoke(null);
}
catch (Exception ex) {
throw new RuntimeException("Failed to invoke generated code '%s':".formatted(generatedClass.getName()), ex);
}
result.accept(compiled.getInstance(Supplier.class).get(), compiled));
}
@Nested
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,13 +21,12 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.util.ClassUtils;
@@ -44,17 +43,14 @@ class QualifierAnnotationAutowireBeanFactoryTests {
private static final String MARK = "mark";
private final DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
@Test
void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception {
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addGenericArgumentValue(JUERGEN);
RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null);
lbf.registerBeanDefinition(JUERGEN, rbd);
assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN,
new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isTrue();
@@ -64,12 +60,12 @@ class QualifierAnnotationAutowireBeanFactoryTests {
@Test
void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Exception {
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addGenericArgumentValue(JUERGEN);
RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null);
rbd.setAutowireCandidate(false);
lbf.registerBeanDefinition(JUERGEN, rbd);
assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse();
assertThat(lbf.isAutowireCandidate(JUERGEN,
new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isFalse();
@@ -77,46 +73,44 @@ class QualifierAnnotationAutowireBeanFactoryTests {
new DependencyDescriptor(Person.class.getDeclaredField("name"), true))).isFalse();
}
@Disabled
@Test
void testAutowireCandidateWithFieldDescriptor() throws Exception {
lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver());
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs1 = new ConstructorArgumentValues();
cavs1.addGenericArgumentValue(JUERGEN);
RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null);
person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class));
lbf.registerBeanDefinition(JUERGEN, person1);
ConstructorArgumentValues cavs2 = new ConstructorArgumentValues();
cavs2.addGenericArgumentValue(MARK);
RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null);
lbf.registerBeanDefinition(MARK, person2);
DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(
QualifiedTestBean.class.getDeclaredField("qualified"), false);
DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(
QualifiedTestBean.class.getDeclaredField("nonqualified"), false);
assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse();
}
@Test
void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception {
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addGenericArgumentValue(JUERGEN);
RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null);
person.setAutowireCandidate(false);
person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class));
lbf.registerBeanDefinition(JUERGEN, person);
DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(
QualifiedTestBean.class.getDeclaredField("qualified"), false);
DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(
QualifiedTestBean.class.getDeclaredField("nonqualified"), false);
assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse();
assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isFalse();
assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isFalse();
@@ -124,61 +118,56 @@ class QualifierAnnotationAutowireBeanFactoryTests {
@Test
void testAutowireCandidateWithShortClassName() throws Exception {
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs = new ConstructorArgumentValues();
cavs.addGenericArgumentValue(JUERGEN);
RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null);
person.addQualifier(new AutowireCandidateQualifier(ClassUtils.getShortName(TestQualifier.class)));
lbf.registerBeanDefinition(JUERGEN, person);
DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(
QualifiedTestBean.class.getDeclaredField("qualified"), false);
DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(
QualifiedTestBean.class.getDeclaredField("nonqualified"), false);
assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue();
}
@Disabled
@Test
void testAutowireCandidateWithConstructorDescriptor() throws Exception {
lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver());
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs1 = new ConstructorArgumentValues();
cavs1.addGenericArgumentValue(JUERGEN);
RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null);
person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class));
lbf.registerBeanDefinition(JUERGEN, person1);
ConstructorArgumentValues cavs2 = new ConstructorArgumentValues();
cavs2.addGenericArgumentValue(MARK);
RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null);
lbf.registerBeanDefinition(MARK, person2);
MethodParameter param = new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0);
DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(param, false);
param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
assertThat(param.getParameterName()).isEqualTo("tpb");
assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse();
}
@Disabled
@Test
void testAutowireCandidateWithMethodDescriptor() throws Exception {
lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver());
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs1 = new ConstructorArgumentValues();
cavs1.addGenericArgumentValue(JUERGEN);
RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null);
person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class));
lbf.registerBeanDefinition(JUERGEN, person1);
ConstructorArgumentValues cavs2 = new ConstructorArgumentValues();
cavs2.addGenericArgumentValue(MARK);
RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null);
lbf.registerBeanDefinition(MARK, person2);
MethodParameter qualifiedParam =
new MethodParameter(QualifiedTestBean.class.getDeclaredMethod("autowireQualified", Person.class), 0);
MethodParameter nonqualifiedParam =
@@ -186,70 +175,37 @@ class QualifierAnnotationAutowireBeanFactoryTests {
DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(qualifiedParam, false);
DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(nonqualifiedParam, false);
qualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
nonqualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb");
nonqualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer());
assertThat(nonqualifiedParam.getParameterName()).isEqualTo("tpb");
assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse();
}
@Test
void testAutowireCandidateWithMultipleCandidatesDescriptor() throws Exception {
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory();
ConstructorArgumentValues cavs1 = new ConstructorArgumentValues();
cavs1.addGenericArgumentValue(JUERGEN);
RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null);
person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class));
lbf.registerBeanDefinition(JUERGEN, person1);
ConstructorArgumentValues cavs2 = new ConstructorArgumentValues();
cavs2.addGenericArgumentValue(MARK);
RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null);
person2.addQualifier(new AutowireCandidateQualifier(TestQualifier.class));
lbf.registerBeanDefinition(MARK, person2);
DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(
new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0),
false);
assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue();
assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isTrue();
}
@Test
void autowireBeanByTypeWithQualifierPrecedence() throws Exception {
lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver());
RootBeanDefinition bd = new RootBeanDefinition(TestBean.class);
RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class);
lbf.registerBeanDefinition("testBean", bd);
lbf.registerBeanDefinition("spouse", bd2);
lbf.registerAlias("test", "testBean");
assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null))
.isSameAs(lbf.getBean("spouse"));
}
@Test
void autowireBeanByTypeWithQualifierPrecedenceInAncestor() throws Exception {
DefaultListableBeanFactory parent = new DefaultListableBeanFactory();
parent.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver());
RootBeanDefinition bd = new RootBeanDefinition(TestBean.class);
RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class);
parent.registerBeanDefinition("test", bd);
parent.registerBeanDefinition("spouse", bd2);
parent.registerAlias("test", "testBean");
DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(parent);
lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver());
assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null))
.isSameAs(lbf.getBean("spouse"));
}
@SuppressWarnings("unused")
private static class QualifiedTestBean {
@@ -291,8 +247,4 @@ class QualifierAnnotationAutowireBeanFactoryTests {
private @interface TestQualifier {
}
@Qualifier("spouse")
private TestBean testBean;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -105,7 +105,7 @@ public abstract class AbstractCacheInvoker {
return valueLoader.call();
}
catch (Exception ex2) {
throw new Cache.ValueRetrievalException(key, valueLoader, ex);
throw new RuntimeException(ex2);
}
}
}
@@ -124,12 +124,16 @@ public abstract class AbstractCacheInvoker {
try {
return cache.retrieve(key);
}
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return null;
}
}
/**
* Execute {@link Cache#retrieve(Object, Supplier)} on the specified
* {@link Cache} and invoke the error handler if an exception occurs.
@@ -142,6 +146,9 @@ public abstract class AbstractCacheInvoker {
try {
return cache.retrieve(key, valueLoader);
}
catch (Cache.ValueRetrievalException ex) {
throw ex;
}
catch (RuntimeException ex) {
getErrorHandler().handleCacheGetError(ex, cache, key);
return valueLoader.get();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,7 +26,6 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Supplier;
import org.apache.commons.logging.Log;
@@ -450,7 +449,6 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return cacheHit;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Nullable
private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
@@ -458,33 +456,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
if (CompletableFuture.class.isAssignableFrom(method.getReturnType())) {
AtomicBoolean invokeFailure = new AtomicBoolean(false);
CompletableFuture<?> result = doRetrieve(cache, key,
() -> {
CompletableFuture<?> invokeResult = ((CompletableFuture<?>) invokeOperation(invoker));
if (invokeResult == null) {
return null;
}
return invokeResult.exceptionallyCompose(ex -> {
invokeFailure.set(true);
return CompletableFuture.failedFuture(ex);
});
});
return result.exceptionallyCompose(ex -> {
if (!(ex instanceof RuntimeException rex)) {
return CompletableFuture.failedFuture(ex);
}
try {
getErrorHandler().handleCacheGetError(rex, cache, key);
if (invokeFailure.get()) {
return CompletableFuture.failedFuture(ex);
}
return (CompletableFuture) invokeOperation(invoker);
}
catch (Throwable ex2) {
return CompletableFuture.failedFuture(ex2);
}
});
return doRetrieve(cache, key, () -> (CompletableFuture<?>) invokeOperation(invoker));
}
if (this.reactiveCachingHandler != null) {
Object returnValue = this.reactiveCachingHandler.executeSynchronized(invoker, method, cache, key);
@@ -545,17 +517,9 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
if (CompletableFuture.class.isAssignableFrom(context.getMethod().getReturnType())) {
CompletableFuture<?> result = doRetrieve(cache, key);
if (result != null) {
return result.exceptionallyCompose(ex -> {
if (!(ex instanceof RuntimeException rex)) {
return CompletableFuture.failedFuture(ex);
}
try {
getErrorHandler().handleCacheGetError(rex, cache, key);
return CompletableFuture.completedFuture(null);
}
catch (Throwable ex2) {
return CompletableFuture.failedFuture(ex2);
}
return result.exceptionally(ex -> {
getErrorHandler().handleCacheGetError((RuntimeException) ex, cache, key);
return null;
}).thenCompose(value -> (CompletableFuture<?>) evaluate(
(value != null ? CompletableFuture.completedFuture(unwrapCacheValue(value)) : null),
invoker, method, contexts));
@@ -1133,72 +1097,32 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance();
@SuppressWarnings({"rawtypes", "unchecked"})
@Nullable
public Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) {
AtomicBoolean invokeFailure = new AtomicBoolean(false);
ReactiveAdapter adapter = this.registry.getAdapter(method.getReturnType());
if (adapter != null) {
if (adapter.isMultiValue()) {
// Flux or similar
return adapter.fromPublisher(Flux.from(Mono.fromFuture(
doRetrieve(cache, key,
() -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().doOnError(ex -> invokeFailure.set(true)).toFuture())))
.flatMap(Flux::fromIterable)
.onErrorResume(RuntimeException.class, ex -> {
try {
getErrorHandler().handleCacheGetError(ex, cache, key);
if (invokeFailure.get()) {
return Flux.error(ex);
}
return Flux.from(adapter.toPublisher(invokeOperation(invoker)));
}
catch (RuntimeException exception) {
return Flux.error(exception);
}
}));
cache.retrieve(key,
() -> Flux.from(adapter.toPublisher(invokeOperation(invoker))).collectList().toFuture())))
.flatMap(Flux::fromIterable));
}
else {
// Mono or similar
return adapter.fromPublisher(Mono.fromFuture(
doRetrieve(cache, key,
() -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).doOnError(ex -> invokeFailure.set(true)).toFuture()))
.onErrorResume(RuntimeException.class, ex -> {
try {
getErrorHandler().handleCacheGetError(ex, cache, key);
if (invokeFailure.get()) {
return Mono.error(ex);
}
return Mono.from(adapter.toPublisher(invokeOperation(invoker)));
}
catch (RuntimeException exception) {
return Mono.error(exception);
}
}));
cache.retrieve(key,
() -> Mono.from(adapter.toPublisher(invokeOperation(invoker))).toFuture())));
}
}
if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) {
return Mono.fromFuture(doRetrieve(cache, key, () -> {
Mono<?> mono = (Mono<?>) invokeOperation(invoker);
if (mono != null) {
mono = mono.doOnError(ex -> invokeFailure.set(true));
}
else {
return Mono.fromFuture(cache.retrieve(key, () -> {
Mono<?> mono = ((Mono<?>) invokeOperation(invoker));
if (mono == null) {
mono = Mono.empty();
}
return mono.toFuture();
})).onErrorResume(RuntimeException.class, ex -> {
try {
getErrorHandler().handleCacheGetError(ex, cache, key);
if (invokeFailure.get()) {
return Mono.error(ex);
}
return (Mono) invokeOperation(invoker);
}
catch (RuntimeException exception) {
return Mono.error(exception);
}
});
}));
}
return NOT_HANDLED;
}
@@ -1213,7 +1137,7 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker
return NOT_HANDLED;
}
@SuppressWarnings({"rawtypes", "unchecked"})
@SuppressWarnings({ "unchecked", "rawtypes" })
@Nullable
public Object findInCaches(CacheOperationContext context, Cache cache, Object key,
CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
@@ -26,7 +26,6 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aop.scope.ScopedProxyFactoryBean;
import org.springframework.aot.AotDetector;
import org.springframework.asm.Opcodes;
import org.springframework.asm.Type;
import org.springframework.beans.factory.BeanDefinitionStoreException;
@@ -116,12 +115,6 @@ class ConfigurationClassEnhancer {
boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader());
if (classLoaderMismatch && classLoader instanceof SmartClassLoader smartClassLoader) {
classLoader = smartClassLoader.getOriginalClassLoader();
classLoaderMismatch = (classLoader != configClass.getClassLoader());
}
// Use original ClassLoader if config class relies on package visibility
if (classLoaderMismatch && reliesOnPackageVisibility(configClass)) {
classLoader = configClass.getClassLoader();
classLoaderMismatch = false;
}
Enhancer enhancer = newEnhancer(configClass, classLoader);
Class<?> enhancedClass = createClass(enhancer, classLoaderMismatch);
@@ -138,26 +131,6 @@ class ConfigurationClassEnhancer {
}
}
/**
* Checks whether the given config class relies on package visibility,
* either for the class itself or for any of its {@code @Bean} methods.
*/
private boolean reliesOnPackageVisibility(Class<?> configSuperClass) {
int mod = configSuperClass.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) {
if (BeanAnnotationHelper.isBeanAnnotated(method)) {
mod = method.getModifiers();
if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) {
return true;
}
}
}
return false;
}
/**
* Creates a new CGLIB {@link Enhancer} instance.
*/
@@ -165,22 +138,26 @@ class ConfigurationClassEnhancer {
Enhancer enhancer = new Enhancer();
if (classLoader != null) {
enhancer.setClassLoader(classLoader);
if (classLoader instanceof SmartClassLoader smartClassLoader &&
smartClassLoader.isClassReloadable(configSuperClass)) {
enhancer.setUseCache(false);
}
}
enhancer.setSuperclass(configSuperClass);
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
enhancer.setUseFactory(false);
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts());
enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader));
enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
enhancer.setCallbackFilter(CALLBACK_FILTER);
enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
return enhancer;
}
/**
* Checks whether the given configuration class is reloadable.
*/
private boolean isClassReloadable(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
return (classLoader instanceof SmartClassLoader smartClassLoader &&
smartClassLoader.isClassReloadable(configSuperClass));
}
/**
* Uses enhancer to generate a subclass of superclass,
* ensuring that callbacks are registered for the new subclass.
@@ -364,9 +341,9 @@ class ConfigurationClassEnhancer {
// proxy that intercepts calls to getObject() and returns any cached bean instance.
// This ensures that the semantics of calling a FactoryBean from within @Bean methods
// is the same as that of referring to a FactoryBean within XML. See SPR-6602.
String factoryBeanName = BeanFactory.FACTORY_BEAN_PREFIX + beanName;
if (factoryContainsBean(beanFactory, factoryBeanName) && factoryContainsBean(beanFactory, beanName)) {
Object factoryBean = beanFactory.getBean(factoryBeanName);
if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
factoryContainsBean(beanFactory, beanName)) {
Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
if (factoryBean instanceof ScopedProxyFactoryBean) {
// Scoped proxy factory beans are a special case and should not be further proxied
}
@@ -571,7 +548,7 @@ class ConfigurationClassEnhancer {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(factoryBean.getClass());
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts());
enhancer.setAttemptLoad(true);
enhancer.setCallbackType(MethodInterceptor.class);
// Ideally create enhanced FactoryBean proxy without constructor side effects,
@@ -179,9 +179,8 @@ class ConfigurationClassParser {
}
// Downgrade to lite (no enhancement) in case of no instance-level @Bean methods.
if (!configClass.getMetadata().isAbstract() && !configClass.hasNonStaticBeanMethods() &&
ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(
bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) {
if (!configClass.hasNonStaticBeanMethods() && ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(
bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) {
bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE,
ConfigurationClassUtils.CONFIGURATION_CLASS_LITE);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@
package org.springframework.context.aot;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import org.springframework.aot.generate.FileSystemGeneratedFiles;
@@ -103,7 +102,7 @@ public abstract class AbstractAotProcessor<T> {
FileSystemUtils.deleteRecursively(path);
}
catch (IOException ex) {
throw new UncheckedIOException("Failed to delete existing output in '" + path + "'", ex);
throw new RuntimeException("Failed to delete existing output in '" + path + "'");
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,12 +26,9 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
import org.apache.commons.logging.Log;
@@ -55,7 +52,6 @@ import org.springframework.core.SpringProperties;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
/**
* Spring's default implementation of the {@link LifecycleProcessor} strategy.
@@ -65,23 +61,12 @@ import org.springframework.util.CollectionUtils;
* interactions on a {@link org.springframework.context.ConfigurableApplicationContext}.
*
* <p>As of 6.1, this also includes support for JVM checkpoint/restore (Project CRaC)
* when the {@code org.crac:crac} dependency is on the classpath. All running beans
* will get stopped and restarted according to the CRaC checkpoint/restore callbacks.
*
* <p>As of 6.2, this processor can be configured with custom timeouts for specific
* shutdown phases, applied to {@link SmartLifecycle#stop(Runnable)} implementations.
* As of 6.2.6, there is also support for the concurrent startup of specific phases
* with individual timeouts, triggering the {@link SmartLifecycle#start()} callbacks
* of all associated beans asynchronously and then waiting for all of them to return,
* as an alternative to the default sequential startup of beans without a timeout.
* when the {@code org.crac:crac} dependency on the classpath.
*
* @author Mark Fisher
* @author Juergen Hoeller
* @author Sebastien Deleuze
* @since 3.0
* @see SmartLifecycle#getPhase()
* @see #setConcurrentStartupForPhase
* @see #setTimeoutForShutdownPhase
*/
public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactoryAware {
@@ -117,8 +102,6 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private final Log logger = LogFactory.getLog(getClass());
private final Map<Integer, Long> concurrentStartupForPhases = new ConcurrentHashMap<>();
private final Map<Integer, Long> timeoutsForShutdownPhases = new ConcurrentHashMap<>();
private volatile long timeoutPerShutdownPhase = 10000;
@@ -147,59 +130,20 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
/**
* Switch to concurrent startup for each given phase (group of {@link SmartLifecycle}
* beans with the same 'phase' value) with corresponding timeouts.
* <p><b>Note: By default, the startup for every phase will be sequential without
* a timeout. Calling this setter with timeouts for the given phases switches to a
* mode where the beans in these phases will be started concurrently, cancelling
* the startup if the corresponding timeout is not met for any of these phases.</b>
* <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
* set for the application context, typically through a "bootstrapExecutor" bean.
* @param phasesWithTimeouts a map of phase values (matching
* {@link SmartLifecycle#getPhase()}) and corresponding timeout values
* (in milliseconds)
* @since 6.2.6
* @see SmartLifecycle#getPhase()
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
*/
public void setConcurrentStartupForPhases(Map<Integer, Long> phasesWithTimeouts) {
this.concurrentStartupForPhases.putAll(phasesWithTimeouts);
}
/**
* Switch to concurrent startup for a specific phase (group of {@link SmartLifecycle}
* beans with the same 'phase' value) with a corresponding timeout.
* <p><b>Note: By default, the startup for every phase will be sequential without
* a timeout. Calling this setter with a timeout for the given phase switches to a
* mode where the beans in this phase will be started concurrently, cancelling
* the startup if the corresponding timeout is not met for this phase.</b>
* <p>For an actual concurrent startup, a bootstrap {@code Executor} needs to be
* set for the application context, typically through a "bootstrapExecutor" bean.
* @param phase the phase value (matching {@link SmartLifecycle#getPhase()})
* @param timeout the corresponding timeout value (in milliseconds)
* @since 6.2.6
* @see SmartLifecycle#getPhase()
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory#getBootstrapExecutor()
*/
public void setConcurrentStartupForPhase(int phase, long timeout) {
this.concurrentStartupForPhases.put(phase, timeout);
}
/**
* Specify the maximum time allotted for the shutdown of each given phase
* (group of {@link SmartLifecycle} beans with the same 'phase' value).
* <p>In case of no specific timeout configured, the default timeout per
* shutdown phase will apply: 10000 milliseconds (10 seconds) as of 6.2.
* @param phasesWithTimeouts a map of phase values (matching
* @param timeoutsForShutdownPhases a map of phase values (matching
* {@link SmartLifecycle#getPhase()}) and corresponding timeout values
* (in milliseconds)
* @since 6.2
* @see SmartLifecycle#getPhase()
* @see #setTimeoutPerShutdownPhase
*/
public void setTimeoutsForShutdownPhases(Map<Integer, Long> phasesWithTimeouts) {
this.timeoutsForShutdownPhases.putAll(phasesWithTimeouts);
public void setTimeoutsForShutdownPhases(Map<Integer, Long> timeoutsForShutdownPhases) {
this.timeoutsForShutdownPhases.putAll(timeoutsForShutdownPhases);
}
/**
@@ -227,15 +171,17 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
this.timeoutPerShutdownPhase = timeoutPerShutdownPhase;
}
private long determineTimeout(int phase) {
Long timeout = this.timeoutsForShutdownPhases.get(phase);
return (timeout != null ? timeout : this.timeoutPerShutdownPhase);
}
@Override
public void setBeanFactory(BeanFactory beanFactory) {
if (!(beanFactory instanceof ConfigurableListableBeanFactory clbf)) {
throw new IllegalArgumentException(
"DefaultLifecycleProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
}
if (!this.concurrentStartupForPhases.isEmpty() && clbf.getBootstrapExecutor() == null) {
throw new IllegalStateException("'bootstrapExecutor' needs to be configured for concurrent startup");
}
this.beanFactory = clbf;
}
@@ -245,22 +191,6 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
return beanFactory;
}
private Executor getBootstrapExecutor() {
Executor executor = getBeanFactory().getBootstrapExecutor();
Assert.state(executor != null, "No 'bootstrapExecutor' available");
return executor;
}
@Nullable
private Long determineConcurrentStartup(int phase) {
return this.concurrentStartupForPhases.get(phase);
}
private long determineShutdownTimeout(int phase) {
Long timeout = this.timeoutsForShutdownPhases.get(phase);
return (timeout != null ? timeout : this.timeoutPerShutdownPhase);
}
// Lifecycle implementation
@@ -355,8 +285,9 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
lifecycleBeans.forEach((beanName, bean) -> {
if (!autoStartupOnly || isAutoStartupCandidate(beanName, bean)) {
int startupPhase = getPhase(bean);
phases.computeIfAbsent(startupPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, autoStartupOnly))
.add(beanName, bean);
phases.computeIfAbsent(startupPhase,
phase -> new LifecycleGroup(phase, determineTimeout(phase), lifecycleBeans, autoStartupOnly)
).add(beanName, bean);
}
});
@@ -377,41 +308,30 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
* @param lifecycleBeans a Map with bean name as key and Lifecycle instance as value
* @param beanName the name of the bean to start
*/
private void doStart(Map<String, ? extends Lifecycle> lifecycleBeans, String beanName,
boolean autoStartupOnly, @Nullable List<CompletableFuture<?>> futures) {
private void doStart(Map<String, ? extends Lifecycle> lifecycleBeans, String beanName, boolean autoStartupOnly) {
Lifecycle bean = lifecycleBeans.remove(beanName);
if (bean != null && bean != this) {
String[] dependenciesForBean = getBeanFactory().getDependenciesForBean(beanName);
for (String dependency : dependenciesForBean) {
doStart(lifecycleBeans, dependency, autoStartupOnly, futures);
doStart(lifecycleBeans, dependency, autoStartupOnly);
}
if (!bean.isRunning() && (!autoStartupOnly || toBeStarted(beanName, bean))) {
if (futures != null) {
futures.add(CompletableFuture.runAsync(() -> doStart(beanName, bean), getBootstrapExecutor()));
if (logger.isTraceEnabled()) {
logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]");
}
else {
doStart(beanName, bean);
try {
bean.start();
}
catch (Throwable ex) {
throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex);
}
if (logger.isDebugEnabled()) {
logger.debug("Successfully started bean '" + beanName + "'");
}
}
}
}
private void doStart(String beanName, Lifecycle bean) {
if (logger.isTraceEnabled()) {
logger.trace("Starting bean '" + beanName + "' of type [" + bean.getClass().getName() + "]");
}
try {
bean.start();
}
catch (Throwable ex) {
throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex);
}
if (logger.isDebugEnabled()) {
logger.debug("Successfully started bean '" + beanName + "'");
}
}
private boolean toBeStarted(String beanName, Lifecycle bean) {
Set<String> stoppedBeans = this.stoppedBeans;
return (stoppedBeans != null ? stoppedBeans.contains(beanName) :
@@ -424,8 +344,9 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean);
phases.computeIfAbsent(shutdownPhase, phase -> new LifecycleGroup(phase, lifecycleBeans, false))
.add(beanName, bean);
phases.computeIfAbsent(shutdownPhase,
phase -> new LifecycleGroup(phase, determineTimeout(phase), lifecycleBeans, false)
).add(beanName, bean);
});
if (!phases.isEmpty()) {
@@ -496,7 +417,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
// Overridable hooks
// overridable hooks
/**
* Retrieve all applicable Lifecycle beans: all singletons that have already been created,
@@ -552,6 +473,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private final int phase;
private final long timeout;
private final Map<String, ? extends Lifecycle> lifecycleBeans;
private final boolean autoStartupOnly;
@@ -560,8 +483,11 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private int smartMemberCount;
public LifecycleGroup(int phase, Map<String, ? extends Lifecycle> lifecycleBeans, boolean autoStartupOnly) {
public LifecycleGroup(
int phase, long timeout, Map<String, ? extends Lifecycle> lifecycleBeans, boolean autoStartupOnly) {
this.phase = phase;
this.timeout = timeout;
this.lifecycleBeans = lifecycleBeans;
this.autoStartupOnly = autoStartupOnly;
}
@@ -580,26 +506,8 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
if (logger.isDebugEnabled()) {
logger.debug("Starting beans in phase " + this.phase);
}
Long concurrentStartup = determineConcurrentStartup(this.phase);
List<CompletableFuture<?>> futures = (concurrentStartup != null ? new ArrayList<>() : null);
for (LifecycleGroupMember member : this.members) {
doStart(this.lifecycleBeans, member.name, this.autoStartupOnly, futures);
}
if (concurrentStartup != null && !CollectionUtils.isEmpty(futures)) {
try {
CompletableFuture.allOf(futures.toArray(new CompletableFuture<?>[0]))
.get(concurrentStartup, TimeUnit.MILLISECONDS);
}
catch (Exception ex) {
if (ex instanceof ExecutionException exEx) {
Throwable cause = exEx.getCause();
if (cause instanceof ApplicationContextException acEx) {
throw acEx;
}
}
throw new ApplicationContextException("Failed to start beans in phase " + this.phase +
" within timeout of " + concurrentStartup + "ms", ex);
}
doStart(this.lifecycleBeans, member.name, this.autoStartupOnly);
}
}
@@ -623,14 +531,11 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
}
try {
long shutdownTimeout = determineShutdownTimeout(this.phase);
if (!latch.await(shutdownTimeout, TimeUnit.MILLISECONDS)) {
// Count is still >0 after timeout
if (!countDownBeanNames.isEmpty() && logger.isInfoEnabled()) {
logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() +
" bean" + (countDownBeanNames.size() > 1 ? "s" : "") +
" still running after timeout of " + shutdownTimeout + "ms: " + countDownBeanNames);
}
latch.await(this.timeout, TimeUnit.MILLISECONDS);
if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) {
logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() +
" bean" + (countDownBeanNames.size() > 1 ? "s" : "") +
" still running after timeout of " + this.timeout + "ms: " + countDownBeanNames);
}
}
catch (InterruptedException ex) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -932,8 +932,8 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo
*/
private boolean isExcluded(String beanName) {
return (this.excludedBeans.contains(beanName) ||
(!beanName.isEmpty() && (beanName.charAt(0) == BeanFactory.FACTORY_BEAN_PREFIX_CHAR) &&
this.excludedBeans.contains(beanName.substring(1)))); // length of '&'
(beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX) &&
this.excludedBeans.contains(beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()))));
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -205,9 +205,9 @@ abstract class ScheduledAnnotationReactiveSupport {
final Supplier<ScheduledTaskObservationContext> contextSupplier;
SubscribingRunnable(Publisher<?> publisher, boolean shouldBlock,
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
String displayName, Supplier<ObservationRegistry> observationRegistrySupplier,
Supplier<ScheduledTaskObservationContext> contextSupplier) {
@Nullable String qualifier, List<Runnable> subscriptionTrackerRegistry,
String displayName, Supplier<ObservationRegistry> observationRegistrySupplier,
Supplier<ScheduledTaskObservationContext> contextSupplier) {
this.publisher = publisher;
this.shouldBlock = shouldBlock;
@@ -236,7 +236,7 @@ abstract class ScheduledAnnotationReactiveSupport {
latch.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -87,7 +87,7 @@ public interface MethodValidationResult {
* on their fields and properties.
* @see #getValueResults()
* @see #getBeanResults()
* @deprecated As of Spring Framework 6.2, in favor of {@link #getParameterValidationResults()}
* @deprecated deprecated in favor of {@link #getParameterValidationResults()}
*/
@Deprecated(since = "6.2", forRemoval = true)
default List<ParameterValidationResult> getAllValidationResults() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,9 +19,7 @@ package org.springframework.cache.annotation;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
@@ -42,7 +40,6 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.lang.Nullable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.AssertionsForClassTypes.catchThrowable;
/**
@@ -61,8 +58,8 @@ class ReactiveCachingTests {
LateCacheHitDeterminationWithValueWrapperConfig.class})
void cacheHitDetermination(Class<?> configClass) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
configClass, ReactiveCacheableService.class);
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Object key = new Object();
@@ -122,6 +119,68 @@ class ReactiveCachingTests {
ctx.close();
}
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandler() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Object key = new Object();
Long r1 = service.cacheFuture(key).join();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFuture").isEqualTo(0L);
key = new Object();
r1 = service.cacheMono(key).block();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheMono").isEqualTo(1L);
key = new Object();
r1 = service.cacheFlux(key).blockFirst();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
}
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndMethodError() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Object key = new Object();
StepVerifier.create(service.cacheMono(key))
.expectErrorMessage("mono service error")
.verify();
key = new Object();
StepVerifier.create(service.cacheFlux(key))
.expectErrorMessage("flux service error")
.verify();
}
@Test
void cacheErrorHandlerWithSimpleCacheErrorHandler() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class)
.extracting(Throwable::getCause)
.isInstanceOf(UnsupportedOperationException.class);
Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
}
@ParameterizedTest
@ValueSource(classes = {EarlyCacheHitDeterminationConfig.class,
EarlyCacheHitDeterminationWithoutNullValuesConfig.class,
@@ -129,8 +188,8 @@ class ReactiveCachingTests {
LateCacheHitDeterminationWithValueWrapperConfig.class})
void fluxCacheDoesntDependOnFirstRequest(Class<?> configClass) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
configClass, ReactiveCacheableService.class);
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Object key = new Object();
@@ -148,117 +207,6 @@ class ReactiveCachingTests {
ctx.close();
}
@Test
void cacheErrorHandlerWithSimpleCacheErrorHandler() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
ExceptionCacheManager.class, ReactiveCacheableService.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class)
.extracting(Throwable::getCause)
.isInstanceOf(UnsupportedOperationException.class);
Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
}
@Test
void cacheErrorHandlerWithSimpleCacheErrorHandlerAndSync() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
ExceptionCacheManager.class, ReactiveSyncCacheableService.class);
ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class);
Throwable completableFutureThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join());
assertThat(completableFutureThrowable).isInstanceOf(CompletionException.class)
.extracting(Throwable::getCause)
.isInstanceOf(UnsupportedOperationException.class);
Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block());
assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class);
Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst());
assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class);
}
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandler() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class);
Long r1 = service.cacheFuture(new Object()).join();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFuture").isEqualTo(0L);
r1 = service.cacheMono(new Object()).block();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheMono").isEqualTo(1L);
r1 = service.cacheFlux(new Object()).blockFirst();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
}
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndSync() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveSyncCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveSyncCacheableService service = ctx.getBean(ReactiveSyncCacheableService.class);
Long r1 = service.cacheFuture(new Object()).join();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFuture").isEqualTo(0L);
r1 = service.cacheMono(new Object()).block();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheMono").isEqualTo(1L);
r1 = service.cacheFlux(new Object()).blockFirst();
assertThat(r1).isNotNull();
assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L);
}
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.class);
assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join())
.withMessage(IllegalStateException.class.getName() + ": future service error");
StepVerifier.create(service.cacheMono(new Object()))
.expectErrorMessage("mono service error")
.verify();
StepVerifier.create(service.cacheFlux(new Object()))
.expectErrorMessage("flux service error")
.verify();
}
@Test
void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationExceptionAndSync() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveSyncFailureCacheableService.class, ErrorHandlerCachingConfiguration.class);
ReactiveSyncFailureCacheableService service = ctx.getBean(ReactiveSyncFailureCacheableService.class);
assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join())
.withMessage(IllegalStateException.class.getName() + ": future service error");
StepVerifier.create(service.cacheMono(new Object()))
.expectErrorMessage("mono service error")
.verify();
StepVerifier.create(service.cacheFlux(new Object()))
.expectErrorMessage("flux service error")
.verify();
}
@CacheConfig(cacheNames = "first")
static class ReactiveCacheableService {
@@ -284,94 +232,16 @@ class ReactiveCachingTests {
}
}
@CacheConfig(cacheNames = "first")
static class ReactiveSyncCacheableService {
private final AtomicLong counter = new AtomicLong();
@Cacheable(sync = true)
CompletableFuture<Long> cacheFuture(Object arg) {
return CompletableFuture.completedFuture(this.counter.getAndIncrement());
}
@Cacheable(sync = true)
Mono<Long> cacheMono(Object arg) {
return Mono.defer(() -> Mono.just(this.counter.getAndIncrement()));
}
@Cacheable(sync = true)
Flux<Long> cacheFlux(Object arg) {
return Flux.defer(() -> Flux.just(this.counter.getAndIncrement(), 0L, -1L, -2L, -3L));
}
}
@CacheConfig(cacheNames = "first")
static class ReactiveFailureCacheableService {
private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean();
private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean();
private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean();
@Cacheable
CompletableFuture<Long> cacheFuture(Object arg) {
if (!this.cacheFutureInvoked.compareAndSet(false, true)) {
return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice"));
}
return CompletableFuture.failedFuture(new IllegalStateException("future service error"));
}
static class ReactiveFailureCacheableService extends ReactiveCacheableService {
@Cacheable
Mono<Long> cacheMono(Object arg) {
if (!this.cacheMonoInvoked.compareAndSet(false, true)) {
return Mono.error(new IllegalStateException("mono service invoked twice"));
}
return Mono.error(new IllegalStateException("mono service error"));
}
@Cacheable
Flux<Long> cacheFlux(Object arg) {
if (!this.cacheFluxInvoked.compareAndSet(false, true)) {
return Flux.error(new IllegalStateException("flux service invoked twice"));
}
return Flux.error(new IllegalStateException("flux service error"));
}
}
@CacheConfig(cacheNames = "first")
static class ReactiveSyncFailureCacheableService {
private final AtomicBoolean cacheFutureInvoked = new AtomicBoolean();
private final AtomicBoolean cacheMonoInvoked = new AtomicBoolean();
private final AtomicBoolean cacheFluxInvoked = new AtomicBoolean();
@Cacheable(sync = true)
CompletableFuture<Long> cacheFuture(Object arg) {
if (!this.cacheFutureInvoked.compareAndSet(false, true)) {
return CompletableFuture.failedFuture(new IllegalStateException("future service invoked twice"));
}
return CompletableFuture.failedFuture(new IllegalStateException("future service error"));
}
@Cacheable(sync = true)
Mono<Long> cacheMono(Object arg) {
if (!this.cacheMonoInvoked.compareAndSet(false, true)) {
return Mono.error(new IllegalStateException("mono service invoked twice"));
}
return Mono.error(new IllegalStateException("mono service error"));
}
@Cacheable(sync = true)
Flux<Long> cacheFlux(Object arg) {
if (!this.cacheFluxInvoked.compareAndSet(false, true)) {
return Flux.error(new IllegalStateException("flux service invoked twice"));
}
return Flux.error(new IllegalStateException("flux service error"));
}
}
@@ -453,7 +323,6 @@ class ReactiveCachingTests {
}
}
@Configuration
static class ErrorHandlerCachingConfiguration implements CachingConfigurer {
@@ -464,7 +333,6 @@ class ReactiveCachingTests {
}
}
@Configuration(proxyBeanMethods = false)
@EnableCaching
static class ExceptionCacheManager {
@@ -477,12 +345,11 @@ class ReactiveCachingTests {
return new ConcurrentMapCache(name, isAllowNullValues()) {
@Override
public CompletableFuture<?> retrieve(Object key) {
return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve"));
}
@Override
public <T> CompletableFuture<T> retrieve(Object key, Supplier<CompletableFuture<T>> valueLoader) {
return CompletableFuture.failedFuture(new UnsupportedOperationException("Test exception on retrieve"));
return CompletableFuture.supplyAsync(() -> {
throw new UnsupportedOperationException("Test exception on retrieve");
});
}
@Override
public void put(Object key, @Nullable Object value) {
throw new UnsupportedOperationException("Test exception on put");
@@ -16,26 +16,15 @@
package org.springframework.context.annotation;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Timeout;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanCurrentlyInCreationException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.UnsatisfiedDependencyException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.SpringProperties;
import org.springframework.core.testfixture.EnabledForTestGroups;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND;
import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
@@ -46,7 +35,7 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
class BackgroundBootstrapTests {
@Test
@Timeout(10)
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithUnmanagedThread() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class);
@@ -56,7 +45,7 @@ class BackgroundBootstrapTests {
}
@Test
@Timeout(10)
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithUnmanagedThreads() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class);
@@ -68,95 +57,17 @@ class BackgroundBootstrapTests {
}
@Test
@Timeout(10)
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithStrictLockingFlag() {
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME);
try {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class);
assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1"));
ctx.close();
}
finally {
SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null);
}
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithStrictLockingInferred() throws InterruptedException {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(InferredLockingBeanConfig.class);
ExecutorService threadPool = Executors.newFixedThreadPool(2);
threadPool.submit(() -> ctx.refresh());
Thread.sleep(500);
threadPool.submit(() -> ctx.getBean("testBean2"));
Thread.sleep(1000);
assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1"));
ctx.close();
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithStrictLockingTurnedOff() throws InterruptedException {
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, false);
try {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(InferredLockingBeanConfig.class);
ExecutorService threadPool = Executors.newFixedThreadPool(2);
threadPool.submit(() -> ctx.refresh());
Thread.sleep(500);
threadPool.submit(() -> ctx.getBean("testBean2"));
Thread.sleep(1000);
assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isNull();
ctx.close();
}
finally {
SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null);
}
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithCircularReferenceAgainstMainThread() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceAgainstMainThreadBeanConfig.class);
void bootstrapWithCircularReference() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceBeanConfig.class);
ctx.getBean("testBean1", TestBean.class);
ctx.getBean("testBean2", TestBean.class);
ctx.close();
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithCircularReferenceWithBlockingMainThread() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceWithBlockingMainThreadBeanConfig.class))
.withRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithCircularReferenceInSameThread() {
assertThatExceptionOfType(UnsatisfiedDependencyException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInSameThreadBeanConfig.class))
.withRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithCircularReferenceInMultipleThreads() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new AnnotationConfigApplicationContext(CircularReferenceInMultipleThreadsBeanConfig.class))
.withRootCauseInstanceOf(BeanCurrentlyInCreationException.class);
}
@Test
@Timeout(10)
@Timeout(5)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithCustomExecutor() {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class);
@@ -167,24 +78,6 @@ class BackgroundBootstrapTests {
ctx.close();
}
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
void bootstrapWithCustomExecutorAndStrictLocking() {
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME);
try {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CustomExecutorBeanConfig.class);
ctx.getBean("testBean1", TestBean.class);
ctx.getBean("testBean2", TestBean.class);
ctx.getBean("testBean3", TestBean.class);
ctx.getBean("testBean4", TestBean.class);
ctx.close();
}
finally {
SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null);
}
}
@Configuration(proxyBeanMethods = false)
static class UnmanagedThreadBeanConfig {
@@ -196,7 +89,7 @@ class BackgroundBootstrapTests {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
return new TestBean();
}
@@ -207,7 +100,7 @@ class BackgroundBootstrapTests {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
return new TestBean();
}
@@ -227,7 +120,7 @@ class BackgroundBootstrapTests {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
return new TestBean();
}
@@ -248,7 +141,7 @@ class BackgroundBootstrapTests {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
return new TestBean();
}
@@ -256,7 +149,7 @@ class BackgroundBootstrapTests {
@Configuration(proxyBeanMethods = false)
static class StrictLockingBeanConfig {
static class CircularReferenceBeanConfig {
@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
@@ -265,50 +158,7 @@ class BackgroundBootstrapTests {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean("testBean1");
}
@Bean
public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) {
return new TestBean((TestBean) beanFactory.getSingleton("testBean1"));
}
}
@Configuration(proxyBeanMethods = false)
static class InferredLockingBeanConfig {
@Bean
public TestBean testBean1() {
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean("testBean1");
}
@Bean
public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) {
return new TestBean((TestBean) beanFactory.getSingleton("testBean1"));
}
}
@Configuration(proxyBeanMethods = false)
static class CircularReferenceAgainstMainThreadBeanConfig {
@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
new Thread(testBean2::getObject).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
return new TestBean();
}
@@ -319,128 +169,13 @@ class BackgroundBootstrapTests {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
throw new RuntimeException(ex);
}
return new TestBean();
}
}
@Configuration(proxyBeanMethods = false)
static class CircularReferenceWithBlockingMainThreadBeanConfig {
@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
new Thread(testBean2::getObject).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean(testBean2.getObject());
}
@Bean
public TestBean testBean2(ObjectProvider<TestBean> testBean1) {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean(testBean1.getObject());
}
}
@Configuration(proxyBeanMethods = false)
static class CircularReferenceInSameThreadBeanConfig {
@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean2) {
new Thread(testBean2::getObject).start();
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean();
}
@Bean
public TestBean testBean2(TestBean testBean3) {
try {
Thread.sleep(2000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean();
}
@Bean
public TestBean testBean3(TestBean testBean2) {
return new TestBean();
}
}
@Configuration(proxyBeanMethods = false)
static class CircularReferenceInMultipleThreadsBeanConfig {
@Bean
public TestBean testBean1(ObjectProvider<TestBean> testBean2, ObjectProvider<TestBean> testBean3,
ObjectProvider<TestBean> testBean4) {
new Thread(testBean2::getObject).start();
new Thread(testBean3::getObject).start();
new Thread(testBean4::getObject).start();
try {
Thread.sleep(3000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean();
}
@Bean
public TestBean testBean2(ObjectProvider<TestBean> testBean3) {
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean(testBean3.getObject());
}
@Bean
public TestBean testBean3(ObjectProvider<TestBean> testBean4) {
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean(testBean4.getObject());
}
@Bean
public TestBean testBean4(ObjectProvider<TestBean> testBean2) {
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
return new TestBean(testBean2.getObject());
}
}
@Configuration(proxyBeanMethods = false)
static class CustomExecutorBeanConfig {
@@ -455,13 +190,13 @@ class BackgroundBootstrapTests {
@Bean(bootstrap = BACKGROUND) @DependsOn("testBean3")
public TestBean testBean1(TestBean testBean3) throws InterruptedException {
Thread.sleep(6000);
Thread.sleep(3000);
return new TestBean();
}
@Bean(bootstrap = BACKGROUND) @Lazy
public TestBean testBean2() throws InterruptedException {
Thread.sleep(6000);
Thread.sleep(3000);
return new TestBean();
}
@@ -76,7 +76,7 @@ class ConfigurationClassEnhancerTests {
classLoader = new BasicSmartClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader);
assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
}
@Test
@@ -111,7 +111,7 @@ class ConfigurationClassEnhancerTests {
ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader());
Class<?> enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass);
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent());
assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader);
classLoader = new OverridingClassLoader(getClass().getClassLoader());
enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader);
@@ -129,22 +129,6 @@ class ConfigurationClassPostProcessorTests {
assertThat(beanFactory.getDependentBeans("config")).contains("bar");
}
@Test // gh-34663
void enhancementIsPresentForAbstractConfigClassWithoutBeanMethods() {
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(AbstractConfigWithoutBeanMethods.class));
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
pp.postProcessBeanFactory(beanFactory);
RootBeanDefinition beanDefinition = (RootBeanDefinition) beanFactory.getBeanDefinition("config");
assertThat(beanDefinition.hasBeanClass()).isTrue();
assertThat(beanDefinition.getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR);
Foo foo = beanFactory.getBean("foo", Foo.class);
Bar bar = beanFactory.getBean("bar", Bar.class);
assertThat(bar.foo).isSameAs(foo);
assertThat(beanFactory.getDependentBeans("foo")).contains("bar");
String[] dependentsOfSingletonBeanConfig = beanFactory.getDependentBeans(SingletonBeanConfig.class.getName());
assertThat(dependentsOfSingletonBeanConfig).containsOnly("foo", "bar");
}
@Test
void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() {
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class));
@@ -197,7 +181,7 @@ class ConfigurationClassPostProcessorTests {
assertThat(bar.foo).isNotSameAs(foo);
}
@Test // gh-34486
@Test
void enhancementIsNotPresentWithEmptyConfig() {
beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class));
ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor();
@@ -1211,12 +1195,6 @@ class ConfigurationClassPostProcessorTests {
}
}
@Configuration
@Import(SingletonBeanConfig.class)
abstract static class AbstractConfigWithoutBeanMethods {
// This class intentionally does NOT declare @Bean methods.
}
@Configuration
static final class EmptyConfig {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.springframework.context.support;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Consumer;
@@ -31,7 +30,6 @@ import org.springframework.context.Lifecycle;
import org.springframework.context.LifecycleProcessor;
import org.springframework.context.SmartLifecycle;
import org.springframework.core.testfixture.EnabledForTestGroups;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -56,11 +54,10 @@ class DefaultLifecycleProcessorTests {
@Test
void customLifecycleProcessorInstance() {
StaticApplicationContext context = new StaticApplicationContext();
BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class);
beanDefinition.getPropertyValues().addPropertyValue("timeoutPerShutdownPhase", 1000);
context.registerBeanDefinition(StaticApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME, beanDefinition);
StaticApplicationContext context = new StaticApplicationContext();
context.registerBeanDefinition("lifecycleProcessor", beanDefinition);
context.refresh();
LifecycleProcessor bean = context.getBean("lifecycleProcessor", LifecycleProcessor.class);
Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor");
@@ -73,12 +70,11 @@ class DefaultLifecycleProcessorTests {
@Test
void singleSmartLifecycleAutoStartup() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(true);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean", bean);
assertThat(bean.isRunning()).isFalse();
context.refresh();
assertThat(bean.isRunning()).isTrue();
@@ -118,13 +114,12 @@ class DefaultLifecycleProcessorTests {
@Test
void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(true);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean", bean);
context.registerSingleton("failingBean", FailingLifecycleBean.class);
assertThat(bean.isRunning()).isFalse();
assertThatExceptionOfType(ApplicationContextException.class)
.isThrownBy(context::refresh).withCauseInstanceOf(IllegalStateException.class);
@@ -135,12 +130,11 @@ class DefaultLifecycleProcessorTests {
@Test
void singleSmartLifecycleWithoutAutoStartup() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(false);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean", bean);
assertThat(bean.isRunning()).isFalse();
context.refresh();
assertThat(bean.isRunning()).isFalse();
@@ -154,16 +148,15 @@ class DefaultLifecycleProcessorTests {
@Test
void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(true);
TestSmartLifecycleBean dependency = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
dependency.setAutoStartup(false);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean", bean);
context.getBeanFactory().registerSingleton("dependency", dependency);
context.getBeanFactory().registerDependentBean("dependency", "bean");
assertThat(bean.isRunning()).isFalse();
assertThat(dependency.isRunning()).isFalse();
context.refresh();
@@ -176,42 +169,20 @@ class DefaultLifecycleProcessorTests {
context.close();
}
@Test
void singleSmartLifecycleAutoStartupWithBootstrapExecutor() {
StaticApplicationContext context = new StaticApplicationContext();
BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class);
beanDefinition.getPropertyValues().addPropertyValue("concurrentStartupForPhases", Map.of(1, 1000));
context.registerBeanDefinition(StaticApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME, beanDefinition);
context.registerSingleton(StaticApplicationContext.BOOTSTRAP_EXECUTOR_BEAN_NAME, ThreadPoolTaskExecutor.class);
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
bean.setAutoStartup(true);
context.getBeanFactory().registerSingleton("bean", bean);
assertThat(bean.isRunning()).isFalse();
context.refresh();
assertThat(bean.isRunning()).isTrue();
context.stop();
assertThat(bean.isRunning()).isFalse();
assertThat(startedBeans).hasSize(1);
context.close();
}
@Test
void smartLifecycleGroupStartup() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans);
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forStartupTests(1, startedBeans);
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans);
TestSmartLifecycleBean bean3 = TestSmartLifecycleBean.forStartupTests(3, startedBeans);
TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean3", bean3);
context.getBeanFactory().registerSingleton("beanMin", beanMin);
context.getBeanFactory().registerSingleton("bean2", bean2);
context.getBeanFactory().registerSingleton("beanMax", beanMax);
context.getBeanFactory().registerSingleton("bean1", bean1);
assertThat(beanMin.isRunning()).isFalse();
assertThat(bean1.isRunning()).isFalse();
assertThat(bean2.isRunning()).isFalse();
@@ -231,17 +202,16 @@ class DefaultLifecycleProcessorTests {
@Test
void contextRefreshThenStartWithMixedBeans() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans);
TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans);
TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans);
TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1);
context.getBeanFactory().registerSingleton("smartBean1", smartBean1);
context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2);
context.getBeanFactory().registerSingleton("smartBean2", smartBean2);
assertThat(simpleBean1.isRunning()).isFalse();
assertThat(simpleBean2.isRunning()).isFalse();
assertThat(smartBean1.isRunning()).isFalse();
@@ -263,17 +233,16 @@ class DefaultLifecycleProcessorTests {
@Test
void contextRefreshThenStopAndRestartWithMixedBeans() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans);
TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans);
TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans);
TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1);
context.getBeanFactory().registerSingleton("smartBean1", smartBean1);
context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2);
context.getBeanFactory().registerSingleton("smartBean2", smartBean2);
assertThat(simpleBean1.isRunning()).isFalse();
assertThat(simpleBean2.isRunning()).isFalse();
assertThat(smartBean1.isRunning()).isFalse();
@@ -301,17 +270,16 @@ class DefaultLifecycleProcessorTests {
@Test
void contextRefreshThenStopForRestartWithMixedBeans() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans);
TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans);
TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans);
TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1);
context.getBeanFactory().registerSingleton("smartBean1", smartBean1);
context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2);
context.getBeanFactory().registerSingleton("smartBean2", smartBean2);
assertThat(simpleBean1.isRunning()).isFalse();
assertThat(simpleBean2.isRunning()).isFalse();
assertThat(smartBean1.isRunning()).isFalse();
@@ -351,7 +319,6 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
void smartLifecycleGroupShutdown() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans);
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans);
@@ -360,6 +327,7 @@ class DefaultLifecycleProcessorTests {
TestSmartLifecycleBean bean5 = TestSmartLifecycleBean.forShutdownTests(2, 700, stoppedBeans);
TestSmartLifecycleBean bean6 = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 200, stoppedBeans);
TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(3, 200, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean1", bean1);
context.getBeanFactory().registerSingleton("bean2", bean2);
context.getBeanFactory().registerSingleton("bean3", bean3);
@@ -367,7 +335,6 @@ class DefaultLifecycleProcessorTests {
context.getBeanFactory().registerSingleton("bean5", bean5);
context.getBeanFactory().registerSingleton("bean6", bean6);
context.getBeanFactory().registerSingleton("bean7", bean7);
context.refresh();
context.stop();
assertThat(stoppedBeans).satisfiesExactly(hasPhase(Integer.MAX_VALUE), hasPhase(3),
@@ -378,12 +345,11 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
void singleSmartLifecycleShutdown() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean", bean);
context.refresh();
assertThat(bean.isRunning()).isTrue();
context.stop();
assertThat(bean.isRunning()).isFalse();
@@ -393,11 +359,10 @@ class DefaultLifecycleProcessorTests {
@Test
void singleLifecycleShutdown() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> stoppedBeans = new CopyOnWriteArrayList<>();
Lifecycle bean = new TestLifecycleBean(null, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean", bean);
context.refresh();
assertThat(bean.isRunning()).isFalse();
bean.start();
@@ -410,7 +375,6 @@ class DefaultLifecycleProcessorTests {
@Test
void mixedShutdown() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> stoppedBeans = new CopyOnWriteArrayList<>();
Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans);
Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans);
@@ -419,6 +383,7 @@ class DefaultLifecycleProcessorTests {
Lifecycle bean5 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans);
Lifecycle bean6 = TestSmartLifecycleBean.forShutdownTests(-1, 100, stoppedBeans);
Lifecycle bean7 = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 300, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("bean1", bean1);
context.getBeanFactory().registerSingleton("bean2", bean2);
context.getBeanFactory().registerSingleton("bean3", bean3);
@@ -426,7 +391,6 @@ class DefaultLifecycleProcessorTests {
context.getBeanFactory().registerSingleton("bean5", bean5);
context.getBeanFactory().registerSingleton("bean6", bean6);
context.getBeanFactory().registerSingleton("bean7", bean7);
context.refresh();
assertThat(bean2.isRunning()).isTrue();
assertThat(bean3.isRunning()).isTrue();
@@ -454,18 +418,17 @@ class DefaultLifecycleProcessorTests {
@Test
void dependencyStartedFirstEvenIfItsPhaseIsHigher() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans);
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans);
TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans);
TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("beanMin", beanMin);
context.getBeanFactory().registerSingleton("bean2", bean2);
context.getBeanFactory().registerSingleton("bean99", bean99);
context.getBeanFactory().registerSingleton("beanMax", beanMax);
context.getBeanFactory().registerDependentBean("bean99", "bean2");
context.refresh();
assertThat(beanMin.isRunning()).isTrue();
assertThat(bean2.isRunning()).isTrue();
@@ -483,7 +446,6 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
void dependentShutdownFirstEvenIfItsPhaseIsLower() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans);
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans);
@@ -491,6 +453,7 @@ class DefaultLifecycleProcessorTests {
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans);
TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans);
TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 400, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("beanMin", beanMin);
context.getBeanFactory().registerSingleton("bean1", bean1);
context.getBeanFactory().registerSingleton("bean2", bean2);
@@ -498,7 +461,6 @@ class DefaultLifecycleProcessorTests {
context.getBeanFactory().registerSingleton("bean99", bean99);
context.getBeanFactory().registerSingleton("beanMax", beanMax);
context.getBeanFactory().registerDependentBean("bean99", "bean2");
context.refresh();
assertThat(beanMin.isRunning()).isTrue();
assertThat(bean1.isRunning()).isTrue();
@@ -524,18 +486,17 @@ class DefaultLifecycleProcessorTests {
@Test
void dependencyStartedFirstAndIsSmartLifecycle() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans);
TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans);
TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans);
TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("beanNegative", beanNegative);
context.getBeanFactory().registerSingleton("bean7", bean7);
context.getBeanFactory().registerSingleton("bean99", bean99);
context.getBeanFactory().registerSingleton("simpleBean", simpleBean);
context.getBeanFactory().registerDependentBean("bean7", "simpleBean");
context.refresh();
context.stop();
startedBeans.clear();
@@ -553,7 +514,6 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
void dependentShutdownFirstAndIsSmartLifecycle() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans);
TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans);
@@ -561,6 +521,7 @@ class DefaultLifecycleProcessorTests {
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans);
TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans);
TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("beanMin", beanMin);
context.getBeanFactory().registerSingleton("beanNegative", beanNegative);
context.getBeanFactory().registerSingleton("bean1", bean1);
@@ -568,7 +529,6 @@ class DefaultLifecycleProcessorTests {
context.getBeanFactory().registerSingleton("bean7", bean7);
context.getBeanFactory().registerSingleton("simpleBean", simpleBean);
context.getBeanFactory().registerDependentBean("simpleBean", "beanNegative");
context.refresh();
assertThat(beanMin.isRunning()).isTrue();
assertThat(beanNegative.isRunning()).isTrue();
@@ -591,16 +551,15 @@ class DefaultLifecycleProcessorTests {
@Test
void dependencyStartedFirstButNotSmartLifecycle() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> startedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans);
TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans);
TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("beanMin", beanMin);
context.getBeanFactory().registerSingleton("bean7", bean7);
context.getBeanFactory().registerSingleton("simpleBean", simpleBean);
context.getBeanFactory().registerDependentBean("simpleBean", "beanMin");
context.refresh();
assertThat(beanMin.isRunning()).isTrue();
assertThat(bean7.isRunning()).isTrue();
@@ -613,20 +572,19 @@ class DefaultLifecycleProcessorTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
void dependentShutdownFirstButNotSmartLifecycle() {
StaticApplicationContext context = new StaticApplicationContext();
CopyOnWriteArrayList<Lifecycle> stoppedBeans = new CopyOnWriteArrayList<>();
TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans);
TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans);
TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans);
TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans);
TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans);
StaticApplicationContext context = new StaticApplicationContext();
context.getBeanFactory().registerSingleton("beanMin", beanMin);
context.getBeanFactory().registerSingleton("bean1", bean1);
context.getBeanFactory().registerSingleton("bean2", bean2);
context.getBeanFactory().registerSingleton("bean7", bean7);
context.getBeanFactory().registerSingleton("simpleBean", simpleBean);
context.getBeanFactory().registerDependentBean("bean2", "simpleBean");
context.refresh();
assertThat(beanMin.isRunning()).isTrue();
assertThat(bean1.isRunning()).isTrue();
@@ -653,7 +611,6 @@ class DefaultLifecycleProcessorTests {
};
}
private static class TestLifecycleBean implements Lifecycle {
private final CopyOnWriteArrayList<Lifecycle> startedBeans;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,11 +81,11 @@ public final class ResourceFiles implements Iterable<ResourceFile> {
/**
* Return a new {@link ResourceFiles} instance that merges files from
* another {@link ResourceFiles} instance.
* @param resourceFiles the instance to merge
* @param ResourceFiles the instance to merge
* @return a new {@link ResourceFiles} instance containing merged content
*/
public ResourceFiles and(ResourceFiles resourceFiles) {
return new ResourceFiles(this.files.and(resourceFiles.files));
public ResourceFiles and(ResourceFiles ResourceFiles) {
return new ResourceFiles(this.files.and(ResourceFiles.files));
}
@Override
+6 -6
View File
@@ -25,40 +25,40 @@ configurations {
graalvm
}
tasks.register('javapoetRepackJar', ShadowJar) {
task javapoetRepackJar(type: ShadowJar) {
archiveBaseName = 'spring-javapoet-repack'
archiveVersion = javapoetVersion
configurations = [project.configurations.javapoet]
relocate('com.squareup.javapoet', 'org.springframework.javapoet')
}
tasks.register('javapoetSource', ShadowSource) {
task javapoetSource(type: ShadowSource) {
configurations = [project.configurations.javapoet]
relocate('com.squareup.javapoet', 'org.springframework.javapoet')
outputDirectory = file("build/shadow-source/javapoet")
}
tasks.register('javapoetSourceJar', Jar) {
task javapoetSourceJar(type: Jar) {
archiveBaseName = 'spring-javapoet-repack'
archiveVersion = javapoetVersion
archiveClassifier = 'sources'
from javapoetSource
}
tasks.register('objenesisRepackJar', ShadowJar) {
task objenesisRepackJar(type: ShadowJar) {
archiveBaseName = 'spring-objenesis-repack'
archiveVersion = objenesisVersion
configurations = [project.configurations.objenesis]
relocate('org.objenesis', 'org.springframework.objenesis')
}
tasks.register('objenesisSource', ShadowSource) {
task objenesisSource(type: ShadowSource) {
configurations = [project.configurations.objenesis]
relocate('org.objenesis', 'org.springframework.objenesis')
outputDirectory = file("build/shadow-source/objenesis")
}
tasks.register('objenesisSourceJar', Jar) {
task objenesisSourceJar(type: Jar) {
archiveBaseName = 'spring-objenesis-repack'
archiveVersion = objenesisVersion
archiveClassifier = 'sources'
@@ -527,26 +527,15 @@ public class ReflectUtils {
c = lookup.defineClass(b);
}
catch (LinkageError | IllegalAccessException 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);
throw new CodeGenerationException(ex) {
@Override
public String getMessage() {
return "ClassLoader mismatch for [" + contextClass.getName() +
"]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " +
"for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() +
"; consider co-locating the affected class in that target ClassLoader instead.";
}
catch (ClassNotFoundException cnfe) {
}
}
if (c == null) {
throw new CodeGenerationException(ex) {
@Override
public String getMessage() {
return "ClassLoader mismatch for [" + contextClass.getName() +
"]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " +
"for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() +
"; consider co-locating the affected class in that target ClassLoader instead.";
}
};
}
};
}
catch (Throwable ex) {
throw new CodeGenerationException(ex);
@@ -19,7 +19,6 @@ package org.springframework.core;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Objects;
import kotlin.Unit;
import kotlin.coroutines.CoroutineContext;
@@ -132,7 +131,11 @@ public abstract class CoroutinesUtils {
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> kClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(kClass))) {
arg = box(kClass, arg);
KFunction<?> constructor = KClasses.getPrimaryConstructor(kClass);
if (!KCallablesJvm.isAccessible(constructor)) {
KCallablesJvm.setAccessible(constructor, true);
}
arg = constructor.call(arg);
}
argMap.put(parameter, arg);
}
@@ -158,20 +161,6 @@ public abstract class CoroutinesUtils {
return mono;
}
private static Object box(KClass<?> kClass, @Nullable Object arg) {
KFunction<?> constructor = Objects.requireNonNull(KClasses.getPrimaryConstructor(kClass));
KType type = constructor.getParameters().get(0).getType();
if (!(type.isMarkedNullable() && arg == null) &&
type.getClassifier() instanceof KClass<?> parameterClass &&
KotlinDetector.isInlineClass(JvmClassMappingKt.getJavaClass(parameterClass))) {
arg = box(parameterClass, arg);
}
if (!KCallablesJvm.isAccessible(constructor)) {
KCallablesJvm.setAccessible(constructor, true);
}
return constructor.call(arg);
}
private static Flux<?> asFlux(Object flow) {
return ReactorFlowKt.asFlux(((Flow<?>) flow));
}
@@ -39,7 +39,6 @@ import org.springframework.lang.Nullable;
* @author Juergen Hoeller
* @since 3.2.7
* @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
* @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME
* @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME
@@ -118,18 +117,7 @@ public final class SpringProperties {
* @param key the property key
*/
public static void setFlag(String key) {
localProperties.setProperty(key, Boolean.TRUE.toString());
}
/**
* Programmatically set a local flag to the given value, overriding
* an entry in the {@code spring.properties} file (if any).
* @param key the property key
* @param value the associated boolean value
* @since 6.2.6
*/
public static void setFlag(String key, boolean value) {
localProperties.setProperty(key, Boolean.toString(value));
localProperties.put(key, Boolean.TRUE.toString());
}
/**
@@ -142,19 +130,4 @@ public final class SpringProperties {
return Boolean.parseBoolean(getProperty(key));
}
/**
* Retrieve the flag for the given property key, returning {@code null}
* instead of {@code false} in case of no actual flag set.
* @param key the property key
* @return {@code true} if the property is set to the string "true"
* (ignoring case), {@code} false if it is set to any other value,
* {@code null} if it is not set at all
* @since 6.2.6
*/
@Nullable
public static Boolean checkFlag(String key) {
String flag = getProperty(key);
return (flag != null ? Boolean.valueOf(flag) : null);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -355,7 +355,6 @@ abstract class AnnotationsScanner {
private static boolean isOverride(Method rootMethod, Method candidateMethod) {
return (!Modifier.isPrivate(candidateMethod.getModifiers()) &&
candidateMethod.getParameterCount() == rootMethod.getParameterCount() &&
candidateMethod.getName().equals(rootMethod.getName()) &&
hasSameParameterTypes(rootMethod, candidateMethod));
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -59,7 +59,7 @@ public class DefaultResourceLoader implements ResourceLoader {
/**
* Create a new DefaultResourceLoader.
* <p>ClassLoader access will happen using the thread context class loader
* at the time of actual resource access. For more control, pass
* at the time of actual resource access (since 5.3). For more control, pass
* a specific ClassLoader to {@link #DefaultResourceLoader(ClassLoader)}.
* @see java.lang.Thread#getContextClassLoader()
*/
@@ -80,7 +80,7 @@ public class DefaultResourceLoader implements ResourceLoader {
* Specify the ClassLoader to load class path resources with, or {@code null}
* for using the thread context class loader at the time of actual resource access.
* <p>The default is that ClassLoader access will happen using the thread context
* class loader at the time of actual resource access.
* class loader at the time of actual resource access (since 5.3).
*/
public void setClassLoader(@Nullable ClassLoader classLoader) {
this.classLoader = classLoader;
@@ -215,8 +215,6 @@ import org.springframework.util.StringUtils;
*/
public class PathMatchingResourcePatternResolver implements ResourcePatternResolver {
private static final Resource[] EMPTY_RESOURCE_ARRAY = {};
private static final Log logger = LogFactory.getLog(PathMatchingResourcePatternResolver.class);
/**
@@ -259,8 +257,6 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
private PathMatcher pathMatcher = new AntPathMatcher();
private boolean useCaches = true;
private final Map<String, Resource[]> rootDirCache = new ConcurrentHashMap<>();
private final Map<String, NavigableSet<String>> jarEntriesCache = new ConcurrentHashMap<>();
@@ -335,22 +331,6 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
return this.pathMatcher;
}
/**
* Specify whether this resolver should use jar caches. Default is {@code true}.
* <p>Switch this flag to {@code false} in order to avoid any jar caching, at
* the {@link JarURLConnection} level as well as within this resolver instance.
* <p>Note that {@link JarURLConnection#setDefaultUseCaches} can be turned off
* independently. This resolver-level setting is designed to only enforce
* {@code JarURLConnection#setUseCaches(false)} if necessary but otherwise
* leaves the JVM-level default in place.
* @since 6.1.19
* @see JarURLConnection#setUseCaches
* @see #clearCache()
*/
public void setUseCaches(boolean useCaches) {
this.useCaches = useCaches;
}
@Override
public Resource getResource(String location) {
@@ -374,7 +354,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
// all class path resources with the given name
Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix));
}
return resources.toArray(EMPTY_RESOURCE_ARRAY);
return resources.toArray(new Resource[0]);
}
else {
// Generally only look for a pattern after a prefix here,
@@ -418,7 +398,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (logger.isTraceEnabled()) {
logger.trace("Resolved class path location [" + path + "] to resources " + result);
}
return result.toArray(EMPTY_RESOURCE_ARRAY);
return result.toArray(new Resource[0]);
}
/**
@@ -555,9 +535,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
Set<ClassPathManifestEntry> entries = this.manifestEntriesCache;
if (entries == null) {
entries = getClassPathManifestEntries();
if (this.useCaches) {
this.manifestEntriesCache = entries;
}
this.manifestEntriesCache = entries;
}
for (ClassPathManifestEntry entry : entries) {
if (!result.contains(entry.resource()) &&
@@ -709,9 +687,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (rootDirResources == null) {
// Lookup for specific directory, creating a cache entry for it.
rootDirResources = getResources(rootDirPath);
if (this.useCaches) {
this.rootDirCache.put(rootDirPath, rootDirResources);
}
this.rootDirCache.put(rootDirPath, rootDirResources);
}
}
@@ -743,7 +719,7 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (logger.isTraceEnabled()) {
logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result);
}
return result.toArray(EMPTY_RESOURCE_ARRAY);
return result.toArray(new Resource[0]);
}
/**
@@ -864,9 +840,6 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
if (con instanceof JarURLConnection jarCon) {
// Should usually be the case for traditional JAR files.
if (!this.useCaches) {
jarCon.setUseCaches(false);
}
try {
jarFile = jarCon.getJarFile();
jarFileUrl = jarCon.getJarFileURL().toExternalForm();
@@ -930,10 +903,8 @@ public class PathMatchingResourcePatternResolver implements ResourcePatternResol
}
}
}
if (this.useCaches) {
// Cache jar entries in TreeSet for efficient searching on re-encounter.
this.jarEntriesCache.put(jarFileUrl, entriesCache);
}
// Cache jar entries in TreeSet for efficient searching on re-encounter.
this.jarEntriesCache.put(jarFileUrl, entriesCache);
return result;
}
finally {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -126,12 +126,13 @@ public class SpringFactoriesLoader {
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader
* and a default argument resolver that expects a no-arg constructor.
* <p>The returned factories are sorted using {@link AnnotationAwareOrderComparator}.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>If a custom instantiation strategy is required, use {@code load(...)}
* with a custom {@link ArgumentResolver ArgumentResolver} and/or
* {@link FailureHandler FailureHandler}.
* <p>If duplicate implementation class names are discovered for a given factory
* type, only one instance of the duplicated implementation type will be instantiated.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* @param factoryType the interface or abstract class representing the factory
* @throws IllegalArgumentException if any factory implementation class cannot
* be loaded or if an error occurs while instantiating any factory
@@ -145,9 +146,10 @@ public class SpringFactoriesLoader {
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader
* and the given argument resolver.
* <p>The returned factories are sorted using {@link AnnotationAwareOrderComparator}.
* <p>If duplicate implementation class names are discovered for a given factory
* type, only one instance of the duplicated implementation type will be instantiated.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* @param factoryType the interface or abstract class representing the factory
* @param argumentResolver strategy used to resolve constructor arguments by their type
* @throws IllegalArgumentException if any factory implementation class cannot
@@ -162,9 +164,10 @@ public class SpringFactoriesLoader {
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader
* with custom failure handling provided by the given failure handler.
* <p>The returned factories are sorted using {@link AnnotationAwareOrderComparator}.
* <p>If duplicate implementation class names are discovered for a given factory
* type, only one instance of the duplicated implementation type will be instantiated.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* <p>For any factory implementation class that cannot be loaded or error that
* occurs while instantiating it, the given failure handler is called.
* @param factoryType the interface or abstract class representing the factory
@@ -180,9 +183,10 @@ public class SpringFactoriesLoader {
* {@value #FACTORIES_RESOURCE_LOCATION}, using the configured class loader,
* the given argument resolver, and custom failure handling provided by the given
* failure handler.
* <p>The returned factories are sorted using {@link AnnotationAwareOrderComparator}.
* <p>If duplicate implementation class names are discovered for a given factory
* type, only one instance of the duplicated implementation type will be instantiated.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* <p>For any factory implementation class that cannot be loaded or error that
* occurs while instantiating it, the given failure handler is called.
* @param factoryType the interface or abstract class representing the factory
@@ -233,11 +237,12 @@ public class SpringFactoriesLoader {
/**
* Load and instantiate the factory implementations of the given type from
* {@value #FACTORIES_RESOURCE_LOCATION}, using the given class loader.
* <p>The returned factories are sorted using {@link AnnotationAwareOrderComparator}.
* <p>If duplicate implementation class names are discovered for a given factory
* type, only one instance of the duplicated implementation type will be instantiated.
* <p>The returned factories are sorted through {@link AnnotationAwareOrderComparator}.
* <p>As of Spring Framework 5.3, if duplicate implementation class names are
* discovered for a given factory type, only one instance of the duplicated
* implementation type will be instantiated.
* <p>For more advanced factory loading with {@link ArgumentResolver} or
* {@link FailureHandler} support, use {@link #forDefaultResourceLocation(ClassLoader)}
* {@link FailureHandler} support use {@link #forDefaultResourceLocation(ClassLoader)}
* to obtain a {@link SpringFactoriesLoader} instance.
* @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading (can be {@code null}
@@ -253,8 +258,9 @@ public class SpringFactoriesLoader {
* Load the fully qualified class names of factory implementations of the
* given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
* class loader.
* <p>If a particular implementation class name is discovered more than once
* for the given factory type, duplicates will be ignored.
* <p>As of Spring Framework 5.3, if a particular implementation class name
* is discovered more than once for the given factory type, duplicates will
* be ignored.
* @param factoryType the interface or abstract class representing the factory
* @param classLoader the ClassLoader to use for loading resources; can be
* {@code null} to use the default
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -92,8 +92,6 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
@Nullable
private Set<Thread> activeThreads;
private boolean rejectTasksWhenLimitReached = false;
private volatile boolean active = true;
@@ -192,17 +190,6 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
this.activeThreads = (timeout > 0 ? ConcurrentHashMap.newKeySet() : null);
}
/**
* Specify whether to reject tasks when the concurrency limit has been reached,
* throwing {@link TaskRejectedException} on any further submission attempts.
* <p>The default is {@code false}, blocking the caller until the submission can
* be accepted. Switch this to {@code true} for immediate rejection instead.
* @since 6.2.6
*/
public void setRejectTasksWhenLimitReached(boolean rejectTasksWhenLimitReached) {
this.rejectTasksWhenLimitReached = rejectTasksWhenLimitReached;
}
/**
* Set the maximum number of parallel task executions allowed.
* The default of -1 indicates no concurrency limit at all.
@@ -385,21 +372,13 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator
* making {@code beforeAccess()} and {@code afterAccess()}
* visible to the surrounding class.
*/
private class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport {
private static class ConcurrencyThrottleAdapter extends ConcurrencyThrottleSupport {
@Override
protected void beforeAccess() {
super.beforeAccess();
}
@Override
protected void onLimitReached() {
if (rejectTasksWhenLimitReached) {
throw new TaskRejectedException("Concurrency limit reached: " + getConcurrencyLimit());
}
super.onLimitReached();
}
@Override
protected void afterAccess() {
super.afterAccess();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -69,7 +69,7 @@ public class SpringObjenesis implements Objenesis {
this.strategy = (strategy != null ? strategy : new StdInstantiatorStrategy());
// Evaluate the "spring.objenesis.ignore" property upfront...
if (SpringProperties.getFlag(IGNORE_OBJENESIS_PROPERTY_NAME)) {
if (SpringProperties.getFlag(SpringObjenesis.IGNORE_OBJENESIS_PROPERTY_NAME)) {
this.worthTrying = Boolean.FALSE;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -637,11 +637,10 @@ public abstract class ClassUtils {
Class<?> resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType);
return (lhsType == resolvedPrimitive);
}
else if (rhsType.isPrimitive()) {
else {
Class<?> resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType);
return (resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper));
}
return false;
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -102,7 +102,7 @@ public abstract class CollectionUtils {
* <p>This differs from the regular {@link LinkedHashMap} constructor
* which takes an initial capacity relative to a load factor but is
* aligned with Spring's own {@link LinkedCaseInsensitiveMap} and
* {@link LinkedMultiValueMap} constructor semantics.
* {@link LinkedMultiValueMap} constructor semantics as of 5.3.
* @param expectedSize the expected number of elements (with a corresponding
* capacity to be derived so that no resize/rehash operations are needed)
* @since 5.3
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -105,7 +105,6 @@ public abstract class ConcurrencyThrottleSupport implements Serializable {
/**
* To be invoked before the main execution logic of concrete subclasses.
* <p>This implementation applies the concurrency throttle.
* @see #onLimitReached()
* @see #afterAccess()
*/
protected void beforeAccess() {
@@ -114,12 +113,29 @@ public abstract class ConcurrencyThrottleSupport implements Serializable {
"Currently no invocations allowed - concurrency limit set to NO_CONCURRENCY");
}
if (this.concurrencyLimit > 0) {
boolean debug = logger.isDebugEnabled();
this.concurrencyLock.lock();
try {
if (this.concurrencyCount >= this.concurrencyLimit) {
onLimitReached();
boolean interrupted = false;
while (this.concurrencyCount >= this.concurrencyLimit) {
if (interrupted) {
throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " +
"but concurrency limit still does not allow for entering");
}
if (debug) {
logger.debug("Concurrency count " + this.concurrencyCount +
" has reached limit " + this.concurrencyLimit + " - blocking");
}
try {
this.concurrencyCondition.await();
}
catch (InterruptedException ex) {
// Re-interrupt current thread, to allow other threads to react.
Thread.currentThread().interrupt();
interrupted = true;
}
}
if (logger.isDebugEnabled()) {
if (debug) {
logger.debug("Entering throttle at concurrency count " + this.concurrencyCount);
}
this.concurrencyCount++;
@@ -130,33 +146,6 @@ public abstract class ConcurrencyThrottleSupport implements Serializable {
}
}
/**
* Triggered by {@link #beforeAccess()} when the concurrency limit has been reached.
* The default implementation blocks until the concurrency count allows for entering.
* @since 6.2.6
*/
protected void onLimitReached() {
boolean interrupted = false;
while (this.concurrencyCount >= this.concurrencyLimit) {
if (interrupted) {
throw new IllegalStateException("Thread was interrupted while waiting for invocation access, " +
"but concurrency limit still does not allow for entering");
}
if (logger.isDebugEnabled()) {
logger.debug("Concurrency count " + this.concurrencyCount +
" has reached limit " + this.concurrencyLimit + " - blocking");
}
try {
this.concurrencyCondition.await();
}
catch (InterruptedException ex) {
// Re-interrupt current thread, to allow other threads to react.
Thread.currentThread().interrupt();
interrupted = true;
}
}
}
/**
* To be invoked after the main execution logic of concrete subclasses.
* @see #beforeAccess()
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,7 +35,8 @@ import java.util.Map;
* @param <K> the key type
* @param <V> the value element type
*/
public class LinkedMultiValueMap<K, V> extends MultiValueMapAdapter<K, V> implements Serializable, Cloneable {
public class LinkedMultiValueMap<K, V> extends MultiValueMapAdapter<K, V> // new public base class in 5.3
implements Serializable, Cloneable {
private static final long serialVersionUID = 3801124242820219131L;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -410,10 +410,10 @@ public abstract class ObjectUtils {
}
/**
* Return a hash code for the given object, typically the value of
* {@link Object#hashCode()}. If the object is an array, this method
* will delegate to one of the {@code Arrays.hashCode} methods. If
* the object is {@code null}, this method returns {@code 0}.
* Return a hash code for the given object; typically the value of
* {@code Object#hashCode()}}. If the object is an array,
* this method will delegate to any of the {@code Arrays.hashCode}
* methods. If the object is {@code null}, this method returns 0.
* @see Object#hashCode()
* @see Arrays
*/
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,11 +28,7 @@ import org.junit.platform.suite.api.Suite;
* @author Sam Brannen
*/
@Suite
@SelectPackages({
"org.springframework.aot",
"org.springframework.core",
"org.springframework.util"
})
@SelectPackages({"org.springframework.core", "org.springframework.util"})
@IncludeClassNamePatterns(".*Tests?$")
class SpringCoreTestSuite {
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -132,7 +132,6 @@ class PathMatchingResourcePatternResolverTests {
Path rootDir = Paths.get("src/test/resources/custom%23root").toAbsolutePath();
URL root = new URL("file:" + rootDir + "/");
resolver = new PathMatchingResourcePatternResolver(new DefaultResourceLoader(new URLClassLoader(new URL[] {root})));
resolver.setUseCaches(false);
assertExactFilenames("classpath*:scanned/*.txt", "resource#test1.txt", "resource#test2.txt");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2025 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,7 +21,6 @@ import org.junit.jupiter.api.Test;
import org.springframework.util.ConcurrencyThrottleSupport;
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;
@@ -32,23 +31,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
class SimpleAsyncTaskExecutorTests {
@Test
void isActiveUntilClose() {
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor();
assertThat(executor.isActive()).isTrue();
assertThat(executor.isThrottleActive()).isFalse();
executor.close();
assertThat(executor.isActive()).isFalse();
assertThat(executor.isThrottleActive()).isFalse();
}
@Test
void throwsExceptionWhenSuppliedWithNullRunnable() {
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
assertThatIllegalArgumentException().isThrownBy(() -> executor.execute(null));
}
}
@Test
void cannotExecuteWhenConcurrencyIsSwitchedOff() {
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
@@ -59,34 +41,35 @@ class SimpleAsyncTaskExecutorTests {
}
@Test
void taskRejectedWhenConcurrencyLimitReached() {
void throttleIsNotActiveByDefault() {
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
executor.setConcurrencyLimit(1);
executor.setRejectTasksWhenLimitReached(true);
assertThat(executor.isThrottleActive()).isTrue();
executor.execute(new NoOpRunnable());
assertThatExceptionOfType(TaskRejectedException.class).isThrownBy(() -> executor.execute(new NoOpRunnable()));
assertThat(executor.isThrottleActive()).as("Concurrency throttle must not default to being active (on)").isFalse();
}
}
@Test
void threadNameGetsSetCorrectly() {
String customPrefix = "chankPop#";
Object monitor = new Object();
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix)) {
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).startsWith(customPrefix);
}
final String customPrefix = "chankPop#";
final Object monitor = new Object();
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(customPrefix);
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).startsWith(customPrefix);
}
@Test
void threadFactoryOverridesDefaults() {
Object monitor = new Object();
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(runnable -> new Thread(runnable, "test"))) {
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).isEqualTo("test");
final Object monitor = new Object();
SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(runnable -> new Thread(runnable, "test"));
ThreadNameHarvester task = new ThreadNameHarvester(monitor);
executeAndWait(executor, task, monitor);
assertThat(task.getThreadName()).isEqualTo("test");
}
@Test
void throwsExceptionWhenSuppliedWithNullRunnable() {
try (SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor()) {
assertThatIllegalArgumentException().isThrownBy(() -> executor.execute(null));
}
}
@@ -106,12 +89,7 @@ class SimpleAsyncTaskExecutorTests {
@Override
public void run() {
try {
Thread.sleep(1000);
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
// no-op
}
}

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