Home > database >  SpringBoot @ConfigurationCondition is evaluated before the Spring Beans have been instantiated
SpringBoot @ConfigurationCondition is evaluated before the Spring Beans have been instantiated

Time:08-20

I'm trying to use the @ConfigurationCondition annotation to register a bean based on a dynamic condition. This dynamic condition is present in another bean.

Putting the code below to better describe this :

Below the class ImplementationA.java which I want to instantiate only if a certain condition is true. I've used the @Conditional annotation here on this class to achieve this.

@Component
@Conditional(MyConfigurationCondition.class)
public class ImplementationA{
    @Override
    public void printIt() {
        System.out.println("Inside ImplementationA");
    }
}

Below is the MyConfigurationCondition.java which has been passed to the @Conditional annotation in the above ImplementationA.java.

public class MyConfigurationCondition implements ConfigurationCondition {

    @Override
    public ConfigurationPhase getConfigurationPhase() {
        return ConfigurationPhase.REGISTER_BEAN;
    }

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        try {

            MyConfig myConfig = (MyConfig)context.getBeanFactory().getBean(MyConfig.class);
            if(myConfig.getCustomFlag()){
                return true;
            }
            return false;
    }
}

Notice in the above code, we are returning true using a function(getCustomFlag()) of the bean MyConfig.class. And we are trying to get this bean from the applicationContext(this is the reason I've returned ConfigurationPhase.REGISTER_BEAN from the overridden getConfigurationPhase method)

Below the code for the MyConfig.class

@Component
public class MyConfig {

    private final Component1 component1;
    private final Component2 component2;

    @Autowired
    public MyConfig(Component1 component1, Component2 component2) {
        System.out.println("Inside MyConfig(Component1,Component2)");
        this.component1 = component1;
        this.component2 = component2;
    }

    public getCustomFlag(){
        return component1.flag() && component2.flag();
    }
}

But when I run the Spring Boot application, I get this exception:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'myConfig' defined in file [com.config.MyConfig.class]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.config.MyConfig]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.config.MyConfig.<init>()
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1334)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1232)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:233)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1282)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1243)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:494)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:349)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:342)
    at com.service.condition.MyConfigurationCondition.matches(MyConfigurationCondition.java:37)
    at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:108)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader$TrackedConditionEvaluator.shouldSkip(ConfigurationClassBeanDefinitionReader.java:489)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:140)
    at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:129)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:343)
    at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:247)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:311)
    at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:112)
    at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:746)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:564)
    at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:66)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:734)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:408)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:308)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1295)
    at com.sagnik.ReactiveWebApplication.ReactiveWebApplication.main(ReactiveWebApplication.java:16)
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.config.MyConfig]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.config.MyConfig.<init>()
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:83)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1326)
    ... 30 more
Caused by: java.lang.NoSuchMethodException: com.config.MyConfig.<init>()
    at java.base/java.lang.Class.getConstructor0(Class.java:3585)
    at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2754)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:78)
    ... 31 more

I can understand from the exception that inside the matches method of MyConfigurationCondition.java, when spring encounters the line MyConfig myConfig = (MyConfig)context.getBeanFactory().getBean(MyConfig.class);, it tries to instantiate MyConfig.java with the default constructor which is ofcourse not available in the class definition. My question is why is Spring trying to instantiate it manually using the default constructor when this bean should be available by default(instantiate using Autowired constructor) once the component scanning is completed.

Any help would be hugely appreciated.

CodePudding user response:

To me it looks like an issue in the design actually

Spring's @Conditional-s are meant to be evaluated during the spring startup. When spring builds an application context - it decides which beans to load. So it uses various conditions to see whether the bean should actually be loaded into the application context. But in general, beans do not exist when this process happens.

However, you're trying to create a conditional that assumes that spring beans are already there. So even if you'll make it work somehow I think you should slightly redesign the application logic to make it work in a spring-ish way.

To provide the actual refactoring guidelines I need to understand the logic more:

  1. These Component1 and Component2 seem to evaluate some kind of feature flag but they're also regular spring beans. So I understand that when you call component1.flag() && component2.flag() in general you can get different results in different times. Otherwise it's just a static configuration or something. In other words, the resolution of the feature flag value is a part of your business logic.

But if so, maybe your Implementation class should not have a Conditional on it and be loaded in any case to the application context:


@Component
@Conditional(MyConfigurationCondition.class)
public class ImplementationA{
    MyConfig config;
    @Override
    public void printIt() {
        if(config.getCustomFlag()) {
           System.out.println("Inside ImplementationA");
         }
    }
}

Alternatively if its a configuration, you can access it from @Conditional-s but in this case they should not be spring beans.

CodePudding user response:

Because AutowiredAnnotationBeanPostProcessor has not been registered, the constructor of MyConfig cannot be determined. In the end, simply use the no-argument constructor.

  • Related