spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.43k stars 38.08k forks source link

Parent Conditionals are ignored on component scanning #29372

Closed levitin closed 9 months ago

levitin commented 2 years ago

Imagine you have the following configuration and the associated properties:

MyConfiguration.java ```java @Configuration @EnableConfigurationProperties(MyProperties.class) public class MyConfiguration { @Configuration @ConditionalOnProperty(value = "my.enabled", havingValue = "true") static class MyEnabledConfiguration { @Configuration @ConditionalOnProperty(value = "my.version", havingValue = "v1") static class MyEnabledFirstConfiguration { @Bean public MyClient firstClient() { return new FirstClient(); } } @Configuration @ConditionalOnProperty(value = "my.version", havingValue = "v2", matchIfMissing = true) static class MyEnabledSecondConfiguration { @Bean public MyClient secondClient() { return new SecondClient(); } } } @Configuration @ConditionalOnProperty(value = "my.enabled", havingValue = "false", matchIfMissing = true) static class MyDisabledConfiguration { @Bean public MyClient thirdClient() { return new ThirdClient(); } } ```
MyProperties.java ```java @Getter @Setter @Validated @ConfigurationProperties(prefix = "my") public class MyProperties { private boolean enabled; private Version version = Version.V1; public enum Version { V1, V2 } } ```

In following case I expect the only single bean of type ThirdClient

my:
  enabled: false
  version: v1

And it actually works fine, if you write your test as following:

@SpringBootTest(classes = MyConfiguration.class)
class MySpringBootTest {

    @Autowired
    private ApplicationContext context;

    @Test
    void contextLoads() {
        assertThat(context.getBeansOfType(MyClient.class).size()).isEqualTo(1);
        assertThat(context.getBean(MyClient.class)).isInstanceOf(ThirdClient.class);
    }
}

However if you are using component scan, the parent conditional is ignored.

+ @SpringBootTest
- @SpringBootTest(classes = MyConfiguration.class)
class MySpringBootTest {
    ...
}

In this case you get the following two beans instead of expected single one as in example above.

raddatzk commented 2 years ago

Seems like @ComponentScan also accepts nested @Configuration classes. Maybe doing an additional check if the candidate has a parent conditional @Configuration before parsing the candidate?

wilkinsona commented 1 year ago

The behavior of condition evaluation during component scanning is out of Spring Boot's control as it's determined by Spring Framework. Here's a minimal example of the behavior that you have described that doesn't use Spring Boot:

package com.example;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;

@Configuration
public class MyConfiguration {

    @Configuration
    @Conditional(Disabled.class)
    static class DisabledConfiguration {

        @Configuration
        @Conditional(Enabled.class)
        static class FirstClientConfiguration {

            @Bean
            public MyClient firstClient() {
                return new FirstClient();
            }

        }

        @Configuration
        @Conditional(Disabled.class)
        static class SecondClientConfiguration {

            @Bean
            public MyClient secondClient() {
                return new SecondClient();
            }

        }

    }

    @Configuration
    @Conditional(Enabled.class)
    static class ThirdClientConfiguration {

        @Bean
        public MyClient thirdClient() {
            return new ThirdClient();
        }

    }

    static class Enabled implements Condition {

        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return true;
        }

    }

    static class Disabled implements Condition {

        @Override
        public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
            return false;
        }

    }

    public static interface MyClient {

    }

    public static class FirstClient implements MyClient {

    }

    public static class SecondClient implements MyClient {

    }

    public static class ThirdClient implements MyClient {

    }

}
package com.example;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import com.example.MyConfiguration.MyClient;
import com.example.MyConfiguration.ThirdClient;

@SpringJUnitConfig(MyConfiguration.class)
public class ContextConfigurationTests {

    @Autowired
    ApplicationContext context;

    @Test
    void contextLoads() {
        assertThat(context.getBeansOfType(MyClient.class).size()).isEqualTo(1);
        assertThat(context.getBean(MyClient.class)).isInstanceOf(ThirdClient.class);
    }

}
package com.example;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import com.example.ComponentScanTests.EnableComponentScan;
import com.example.MyConfiguration.MyClient;
import com.example.MyConfiguration.ThirdClient;

@SpringJUnitConfig(EnableComponentScan.class)
public class ComponentScanTests {

    @Autowired
    ApplicationContext context;

    @Test
    void contextLoads() {
        assertThat(context.getBeansOfType(MyClient.class).size()).isEqualTo(1);
        assertThat(context.getBean(MyClient.class)).isInstanceOf(ThirdClient.class);
    }

    @ComponentScan("com.example")
    static class EnableComponentScan {

    }

}

We'll transfer this issue to the Spring Framework issue tracker so that the Framework team can take a look.

jhoeller commented 9 months ago

As mentioned on #30750, the classpath scan finds the nested classes directly rather than through their containing class. As a consequence, it processes them in the order that it found them in the classpath. Through declaring those nested classes as non-static, classpath scanning does not consider them as independent anymore, so they will actually be processed through their containing class then - with the order for nested classes respected there. You can achieve the same effect by removing the @Configuration stereotype from the nested classes so that classpath scanning does not identify them anymore; this works with static classes as well since they will only be found through their containing class then.