diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BootstrapExecutor.java b/spring-context/src/main/java/org/springframework/context/annotation/BootstrapExecutor.java new file mode 100644 index 000000000000..9583f373132b --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/annotation/BootstrapExecutor.java @@ -0,0 +1,32 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface BootstrapExecutor { + String threadNamePrefix() default "bootstrap-"; + int corePoolSize() default 1; + // Set more properties here ... +} diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index b1a4408c6fb5..90301e4b8cdf 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -70,6 +70,7 @@ import org.springframework.beans.factory.parsing.ProblemReporter; import org.springframework.beans.factory.parsing.SourceExtractor; import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.BeanNameGenerator; @@ -82,6 +83,7 @@ import org.springframework.context.annotation.ConfigurationClassEnhancer.EnhancedConfiguration; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; @@ -103,6 +105,7 @@ import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.lang.Nullable; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -379,6 +382,48 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. return; } + // Got a problem parsing @BootstrapExecutor in BootstrapExecutorBeanDefinitionParser + // So I temporarily copied the logic here. + // Detect the @BootstrapExecutor annotation + BootstrapExecutor bootstrapExecutorAnnotation = null; + for (BeanDefinitionHolder holder : configCandidates) { + BeanDefinition beanDef = holder.getBeanDefinition(); + if (beanDef instanceof AnnotatedBeanDefinition annotatedBeanDefinition) { + AnnotationMetadata metadata = annotatedBeanDefinition.getMetadata(); + String className = metadata.getClassName(); + try { + Class configClass = ClassUtils.forName(className, this.beanClassLoader); + bootstrapExecutorAnnotation = AnnotationUtils.findAnnotation(configClass, BootstrapExecutor.class); + if (bootstrapExecutorAnnotation != null) { + break; + } + } + catch (ClassNotFoundException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Could not load class: " + className, ex); + } + } + } + } + + // Got a problem parsing @BootstrapExecutor in BootstrapExecutorBeanDefinitionParser + // So I temporarily copied the logic here. + // Register the ThreadPoolTaskExecutor bean if @BootstrapExecutor is found + if (bootstrapExecutorAnnotation != null) { + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ThreadPoolTaskExecutor.class); + builder.addPropertyValue("threadNamePrefix", bootstrapExecutorAnnotation.threadNamePrefix()); + builder.addPropertyValue("corePoolSize", bootstrapExecutorAnnotation.corePoolSize()); + // Set other properties here... + builder.addPropertyValue("daemon", true); + + // Register as a singleton bean + registry.registerBeanDefinition("bootstrapExecutor", builder.getBeanDefinition()); + } + // We could add another check, if bootstrapExecutorAnnotation is not found, + // and we have @Bean(bootstrap=BACKGROUND) annotations found, + // we can automatically configure a bootstrapExecutor based on the number of + // @Bean(bootstrap=BACKGROUND) annotations. + // Sort by previously determined @Order value, if applicable configCandidates.sort((bd1, bd2) -> { int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition()); diff --git a/spring-context/src/main/java/org/springframework/context/config/BootstrapExecutorBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/BootstrapExecutorBeanDefinitionParser.java new file mode 100644 index 000000000000..2f9ed6a1342c --- /dev/null +++ b/spring-context/src/main/java/org/springframework/context/config/BootstrapExecutorBeanDefinitionParser.java @@ -0,0 +1,48 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.context.config; + +import org.w3c.dom.Element; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +public class BootstrapExecutorBeanDefinitionParser implements BeanDefinitionParser { + + @Override + public BeanDefinition parse(Element element, ParserContext parserContext) { +// It's not getting executed, I'm looking into the problem +// This class's task is done by the processConfigBeanDefinitions method +// in ConfigurationClassPostProcessor right now, but I plan to move the logic back to this +// class once the problem is solved. + BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ThreadPoolTaskExecutor.class); + + builder.addPropertyValue("threadNamePrefix", element.getAttribute("thread-name-prefix")); + builder.addPropertyValue("corePoolSize", element.getAttribute("core-pool-size")); + // Set more properties here ... + builder.addPropertyValue("daemon", true); + + // Register bean + String beanName = "bootstrapExecutor"; + parserContext.getRegistry().registerBeanDefinition(beanName, builder.getBeanDefinition()); + return null; + } + +} diff --git a/spring-context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java b/spring-context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java index e68c8a521c70..e55dfaf05128 100644 --- a/spring-context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java +++ b/spring-context/src/main/java/org/springframework/context/config/ContextNamespaceHandler.java @@ -40,6 +40,7 @@ public void init() { registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); registerBeanDefinitionParser("mbean-export", new MBeanExportBeanDefinitionParser()); registerBeanDefinitionParser("mbean-server", new MBeanServerBeanDefinitionParser()); + registerBeanDefinitionParser("bootstrap-executor", new BootstrapExecutorBeanDefinitionParser()); } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java index c205ac781ab9..fb31efd2df16 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -65,12 +65,14 @@ import org.springframework.core.io.DescriptiveResource; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.SyncTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; +import static org.springframework.context.annotation.Bean.Bootstrap.BACKGROUND; /** * @author Chris Beams @@ -1129,6 +1131,66 @@ void testBeanDefinitionRegistryPostProcessorConfig() { ctx.close(); } + @Test + void testParallelBeanInitialization() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(TestAppConfig.class); + + long startTime = System.currentTimeMillis(); + context.refresh(); + long endTime = System.currentTimeMillis(); + + assertThat(context.getBean("bootstrapExecutor")).isInstanceOf(ThreadPoolTaskExecutor.class); + assertThat(context.getBean("slowInitBean")).isNotNull(); + assertThat(context.getBean("fastInitBean")).isNotNull(); + + // Total init time should be under 2000ms because two beans are initialized in parallel + assertThat(endTime - startTime).isLessThan(4000L); + } + + + @Test + void testParallelBeanInitializationWithMultipleBackgroundBeans() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(TestAppConfigWithMultipleBackgroundBeans.class); + + long startTime = System.currentTimeMillis(); + context.refresh(); + long endTime = System.currentTimeMillis(); + + ThreadPoolTaskExecutor executor = context.getBean("bootstrapExecutor", ThreadPoolTaskExecutor.class); + assertThat(executor.getCorePoolSize()).isEqualTo(2); + assertThat(executor.getThreadNamePrefix()).isEqualTo("test-bootstrap-"); + + assertThat(context.getBean("backgroundInitBean1")).isNotNull(); + assertThat(context.getBean("backgroundInitBean2")).isNotNull(); + assertThat(context.getBean("foregroundInitBean")).isNotNull(); + + // Total init time should be around 2000ms because two background beans are initialized in parallel + assertThat(endTime - startTime).isLessThan(4000L); + } + + @Test + void testParallelBeanInitializationWithLimitedThreadPool() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(TestAppConfigWithLimitedThreadPool.class); + + long startTime = System.currentTimeMillis(); + context.refresh(); + long endTime = System.currentTimeMillis(); + + ThreadPoolTaskExecutor executor = context.getBean("bootstrapExecutor", ThreadPoolTaskExecutor.class); + assertThat(executor.getCorePoolSize()).isEqualTo(1); + assertThat(executor.getThreadNamePrefix()).isEqualTo("test-bootstrap-"); + + assertThat(context.getBean("backgroundInitBean1")).isNotNull(); + assertThat(context.getBean("backgroundInitBean2")).isNotNull(); + assertThat(context.getBean("foregroundInitBean")).isNotNull(); + + // Total init time should be around 4000ms because the two background beans are initialized sequentially + assertThat(endTime - startTime).isGreaterThanOrEqualTo(4000L); + } + // ------------------------------------------------------------------------- @@ -2067,4 +2129,78 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) } } + static class BackgroundInitBean { + public BackgroundInitBean() { + try { + Thread.sleep(2000); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + static class ForegroundInitBean { + public ForegroundInitBean() { + try { + Thread.sleep(2000); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + @BootstrapExecutor + @Configuration + static class TestAppConfig { + @Bean(bootstrap = BACKGROUND) + public BackgroundInitBean slowInitBean() { + return new BackgroundInitBean(); + } + + @Bean + public ForegroundInitBean fastInitBean() { + return new ForegroundInitBean(); + } + } + + @BootstrapExecutor(threadNamePrefix = "test-bootstrap-", corePoolSize = 2) + @Configuration + static class TestAppConfigWithMultipleBackgroundBeans { + @Bean(bootstrap = BACKGROUND) + public BackgroundInitBean backgroundInitBean1() { + return new BackgroundInitBean(); + } + + @Bean(bootstrap = BACKGROUND) + public BackgroundInitBean backgroundInitBean2() { + return new BackgroundInitBean(); + } + + @Bean + public ForegroundInitBean foregroundInitBean() { + return new ForegroundInitBean(); + } + } + + @BootstrapExecutor(threadNamePrefix = "test-bootstrap-", corePoolSize = 1) + @Configuration + static class TestAppConfigWithLimitedThreadPool { + @Bean(bootstrap = BACKGROUND) + public BackgroundInitBean backgroundInitBean1() { + return new BackgroundInitBean(); + } + + @Bean(bootstrap = BACKGROUND) + public BackgroundInitBean backgroundInitBean2() { + return new BackgroundInitBean(); + } + + @Bean + public ForegroundInitBean foregroundInitBean() { + return new ForegroundInitBean(); + } + } + }