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 41e501d5f77..8aa4231646f 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 @@ -77,6 +77,7 @@ 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; @@ -128,6 +129,17 @@ import org.springframework.util.StringUtils; public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { + /** + * System property that instructs Spring to enforce string 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. + * @since 6.2.6 + * @see #preInstantiateSingletons() + */ + public static final String STRICT_LOCKING_PROPERTY_NAME = "spring.locking.strict"; + + private static final boolean lenientLockingAllowed = !SpringProperties.getFlag(STRICT_LOCKING_PROPERTY_NAME); + private static @Nullable Class jakartaInjectProviderClass; static { @@ -1015,7 +1027,8 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto @Override protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (this.preInstantiationPhase ? this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); + return (lenientLockingAllowed && this.preInstantiationPhase ? + this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); } @Override 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 dda782ce897..913cc863d04 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 @@ -20,11 +20,15 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.springframework.beans.factory.ObjectProvider; +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.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -56,6 +60,21 @@ class BackgroundBootstrapTests { ctx.close(); } + @Test + @Timeout(5) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingThread() { + 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(5) @EnabledForTestGroups(LONG_RUNNING) @@ -148,6 +167,28 @@ class BackgroundBootstrapTests { } + @Configuration(proxyBeanMethods = false) + static class StrictLockingBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + throw new RuntimeException(ex); + } + return new TestBean(); + } + + @Bean + public TestBean testBean2(ConfigurableListableBeanFactory beanFactory) { + return new TestBean((TestBean) beanFactory.getSingleton("testBean1")); + } + } + + @Configuration(proxyBeanMethods = false) static class CircularReferenceBeanConfig { 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 70e64606f87..75991b0dbf9 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -39,6 +39,7 @@ import org.jspecify.annotations.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 diff --git a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java index 755ddb7232c..11b3aa004ca 100644 --- a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java +++ b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -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(SpringObjenesis.IGNORE_OBJENESIS_PROPERTY_NAME)) { + if (SpringProperties.getFlag(IGNORE_OBJENESIS_PROPERTY_NAME)) { this.worthTrying = Boolean.FALSE; } } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java index 6ba62252e7a..0236120c7da 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -56,6 +56,8 @@ public abstract class AbstractJmsListenerContainerFactoryAs of 6.2, the default is {@code true}: The listener container will + * acknowledge each JMS Message even in case of a vendor-specific mode, + * assuming client-acknowledge style processing for custom vendor modes. + *

If the provided listener prefers to manually acknowledge each message in + * the listener itself, in combination with an "individual acknowledge" mode, + * switch this flag to {code false} along with the vendor-specific mode. + * @since 6.2.6 + * @see #setSessionAcknowledgeMode + * @see #setMessageListener + * @see Message#acknowledge() + */ + public void setAcknowledgeAfterListener(boolean acknowledgeAfterListener) { + this.acknowledgeAfterListener = acknowledgeAfterListener; + } + + /** + * Determine whether the listener container should automatically acknowledge + * each JMS Message after the message listener returned. + * @since 6.2.6 + * @see #setAcknowledgeAfterListener + * @see #isClientAcknowledge(Session) + */ + public boolean isAcknowledgeAfterListener() { + return this.acknowledgeAfterListener; + } + /** * Set whether to expose the listener JMS Session to a registered * {@link SessionAwareMessageListener} as well as to @@ -812,7 +841,7 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen JmsUtils.commitIfNecessary(session); } } - else if (message != null && isClientAcknowledge(session)) { + else if (message != null && isAcknowledgeAfterListener() && isClientAcknowledge(session)) { message.acknowledge(); } }