Closed levitin closed 9 months 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?
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.
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.
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
And it actually works fine, if you write your test as following:
However if you are using component scan, the parent conditional is ignored.
In this case you get the following two beans instead of expected single one as in example above.