Skip to content

Thread-scoped bean creation freezes if dependent bean is retrieved before dependency bean  #25801

@nguyenquoc

Description

@nguyenquoc

Affects: 5.2.9


It seems that if a dependent thread-scoped bean is retrieved from the ApplicationContext before the thread-scoped dependency, then bean creation could lock up in an infinite loop, never to return again.

Regression bug. Works fine with spring-framework 5.2.7, fails with spring-framework 5.2.9. May be a bug in spring-test rather than spring-context. May be hard to reproduce, hoping that this limited information is sufficient to lead to a fix.

SimpleThreadScope likely relevant since SimpleThreadScope has an infinite loop - the bean creation freezes in:

// org.springframework.context.support.SimpleThreadScope.class, the scope map is empty
public Object get(String name, ObjectFactory<?> objectFactory) {
    
  Map<String, Object> scope = (Map)this.threadScope.get();
    
    return scope.computeIfAbsent(name, (k) -> {
        
  return objectFactory.getObject();
    
});


Standalone test case:

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.Scope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.context.support.SimpleThreadScope;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListeners;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.support.AbstractTestExecutionListener;
import org.springframework.test.context.support.AnnotationConfigContextLoader;
import org.springframework.test.context.support.DependencyInjectionTestExecutionListener;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes={MyTest.MyTestConfig.class},
        loader= AnnotationConfigContextLoader.class)
@TestExecutionListeners({MyTest.MyTestExecutionListener.class,
        DependencyInjectionTestExecutionListener.class})
public class MyTest {
    @Autowired
    private MyView removeNodeStatusScreen;

    @Test
    void justRun() {
        System.out.println("This test should run");
    }

    @Configuration
    static class MyTestConfig {
        @Bean
        @org.springframework.context.annotation.Scope("thread")
        // The bean name seems to matter, if I change the name the test passes
        public MyView removeNodeStatusScreen(
                MyPresenter myPresenter) {
            return new MyView(myPresenter);
        }

        @Bean
        @org.springframework.context.annotation.Scope("thread")
        // The bean name seems to matter, if I change the name the test passes
        public MyPresenter removeNodeStatusPresenter() {
            return new MyPresenter();
        }
    }

    static class MyTestExecutionListener
            extends AbstractTestExecutionListener {

        @Override
        public void prepareTestInstance(TestContext testContext) throws Exception {
            if (testContext.getApplicationContext() instanceof GenericApplicationContext) {
                GenericApplicationContext context =
                        (GenericApplicationContext) testContext.getApplicationContext();
                ConfigurableListableBeanFactory beanFactory = context
                        .getBeanFactory();
                Scope viewScope = new SimpleThreadScope();
                beanFactory.registerScope("thread", viewScope);
            }
        }
    }

    public static class MyPresenter {
        public MyPresenter() {
        }
    }

    public static class MyView {
        public MyView(MyPresenter myPresenter) {
        }
    }
}

2 possible workarounds found:

Workaround 1: Add @DependsOn annotation to configuration class. However, @DependsOn would not be necessary if there were no bug:

@Configuration
public class MyConfig {
  @ViewScope
  @Bean 
  @DependsOn("myPresenter")
  public MyView myView(MyPresenter myPresenter) {
    return new MyView(myPresenter);
  }

  @Bean
  @ViewScope
  public MyPresenter myPresenter() {
     return new myPresenter();
  }
}

Workaround 2: swap order of beans in test. The order of the beans would not matter if there were no bug.

class MyConfigTest {
  // myView depends on myPresenter, so make sure the myPresenter is declared first
  // Otherwise test freezes on bean creation
  @Autowired
  MyPresenter myPresenter;

  @Autowired
  MyView myView;
...

Metadata

Metadata

Assignees

Labels

in: coreIssues in core modules (aop, beans, core, context, expression)type: regressionA bug that is also a regression

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions