Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-configure a bootstrapExecutor bean to be used by Framework's background bean initialization #32551

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 ...
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}


// -------------------------------------------------------------------------

Expand Down Expand Up @@ -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();
}
}

}