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> serializableFactories = new ConcurrentHashMap<>(8); - /** Whether lenient locking is allowed in this factory. */ - private final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); + /** Whether strict locking is enforced or relaxed in this factory. */ + private @Nullable final Boolean strictLocking = SpringProperties.checkFlag(STRICT_LOCKING_PROPERTY_NAME); /** Optional id for this factory, for serialization purposes. */ private @Nullable String serializationId; @@ -208,6 +213,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private volatile boolean preInstantiationPhase; + private @Nullable volatile String mainThreadPrefix; + private final NamedThreadLocal preInstantiationThread = new NamedThreadLocal<>("Pre-instantiation thread marker"); @@ -1030,7 +1037,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 " + @@ -1041,8 +1048,28 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Override protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (this.lenientLockingAllowed && this.preInstantiationPhase ? - this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); + if (this.preInstantiationPhase) { + // We only differentiate in the preInstantiateSingletons phase. + PreInstantiation preInstantiation = this.preInstantiationThread.get(); + if (preInstantiation != null) { + // A Spring-managed 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; + }; + } + if (Boolean.FALSE.equals(this.strictLocking) || + (this.strictLocking == null && !getThreadNamePrefix().equals(this.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; } @Override @@ -1060,6 +1087,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto this.preInstantiationPhase = true; this.preInstantiationThread.set(PreInstantiation.MAIN); + this.mainThreadPrefix = getThreadNamePrefix(); try { for (String beanName : beanNames) { RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); @@ -1072,6 +1100,7 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto } } finally { + this.mainThreadPrefix = null; this.preInstantiationThread.remove(); this.preInstantiationPhase = false; } @@ -1166,6 +1195,12 @@ 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 diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java index 7a23ac75190..69818786e6d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DefaultSingletonBeanRegistry.java @@ -270,7 +270,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("Creating singleton bean '" + beanName + "' in thread \"" + + logger.info("Obtaining singleton bean '" + beanName + "' in thread \"" + Thread.currentThread().getName() + "\" while other thread holds " + "singleton lock for other beans " + this.singletonsCurrentlyInCreation); } @@ -441,12 +441,16 @@ public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements /** * Determine whether the current thread is allowed to hold the singleton lock. - *

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, Integer> javaTypeToSqlTypeMap = new HashMap<>(64); - static @Nullable Boolean shouldIgnoreGetParameterType; + static @Nullable Boolean shouldIgnoreGetParameterType = SpringProperties.checkFlag(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); static { javaTypeToSqlTypeMap.put(boolean.class, Types.BOOLEAN); @@ -114,11 +114,6 @@ public abstract class StatementCreatorUtils { javaTypeToSqlTypeMap.put(java.sql.Timestamp.class, Types.TIMESTAMP); javaTypeToSqlTypeMap.put(Blob.class, Types.BLOB); javaTypeToSqlTypeMap.put(Clob.class, Types.CLOB); - - String flag = SpringProperties.getProperty(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); - if (flag != null) { - shouldIgnoreGetParameterType = Boolean.valueOf(flag); - } }