diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java index 0ad5748296d..df7caafc167 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultListableBeanFactory.java @@ -133,6 +133,11 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto * 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. + *
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()
*/
@@ -156,8 +161,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto
private static final Map 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)
+ * 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)
* @since 6.2
*/
protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() {
diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java
index ce9f059ce17..1f6b9f2414d 100644
--- a/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java
+++ b/spring-beans/src/test/java/org/springframework/beans/factory/DefaultListableBeanFactoryTests.java
@@ -77,7 +77,6 @@ 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;
@@ -1419,7 +1418,6 @@ 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))
@@ -1490,7 +1488,6 @@ 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");
@@ -1508,7 +1505,6 @@ 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");
@@ -1526,7 +1522,6 @@ 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");
diff --git a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java
index a0731445314..75f446f6ad3 100644
--- a/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java
+++ b/spring-context/src/test/java/org/springframework/context/annotation/BackgroundBootstrapTests.java
@@ -16,6 +16,9 @@
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;
@@ -67,7 +70,7 @@ class BackgroundBootstrapTests {
@Test
@Timeout(10)
@EnabledForTestGroups(LONG_RUNNING)
- void bootstrapWithStrictLockingThread() {
+ void bootstrapWithStrictLockingFlag() {
SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME);
try {
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class);
@@ -79,6 +82,42 @@ class BackgroundBootstrapTests {
}
}
+ @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)
@@ -128,6 +167,24 @@ 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 {
@@ -220,6 +277,27 @@ class BackgroundBootstrapTests {
}
+ @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 {
@@ -377,13 +455,13 @@ class BackgroundBootstrapTests {
@Bean(bootstrap = BACKGROUND) @DependsOn("testBean3")
public TestBean testBean1(TestBean testBean3) throws InterruptedException {
- Thread.sleep(3000);
+ Thread.sleep(6000);
return new TestBean();
}
@Bean(bootstrap = BACKGROUND) @Lazy
public TestBean testBean2() throws InterruptedException {
- Thread.sleep(3000);
+ Thread.sleep(6000);
return new TestBean();
}
diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java
index 102f333c074..fd4077b78b1 100644
--- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java
+++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java
@@ -527,15 +527,26 @@ public class ReflectUtils {
c = lookup.defineClass(b);
}
catch (LinkageError | IllegalAccessException ex) {
- 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.";
+ if (ex instanceof LinkageError) {
+ // Could be a ClassLoader mismatch with the class pre-existing in a
+ // parent ClassLoader -> try loadClass before giving up completely.
+ try {
+ c = contextClass.getClassLoader().loadClass(className);
}
- };
+ catch (ClassNotFoundException cnfe) {
+ }
+ }
+ if (c == null) {
+ 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);
diff --git a/spring-core/src/main/java/org/springframework/core/SpringProperties.java b/spring-core/src/main/java/org/springframework/core/SpringProperties.java
index 75991b0dbf9..7e25dd30e02 100644
--- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java
+++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java
@@ -117,7 +117,18 @@ public final class SpringProperties {
* @param key the property key
*/
public static void setFlag(String key) {
- localProperties.put(key, Boolean.TRUE.toString());
+ 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));
}
/**
@@ -130,4 +141,19 @@ 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);
+ }
+
}
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java
index 0f3fb0f2a5a..bd559b6e933 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCreatorUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2024 the original author or authors.
+ * Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -85,7 +85,7 @@ public abstract class StatementCreatorUtils {
private static final Map